// // KMNThumbnailBaseViewController.swift // PDF Reader Pro // // Created by 丁林圭 on 2024/10/21. // import Cocoa import KMComponentLibrary @objc protocol KMNThumbnailBaseViewDelegate: AnyObject { @objc optional func clickThumbnailViewControlle(pageEditVC:KMNThumbnailBaseViewController?,currentIndex:Int) @objc optional func insertPDFThumbnailViewControlle(pageEditVC:KMNThumbnailBaseViewController?,pdfDocment:CPDFDocument?) @objc optional func changeIndexPathsThumbnailViewControlle(pageEditVC:KMNThumbnailBaseViewController?,selectionIndexPaths: Set,selectionStrings:String ) } enum KMNThumbnailChoosePageStyle: Int { case odd case even case horizontal case vertical case allPage case custom } internal let kmnThumLocalForDraggedTypes = NSPasteboard.PasteboardType(rawValue: "kmnThumLocalForDraggedTypes") let topThumOffset: CGFloat = 12.0 // 缩图顶部高度 let infoThumTitleHeight: CGFloat = 16.0 //文字高度 let infoThumTitleBottom: CGFloat = 16.0 // 底部高度 let ThumbnailMenuIdentifier_Copy = "ThumbnailMenuIdentifier_Copy" let ThumbnailMenuIdentifier_Cut = "ThumbnailMenuIdentifier_Cut" let ThumbnailMenuIdentifier_Paste = "ThumbnailMenuIdentifier_Paste" let ThumbnailMenuIdentifier_Delete = "ThumbnailMenuIdentifier_Delete" let ThumbnailMenuIdentifier_RotateRight = "ThumbnailMenuIdentifier_RotateRight" let ThumbnailMenuIdentifier_RotateLeft = "ThumbnailMenuIdentifier_RotateLeft" let ThumbnailMenuIdentifier_InsertFile = "ThumbnailMenuIdentifier_InsertFile" let ThumbnailMenuIdentifier_InsertBlank = "ThumbnailMenuIdentifier_InsertBlank" let ThumbnailMenuIdentifier_Replace = "ThumbnailMenuIdentifier_Replace" let ThumbnailMenuIdentifier_Share = "ThumbnailMenuIdentifier_Share" let ThumbnailMenuIdentifier_Export = "ThumbnailMenuIdentifier_Export" let ThumbnailMenuIdentifier_PastNull = "ThumbnailMenuIdentifier_PastNull" let ThumbnailMenuIdentifier_FileShowSize = "ThumbnailMenuIdentifier_FileShowSize" struct ThumbnailMenuStruct { var menuitems: [ComponentMenuitemProperty] var viewHeight: CGFloat // 不可变属性 } class KMNThumbnailBaseViewController: KMNBaseViewController,NSCollectionViewDelegate, NSCollectionViewDataSource,NSCollectionViewDelegateFlowLayout { let maxCellHeight: CGFloat = 320.0 - infoThumTitleBottom - infoThumTitleHeight - topThumOffset * 2 let minCellHeight: CGFloat = 80.0 - infoThumTitleBottom - infoThumTitleHeight - topThumOffset - topThumOffset * 2 weak open var thumbnailBaseViewDelegate: KMNThumbnailBaseViewDelegate? @IBOutlet var backViewBox: NSBox! @IBOutlet var scrollView: NSScrollView! @IBOutlet var collectionView: KMNThumbnailCollectionView! @IBOutlet var flowLayout: KMNThumCustomCollectionViewFlowLayout! private var currentDocument:CPDFDocument? private var isChangeIndexPaths = false var lockedFiles: [URL] = [] var filePromiseQueue: OperationQueue = { let queue = OperationQueue() return queue }() fileprivate var dragFilePath: String? fileprivate var dragFlag = false public var thumbnails:[KMNThumbnail] = [] public var dragLocalityPages: [CPDFPage] = [] public var currentUndoManager:UndoManager? public var showDocument: CPDFDocument? { return currentDocument } public var changeDocument:CPDFDocument? { didSet { if(changeDocument != nil && changeDocument != currentDocument) { currentDocument = changeDocument refreshDatas() collectionView.reloadData() } } } public var thumbnailChoosePageStyle:KMNThumbnailChoosePageStyle = .custom { didSet { var tSelectionIndexPaths: Set = [] let pageCount = currentDocument?.pageCount ?? 0 switch thumbnailChoosePageStyle { case .even: for i in 0 ..< pageCount { if(i % 2 == 0) { tSelectionIndexPaths.insert(IndexPath(item: Int(i), section: 0)) } } case .odd: for i in 0 ..< pageCount { if(i % 2 != 0) { tSelectionIndexPaths.insert(IndexPath(item: Int(i), section: 0)) } } case .allPage: for i in 0 ..< pageCount { tSelectionIndexPaths.insert(IndexPath(item: Int(i), section: 0)) } case .vertical: for i in 0 ..< pageCount { let page = showDocument?.page(at: i) if(page != nil) { if(page!.rotation % 180 != 0) { tSelectionIndexPaths.insert(IndexPath(item: Int(i), section: 0)) } } } case .horizontal: for i in 0 ..< pageCount { let page = showDocument?.page(at: i) if(page != nil) { if(page!.rotation % 180 == 0) { tSelectionIndexPaths.insert(IndexPath(item: Int(i), section: 0)) } } } default: break } isChangeIndexPaths = true collectionView.selectionIndexPaths = tSelectionIndexPaths isChangeIndexPaths = false } } var selectionIndexPaths: Set = [] { didSet { var indexpaths: Set = [] for indexpath in selectionIndexPaths { if (indexpath.section >= collectionView.numberOfSections) { continue } if indexpath.section < 0 { continue } if (indexpath.item >= collectionView.numberOfItems(inSection: indexpath.section)) { continue } if indexpath.item < 0 { continue } indexpaths.insert(indexpath) } collectionView.selectionIndexPaths = indexpaths if(indexpaths.count > 0) { let firstIndexPath = indexpaths.first let itemFrame = collectionView.frameForItem(at: firstIndexPath!.item) let itemFrameInCollectionView = collectionView.convert(itemFrame, to: self.view) if collectionView.bounds.contains(itemFrameInCollectionView) { } else { collectionView.scrollToItems(at: indexpaths, scrollPosition: .bottom) } } } } public var isShowPageSize:Bool = false { didSet { if oldValue != isShowPageSize { var pageSize = pageThumbnailSize if(isShowPageSize) { pageSize.height += infoThumTitleHeight } else { pageSize.height -= infoThumTitleHeight } pageThumbnailSize = pageSize collectionView.reloadData() } } } public var pageThumbnailSize:CGSize = CGSizeMake(185.0, 260) { didSet { collectionView.reloadData() } } public let defaultItemSize = NSMakeSize(185.0, 260) deinit { thumbnailBaseViewDelegate = nil KMPrint("KMNThumbnailBaseViewController deinit.") } init(_ document: CPDFDocument?) { super.init(nibName: "KMNThumbnailBaseViewController", bundle: nil) currentDocument = document } init(_ filePath: String,password:String?) { super.init(nibName: "KMNThumbnailBaseViewController", bundle: nil) let document = CPDFDocument.init(url: URL(fileURLWithPath: filePath)) if password != nil { document?.unlock(withPassword: password as String?) } if document?.allowsCopying == false || document?.allowsPrinting == false { exitCurrentView() } else { currentDocument = document } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() collectionView.delegate = self collectionView.dataSource = self collectionView.isSelectable = true //支持拖拽需设置未True collectionView.allowsMultipleSelection = true collectionView.enclosingScrollView?.hasVerticalScroller = false collectionView.enclosingScrollView?.hasHorizontalScroller = false scrollView.scrollerStyle = .overlay collectionView.register(KMNThumbnailCollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "thumbnailCollectionViewItem")) collectionView.registerForDraggedTypes([.fileURL,.string,.pdf]) collectionView.setDraggingSourceOperationMask(.every, forLocal: false) collectionView.setDraggingSourceOperationMask(.every, forLocal: true) collectionView.collectionSelectChanges = {[weak self] in if self?.isChangeIndexPaths == false { let indexpathsz = self?.collectionView.selectionIndexPaths let dex:IndexSet = KMNTools.indexpathsToIndexs(indexpaths: indexpathsz ?? []) let selectedIndexPathsString = KMNTools.parseIndexSet(indexSet: dex) self?.thumbnailBaseViewDelegate?.changeIndexPathsThumbnailViewControlle?(pageEditVC: self, selectionIndexPaths: indexpathsz ?? [], selectionStrings: selectedIndexPathsString) } } refreshDatas() } public func exitCurrentView() { let minIndexPath = selectionIndexPaths.max(by: { $0.item < $1.item }) thumbnailBaseViewDelegate?.clickThumbnailViewControlle?(pageEditVC: self, currentIndex: minIndexPath?.item ?? 0) } public func supportDragFileTypes()->[String] { let supportFiles = KMNConvertTool.pdfExtensions + KMConvertPDFManager.supportFileType() return supportFiles } public func refreshDatas() { thumbnails = [] if currentDocument != nil { for i in 0 ... currentDocument!.pageCount - 1 { let thumbnail = KMNThumbnail.init(document: currentDocument!, currentPageIndex: Int(i)) thumbnails.append(thumbnail) } } } public func reloadDataDatas() { refreshDatas() collectionView.reloadData() } // MARK: - private public func clickMenu(point:NSPoint)->ThumbnailMenuStruct { let copyPages: [CPDFPage] = KMNThumbnailManager.manager.copyPages let selectedIndexPaths = collectionView.selectionIndexPaths var viewHeight: CGFloat = 8 var menuItemArr: [ComponentMenuitemProperty] = [] var items: [(String, String)] = [] if selectedIndexPaths.count > 0 { items.append(("Copy", ThumbnailMenuIdentifier_Copy)) items.append(("Cut", ThumbnailMenuIdentifier_Cut)) if(copyPages.count > 0) { items.append(("Paste", ThumbnailMenuIdentifier_Paste)) } items.append(("Delete", ThumbnailMenuIdentifier_Delete)) items.append(("", "")) items.append(("90° CW", ThumbnailMenuIdentifier_RotateRight)) items.append(("90° CCW", ThumbnailMenuIdentifier_RotateLeft)) items.append(("", "")) if(selectedIndexPaths.count == 1) { items.append(("Insert File", ThumbnailMenuIdentifier_InsertFile)) items.append(("Insert a Blank Page", ThumbnailMenuIdentifier_InsertBlank)) items.append(("Replace", ThumbnailMenuIdentifier_Replace)) } items.append(("Extract", ThumbnailMenuIdentifier_Export)) items.append(("Share", ThumbnailMenuIdentifier_Share)) items.append(("", "")) items.append(("Display Page Size", ThumbnailMenuIdentifier_FileShowSize)) } else { if(copyPages.count > 0) { items.append(("Paste", ThumbnailMenuIdentifier_Paste)) items.append(("", "")) } else { items.append(("Paste", ThumbnailMenuIdentifier_PastNull)) items.append(("", "")) } items.append(("Display Page Size", ThumbnailMenuIdentifier_FileShowSize)) } for (i, value) in items { if value.count == 0 { let property: ComponentMenuitemProperty = ComponentMenuitemProperty.divider() menuItemArr.append(property) viewHeight += 8 } else { var isDisabled = false if(value == ThumbnailMenuIdentifier_PastNull) { isDisabled = true } let properties_Menuitem: ComponentMenuitemProperty = ComponentMenuitemProperty(multipleSelect: false, itemSelected: false, isDisabled: isDisabled, keyEquivalent: nil, text: KMLocalizedString(i), identifier: value,representedObject: point) if value == ThumbnailMenuIdentifier_Copy { properties_Menuitem.keyEquivalent = "⌘ C" } else if value == ThumbnailMenuIdentifier_Cut { properties_Menuitem.keyEquivalent = "⌘ x" } else if value == ThumbnailMenuIdentifier_Paste { properties_Menuitem.keyEquivalent = "⌘ v" } else if value == ThumbnailMenuIdentifier_PastNull { properties_Menuitem.keyEquivalent = "⌘ v" } else if value == ThumbnailMenuIdentifier_Delete { properties_Menuitem.keyEquivalent = "⌘ " + String(Unicode.Scalar(NSBackspaceCharacter)!) } else if value == ThumbnailMenuIdentifier_RotateRight { properties_Menuitem.keyEquivalent = "⌥ ⌘ R" } else if value == ThumbnailMenuIdentifier_RotateLeft { properties_Menuitem.keyEquivalent = "⌥ ⌘ L" } if(value == ThumbnailMenuIdentifier_FileShowSize && isShowPageSize) { properties_Menuitem.righticon = NSImage(named: "KMNImageNameMenuSelect") } menuItemArr.append(properties_Menuitem) viewHeight += 36 } } let menuStruct = ThumbnailMenuStruct(menuitems: menuItemArr, viewHeight: viewHeight) return menuStruct } // MARK: - NSCollectionViewDataSource func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return thumbnails.count } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let item: KMNThumbnailCollectionViewItem = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "thumbnailCollectionViewItem"), for: indexPath) as! KMNThumbnailCollectionViewItem item.isShowFileSize = isShowPageSize item.doubleClickBack = { [weak self] in self?.thumbnailBaseViewDelegate?.clickThumbnailViewControlle?(pageEditVC: self, currentIndex: indexPath.item) } item.thumbnailMode = thumbnails[indexPath.item] return item } func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { if isChangeIndexPaths == false { let indexpathsz = collectionView.selectionIndexPaths let dex:IndexSet = KMNTools.indexpathsToIndexs(indexpaths: indexpathsz) let selectedIndexPathsString = KMNTools.parseIndexSet(indexSet: dex) thumbnailBaseViewDelegate?.changeIndexPathsThumbnailViewControlle?(pageEditVC: self, selectionIndexPaths: indexpathsz, selectionStrings: selectedIndexPathsString) } } func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { if isChangeIndexPaths == false { let indexpathsz = self.collectionView.selectionIndexPaths let dex:IndexSet = KMNTools.indexpathsToIndexs(indexpaths: indexpathsz) let selectedIndexPathsString = KMNTools.parseIndexSet(indexSet: dex) thumbnailBaseViewDelegate?.changeIndexPathsThumbnailViewControlle?(pageEditVC: self, selectionIndexPaths: indexpathsz, selectionStrings: selectedIndexPathsString) } } // MARK: - NSCollectionViewDelegateFlowLayout func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { return pageThumbnailSize } func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 16.0 } func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return 24.0 } public func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, insetForSectionAt section: Int) -> NSEdgeInsets { return NSEdgeInsetsMake(24.0, 24.0, 24.0, 24.0) } //MARK: - NSCollectionViewDelegate func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { var provider: NSFilePromiseProvider? // 创建数据提供者 let fileExtension = "pdf" if #available(macOS 11.0, *) { if let typeIdentifier = UTType(filenameExtension: fileExtension) { provider = KMFilePromiseProvider(fileType: typeIdentifier.identifier, delegate: self) } } else { if let typeIdentifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil) { provider = KMFilePromiseProvider(fileType: typeIdentifier.takeRetainedValue() as String, delegate: self) } } do { if let _url = showDocument?.documentURL { let data = try NSKeyedArchiver.archivedData(withRootObject: indexPath, requiringSecureCoding: false) provider?.userInfo = [KMFilePromiseProvider.UserInfoKeys.urlKey: _url, KMFilePromiseProvider.UserInfoKeys.indexPathKey: data] } else { let data = try NSKeyedArchiver.archivedData(withRootObject: indexPath, requiringSecureCoding: false) provider?.userInfo = [KMFilePromiseProvider.UserInfoKeys.indexPathKey: data] } } catch { fatalError("failed to archive indexPath to pasteboard") } return provider } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set) { let sortedIndexPaths = indexPaths.sorted { (ip1, ip2) -> Bool in if ip1.section == ip2.section { return ip1.item < ip2.item } return ip1.section < ip2.section } dragLocalityPages = [] for fromIndexPath in sortedIndexPaths { let page = thumbnails[fromIndexPath.item].thumbnaiPage if(page != nil) { dragLocalityPages.append(page!) } } var docmentName = currentDocument?.documentURL.lastPathComponent.deletingPathExtension ?? "" let pagesName = indexPaths.count > 1 ? " pages" : " page" var tFileName = pagesName + KMNTools.parseIndexPathsSet(indexSets: collectionView.selectionIndexPaths) if tFileName.count > 50 { tFileName = String(tFileName.prefix(50)) } var cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! cachesDir = cachesDir.appendingPathComponent("PageEdit_Pasteboard") let fileManager = FileManager.default if !fileManager.fileExists(atPath: cachesDir.path) { try? FileManager.default.createDirectory(atPath: cachesDir.path, withIntermediateDirectories: true, attributes: nil) } docmentName = "\(docmentName)\(tFileName)" if docmentName.count > 50 { docmentName = String(docmentName.prefix(50)) } // 将拖拽的page插入临时路径(文档) var indexs = IndexSet() for indexpath in indexPaths { indexs.insert(indexpath.item) } // 重置拖拽标识 self.dragFlag = false if (indexs.count > 0) { let writePDFDocument = CPDFDocument() writePDFDocument?.importPages(indexs, from: self.showDocument, at: 0) let filePathURL = cachesDir.appendingPathComponent(docmentName).appendingPathExtension("pdf") let success = writePDFDocument?.write(to: filePathURL, isSaveFontSubset:false) if(success == true) { self.dragFilePath = filePathURL.path } } } func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { let pboard = draggingInfo.draggingPasteboard if dragLocalityPages.count != 0 { if proposedDropOperation.pointee == .on { proposedDropOperation.pointee = .before } return .move } else if ((pboard.availableType(from: [.fileURL])) != nil) { guard let pbItems = pboard.pasteboardItems else { return NSDragOperation(rawValue: 0) } 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 (supportDragFileTypes().contains(type)) { hasValidFile = true break } } if (!hasValidFile) { return NSDragOperation(rawValue: 0) } } return .copy } func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool { let pboard = draggingInfo.draggingPasteboard if dragLocalityPages.count != 0 { movePages(dragPages: dragLocalityPages, destinationDex: indexPath.item) return true } else if ((pboard.availableType(from: [.fileURL])) != nil) { let index = indexPath.item guard let pbItems = pboard.pasteboardItems else { return false } //获取url var fileNames: [String] = [] for item in pbItems { guard let data = item.string(forType: .fileURL), let url = URL(string: data) else { continue } let type = url.pathExtension.lowercased() if (supportDragFileTypes().contains(type)) { if(FileManager.default.fileExists(atPath: url.path)) { fileNames.append(url.path) } } } if(fileNames.count > 0) { insertFromFilePath(fileNames: fileNames, formDex: 0, indexDex: UInt(index), selectIndexs: []) { newSelectIndexs in } return true } } return false } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) { dragLocalityPages = [] } } // MARK: - NSFilePromiseProviderDelegate extension KMNThumbnailBaseViewController: NSFilePromiseProviderDelegate { func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue { return self.filePromiseQueue } func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) { do { /** Copy the file to the location provided to you. You always do a copy, not a move. It's important you call the completion handler. */ if let _urlString = dragFilePath, !dragFlag { dragFlag = true try FileManager.default.copyItem(at: URL(fileURLWithPath: _urlString), to: url) } completionHandler(nil) } catch let error { OperationQueue.main.addOperation { if let win = self.view.window { self.presentError(error, modalFor: win, delegate: nil, didPresent: nil, contextInfo: nil) } } completionHandler(error) } } func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String { var fileName: String = "Untitle" if let _string = showDocument?.documentURL.deletingPathExtension().lastPathComponent { fileName = _string } fileName.append(" pages") var indexs = IndexSet() for page in dragLocalityPages { indexs.insert(Int(page.pageIndex())) } fileName.append(" ") fileName.append(KMPageRangeTools.newParseSelectedIndexs(selectedIndex: indexs.sorted())) fileName.append(".pdf") return fileName } }