// // KMLeftSideViewController+Outline.swift // PDF Reader Pro // // 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 outline_initSubViews() { self.outlineSearchField.delegate = self self.outlineAddButton.target = self self.outlineAddButton.action = #selector(outlineContextMenuItemClicked_AddEntry) self.outlineMoreButton.target = self self.outlineMoreButton.tag = 302 self.outlineMoreButton.action = #selector(leftSideViewMoreButtonAction) self.outlineDoneButton.target = self self.outlineDoneButton.tag = 310 self.outlineDoneButton.action = #selector(leftSideViewDoneButtonAction) let menuOutline = NSMenu() _ = menuOutline.addItem(title: KMLocalizedString("Ignore Case", "Menu item title"), action: #selector(toggleOutlineCaseInsensitiveSearch), target: self) (self.outlineSearchField.cell as? NSSearchFieldCell)?.searchMenuTemplate = menuOutline self.outlineSearchField.target = self self.tocOutlineView.menu = NSMenu() self.tocOutlineView.menu?.delegate = self self.tocOutlineView.delegate = self self.tocOutlineView.dataSource = self self.tocOutlineView.botaDelegate = self self.tocOutlineView.botaDataSource = self self.tocOutlineView.tocDelegate = self self.tocOutlineView.registerForDraggedTypes([.localDraggedTypes]) self.tocOutlineView.target = self self.tocOutlineView.doubleAction = #selector(outlineContextMenuItemClicked_Rename) } func outline_initDefalutValue() { self.outlineView.wantsLayer = true self.outlineView.layer?.backgroundColor = KMAppearance.Layout.l0Color().cgColor self.outlineLabel.stringValue = KMLocalizedString("Outline", nil); self.outlineLabel.textColor = KMAppearance.Layout.h0Color() self.outlineAddButton.toolTip = KMLocalizedString("Add Item", nil) self.outlineSearchButton.toolTip = KMLocalizedString("Search", nil) self.outlineDoneButton.title = KMLocalizedString("Done", nil); self.outlineDoneButton.toolTip = KMLocalizedString("Done", nil); self.outlineDoneButton.setTitleColor(KMAppearance.Layout.w0Color()) self.outlineDoneButton.wantsLayer = true self.outlineDoneButton.layer?.backgroundColor = KMAppearance.Interactive.a0Color().cgColor self.outlineDoneButton.layer?.cornerRadius = 4.0 self.outlineSearchField.wantsLayer = true self.outlineSearchField.backgroundColor = KMAppearance.Layout.l_1Color() self.outlineSearchField.layer?.backgroundColor = KMAppearance.Layout.l_1Color().cgColor self.outlineSearchField.layer?.borderWidth = 1.0 self.outlineSearchField.layer?.borderColor = KMAppearance.Interactive.a0Color().cgColor self.outlineDoneButton.isHidden = true self.outlineSearchField.isHidden = true (self.outlineSearchField.cell as? NSSearchFieldCell)?.placeholderString = KMLocalizedString("Search Outline", nil) self.tocOutlineView.backgroundColor = KMAppearance.Layout.l0Color() self.tocOutlineView.autoresizesOutlineColumn = false self.tocOutlineView.allowsMultipleSelection = true self.tocOutlineView.allowsEmptySelection = true self.tocOutlineView.focusRingType = .none self.tocOutlineView.autoresizesSubviews = true } func updateOutlineSelection() { if self.outlineRoot() == nil || self.updatingOutlineSelection { return } self.updatingOutlineSelection = true let numRows = self.tocOutlineView.numberOfRows let currentPage = self.currentPage() var flagIdx = NSNotFound for i in 0 ..< numRows { guard let ol = self.tocOutlineView.item(atRow: i) as? CPDFOutline else { continue } guard let page = ol.km_page else { continue } if page.isEqual(to: currentPage) { flagIdx = i break } } if flagIdx != NSNotFound { self.tocOutlineView.km_safe_selectRowIndexes(.init(integer: flagIdx), byExtendingSelection: false) self.tocOutlineView.scrollRowToVisible(flagIdx) } else { self.tocOutlineView.deselectRow(self.tocOutlineView.selectedRow) } self.updatingOutlineSelection = false } 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 selectedRowIndexes() -> IndexSet { var selectedIndexes = self.tocOutlineView.selectedRowIndexes let clickedRow = self.tocOutlineView.clickedRow if clickedRow != -1 && selectedIndexes.contains(clickedRow) == false { let indexes = IndexSet(integer: clickedRow) selectedIndexes = indexes } return selectedIndexes } func editOutlineUI(_ editVC: KMOutlineEditViewController) { guard let ol = editVC.outline else { return } 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)) if let data = newPage?.pageIndex(), data != editVC.originalPageIndex { //新page不存在 if (newPage == nil) { } else { let pageSize = newPage?.bounds(for: .cropBox).size ?? .zero if let dest = CPDFDestination(page: newPage, at: CGPointMake(pageSize.width, pageSize.height)) { self.changePDFOutlineDestination(dest, PDFoutline: ol) } } } } else if editVC.urlButton.state == .on { var urlString = editVC.outlineURLTextField.stringValue let tLowerUrl = urlString.lowercased() if tLowerUrl.hasPrefix(kHTTPSPrefix) == false && tLowerUrl.hasPrefix(kFTPPrefix) == false && tLowerUrl.hasPrefix(kHTTPPrefix) == false && urlString.isEmpty == false { urlString = "\(kHTTPPrefix)\(urlString)" } if let act = CPDFURLAction(url: urlString) { if editVC.originalURLString != editVC.outlineURLTextField.stringValue { self.changePDFAction(act, PDFOutline: ol) } } } else if editVC.mailButton.state == .on { var mailString = editVC.mailAddressTextField.stringValue let tLowerStr = mailString.lowercased() if tLowerStr.hasPrefix(kEmailPrefix) == false { mailString = "\(kEmailPrefix)\(mailString)" } if var urlAction = CPDFURLAction(url: mailString) { if urlAction.url() == nil { urlAction = CPDFURLAction(url: kEmailPrefix) } if mailString != editVC.originalURLString { self.changePDFAction(urlAction, PDFOutline: ol) } } } if editVC.outlineNameTextView.string != editVC.originalLabel { self.renamePDFOutline(ol, label: editVC.outlineNameTextView.string) } } @objc func outlineContextMenuItemClicked_AddEntry(_ sender: AnyObject?) { let ris = self.selectedRowIndexes() if ris.isEmpty { var rootOL = self.outlineRoot() if rootOL == nil { rootOL = self.setNewOutlineRoot() } let ol = CPDFOutline() ol.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.currentPageIndex()+1) ol.destination = self.currentDestination() self.addoutline(parent: rootOL, addOutline: ol, index: Int(rootOL?.numberOfChildren ?? 0), needExpand: false) return } if let row = ris.last { let item = self.tocOutlineView.item(atRow: row) as? CPDFOutline let ol = CPDFOutline() ol.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.currentPageIndex()+1) ol.destination = self.currentDestination() let idx = Int(item?.index ?? 0) + 1 self.addoutline(parent: item?.parent, addOutline: ol, index: idx, needExpand: false) self.tocOutlineView.scrollRowToVisible(idx) self.tocOutlineView.deselectRow(idx) } } @IBAction func outlineNormalSearchButtonAction(_ sender: NSButton) { self.outlineSearchField.isHidden = false self.outlineDoneButton.isHidden = false self.outlineLabel.isHidden = true self.outlineSearchButton.isHidden = true self.outlineMoreButton.isHidden = true self.outlineAddButton.isHidden = true self.outlineSearchField.becomeFirstResponder() } @IBAction func toc_expandAllComments(_ sender: AnyObject?) { if (self.tocType == .unfold) { return } self.tocType = .unfold self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(nil, expandChildren: true) } @IBAction func toc_foldAllComments(_ sender: AnyObject?) { if (self.tocType == .fold) { return } self.tocType = .fold self.tocOutlineView.reloadData() self.tocOutlineView.collapseItem(nil, collapseChildren: true) } @objc func leftSideEmptyAnnotationClick_DeleteOutline(_ sender: AnyObject?) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "" alert.informativeText = KMLocalizedString("This will permanently remove all outlines. Are you sure to continue?", nil) alert.addButton(withTitle: KMLocalizedString("Yes", nil)) alert.addButton(withTitle: KMLocalizedString("No", nil)) let response = alert.runModal() if response == .alertFirstButtonReturn { if let item = self.outlineRoot() { self.removeAllOutline(item) } } } func showSearchOutlineBlankState(_ toShowState: Bool) { if (toShowState) { let documentViewFrame = self.tocOutlineView.enclosingScrollView?.documentView?.frame ?? .zero let width = self.leftSideEmptyVC.outlineSearchView.bounds.size.width let height = self.leftSideEmptyVC.outlineSearchView.bounds.size.height self.leftSideEmptyVC.outlineSearchView.frame = CGRectMake((documentViewFrame.size.width - width)/2.0, (documentViewFrame.size.height - height)/2.0, width, height) self.tocOutlineView.enclosingScrollView?.documentView?.addSubview(self.leftSideEmptyVC.outlineSearchView) self.leftSideEmptyVC.outlineSearchView.autoresizingMask = [.minXMargin, .maxXMargin, .minYMargin, .maxYMargin] } else { self.leftSideEmptyVC.outlineSearchView.removeFromSuperview() } } func removeAllOutline(_ outline: CPDFOutline) { _ = self.setNewOutlineRoot() for i in 0 ..< self.pageCount() { self.pdfDocument()?.removeBookmark(forPageIndex: UInt(i)) } self.layoutDocumentView() DispatchQueue.main.async { self.tocOutlineView.reloadData() } } func updateSelectRowHeight() { guard let ol = self.tocOutlineView.selectedItem() as? CPDFOutline else { return } let attriString = NSMutableAttributedString() let attri = [NSAttributedString.Key.foregroundColor : KMAppearance.Layout.h0Color()] attriString.append(.init(string: ol.label ?? "", attributes: attri)) let row = self.tocOutlineView.selectedRow let column = self.tocOutlineView.tableColumn(withIdentifier: kLabelColumnId) let cell = column?.dataCell(forRow: row) (cell as? NSCell)?.objectValue = attriString let w = self.view.frame.size.width-86 let num = self.getNum(ol) let gap = self.tocOutlineView.indentationPerLevel var rowH = ((cell as? NSCell)?.cellSize(forBounds: NSMakeRect(0, 0, w - (num > 0 ? 16 : 0) - gap * num.cgFloat, CGFLOAT_MAX)).height) ?? 0 rowH = fmax(rowH, self.tocOutlineView.rowHeight) + 25 let view = self.tocOutlineView.view(atColumn: 0, row: row, makeIfNecessary: true) let frame = view?.frame ?? .zero view?.frame = NSMakeRect(frame.origin.x, frame.origin.y, frame.size.width, rowH) self.tocOutlineView.reloadData() } func getNum(_ ol: CPDFOutline?) -> Int { var num = 0 var outline = ol?.parent repeat { outline = outline?.parent if outline != nil { num += 1 } } while outline != nil return num } func addOutlineAfter(_ ol: CPDFOutline) { if self.leftView.segmentedControl.selectedSegment == 1 { } else { self.leftView.segmentedControl.selectedSegment = 1 } Task { @MainActor in self.tocOutlineView.reloadData() let idx = self.tocOutlineView.row(forItem: ol) self.tocOutlineView.km_selectItem(ol, byExtendingSelection: false) self.newAddOutlineEntryEditingMode(idx) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.tocOutlineView.scrollRowToVisible(self.tocOutlineView.row(forItem: ol)) } } func removeOutlineAfter(_ ol: CPDFOutline) { Task { @MainActor in self.tocOutlineView.reloadData() } } func demoteOutlineAfter(_ ol: CPDFOutline) { Task { @MainActor in self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(ol.parent) self.tocOutlineView.km_selectItem(ol, byExtendingSelection: false) } } func promoteOutlineAfter(_ ol: CPDFOutline) { Task { @MainActor in self.tocOutlineView.reloadData() self.tocOutlineView.km_selectItem(ol, byExtendingSelection: false) } } func showOutlineEmptyView() { let view = self.tocOutlineView.enclosingScrollView?.documentView let viewFrame = view?.frame ?? .zero let emptyVcSize = self.leftSideEmptyVC.emptyOutlineView.frame.size self.leftSideEmptyVC.emptyOutlineView.frame = NSMakeRect((viewFrame.size.width-emptyVcSize.width)/2.0,(viewFrame.size.height-emptyVcSize.height)/2.0, emptyVcSize.width, emptyVcSize.height) self.leftSideEmptyVC.emptyOutlineView.autoresizingMask = [.minXMargin, .maxXMargin, .minYMargin, .maxYMargin] self.tocOutlineView.enclosingScrollView?.documentView?.addSubview(self.leftSideEmptyVC.emptyOutlineView) self.leftSideEmptyVC.deleteOutlineBtn.isEnabled = false } func hideOutlineEmptyView() { self.leftSideEmptyVC.emptyOutlineView.removeFromSuperview() self.leftSideEmptyVC.deleteOutlineBtn.isEnabled = true } func fetchOutlines(for ol: CPDFOutline, searchString: String) -> [CPDFOutline] { var ols: [CPDFOutline] = [] for i in 0 ..< ol.numberOfChildren { guard let child = ol.child(at: i) else { continue } if self.hasContainString(searchString, rootOutline: child) { ols.append(child) } } return ols } @objc func goToSelectedOutlineItem(_ sender: AnyObject?) { let cnt = self.tocOutlineView.selectedRowIndexes.count if cnt == 0 || cnt > 1 { return } guard let item = self.tocOutlineView.selectedItem() else { return } if let bk = item as? CPDFBookmark { self.listView?.go(toPageIndex: bk.pageIndex, animated: true) } else if let ol = item as? CPDFOutline { if let dest = ol.destination { self.listView?.go(to: dest) } else if let act = ol.action { self.listView?.perform(act) } } } } // 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 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?.km_insertChild(label: String(format: "%@ %ld", KMLocalizedString("Page", nil), self.currentPageIndex()+1), dest: self.currentDestination(), at: index) 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() } } @objc dynamic func dragPDFOutline(_ outline: CPDFOutline?, toIndex index: Int, newParentOutline: CPDFOutline?) { guard let doc = self.view.window?.windowController?.document as? NSDocument else { NSSound.beep() return } if outline == nil { NSSound.beep() return } (doc.undoManager?.prepare(withInvocationTarget: self) as AnyObject).dragPDFOutline(outline, toIndex: Int(outline!.index), newParentOutline: outline?.parent) outline?.removeFromParent() newParentOutline?.insertChild(outline, at: UInt(index)) self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(newParentOutline) self.tocOutlineView.selectRowIndexes(IndexSet(integer: self.tocOutlineView.row(forItem: outline)), byExtendingSelection: false) } } // MARK: - Menu Actions extension KMLeftSideViewController { func outlineListMenu(_ menu: NSMenu) { _ = menu.addItem(withTitle: NSLocalizedString("Add Item", comment: ""), action: #selector(outlineContextMenuItemClicked_AddEntry), target: self, tag: KMOutlineViewMenuItemTag.addEntry.rawValue) _ = menu.addItem(withTitle: NSLocalizedString("Add Sub-Item", comment: ""), action: #selector(outlineContextMenuItemClicked_AddChildEntry), target: self, tag: KMOutlineViewMenuItemTag.addChild.rawValue) _ = menu.addItem(withTitle: NSLocalizedString("Add To A Higher Level", comment: ""), action: #selector(outlineContextMenuItemClicked_AddAuntEntry), target: self, tag: KMOutlineViewMenuItemTag.addAunt.rawValue) menu.addItem(.separator()) _ = menu.addItem(withTitle: NSLocalizedString("Delete", comment: ""), action: #selector(outlineContextMenuItemClicked_RemoveEntry), target: self, tag: KMOutlineViewMenuItemTag.remove.rawValue) menu.addItem(.separator()) _ = menu.addItem(withTitle: NSLocalizedString("Edit", comment: ""), action: #selector(outlineContextMenuItemClicked_Edit), target: self, tag: KMOutlineViewMenuItemTag.edit.rawValue) _ = menu.addItem(withTitle: NSLocalizedString("Change Destination", comment: ""), action: #selector(outlineContextMenuItemClicked_SetDestination), target: self, tag: KMOutlineViewMenuItemTag.setDestination.rawValue) _ = menu.addItem(withTitle: NSLocalizedString("Rename", comment: ""), action: #selector(outlineContextMenuItemClicked_Rename), target: self, tag: KMOutlineViewMenuItemTag.rename.rawValue) menu.addItem(.separator()) _ = menu.addItem(withTitle: NSLocalizedString("Promote", comment: ""), action: #selector(outlineContextMenuItemClicked_Promote), target: self, tag: KMOutlineViewMenuItemTag.promote.rawValue) _ = menu.addItem(withTitle: NSLocalizedString("Demote", comment: ""), action: #selector(outlineContextMenuItemClicked_Demote), target: self, tag: KMOutlineViewMenuItemTag.demote.rawValue) } func outlineMoreMenu(_ view: NSButton) { let menu = NSMenu() let expandAllItem = menu.addItem(title: KMLocalizedString("Expand All", nil), action: #selector(toc_expandAllComments), target: self) expandAllItem?.representedObject = self.tocOutlineView let foldAllItem = menu.addItem(title: KMLocalizedString("Collapse All", nil), action: #selector(toc_foldAllComments), target: self) foldAllItem?.representedObject = self.tocOutlineView let rootOL = self.outlineRoot() let childNum = rootOL?.numberOfChildren ?? 0 var num = 0 for i in 0 ..< childNum { let ol = rootOL?.child(at: i) if self.tocOutlineView.isItemExpanded(ol) { num += 1 } } self.tocType = .none if childNum > 0 { if num == 0 { self.tocType = .fold } else if num == childNum { self.tocType = .unfold } } expandAllItem?.state = self.tocType == .unfold ? .on : .off foldAllItem?.state = self.tocType == .fold ? .on : .off let removeEntryItem = menu.addItem(title: KMLocalizedString("Remove All Outlines", nil), action: #selector(leftSideEmptyAnnotationClick_DeleteOutline), target: self) removeEntryItem?.representedObject = self.tocOutlineView if let data = NSApp.currentEvent { NSMenu.popUpContextMenu(menu, with: data, for: view) } } func outlineListValidateMenuItem(_ menuItem: NSMenuItem) -> Bool { if self.isLocked() { return false } let action = menuItem.action if (action == #selector(leftSideEmptyAnnotationClick_DeleteOutline)) { guard let rootOL = self.outlineRoot() else { return false } let childNum = rootOL.numberOfChildren if (self.isSearchOutlineMode) { let ols = self.fetchOutlines(for: rootOL, searchString: self.outlineSearchField.stringValue) return ols.count > 0 } else { return childNum > 0 } } else if action == #selector(toggleOutlineCaseInsensitiveSearch) { menuItem.state = self.outlineIgnoreCaseFlag ? .on : .off return true } if (self.isSearchOutlineMode) { if (action == #selector(outlineContextMenuItemClicked_AddEntry) || action == #selector(outlineContextMenuItemClicked_AddChildEntry) || action == #selector(outlineContextMenuItemClicked_AddAuntEntry) || action == #selector(outlineContextMenuItemClicked_Edit) || action == #selector(outlineContextMenuItemClicked_Rename) ) { return false } } if (self.tocOutlineView.selectedRowIndexes.count > 1) { if (menuItem.tag == KMOutlineViewMenuItemTag.remove.rawValue) { return true } return false } else if (self.tocOutlineView.selectedRowIndexes.count > 0) { if (action == #selector(outlineContextMenuItemClicked_AddChildEntry) || action == #selector(outlineContextMenuItemClicked_SetDestination)) { return true } } if (self.tocOutlineView.clickedRow == -1) { if (action == #selector(outlineContextMenuItemClicked_AddEntry)) { return true } else { return false } } else { let clickedOutline = self.tocOutlineView.item(atRow: self.tocOutlineView.clickedRow) as? CPDFOutline if (action == #selector(outlineContextMenuItemClicked_Demote)) { if let data = clickedOutline { return self.pdfDocument()?.canDemote(outline: data) ?? false } return false } if (action == #selector(outlineContextMenuItemClicked_Promote)) { if let data = clickedOutline { return self.pdfDocument()?.canPromote(outline: data) ?? false } else { return false } } if (action == #selector(outlineContextMenuItemClicked_AddAuntEntry)) { let parentOutLine = clickedOutline?.parent let grandparentOutLine = parentOutLine?.parent if (grandparentOutLine != nil) { return true } else { return false } } } return true } //添加子节点 @objc func outlineContextMenuItemClicked_AddChildEntry(_ sender: AnyObject?) { guard let row = self.selectedRowIndexes().last else { return } let item = self.tocOutlineView.item(atRow: row) as? CPDFOutline let ol = CPDFOutline() ol.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.currentPageIndex() + 1)") ol.destination = self.currentDestination() self.addoutline(parent: item, addOutline: ol, index: Int(item?.numberOfChildren ?? 0), needExpand: true) } //添加上一级节点 @objc func outlineContextMenuItemClicked_AddAuntEntry(_ sender: AnyObject?) { guard let row = self.selectedRowIndexes().last else { return } let item = self.tocOutlineView.item(atRow: row) as? CPDFOutline let parentOL = item?.parent if parentOL?.parent != nil { let ol = CPDFOutline() ol.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.currentPageIndex() + 1)") ol.destination = self.currentDestination() self.addoutline(parent: parentOL?.parent, addOutline: ol, index: Int(parentOL?.index ?? 0) + 1, needExpand: false) } } //移除节点 @objc func outlineContextMenuItemClicked_RemoveEntry(_ sender: AnyObject?) { for idx in self.selectedRowIndexes() { let item = self.tocOutlineView.item(atRow: idx) if let ol = item as? CPDFOutline { self.removeOutline(parent: ol.parent , removeOutline: ol, index: Int(ol.index), needExpand: false) } } } //重置目的 @objc func outlineContextMenuItemClicked_SetDestination(_ sender: AnyObject?) { guard let ol = self.tocOutlineView.clickedItem() as? CPDFOutline else { return } Task { let res = 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 (res == .alertFirstButtonReturn) { self.changePDFOutlineDestination(self.currentDestination(), PDFoutline: ol) let idx = self.tocOutlineView.row(forItem: ol) self.tocOutlineView.km_safe_selectRowIndexes(.init(integer: idx), byExtendingSelection: false) } } } //弹出菜单 @objc func outlineContextMenuItemClicked_Edit(_ sender: AnyObject?) { let popover = NSPopover() popover.delegate = self let ol: CPDFOutline? = self.tocOutlineView.km.clickedItem() let vc = KMOutlineEditViewController(outline: ol, document: self.listView) if let cell = self.tocOutlineView.rowView(atRow: self.tocOutlineView.clickedRow, makeIfNecessary: true) { popover.contentViewController = vc popover.animates = true popover.behavior = .transient popover.show(relativeTo: cell.bounds, of: cell, preferredEdge: .minX) } } //重命名 @objc func outlineContextMenuItemClicked_Rename(_ sender: AnyObject?) { if (self.tocOutlineView.clickedRow < 0) { return } self.renamePDFOutline = self.tocOutlineView.km.clickedItem() let cell = self.tocOutlineView.view(atColumn: 0, row: self.tocOutlineView.clickedRow, makeIfNecessary: true) let tf = cell?.subviews.first as? NSTextField self.renamePDFOutlineTextField = tf tf?.delegate = self tf?.isEditable = true tf?.becomeFirstResponder() } //降级节点 @objc func outlineContextMenuItemClicked_Demote(_ sender: AnyObject?) { self.delegate?.controller?(controller: self, itemClick: sender, itemKey: .demote, params: nil) } //升级节点 @objc func outlineContextMenuItemClicked_Promote(_ sender: AnyObject?) { self.delegate?.controller?(controller: self, itemClick: sender, itemKey: .promote, params: nil) } @objc func toggleOutlineCaseInsensitiveSearch(_ sender: NSMenuItem) { if (sender.state == .on) { self.outlineIgnoreCaseFlag = false } else { self.outlineIgnoreCaseFlag = true } if (self.outlineSearchField.stringValue.isEmpty == false) { self.tocOutlineView.reloadData() self.tocOutlineView.expandItem(nil, expandChildren: true) } } } // MARK: - KMTocOutlineViewDelegate extension KMLeftSideViewController: KMTocOutlineViewDelegate { func outlineView(_ anOutlineView: NSOutlineView, highlightLevelForRow row: Int) -> Int { if self.tocOutlineView.isEqual(to: anOutlineView) { let numRows = anOutlineView.numberOfRows if let outline = anOutlineView.item(atRow: row) as? CPDFOutline { let firstPage = outline.km_pageIndex var lastPage = self.pageCount() if row + 1 < numRows { lastPage = Int((anOutlineView.item(atRow: row + 1) as? CPDFOutline)?.km_pageIndex ?? UInt(NSNotFound)) } // NSRange range = NSMakeRange(firstPage, MAX(1LU, lastPage - firstPage)); // NSUInteger i, iMax = [lastViewedPages count]; // for (i = 0; i < iMax; i++) { // if (NSLocationInRange((NSUInteger)[lastViewedPages pointerAtIndex:i], range)) // return i; // } } } return NSNotFound } func outlineView(_ anOutlineView: NSOutlineView, imageContextForItem item: Any?) -> AnyObject? { if anOutlineView.isEqual(to: self.tocOutlineView) { if item == nil { return true as AnyObject } if item is CPDFOutline { return (item as! CPDFOutline).destination } } return nil } } extension KMLeftSideViewController: KMNoteOutlineViewDelegate { func outlineView(_ anOutlineView: NSOutlineView, canResizeRowByItem item: AnyObject?) -> Bool? { if anOutlineView.isEqual(to: self.noteOutlineView) { return true } return false } func outlineView(_ anOutlineView: NSOutlineView, setHeight newHeight: CGFloat, ofRowByItem item: AnyObject?) { // [rowHeights setFloat:newHeight forKey:item]; } func outlineView(_ anOutlineView: NSOutlineView, didChangeHiddenOfTableColumn aTableColumn: NSTableColumn) { // if (mwcFlags.autoResizeNoteRows && // [ov isEqual:rightSideController.noteOutlineView] && // [[tableColumn identifier] isEqualToString:NOTE_COLUMNID]) { // [rowHeights removeAllFloats]; // [rightSideController.noteOutlineView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [rightSideController.noteOutlineView numberOfRows])]]; // } } func outlineViewCommandKeyPressedDuringNavigation(_ anOutlineView: NSOutlineView) { // PDFAnnotation *annotation = [[self selectedNotes] lastObject]; // if (annotation) { // [pdfView scrollAnnotationToVisible:annotation]; // [pdfView setActiveAnnotation:annotation]; // } } }