macOSのスクリーンショットの保存場所を変更するメニュバーエクストラ
修订版 | bb8adaf3739e5f08a6b5162c883ae57a968a1b04 (tree) |
---|---|
时间 | 2018-04-16 23:20:55 |
作者 | masakih <masakih@user...> |
Commiter | masakih |
コーディング規約を変更
@@ -14,9 +14,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { | ||
14 | 14 | let statusBar = StatusBar() |
15 | 15 | |
16 | 16 | class var appName: String { |
17 | + | |
17 | 18 | guard let dict = Bundle.main.localizedInfoDictionary, |
18 | - let name = dict["CFBundleDisplayName"] as? String | |
19 | - else { return "GO into" } | |
19 | + let name = dict["CFBundleDisplayName"] as? String else { | |
20 | + | |
21 | + return "GO into" | |
22 | + } | |
20 | 23 | return name |
21 | 24 | } |
22 | 25 | } |
@@ -9,10 +9,12 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | class ChooseFolderItem: StatusItem { |
12 | + | |
12 | 13 | let menuItem = NSMenuItem() |
13 | 14 | let urlSelector: (URL) -> Void |
14 | 15 | |
15 | 16 | init(_ handler: @escaping ((URL) -> Void)) { |
17 | + | |
16 | 18 | urlSelector = handler |
17 | 19 | menuItem.title = NSLocalizedString("Choose Folder", comment: "Choose Folder MenuItem") |
18 | 20 | menuItem.action = #selector(selectFolder(_:)) |
@@ -20,6 +22,7 @@ class ChooseFolderItem: StatusItem { | ||
20 | 22 | } |
21 | 23 | |
22 | 24 | @IBAction func selectFolder(_ sender: Any?) { |
25 | + | |
23 | 26 | let panel = NSOpenPanel() |
24 | 27 | panel.canChooseDirectories = true |
25 | 28 | panel.allowsMultipleSelection = false |
@@ -31,7 +34,10 @@ class ChooseFolderItem: StatusItem { | ||
31 | 34 | |
32 | 35 | NSApplication.shared.activate(ignoringOtherApps: true) |
33 | 36 | guard panel.runModal() == NSApplication.ModalResponse(NSFileHandlingPanelOKButton), |
34 | - let url = panel.directoryURL else { return } | |
37 | + let url = panel.directoryURL else { | |
38 | + | |
39 | + return | |
40 | + } | |
35 | 41 | urlSelector(url) |
36 | 42 | } |
37 | 43 | } |
@@ -9,16 +9,21 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | final class FolderItem: StatusItem { |
12 | + | |
12 | 13 | let url: URL |
13 | 14 | let menuItem = NSMenuItem() |
14 | 15 | |
15 | 16 | init(_ url: URL) { |
17 | + | |
16 | 18 | self.url = url |
17 | 19 | |
18 | 20 | if let either = try? url.resourceValues(forKeys: [.localizedNameKey]), |
19 | 21 | let name = either.localizedName { |
22 | + | |
20 | 23 | menuItem.title = name |
24 | + | |
21 | 25 | } else { |
26 | + | |
22 | 27 | menuItem.title = FileManager.default.displayName(atPath: url.path) |
23 | 28 | } |
24 | 29 |
@@ -27,44 +32,59 @@ final class FolderItem: StatusItem { | ||
27 | 32 | menuItem.action = #selector(changeFolder(_:)) |
28 | 33 | menuItem.target = self |
29 | 34 | } |
35 | + | |
30 | 36 | deinit { |
37 | + | |
31 | 38 | remove() |
32 | 39 | } |
33 | 40 | |
34 | 41 | func set() { |
42 | + | |
35 | 43 | let newUrl = url |
44 | + | |
36 | 45 | DispatchQueue(label: "Launch defaults").async { |
46 | + | |
37 | 47 | Screenshot.shared.location = newUrl |
38 | 48 | if #available(macOS 10.12, *) { |
49 | + | |
39 | 50 | return |
51 | + | |
40 | 52 | } else { |
53 | + | |
41 | 54 | Screenshot.shared.apply() |
42 | 55 | } |
43 | 56 | } |
44 | 57 | } |
45 | 58 | |
46 | 59 | func update(_ url: URL) { |
60 | + | |
47 | 61 | menuItem.state = (self.url == url ? .on : .off) |
48 | 62 | } |
49 | 63 | |
50 | 64 | @IBAction func changeFolder(_ sender: Any?) { |
65 | + | |
51 | 66 | set() |
52 | 67 | } |
53 | 68 | } |
54 | 69 | |
55 | 70 | func fitSize(_ image: NSImage) -> NSImage { |
71 | + | |
56 | 72 | let fitSize: CGFloat = 19 |
57 | 73 | let size = image.size |
58 | 74 | guard size.width > fitSize else { return image } |
75 | + | |
59 | 76 | let ratio = fitSize / size.width |
60 | 77 | let newSize = NSSize(width: size.width * ratio, height: size.height * ratio) |
61 | 78 | image.resizingMode = .stretch |
62 | 79 | image.size = newSize |
80 | + | |
63 | 81 | return image |
64 | 82 | } |
65 | 83 | |
66 | 84 | extension FolderItem: Equatable { |
85 | + | |
67 | 86 | static func ==(lhs: FolderItem, rhs: FolderItem) -> Bool { |
87 | + | |
68 | 88 | return lhs.url == rhs.url |
69 | 89 | } |
70 | 90 | } |
@@ -9,17 +9,23 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | private func loadImageTypes() -> [String] { |
12 | + | |
12 | 13 | guard let url = Bundle.main.url(forResource: "ImageType", withExtension: "plist"), |
13 | - let array = NSArray(contentsOf: url) | |
14 | - else { return [] } | |
14 | + let array = NSArray(contentsOf: url) else { | |
15 | + | |
16 | + return [] | |
17 | + } | |
18 | + | |
15 | 19 | return array as? [String] ?? [] |
16 | 20 | } |
17 | 21 | |
18 | 22 | class ImageTypeItem: StatusItem { |
23 | + | |
19 | 24 | let menuItem = NSMenuItem() |
20 | 25 | private let supportTypes = loadImageTypes() |
21 | 26 | |
22 | - init() { | |
27 | + init() { | |
28 | + | |
23 | 29 | menuItem.title = NSLocalizedString("Image Type", comment: "Image Type MenuItem") |
24 | 30 | |
25 | 31 | let ws = NSWorkspace.shared |
@@ -28,38 +34,51 @@ class ImageTypeItem: StatusItem { | ||
28 | 34 | supportTypes |
29 | 35 | .filter { ws.localizedDescription(forType: $0) != nil } |
30 | 36 | .map { |
37 | + | |
31 | 38 | let item = NSMenuItem() |
32 | 39 | item.title = ws.localizedDescription(forType: $0) ?? "Never Use Default Value" |
33 | 40 | item.action = #selector(selectType(_:)) |
34 | 41 | item.target = self |
35 | 42 | item.representedObject = ws.preferredFilenameExtension(forType: $0) |
43 | + | |
36 | 44 | return item |
37 | 45 | } |
38 | 46 | .forEach { menuItem.submenu?.addItem($0) } |
39 | 47 | } |
40 | 48 | |
41 | 49 | func update() { |
50 | + | |
42 | 51 | let current = Screenshot.shared.type |
43 | 52 | menuItem.submenu?.items.forEach { |
53 | + | |
44 | 54 | if let type = $0.representedObject as? String, |
45 | 55 | type == current { |
56 | + | |
46 | 57 | $0.state = .on |
58 | + | |
47 | 59 | } else { |
60 | + | |
48 | 61 | $0.state = .off |
49 | 62 | } |
50 | 63 | } |
51 | 64 | } |
52 | 65 | |
53 | 66 | fileprivate func set(_ typeName: String) { |
67 | + | |
54 | 68 | DispatchQueue(label: "Launch defaults").async { |
69 | + | |
55 | 70 | Screenshot.shared.type = typeName |
56 | 71 | } |
57 | 72 | } |
58 | 73 | |
59 | 74 | @IBAction func selectType(_ sender: Any?) { |
75 | + | |
60 | 76 | guard let item = sender as? NSMenuItem, |
61 | - let typeName = item.representedObject as? String | |
62 | - else { return } | |
77 | + let typeName = item.representedObject as? String else { | |
78 | + | |
79 | + return | |
80 | + } | |
81 | + | |
63 | 82 | set(typeName) |
64 | 83 | } |
65 | 84 | } |
@@ -14,33 +14,43 @@ struct LimitedArray<Element: Equatable>: Collection { | ||
14 | 14 | let size: Int |
15 | 15 | |
16 | 16 | init(_ size: Int) { |
17 | + | |
17 | 18 | self.size = size |
18 | 19 | } |
20 | + | |
19 | 21 | mutating func append(_ newObject: Element) { |
22 | + | |
20 | 23 | if let index = array.index(of: newObject) { |
24 | + | |
21 | 25 | array.remove(at: index) |
22 | 26 | } |
23 | 27 | array.insert(newObject, at: 0) |
24 | 28 | if array.count > size { |
29 | + | |
25 | 30 | array.remove(at: size) |
26 | 31 | } |
27 | 32 | } |
28 | 33 | |
29 | 34 | // Collection |
30 | 35 | var startIndex: Int { |
36 | + | |
31 | 37 | return array.startIndex |
32 | 38 | } |
33 | 39 | var endIndex: Int { |
40 | + | |
34 | 41 | return array.endIndex |
35 | 42 | } |
36 | 43 | func index(after i: Int) -> Int { |
44 | + | |
37 | 45 | return array.index(after: i) |
38 | 46 | } |
39 | 47 | subscript(position: Int) -> Element { |
48 | + | |
40 | 49 | return array[position] |
41 | 50 | } |
42 | 51 | } |
43 | 52 | extension LimitedArray: CustomDebugStringConvertible { |
53 | + | |
44 | 54 | var description: String { return array.description } |
45 | 55 | var debugDescription: String { return array.debugDescription } |
46 | 56 | } |
@@ -9,9 +9,11 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | class QuitItem: StatusItem { |
12 | + | |
12 | 13 | let menuItem = NSMenuItem() |
13 | 14 | |
14 | 15 | init() { |
16 | + | |
15 | 17 | let format = NSLocalizedString("Quit %@", comment: "Quit Menu Item") |
16 | 18 | menuItem.title = String(format: format, AppDelegate.appName) |
17 | 19 | menuItem.action = #selector(quit(_:)) |
@@ -19,6 +21,7 @@ class QuitItem: StatusItem { | ||
19 | 21 | } |
20 | 22 | |
21 | 23 | @IBAction func quit(_ sender: Any?) { |
24 | + | |
22 | 25 | NSApplication.shared.terminate(nil) |
23 | 26 | } |
24 | 27 | } |
@@ -11,7 +11,9 @@ import Foundation | ||
11 | 11 | class Screenshot { |
12 | 12 | |
13 | 13 | private enum Attrubute: String { |
14 | + | |
14 | 15 | case location = "location" |
16 | + | |
15 | 17 | case type = "type" |
16 | 18 | } |
17 | 19 |
@@ -20,16 +22,19 @@ class Screenshot { | ||
20 | 22 | private init() {} |
21 | 23 | |
22 | 24 | var location: URL { |
25 | + | |
23 | 26 | get { return screencaptureAttribute(.location).map { URL(fileURLWithPath: $0) } ?? desktopURL() } |
24 | 27 | set { setScreencaptureAttribute(newValue.path, for: .location) } |
25 | 28 | } |
26 | 29 | var type: String { |
30 | + | |
27 | 31 | get { return screencaptureAttribute(.type) ?? "jpeg" } |
28 | 32 | set { setScreencaptureAttribute(newValue, for: .type) } |
29 | 33 | } |
30 | 34 | |
31 | 35 | @available(macOS, deprecated: 10.12) |
32 | 36 | func apply() { |
37 | + | |
33 | 38 | let process = Process() |
34 | 39 | process.launchPath = "/usr/bin/killall" |
35 | 40 | process.arguments = ["SystemUIServer"] |
@@ -37,6 +42,7 @@ class Screenshot { | ||
37 | 42 | } |
38 | 43 | |
39 | 44 | private func screencaptureAttribute(_ attr: Attrubute) -> String? { |
45 | + | |
40 | 46 | let process = Process() |
41 | 47 | process.launchPath = "/usr/bin/defaults" |
42 | 48 | process.arguments = ["read", "com.apple.screencapture", attr.rawValue] |
@@ -48,20 +54,26 @@ class Screenshot { | ||
48 | 54 | let data = pipe.fileHandleForReading.readDataToEndOfFile() |
49 | 55 | guard let output = String(data: data, encoding: .utf8), |
50 | 56 | let type = output.components(separatedBy: "\n").first, |
51 | - process.terminationStatus == 0 | |
52 | - else { return nil } | |
57 | + process.terminationStatus == 0 else { | |
58 | + | |
59 | + return nil | |
60 | + } | |
61 | + | |
53 | 62 | return type |
54 | 63 | } |
64 | + | |
55 | 65 | private func setScreencaptureAttribute(_ value: String, for attr: Attrubute) { |
66 | + | |
56 | 67 | let process = Process() |
57 | 68 | process.launchPath = "/usr/bin/defaults" |
58 | 69 | process.arguments = ["write", "com.apple.screencapture", attr.rawValue, value] |
59 | 70 | process.launch() |
60 | 71 | process.waitUntilExit() |
61 | 72 | |
62 | - guard process.terminationStatus == 0 | |
63 | - else { | |
73 | + guard process.terminationStatus == 0 else { | |
74 | + | |
64 | 75 | print("Can not set location") |
76 | + | |
65 | 77 | return |
66 | 78 | } |
67 | 79 | } |
@@ -9,15 +9,20 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | final class StatusBar: NSObject { |
12 | + | |
12 | 13 | let myStatusBar = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) |
13 | 14 | let menu = NSMenu() |
15 | + | |
14 | 16 | private(set) var items: [StatusItem] = [] |
15 | 17 | private(set) var recentItems = LimitedArray<FolderItem>(5) { |
18 | + | |
16 | 19 | didSet { UserDefaults.standard.recentURLs = recentItems.map { $0.url } } |
17 | 20 | } |
18 | 21 | |
19 | 22 | override init() { |
23 | + | |
20 | 24 | super.init() |
25 | + | |
21 | 26 | menu.delegate = self |
22 | 27 | |
23 | 28 | myStatusBar.menu = menu |
@@ -27,6 +32,7 @@ final class StatusBar: NSObject { | ||
27 | 32 | } |
28 | 33 | |
29 | 34 | private func build() { |
35 | + | |
30 | 36 | items = [ |
31 | 37 | FolderItem(desktopURL()), |
32 | 38 | FolderItem(picturesURL()), |
@@ -53,26 +59,35 @@ final class StatusBar: NSObject { | ||
53 | 59 | let newItem = FolderItem(url) |
54 | 60 | recentItems.append(newItem) |
55 | 61 | newItem.enter(menu) |
62 | + | |
56 | 63 | return newItem |
57 | 64 | } |
58 | 65 | |
59 | 66 | private func appendFolder(_ url: URL) { |
67 | + | |
60 | 68 | _ = newFolderItem(url) |
61 | 69 | } |
62 | 70 | |
63 | 71 | private func appendAndChooseFolder(_ url: URL) { |
72 | + | |
64 | 73 | newFolderItem(url)?.set() |
65 | 74 | } |
66 | 75 | } |
67 | 76 | |
68 | 77 | extension StatusBar: NSMenuDelegate { |
78 | + | |
69 | 79 | func menuWillOpen(_ menu: NSMenu) { |
80 | + | |
70 | 81 | let url = Screenshot.shared.location |
71 | 82 | recentItems.forEach { $0.update(url) } |
72 | 83 | items.forEach { item in |
84 | + | |
73 | 85 | switch item { |
86 | + | |
74 | 87 | case let f as FolderItem: f.update(url) |
88 | + | |
75 | 89 | case let i as ImageTypeItem: i.update() |
90 | + | |
76 | 91 | default: () |
77 | 92 | } |
78 | 93 | } |
@@ -81,15 +96,17 @@ extension StatusBar: NSMenuDelegate { | ||
81 | 96 | |
82 | 97 | |
83 | 98 | fileprivate func picturesURL() -> URL { |
99 | + | |
84 | 100 | return FileManager |
85 | 101 | .default |
86 | 102 | .urls(for: .picturesDirectory, |
87 | - in: .userDomainMask).last ?? URL(fileURLWithPath: NSHomeDirectory()) | |
103 | + in: .userDomainMask).last ?? FileManager.default.homeDirectoryForCurrentUser | |
88 | 104 | } |
89 | 105 | |
90 | 106 | func desktopURL() -> URL { |
107 | + | |
91 | 108 | return FileManager |
92 | 109 | .default |
93 | 110 | .urls(for: .desktopDirectory, |
94 | - in: .userDomainMask).last ?? URL(fileURLWithPath: NSHomeDirectory()) | |
111 | + in: .userDomainMask).last ?? FileManager.default.homeDirectoryForCurrentUser | |
95 | 112 | } |
@@ -9,6 +9,7 @@ | ||
9 | 9 | import Cocoa |
10 | 10 | |
11 | 11 | protocol StatusItem { |
12 | + | |
12 | 13 | var menuItem: NSMenuItem { get } |
13 | 14 | |
14 | 15 | func enter(_ menu: NSMenu) |
@@ -16,18 +17,25 @@ protocol StatusItem { | ||
16 | 17 | } |
17 | 18 | |
18 | 19 | extension StatusItem { |
20 | + | |
19 | 21 | func enter(_ menu: NSMenu) { |
22 | + | |
20 | 23 | if let currentMenu = menuItem.menu, |
21 | 24 | currentMenu == menu { |
25 | + | |
22 | 26 | return |
23 | 27 | } |
28 | + | |
24 | 29 | menu.insertItem(menuItem, at: 0) |
25 | 30 | } |
31 | + | |
26 | 32 | func remove() { |
33 | + | |
27 | 34 | menuItem.menu?.removeItem(menuItem) |
28 | 35 | } |
29 | 36 | } |
30 | 37 | |
31 | 38 | struct SeparatorItem: StatusItem { |
39 | + | |
32 | 40 | let menuItem = NSMenuItem.separator() |
33 | 41 | } |
@@ -9,22 +9,32 @@ | ||
9 | 9 | import Foundation |
10 | 10 | |
11 | 11 | extension UserDefaults { |
12 | + | |
12 | 13 | func set(archived: Any?, forKey: String) { |
14 | + | |
13 | 15 | if let object = archived { |
16 | + | |
14 | 17 | let data = NSKeyedArchiver.archivedData(withRootObject: object) |
15 | 18 | set(data, forKey: forKey) |
19 | + | |
16 | 20 | } else { |
21 | + | |
17 | 22 | set(nil, forKey: forKey) |
18 | 23 | } |
19 | 24 | } |
25 | + | |
20 | 26 | func unarchiveObject(forKey: String) -> Any? { |
27 | + | |
21 | 28 | if let data = object(forKey: forKey) as? Data { |
29 | + | |
22 | 30 | return NSKeyedUnarchiver.unarchiveObject(with: data) |
23 | 31 | } |
32 | + | |
24 | 33 | return nil |
25 | 34 | } |
26 | 35 | |
27 | 36 | var recentURLs: [URL]? { |
37 | + | |
28 | 38 | get { return unarchiveObject(forKey: "recentURLs") as? [URL] } |
29 | 39 | set { set(archived: newValue, forKey: "recentURLs") } |
30 | 40 | } |
@@ -13,6 +13,7 @@ import XCTest | ||
13 | 13 | class LimitedArrayTest: XCTestCase { |
14 | 14 | |
15 | 15 | func testExample() { |
16 | + | |
16 | 17 | var limited = LimitedArray<Int>(3) |
17 | 18 | XCTAssertEqual(limited.array, []) |
18 | 19 |
@@ -11,19 +11,23 @@ import XCTest | ||
11 | 11 | @testable import GoInto |
12 | 12 | |
13 | 13 | class UserDefaultsTest: XCTestCase { |
14 | + | |
14 | 15 | var originalURLs: [URL]? = [] |
15 | 16 | |
16 | 17 | override func setUp() { |
18 | + | |
17 | 19 | super.setUp() |
18 | 20 | originalURLs = UserDefaults.standard.recentURLs |
19 | 21 | } |
20 | 22 | |
21 | 23 | override func tearDown() { |
24 | + | |
22 | 25 | UserDefaults.standard.recentURLs = originalURLs |
23 | 26 | super.tearDown() |
24 | 27 | } |
25 | 28 | |
26 | 29 | func testRecentFolders() { |
30 | + | |
27 | 31 | let urls = [URL(fileURLWithPath: "/System/"), |
28 | 32 | URL(fileURLWithPath: "/Users/"), |
29 | 33 | URL(fileURLWithPath: "/var/")] |