KMThumbnailView.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. //
  2. // KMThumbnailView.swift
  3. // PDF Master
  4. //
  5. // Created by tangchao on 2023/5/4.
  6. //
  7. import Cocoa
  8. @objc enum KMThumbnailViewDragInfoKey: Int {
  9. case dropOperation = 0
  10. case draggingInfo = 1
  11. }
  12. @objc protocol KMThumbnailViewDelegate : NSObjectProtocol {
  13. // layout
  14. @objc optional func thumbnailView(thumbanView: KMThumbnailView, minimumLineSpacingForSectionAt section: Int) -> CGFloat
  15. @objc optional func thumbnailView(thumbanView: KMThumbnailView, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
  16. @objc optional func thumbnailView(thumbanView: KMThumbnailView, insetForSectionAt section: Int) -> NSEdgeInsets
  17. @objc optional func thumbnailView(thumbanView: KMThumbnailView, sizeForItemAt indexpath: IndexPath) -> NSSize
  18. @objc optional func thumbnailView(thumbanView: KMThumbnailView, numberOfItemsInSection section: Int) -> Int
  19. @objc optional func thumbnailView(thumbanView: KMThumbnailView, itemForRepresentedObjectAt indexpath: IndexPath) -> NSCollectionViewItem
  20. // Drag & Drop
  21. @objc optional func thumbnailView(thumbanView: KMThumbnailView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexes: IndexSet)
  22. @objc optional func thumbnailView(thumbanView: KMThumbnailView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation)
  23. @objc optional func thumbnailView(thumbanView: KMThumbnailView, canPasteboardWriterForItemAt indexPath: IndexPath) -> Bool
  24. @objc optional func thumbnailView(thumbanView: KMThumbnailView, shouldPasteboardWriterForItemAt indexPath: IndexPath) -> Bool
  25. @objc optional func thumbnailView(thumbanView: KMThumbnailView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting?
  26. @objc optional func thumbnailView(thumbanView: KMThumbnailView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool
  27. @objc optional func thumbnailView(thumbanView: KMThumbnailView, shouldAcceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool
  28. // 本地拖拽
  29. @objc optional func thumbnailView(thumbanView: KMThumbnailView, didDrag dragedIndexPaths: [IndexPath], indexpath: IndexPath, dragInfo:[KMThumbnailViewDragInfoKey.RawValue:Any])
  30. // 外部拖拽
  31. @objc optional func thumbnailView(thumbanView: KMThumbnailView, didDragAddFiles files: [URL], indexpath: IndexPath)
  32. @objc optional func thumbnailView(thumbanView: KMThumbnailView, didSelectItemAt indexpath: IndexPath, object: AnyObject?)
  33. @objc optional func thumbnailView(thumbanView: KMThumbnailView, rightMouseDidClick indexpath: IndexPath, item: NSCollectionViewItem?, object: AnyObject?)
  34. }
  35. @objc class KMThumbnailView: NSView {
  36. open weak var delegate: KMThumbnailViewDelegate?
  37. internal let localForDraggedTypes = kKMLocalForDraggedTypes
  38. internal var dragedIndexPaths: [IndexPath] = []
  39. var kmAllowedFileTypes: [String]?
  40. override init(frame frameRect: NSRect) {
  41. super.init(frame: frameRect)
  42. self.initDefaultValue()
  43. self.initSubViews()
  44. }
  45. required public init?(coder: NSCoder) {
  46. super.init(coder: coder)
  47. self.initDefaultValue()
  48. self.initSubViews()
  49. }
  50. open var minimumLineSpacing: CGFloat = 0.0 {
  51. didSet {
  52. self.reloadData()
  53. }
  54. }
  55. open var minimumInteritemSpacing: CGFloat = 0.0 {
  56. didSet {
  57. self.reloadData()
  58. }
  59. }
  60. open var itemSize: NSSize = NSMakeSize(60, 80) {
  61. didSet {
  62. self.reloadData()
  63. }
  64. }
  65. open var sectionInset: NSEdgeInsets = NSEdgeInsetsZero {
  66. didSet {
  67. self.reloadData()
  68. }
  69. }
  70. open var numberOfSections: Int = 0 {
  71. didSet {
  72. self.reloadData()
  73. }
  74. }
  75. var selectionIndexPaths: Set<IndexPath> {
  76. get {
  77. return self.collectionView.selectionIndexPaths
  78. }
  79. set {
  80. var indexpaths: Set<IndexPath> = []
  81. for indexpath in newValue {
  82. if (indexpath.section >= self.collectionView.numberOfSections) {
  83. continue
  84. }
  85. if (indexpath.item >= self.collectionView.numberOfItems(inSection: indexpath.section)) {
  86. continue
  87. }
  88. indexpaths.insert(indexpath)
  89. }
  90. self.collectionView.selectionIndexPaths = indexpaths
  91. }
  92. }
  93. // MARK: - Publick Methods
  94. public func initDefaultValue() {
  95. self.collectionView.registerForDraggedTypes([self.localForDraggedTypes, .fileURL,.string,.pdf])
  96. }
  97. public func initSubViews() {
  98. self.addSubview(self.scrollView)
  99. self.scrollView.frame = self.bounds
  100. self.scrollView.autoresizingMask = [.width, .height]
  101. self.scrollView.documentView = self.collectionView
  102. }
  103. // MARK: - register ItemClass
  104. open func register(_ itemClass: AnyClass?) {
  105. guard let itemClass_ = itemClass else {
  106. return
  107. }
  108. self.register(itemClass_, forItemWithIdentifier: NSStringFromClass(itemClass_))
  109. }
  110. open func register(_ itemClass: AnyClass?, forItemWithIdentifier identifier: String) {
  111. self.collectionView.register(itemClass, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier))
  112. }
  113. // MARK: - 刷新UI
  114. public func reloadData(indexs: Set<IndexPath> = []) {
  115. if (indexs.count == 0) {
  116. if (Thread.isMainThread) {
  117. self.collectionView.reloadData()
  118. } else {
  119. DispatchQueue.main.async {
  120. self.collectionView.reloadData()
  121. }
  122. }
  123. } else {
  124. var indexpaths: Set<IndexPath> = []
  125. for index in indexs {
  126. if (index.section >= self.collectionView.numberOfSections) {
  127. continue
  128. }
  129. if (index.item >= self.collectionView.numberOfItems(inSection: index.section)) {
  130. continue
  131. }
  132. indexpaths.insert(index)
  133. }
  134. if (indexpaths.count == 0) {
  135. return
  136. }
  137. if (Thread.isMainThread) {
  138. self.collectionView.reloadItems(at: indexpaths)
  139. } else {
  140. DispatchQueue.main.async {
  141. self.collectionView.reloadItems(at: indexpaths)
  142. }
  143. }
  144. }
  145. }
  146. // MARK: - 属性 【懒加载】
  147. internal lazy var scrollView_: NSScrollView = {
  148. let view = NSScrollView()
  149. view.hasHorizontalScroller = true
  150. view.hasVerticalScroller = true
  151. view.autohidesScrollers = true
  152. view.minMagnification = 1.0
  153. view.scrollerStyle = .overlay
  154. view.wantsLayer = true
  155. view.layer?.backgroundColor = NSColor.clear.cgColor
  156. view.wantsLayer = true
  157. view.contentView.layer?.backgroundColor = .white
  158. return view
  159. }()
  160. var scrollView: NSScrollView {
  161. get {
  162. return self.scrollView_
  163. }
  164. }
  165. internal lazy var collectionView_: NSCollectionView = {
  166. let view = NSCollectionView()
  167. view.autoresizingMask = [.width, .height]
  168. let layout = NSCollectionViewFlowLayout()
  169. layout.sectionInset = NSEdgeInsetsMake(8, 15, 8, 15)
  170. layout.minimumLineSpacing = 0
  171. layout.minimumInteritemSpacing = 0
  172. view.collectionViewLayout = layout
  173. view.delegate = self
  174. view.dataSource = self
  175. view.register(NSCollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "KMPDFThumbnailItem"))
  176. view.isSelectable = true
  177. view.wantsLayer = true
  178. view.layer?.backgroundColor = NSColor(hex: "#F7F8FA").cgColor
  179. return view
  180. }()
  181. var collectionView: NSCollectionView {
  182. get {
  183. return self.collectionView_
  184. }
  185. }
  186. }
  187. // MARK: - NSCollectionViewDataSource, NSCollectionViewDelegate
  188. extension KMThumbnailView: NSCollectionViewDataSource {
  189. public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
  190. if let items = self.delegate?.thumbnailView?(thumbanView: self, numberOfItemsInSection: section) {
  191. return items
  192. }
  193. return self.numberOfSections
  194. }
  195. func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
  196. if let item = self.delegate?.thumbnailView?(thumbanView: self, itemForRepresentedObjectAt: indexPath) {
  197. return item
  198. }
  199. return NSCollectionViewItem()
  200. }
  201. }
  202. // MARK: - NSCollectionViewDelegate
  203. extension KMThumbnailView: NSCollectionViewDelegate {
  204. // func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set<IndexPath>) -> Set<IndexPath> {
  205. // return indexPaths
  206. // }
  207. func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set<IndexPath>) -> Set<IndexPath> {
  208. if let lastSelectedIndexPath = collectionView.selectionIndexPaths.first {
  209. if NSApp.currentEvent?.modifierFlags.contains(.shift) == true {
  210. // Shift 键按住,进行连续多选
  211. let selectedIndexPaths = collectionView.selectionIndexPaths
  212. var allIndexPaths = Set<IndexPath>(selectedIndexPaths)
  213. // 获取两个 IndexPath 之间的所有 IndexPath
  214. let startIndex = lastSelectedIndexPath.item
  215. let endIndex = indexPaths.first?.item ?? startIndex
  216. let range = startIndex < endIndex ? startIndex...endIndex : endIndex...startIndex
  217. for index in range {
  218. let indexPath = IndexPath(item: index, section: lastSelectedIndexPath.section)
  219. allIndexPaths.insert(indexPath)
  220. }
  221. return allIndexPaths
  222. }
  223. }
  224. return indexPaths
  225. }
  226. func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {}
  227. func collectionView(_ collectionView: NSCollectionView, shouldDeselectItemsAt indexPaths: Set<IndexPath>) -> Set<IndexPath> {
  228. return indexPaths
  229. }
  230. func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) {}
  231. func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set<IndexPath>, with event: NSEvent) -> Bool {
  232. if let can = self.delegate?.thumbnailView?(thumbanView: self, canDragItemsAt: indexPaths, with: event) {
  233. return can
  234. }
  235. return true
  236. }
  237. func collectionView(_ collectionView: NSCollectionView, writeItemsAt indexPaths: Set<IndexPath>, to pasteboard: NSPasteboard) -> Bool {
  238. let data: Data = try! NSKeyedArchiver.archivedData(withRootObject: indexPaths, requiringSecureCoding: true)
  239. pasteboard.declareTypes([self.localForDraggedTypes], owner: self)
  240. pasteboard.setData(data, forType: self.localForDraggedTypes)
  241. self.dragedIndexPaths.removeAll()
  242. for indexPath in indexPaths {
  243. self.dragedIndexPaths.append(indexPath)
  244. }
  245. return true
  246. }
  247. func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexes: IndexSet) {
  248. self.delegate?.thumbnailView?(thumbanView: self, draggingSession: session, willBeginAt: screenPoint, forItemsAt: indexes)
  249. }
  250. func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
  251. self.delegate?.thumbnailView?(thumbanView: self, draggingSession: session, endedAt: screenPoint, dragOperation: operation)
  252. }
  253. func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
  254. let pboard = draggingInfo.draggingPasteboard
  255. if (pboard.availableType(from: [self.localForDraggedTypes]) != nil) {
  256. return .move
  257. } else if ((pboard.availableType(from: [.fileURL])) != nil) {
  258. guard let pbItems = pboard.pasteboardItems else {
  259. return NSDragOperation(rawValue: 0)
  260. }
  261. guard let _allowedFileTypes = self.kmAllowedFileTypes else {
  262. return .generic
  263. }
  264. var hasValidFile = false
  265. for item in pbItems {
  266. guard let data = item.string(forType: .fileURL), let _url = URL(string: data) else {
  267. continue
  268. }
  269. let type = _url.pathExtension.lowercased()
  270. if (_allowedFileTypes.contains(type)) {
  271. hasValidFile = true
  272. break
  273. }
  274. }
  275. if (!hasValidFile) {
  276. return NSDragOperation(rawValue: 0)
  277. }
  278. }
  279. return .generic
  280. }
  281. func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool {
  282. if let should = self.delegate?.thumbnailView?(thumbanView: self, shouldAcceptDrop: draggingInfo, indexPath: indexPath, dropOperation: dropOperation), !should {
  283. return should
  284. }
  285. let pboard = draggingInfo.draggingPasteboard
  286. if (pboard.availableType(from: [self.localForDraggedTypes]) != nil) {
  287. let dragInfo = [
  288. KMThumbnailViewDragInfoKey.draggingInfo.rawValue : draggingInfo,
  289. KMThumbnailViewDragInfoKey.dropOperation.rawValue : dropOperation
  290. ] as [Int : Any]
  291. self.delegate?.thumbnailView?(thumbanView: self, didDrag: self.dragedIndexPaths, indexpath: indexPath, dragInfo: dragInfo)
  292. self.dragedIndexPaths.removeAll()
  293. return true
  294. } else if (pboard.availableType(from: [.localDraggedTypes]) != nil) {
  295. var _dragIndexpaths = Set<IndexPath>()
  296. draggingInfo.enumerateDraggingItems(
  297. options: NSDraggingItemEnumerationOptions.concurrent,
  298. for: collectionView,
  299. classes: [NSPasteboardItem.self],
  300. searchOptions: [:],
  301. using: {(draggingItem, idx, stop) in
  302. if let pasteboardItem = draggingItem.item as? NSPasteboardItem {
  303. do {
  304. if let indexPathData = pasteboardItem.data(forType: .localDraggedTypes), let _indexPath =
  305. try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(indexPathData) as? IndexPath {
  306. _dragIndexpaths.insert(_indexPath)
  307. }
  308. } catch {
  309. Swift.debugPrint("failed to unarchive indexPath for dropped photo item.")
  310. }
  311. }
  312. })
  313. let dragInfo = [
  314. KMThumbnailViewDragInfoKey.draggingInfo.rawValue : draggingInfo,
  315. KMThumbnailViewDragInfoKey.dropOperation.rawValue : dropOperation
  316. ] as [Int : Any]
  317. self.delegate?.thumbnailView?(thumbanView: self, didDrag: _dragIndexpaths.sorted(), indexpath: indexPath, dragInfo: dragInfo)
  318. self.dragedIndexPaths.removeAll()
  319. } else if ((pboard.availableType(from: [.fileURL])) != nil) {
  320. var array: [URL] = []
  321. for item: NSPasteboardItem in pboard.pasteboardItems! {
  322. let string: String = item.string(forType: NSPasteboard.PasteboardType.fileURL)!
  323. let url = NSURL(string: string)
  324. array.append(url! as URL)
  325. }
  326. self.delegate?.thumbnailView?(thumbanView: self, didDragAddFiles: array, indexpath: indexPath)
  327. return true
  328. }
  329. return false
  330. }
  331. }
  332. // MARK: - NSCollectionViewDelegateFlowLayout
  333. extension KMThumbnailView: NSCollectionViewDelegateFlowLayout {
  334. func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
  335. if let size_ = self.delegate?.thumbnailView?(thumbanView: self, sizeForItemAt: indexPath) {
  336. return size_
  337. }
  338. return self.itemSize
  339. }
  340. func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  341. if let minimumLineSpacing_ = self.delegate?.thumbnailView?(thumbanView: self, minimumLineSpacingForSectionAt: section) {
  342. return minimumLineSpacing_
  343. }
  344. return self.minimumLineSpacing
  345. }
  346. func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
  347. if let minimumInteritemSpacing_ = self.delegate?.thumbnailView?(thumbanView: self, minimumInteritemSpacingForSectionAt: section) {
  348. return minimumInteritemSpacing_
  349. }
  350. return self.minimumInteritemSpacing
  351. }
  352. func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, insetForSectionAt section: Int) -> NSEdgeInsets {
  353. if let inset = self.delegate?.thumbnailView?(thumbanView: self, insetForSectionAt: section) {
  354. return inset
  355. }
  356. return self.sectionInset
  357. }
  358. }