KMThumbnailView.swift 15 KB

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