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