2 // ScreenshotListViewController.swift
5 // Created by Hori,Masaki on 2016/12/30.
6 // Copyright © 2016年 Hori,Masaki. All rights reserved.
11 fileprivate struct CacheVersionInfo {
14 init(url: URL, version: Int = 0) {
16 self.version = version
19 private(set) var version: Int
21 mutating func incrementVersion() { version = version + 1 }
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
29 var screenshots: ScreenshotModel = ScreenshotModel()
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!
38 dynamic var zoom: Double = UserDefaults.standard.screenshotPreviewZoomValue {
40 collectionView.reloadData()
41 UserDefaults.standard.screenshotPreviewZoomValue = zoom
44 dynamic var maxZoom: Double = 1.0
46 fileprivate var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
47 fileprivate var reloadHandler: (() -> Void)?
48 fileprivate var collectionSelectionDidChangeHandler: ((Int) -> Void)?
49 fileprivate(set) var inLiveScrolling = false
50 fileprivate var arrangedInformations: [ScreenshotInformation]? {
51 return screenshotsController.arrangedObjects as? [ScreenshotInformation]
53 fileprivate var selectionInformations: [ScreenshotInformation] {
54 return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
57 private var deletedPaths: [CacheVersionInfo] = []
58 private var dirName: String {
59 guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
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
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.")
77 print("Can not create screenshot save directory.")
82 private var cachURL: URL {
83 return screenshotSaveDirectoryURL.appendingPathComponent("Cache.db")
87 override func viewDidLoad() {
90 screenshots.screenshots = loadCache()
92 let nib = NSNib(nibNamed: "ScreenshotCollectionViewItem", bundle: nil)
93 collectionView.register(NSCollectionView.self, forItemWithIdentifier: "item")
94 collectionView.register(nib, forItemWithIdentifier: "item")
96 screenshots.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
97 collectionView.addObserver(self, forKeyPath: "selectionIndexPaths", context: nil)
98 collectionView.postsFrameChangedNotifications = true
100 let nc = NotificationCenter.default
101 let scrollView = collectionView.enclosingScrollView
103 nc.addObserver(forName: .NSViewFrameDidChange, object: collectionView, queue: nil, using: viewFrameDidChange)
104 nc.addObserver(forName: .NSScrollViewDidLiveScroll,
105 object: collectionView.enclosingScrollView, queue: nil) { _ in
106 let visibleItems = self.collectionView.indexPathsForVisibleItems()
107 self.collectionVisibleDidChangeHandler?(visibleItems)
109 nc.addObserver(forName: .NSScrollViewWillStartLiveScroll, object: scrollView, queue: nil) { _ in
110 self.inLiveScrolling = true
112 nc.addObserver(forName: .NSScrollViewDidEndLiveScroll, object: scrollView, queue: nil) { _ in
113 self.inLiveScrolling = false
116 viewFrameDidChange(nil)
119 .asyncAfter(deadline: .now() + 0.0001 ) { self.reloadData() }
121 override func observeValue(forKeyPath keyPath: String?,
123 change: [NSKeyValueChangeKey : Any]?,
124 context: UnsafeMutableRawPointer?) {
125 if let object = object as? NSCollectionView,
126 object == collectionView {
127 let selections = collectionView.selectionIndexPaths
128 var selectionIndexes = IndexSet()
129 selections.forEach { selectionIndexes.insert($0.item) }
130 screenshots.selectedIndexes = selectionIndexes
131 selectionIndexes.first.map { collectionSelectionDidChangeHandler?($0) }
135 super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
137 override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
138 guard let vc = segue.destinationController as? NSViewController else { return }
139 vc.representedObject = screenshots
142 func registerImage(_ image: NSImage?) {
143 image?.tiffRepresentation
144 .flatMap { NSBitmapImageRep(data: $0) }
145 .map { registerScreenshot($0, fromOnScreen: .zero) }
147 func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
148 DispatchQueue(label: "Screenshot queue")
150 guard let data = image.representation(using: .JPEG, properties: [:])
152 let url = self.screenshotSaveDirectoryURL
153 .appendingPathComponent(self.dirName)
154 .appendingPathExtension("jpg")
155 let pathURL = FileManager.default.uniqueFileURL(url)
157 try data.write(to: pathURL)
159 print("Can not write image")
163 DispatchQueue.main.async {
164 let info = ScreenshotInformation(url: pathURL, version: self.cacheVersion(forUrl: pathURL))
166 self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
167 let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
168 self.collectionView.selectionIndexPaths = set
170 self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
171 if UserDefaults.standard.showsListWindowAtScreenshot {
172 self.view.window?.makeKeyAndOrderFront(nil)
178 func viewFrameDidChange(_ notification: Notification?) {
179 maxZoom = self.maxZoom(width: collectionView.frame.size.width)
180 if zoom > maxZoom { zoom = maxZoom }
183 fileprivate func realFromZoom(zoom: Double) -> CGFloat {
184 if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
185 return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom + 0.2))
187 private func maxZoom(width: CGFloat) -> Double {
188 let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
189 if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
190 if w > 800 { return 1.0 }
191 return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
194 fileprivate func reloadData() {
195 guard let f = try? FileManager
197 .contentsOfDirectory(at: screenshotSaveDirectoryURL, includingPropertiesForKeys: nil) else {
198 print("can not read list of screenshot directory")
201 let imageTypes = NSImage.imageTypes()
202 let ws = NSWorkspace.shared()
203 var current = screenshots.screenshots
204 let newFiles: [URL] = f.flatMap {
205 guard let type = try? ws.type(ofFile: $0.path) else { return nil }
206 return imageTypes.contains(type) ? $0 : nil
210 current = current.filter { newFiles.contains($0.url) }
213 let new: [ScreenshotInformation] = newFiles.flatMap { url in
214 let index = current.index { url == $0.url }
215 return index == nil ? ScreenshotInformation(url: url) : nil
218 screenshots.screenshots = current + new
220 collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
227 fileprivate func saveCache() {
228 let data = NSKeyedArchiver.archivedData(withRootObject: screenshots.screenshots)
230 try data.write(to: cachURL)
232 print("Can not write cache: \(e)")
235 private func loadCache() -> [ScreenshotInformation] {
236 guard let data = try? Data(contentsOf: cachURL)
238 print("can not load cach \(cachURL)")
241 guard let l = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data as NSData),
242 let loaded = l as? [ScreenshotInformation]
244 print("Can not decode \(cachURL)")
251 fileprivate func incrementCacheVersion(forUrl url: URL) {
252 let infos = deletedPaths.filter { $0.url == url }
253 if var info = infos.first {
254 info.incrementVersion()
256 deletedPaths.append(CacheVersionInfo(url: url))
260 private func cacheVersion(forUrl url: URL) -> Int {
262 .filter { $0.url == url }
269 extension ScreenshotListViewController {
270 @IBAction func reloadData(_ sender: AnyObject?) {
273 @IBAction func delete(_ sender: AnyObject?) {
274 let list = selectionInformations
276 .map { "(\"\($0)\" as POSIX file)" }
277 .joined(separator: " , ")
278 let script = "tell application \"Finder\"\n"
279 + " delete { \(list) }\n"
281 guard let aps = NSAppleScript(source: script) else { return }
282 aps.executeAndReturnError(nil)
284 let selectionIndexes = screenshotsController.selectionIndexes
285 screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
286 selectionInformations.forEach { incrementCacheVersion(forUrl: $0.url) }
290 guard var index = selectionIndexes.first,
291 let newInfos = arrangedInformations
293 if newInfos.count <= index { index = newInfos.count - 1 }
294 collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
296 @IBAction func revealInFinder(_ sender: AnyObject?) {
297 guard let infos = arrangedInformations else { return }
298 let urls = infos.map { $0.url }
299 NSWorkspace.shared().activateFileViewerSelecting(urls)
303 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
304 func collectionView(_ collectionView: NSCollectionView,
305 layout collectionViewLayout: NSCollectionViewLayout,
306 sizeForItemAt indexPath: IndexPath) -> NSSize {
307 let f = realFromZoom(zoom: zoom)
308 return NSSize(width: f, height: f)
312 @available(OSX 10.12.2, *)
313 fileprivate var kTouchBars: [Int: NSTouchBar] = [:]
314 @available(OSX 10.12.2, *)
315 fileprivate var kScrubbers: [Int: NSScrubber] = [:]
316 @available(OSX 10.12.2, *)
317 fileprivate var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
319 @available(OSX 10.12.2, *)
320 extension ScreenshotListViewController: NSTouchBarDelegate {
321 static let ServicesItemIdentifier: NSTouchBarItemIdentifier
322 = NSTouchBarItemIdentifier(rawValue: "com.masakih.sharingTouchBarItem")
324 @IBOutlet var screenshotTouchBar: NSTouchBar! {
325 get { return kTouchBars[hashValue] }
326 set { kTouchBars[hashValue] = newValue }
328 @IBOutlet var scrubber: NSScrubber! {
329 get { return kScrubbers[hashValue] }
330 set { kScrubbers[hashValue] = newValue }
332 @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
333 get { return kPickers[hashValue] }
334 set { kPickers[hashValue] = newValue }
337 override func makeTouchBar() -> NSTouchBar? {
338 var array: NSArray = []
339 Bundle.main.loadNibNamed("ScreenshotTouchBar", owner: self, topLevelObjects: &array)
340 let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
341 + [ScreenshotListViewController.ServicesItemIdentifier]
342 screenshotTouchBar.defaultItemIdentifiers = identifiers
344 if collectionVisibleDidChangeHandler == nil {
345 collectionVisibleDidChangeHandler = { [weak self] in
346 guard let `self` = self,
347 let objects = self.arrangedInformations,
350 let middle = index.item + $0.count / 2
351 if middle < objects.count - 1 {
352 self.scrubber.scrollItem(at: middle, to: .none)
356 if collectionSelectionDidChangeHandler == nil {
357 collectionSelectionDidChangeHandler = { [weak self] in
358 guard let `self` = self else { return }
359 self.scrubber.selectedIndex = $0
362 if reloadHandler == nil {
363 reloadHandler = { [weak self] _ in
364 guard let `self` = self else { return }
365 self.scrubber.reloadData()
369 return screenshotTouchBar
372 func touchBar(_ touchBar: NSTouchBar,
373 makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
374 guard identifier == ScreenshotListViewController.ServicesItemIdentifier
376 if sharingItem == nil {
377 sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
378 if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
379 sharingItem.delegate = w
386 @available(OSX 10.12.2, *)
387 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
388 func numberOfItems(for scrubber: NSScrubber) -> Int {
389 return arrangedInformations?.count ?? 0
391 func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
392 guard let objects = arrangedInformations else { return NSScrubberImageItemView() }
393 guard objects.count > index else { return NSScrubberImageItemView() }
394 let info = objects[index]
395 let itemView = NSScrubberImageItemView()
396 if let image = NSImage(contentsOf: info.url) {
397 itemView.image = image
403 func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
404 let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
405 collectionView.selectionIndexPaths = [p]
407 func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
408 if inLiveScrolling { return }
409 let center = visibleRange.location + visibleRange.length / 2
410 let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
411 collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])