OSDN Git Service

127f47880db4f817e28234a4db086cfa59d26c3e
[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         Future<[ScreenshotInformation]> {
214             
215             ScreenshotLoader(self.screenshotSaveDirectoryURL).merge(screenshots: [])
216             
217             }
218             .onSuccess { screenshots in
219                 
220                 DispatchQueue.main.async {
221                     
222                     self.screenshots.screenshots = screenshots
223                     
224                     self.collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
225                     
226                     self.reloadHandler?()
227                 }
228         }
229     }
230     
231 }
232
233 // MARK: - IBAction
234 extension ScreenshotListViewController {
235     
236     @IBAction func reloadContent(_ sender: AnyObject?) {
237         
238         reloadData()
239     }
240     
241     @IBAction func reloadData(_ sender: AnyObject?) {
242         
243         reloadData()
244     }
245     
246     private func moveToTrash(_ urls: [URL]) {
247         
248         let list = urls.map { $0.path }
249             .map { "(\"\($0)\" as POSIX file)" }
250             .joined(separator: " , ")
251         let script = "tell application \"Finder\"\n"
252             + "    delete { \(list) }\n"
253             + "end tell"
254         
255         guard let aps = NSAppleScript(source: script) else {
256             
257             return
258         }
259         
260         aps.executeAndReturnError(nil)
261     }
262     
263     @IBAction func delete(_ sender: AnyObject?) {
264         
265         let selectionURLs = selectionInformations.map { $0.url }
266         
267         let selectionIndexes = screenshotsController.selectionIndexes
268         screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
269         reloadHandler?()
270         
271         guard var index = selectionIndexes.first else {
272             
273             return
274         }
275         
276         if arrangedInformations.count <= index {
277             
278             index = arrangedInformations.count - 1
279         }
280         collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
281         
282         moveToTrash(selectionURLs)
283     }
284     
285     @IBAction func revealInFinder(_ sender: AnyObject?) {
286         
287         let urls = selectionInformations.map { $0.url }
288         NSWorkspace.shared.activateFileViewerSelecting(urls)
289     }
290 }
291
292 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
293     
294     func collectionView(_ collectionView: NSCollectionView,
295                         layout collectionViewLayout: NSCollectionViewLayout,
296                         sizeForItemAt indexPath: IndexPath) -> NSSize {
297         
298         let size = sizeFrom(zoom: zoom)
299         
300         return NSSize(width: size, height: size)
301     }
302     
303     // Drag and Drop
304     func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
305         
306         return true
307     }
308     
309     func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
310         
311         return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
312     }
313     
314     func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
315         
316         indexPathsOfItemsBeingDragged = indexPaths
317     }
318     
319     func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
320         
321         defer { indexPathsOfItemsBeingDragged = nil }
322         
323         guard let dragged = indexPathsOfItemsBeingDragged else {
324             
325             return
326         }
327         guard operation.contains(.move) || operation.contains(.delete) else {
328             
329             return
330         }
331         
332         var indexes = IndexSet()
333         dragged.forEach { indexes.insert($0.item) }
334         
335         screenshotsController.remove(atArrangedObjectIndexes: indexes)
336     }
337     
338 }
339
340 @available(OSX 10.12.2, *)
341 private var kTouchBars: [Int: NSTouchBar] = [:]
342 @available(OSX 10.12.2, *)
343 private var kScrubbers: [Int: NSScrubber] = [:]
344 @available(OSX 10.12.2, *)
345 private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
346
347 @available(OSX 10.12.2, *)
348 extension ScreenshotListViewController: NSTouchBarDelegate {
349     
350     static let ServicesItemIdentifier: NSTouchBarItem.Identifier
351         = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
352     
353     @IBOutlet private var screenshotTouchBar: NSTouchBar! {
354         
355         get { return kTouchBars[hashValue] }
356         set { kTouchBars[hashValue] = newValue }
357     }
358     
359     @IBOutlet private var scrubber: NSScrubber! {
360         
361         get { return kScrubbers[hashValue] }
362         set { kScrubbers[hashValue] = newValue }
363     }
364     
365     @IBOutlet private var sharingItem: NSSharingServicePickerTouchBarItem! {
366         
367         get { return kPickers[hashValue] }
368         set { kPickers[hashValue] = newValue }
369     }
370     
371     override func makeTouchBar() -> NSTouchBar? {
372         
373         Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: nil)
374         let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
375             + [type(of: self).ServicesItemIdentifier]
376         screenshotTouchBar.defaultItemIdentifiers = identifiers
377         
378         if collectionVisibleDidChangeHandler == nil {
379             
380             collectionVisibleDidChangeHandler = { [weak self] in
381                 
382                 guard let `self` = self else {
383                     
384                     return
385                 }
386                 guard let index = $0.first else {
387                     
388                     return
389                 }
390                 
391                 let middle = index.item + $0.count / 2
392                 
393                 if middle < self.arrangedInformations.count - 1 {
394                     
395                     self.scrubber.scrollItem(at: middle, to: .none)
396                 }
397             }
398         }
399         
400         if collectionSelectionDidChangeHandler == nil {
401             
402             collectionSelectionDidChangeHandler = { [weak self] in
403                 
404                 self?.scrubber.selectedIndex = $0
405             }
406         }
407         
408         if reloadHandler == nil {
409             
410             reloadHandler = { [weak self] in
411                 
412                 self?.scrubber.reloadData()
413             }
414         }
415         
416         return screenshotTouchBar
417     }
418     
419     func touchBar(_ touchBar: NSTouchBar,
420                   makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
421         
422         guard identifier == type(of: self).ServicesItemIdentifier else {
423             
424             return nil
425         }
426         
427         if sharingItem == nil {
428             
429             sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
430             
431             if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
432                 
433                 sharingItem.delegate = w
434             }
435         }
436         
437         return sharingItem
438     }
439 }
440
441 @available(OSX 10.12.2, *)
442 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
443     
444     func numberOfItems(for scrubber: NSScrubber) -> Int {
445         
446         return arrangedInformations.count
447     }
448     
449     func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
450         
451         guard case 0..<arrangedInformations.count = index else {
452             
453             return NSScrubberImageItemView()
454         }
455         
456         let info = arrangedInformations[index]
457         let itemView = NSScrubberImageItemView()
458         
459         if let image = NSImage(contentsOf: info.url) {
460             
461             itemView.image = image
462         }
463         
464         return itemView
465     }
466     
467     func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
468         
469         let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
470         
471         collectionView.selectionIndexPaths = [p]
472     }
473     
474     func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
475         
476         if inLiveScrolling {
477             
478             return
479         }
480         
481         let center = visibleRange.location + visibleRange.length / 2
482         let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
483         collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])
484     }
485 }