OSDN Git Service

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