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] {
33 collectionView.reloadData()
34 UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
37 @objc dynamic var maxZoom: Double = 1.0
39 private var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
40 private var reloadHandler: (() -> Void)?
41 private var collectionSelectionDidChangeHandler: ((Int) -> Void)?
42 private(set) var inLiveScrolling = false
44 private var arrangedInformations: [ScreenshotInformation] {
46 return screenshotsController.arrangedObjects as? [ScreenshotInformation] ?? []
49 private var selectionInformations: [ScreenshotInformation] {
51 return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
54 private var dirName: String {
56 guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
65 var indexPathsOfItemsBeingDragged: Set<IndexPath>?
68 override func viewDidLoad() {
72 let nib = NSNib(nibNamed: ScreenshotCollectionViewItem.nibName, bundle: nil)
73 collectionView.register(nib, forItemWithIdentifier: .item)
75 screenshots.sortDescriptors = [NSSortDescriptor(key: #keyPath(ScreenshotInformation.creationDate), ascending: false)]
76 selectionObservation = collectionView.observe(\NSCollectionView.selectionIndexPaths) { [weak self] (_, _) in
78 guard let `self` = self else {
83 let selections = self.collectionView.selectionIndexPaths
84 let selectionIndexes = selections.reduce(into: IndexSet()) { $0.insert($1.item) }
85 self.screenshots.selectedIndexes = selectionIndexes
86 selectionIndexes.first.map { self.collectionSelectionDidChangeHandler?($0) }
88 collectionView.postsFrameChangedNotifications = true
90 let nc = NotificationCenter.default
91 let scrollView = collectionView.enclosingScrollView
93 nc.addObserver(forName: NSView.frameDidChangeNotification, object: collectionView, queue: nil, using: viewFrameDidChange)
94 nc.addObserver(forName: NSScrollView.didLiveScrollNotification,
95 object: collectionView.enclosingScrollView, queue: nil) { _ in
97 let visibleItems = self.collectionView.indexPathsForVisibleItems()
98 self.collectionVisibleDidChangeHandler?(visibleItems)
100 nc.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil) { _ in
102 self.inLiveScrolling = true
104 nc.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { _ in
106 self.inLiveScrolling = false
108 nc.addObserver(forName: .didRegisterScreenshot,
110 queue: .main) { notification in
112 guard let url = notification.userInfo?[ScreenshotRegister.screenshotURLKey] as? URL else {
117 let info = ScreenshotInformation(url: url)
119 self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
120 let set: Set<IndexPath> = [IndexPath(item: 0, section: 0)]
121 self.collectionView.selectionIndexPaths = set
123 self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
124 if UserDefaults.standard[.showsListWindowAtScreenshot] {
126 self.view.window?.makeKeyAndOrderFront(nil)
130 collectionView.setDraggingSourceOperationMask([.move, .copy, .delete], forLocal: false)
132 viewFrameDidChange(nil)
134 DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: self.reloadData)
137 override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
139 guard let vc = segue.destinationController as? NSViewController else {
144 vc.representedObject = screenshots
147 func viewFrameDidChange(_ notification: Notification?) {
149 maxZoom = calcMaxZoom()
150 if zoom > maxZoom { zoom = maxZoom }
153 /// 画像の大きさの変化が自然になるようにzoom値から画像サイズを計算
154 private func sizeFrom(zoom: Double) -> CGFloat {
158 return CGFloat(type(of: self).maxImageSize * zoom * 0.6)
161 return CGFloat(type(of: self).maxImageSize * (0.8 * zoom * zoom * zoom + 0.2))
164 /// ビューの幅に合わせたzoomの最大値を計算
165 private func calcMaxZoom() -> Double {
167 let effectiveWidth = Double(collectionView.frame.size.width) - type(of: self).leftMergin - type(of: self).rightMergin
169 if effectiveWidth < 240 {
171 return effectiveWidth / type(of: self).maxImageSize / 0.6
173 if effectiveWidth > 800 {
178 return pow((effectiveWidth / type(of: self).maxImageSize - 0.2) / 0.8, 1.0 / 3.0)
181 private func reloadData() {
183 Future<[ScreenshotInformation]> {
185 ScreenshotLoader(ApplicationDirecrories.shared.screenshotSaveDirectoryURL)
186 .merge(screenshots: [])
189 .onSuccess { screenshots in
191 DispatchQueue.main.async {
193 self.screenshots.screenshots = screenshots
195 self.collectionView.selectionIndexPaths = [IndexPath(item: 0, section: 0)]
197 self.reloadHandler?()
205 extension ScreenshotListViewController {
207 @IBAction func reloadContent(_ sender: AnyObject?) {
212 @IBAction func reloadData(_ sender: AnyObject?) {
217 private func moveToTrash(_ urls: [URL]) {
219 let list = urls.map { $0.path }
220 .map { "(\"\($0)\" as POSIX file)" }
221 .joined(separator: " , ")
222 let script = "tell application \"Finder\"\n"
223 + " delete { \(list) }\n"
226 guard let aps = NSAppleScript(source: script) else {
231 aps.executeAndReturnError(nil)
234 @IBAction func delete(_ sender: AnyObject?) {
236 let selectionURLs = selectionInformations.map { $0.url }
238 let selectionIndexes = screenshotsController.selectionIndexes
239 screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
242 guard var index = selectionIndexes.first else {
247 if arrangedInformations.count <= index {
249 index = arrangedInformations.count - 1
251 collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
253 moveToTrash(selectionURLs)
256 @IBAction func revealInFinder(_ sender: AnyObject?) {
258 let urls = selectionInformations.map { $0.url }
259 NSWorkspace.shared.activateFileViewerSelecting(urls)
263 extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
265 func collectionView(_ collectionView: NSCollectionView,
266 layout collectionViewLayout: NSCollectionViewLayout,
267 sizeForItemAt indexPath: IndexPath) -> NSSize {
269 let size = sizeFrom(zoom: zoom)
271 return NSSize(width: size, height: size)
275 func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
280 func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
282 return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
285 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
287 indexPathsOfItemsBeingDragged = indexPaths
290 func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
292 defer { indexPathsOfItemsBeingDragged = nil }
294 guard let dragged = indexPathsOfItemsBeingDragged else {
298 guard operation.contains(.move) || operation.contains(.delete) else {
303 var indexes = IndexSet()
304 dragged.forEach { indexes.insert($0.item) }
306 screenshotsController.remove(atArrangedObjectIndexes: indexes)
311 @available(OSX 10.12.2, *)
312 private var kTouchBars: [Int: NSTouchBar] = [:]
313 @available(OSX 10.12.2, *)
314 private var kScrubbers: [Int: NSScrubber] = [:]
315 @available(OSX 10.12.2, *)
316 private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
318 @available(OSX 10.12.2, *)
319 extension ScreenshotListViewController: NSTouchBarDelegate {
321 static let ServicesItemIdentifier: NSTouchBarItem.Identifier
322 = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
324 @IBOutlet private var screenshotTouchBar: NSTouchBar! {
326 get { return kTouchBars[hashValue] }
327 set { kTouchBars[hashValue] = newValue }
330 @IBOutlet private var scrubber: NSScrubber! {
332 get { return kScrubbers[hashValue] }
333 set { kScrubbers[hashValue] = newValue }
336 @IBOutlet private var sharingItem: NSSharingServicePickerTouchBarItem! {
338 get { return kPickers[hashValue] }
339 set { kPickers[hashValue] = newValue }
342 override func makeTouchBar() -> NSTouchBar? {
344 Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: nil)
345 let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
346 + [type(of: self).ServicesItemIdentifier]
347 screenshotTouchBar.defaultItemIdentifiers = identifiers
349 if collectionVisibleDidChangeHandler == nil {
351 collectionVisibleDidChangeHandler = { [weak self] in
353 guard let `self` = self else {
357 guard let index = $0.first else {
362 let middle = index.item + $0.count / 2
364 if middle < self.arrangedInformations.count - 1 {
366 self.scrubber.scrollItem(at: middle, to: .none)
371 if collectionSelectionDidChangeHandler == nil {
373 collectionSelectionDidChangeHandler = { [weak self] in
375 self?.scrubber.selectedIndex = $0
379 if reloadHandler == nil {
381 reloadHandler = { [weak self] in
383 self?.scrubber.reloadData()
387 return screenshotTouchBar
390 func touchBar(_ touchBar: NSTouchBar,
391 makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
393 guard identifier == type(of: self).ServicesItemIdentifier else {
398 if sharingItem == nil {
400 sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
402 if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
404 sharingItem.delegate = w
412 @available(OSX 10.12.2, *)
413 extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
415 func numberOfItems(for scrubber: NSScrubber) -> Int {
417 return arrangedInformations.count
420 func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
422 guard case 0..<arrangedInformations.count = index else {
424 return NSScrubberImageItemView()
427 let info = arrangedInformations[index]
428 let itemView = NSScrubberImageItemView()
430 if let image = NSImage(contentsOf: info.url) {
432 itemView.image = image
438 func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
440 let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
442 collectionView.selectionIndexPaths = [p]
445 func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
452 let center = visibleRange.location + visibleRange.length / 2
453 let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
454 collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])