KMThumbnailView.swift 16 KB

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