// // KMAnnotationViewController.swift // PDF Reader Pro // // Created by lxy on 2022/10/10. // import Cocoa enum KMAnnotationViewShowType: Int { case none case hidden } class KMAnnotationViewController: KMSideViewController { @IBOutlet weak var topView: NSView! @IBOutlet weak var filtrateButton: NSButton! @IBOutlet weak var moreButton: NSButton! @IBOutlet weak var markupTitleLabel: NSTextField! @IBOutlet weak var emptyView: NSView! @IBOutlet weak var bigTipLabel: NSTextField! @IBOutlet weak var tipLabel: NSTextField! @IBOutlet weak var annotationOutlineView: KMAnnotationOutlineView! var annotations: [KMBOTAAnnotationSection] = [] { didSet { self.annotationOutlineView.inputData = annotations self.updateExtempViewState() } } var screenAnnotations: [KMBOTAAnnotationSection] = [] { didSet { self.annotations = screenAnnotations } } //注释状态 var annotationShowState: KMAnnotationViewShowType = .none { didSet { self.reloadData() } } var allAnnotations: [CPDFAnnotation] = [] //localEvent var localEvent: Bool = false deinit { KMPrint("KMAnnotationViewController") self.removeNotification() } //MARK: View override func viewDidLoad() { super.viewDidLoad() self.setup() self.updateUI() self.updateLanguage() self.addNotification() self.reloadData() } func setup() { self.view.wantsLayer = true self.view.layer?.backgroundColor = NSColor.km_init(hex: "#F7F8FA").cgColor self.emptyView.backgroundColor(NSColor.km_init(hex: "#F7F8FA")) self.topView.wantsLayer = true self.topView.layer?.backgroundColor = NSColor.clear.cgColor self.annotationOutlineView.outlineView.doubleAction = #selector(tableViewDoubleAction) self.annotationOutlineView.delegate = self } func updateUI() { self.markupTitleLabel.font = NSFont.SFProTextSemiboldFont(14.0) self.markupTitleLabel.textColor = NSColor.km_init(hex: "#252629") self.bigTipLabel.font = NSFont.SFProTextRegularFont(14.0) self.bigTipLabel.textColor = NSColor.km_init(hex: "#616469") if self.annotationShowState == .none { } else { self.bigTipLabel.font = NSFont.SFProTextRegularFont(12.0) self.bigTipLabel.textColor = NSColor.km_init(hex: "#94989C") } } func updateLanguage() { self.markupTitleLabel.stringValue = NSLocalizedString("Annotation", comment: "") self.filtrateButton.toolTip = NSLocalizedString("Sort", comment: "") self.moreButton.toolTip = NSLocalizedString("More", comment: "") if self.annotationShowState == .none { self.bigTipLabel.stringValue = NSLocalizedString("No Annotations", comment: "") let title = NSLocalizedString("All annotations will be displayed here.", comment: "") let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.32 paragraphStyle.alignment = .center self.tipLabel.attributedStringValue = NSMutableAttributedString(string: title, attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle, .foregroundColor : NSColor.km_init(hex: "#94989C"), NSAttributedString.Key.font: NSFont.SFProTextRegularFont(12)]) } else { self.bigTipLabel.stringValue = NSLocalizedString("The Annotations are hidden", comment: "") self.tipLabel.stringValue = "" } } func addNotification() { NotificationCenter.default.addObserver(self, selector: #selector(documentPageCountChangedNotification), name: NSNotification.Name.init(rawValue: "CPDFDocumentPageCountChangedNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(CPDFListViewActiveAnnotationsChangeNotification), name: NSNotification.Name.init(rawValue: "CPDFListViewActiveAnnotationsChangeNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(CPDFListViewAnnotationsAttributeHasChangeNotification), name: NSNotification.Name.init(rawValue: "CPDFListViewAnnotationsAttributeHasChangeNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadDataAfter), name: NSNotification.Name.init(rawValue: "CPDFPageDidAddAnnotationNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadDataAfter), name: NSNotification.Name.init(rawValue: "CPDFPageDidRemoveAnnotationNotification"), object: nil) } func removeNotification() { NotificationCenter.default.removeObserver(self) } public func clear() { self.annotations.removeAll() self.annotationOutlineView.reloadData() } } //MARK: Data extension KMAnnotationViewController { @objc public func reloadData() { // self.reloadAnnotation() // self.annotationSort(sortArray: []) } func reloadAnnotation() { if self.listView != nil { var dataArray: [KMBOTAAnnotationSection] = [] var annotationArray: [CPDFAnnotation] = [] for i in 0 ..< self.pageCount() { var annotationItemArray: [KMBOTAAnnotationItem] = [] let page = self.pdfDocument()?.page(at: UInt(i)) let types = ["Highlight","Underline","Strikeout","Freehand","FreeText","Note","Square","Circle","Line","Stamp","Arrow","Image","Redact","Sign"] var pageAnnotations: [CPDFAnnotation] = KMOCToolClass.filterAnnotation(annotations: page!.annotations,types: types) as! [CPDFAnnotation] //添加签名注释 for annotation in page!.annotations { if annotation.isKind(of: CPDFSignatureAnnotation.self) { pageAnnotations.append(annotation) } } for annotation in pageAnnotations { if annotation.annotationShouldDisplay() == false { pageAnnotations.removeObject(annotation) } } //转换所有annotation类型 let section = KMBOTAAnnotationSection() for annotation in pageAnnotations { let item = KMBOTAAnnotationItem() item.section = section item.annotation = annotation item.index = Int(annotation.page.pageIndex()) annotationItemArray.append(item) } if annotationItemArray.count != 0 { section.annotations = annotationItemArray section.page = page section.isItemExpanded = true dataArray.append(section) } //添加所有annotation 用于筛选 annotationArray += pageAnnotations } //转换对象,用于数据显示 self.annotations = dataArray self.allAnnotations = annotationArray if self.annotations.count < 1 { self.filtrateButton.isEnabled = false } else { self.filtrateButton.isEnabled = true } } } func annotationSort(sortArray:[[Any]]) { if self.listView != nil { var typeArr: [Any] = [] var colorArr: [Any] = [] var authorArr: [Any] = [] let sud = UserDefaults.standard let typeData = sud.object(forKey: "KMNoteOutlineFilterSelectArray_Type" + (self.pdfDocument()?.documentURL.path ?? "")) as? Data if typeData != nil { typeArr = NSKeyedUnarchiver.unarchiveObject(with: typeData!) as! [Any] } let colorData = sud.object(forKey: "KMNoteOutlineFilterSelectArray_Color" + (self.pdfDocument()?.documentURL.path ?? "")) as? Data if colorData != nil { colorArr = NSKeyedUnarchiver.unarchiveObject(with: colorData!) as! [Any] } let authorData = sud.object(forKey: "KMNoteOutlineFilterSelectArray_Author" + (self.pdfDocument()?.documentURL.path ?? "")) as? Data if authorData != nil { authorArr = NSKeyedUnarchiver.unarchiveObject(with: authorData!) as! [Any] } if typeArr.count == 0 && colorArr.count == 0 && authorArr.count == 0 { self.filtrateButton.image = NSImage(named: "KMImageNameAnnotationsFiltrate") self.reloadAnnotation() } else { self.filtrateButton.image = NSImage(named: "icon_annotation_screening_select") var dataArray: [KMBOTAAnnotationSection] = [] for i in 0 ..< self.pageCount() { var annotationItemArray: [KMBOTAAnnotationItem] = [] let page = self.listView?.document?.page(at: UInt(i)) if page!.annotations.count > 0 { var filterAnnotations: [CPDFAnnotation] = page!.annotations if typeArr.count > 0 { filterAnnotations = (KMOCToolClass.filterAnnotation(annotations: filterAnnotations, types: typeArr) as! [CPDFAnnotation]) } if (colorArr.count > 0) { filterAnnotations = (KMOCToolClass.filterAnnotation(annotations: filterAnnotations,colors: colorArr) as! [CPDFAnnotation]) } if (authorArr.count > 0) { filterAnnotations = (KMOCToolClass.filterAnnotation(annotations: filterAnnotations,authors: authorArr) as! [CPDFAnnotation]) } let section = KMBOTAAnnotationSection() for annotation in filterAnnotations { let item = KMBOTAAnnotationItem() item.section = section item.annotation = annotation item.index = Int(page!.pageIndex()) annotationItemArray.append(item) } if annotationItemArray.count != 0 { section.annotations = annotationItemArray section.page = page section.isItemExpanded = true dataArray.append(section) } } } self.annotations = dataArray } } } } //MARK: reloadData extension KMAnnotationViewController { func updateExtempViewState() { if self.emptyView != nil { var hidden = false if self.annotationOutlineView.outlineView.numberOfRows != 0 { hidden = true } self.emptyView.isHidden = hidden self.annotationOutlineView.isHidden = !hidden //刷新 self.updateUI() self.updateLanguage() } } } //MARK: Notification extension KMAnnotationViewController { @objc public func reloadDataAfter() { if !localEvent { let rect = self.annotationOutlineView.outlineView.visibleRect self.reloadData() self.annotationOutlineView.outlineView.scrollToVisible(rect) } // localEvent = false } @objc func documentPageCountChangedNotification(notification: NSNotification) { if notification.object is CPDFDocument { let pdfdocument : CPDFDocument = notification.object as! CPDFDocument if pdfdocument.isEqual(self.listView?.document) { if !localEvent { self.reloadData() } // localEvent = false } } } @objc func CPDFListViewActiveAnnotationsChangeNotification(notification: NSNotification) { if notification.object is CPDFListView { let listView : CPDFListView = notification.object as! CPDFListView if listView.isEqual(self.listView) { if self.listView?.activeAnnotations.count == 0 { self.escButtonAction(Any.self) } else { if !localEvent { let tempAnnotations : [CPDFAnnotation] = self.listView?.activeAnnotations as! [CPDFAnnotation] var indexset = IndexSet() for annotation in tempAnnotations { if self.annotations.count > 0 { for section in self.annotations { for item in section.annotations! { if item.annotation == annotation { let index = self.annotationOutlineView.outlineView.row(forItem: item) indexset.insert(index) } } } } } if indexset.count != 0 && indexset.first != -1 { self.annotationOutlineView.outlineView.selectRowIndexes(indexset, byExtendingSelection: false) self.annotationOutlineView.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false) } } // localEvent = false } } } } @objc func CPDFListViewAnnotationsAttributeHasChangeNotification(notification: NSNotification) { if notification.object != nil { let dic = notification.object as? NSDictionary if dic?["keyPath"] as! String != CPDFAnnotationBoundsKey && dic?["keyPath"] as! String != CPDFAnnotationStartPointKey && dic?["keyPath"] as! String != CPDFAnnotationEndPointKey{ if dic?["object"] is CPDFAnnotation { let annotation : CPDFAnnotation = dic?["object"] as? CPDFAnnotation ?? CPDFAnnotation() for section in self.annotations { for (_, item) in section.annotations!.enumerated() { if item.annotation == annotation { let row = self.annotationOutlineView.outlineView.row(forItem: item) let indexSet = IndexSet.init(integer: row) self.annotationOutlineView.outlineView.noteHeightOfRows(withIndexesChanged: indexSet) self.annotationOutlineView.outlineView.reloadItem(item) break } } } } } } } } //MARK: Action extension KMAnnotationViewController { @IBAction func tableViewDoubleAction(_ sender: Any) { if self.annotationOutlineView.selectItems.count > 1 { return } let selectedRow = self.annotationOutlineView.outlineView.selectedRow if selectedRow >= 0 { let annotationItem = self.annotationOutlineView.outlineView.item(atRow: selectedRow) if (annotationItem is KMBOTAAnnotationItem) { let annotation = (annotationItem as! KMBOTAAnnotationItem).annotation self.listView?.go(to: annotation!.bounds, on: annotation!.page, animated: true) } } } @IBAction func moreButtonAction(_ sender: NSButton) { self.addMoreMenu(sender: sender) } @IBAction func filtrateButtonAction(_ sender: NSButton) { let menu = NSMenu() let annotationScreenView = KMAnnotationScreenCollectionView(frame: CGRect(x: 0, y: 0, width: 304, height: 296)) annotationScreenView.path = self.listView?.document.documentURL.path ?? "" annotationScreenView.annotations = self.allAnnotations annotationScreenView.applyAction = { [unowned self] (view, typeArray, colorArray, authArray) in menu.cancelTracking() self.annotationSort(sortArray: [typeArray, colorArray, authArray]) } annotationScreenView.cancelAction = { [unowned self] view in menu.cancelTracking() } let item = menu.addItem(withTitle: "", action: nil, keyEquivalent: "") item.target = self // item.representedObject = filterVC item.view = annotationScreenView menu.popUp(positioning: nil, at: CGPoint(x: -130, y: 30), in: sender) } @IBAction func deleteButtonAction(_ sender: Any) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("This will permanently remove all annotations. Are you sure to continue?", comment: "") alert.informativeText = NSLocalizedString("You cannot undo this operation.", comment: "") alert.addButton(withTitle: NSLocalizedString("Yes", comment: "")) alert.addButton(withTitle: NSLocalizedString("No", comment: "")) alert.beginSheetModal(for: self.view.window!, completionHandler: { result in if result == .OK { for i in 0 ..< self.pageCount() { let page = self.listView?.document?.page(at: UInt(i)) page?.removeAllAnnotations() } // let undoManager : UndoManager = self.mainWindowController.document?.undoManager ?? UndoManager() // undoManager.setActionName("") self.listView?.updateActiveAnnotations([CPDFAnnotation()]) self.listView?.setNeedsDisplayForVisiblePages() self.reloadData() } }) } // @IBAction func flattenButtonAction(_ sender: NSMenuItem) { let selects = sender.representedObject as! NSIndexSet var indexs : [Int] = [] for index in selects { indexs.append(index) } if selects.count == 1 { let index = selects.firstIndex let annotationItem: KMBOTAAnnotationItem = self.annotationOutlineView.outlineView.item(atRow: index) as! KMBOTAAnnotationItem if annotationItem.annotation != nil { if annotationItem.annotation!.contents?.lengthOfBytes(using: String.Encoding(rawValue: String.Encoding.utf16.rawValue)) ?? 0 > 0 { var content: String = annotationItem.annotation!.contents! let item: CPDFMarkupAnnotation = annotationItem.annotation! as? CPDFMarkupAnnotation ?? CPDFMarkupAnnotation() if item.markupText() != nil { KMPrint(item.markupText() as Any) content = content + "\n" + item.markupText() } let pasteBoard = NSPasteboard.general pasteBoard.clearContents() pasteBoard.setString(content, forType: .string) } } } } // @IBAction func exportItemAction(_ sender: Any) { let panel = NSSavePanel() panel.nameFieldStringValue = "\(NSLocalizedString("Untitled", comment: "")).xfdf" panel.isExtensionHidden = true let response = panel.runModal() if response == .OK { let url = panel.url let result = self.listView?.document.exportAnnotation(toXFDFPath: url!.path) ?? false if result { // NSWorkspace.shared.openFile(url!.path.deletingLastPathComponent) // NSWorkspace.shared.open(url!) let filePath = url!.path // 要打开的文件路径 let fileURL = URL(fileURLWithPath: filePath) let fileDirectoryURL = fileURL.deletingLastPathComponent() // 获取文件所在的文件夹路径 NSWorkspace.shared.activateFileViewerSelecting([fileDirectoryURL]) } } } // @IBAction func importItemAction(_ sender: Any) { let panel = NSOpenPanel() panel.allowsMultipleSelection = false panel.allowedFileTypes = ["xfdf"] panel.beginSheetModal(for: NSApp.mainWindow!) { response in if response == .OK { let openPath = panel.url?.path let result = self.listView?.document.importAnnotation(fromXFDFPath: openPath!) ?? false if result { self.listView?.setNeedsDisplayAnnotationViewForVisiblePages() self.reloadData() } } } } // @IBAction func deleteItemAction(_ sender: NSMenuItem) { let selects = sender.representedObject as! NSIndexSet var indexs : [KMBOTAAnnotationItem] = [] for index in selects { if self.annotationOutlineView.outlineView.item(atRow: index) is KMBOTAAnnotationItem { let annotationItem: KMBOTAAnnotationItem = self.annotationOutlineView.outlineView.item(atRow: index) as! KMBOTAAnnotationItem annotationItem.index = index indexs.append(annotationItem) } } indexs.sort(){$0.index! > $1.index!} self.deleteAnnotations(annotationItems: indexs) } // @IBAction func deleteAllAnonationAction(_ sender: NSMenuItem) { let alter = NSAlert() alter.alertStyle = NSAlert.Style.informational alter.messageText = NSLocalizedString("This will permanently remove all annotations. Are you sure to continue?", comment: "") alter.addButton(withTitle: NSLocalizedString("Yes", comment:"")) alter.addButton(withTitle: NSLocalizedString("No", comment:"")) let modlres = alter.runModal() if modlres == NSApplication.ModalResponse.alertFirstButtonReturn { // for i in 0 ..< self.listView.document.pageCount { // let page = self.listView.document.page(at: i) // for annotation in page!.annotations { // page?.removeAnnotation(annotation) // // } // } // self.listView.setNeedsDisplayForVisiblePages() // self.reloadData() var indexs : [KMBOTAAnnotationItem] = [] for section in self.annotations { indexs.append(contentsOf: section.annotations!) } indexs.sort(){$0.index! > $1.index!} self.deleteAnnotations(annotationItems: indexs) } } // @IBAction func escButtonAction(_ sender: Any) { self.cancelSelect() } // func cancelSelect() { self.annotationOutlineView.cancelSelect() } // func selectItem(index: Int) { self.annotationOutlineView.outlineView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false) self.annotationOutlineView.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false) } func updateListViewData(annotationItems: [KMBOTAAnnotationItem]) { if annotationItems.count > 0 { if annotationItems.count == 1 { let annotationItem = annotationItems.first! if annotationItem.annotation != nil { self.listView?.go(to: annotationItem.annotation!.bounds, on: annotationItem.annotation!.page, animated: true) } } var annotations: [CPDFAnnotation] = [] for item in annotationItems { if item.annotation != nil { annotations.append(item.annotation!) } } self.listView?.updateActiveAnnotations(annotations) self.listView?.setNeedsDisplayAnnotationViewForVisiblePages() } } } extension KMAnnotationViewController: KMAnnotationOutlineViewDelegate { func annotationOutlineView(_ outlineView: KMAnnotationOutlineView, rightMouseDownDidSelectView: NSView, evnet: NSEvent) { self.addRightMenuItem(view: rightMouseDownDidSelectView, event: evnet) } func annotationOutlineView(_ outlineView: KMAnnotationOutlineView, didReloadData: KMBOTAOutlineItem) { } func annotationOutlineView(_ outlineView: KMAnnotationOutlineView, didSelectItem: [KMBOTAAnnotationItem]) { self.localEvent = true self.updateListViewData(annotationItems: didSelectItem) self.localEvent = false } } //MARK: Menu extension KMAnnotationViewController : NSMenuDelegate, NSMenuItemValidation { @objc private func expandAllComments(sender:NSMenuItem) { self.annotationOutlineView.expandAllComments(item: sender) } @objc private func collapseAllComments(sender:NSMenuItem) { self.annotationOutlineView.collapseAllComments(item: sender) } // @objc private func expandAllComments(sender:NSMenuItem) { // if sender.tag == 0 { // self.annotationOutlineView.expandAllComments(item: sender) // } else if sender.tag == 1 { // self.annotationOutlineView.collapseAllComments(item: sender) // } else if sender.tag == 2 { // let alter = NSAlert() // alter.alertStyle = NSAlert.Style.informational // alter.messageText = NSLocalizedString("This will permanently remove all outlines. Are you sure to continue?", comment: "") // alter.addButton(withTitle: NSLocalizedString("Yes", comment:"")) // alter.addButton(withTitle: NSLocalizedString("No", comment:"")) // let modlres = alter.runModal() // if modlres == NSApplication.ModalResponse.alertFirstButtonReturn { // self.deleteAllAnonationAction(sender) // } // } // } func addRightMenuItem(view: NSView, event: NSEvent) { let menu = NSMenu() menu.delegate = self let selectedRowIndexes = self.annotationOutlineView.outlineView.selectedRowIndexes var menuItem = NSMenuItem() if selectedRowIndexes.count == 1 { let item: KMBOTAAnnotationItem = self.annotationOutlineView.outlineView.item(atRow: selectedRowIndexes.first!) as! KMBOTAAnnotationItem if item.annotation != nil { if item.annotation!.contents != nil { if item.annotation!.contents.count > 0 { menuItem = menu.addItem(withTitle: NSLocalizedString("Copy Text", comment: ""), action: #selector(flattenButtonAction), target: self)! menuItem.representedObject = selectedRowIndexes menu.addItem(NSMenuItem.separator()) } } } } menuItem = menu.addItem(withTitle: NSLocalizedString("Export Annotation", comment: ""), action: #selector(exportItemAction), target: self)! if self.annotationOutlineView.selectItems.count == 1 { menuItem = menu.addItem(withTitle: NSLocalizedString("Import Annotation", comment: ""), action: #selector(importItemAction), target: self)! } menu.addItem(NSMenuItem.separator()) menuItem = menu.addItem(withTitle: NSLocalizedString("Delete", comment: ""), action: #selector(deleteItemAction), target: self)! menuItem.representedObject = selectedRowIndexes menu.addItem(NSMenuItem.separator()) let point = view.convert(event.locationInWindow, from: nil) menu.popUp(positioning: nil, at: point, in: view) } func addMoreMenu(sender: NSView) { let moreMenu = NSMenu() _ = moreMenu.addItem(withTitle: NSLocalizedString("Expand All", comment: ""), action: #selector(expandAllComments), target: self, tag: 0) _ = moreMenu.addItem(withTitle: NSLocalizedString("Collapse All", comment: ""), action: #selector(collapseAllComments), target: self, tag: 1) // let soreItem = moreMenu.addItem(withTitle: NSLocalizedString("Sort", comment: ""), action: nil, target: self) // let soreMenu = NSMenu() // soreMenu.addItem(withTitle: NSLocalizedString("Page", comment: ""), action: #selector(expandAllComments), target: self, tag: 0) // soreMenu.addItem(withTitle: NSLocalizedString("Chronologically - ascending", comment: ""), action: #selector(expandAllComments), target: self, tag: 1) // soreMenu.addItem(withTitle: NSLocalizedString("Chronologically - reverse", comment: ""), action: #selector(expandAllComments), target: self, tag: 0) // soreItem?.submenu = soreMenu _ = moreMenu.addItem(withTitle: NSLocalizedString("Import Annotations", comment: ""), action: #selector(importItemAction), target: self) _ = moreMenu.addItem(withTitle: NSLocalizedString("Export Annotations to XFDF", comment: ""), action: #selector(exportItemAction), target: self) _ = moreMenu.addItem(withTitle: NSLocalizedString("Remove All Annotations", comment: ""), action: #selector(deleteAllAnonationAction), target: self) let rect = sender.convert(sender.bounds, to: self.view) moreMenu.popUp(positioning: nil, at: NSPoint(x: rect.origin.x, y: rect.origin.y-10), in: self.view) } func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { let action = menuItem.action if (action == #selector(undo)) { return self.listView?.undoManager?.canUndo ?? false } if (action == #selector(redo)) { return self.listView?.undoManager?.canRedo ?? false } if (action == #selector(flattenButtonAction)) { if self.annotationOutlineView.selectItems.count != 1 { return false } } if (action == #selector(deleteAllAnonationAction)) { if self.annotations.count == 0 { return false } } if action == #selector(exportItemAction) { if self.annotations.count == 0 { return false } } if action == #selector(exportItemAction) { if self.annotations.count == 0 { return false } } if (action == #selector(expandAllComments)) { var canExpand = false for row in 0..