// // KMThumbnailView.swift // PDF Master // // Created by tangchao on 2023/5/4. // import Cocoa @objc enum KMThumbnailViewDragInfoKey: Int { case dropOperation = 0 case draggingInfo = 1 } @objc protocol KMThumbnailViewDelegate : NSObjectProtocol { // layout @objc optional func thumbnailView(thumbanView: KMThumbnailView, minimumLineSpacingForSectionAt section: Int) -> CGFloat @objc optional func thumbnailView(thumbanView: KMThumbnailView, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat @objc optional func thumbnailView(thumbanView: KMThumbnailView, insetForSectionAt section: Int) -> NSEdgeInsets @objc optional func thumbnailView(thumbanView: KMThumbnailView, sizeForItemAt indexpath: IndexPath) -> NSSize @objc optional func thumbnailView(thumbanView: KMThumbnailView, numberOfItemsInSection section: Int) -> Int @objc optional func thumbnailView(thumbanView: KMThumbnailView, itemForRepresentedObjectAt indexpath: IndexPath) -> NSCollectionViewItem // Drag & Drop @objc optional func thumbnailView(thumbanView: KMThumbnailView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexes: IndexSet) @objc optional func thumbnailView(thumbanView: KMThumbnailView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) @objc optional func thumbnailView(thumbanView: KMThumbnailView, canPasteboardWriterForItemAt indexPath: IndexPath) -> Bool @objc optional func thumbnailView(thumbanView: KMThumbnailView, shouldPasteboardWriterForItemAt indexPath: IndexPath) -> Bool @objc optional func thumbnailView(thumbanView: KMThumbnailView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? @objc optional func thumbnailView(thumbanView: KMThumbnailView, canDragItemsAt indexPaths: Set, with event: NSEvent) -> Bool @objc optional func thumbnailView(thumbanView: KMThumbnailView, shouldAcceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool // 本地拖拽 @objc optional func thumbnailView(thumbanView: KMThumbnailView, didDrag dragedIndexPaths: [IndexPath], indexpath: IndexPath, dragInfo:[KMThumbnailViewDragInfoKey.RawValue:Any]) // 外部拖拽 @objc optional func thumbnailView(thumbanView: KMThumbnailView, didDragAddFiles files: [URL], indexpath: IndexPath) @objc optional func thumbnailView(thumbanView: KMThumbnailView, didSelectItemAt indexpath: IndexPath, object: AnyObject?) @objc optional func thumbnailView(thumbanView: KMThumbnailView, rightMouseDidClick indexpath: IndexPath, item: NSCollectionViewItem?, object: AnyObject?) } @objc class KMThumbnailView: NSView { open weak var delegate: KMThumbnailViewDelegate? internal let localForDraggedTypes = kKMLocalForDraggedTypes internal var dragedIndexPaths: [IndexPath] = [] var kmAllowedFileTypes: [String]? override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.initDefaultValue() self.initSubViews() } required public init?(coder: NSCoder) { super.init(coder: coder) self.initDefaultValue() self.initSubViews() } open var minimumLineSpacing: CGFloat = 0.0 { didSet { self.reloadData() } } open var minimumInteritemSpacing: CGFloat = 0.0 { didSet { self.reloadData() } } open var itemSize: NSSize = NSMakeSize(60, 80) { didSet { self.reloadData() } } open var sectionInset: NSEdgeInsets = NSEdgeInsetsZero { didSet { self.reloadData() } } open var numberOfSections: Int = 0 { didSet { self.reloadData() } } var selectionIndexPaths: Set { get { return self.collectionView.selectionIndexPaths } set { var indexpaths: Set = [] for indexpath in newValue { if (indexpath.section >= self.collectionView.numberOfSections) { continue } if (indexpath.item >= self.collectionView.numberOfItems(inSection: indexpath.section)) { continue } indexpaths.insert(indexpath) } self.collectionView.selectionIndexPaths = indexpaths } } // MARK: - Publick Methods public func initDefaultValue() { self.collectionView.registerForDraggedTypes([self.localForDraggedTypes, .fileURL,.string,.pdf]) } public func initSubViews() { self.addSubview(self.scrollView) self.scrollView.frame = self.bounds self.scrollView.autoresizingMask = [.width, .height] self.scrollView.documentView = self.collectionView } // MARK: - register ItemClass open func register(_ itemClass: AnyClass?) { guard let itemClass_ = itemClass else { return } self.register(itemClass_, forItemWithIdentifier: NSStringFromClass(itemClass_)) } open func register(_ itemClass: AnyClass?, forItemWithIdentifier identifier: String) { self.collectionView.register(itemClass, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier)) } // MARK: - 刷新UI public func reloadData(indexs: Set = []) { if (indexs.count == 0) { if (Thread.isMainThread) { self.collectionView.reloadData() } else { DispatchQueue.main.async { self.collectionView.reloadData() } } } else { var indexpaths: Set = [] for index in indexs { if (index.section >= self.collectionView.numberOfSections) { continue } if (index.item >= self.collectionView.numberOfItems(inSection: index.section)) { continue } indexpaths.insert(index) } if (indexpaths.count == 0) { return } if (Thread.isMainThread) { self.collectionView.reloadItems(at: indexpaths) } else { DispatchQueue.main.async { self.collectionView.reloadItems(at: indexpaths) } } } } // MARK: - 属性 【懒加载】 internal lazy var scrollView_: NSScrollView = { let view = NSScrollView() view.hasHorizontalScroller = true view.hasVerticalScroller = true view.autohidesScrollers = true view.minMagnification = 1.0 view.scrollerStyle = .overlay view.wantsLayer = true view.layer?.backgroundColor = NSColor.clear.cgColor view.wantsLayer = true view.contentView.layer?.backgroundColor = .white return view }() var scrollView: NSScrollView { get { return self.scrollView_ } } internal lazy var collectionView_: NSCollectionView = { let view = NSCollectionView() view.autoresizingMask = [.width, .height] let layout = NSCollectionViewFlowLayout() layout.sectionInset = NSEdgeInsetsMake(8, 15, 8, 15) layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 view.collectionViewLayout = layout view.delegate = self view.dataSource = self view.register(NSCollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "KMPDFThumbnailItem")) view.isSelectable = true view.wantsLayer = true view.layer?.backgroundColor = NSColor(hex: "#F7F8FA").cgColor return view }() var collectionView: NSCollectionView { get { return self.collectionView_ } } } // MARK: - NSCollectionViewDataSource, NSCollectionViewDelegate extension KMThumbnailView: NSCollectionViewDataSource { public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { if let items = self.delegate?.thumbnailView?(thumbanView: self, numberOfItemsInSection: section) { return items } return self.numberOfSections } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { if let item = self.delegate?.thumbnailView?(thumbanView: self, itemForRepresentedObjectAt: indexPath) { return item } return NSCollectionViewItem() } } // MARK: - NSCollectionViewDelegate extension KMThumbnailView: NSCollectionViewDelegate { // func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set) -> Set { // return indexPaths // } func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set) -> Set { if let lastSelectedIndexPath = collectionView.selectionIndexPaths.first { if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { // Shift 键按住,进行连续多选 let selectedIndexPaths = collectionView.selectionIndexPaths var allIndexPaths = Set(selectedIndexPaths) // 获取两个 IndexPath 之间的所有 IndexPath let startIndex = lastSelectedIndexPath.item let endIndex = indexPaths.first?.item ?? startIndex let range = startIndex < endIndex ? startIndex...endIndex : endIndex...startIndex for index in range { let indexPath = IndexPath(item: index, section: lastSelectedIndexPath.section) allIndexPaths.insert(indexPath) } return allIndexPaths } } return indexPaths } func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) {} func collectionView(_ collectionView: NSCollectionView, shouldDeselectItemsAt indexPaths: Set) -> Set { return indexPaths } func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) {} func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set, with event: NSEvent) -> Bool { if let can = self.delegate?.thumbnailView?(thumbanView: self, canDragItemsAt: indexPaths, with: event) { return can } return true } func collectionView(_ collectionView: NSCollectionView, writeItemsAt indexPaths: Set, to pasteboard: NSPasteboard) -> Bool { let data: Data = try! NSKeyedArchiver.archivedData(withRootObject: indexPaths, requiringSecureCoding: true) pasteboard.declareTypes([self.localForDraggedTypes], owner: self) pasteboard.setData(data, forType: self.localForDraggedTypes) self.dragedIndexPaths.removeAll() for indexPath in indexPaths { self.dragedIndexPaths.append(indexPath) } return true } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexes: IndexSet) { self.delegate?.thumbnailView?(thumbanView: self, draggingSession: session, willBeginAt: screenPoint, forItemsAt: indexes) } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) { self.delegate?.thumbnailView?(thumbanView: self, draggingSession: session, endedAt: screenPoint, dragOperation: operation) } func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { let pboard = draggingInfo.draggingPasteboard if (pboard.availableType(from: [self.localForDraggedTypes]) != nil) { return .move } else if ((pboard.availableType(from: [.fileURL])) != nil) { guard let pbItems = pboard.pasteboardItems else { return NSDragOperation(rawValue: 0) } guard let _allowedFileTypes = self.kmAllowedFileTypes else { return .generic } var hasValidFile = false for item in pbItems { guard let data = item.string(forType: .fileURL), let _url = URL(string: data) else { continue } let type = _url.pathExtension.lowercased() if (_allowedFileTypes.contains(type)) { hasValidFile = true break } } if (!hasValidFile) { return NSDragOperation(rawValue: 0) } } return .generic } func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool { if let should = self.delegate?.thumbnailView?(thumbanView: self, shouldAcceptDrop: draggingInfo, indexPath: indexPath, dropOperation: dropOperation), !should { return should } let pboard = draggingInfo.draggingPasteboard if (pboard.availableType(from: [self.localForDraggedTypes]) != nil) { let dragInfo = [ KMThumbnailViewDragInfoKey.draggingInfo.rawValue : draggingInfo, KMThumbnailViewDragInfoKey.dropOperation.rawValue : dropOperation ] as [Int : Any] self.delegate?.thumbnailView?(thumbanView: self, didDrag: self.dragedIndexPaths, indexpath: indexPath, dragInfo: dragInfo) self.dragedIndexPaths.removeAll() return true } else if (pboard.availableType(from: [.localDraggedTypes]) != nil) { var _dragIndexpaths = Set() draggingInfo.enumerateDraggingItems( options: NSDraggingItemEnumerationOptions.concurrent, for: collectionView, classes: [NSPasteboardItem.self], searchOptions: [:], using: {(draggingItem, idx, stop) in if let pasteboardItem = draggingItem.item as? NSPasteboardItem { do { if let indexPathData = pasteboardItem.data(forType: .localDraggedTypes), let _indexPath = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(indexPathData) as? IndexPath { _dragIndexpaths.insert(_indexPath) } } catch { Swift.debugPrint("failed to unarchive indexPath for dropped photo item.") } } }) let dragInfo = [ KMThumbnailViewDragInfoKey.draggingInfo.rawValue : draggingInfo, KMThumbnailViewDragInfoKey.dropOperation.rawValue : dropOperation ] as [Int : Any] self.delegate?.thumbnailView?(thumbanView: self, didDrag: _dragIndexpaths.sorted(), indexpath: indexPath, dragInfo: dragInfo) self.dragedIndexPaths.removeAll() } else if ((pboard.availableType(from: [.fileURL])) != nil) { var array: [URL] = [] for item: NSPasteboardItem in pboard.pasteboardItems! { let string: String = item.string(forType: NSPasteboard.PasteboardType.fileURL)! let url = NSURL(string: string) array.append(url! as URL) } self.delegate?.thumbnailView?(thumbanView: self, didDragAddFiles: array, indexpath: indexPath) return true } return false } } // MARK: - NSCollectionViewDelegateFlowLayout extension KMThumbnailView: NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { if let size_ = self.delegate?.thumbnailView?(thumbanView: self, sizeForItemAt: indexPath) { return size_ } return self.itemSize } func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { if let minimumLineSpacing_ = self.delegate?.thumbnailView?(thumbanView: self, minimumLineSpacingForSectionAt: section) { return minimumLineSpacing_ } return self.minimumLineSpacing } func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { if let minimumInteritemSpacing_ = self.delegate?.thumbnailView?(thumbanView: self, minimumInteritemSpacingForSectionAt: section) { return minimumInteritemSpacing_ } return self.minimumInteritemSpacing } func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, insetForSectionAt section: Int) -> NSEdgeInsets { if let inset = self.delegate?.thumbnailView?(thumbanView: self, insetForSectionAt: section) { return inset } return self.sectionInset } }