// // KMLeftSideViewController+Outline.swift // PDF Master // // Created by tangchao on 2023/12/22. // import Foundation enum KMOutlineViewMenuItemTag: Int { case addEntry = 0 case addChild case addAunt case remove case edit case setDestination case rename case promote case demote } extension KMLeftSideViewController { func updateOutlineSelection() { // if ([[pdfView document] outlineRoot] == nil || mwcFlags.updatingOutlineSelection) if self.listView.document.outlineRoot() == nil { return } // mwcFlags.updatingOutlineSelection = YES; let numRows = self.tocOutlineView.numberOfRows var arr = NSMutableArray() for i in 0 ..< numRows { guard let tPDFOutline = self.tocOutlineView.item(atRow: i) as? CPDFOutline else { continue } let tPage = tPDFOutline.destination.page() if (tPage == nil) { continue } let tDict = NSDictionary(object: tPage as Any, forKey: "\(i)" as NSCopying) arr.add(tDict) } let currentPage = self.listView.currentPage() var hasExistInOutlineView = false for dict in arr { guard let _dict = dict as? NSDictionary else { continue } let page = _dict.allValues.last as? CPDFPage // NSInteger index = [dict.allKeys.lastObject integerValue]; let index = Int(_dict.allKeys.last as? String ?? "0") ?? 0 if let data = page?.isEqual(to: currentPage), data { self.tocOutlineView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false) self.tocOutlineView.scrollRowToVisible(index) hasExistInOutlineView = true break } } if (!hasExistInOutlineView) { self.tocOutlineView.deselectRow(self.tocOutlineView.selectedRow) } // mwcFlags.updatingOutlineSelection = NO; } func newAddOutlineEntryEditingMode(_ index: Int) { self.renamePDFOutline = self.tocOutlineView.item(atRow: self.tocOutlineView.selectedRow) as? CPDFOutline let row = self.tocOutlineView.selectedRow let viewS = self.tocOutlineView.view(atColumn: 0, row: row, makeIfNecessary: true) let targrtTextField = viewS?.subviews.first as? NSTextField self.renamePDFOutlineTextField = targrtTextField targrtTextField?.delegate = self targrtTextField?.isEditable = true targrtTextField?.becomeFirstResponder() } func loadUnfoldDate(_ foldType: KMFoldType) { self.allFoldNotes.removeAll() var mutableArray: [CPDFAnnotation] = [] for note in self.notes { if note is CPDFMarkupAnnotation { mutableArray.append(note) } } self.canFoldNotes = mutableArray self.allFoldNotes = [] } func selectedRowIndexes() -> IndexSet { var selectedIndexes = self.tocOutlineView.selectedRowIndexes let clickedRow = self.tocOutlineView.clickedRow if clickedRow != -1 && selectedIndexes.contains(clickedRow) == false { var indexes = IndexSet(integer: clickedRow) selectedIndexes = indexes } return selectedIndexes } func editOutlineUI(_ editVC: KMOutlineEditViewController) { if editVC.pageButton.state == .on { let numberString = editVC.outlineTargetPageIndexTextField.stringValue let idx = Int(numberString) ?? 1 let newPage = editVC.pdfView?.document.page(at: UInt(idx-1)) let originalPage = editVC.originalDestination?.page() if let data = newPage?.pageIndex(), data != editVC.originalPageIndex { // if let data = newPage?.isEqual(to: originalPage), data { //新page不存在 if (newPage == nil) { } else { if let outL = editVC.outline { let pageSize = newPage?.bounds(for: .cropBox).size ?? .zero if let destination = CPDFDestination(page: newPage, at: CGPointMake(pageSize.width, pageSize.height)) { self.changePDFOutlineDestination(destination, PDFoutline: outL) } } } } } else if editVC.urlButton.state == .on { var urlString = editVC.outlineURLTextField.stringValue let tLowerUrl = urlString.lowercased() if tLowerUrl.hasPrefix("https://") == false && tLowerUrl.hasPrefix("ftp://") == false && tLowerUrl.hasPrefix("http://") == false && urlString.isEmpty == false { urlString = "http://\(urlString)" } if let urlAction = CPDFURLAction(url: urlString), editVC.outline != nil { if editVC.originalURLString != editVC.outlineURLTextField.stringValue { self.changePDFAction(urlAction, PDFOutline: editVC.outline!) } } } else if editVC.mailButton.state == .on { var mailString = editVC.mailAddressTextField.stringValue let tLowerStr = mailString.lowercased() if tLowerStr.hasPrefix("mailto:") == false { mailString = "mailto:\(mailString)" } if var urlAction = CPDFURLAction(url: mailString) { if urlAction.url() == nil { urlAction = CPDFURLAction(url: "mailto:") } if mailString != editVC.originalURLString && editVC.outline != nil { self.changePDFAction(urlAction, PDFOutline: editVC.outline!) } } } if editVC.outlineNameTextView.string == editVC.originalLabel { return } if let outL = editVC.outline { self.renamePDFOutline(outL, label: editVC.outlineNameTextView.string) } } @objc func outlineContextMenuItemClicked_AddEntry(_ sender: AnyObject?) { let PDFOutlineArray = NSMutableArray() let rowSet = self.selectedRowIndexes() for idx in rowSet { PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx)) } if (PDFOutlineArray.count == 0) { var lastPDFLine = self.tocOutlineView.item(atRow: self.tocOutlineView.numberOfRows-1) as? CPDFOutline var rootPDFOutline: CPDFOutline? if (lastPDFLine != nil) { while (lastPDFLine!.parent != nil) { lastPDFLine = lastPDFLine?.parent } rootPDFOutline = lastPDFLine } else { rootPDFOutline = self.listView.document.outlineRoot() if ((rootPDFOutline == nil)) { // rootPDFOutline = CPDFOutline() // self.listView.document.setOutlineRoot(rootPDFOutline) rootPDFOutline = self.listView.document.setNewOutlineRoot() } } let addOutLine = CPDFOutline() addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1) addOutLine.destination = self.listView.currentDestination self.addoutline(parent: rootPDFOutline, addOutline: addOutLine, index: Int(rootPDFOutline?.numberOfChildren ?? 0), needExpand: false) } else { let currentPDFline = PDFOutlineArray.lastObject as? CPDFOutline let currentIndex = currentPDFline?.index ?? 0 var parent: CPDFOutline? parent = currentPDFline?.parent let addOutLine = CPDFOutline() addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1) addOutLine.destination = self.listView.currentDestination self.addoutline(parent: parent, addOutline: addOutLine, index: Int(currentIndex) + 1, needExpand: false) self.tocOutlineView.scrollRowToVisible(Int(currentIndex) + 1) self.tocOutlineView.deselectRow(Int(currentIndex)+1) } } } // MARK: - Undo & Redo extension KMLeftSideViewController { @objc dynamic func changePDFOutlineDestination(_ destination: CPDFDestination?, PDFoutline outline: CPDFOutline) { if let des = destination { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).changePDFOutlineDestination(outline.destination, PDFoutline: outline) let newDes = CPDFDestination(document: des.document, pageIndex: des.pageIndex, at: des.point, zoom: des.zoom) outline.destination = newDes self.tocOutlineView.reloadItem(outline) } } @objc dynamic func changePDFAction(_ action: CPDFAction, PDFOutline outline: CPDFOutline) { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).changePDFAction(outline.action, PDFOutline: outline) outline.action = action } @objc dynamic func renamePDFOutline(_ outline: CPDFOutline, label: String) { if (self.isRenameNoteOutline) { if outline is CPDFAnnotation { // if([outline isKindOfClass:[PDFAnnotation class]]) { // PDFAnnotation *annotation = (PDFAnnotation *)outline; // annotation.string = label; // [rightSideController.noteOutlineView reloadData]; // [rightSideController.noteOutlineView selectRowIndexes:[[[NSIndexSet alloc] initWithIndex:[rightSideController.noteOutlineView rowForItem:outline]] autorelease] byExtendingSelection:NO]; } } else { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).renamePDFOutline(outline, label: outline.label) outline.label = label self.tocOutlineView.reloadData() self.tocOutlineView.km_selectItem(outline, byExtendingSelection: false) } } @objc dynamic func demoteOutlineWithGrandParent(_ grandParentOutline: CPDFOutline, demoteOutline: CPDFOutline, index: Int) { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).promoteOutlineWithGrandParent(grandParentOutline, promoteOutline: demoteOutline, rowIndex: index) if grandParentOutline.isEqual(to: demoteOutline.parent) { let demoteIndex = demoteOutline.index let previousOutline = grandParentOutline.child(at: demoteIndex-1) demoteOutline.removeFromParent() previousOutline?.insertChild(demoteOutline, at: UInt(index)) self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(previousOutline) } else { demoteOutline.removeFromParent() grandParentOutline.insertChild(demoteOutline, at: grandParentOutline.numberOfChildren) self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(grandParentOutline) } self.tocOutlineView.km_selectItem(demoteOutline, byExtendingSelection: false) } @objc dynamic func promoteOutlineWithGrandParent(_ grandParentOutline: CPDFOutline, promoteOutline: CPDFOutline, rowIndex: Int) { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).demoteOutlineWithGrandParent(grandParentOutline, demoteOutline: promoteOutline, index: rowIndex) let index = promoteOutline.parent?.index ?? 0 if grandParentOutline.isEqual(to: promoteOutline.parent) { promoteOutline.removeFromParent() grandParentOutline.parent.insertChild(promoteOutline, at: index+1) } else { promoteOutline.removeFromParent() grandParentOutline.insertChild(promoteOutline, at: index+1) } self.tocOutlineView.reloadData() self.tocOutlineView.km_selectItem(promoteOutline, byExtendingSelection: false) } @objc dynamic func addoutline(parent parentOutline: CPDFOutline?, addOutline: CPDFOutline, index: Int, needExpand: Bool) { var tempO: CPDFOutline? = addOutline if addOutline.label != nil { parentOutline?.insertChild(addOutline, at: UInt(index)) } else { let outline = parentOutline?.insertChild(at: UInt(index)) outline?.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1) outline?.destination = self.listView.currentDestination tempO = outline } guard let outline = tempO else { return } (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).removeOutline(parent: parentOutline, removeOutline: outline, index: index, needExpand: needExpand) self.view.window?.makeFirstResponder(nil) Task { @MainActor in self.tocOutlineView.reloadData() if (needExpand) { self.tocOutlineView.expandItem(parentOutline) } let idx = self.tocOutlineView.row(forItem: outline) self.tocOutlineView.selectRowIndexes(IndexSet(integer: idx), byExtendingSelection: false) self.newAddOutlineEntryEditingMode(index) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.tocOutlineView.scrollRowToVisible(self.tocOutlineView.row(forItem: outline)) } } @objc dynamic func removeOutline(parent parentOutline: CPDFOutline?, removeOutline: CPDFOutline, index: Int, needExpand: Bool) { (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).addoutline(parent: parentOutline, addOutline: removeOutline, index: index, needExpand: true) removeOutline.removeFromParent() if (needExpand) { self.tocOutlineView.expandItem(parentOutline) } Task { @MainActor in self.tocOutlineView.reloadData() } } } // MARK: - Menu Actions extension KMLeftSideViewController { //添加子节点 @objc func outlineContextMenuItemClicked_AddChildEntry(_ sender: AnyObject?) { let PDFOutlineArray = NSMutableArray() let rowSet = self.selectedRowIndexes() for idx in rowSet { PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx)) } let currentPDFline = PDFOutlineArray.lastObject as? CPDFOutline let addOutLine = CPDFOutline() addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.listView.currentPageIndex + 1)") addOutLine.destination = self.listView.currentDestination self.addoutline(parent: currentPDFline, addOutline: addOutLine, index: Int(currentPDFline?.numberOfChildren ?? 0), needExpand: true) } //添加上一级节点 @objc func outlineContextMenuItemClicked_AddAuntEntry(_ sender: AnyObject?) { let PDFOutlineArray = NSMutableArray() let rowSet = self.selectedRowIndexes() for idx in rowSet { PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx)) } let clickedOutline = PDFOutlineArray.lastObject as? CPDFOutline let fatherOutLine = clickedOutline?.parent let grandfatherOutLine = fatherOutLine?.parent if (grandfatherOutLine != nil) { let addOutLine = CPDFOutline() addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.listView.currentPageIndex + 1)") addOutLine.destination = self.listView.currentDestination self.addoutline(parent: grandfatherOutLine, addOutline: addOutLine, index: Int(fatherOutLine?.index ?? 0) + 1, needExpand: false) } } //移除节点 @objc func outlineContextMenuItemClicked_RemoveEntry(_ sender: AnyObject?) { let set = self.selectedRowIndexes() var selectedPDFOutlineArr = NSMutableArray() for idx in set { selectedPDFOutlineArr.add(self.tocOutlineView.item(atRow: idx)) } //整体移除多选 for tOutline in selectedPDFOutlineArr { guard let outL = tOutline as? CPDFOutline else { continue } self.removeOutline(parent: outL.parent , removeOutline: outL, index: Int(outL.index), needExpand: false) } } //重置目的 @objc func outlineContextMenuItemClicked_SetDestination(_ sender: AnyObject?) { guard let setPDFOutline = self.tocOutlineView.item(atRow: self.tocOutlineView.clickedRow) as? CPDFOutline else { return } Task { let modalRes = await KMAlertTool.runModel(style: .informational, message: KMLocalizedString("Are you sure you want to set the destination as the current location?", nil), buttons: [KMLocalizedString("Yes", nil), KMLocalizedString("No", nil)]) if (modalRes == .alertFirstButtonReturn) { self.changePDFOutlineDestination(self.listView.currentDestination, PDFoutline: setPDFOutline) self.tocOutlineView.reloadData() let idx = self.tocOutlineView.row(forItem: setPDFOutline) self.tocOutlineView.selectRowIndexes(IndexSet(integer: idx), byExtendingSelection: false) } } } //弹出菜单 @objc func outlineContextMenuItemClicked_Edit(_ sender: AnyObject?) { let popover = NSPopover() popover.delegate = self let targetOutline: CPDFOutline? = self.tocOutlineView.km.clickedItem() let outlineEditViewController = KMOutlineEditViewController(outline: targetOutline, document: self.listView) let cell = self.tocOutlineView.rowView(atRow: self.tocOutlineView.clickedRow, makeIfNecessary: true) popover.contentViewController = outlineEditViewController popover.animates = true popover.behavior = .transient popover.show(relativeTo: cell?.bounds ?? .zero, of: cell!, preferredEdge: .minX) } //重命名 @objc func outlineContextMenuItemClicked_Rename(_ sender: AnyObject?) { if (self.tocOutlineView.clickedRow < 0) { return } self.renamePDFOutline = self.tocOutlineView.km.clickedItem() let viewS = self.tocOutlineView.view(atColumn: 0, row: self.tocOutlineView.clickedRow, makeIfNecessary: true) let targrtTextField = viewS?.subviews.first as? NSTextField self.renamePDFOutlineTextField = targrtTextField targrtTextField?.delegate = self targrtTextField?.isEditable = true targrtTextField?.becomeFirstResponder() } //降级节点 @objc func outlineContextMenuItemClicked_Demote(_ sender: AnyObject?) { guard let currentOutline: CPDFOutline = self.tocOutlineView.km.clickedItem() else { return } let parentOutLine = currentOutline.parent let newParentOutLine = parentOutLine?.child(at: currentOutline.index-1) var newIndex = 0 let newParentOutLineExpandState = self.tocOutlineView.isItemExpanded(newParentOutLine) if (newParentOutLineExpandState) { newIndex = self.tocOutlineView.clickedRow } else { newIndex = self.tocOutlineView.clickedRow + Int(newParentOutLine?.numberOfChildren ?? 0) } let currentIndex = currentOutline.index currentOutline.removeFromParent() self.demoteOutlineWithGrandParent(newParentOutLine!, demoteOutline: currentOutline, index: Int(currentIndex)) } //升级节点 @objc func outlineContextMenuItemClicked_Promote(_ sender: AnyObject?) { guard let currentOutline: CPDFOutline = self.tocOutlineView.km.clickedItem() else { return } let parentOutLine = currentOutline.parent if let grandParentOutLine = parentOutLine?.parent { self.promoteOutlineWithGrandParent(grandParentOutLine, promoteOutline: currentOutline, rowIndex:Int(currentOutline.index)) } } }