2 // ScreenshotListViewController.swift
5 // Created by Hori,Masaki on 2016/12/30.
6 // Copyright © 2016年 Hori,Masaki. All rights reserved.
11 extension NSUserInterfaceItemIdentifier {
13 static let item = NSUserInterfaceItemIdentifier("item")
16 final class ScreenshotListViewController: NSViewController {
18 private static let def = 800.0
19 private static let leftMergin = 8.0 + 1.0
20 private static let rightMergin = 8.0 + 1.0
22 var screenshots: ScreenshotModel = ScreenshotModel()
24 @IBOutlet var screenshotsController: NSArrayController!
25 @IBOutlet weak var collectionView: NSCollectionView!
26 @IBOutlet var contextMenu: NSMenu!
27 @IBOutlet weak var shareButton: NSButton!
28 @IBOutlet weak var standardView: NSView!
29 @IBOutlet weak var editorView: NSView!
31 private var selectionObservation: NSKeyValueObservation?
33 @objc dynamic var zoom: Double = UserDefaults.standard[.screenshotPreviewZoomValue] {
36 collectionView.reloadData()
37 UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
40 @objc dynamic var maxZoom: Double = 1.0
42 private var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
43 private var reloadHandler: (() -> Void)?
44 private var collectionSelectionDidChangeHandler: ((Int) -> Void)?
45 private(set) var inLiveScrolling = false
46 private var arrangedInformations: [ScreenshotInformation] {
48 return screenshotsController.arrangedObjects as? [ScreenshotInformation] ?? []
50 private var selectionInformations: [ScreenshotInformation] {
52 return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
55 private var dirName: String {
57 guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
65 private var screenshotSaveDirectoryURL: URL {
67 let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
68 let url = parentURL.appendingPathComponent(dirName)
69 let fm = FileManager.default
70 var isDir: ObjCBool = false
74 if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
76 try fm.createDirectory(at: url, withIntermediateDirectories: false)
78 } else if !isDir.boolValue {
80 print("\(url) is regular file, not direcory.")
86 print("Can not create screenshot save directory.")
93 var indexPathsOfItemsBeingDragged: Set<IndexPath>?
96 override func viewDidLoad() {
100 let nib = NSNib(nibNamed: ScreenshotCollectionViewItem.nibName, bundle: nil)
101 collectionView.register(nib, forItemWithIdentifier: .item)
103 screenshots.sortDescriptors = [NSSortDescriptor(key: #keyPath(ScreenshotInformation.creationDate), ascending: false)]
104 selectionObservation = collectionView.observe(\NSCollectionView.selectionIndexPaths) { [weak self] (_, _) in
106 guard let `self` = self else { return }
108 let selections = self.collectionView.selectionIndexPaths
109 let selectionIndexes = selections.reduce(into: IndexSet()) { $0.insert($1.item) }
110 self.screenshots.selectedIndexes = selectionIndexes
111 selectionIndexes.first.map { self.collectionSelectionDidChangeHandler?($0) }
113 collectionView.postsFrameChangedNotifications = true
115 let nc = NotificationCenter.default
116 let scrollView = collectionView.enclosingScrollView
118 nc.addObserver(forName: NSView.frameDidChangeNotification, object: collectionView, queue: nil, using: viewFrameDidChange)
119 nc.addObserver(forName: NSScrollView.didLiveScrollNotification,
120 object: collectionView.enclosingScrollView, queue: nil) { _ in
122 let visibleItems = self.collectionView.indexPathsForVisibleItems()
123 self.collectionVisibleDidChangeHandler?(visibleItems)
125 nc.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil) { _ in
127 self.inLiveScrolling = true
129 nc.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { _ in
131 self.inLiveScrolling = false
134 collectionView.setDraggingSourceOperationMask([.move, .copy, .delete], forLocal: false)
136 viewFrameDidChange(nil)
138 DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: self.reloadData)
141 override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
143 guard let vc = segue.destinationController as? NSViewController else { return }
145 vc.representedObject = screenshots
148 func registerImage(_ image: NSImage?) {
150 image?.tiffRepresentation
151 .flatMap { NSBitmapImageRep(data: $0) }
152 .map { registerScreenshot($0, fromOnScreen: .zero) }
155 func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
157 let register = ScreenshotRegister(screenshotSaveDirectoryURL)
159 register.registerScreenshot(image, name: dirName) { url in
161 let info = ScreenshotInformation(url: url)
163 self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
164 let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
165 self.collectionView.selectionIndexPaths = set
167 self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
168 if UserDefaults.standard[.showsListWindowAtScreenshot] {
170 self.view.window?.makeKeyAndOrderFront(nil)
175 func viewFrameDidChange(_ notification: Notification?) {
177 maxZoom = maxZoom(width: collectionView.frame.size.width)
178 if zoom > maxZoom { zoom = maxZoom }
181 private func realFromZoom(zoom: Double) -> CGFloat {
183 if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
185 return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom + 0.2))
188 private func maxZoom(width: CGFloat) -> Double {
190 let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
192 if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
193 if w > 800 { return 1.0 }
195 return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
198 private func reloadData() {
200 screenshots.screenshots = ScreenshotLoader(screenshotSaveDirectoryURL).merge(screenshots: [])
202 collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
210 extension ScreenshotListViewController {
212 @IBAction func reloadContent(_ sender: AnyObject?) {
217 @IBAction func reloadData(_ sender: AnyObject?) {
222 private func moveToTrash(_ urls: [URL]) {
224 let list = urls.map { $0.path }
225 .map { "(\"\($0)\" as POSIX file)" }
226 .joined(separator: " , ")
227 let script = "tell application \"Finder\"\n"
228 + " delete { \(list) }\n"
231 guard let aps = NSAppleScript(source: script) else { return }
233 aps.executeAndReturnError(nil)
236 @IBAction func delete(_ sender: AnyObject?) {
238 let selectionURLs = selectionInformations.map { $0.url }
240 let selectionIndexes = screenshotsController.selectionIndexes
241 screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
244 guard var index = selectionIndexes.first else { return }
246 if arrangedInformations.count <= index {
248 index = arrangedInformations.count - 1
250 collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
252 moveToTrash(selectionURLs)
255 @IBAction func revealInFinder(_ sender: AnyObject?) {
257 let urls = selectionInformations.map { $0.url }
258 NSWorkspace.shared.activateFileViewerSelecting(urls)
262 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
264 func collectionView(_ collectionView: NSCollectionView,
265 layout collectionViewLayout: NSCollectionViewLayout,
266 sizeForItemAt indexPath: IndexPath) -> NSSize {
268 let f = realFromZoom(zoom: zoom)
270 return NSSize(width: f, height: f)
274 func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
279 func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
281 return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
284 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
286 indexPathsOfItemsBeingDragged = indexPaths
289 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
291 defer { indexPathsOfItemsBeingDragged = nil }
293 guard let dragged = indexPathsOfItemsBeingDragged else { return }
294 guard operation.contains(.move) || operation.contains(.delete) else { return }
296 var indexes = IndexSet()
297 dragged.forEach { indexes.insert($0.item) }
299 screenshotsController.remove(atArrangedObjectIndexes: indexes)
304 @available(OSX 10.12.2, *)
305 private var kTouchBars: [Int: NSTouchBar] = [:]
306 @available(OSX 10.12.2, *)
307 private var kScrubbers: [Int: NSScrubber] = [:]
308 @available(OSX 10.12.2, *)
309 private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
311 @available(OSX 10.12.2, *)
312 extension ScreenshotListViewController: NSTouchBarDelegate {
314 static let ServicesItemIdentifier: NSTouchBarItem.Identifier
315 = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
317 @IBOutlet var screenshotTouchBar: NSTouchBar! {
319 get { return kTouchBars[hashValue] }
320 set { kTouchBars[hashValue] = newValue }
323 @IBOutlet var scrubber: NSScrubber! {
325 get { return kScrubbers[hashValue] }
326 set { kScrubbers[hashValue] = newValue }
329 @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
331 get { return kPickers[hashValue] }
332 set { kPickers[hashValue] = newValue }
335 override func makeTouchBar() -> NSTouchBar? {
339 Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: &array)
340 let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
341 + [ScreenshotListViewController.ServicesItemIdentifier]
342 screenshotTouchBar.defaultItemIdentifiers = identifiers
344 if collectionVisibleDidChangeHandler == nil {
346 collectionVisibleDidChangeHandler = { [weak self] in
348 guard let `self` = self else { return }
349 guard let index = $0.first else { return }
351 let middle = index.item + $0.count / 2
353 if middle < self.arrangedInformations.count - 1 {
355 self.scrubber.scrollItem(at: middle, to: .none)
360 if collectionSelectionDidChangeHandler == nil {
362 collectionSelectionDidChangeHandler = { [weak self] in
364 self?.scrubber.selectedIndex = $0
368 if reloadHandler == nil {
370 reloadHandler = { [weak self] in
372 self?.scrubber.reloadData()
376 return screenshotTouchBar
379 func touchBar(_ touchBar: NSTouchBar,
380 makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
382 guard identifier == ScreenshotListViewController.ServicesItemIdentifier else { return nil }
384 if sharingItem == nil {
386 sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
388 if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
390 sharingItem.delegate = w
397 @available(OSX 10.12.2, *)
398 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
400 func numberOfItems(for scrubber: NSScrubber) -> Int {
402 return arrangedInformations.count
405 func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
407 guard case 0..<arrangedInformations.count = index else { return NSScrubberImageItemView() }
409 let info = arrangedInformations[index]
410 let itemView = NSScrubberImageItemView()
412 if let image = NSImage(contentsOf: info.url) {
414 itemView.image = image
420 func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
422 let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
424 collectionView.selectionIndexPaths = [p]
427 func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
429 if inLiveScrolling { return }
431 let center = visibleRange.location + visibleRange.length / 2
432 let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
433 collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])