2 // ScreenshotListViewController.swift
5 // Created by Hori,Masaki on 2016/12/30.
6 // Copyright © 2016年 Hori,Masaki. All rights reserved.
11 fileprivate struct CacheVersionInfo {
15 init(url: URL, version: Int = 0) {
18 self.version = version
21 private(set) var version: Int
23 mutating func incrementVersion() { version = version + 1 }
26 final class ScreenshotListViewController: NSViewController {
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
32 var screenshots: ScreenshotModel = ScreenshotModel()
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!
41 dynamic var zoom: Double = UserDefaults.standard[.screenshotPreviewZoomValue] {
44 collectionView.reloadData()
45 UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
48 dynamic var maxZoom: Double = 1.0
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]? {
56 return screenshotsController.arrangedObjects as? [ScreenshotInformation]
58 fileprivate var selectionInformations: [ScreenshotInformation] {
60 return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
63 private var deletedPaths: [CacheVersionInfo] = []
64 private var dirName: String {
66 guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
72 private var screenshotSaveDirectoryURL: URL {
74 let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
75 let url = parentURL.appendingPathComponent(dirName)
76 let fm = FileManager.default
77 var isDir: ObjCBool = false
81 if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
83 try fm.createDirectory(at: url, withIntermediateDirectories: false)
85 } else if !isDir.boolValue {
87 print("\(url) is regular file, not direcory.")
93 print("Can not create screenshot save directory.")
100 private var cachURL: URL {
102 return screenshotSaveDirectoryURL.appendingPathComponent("Cache.db")
106 override func viewDidLoad() {
110 screenshots.screenshots = loadCache()
112 let nib = NSNib(nibNamed: "ScreenshotCollectionViewItem", bundle: nil)
113 collectionView.register(NSCollectionView.self, forItemWithIdentifier: "item")
114 collectionView.register(nib, forItemWithIdentifier: "item")
116 screenshots.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
117 collectionView.addObserver(self, forKeyPath: "selectionIndexPaths", context: nil)
118 collectionView.postsFrameChangedNotifications = true
120 let nc = NotificationCenter.default
121 let scrollView = collectionView.enclosingScrollView
123 nc.addObserver(forName: .NSViewFrameDidChange, object: collectionView, queue: nil, using: viewFrameDidChange)
124 nc.addObserver(forName: .NSScrollViewDidLiveScroll,
125 object: collectionView.enclosingScrollView, queue: nil) { _ in
127 let visibleItems = self.collectionView.indexPathsForVisibleItems()
128 self.collectionVisibleDidChangeHandler?(visibleItems)
130 nc.addObserver(forName: .NSScrollViewWillStartLiveScroll, object: scrollView, queue: nil) { _ in
132 self.inLiveScrolling = true
134 nc.addObserver(forName: .NSScrollViewDidEndLiveScroll, object: scrollView, queue: nil) { _ in
136 self.inLiveScrolling = false
139 viewFrameDidChange(nil)
142 .asyncAfter(deadline: .now() + 0.0001 ) { self.reloadData() }
145 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
147 if let object = object as? NSCollectionView,
148 object == collectionView {
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) }
159 super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
162 override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
164 guard let vc = segue.destinationController as? NSViewController else { return }
166 vc.representedObject = screenshots
169 func registerImage(_ image: NSImage?) {
171 image?.tiffRepresentation
172 .flatMap { NSBitmapImageRep(data: $0) }
173 .map { registerScreenshot($0, fromOnScreen: .zero) }
176 func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
178 DispatchQueue(label: "Screenshot queue")
181 guard let data = image.representation(using: .JPEG, properties: [:])
184 let url = self.screenshotSaveDirectoryURL
185 .appendingPathComponent(self.dirName)
186 .appendingPathExtension("jpg")
187 let pathURL = FileManager.default.uniqueFileURL(url)
191 try data.write(to: pathURL)
195 print("Can not write image")
199 DispatchQueue.main.async {
201 let info = ScreenshotInformation(url: pathURL, version: self.cacheVersion(forUrl: pathURL))
203 self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
204 let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
205 self.collectionView.selectionIndexPaths = set
207 self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
208 if UserDefaults.standard[.showsListWindowAtScreenshot] {
210 self.view.window?.makeKeyAndOrderFront(nil)
218 func viewFrameDidChange(_ notification: Notification?) {
220 maxZoom = self.maxZoom(width: collectionView.frame.size.width)
221 if zoom > maxZoom { zoom = maxZoom }
224 fileprivate func realFromZoom(zoom: Double) -> CGFloat {
226 if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
228 return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom + 0.2))
231 private func maxZoom(width: CGFloat) -> Double {
233 let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
235 if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
236 if w > 800 { return 1.0 }
238 return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
241 fileprivate func reloadData() {
243 guard let f = try? FileManager
245 .contentsOfDirectory(at: screenshotSaveDirectoryURL, includingPropertiesForKeys: nil) else {
246 print("can not read list of screenshot directory")
250 let imageTypes = NSImage.imageTypes()
251 let ws = NSWorkspace.shared()
252 var current = screenshots.screenshots
253 let newFiles: [URL] = f.flatMap {
255 guard let type = try? ws.type(ofFile: $0.path)
258 return imageTypes.contains(type) ? $0 : nil
262 current = current.filter { newFiles.contains($0.url) }
265 let new: [ScreenshotInformation] = newFiles.flatMap { url in
267 let index = current.index { url == $0.url }
269 return index == nil ? ScreenshotInformation(url: url) : nil
272 screenshots.screenshots = current + new
274 collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
281 fileprivate func saveCache() {
283 let data = NSKeyedArchiver.archivedData(withRootObject: screenshots.screenshots)
287 try data.write(to: cachURL)
291 print("Can not write cache: \(error)")
296 private func loadCache() -> [ScreenshotInformation] {
298 guard let data = try? Data(contentsOf: cachURL)
300 print("can not load cach \(cachURL)")
304 guard let l = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data as NSData),
305 let loaded = l as? [ScreenshotInformation]
307 print("Can not decode \(cachURL)")
314 fileprivate func incrementCacheVersion(forUrl url: URL) {
316 let infos = deletedPaths.filter { $0.url == url }
318 if var info = infos.first {
320 info.incrementVersion()
324 deletedPaths.append(CacheVersionInfo(url: url))
329 private func cacheVersion(forUrl url: URL) -> Int {
332 .filter { $0.url == url }
339 extension ScreenshotListViewController {
341 @IBAction func reloadData(_ sender: AnyObject?) {
346 @IBAction func delete(_ sender: AnyObject?) {
348 let list = selectionInformations
350 .map { "(\"\($0)\" as POSIX file)" }
351 .joined(separator: " , ")
352 let script = "tell application \"Finder\"\n"
353 + " delete { \(list) }\n"
356 guard let aps = NSAppleScript(source: script)
359 aps.executeAndReturnError(nil)
361 let selectionIndexes = screenshotsController.selectionIndexes
362 screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
363 selectionInformations.forEach { incrementCacheVersion(forUrl: $0.url) }
367 guard var index = selectionIndexes.first,
368 let newInfos = arrangedInformations
371 if newInfos.count <= index { index = newInfos.count - 1 }
372 collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
375 @IBAction func revealInFinder(_ sender: AnyObject?) {
377 guard let infos = arrangedInformations
380 let urls = infos.map { $0.url }
381 NSWorkspace.shared().activateFileViewerSelecting(urls)
385 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
387 func collectionView(_ collectionView: NSCollectionView,
388 layout collectionViewLayout: NSCollectionViewLayout,
389 sizeForItemAt indexPath: IndexPath) -> NSSize {
391 let f = realFromZoom(zoom: zoom)
393 return NSSize(width: f, height: f)
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] = [:]
404 @available(OSX 10.12.2, *)
405 extension ScreenshotListViewController: NSTouchBarDelegate {
407 static let ServicesItemIdentifier: NSTouchBarItemIdentifier
408 = NSTouchBarItemIdentifier(rawValue: "com.masakih.sharingTouchBarItem")
410 @IBOutlet var screenshotTouchBar: NSTouchBar! {
412 get { return kTouchBars[hashValue] }
413 set { kTouchBars[hashValue] = newValue }
416 @IBOutlet var scrubber: NSScrubber! {
418 get { return kScrubbers[hashValue] }
419 set { kScrubbers[hashValue] = newValue }
422 @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
424 get { return kPickers[hashValue] }
425 set { kPickers[hashValue] = newValue }
428 override func makeTouchBar() -> NSTouchBar? {
430 var array: NSArray = []
432 Bundle.main.loadNibNamed("ScreenshotTouchBar", owner: self, topLevelObjects: &array)
433 let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
434 + [ScreenshotListViewController.ServicesItemIdentifier]
435 screenshotTouchBar.defaultItemIdentifiers = identifiers
437 if collectionVisibleDidChangeHandler == nil {
439 collectionVisibleDidChangeHandler = { [weak self] in
441 guard let `self` = self,
442 let objects = self.arrangedInformations,
446 let middle = index.item + $0.count / 2
448 if middle < objects.count - 1 {
450 self.scrubber.scrollItem(at: middle, to: .none)
455 if collectionSelectionDidChangeHandler == nil {
457 collectionSelectionDidChangeHandler = { [weak self] in
459 guard let `self` = self else { return }
461 self.scrubber.selectedIndex = $0
465 if reloadHandler == nil {
467 reloadHandler = { [weak self] _ in
469 guard let `self` = self else { return }
471 self.scrubber.reloadData()
475 return screenshotTouchBar
478 func touchBar(_ touchBar: NSTouchBar,
479 makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
481 guard identifier == ScreenshotListViewController.ServicesItemIdentifier
484 if sharingItem == nil {
486 sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
488 if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
490 sharingItem.delegate = w
497 @available(OSX 10.12.2, *)
498 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
500 func numberOfItems(for scrubber: NSScrubber) -> Int {
502 return arrangedInformations?.count ?? 0
505 func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
507 guard let objects = arrangedInformations
508 else { return NSScrubberImageItemView() }
510 guard objects.count > index
511 else { return NSScrubberImageItemView() }
513 let info = objects[index]
514 let itemView = NSScrubberImageItemView()
516 if let image = NSImage(contentsOf: info.url) {
518 itemView.image = image
524 func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
526 let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
528 collectionView.selectionIndexPaths = [p]
531 func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
533 if inLiveScrolling { return }
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])