OSDN Git Service

swiftlint 'colon' の警告を修正
[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)? = nil
47     fileprivate var reloadHandler: (() -> Void)? = nil
48     fileprivate var collectionSelectionDidChangeHandler: ((Int) -> Void)? = nil
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         }
77         catch {
78             print("Can not create screenshot save directory.")
79             return parentURL
80         }
81         return url
82     }
83     private var cachURL: URL {
84         return screenshotSaveDirectoryURL.appendingPathComponent("Cache.db")
85     }
86     
87     // MARK: - Function
88     override func viewDidLoad() {
89         super.viewDidLoad()
90         
91         screenshots.screenshots = loadCache()
92         
93         let nib = NSNib(nibNamed: "ScreenshotCollectionViewItem", bundle: nil)
94         collectionView.register(NSCollectionView.self, forItemWithIdentifier: "item")
95         collectionView.register(nib, forItemWithIdentifier: "item")
96         
97         screenshots.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
98         collectionView.addObserver(self, forKeyPath: "selectionIndexPaths", context: nil)
99         collectionView.postsFrameChangedNotifications = true
100         
101         let nc = NotificationCenter.default
102         let scrollView = collectionView.enclosingScrollView
103         
104         nc.addObserver(forName: .NSViewFrameDidChange, object: collectionView, queue: nil, using: viewFrameDidChange)
105         nc.addObserver(forName: .NSScrollViewDidLiveScroll, 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?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
122         if let object = object as? NSCollectionView,
123             object == collectionView {
124             let selections = collectionView.selectionIndexPaths
125             var selectionIndexes = IndexSet()
126             selections.forEach { selectionIndexes.insert($0.item) }
127             screenshots.selectedIndexes = selectionIndexes
128             selectionIndexes.first.map { collectionSelectionDidChangeHandler?($0) }
129             return
130         }
131         
132         super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
133     }
134     override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
135         guard let vc = segue.destinationController as? NSViewController else { return }
136         vc.representedObject = screenshots
137     }
138     
139     func registerImage(_ image: NSImage?) {
140         image?.tiffRepresentation
141             .flatMap { NSBitmapImageRep(data: $0) }
142             .map { registerScreenshot($0, fromOnScreen: .zero) }
143     }
144     func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
145         DispatchQueue(label: "Screenshot queue")
146             .async {
147                 guard let data = image.representation(using: .JPEG, properties: [:])
148                     else { return }
149                 let url = self.screenshotSaveDirectoryURL
150                     .appendingPathComponent(self.dirName)
151                     .appendingPathExtension("jpg")
152                 let pathURL = FileManager.default.uniqueFileURL(url)
153                 do {
154                     try data.write(to: pathURL)
155                 }
156                 catch {
157                     print("Can not write image")
158                     return
159                 }
160                 
161                 DispatchQueue.main.async {
162                     let info = ScreenshotInformation(url: pathURL, version: self.cacheVersion(forUrl: pathURL))
163                     
164                     self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
165                     let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
166                     self.collectionView.selectionIndexPaths = set
167                     
168                     self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
169                     if UserDefaults.standard.showsListWindowAtScreenshot {
170                         self.view.window?.makeKeyAndOrderFront(nil)
171                     }
172                     self.saveCache()
173                 }
174         }
175     }
176     func viewFrameDidChange(_ notification: Notification?) {
177         maxZoom = self.maxZoom(width: collectionView.frame.size.width)
178         if zoom > maxZoom { zoom = maxZoom }
179     }
180     
181     fileprivate func realFromZoom(zoom: Double) -> CGFloat {
182         if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
183         return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom  + 0.2))
184     }
185     private func maxZoom(width: CGFloat) -> Double {
186         let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
187         if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
188         if w > 800 { return 1.0 }
189         return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
190     }
191     
192     private func reloadData() {
193         guard let f = try? FileManager.default.contentsOfDirectory(at: screenshotSaveDirectoryURL, includingPropertiesForKeys: nil) else {
194             print("can not read list of screenshot directory")
195             return
196         }
197         let imageTypes = NSImage.imageTypes()
198         let ws = NSWorkspace.shared()
199         var current = screenshots.screenshots
200         let newFiles: [URL] = f.flatMap {
201             guard let type = try? ws.type(ofFile: $0.path) else { return nil }
202             if imageTypes.contains(type) {
203                 return $0
204             }
205             return nil
206         }
207         
208         // なくなっているものを削除
209         current = current.filter { newFiles.contains($0.url) }
210         
211         // 新しいものを追加
212         let new: [ScreenshotInformation] = newFiles.flatMap { (url) in
213             let index = current.index(where: { (info) -> Bool in
214                 url == info.url
215             })
216             if index == nil {
217                 return ScreenshotInformation(url: url)
218             }
219             return nil
220         }
221         
222         screenshots.screenshots = current + new
223         
224         collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
225         
226         reloadHandler?()
227         saveCache()
228         
229     }
230     
231     private func saveCache() {
232         let data = NSKeyedArchiver.archivedData(withRootObject: screenshots.screenshots)
233         do {
234             try data.write(to: cachURL)
235         }
236         catch let e {
237             print("Can not write cache: \(e)")
238         }
239     }
240     private func loadCache() -> [ScreenshotInformation] {
241         guard let data = try? Data(contentsOf: cachURL)
242             else {
243                 print("can not load cach \(cachURL)")
244                 return []
245         }
246         guard let l = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data as NSData),
247             let loaded = l as? [ScreenshotInformation]
248             else {
249                 print("Can not decode \(cachURL)")
250                 return []
251         }
252         
253         return loaded
254     }
255     
256     private func incrementCacheVersion(forUrl url: URL) {
257         let infos = deletedPaths.filter { $0.url == url }
258         if var info = infos.first {
259             info.incrementVersion()
260         }
261         else {
262             deletedPaths.append(CacheVersionInfo(url: url))
263         }
264     }
265     
266     private func cacheVersion(forUrl url: URL) -> Int {
267         return deletedPaths
268             .filter { $0.url == url }
269             .first?
270             .version ?? 0
271     }
272     
273     // MARK: - IBAction
274     @IBAction func reloadData(_ sender: AnyObject?) {
275         reloadData()
276     }
277     @IBAction func delete(_ sender: AnyObject?) {
278         let posixPaths = selectionInformations
279             .map { $0.url.path }
280             .map { "(\"\($0)\" as POSIX file)" }
281         let list = posixPaths.joined(separator: " , ")
282         let script = "tell application \"Finder\"\n"
283         + "    delete { \(list) }\n"
284         + "end tell"
285         guard let aps = NSAppleScript(source: script) else { return }
286         aps.executeAndReturnError(nil)
287         
288         let selectionIndexes = screenshotsController.selectionIndexes
289         screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
290         selectionInformations.forEach { incrementCacheVersion(forUrl: $0.url) }
291         saveCache()
292         reloadHandler?()
293         
294         guard var index = selectionIndexes.first,
295             let newInfos = arrangedInformations
296             else { return }
297         if newInfos.count <= index { index = newInfos.count - 1 }
298         collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
299     }
300     @IBAction func revealInFinder(_ sender: AnyObject?) {
301         guard let infos = arrangedInformations else { return }
302         let urls = infos.map { $0.url }
303         NSWorkspace.shared().activateFileViewerSelecting(urls)
304     }
305 }
306
307 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
308     func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
309         let f = realFromZoom(zoom: zoom)
310         return NSMakeSize(f, f)
311     }
312 }
313
314 @available(OSX 10.12.2, *)
315 fileprivate var kTouchBars: [Int: NSTouchBar] = [:]
316 @available(OSX 10.12.2, *)
317 fileprivate var kScrubbers: [Int: NSScrubber] = [:]
318 @available(OSX 10.12.2, *)
319 fileprivate var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
320
321 @available(OSX 10.12.2, *)
322 extension ScreenshotListViewController: NSTouchBarDelegate {
323     static let ServicesItemIdentifier: NSTouchBarItemIdentifier = NSTouchBarItemIdentifier(rawValue: "com.masakih.sharingTouchBarItem")
324     
325     @IBOutlet var screenshotTouchBar: NSTouchBar! {
326         get { return kTouchBars[hashValue] }
327         set { kTouchBars[hashValue] = newValue }
328     }
329     @IBOutlet var scrubber: NSScrubber! {
330         get { return kScrubbers[hashValue] }
331         set { kScrubbers[hashValue] = newValue }
332     }
333     @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
334         get { return kPickers[hashValue] }
335         set { kPickers[hashValue] = newValue }
336     }
337     
338     override func makeTouchBar() -> NSTouchBar? {
339         var array: NSArray = []
340         Bundle.main.loadNibNamed("ScreenshotTouchBar", owner: self, topLevelObjects: &array)
341         let identifiers = self.screenshotTouchBar.defaultItemIdentifiers + [ScreenshotListViewController.ServicesItemIdentifier]
342         screenshotTouchBar.defaultItemIdentifiers = identifiers
343         
344         if collectionVisibleDidChangeHandler == nil {
345             collectionVisibleDidChangeHandler = { [unowned self] (visible) in
346                 guard let objects = self.arrangedInformations else { return }
347                 guard let index = visible.first else { return }
348                 let middle = index.item + visible.count / 2
349                 if middle < objects.count - 1 {
350                     self.scrubber.scrollItem(at: middle, to: .none)
351                 }
352             }
353         }
354         if collectionSelectionDidChangeHandler == nil {
355             collectionSelectionDidChangeHandler = { [unowned self] (index) in
356                 self.scrubber.selectedIndex = index
357             }
358         }
359         if reloadHandler == nil {
360             reloadHandler = { [unowned self] _ in
361                 self.scrubber.reloadData()
362             }
363         }
364         
365         return screenshotTouchBar
366     }
367     
368     func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
369         guard identifier == ScreenshotListViewController.ServicesItemIdentifier
370             else { return nil }
371         if sharingItem == nil {
372             sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
373             if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
374                 sharingItem.delegate = w
375             }
376         }
377         return sharingItem
378     }
379 }
380
381 @available(OSX 10.12.2, *)
382 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
383     func numberOfItems(for scrubber: NSScrubber) -> Int {
384         return arrangedInformations?.count ?? 0
385     }
386     func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
387         guard let objects = arrangedInformations else { return NSScrubberImageItemView() }
388         guard objects.count > index else { return NSScrubberImageItemView() }
389         let info = objects[index]
390         let itemView = NSScrubberImageItemView()
391         if let image = NSImage(contentsOf: info.url) {
392             itemView.image = image
393         }
394         
395         return itemView
396     }
397     
398     func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
399         let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
400         collectionView.selectionIndexPaths = [p]
401     }
402     func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
403         if inLiveScrolling { return }
404         let center = visibleRange.location + visibleRange.length / 2
405         let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
406         collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])
407     }
408 }