import Cocoa
-fileprivate struct CacheVersionInfo {
- let url: URL
+extension NSUserInterfaceItemIdentifier {
- init(url: URL, version: Int = 0) {
- self.url = url
- self.version = version
- }
-
- private(set) var version: Int
-
- mutating func incrementVersion() { version = version + 1 }
+ static let item = NSUserInterfaceItemIdentifier("item")
}
-class ScreenshotListViewController: NSViewController {
- private static let def = 800.0
+final class ScreenshotListViewController: NSViewController {
+
+ private static let maxImageSize = 800.0
private static let leftMergin = 8.0 + 1.0
private static let rightMergin = 8.0 + 1.0
var screenshots: ScreenshotModel = ScreenshotModel()
- @IBOutlet var screenshotsController: NSArrayController!
- @IBOutlet weak var collectionView: NSCollectionView!
- @IBOutlet var contextMenu: NSMenu!
- @IBOutlet weak var shareButton: NSButton!
- @IBOutlet weak var standardView: NSView!
- @IBOutlet weak var editorView: NSView!
+ @IBOutlet private var screenshotsController: NSArrayController!
+ @IBOutlet private weak var collectionView: NSCollectionView!
+
+ private var selectionObservation: NSKeyValueObservation?
- dynamic var zoom: Double = UserDefaults.standard.screenshotPreviewZoomValue {
+ @objc dynamic var zoom: Double = UserDefaults.standard[.screenshotPreviewZoomValue] {
+
didSet {
collectionView.reloadData()
- UserDefaults.standard.screenshotPreviewZoomValue = zoom
+ UserDefaults.standard[.screenshotPreviewZoomValue] = zoom
}
}
- dynamic var maxZoom: Double = 1.0
-
- fileprivate var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)? = nil
- fileprivate var reloadHandler: (() -> Void)? = nil
- fileprivate var collectionSelectionDidChangeHandler: ((Int) -> Void)? = nil
- fileprivate(set) var inLiveScrolling = false
- fileprivate var arrangedInformations: [ScreenshotInformation]? {
- return screenshotsController.arrangedObjects as? [ScreenshotInformation]
+ @objc dynamic var maxZoom: Double = 1.0
+
+ private var collectionVisibleDidChangeHandler: ((Set<IndexPath>) -> Void)?
+ private var reloadHandler: (() -> Void)?
+ private var collectionSelectionDidChangeHandler: ((Int) -> Void)?
+ private(set) var inLiveScrolling = false
+ private var arrangedInformations: [ScreenshotInformation] {
+
+ return screenshotsController.arrangedObjects as? [ScreenshotInformation] ?? []
}
- fileprivate var selectionInformations : [ScreenshotInformation] {
+ private var selectionInformations: [ScreenshotInformation] {
+
return screenshotsController.selectedObjects as? [ScreenshotInformation] ?? []
}
- private var deletedPaths: [CacheVersionInfo] = []
private var dirName: String {
+
guard let name = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String,
- !name.isEmpty
- else { return "KCD" }
+ !name.isEmpty else {
+
+ return "KCD"
+ }
+
return name
}
private var screenshotSaveDirectoryURL: URL {
+
let parentURL = URL(fileURLWithPath: AppDelegate.shared.screenShotSaveDirectory)
let url = parentURL.appendingPathComponent(dirName)
let fm = FileManager.default
var isDir: ObjCBool = false
+
do {
+
if !fm.fileExists(atPath: url.path, isDirectory: &isDir) {
+
try fm.createDirectory(at: url, withIntermediateDirectories: false)
+
} else if !isDir.boolValue {
+
print("\(url) is regular file, not direcory.")
return parentURL
}
- }
- catch {
+
+ } catch {
+
print("Can not create screenshot save directory.")
return parentURL
}
+
return url
}
- private var cachURL: URL {
- return screenshotSaveDirectoryURL.appendingPathComponent("Cache.db")
- }
+
+ var indexPathsOfItemsBeingDragged: Set<IndexPath>?
// MARK: - Function
override func viewDidLoad() {
- super.viewDidLoad()
- screenshots.screenshots = loadCache()
+ super.viewDidLoad()
- let nib = NSNib(nibNamed: "ScreenshotCollectionViewItem", bundle: nil)
- collectionView.register(NSCollectionView.self, forItemWithIdentifier: "item")
- collectionView.register(nib, forItemWithIdentifier: "item")
+ let nib = NSNib(nibNamed: ScreenshotCollectionViewItem.nibName, bundle: nil)
+ collectionView.register(nib, forItemWithIdentifier: .item)
- screenshots.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
- collectionView.addObserver(self, forKeyPath: "selectionIndexPaths", context: nil)
+ screenshots.sortDescriptors = [NSSortDescriptor(key: #keyPath(ScreenshotInformation.creationDate), ascending: false)]
+ selectionObservation = collectionView.observe(\NSCollectionView.selectionIndexPaths) { [weak self] (_, _) in
+
+ guard let `self` = self else { return }
+
+ let selections = self.collectionView.selectionIndexPaths
+ let selectionIndexes = selections.reduce(into: IndexSet()) { $0.insert($1.item) }
+ self.screenshots.selectedIndexes = selectionIndexes
+ selectionIndexes.first.map { self.collectionSelectionDidChangeHandler?($0) }
+ }
collectionView.postsFrameChangedNotifications = true
let nc = NotificationCenter.default
let scrollView = collectionView.enclosingScrollView
- nc.addObserver(forName: .NSViewFrameDidChange, object: collectionView, queue: nil, using: viewFrameDidChange)
- nc.addObserver(forName: .NSScrollViewDidLiveScroll, object: collectionView.enclosingScrollView, queue: nil) { _ in
+ nc.addObserver(forName: NSView.frameDidChangeNotification, object: collectionView, queue: nil, using: viewFrameDidChange)
+ nc.addObserver(forName: NSScrollView.didLiveScrollNotification,
+ object: collectionView.enclosingScrollView, queue: nil) { _ in
+
let visibleItems = self.collectionView.indexPathsForVisibleItems()
self.collectionVisibleDidChangeHandler?(visibleItems)
}
- nc.addObserver(forName: .NSScrollViewWillStartLiveScroll, object: scrollView, queue: nil) { _ in
+ nc.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil) { _ in
+
self.inLiveScrolling = true
}
- nc.addObserver(forName: .NSScrollViewDidEndLiveScroll, object: scrollView, queue: nil) { _ in
+ nc.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { _ in
+
self.inLiveScrolling = false
}
+ nc.addObserver(forName: .didRegisterScreenshot,
+ object: nil,
+ queue: .main) { notification in
+
+ guard let url = notification.userInfo?[ScreenshotRegister.screenshotURLKey] as? URL else { return }
+
+ let info = ScreenshotInformation(url: url)
+
+ self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
+ let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
+ self.collectionView.selectionIndexPaths = set
+
+ self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
+ if UserDefaults.standard[.showsListWindowAtScreenshot] {
+
+ self.view.window?.makeKeyAndOrderFront(nil)
+ }
+ }
- viewFrameDidChange(nil)
+ collectionView.setDraggingSourceOperationMask([.move, .copy, .delete], forLocal: false)
- DispatchQueue.main
- .asyncAfter(deadline: .now() + 0.0001 ) { self.reloadData() }
- }
- override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- if let object = object as? NSCollectionView,
- object == collectionView {
- let selections = collectionView.selectionIndexPaths
- var selectionIndexes = IndexSet()
- selections.forEach { selectionIndexes.insert($0.item) }
- screenshots.selectedIndexes = selectionIndexes
- selectionIndexes.first.map { collectionSelectionDidChangeHandler?($0) }
- return
- }
+ viewFrameDidChange(nil)
- super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: self.reloadData)
}
+
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
+
guard let vc = segue.destinationController as? NSViewController else { return }
+
vc.representedObject = screenshots
}
- func registerImage(_ image: NSImage?) {
- image?.tiffRepresentation
- .flatMap { NSBitmapImageRep(data: $0) }
- .map { registerScreenshot($0, fromOnScreen: .zero) }
- }
- func registerScreenshot(_ image: NSBitmapImageRep, fromOnScreen: NSRect) {
- DispatchQueue(label: "Screenshot queue")
- .async {
- guard let data = image.representation(using: .JPEG, properties: [:])
- else { return }
- let url = self.screenshotSaveDirectoryURL
- .appendingPathComponent(self.dirName)
- .appendingPathExtension("jpg")
- let pathURL = FileManager.default.uniqueFileURL(url)
- do {
- try data.write(to: pathURL)
- }
- catch {
- print("Can not write image")
- return
- }
-
- DispatchQueue.main.async {
- let info = ScreenshotInformation(url: pathURL, version: self.cacheVersion(forUrl: pathURL))
-
- self.screenshotsController.insert(info, atArrangedObjectIndex: 0)
- let set: Set<IndexPath> = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
- self.collectionView.selectionIndexPaths = set
-
- self.collectionView.scrollToItems(at: set, scrollPosition: .nearestHorizontalEdge)
- if UserDefaults.standard.showsListWindowAtScreenshot {
- self.view.window?.makeKeyAndOrderFront(nil)
- }
- self.saveCache()
- }
- }
- }
func viewFrameDidChange(_ notification: Notification?) {
- maxZoom = self.maxZoom(width: collectionView.frame.size.width)
+
+ maxZoom = calcMaxZoom()
if zoom > maxZoom { zoom = maxZoom }
}
- fileprivate func realFromZoom(zoom: Double) -> CGFloat {
- if zoom < 0.5 { return CGFloat(ScreenshotListViewController.def * zoom * 0.6) }
- return CGFloat(ScreenshotListViewController.def * (0.8 * zoom * zoom * zoom + 0.2))
- }
- private func maxZoom(width: CGFloat) -> Double {
- let w = Double(width) - ScreenshotListViewController.leftMergin - ScreenshotListViewController.rightMergin
- if w < 240 { return w / ScreenshotListViewController.def / 0.6 }
- if w > 800 { return 1.0 }
- return pow((w / ScreenshotListViewController.def - 0.2) / 0.8, 1.0 / 3.0)
- }
-
- private func reloadData() {
- guard let f = try? FileManager.default.contentsOfDirectory(at: screenshotSaveDirectoryURL, includingPropertiesForKeys: nil) else {
- print("can not read list of screenshot directory")
- return
- }
- let imageTypes = NSImage.imageTypes()
- let ws = NSWorkspace.shared()
- var current = screenshots.screenshots
- let newFiles: [URL] = f.flatMap {
- guard let type = try? ws.type(ofFile: $0.path) else { return nil }
- if imageTypes.contains(type) {
- return $0
- }
- return nil
- }
-
- // なくなっているものを削除
- current = current.filter { newFiles.contains($0.url) }
+ /// 画像の大きさの変化が自然になるようにzoom値から画像サイズを計算
+ private func sizeFrom(zoom: Double) -> CGFloat {
- // 新しいものを追加
- let new: [ScreenshotInformation] = newFiles.flatMap { (url) in
- let index = current.index(where: { (info) -> Bool in
- url == info.url
- })
- if index == nil {
- return ScreenshotInformation(url: url)
- }
- return nil
- }
+ if zoom < 0.5 { return CGFloat(type(of: self).maxImageSize * zoom * 0.6) }
- screenshots.screenshots = current + new
+ return CGFloat(type(of: self).maxImageSize * (0.8 * zoom * zoom * zoom + 0.2))
+ }
+
+ /// ビューの幅に合わせたzoomの最大値を計算
+ private func calcMaxZoom() -> Double {
- collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
+ let effectiveWidth = Double(collectionView.frame.size.width) - type(of: self).leftMergin - type(of: self).rightMergin
- reloadHandler?()
- saveCache()
+ if effectiveWidth < 240 { return effectiveWidth / type(of: self).maxImageSize / 0.6 }
+ if effectiveWidth > 800 { return 1.0 }
+ return pow((effectiveWidth / type(of: self).maxImageSize - 0.2) / 0.8, 1.0 / 3.0)
}
- private func saveCache() {
- let data = NSKeyedArchiver.archivedData(withRootObject: screenshots.screenshots)
- do {
- try data.write(to: cachURL)
- }
- catch let e {
- print("Can not write cache: \(e)")
- }
- }
- private func loadCache() -> [ScreenshotInformation] {
- guard let data = try? Data(contentsOf: cachURL)
- else {
- print("can not load cach \(cachURL)")
- return []
- }
- guard let l = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data as NSData),
- let loaded = l as? [ScreenshotInformation]
- else {
- print("Can not decode \(cachURL)")
- return []
- }
+ private func reloadData() {
- return loaded
- }
-
- private func incrementCacheVersion(forUrl url: URL) {
- let infos = deletedPaths.filter { $0.url == url }
- if var info = infos.first {
- info.incrementVersion()
- }
- else {
- deletedPaths.append(CacheVersionInfo(url: url))
+ Promise<[ScreenshotInformation]>()
+ .complete {
+ Result(ScreenshotLoader(self.screenshotSaveDirectoryURL).merge(screenshots: []))
+ }
+ .future
+ .onSuccess { screenshots in
+
+ DispatchQueue.main.async {
+ self.screenshots.screenshots = screenshots
+
+ self.collectionView.selectionIndexPaths = [NSIndexPath(forItem: 0, inSection: 0) as IndexPath]
+
+ self.reloadHandler?()
+ }
}
}
- private func cacheVersion(forUrl url: URL) -> Int {
- return deletedPaths
- .filter { $0.url == url }
- .first?
- .version ?? 0
+}
+
+// MARK: - IBAction
+extension ScreenshotListViewController {
+
+ @IBAction func reloadContent(_ sender: AnyObject?) {
+
+ reloadData()
}
- // MARK: - IBAction
@IBAction func reloadData(_ sender: AnyObject?) {
+
reloadData()
}
- @IBAction func delete(_ sender: AnyObject?) {
- let posixPaths = selectionInformations
- .map { $0.url.path }
+
+ private func moveToTrash(_ urls: [URL]) {
+
+ let list = urls.map { $0.path }
.map { "(\"\($0)\" as POSIX file)" }
- let list = posixPaths.joined(separator: " , ")
+ .joined(separator: " , ")
let script = "tell application \"Finder\"\n"
- + " delete { \(list) }\n"
- + "end tell"
+ + " delete { \(list) }\n"
+ + "end tell"
+
guard let aps = NSAppleScript(source: script) else { return }
+
aps.executeAndReturnError(nil)
+ }
+
+ @IBAction func delete(_ sender: AnyObject?) {
+
+ let selectionURLs = selectionInformations.map { $0.url }
let selectionIndexes = screenshotsController.selectionIndexes
screenshotsController.remove(atArrangedObjectIndexes: selectionIndexes)
- selectionInformations.forEach { incrementCacheVersion(forUrl: $0.url) }
- saveCache()
reloadHandler?()
- guard var index = selectionIndexes.first,
- let newInfos = arrangedInformations
- else { return }
- if newInfos.count <= index { index = newInfos.count - 1 }
+ guard var index = selectionIndexes.first else { return }
+
+ if arrangedInformations.count <= index {
+
+ index = arrangedInformations.count - 1
+ }
collectionView.selectionIndexPaths = [NSIndexPath(forItem: index, inSection: 0) as IndexPath]
+
+ moveToTrash(selectionURLs)
}
+
@IBAction func revealInFinder(_ sender: AnyObject?) {
- guard let infos = arrangedInformations else { return }
- let urls = infos.map { $0.url }
- NSWorkspace.shared().activateFileViewerSelecting(urls)
+
+ let urls = selectionInformations.map { $0.url }
+ NSWorkspace.shared.activateFileViewerSelecting(urls)
}
}
extension ScreenshotListViewController: NSCollectionViewDelegateFlowLayout {
- func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
- let f = realFromZoom(zoom: zoom)
- return NSMakeSize(f, f)
+
+ func collectionView(_ collectionView: NSCollectionView,
+ layout collectionViewLayout: NSCollectionViewLayout,
+ sizeForItemAt indexPath: IndexPath) -> NSSize {
+
+ let size = sizeFrom(zoom: zoom)
+
+ return NSSize(width: size, height: size)
+ }
+
+ // Drag and Drop
+ func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
+
+ return true
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
+
+ return arrangedInformations[indexPath.item].url.absoluteURL as NSURL
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
+
+ indexPathsOfItemsBeingDragged = indexPaths
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
+
+ defer { indexPathsOfItemsBeingDragged = nil }
+
+ guard let dragged = indexPathsOfItemsBeingDragged else { return }
+ guard operation.contains(.move) || operation.contains(.delete) else { return }
+
+ var indexes = IndexSet()
+ dragged.forEach { indexes.insert($0.item) }
+
+ screenshotsController.remove(atArrangedObjectIndexes: indexes)
}
+
}
@available(OSX 10.12.2, *)
-fileprivate var kTouchBars:[Int: NSTouchBar] = [:]
+private var kTouchBars: [Int: NSTouchBar] = [:]
@available(OSX 10.12.2, *)
-fileprivate var kScrubbers:[Int: NSScrubber] = [:]
+private var kScrubbers: [Int: NSScrubber] = [:]
@available(OSX 10.12.2, *)
-fileprivate var kPickers:[Int: NSSharingServicePickerTouchBarItem] = [:]
+private var kPickers: [Int: NSSharingServicePickerTouchBarItem] = [:]
@available(OSX 10.12.2, *)
extension ScreenshotListViewController: NSTouchBarDelegate {
- static let ServicesItemIdentifier: NSTouchBarItemIdentifier = NSTouchBarItemIdentifier(rawValue: "com.masakih.sharingTouchBarItem")
- @IBOutlet var screenshotTouchBar: NSTouchBar! {
+ static let ServicesItemIdentifier: NSTouchBarItem.Identifier
+ = NSTouchBarItem.Identifier(rawValue: "com.masakih.sharingTouchBarItem")
+
+ @IBOutlet private var screenshotTouchBar: NSTouchBar! {
+
get { return kTouchBars[hashValue] }
set { kTouchBars[hashValue] = newValue }
}
- @IBOutlet var scrubber: NSScrubber! {
+
+ @IBOutlet private var scrubber: NSScrubber! {
+
get { return kScrubbers[hashValue] }
set { kScrubbers[hashValue] = newValue }
}
- @IBOutlet var sharingItem: NSSharingServicePickerTouchBarItem! {
+
+ @IBOutlet private var sharingItem: NSSharingServicePickerTouchBarItem! {
+
get { return kPickers[hashValue] }
set { kPickers[hashValue] = newValue }
}
override func makeTouchBar() -> NSTouchBar? {
- var array: NSArray = []
- Bundle.main.loadNibNamed("ScreenshotTouchBar", owner: self, topLevelObjects: &array)
- let identifiers = self.screenshotTouchBar.defaultItemIdentifiers + [ScreenshotListViewController.ServicesItemIdentifier]
+
+ Bundle.main.loadNibNamed(NSNib.Name("ScreenshotTouchBar"), owner: self, topLevelObjects: nil)
+ let identifiers = self.screenshotTouchBar.defaultItemIdentifiers
+ + [type(of: self).ServicesItemIdentifier]
screenshotTouchBar.defaultItemIdentifiers = identifiers
if collectionVisibleDidChangeHandler == nil {
- collectionVisibleDidChangeHandler = { [unowned self] (visible) in
- guard let objects = self.arrangedInformations else { return }
- guard let index = visible.first else { return }
- let middle = index.item + visible.count / 2
- if middle < objects.count - 1 {
+
+ collectionVisibleDidChangeHandler = { [weak self] in
+
+ guard let `self` = self else { return }
+ guard let index = $0.first else { return }
+
+ let middle = index.item + $0.count / 2
+
+ if middle < self.arrangedInformations.count - 1 {
+
self.scrubber.scrollItem(at: middle, to: .none)
}
}
}
+
if collectionSelectionDidChangeHandler == nil {
- collectionSelectionDidChangeHandler = { [unowned self] (index) in
- self.scrubber.selectedIndex = index
+
+ collectionSelectionDidChangeHandler = { [weak self] in
+
+ self?.scrubber.selectedIndex = $0
}
}
+
if reloadHandler == nil {
- reloadHandler = { [unowned self] _ in
- self.scrubber.reloadData()
+
+ reloadHandler = { [weak self] in
+
+ self?.scrubber.reloadData()
}
}
return screenshotTouchBar
}
- func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
- guard identifier == ScreenshotListViewController.ServicesItemIdentifier
- else { return nil }
+ func touchBar(_ touchBar: NSTouchBar,
+ makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
+
+ guard identifier == type(of: self).ServicesItemIdentifier else { return nil }
+
if sharingItem == nil {
+
sharingItem = NSSharingServicePickerTouchBarItem(identifier: identifier)
+
if let w = view.window?.windowController as? NSSharingServicePickerTouchBarItemDelegate {
+
sharingItem.delegate = w
}
}
@available(OSX 10.12.2, *)
extension ScreenshotListViewController: NSScrubberDataSource, NSScrubberDelegate {
+
func numberOfItems(for scrubber: NSScrubber) -> Int {
- return arrangedInformations?.count ?? 0
+
+ return arrangedInformations.count
}
+
func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
- guard let objects = arrangedInformations else { return NSScrubberImageItemView() }
- guard objects.count > index else { return NSScrubberImageItemView() }
- let info = objects[index]
+
+ guard case 0..<arrangedInformations.count = index else { return NSScrubberImageItemView() }
+
+ let info = arrangedInformations[index]
let itemView = NSScrubberImageItemView()
+
if let image = NSImage(contentsOf: info.url) {
+
itemView.image = image
}
}
func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int) {
+
let p = NSIndexPath(forItem: selectedIndex, inSection: 0) as IndexPath
+
collectionView.selectionIndexPaths = [p]
}
+
func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange) {
+
if inLiveScrolling { return }
+
let center = visibleRange.location + visibleRange.length / 2
let p = NSIndexPath(forItem: center, inSection: 0) as IndexPath
collectionView.scrollToItems(at: [p], scrollPosition: [.centeredVertically])