OSDN Git Service

UAをVersion/10.0.3 Safari/602.4.8に変更
[kcd/KCD.git] / KCD / ScreenshotListViewController.swift
1 //
2 //  ScreenshotListViewController.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2016/12/30.
6 //  Copyright © 2016年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10
11 fileprivate struct CacheVersionInfo {
12     let url: URL
13     
14     init(url: URL, version: Int = 0) {
15         self.url = url
16         self.version = version
17     }
18     
19     private(set) var version: Int
20     
21     mutating func incrementVersion() { version = version + 1 }
22 }
23
24 class ScreenshotListViewController: NSViewController {
25     private static let def = 800.0
26     private static let leftMergin = 8.0 + 1.0
27     private static let rightMergin = 8.0 + 1.0
28     
29     var screenshots: ScreenshotModel = ScreenshotModel()
30     
31     @IBOutlet var screenshotsController: NSArrayController!
32     @IBOutlet weak var collectionView: NSCollectionView!
33     @IBOutlet var contextMenu: NSMenu!
34     @IBOutlet weak var shareButton: NSButton!
35     @IBOutlet weak var standardView: NSView!
36     @IBOutlet weak var editorView: NSView!
37     
38     dynamic var zoom: Double = UserDefaults.standard.screenshotPreviewZoomValue {
39         didSet {
40             collectionView.reloadData()
41             UserDefaults.standard.screenshotPreviewZoomValue = zoom
42         }
43     }
44     dynamic var maxZoom: Double = 1.0
45     
46     fileprivate var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
47     fileprivate var reloadHandler: (() -> Void)?
48     fileprivate var collectionSelectionDidChangeHandler: ((Int) -> Void)?
49     fileprivate(set) var inLiveScrolling = false
50     fileprivate var arrangedInformations: [ScreenshotInformation]? {
51         return screenshotsController.arrangedObjects as? [ScreenshotInformation]
52     }
53     fileprivate var selectionInformations: [ScreenshotInformation] {
54         return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
55     }
56     
57     private var deletedPaths: [CacheVersionInfo] = []
58     private var dirName: String {
59         guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
60             !name.isEmpty
61             else { return "KCD" }
62         return name
63     }
64     private var screenshotSaveDirectoryURL: URL {
65         let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
66         let url = parentURL.appendingPathComponent(dirName)
67         let fm = FileManager.default
68         var isDir: ObjCBool = false
69         do {
70             if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
71                 try fm.createDirectory(at: url, withIntermediateDirectories: false)
72             } else if !isDir.boolValue {
73                 print("\(url) is regular file, not direcory.")
74                 return parentURL
75             }
76         } catch {
77             print("Can not create screenshot save directory.")
78             return parentURL
79         }
80         return url
81     }
82     private var cachURL: URL {
83         return screenshotSaveDirectoryURL.appendingPathComponent("Cache.db")
84     }
85     
86     // MARK: - Function
87     override func viewDidLoad() {
88         super.viewDidLoad()
89         
90         screenshots.screenshots = loadCache()
91         
92         let nib = NSNib(nibNamed: "ScreenshotCollectionViewItem", bundle: nil)
93         collectionView.register(NSCollectionView.self, forItemWithIdentifier: "item")
94         collectionView.register(nib, forItemWithIdentifier: "item")
95         
96         screenshots.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
97         collectionView.addObserver(self, forKeyPath: "selectionIndexPaths", context: nil)
98         collectionView.postsFrameChangedNotifications = true
99         
100         let nc = NotificationCenter.default
101         let scrollView = collectionView.enclosingScrollView
102         
103         nc.addObserver(forName: .NSViewFrameDidChange, object: collectionView, queue: nil, using: viewFrameDidChange)
104         nc.addObserver(forName: .NSScrollViewDidLiveScroll,
105                        object: collectionView.enclosingScrollView, queue: nil) { _ in
106             let visibleItems = self.collectionView.indexPathsForVisibleItems()
107             self.collectionVisibleDidChangeHandler?(visibleItems)
108         }
109         nc.addObserver(forName: .NSScrollViewWillStartLiveScroll, object: scrollView, queue: nil) { _ in
110             self.inLiveScrolling = true
111         }
112         nc.addObserver(forName: .NSScrollViewDidEndLiveScroll, object: scrollView, queue: nil) { _ in
113             self.inLiveScrolling = false
114         }
115         
116         viewFrameDidChange(nil)
117         
118         DispatchQueue.main
119             .asyncAfter(deadline: .now() + 0.0001 ) { self.reloadData() }
120     }
121     override func observeValue(forKeyPath keyPath: String?,
122                                of object: Any?,
123                                change: [NSKeyValueChangeKey : Any]?,
124                                context: UnsafeMutableRawPointer?) {
125         if let object = object as? NSCollectionView,
126             object == collectionView {
127             let selections = collectionView.selectionIndexPaths
128             var selectionIndexes = IndexSet()
129             selections.forEach { selectionIndexes.insert($0.item) }
130             screenshots.selectedIndexes = selectionIndexes
131             selectionIndexes.first.map { collectionSelectionDidChangeHandler?($0) }
132             return
133         }
134         
135         super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
136     }
137     override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
138         guard let vc = segue.destinationController as? NSViewController else { return }
139         vc.representedObject = screenshots
140     }
141     
142     func registerImage(_ image: NSImage?) {
143         image?.tiffRepresentation
144             .flatMap { NSBitmapImageRep(data: $0) }
145             .map { registerScreenshot($0, fromOnScreen: .zero) }
146     }
147     func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
148         DispatchQueue(label: "Screenshot queue")
149             .async {
150                 guard let data = image.representation(using: .JPEG, properties: [:])
151                     else { return }
152                 let url = self.screenshotSaveDirectoryURL
153                     .appendingPathComponent(self.dirName)
154                     .appendingPathExtension("jpg")
155                 let pathURL = FileManager.default.uniqueFileURL(url)
156                 do {
157                     try data.write(to: pathURL)
158                 } catch {
159                     print("Can not write image")
160                     return
161                 }
162                 
163                 DispatchQueue.main.async {
164                     let info = ScreenshotInformation(url: pathURL, version: self.cacheVersion(forUrl: pathURL))
165                     
166                     self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
167                     let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
168                     self.collectionView.selectionIndexPaths = set
169                     
170                     self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
171                     if UserDefaults.standard.showsListWindowAtScreenshot {
172                         self.view.window?.makeKeyAndOrderFront(nil)
173                     }
174                     self.saveCache()
175                 }
176         }
177     }
178     func viewFrameDidChange(_ notification: Notification?) {
179         maxZoom = self.maxZoom(width: collectionView.frame.size.width)
180         if zoom > maxZoom { zoom = maxZoom }
181     }
182     
183     fileprivate func realFromZoom(zoom: Double) -> CGFloat {
184         if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
185         return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom  + 0.2))
186     }
187     private func maxZoom(width: CGFloat) -> Double {
188         let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
189         if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
190         if w > 800 { return 1.0 }
191         return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
192     }
193     
194     fileprivate func reloadData() {
195         guard let f = try? FileManager
196             .default
197             .contentsOfDirectory(at: screenshotSaveDirectoryURL, includingPropertiesForKeys: nil) else {
198             print("can not read list of screenshot directory")
199             return
200         }
201         let imageTypes = NSImage.imageTypes()
202         let ws = NSWorkspace.shared()
203         var current = screenshots.screenshots
204         let newFiles: [URL] = f.flatMap {
205             guard let type = try? ws.type(ofFile: $0.path) else { return nil }
206             return imageTypes.contains(type) ? $0 : nil
207         }
208         
209         // なくなっているものを削除
210         current = current.filter { newFiles.contains($0.url) }
211         
212         // 新しいものを追加
213         let new: [ScreenshotInformation] = newFiles.flatMap { url in
214             let index = current.index { url == $0.url }
215             return index == nil ? ScreenshotInformation(url: url) : nil
216         }
217         
218         screenshots.screenshots = current + new
219         
220         collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
221         
222         reloadHandler?()
223         saveCache()
224         
225     }
226     
227     fileprivate func saveCache() {
228         let data = NSKeyedArchiver.archivedData(withRootObject: screenshots.screenshots)
229         do {
230             try data.write(to: cachURL)
231         } catch let e {
232             print("Can not write cache: \(e)")
233         }
234     }
235     private func loadCache() -> [ScreenshotInformation] {
236         guard let data = try? Data(contentsOf: cachURL)
237             else {
238                 print("can not load cach \(cachURL)")
239                 return []
240         }
241         guard let l = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data as NSData),
242             let loaded = l as? [ScreenshotInformation]
243             else {
244                 print("Can not decode \(cachURL)")
245                 return []
246         }
247         
248         return loaded
249     }
250     
251     fileprivate func incrementCacheVersion(forUrl url: URL) {
252         let infos = deletedPaths.filter { $0.url == url }
253         if var info = infos.first {
254             info.incrementVersion()
255         } else {
256             deletedPaths.append(CacheVersionInfo(url: url))
257         }
258     }
259     
260     private func cacheVersion(forUrl url: URL) -> Int {
261         return deletedPaths
262             .filter { $0.url == url }
263             .first?
264             .version ?? 0
265     }
266 }
267
268 // MARK: - IBAction
269 extension ScreenshotListViewController {
270     @IBAction func reloadData(_ sender: AnyObject?) {
271         reloadData()
272     }
273     @IBAction func delete(_ sender: AnyObject?) {
274         let list = selectionInformations
275             .map { $0.url.path }
276             .map { "(\"\($0)\" as POSIX file)" }
277             .joined(separator: " , ")
278         let script = "tell application \"Finder\"\n"
279         + "    delete { \(list) }\n"
280         + "end tell"
281         guard let aps = NSAppleScript(source: script) else { return }
282         aps.executeAndReturnError(nil)
283         
284         let selectionIndexes = screenshotsController.selectionIndexes
285         screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
286         selectionInformations.forEach { incrementCacheVersion(forUrl: $0.url) }
287         saveCache()
288         reloadHandler?()
289         
290         guard var index = selectionIndexes.first,
291             let newInfos = arrangedInformations
292             else { return }
293         if newInfos.count <= index { index = newInfos.count - 1 }
294         collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
295     }
296     @IBAction func revealInFinder(_ sender: AnyObject?) {
297         guard let infos = arrangedInformations else { return }
298         let urls = infos.map { $0.url }
299         NSWorkspace.shared().activateFileViewerSelecting(urls)
300     }
301 }
302
303 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
304     func collectionView(_ collectionView: NSCollectionView,
305                         layout collectionViewLayout: NSCollectionViewLayout,
306                         sizeForItemAt indexPath: IndexPath) -> NSSize {
307         let f = realFromZoom(zoom: zoom)
308         return NSSize(width: f, height: f)
309     }
310 }
311
312 @available(OSX 10.12.2, *)
313 fileprivate var kTouchBars: [Int: NSTouchBar] = [:]
314 @available(OSX 10.12.2, *)
315 fileprivate var kScrubbers: [Int: NSScrubber] = [:]
316 @available(OSX 10.12.2, *)
317 fileprivate var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
318
319 @available(OSX 10.12.2, *)
320 extension ScreenshotListViewController: NSTouchBarDelegate {
321     static let ServicesItemIdentifier: NSTouchBarItemIdentifier
322         = NSTouchBarItemIdentifier(rawValue: "com.masakih.sharingTouchBarItem")
323     
324     @IBOutlet var screenshotTouchBar: NSTouchBar! {
325         get { return kTouchBars[hashValue] }
326         set { kTouchBars[hashValue] = newValue }
327     }
328     @IBOutlet var scrubber: NSScrubber! {
329         get { return kScrubbers[hashValue] }
330         set { kScrubbers[hashValue] = newValue }
331     }
332     @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
333         get { return kPickers[hashValue] }
334         set { kPickers[hashValue] = newValue }
335     }
336     
337     override func makeTouchBar() -> NSTouchBar? {
338         var array: NSArray = []
339         Bundle.main.loadNibNamed("ScreenshotTouchBar", owner: self, topLevelObjects: &array)
340         let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
341             + [ScreenshotListViewController.ServicesItemIdentifier]
342         screenshotTouchBar.defaultItemIdentifiers = identifiers
343         
344         if collectionVisibleDidChangeHandler == nil {
345             collectionVisibleDidChangeHandler = { [weak self] in
346                 guard let `self` = self,
347                     let objects = self.arrangedInformations,
348                     let index = $0.first
349                     else { return }
350                 let middle = index.item + $0.count / 2
351                 if middle < objects.count - 1 {
352                     self.scrubber.scrollItem(at: middle, to: .none)
353                 }
354             }
355         }
356         if collectionSelectionDidChangeHandler == nil {
357             collectionSelectionDidChangeHandler = { [weak self] in
358                 guard let `self` = self else { return }
359                 self.scrubber.selectedIndex = $0
360             }
361         }
362         if reloadHandler == nil {
363             reloadHandler = { [weak self] _ in
364                 guard let `self` = self else { return }
365                 self.scrubber.reloadData()
366             }
367         }
368         
369         return screenshotTouchBar
370     }
371     
372     func touchBar(_ touchBar: NSTouchBar,
373                   makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
374         guard identifier == ScreenshotListViewController.ServicesItemIdentifier
375             else { return nil }
376         if sharingItem == nil {
377             sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
378             if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
379                 sharingItem.delegate = w
380             }
381         }
382         return sharingItem
383     }
384 }
385
386 @available(OSX 10.12.2, *)
387 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
388     func numberOfItems(for scrubber: NSScrubber) -> Int {
389         return arrangedInformations?.count ?? 0
390     }
391     func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
392         guard let objects = arrangedInformations else { return NSScrubberImageItemView() }
393         guard objects.count > index else { return NSScrubberImageItemView() }
394         let info = objects[index]
395         let itemView = NSScrubberImageItemView()
396         if let image = NSImage(contentsOf: info.url) {
397             itemView.image = image
398         }
399         
400         return itemView
401     }
402     
403     func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
404         let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
405         collectionView.selectionIndexPaths = [p]
406     }
407     func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
408         if inLiveScrolling { return }
409         let center = visibleRange.location + visibleRange.length / 2
410         let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
411         collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])
412     }
413 }