OSDN Git Service

カラーセットを使用するように
[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 extension NSUserInterfaceItemIdentifier {
12     
13     static let item = NSUserInterfaceItemIdentifier("item")
14 }
15
16 final class ScreenshotListViewController: NSViewController {
17     
18     private static let maxImageSize = 800.0
19     private static let leftMergin = 8.0 + 1.0
20     private static let rightMergin = 8.0 + 1.0
21     
22     var screenshots: ScreenshotModel = ScreenshotModel()
23     
24     @IBOutlet private var screenshotsController: NSArrayController!
25     @IBOutlet private weak var collectionView: NSCollectionView!
26     
27     private var selectionObservation: NSKeyValueObservation?
28     
29     @objc dynamic var zoom: Double = UserDefaults.standard[.screenshotPreviewZoomValue] {
30         
31         didSet {
32             
33             collectionView.reloadData()
34             UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
35         }
36     }
37     @objc dynamic var maxZoom: Double = 1.0
38     
39     private var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
40     private var reloadHandler: (() -> Void)?
41     private var collectionSelectionDidChangeHandler: ((Int) -> Void)?
42     private(set) var inLiveScrolling = false
43     
44     private var arrangedInformations: [ScreenshotInformation] {
45         
46         return screenshotsController.arrangedObjects as? [ScreenshotInformation] ?? []
47     }
48     
49     private var selectionInformations: [ScreenshotInformation] {
50         
51         return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
52     }
53     
54     private var dirName: String {
55         
56         guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
57             !name.isEmpty else {
58                 
59                 return "KCD"
60         }
61         
62         return name
63     }
64     
65     private var screenshotSaveDirectoryURL: URL {
66         
67         let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
68         let url = parentURL.appendingPathComponent(dirName)
69         let fm = FileManager.default
70         var isDir: ObjCBool = false
71         
72         do {
73             
74             if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
75                 
76                 try fm.createDirectory(at: url, withIntermediateDirectories: false)
77                 
78             } else if !isDir.boolValue {
79                 
80                 Logger.shared.log("\(url) is regular file, not direcory.")
81                 
82                 return parentURL
83             }
84             
85         } catch {
86             
87             Logger.shared.log("Can not create screenshot save directory.")
88             
89             return parentURL
90         }
91         
92         return url
93     }
94     
95     var indexPathsOfItemsBeingDragged: Set<IndexPath>?
96     
97     // MARK: - Function
98     override func viewDidLoad() {
99         
100         super.viewDidLoad()
101         
102         let nib = NSNib(nibNamed: ScreenshotCollectionViewItem.nibName, bundle: nil)
103         collectionView.register(nib, forItemWithIdentifier: .item)
104         
105         screenshots.sortDescriptors = [NSSortDescriptor(key: #keyPath(ScreenshotInformation.creationDate), ascending: false)]
106         selectionObservation = collectionView.observe(\NSCollectionView.selectionIndexPaths) { [weak self] (_, _) in
107             
108             guard let `self` = self else {
109                 
110                 return
111             }
112             
113             let selections = self.collectionView.selectionIndexPaths
114             let selectionIndexes = selections.reduce(into: IndexSet()) { $0.insert($1.item) }
115             self.screenshots.selectedIndexes = selectionIndexes
116             selectionIndexes.first.map { self.collectionSelectionDidChangeHandler?($0) }
117         }
118         collectionView.postsFrameChangedNotifications = true
119         
120         let nc = NotificationCenter.default
121         let scrollView = collectionView.enclosingScrollView
122         
123         nc.addObserver(forName: NSView.frameDidChangeNotification, object: collectionView, queue: nil, using: viewFrameDidChange)
124         nc.addObserver(forName: NSScrollView.didLiveScrollNotification,
125                        object: collectionView.enclosingScrollView, queue: nil) { _ in
126                         
127             let visibleItems = self.collectionView.indexPathsForVisibleItems()
128             self.collectionVisibleDidChangeHandler?(visibleItems)
129         }
130         nc.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil) { _ in
131             
132             self.inLiveScrolling = true
133         }
134         nc.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { _ in
135             
136             self.inLiveScrolling = false
137         }
138         nc.addObserver(forName: .didRegisterScreenshot,
139                        object: nil,
140                        queue: .main) { notification in
141                         
142                         guard let url = notification.userInfo?[ScreenshotRegister.screenshotURLKey] as? URL else {
143                             
144                             return
145                         }
146                         
147                         let info = ScreenshotInformation(url: url)
148                         
149                         self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
150                         let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
151                         self.collectionView.selectionIndexPaths = set
152                         
153                         self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
154                         if UserDefaults.standard[.showsListWindowAtScreenshot] {
155                             
156                             self.view.window?.makeKeyAndOrderFront(nil)
157                         }
158         }
159         
160         collectionView.setDraggingSourceOperationMask([.move, .copy, .delete], forLocal: false)
161         
162         viewFrameDidChange(nil)
163         
164         DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: self.reloadData)
165     }
166     
167     override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
168         
169         guard let vc = segue.destinationController as? NSViewController else {
170             
171             return
172         }
173         
174         vc.representedObject = screenshots
175     }
176     
177     func viewFrameDidChange(_ notification: Notification?) {
178         
179         maxZoom = calcMaxZoom()
180         if zoom > maxZoom { zoom = maxZoom }
181     }
182     
183     /// 画像の大きさの変化が自然になるようにzoom値から画像サイズを計算
184     private func sizeFrom(zoom: Double) -> CGFloat {
185         
186         if zoom < 0.5 {
187             
188             return CGFloat(type(of: self).maxImageSize * zoom * 0.6)
189         }
190         
191         return CGFloat(type(of: self).maxImageSize * (0.8 * zoom * zoom * zoom  + 0.2))
192     }
193     
194     /// ビューの幅に合わせたzoomの最大値を計算
195     private func calcMaxZoom() -> Double {
196         
197         let effectiveWidth = Double(collectionView.frame.size.width) - type(of: self).leftMergin - type(of: self).rightMergin
198         
199         if effectiveWidth < 240 {
200             
201             return effectiveWidth / type(of: self).maxImageSize / 0.6
202         }
203         if effectiveWidth > 800 {
204             
205             return 1.0
206         }
207         
208         return pow((effectiveWidth / type(of: self).maxImageSize - 0.2) / 0.8, 1.0 / 3.0)
209     }
210     
211     private func reloadData() {
212         
213         Promise<[ScreenshotInformation]>()
214             .complete {
215                 
216                 Result(ScreenshotLoader(self.screenshotSaveDirectoryURL).merge(screenshots: []))
217             }
218             .future
219             .onSuccess { screenshots in
220                 
221                 DispatchQueue.main.async {
222                     
223                     self.screenshots.screenshots = screenshots
224                     
225                     self.collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
226                     
227                     self.reloadHandler?()
228                 }
229         }
230     }
231     
232 }
233
234 // MARK: - IBAction
235 extension ScreenshotListViewController {
236     
237     @IBAction func reloadContent(_ sender: AnyObject?) {
238         
239         reloadData()
240     }
241     
242     @IBAction func reloadData(_ sender: AnyObject?) {
243         
244         reloadData()
245     }
246     
247     private func moveToTrash(_ urls: [URL]) {
248         
249         let list = urls.map { $0.path }
250             .map { "(\"\($0)\" as POSIX file)" }
251             .joined(separator: " , ")
252         let script = "tell application \"Finder\"\n"
253             + "    delete { \(list) }\n"
254             + "end tell"
255         
256         guard let aps = NSAppleScript(source: script) else {
257             
258             return
259         }
260         
261         aps.executeAndReturnError(nil)
262     }
263     
264     @IBAction func delete(_ sender: AnyObject?) {
265         
266         let selectionURLs = selectionInformations.map { $0.url }
267         
268         let selectionIndexes = screenshotsController.selectionIndexes
269         screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
270         reloadHandler?()
271         
272         guard var index = selectionIndexes.first else {
273             
274             return
275         }
276         
277         if arrangedInformations.count <= index {
278             
279             index = arrangedInformations.count - 1
280         }
281         collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
282         
283         moveToTrash(selectionURLs)
284     }
285     
286     @IBAction func revealInFinder(_ sender: AnyObject?) {
287         
288         let urls = selectionInformations.map { $0.url }
289         NSWorkspace.shared.activateFileViewerSelecting(urls)
290     }
291 }
292
293 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
294     
295     func collectionView(_ collectionView: NSCollectionView,
296                         layout collectionViewLayout: NSCollectionViewLayout,
297                         sizeForItemAt indexPath: IndexPath) -> NSSize {
298         
299         let size = sizeFrom(zoom: zoom)
300         
301         return NSSize(width: size, height: size)
302     }
303     
304     // Drag and Drop
305     func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
306         
307         return true
308     }
309     
310     func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
311         
312         return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
313     }
314     
315     func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
316         
317         indexPathsOfItemsBeingDragged = indexPaths
318     }
319     
320     func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
321         
322         defer { indexPathsOfItemsBeingDragged = nil }
323         
324         guard let dragged = indexPathsOfItemsBeingDragged else {
325             
326             return
327         }
328         guard operation.contains(.move) || operation.contains(.delete) else {
329             
330             return
331         }
332         
333         var indexes = IndexSet()
334         dragged.forEach { indexes.insert($0.item) }
335         
336         screenshotsController.remove(atArrangedObjectIndexes: indexes)
337     }
338     
339 }
340
341 @available(OSX 10.12.2, *)
342 private var kTouchBars: [Int: NSTouchBar] = [:]
343 @available(OSX 10.12.2, *)
344 private var kScrubbers: [Int: NSScrubber] = [:]
345 @available(OSX 10.12.2, *)
346 private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
347
348 @available(OSX 10.12.2, *)
349 extension ScreenshotListViewController: NSTouchBarDelegate {
350     
351     static let ServicesItemIdentifier: NSTouchBarItem.Identifier
352         = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
353     
354     @IBOutlet private var screenshotTouchBar: NSTouchBar! {
355         
356         get { return kTouchBars[hashValue] }
357         set { kTouchBars[hashValue] = newValue }
358     }
359     
360     @IBOutlet private var scrubber: NSScrubber! {
361         
362         get { return kScrubbers[hashValue] }
363         set { kScrubbers[hashValue] = newValue }
364     }
365     
366     @IBOutlet private var sharingItem: NSSharingServicePickerTouchBarItem! {
367         
368         get { return kPickers[hashValue] }
369         set { kPickers[hashValue] = newValue }
370     }
371     
372     override func makeTouchBar() -> NSTouchBar? {
373         
374         Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: nil)
375         let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
376             + [type(of: self).ServicesItemIdentifier]
377         screenshotTouchBar.defaultItemIdentifiers = identifiers
378         
379         if collectionVisibleDidChangeHandler == nil {
380             
381             collectionVisibleDidChangeHandler = { [weak self] in
382                 
383                 guard let `self` = self else {
384                     
385                     return
386                 }
387                 guard let index = $0.first else {
388                     
389                     return
390                 }
391                 
392                 let middle = index.item + $0.count / 2
393                 
394                 if middle < self.arrangedInformations.count - 1 {
395                     
396                     self.scrubber.scrollItem(at: middle, to: .none)
397                 }
398             }
399         }
400         
401         if collectionSelectionDidChangeHandler == nil {
402             
403             collectionSelectionDidChangeHandler = { [weak self] in
404                 
405                 self?.scrubber.selectedIndex = $0
406             }
407         }
408         
409         if reloadHandler == nil {
410             
411             reloadHandler = { [weak self] in
412                 
413                 self?.scrubber.reloadData()
414             }
415         }
416         
417         return screenshotTouchBar
418     }
419     
420     func touchBar(_ touchBar: NSTouchBar,
421                   makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
422         
423         guard identifier == type(of: self).ServicesItemIdentifier else {
424             
425             return nil
426         }
427         
428         if sharingItem == nil {
429             
430             sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
431             
432             if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
433                 
434                 sharingItem.delegate = w
435             }
436         }
437         
438         return sharingItem
439     }
440 }
441
442 @available(OSX 10.12.2, *)
443 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
444     
445     func numberOfItems(for scrubber: NSScrubber) -> Int {
446         
447         return arrangedInformations.count
448     }
449     
450     func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
451         
452         guard case 0..<arrangedInformations.count = index else {
453             
454             return NSScrubberImageItemView()
455         }
456         
457         let info = arrangedInformations[index]
458         let itemView = NSScrubberImageItemView()
459         
460         if let image = NSImage(contentsOf: info.url) {
461             
462             itemView.image = image
463         }
464         
465         return itemView
466     }
467     
468     func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
469         
470         let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
471         
472         collectionView.selectionIndexPaths = [p]
473     }
474     
475     func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
476         
477         if inLiveScrolling {
478             
479             return
480         }
481         
482         let center = visibleRange.location + visibleRange.length / 2
483         let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
484         collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])
485     }
486 }