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 maxImageSize = 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 private var screenshotsController: NSArrayController!
25 @IBOutlet private weak var collectionView: NSCollectionView!
27 private var selectionObservation: NSKeyValueObservation?
29 @objc dynamic var zoom: Double = UserDefaults.standard[.screenshotPreviewZoomValue] {
32 collectionView.reloadData()
33 UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
36 @objc dynamic var maxZoom: Double = 1.0
38 private var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
39 private var reloadHandler: (() -> Void)?
40 private var collectionSelectionDidChangeHandler: ((Int) -> Void)?
41 private(set) var inLiveScrolling = false
42 private var arrangedInformations: [ScreenshotInformation] {
44 return screenshotsController.arrangedObjects as? [ScreenshotInformation] ?? []
46 private var selectionInformations: [ScreenshotInformation] {
48 return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
51 private var dirName: String {
53 guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
61 private var screenshotSaveDirectoryURL: URL {
63 let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
64 let url = parentURL.appendingPathComponent(dirName)
65 let fm = FileManager.default
66 var isDir: ObjCBool = false
70 if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
72 try fm.createDirectory(at: url, withIntermediateDirectories: false)
74 } else if !isDir.boolValue {
76 print("\(url) is regular file, not direcory.")
82 print("Can not create screenshot save directory.")
89 var indexPathsOfItemsBeingDragged: Set<IndexPath>?
92 override func viewDidLoad() {
96 let nib = NSNib(nibNamed: ScreenshotCollectionViewItem.nibName, bundle: nil)
97 collectionView.register(nib, forItemWithIdentifier: .item)
99 screenshots.sortDescriptors = [NSSortDescriptor(key: #keyPath(ScreenshotInformation.creationDate), ascending: false)]
100 selectionObservation = collectionView.observe(\NSCollectionView.selectionIndexPaths) { [weak self] (_, _) in
102 guard let `self` = self else { return }
104 let selections = self.collectionView.selectionIndexPaths
105 let selectionIndexes = selections.reduce(into: IndexSet()) { $0.insert($1.item) }
106 self.screenshots.selectedIndexes = selectionIndexes
107 selectionIndexes.first.map { self.collectionSelectionDidChangeHandler?($0) }
109 collectionView.postsFrameChangedNotifications = true
111 let nc = NotificationCenter.default
112 let scrollView = collectionView.enclosingScrollView
114 nc.addObserver(forName: NSView.frameDidChangeNotification, object: collectionView, queue: nil, using: viewFrameDidChange)
115 nc.addObserver(forName: NSScrollView.didLiveScrollNotification,
116 object: collectionView.enclosingScrollView, queue: nil) { _ in
118 let visibleItems = self.collectionView.indexPathsForVisibleItems()
119 self.collectionVisibleDidChangeHandler?(visibleItems)
121 nc.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil) { _ in
123 self.inLiveScrolling = true
125 nc.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { _ in
127 self.inLiveScrolling = false
129 nc.addObserver(forName: .didRegisterScreenshot,
131 queue: .main) { notification in
133 guard let url = notification.userInfo?[ScreenshotRegister.screenshotURLKey] as? URL else { return }
135 let info = ScreenshotInformation(url: url)
137 self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
138 let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
139 self.collectionView.selectionIndexPaths = set
141 self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
142 if UserDefaults.standard[.showsListWindowAtScreenshot] {
144 self.view.window?.makeKeyAndOrderFront(nil)
148 collectionView.setDraggingSourceOperationMask([.move, .copy, .delete], forLocal: false)
150 viewFrameDidChange(nil)
152 DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: self.reloadData)
155 override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
157 guard let vc = segue.destinationController as? NSViewController else { return }
159 vc.representedObject = screenshots
162 func viewFrameDidChange(_ notification: Notification?) {
164 maxZoom = calcMaxZoom()
165 if zoom > maxZoom { zoom = maxZoom }
168 /// 画像の大きさの変化が自然になるようにzoom値から画像サイズを計算
169 private func sizeFrom(zoom: Double) -> CGFloat {
171 if zoom < 0.5 { return CGFloat(type(of: self).maxImageSize * zoom * 0.6) }
173 return CGFloat(type(of: self).maxImageSize * (0.8 * zoom * zoom * zoom + 0.2))
176 /// ビューの幅に合わせたzoomの最大値を計算
177 private func calcMaxZoom() -> Double {
179 let effectiveWidth = Double(collectionView.frame.size.width) - type(of: self).leftMergin - type(of: self).rightMergin
181 if effectiveWidth < 240 { return effectiveWidth / type(of: self).maxImageSize / 0.6 }
182 if effectiveWidth > 800 { return 1.0 }
184 return pow((effectiveWidth / type(of: self).maxImageSize - 0.2) / 0.8, 1.0 / 3.0)
187 private func reloadData() {
189 Promise<[ScreenshotInformation]>()
191 Result(ScreenshotLoader(self.screenshotSaveDirectoryURL).merge(screenshots: []))
194 .onSuccess { screenshots in
196 DispatchQueue.main.async {
197 self.screenshots.screenshots = screenshots
199 self.collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
201 self.reloadHandler?()
209 extension ScreenshotListViewController {
211 @IBAction func reloadContent(_ sender: AnyObject?) {
216 @IBAction func reloadData(_ sender: AnyObject?) {
221 private func moveToTrash(_ urls: [URL]) {
223 let list = urls.map { $0.path }
224 .map { "(\"\($0)\" as POSIX file)" }
225 .joined(separator: " , ")
226 let script = "tell application \"Finder\"\n"
227 + " delete { \(list) }\n"
230 guard let aps = NSAppleScript(source: script) else { return }
232 aps.executeAndReturnError(nil)
235 @IBAction func delete(_ sender: AnyObject?) {
237 let selectionURLs = selectionInformations.map { $0.url }
239 let selectionIndexes = screenshotsController.selectionIndexes
240 screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
243 guard var index = selectionIndexes.first else { return }
245 if arrangedInformations.count <= index {
247 index = arrangedInformations.count - 1
249 collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
251 moveToTrash(selectionURLs)
254 @IBAction func revealInFinder(_ sender: AnyObject?) {
256 let urls = selectionInformations.map { $0.url }
257 NSWorkspace.shared.activateFileViewerSelecting(urls)
261 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
263 func collectionView(_ collectionView: NSCollectionView,
264 layout collectionViewLayout: NSCollectionViewLayout,
265 sizeForItemAt indexPath: IndexPath) -> NSSize {
267 let size = sizeFrom(zoom: zoom)
269 return NSSize(width: size, height: size)
273 func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
278 func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
280 return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
283 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
285 indexPathsOfItemsBeingDragged = indexPaths
288 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
290 defer { indexPathsOfItemsBeingDragged = nil }
292 guard let dragged = indexPathsOfItemsBeingDragged else { return }
293 guard operation.contains(.move) || operation.contains(.delete) else { return }
295 var indexes = IndexSet()
296 dragged.forEach { indexes.insert($0.item) }
298 screenshotsController.remove(atArrangedObjectIndexes: indexes)
303 @available(OSX 10.12.2, *)
304 private var kTouchBars: [Int: NSTouchBar] = [:]
305 @available(OSX 10.12.2, *)
306 private var kScrubbers: [Int: NSScrubber] = [:]
307 @available(OSX 10.12.2, *)
308 private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
310 @available(OSX 10.12.2, *)
311 extension ScreenshotListViewController: NSTouchBarDelegate {
313 static let ServicesItemIdentifier: NSTouchBarItem.Identifier
314 = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
316 @IBOutlet private var screenshotTouchBar: NSTouchBar! {
318 get { return kTouchBars[hashValue] }
319 set { kTouchBars[hashValue] = newValue }
322 @IBOutlet private var scrubber: NSScrubber! {
324 get { return kScrubbers[hashValue] }
325 set { kScrubbers[hashValue] = newValue }
328 @IBOutlet private var sharingItem: NSSharingServicePickerTouchBarItem! {
330 get { return kPickers[hashValue] }
331 set { kPickers[hashValue] = newValue }
334 override func makeTouchBar() -> NSTouchBar? {
336 Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: nil)
337 let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
338 + [type(of: self).ServicesItemIdentifier]
339 screenshotTouchBar.defaultItemIdentifiers = identifiers
341 if collectionVisibleDidChangeHandler == nil {
343 collectionVisibleDidChangeHandler = { [weak self] in
345 guard let `self` = self else { return }
346 guard let index = $0.first else { return }
348 let middle = index.item + $0.count / 2
350 if middle < self.arrangedInformations.count - 1 {
352 self.scrubber.scrollItem(at: middle, to: .none)
357 if collectionSelectionDidChangeHandler == nil {
359 collectionSelectionDidChangeHandler = { [weak self] in
361 self?.scrubber.selectedIndex = $0
365 if reloadHandler == nil {
367 reloadHandler = { [weak self] in
369 self?.scrubber.reloadData()
373 return screenshotTouchBar
376 func touchBar(_ touchBar: NSTouchBar,
377 makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
379 guard identifier == type(of: self).ServicesItemIdentifier else { return nil }
381 if sharingItem == nil {
383 sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
385 if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
387 sharingItem.delegate = w
394 @available(OSX 10.12.2, *)
395 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
397 func numberOfItems(for scrubber: NSScrubber) -> Int {
399 return arrangedInformations.count
402 func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
404 guard case 0..<arrangedInformations.count = index else { return NSScrubberImageItemView() }
406 let info = arrangedInformations[index]
407 let itemView = NSScrubberImageItemView()
409 if let image = NSImage(contentsOf: info.url) {
411 itemView.image = image
417 func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
419 let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
421 collectionView.selectionIndexPaths = [p]
424 func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
426 if inLiveScrolling { return }
428 let center = visibleRange.location + visibleRange.length / 2
429 let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
430 collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])