// // KMOutlineViewController.swift // PDF Reader Pro // // Created by lxy on 2022/10/10. // import Cocoa import KMComponentLibrary extension KMNSearchKey.wholeWords { static let outline = "OutlineSearchWholeWordsKey" } extension KMNSearchKey.caseSensitive { static let outline = "OutlineSearchCaseSensitiveKey" } class KMOutlineViewController: KMNBotaBaseViewController { @IBOutlet var contendView: NSView! @IBOutlet weak var topView: NSView! @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var lineView: NSView! @IBOutlet weak var addButton: NSButton! @IBOutlet weak var moreButton: NSButton! @IBOutlet var topSepline: NSView! @IBOutlet weak var emptyView: NSView! @IBOutlet weak var bigTipLabel: NSTextField! @IBOutlet weak var tipLabel: NSTextField! @IBOutlet weak var BOTAOutlineView: KMBOTAOutlineView! var dragPDFOutline : KMBOTAOutlineItem! private var dragPDFOutlines_: [KMBOTAOutlineItem] = [] var renameTextField : NSTextField! var renamePDFOutline : KMBOTAOutlineItem! let moreMenu = NSMenu() var isLocalEvent = false var model = KMNOutlineModel() var handdler = KMNOutlineHanddler() private weak var popover_: NSPopover? private lazy var addButton_: ComponentButton = { let view = ComponentButton() view.properties = ComponentButtonProperty(type: .text_gray, size: .xxs, state: .normal, isDisable: false, onlyIcon: true, keepPressState: false) return view }() private lazy var moreDropdown_: ComponentButton = { let view = ComponentButton() view.properties = ComponentButtonProperty(type: .text_gray, size: .xxs, state: .normal, isDisable: false, onlyIcon: true, icon: NSImage(named: "KMImageNameOutlineMore")) return view }() private var emptyView_: ComponentEmpty = { let view = ComponentEmpty() view.properties = ComponentEmptyProperty(emptyType: .noOutline, state: .normal, image: NSImage(named: "KMImageNameOutlineEmpty"), text: KMLocalizedString("No Outline"), subText: KMLocalizedString("Here is the description.")) return view }() private var groupView_: ComponentGroup? private var menuGroupView_: ComponentGroup? private var outlineView_: KMOutlineView? { get { return BOTAOutlineView.outlineView } } deinit { self.BOTAOutlineView.delegate = nil } override func viewWillDisappear() { super.viewWillDisappear() self.cancelSelect() } override func viewDidLoad() { super.viewDidLoad() handdler.delegate = self titleLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-m-bold") self.topView.wantsLayer = true addButton.image = nil topView.addSubview(searchButton) searchButton.km_add_size_constraint(size: NSMakeSize(24, 24)) searchButton.km_add_centerY_constraint(constant: 1) searchButton.km_add_trailing_constraint(equalTo: addButton, attribute: .leading, constant: -4) searchButton.setTarget(self, action: #selector(_searchAction)) addButton.addSubview(addButton_) addButton_.km_add_size_constraint(size: NSMakeSize(24, 24)) addButton_.km_add_centerX_constraint() addButton_.km_add_centerY_constraint() addButton_.setTarget(self, action: #selector(addNewOutline)) moreButton.image = nil moreButton.addSubview(moreDropdown_) moreDropdown_.km_add_size_constraint(size: NSMakeSize(24, 24)) moreDropdown_.km_add_centerX_constraint() moreDropdown_.km_add_centerY_constraint() moreDropdown_.setTarget(self, action: #selector(_moreAction)) if let data = headerSearchView { topView.addSubview(data) headerSearchView?.frame = topView.bounds headerSearchView?.autoresizingMask = [.width, .height] } hideHeaderSearch() headerSearchView?.itemClick = { [weak self] idx, params in if idx == 1 { // 显示搜索限制条件 guard let button = params.first as? ComponentButton else { return } self?.showSearchGroupView(sender: button) } else if idx == 2 { // 关闭搜索 self?.hideHeaderSearch() self?.reloadData() } } headerSearchView?.valueDidChange = { [weak self] sender, info in let value = info?[.newKey] as? String ?? "" self?.BOTAOutlineView.searchKey = value self?.reloadData() self?.BOTAOutlineView.outlineView.expandItem(nil, expandChildren: true) } emptyView.wantsLayer = true bigTipLabel.stringValue = "" tipLabel.stringValue = "" emptyView.addSubview(emptyView_) emptyView_.km_add_top_constraint(constant: 232) emptyView_.km_add_bottom_constraint() emptyView_.km_add_leading_constraint() emptyView_.km_add_trailing_constraint() self.BOTAOutlineView.delegate = self self.BOTAOutlineView.inputData = self.handdler.outlineRoot() self.BOTAOutlineView.outlineView.doubleAction = #selector(outlineViewDoubleAction) } // To create an outline, please right-click on the selected page and choose \"Add Outline\", or click \"Add\" in the upper right corner. override func updateUILanguage() { super.updateUILanguage() KMMainThreadExecute { self.titleLabel.stringValue = KMLocalizedString("Outline") self.moreButton.toolTip = KMLocalizedString("More") self.addButton.toolTip = KMLocalizedString("Add Outline") } } override func updateUIThemeColor() { super.updateUIThemeColor() KMMainThreadExecute { self.contendView.wantsLayer = true let color = KMNColorTools.colorBg_layoutMiddle() self.contendView.layer?.backgroundColor = color.cgColor self.headerSearchView?.wantsLayer = true self.headerSearchView?.layer?.backgroundColor = color.cgColor self.headerSearchView?.bottomLine.wantsLayer = true self.headerSearchView?.bottomLine.layer?.backgroundColor = KMNColorTools.colorPrimary_border1().cgColor self.titleLabel.textColor = KMNColorTools.colorText_2() self.addButton_.properties.icon = NSImage(named: "KMBookmarkAdd") self.addButton_.reloadData() self.searchButton.properties.icon = NSImage(named: "KMImageNameOutlineSearch") self.searchButton.reloadData() let dividerColor = KMNColorTools.colorBorder_divider() self.topSepline.wantsLayer = true self.topSepline.layer?.backgroundColor = dividerColor.cgColor self.lineView.backgroundColor(dividerColor) } } override func showHeaderSearch() { super.showHeaderSearch() BOTAOutlineView.isSearchMode = true } override func hideHeaderSearch() { super.hideHeaderSearch() BOTAOutlineView.isSearchMode = false } func addRightMenu(view: NSView, event: NSEvent) { let point = event.locationInWindow let tempView = view var viewHeight: CGFloat = 0 let items: [String] = ["Add Item", "Add Sub-Item", "Add A Higher Level","", "Delete","", "Edit", "Rename", "Change Destination","", "Promote", "Demote"] var menuItemArr: [ComponentMenuitemProperty] = [] for value in items { if value.count == 0 { let property: ComponentMenuitemProperty = ComponentMenuitemProperty.divider() menuItemArr.append(property) viewHeight += 8 } else { let properties_Menuitem: ComponentMenuitemProperty = ComponentMenuitemProperty(multipleSelect: false, itemSelected: false, isDisabled: false, keyEquivalent: nil, text: KMLocalizedString(value), identifier: value) menuItemArr.append(properties_Menuitem) viewHeight += 36 } } if _isEmptySelection() { for data in menuItemArr { if data.text == KMLocalizedString("Add Item") { data.isDisabled = false } else { data.isDisabled = true } } } else if _isMutilSelection() { for data in menuItemArr { if data.text == KMLocalizedString("Delete") { data.isDisabled = false } else { data.isDisabled = true } } } else { let clickedRow = BOTAOutlineView.outlineView.clickedRow let outlineItem = BOTAOutlineView.outlineView.item(atRow: clickedRow) as? KMBOTAOutlineItem let idx = outlineItem?.outline.index ?? 0 let canDemote = idx > 0 let grandparentOutline = outlineItem?.outline.parent?.parent let canPromote = grandparentOutline != nil let canAddHigher = grandparentOutline != nil if BOTAOutlineView.isValidSearchMode() { for data in menuItemArr { if data.text == KMLocalizedString("Delete") || data.text == KMLocalizedString("Change Destination") { data.isDisabled = false } else if data.text == KMLocalizedString("Demote") { data.isDisabled = !canDemote } else if data.text == KMLocalizedString("Promote") { data.isDisabled = !canPromote } else { data.isDisabled = true } } } else { for data in menuItemArr { if data.text == KMLocalizedString("Add Sub-Item") || data.text == KMLocalizedString("Change Destination") { data.isDisabled = false } else if data.text == KMLocalizedString("Demote") { data.isDisabled = !canDemote } else if data.text == KMLocalizedString("Promote") { data.isDisabled = !canPromote } else if data.text == KMLocalizedString("Add A Higher Level") { data.isDisabled = !canAddHigher } } } } if menuGroupView_ == nil { menuGroupView_ = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle()) } if menuGroupView_ != nil { menuGroupView_?.clickedAutoHide = false menuGroupView_?.groupDelegate = self menuGroupView_?.frame = CGRectMake(0, 0, 180, viewHeight) menuGroupView_?.updateGroupInfo(menuItemArr) menuGroupView_?.showWithPoint(CGPoint(x: point.x, y: point.y - viewHeight), relativeTo: tempView) } } func reloadData() { self.BOTAOutlineView.reloadData(expandItemType: .none) } func editOutlineUI(editVC : KMOutlineEditViewController!) { if editVC.pageRadio.properties.checkboxType == .selected { let index = Int(editVC.pageInput.properties.text) ?? 0 let pageIndex = max(0, index-1) if editVC.originalDestination?.pageIndex != pageIndex { let page = editVC.pdfView?.document.page(at: UInt(pageIndex)) if page != nil { let destination = CPDFDestination.init(document: editVC.pdfView!.document, pageIndex: pageIndex) editVC.outline?.destination = destination } else { __NSBeep() } } } else if editVC.webRaido.properties.checkboxType == .selected { if editVC.originalURLString != editVC.webInput.properties.text { var urlString = editVC.webInput.properties.text let tLowerUrl = urlString.lowercased() if !tLowerUrl.hasPrefix("https://") && !tLowerUrl.hasPrefix("pf]://") && !urlString.hasPrefix("https://") && urlString.lengthOfBytes(using: String.Encoding(rawValue: String.Encoding.utf16.rawValue)) > 0 { urlString = "http://\(urlString)" } let action = CPDFURLAction.init(url: urlString) editVC.outline?.action = action } } else if editVC.emailRadio.properties.checkboxType == .selected { var mailString = editVC.emailInput.properties.text let tLowerStr = mailString.lowercased() if !tLowerStr.hasPrefix("mailto:") { mailString = "mailto:\(mailString)" } if mailString != editVC.originalURLString { var action = CPDFURLAction.init(url: mailString) if action?.url == nil { action = CPDFURLAction.init(url: "mailto:") } editVC.outline?.action = action } } } // MARK: - Private Methods private func _showAlert(style: NSAlert.Style, message: String, info: String, buttons: [String]) -> NSApplication.ModalResponse { let alert = NSAlert() alert.alertStyle = style alert.messageText = message alert.informativeText = info for button in buttons { alert.addButton(withTitle: button) } return alert.runModal() } @objc private func _moreAction() { self.showGroupView() } private func _isEmptySelection() -> Bool { return BOTAOutlineView.outlineView.clickedRow == -1 } private func _isMutilSelection() -> Bool { return BOTAOutlineView.outlineView.selectedRowIndexes.count > 1 } //MARK: - GroupView func showGroupView() { var viewHeight: CGFloat = 8 var menuItemArr: [ComponentMenuitemProperty] = [] for i in ["Expand All", "Collapse All", "Remove All Outlines"] { let properties_Menuitem: ComponentMenuitemProperty = ComponentMenuitemProperty(multipleSelect: false, itemSelected: false, isDisabled: false, keyEquivalent: nil, text: KMLocalizedString(i)) menuItemArr.append(properties_Menuitem) viewHeight += 36 } if let data = menuItemArr.first { var canExpand = false for row in 0..= 0) { self.renameItemAction() } } @objc func addItemAction() { guard let outlineView = BOTAOutlineView.outlineView else { return } let selectRowIndexs = outlineView.selectedRowIndexes let dataCount = BOTAOutlineView.data?.children.count ?? 0 var index: Int = 0 var parent: KMBOTAOutlineItem? var outlineItem: KMBOTAOutlineItem? if selectRowIndexs.count == 0 { var lastOulineItem: KMBOTAOutlineItem? if dataCount == 0 { let item = KMBOTAOutlineItem() item.outline = self.handdler.document!.setNewOutlineRoot() item.parent = nil parent = item lastOulineItem = item } else { outlineItem = outlineView.item(atRow: outlineView.numberOfRows - 1) as? KMBOTAOutlineItem lastOulineItem = outlineItem while lastOulineItem?.parent != nil { lastOulineItem = lastOulineItem?.parent } parent = lastOulineItem } index = Int(lastOulineItem?.outline.numberOfChildren ?? 0) } else { outlineItem = outlineView.item(atRow: selectRowIndexs.last ?? 0) as? KMBOTAOutlineItem parent = outlineItem?.parent ?? KMBOTAOutlineItem() index = Int(((outlineItem?.outline.index) ?? 0) + 1) } self.addOutlineToIndex(index: index, parent: parent) } @objc func addChildItemAction() { let outlineView: KMOutlineView = self.BOTAOutlineView.outlineView let selectRowIndexs = outlineView.selectedRowIndexes if selectRowIndexs.count != 0 { let outlineItem: KMBOTAOutlineItem = outlineView.item(atRow: selectRowIndexs.last!) as! KMBOTAOutlineItem let index = outlineItem.outline.numberOfChildren self.addOutlineToIndex(index: NSInteger(index), parent: outlineItem) } } @objc func addHigherItemAction() { let outlineView: KMOutlineView = self.BOTAOutlineView.outlineView let selectRowIndexs = outlineView.selectedRowIndexes if selectRowIndexs.count != 0 { let outlineItem: KMBOTAOutlineItem = outlineView.item(atRow: selectRowIndexs.last!) as! KMBOTAOutlineItem var parent = outlineItem.parent let index = NSInteger(parent!.outline.index) + 1 parent = parent?.parent if parent != nil { self.addOutlineToIndex(index: index, parent: parent!) } } } @objc func deleteItemAction() { let outlineView: KMOutlineView = self.BOTAOutlineView.outlineView let selectRowIndexs = outlineView.selectedRowIndexes if selectRowIndexs.count != 0 { var outlineItems: [KMBOTAOutlineItem] = [] for index in selectRowIndexs { let outlineItem: KMBOTAOutlineItem = self.BOTAOutlineView.outlineView.item(atRow: index) as! KMBOTAOutlineItem outlineItem.toIndex = index outlineItem.parent = outlineItem.parent ?? KMBOTAOutlineItem() outlineItems.append(outlineItem) } self.deleteOutline(outlineItems: outlineItems) } } @objc func editItemAction() { let clickedRow = BOTAOutlineView.outlineView.clickedRow if clickedRow < 0 { NSSound.beep() return } if let rowView = self.BOTAOutlineView.outlineView.rowView(atRow: clickedRow, makeIfNecessary: true) { let item = self.BOTAOutlineView.outlineView.item(atRow: clickedRow) as? KMBOTAOutlineItem let vc = KMOutlineEditViewController.init(outline: item?.outline, document: self.handdler.pdfView) vc.pageCount = handdler.pageCount() vc.itemClick = { [weak self] idx, params in if idx == 1 { self?.popover_?.close() } else if idx == 2 { self?.popover_?.close() if let viewC = params.first as? KMOutlineEditViewController { let resp = self?._showAlert(style: .informational, message: KMLocalizedString("Are you sure you want to apply edits to this outline?"), info: "", buttons: [KMLocalizedString("Apply"), KMLocalizedString("Cancel")]) if resp == .alertFirstButtonReturn { self?.editOutlineUI(editVC: viewC) } } } } let popover = NSPopover() popover_ = popover popover.delegate = self popover.contentViewController = vc popover.animates = true popover.behavior = .transient popover.setValue(true, forKey: "shouldHideAnchor") popover.show(relativeTo: rowView.bounds, of: rowView, preferredEdge: .minX) } } @objc func renameItemAction() { if self.BOTAOutlineView.outlineView.clickedRow >= 0 { self.renameOutlineWithRow(row: self.BOTAOutlineView.outlineView.clickedRow) } else { __NSBeep() } } @objc func changeItemAction() { guard let currentDest = handdler.currentDestination() else { NSSound.beep() return } guard let item = outlineView_?.clickedItem() as? KMBOTAOutlineItem else { NSSound.beep() return } let resp = _showAlert(style: .informational, message: KMLocalizedString("Are you sure you want to set the destination as the current location?"), info: "", buttons: [KMLocalizedString("Yes"), KMLocalizedString("No")]) if resp == .alertFirstButtonReturn { handdler.changeLocation(outlineItem: item, destination: currentDest) } } @objc func promoteItemAction() { if self.BOTAOutlineView.outlineView.clickedRow >= 0 { let outlineItem: KMBOTAOutlineItem = self.BOTAOutlineView.outlineView.item(atRow: self.BOTAOutlineView.outlineView.clickedRow) as! KMBOTAOutlineItem var parent = outlineItem.parent let index = NSInteger(parent!.outline.index) + 1 parent = parent?.parent if parent != nil { self.moveOutline(outlineItem: outlineItem, index: index, parent: parent) } } } @objc func demoteItemAction() { if self.BOTAOutlineView.outlineView.clickedRow >= 0 { let outlineItem: KMBOTAOutlineItem = self.BOTAOutlineView.outlineView.item(atRow: self.BOTAOutlineView.outlineView.clickedRow) as! KMBOTAOutlineItem let parent = outlineItem.parent let newParent = parent?.children[Int(outlineItem.outline.index) - 1] let index = newParent?.children.count if (index != nil) { self.moveOutline(outlineItem: outlineItem, index: NSInteger(index ?? 0), parent: newParent) } } } @objc private func expandAllComments(item: NSMenuItem) { self.BOTAOutlineView.expandAllComments(item: item) } @objc private func collapseAllComments(item: NSMenuItem) { self.BOTAOutlineView.collapseAllComments(item: item) } @objc private func removeAllOutlineItem(item: NSMenuItem) { 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.removeAllOutline() } } @objc private func removeAllOutline() { guard let data = self.BOTAOutlineView.data else { return } for item in data.children { item.toIndex = Int(item.outline.index) } self.deleteOutline(outlineItems: data.children) self.BOTAOutlineView.reloadData(expandItemType: .none) } @objc private func _searchAction() { searchButton.properties.state = .normal searchButton.reloadData() showHeaderSearch() } } //MARK: - Action extension KMOutlineViewController { @IBAction func addNewOutline(_ sender: Any) { self.addItemAction() } @IBAction func escButtonAction(_ sender: Any) { self.cancelSelect() } func cancelSelect() { self.BOTAOutlineView.cancelSelect() } func renameOutlineWithRow(row: NSInteger) { DispatchQueue.main.async { self.renamePDFOutline = self.BOTAOutlineView.outlineView.item(atRow: row) as? KMBOTAOutlineItem let cell : KMBOTAOutlineCellView = self.BOTAOutlineView.outlineView.view(atColumn: 0, row: row, makeIfNecessary: true) as! KMBOTAOutlineCellView self.renameTextField = cell.titleLabel self.renameTextField.delegate = self self.renameTextField.isEditable = true self.renameTextField.becomeFirstResponder() } } func addOutlineToIndex(index: Int, parent: KMBOTAOutlineItem?) { let pageIndex = self.handdler.currentPageIndex let label = self.fetchCurrentLabel(pageIndex: pageIndex) let destination = self.handdler.currentDestination() self.addOutlineToIndex(index: index, pageIndex: pageIndex, destination: destination, lable: label, parent: parent) } func addOutlineToIndex(index: Int, pageIndex: Int, destination: CPDFDestination?, lable: String, parent: KMBOTAOutlineItem?) { let outlineItem = KMBOTAOutlineItem() outlineItem.destination = destination outlineItem.label = lable outlineItem.parent = parent outlineItem.toIndex = index self.addOutline(outlineItems: [outlineItem]) let tempOutlineView = self.BOTAOutlineView! var index = -1 if tempOutlineView.outlineView.numberOfRows == 1 || tempOutlineView.data == nil { index = 0 } else { index = tempOutlineView.outlineView.row(forItem: outlineItem) } tempOutlineView.selectIndex(index: index) //滑动到指定位置 if(tempOutlineView.outlineView.selectedRow >= 0) { self.renameOutlineWithRow(row: tempOutlineView.outlineView.selectedRow) } let row = tempOutlineView.outlineView.row(forItem: outlineItem) if Thread.current.isMainThread { tempOutlineView.outlineView.scrollToVisible(tempOutlineView.outlineView.rect(ofRow: row)) } else { DispatchQueue.main.async { tempOutlineView.outlineView.scrollToVisible(tempOutlineView.outlineView.rect(ofRow: row)) } } } func updateOutlineSelection() { let currentPageIndex = self.handdler.currentPageIndex let numRows = self.BOTAOutlineView.outlineView.numberOfRows if numRows > 0 { for i in 0...numRows - 1 { let outlineItem: KMBOTAOutlineItem = self.BOTAOutlineView.outlineView.item(atRow: i) as! KMBOTAOutlineItem if (outlineItem.outline.destination == nil) { continue } if outlineItem.outline.destination.pageIndex == currentPageIndex { self.BOTAOutlineView.selectIndex(index: currentPageIndex) break } } } } func fetchCurrentLabel(pageIndex: Int) -> String { var label = "\(NSLocalizedString("Page", comment: ""))\(pageIndex + 1)" let currentSelection = self.handdler.currentSelection() if currentSelection != nil && currentSelection?.selectionsByLine != nil { for data in currentSelection?.selectionsByLine ?? [] { label = data.string() ?? "" } } return label } } //MARK: - KMBOTAOutlineViewDelegate extension KMOutlineViewController: KMBOTAOutlineViewDelegate { func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, rightDidMoseDown: KMBOTAOutlineItem, event: NSEvent) { let row = outlineView.outlineView.row(forItem: rightDidMoseDown) if outlineView.outlineView.rowView(atRow: row, makeIfNecessary: false) != nil { let rowView = outlineView.outlineView.rowView(atRow: row, makeIfNecessary: false) self.addRightMenu(view: rowView!, event: event) } } func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didReloadData: KMBOTAOutlineItem) { self.updateExtempViewState() } func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didSelectItem: [KMBOTAOutlineItem]) { if outlineView_?.selectedRowIndexes.count == 1 { isLocalEvent = true guard let item = outlineView_?.selectedItem() as? KMBOTAOutlineItem else { return } handdler.selectOutline(item.outline) } } func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool { if outlineView.outlineView.selectedRow == -1 { return false } self.dragPDFOutline = items.first as? KMBOTAOutlineItem self.dragPDFOutlines_ = items as? [KMBOTAOutlineItem] ?? [] let indexSet = [outlineView.outlineView.clickedRow] let indexSetData: Data = try!NSKeyedArchiver.archivedData(withRootObject: indexSet, requiringSecureCoding: true) pasteboard.declareTypes([NSPasteboard.PasteboardType(rawValue: "kKMPDFViewOutlineDragDataType")], owner: self) pasteboard.setData(indexSetData, forType: NSPasteboard.PasteboardType(rawValue: NSPasteboard.PasteboardType.RawValue("kKMPDFViewOutlineDragDataType"))) return true } func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { var dragOperation = NSDragOperation.init(rawValue: 0) if index >= 0 { dragOperation = NSDragOperation.move } return dragOperation } func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { guard let dragOutlineItem = self.dragPDFOutline else { return false } let outlineItem: KMBOTAOutlineItem = (item ?? KMBOTAOutlineItem()) as! KMBOTAOutlineItem if index < 0 { return false } for dragOutlineItem in dragPDFOutlines_ { if outlineItem.parent == nil { var root = dragOutlineItem.parent while root?.parent?.children != nil { root = root?.parent! } if dragOutlineItem.parent!.isEqual(root) { if dragOutlineItem.outline.index > index { self.moveOutline(outlineItem: dragOutlineItem, index: index, parent: root) } else { self.moveOutline(outlineItem: dragOutlineItem, index: index - 1, parent: root) } } else { self.moveOutline(outlineItem: dragOutlineItem, index: index, parent: root) } } else { if dragOutlineItem.parent!.isEqual(item) { // if dragOutlineItem.outline.index != 0 { if dragOutlineItem.outline.index > index { self.moveOutline(outlineItem: dragOutlineItem, index: index, parent: item as? KMBOTAOutlineItem) } else { self.moveOutline(outlineItem: dragOutlineItem, index: index - 1, parent: item as? KMBOTAOutlineItem) } // } else { // return false // } } else { var tOutline = outlineItem var isContains = false while (tOutline.parent != nil) { if tOutline.outline.isEqual(dragOutlineItem.outline) { isContains = true break } tOutline = tOutline.parent! } if isContains == false { self.moveOutline(outlineItem: dragOutlineItem, index: index, parent: item as? KMBOTAOutlineItem) } } } } self.BOTAOutlineView.selectItem(outlineItem: dragOutlineItem) return true } } //MARK: - NSTextFieldDelegate extension KMOutlineViewController: NSTextFieldDelegate { func controlTextDidEndEditing(_ obj: Notification) { if (self.renameTextField.isEqual(obj.object)) { let textField : NSTextField = obj.object as! NSTextField self.renamePDFOutline(outlineItem: self.renamePDFOutline, label: textField.stringValue) } } } //MARK: - NSPopoverDelegate extension KMOutlineViewController: NSPopoverDelegate { func popoverWillClose(_ notification: Notification) { let popover : NSPopover = notification.object as! NSPopover if popover.contentViewController!.isKind(of: KMOutlineEditViewController.self) { } } func popoverDidClose(_ notification: Notification) { if popover_ == (notification.object as? NSPopover) { popover_ = nil } } } //MARK: - NSMenuItemValidation extension KMOutlineViewController: NSMenuDelegate, NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { let action = menuItem.action if (action == #selector(undo)) { return handdler.canUndo() } if (action == #selector(redo)) { return handdler.canRedo() } return true } } // MARK: - KMNOutlineHanddlerDelegate extension KMOutlineViewController: KMNOutlineHanddlerDelegate { func handdler(_ handdler: KMNOutlineHanddler, didAdd info: [String : Any]?) { let tempOutlineItems = info?["data"] as? [KMBOTAOutlineItem] ?? [] let tempOutlineView = self.BOTAOutlineView if tempOutlineView?.data?.children.count == 0 || tempOutlineView?.data == nil { tempOutlineView?.inputData = self.handdler.outlineRoot() } else { if BOTAOutlineView.isValidSearchMode() { BOTAOutlineView.reloadSearchChildren(item: BOTAOutlineView.data) } tempOutlineView?.outlineView.reloadData() } //展开 // DispatchQueue.main.async { for outlineItem in tempOutlineItems { var tempParent = outlineItem while tempParent.parent != nil { tempParent.isItemExpanded = true tempParent = tempParent.parent! tempOutlineView?.outlineView.expandItem(tempParent) } tempOutlineView?.outlineView.expandItem(tempParent.parent) } // } self.updateExtempViewState() } func handdler(_ handdler: KMNOutlineHanddler, didRemove info: [String : Any]?) { let tempOutlineItems = info?["data"] as? [KMBOTAOutlineItem] ?? [] let tempOutlineView = self.BOTAOutlineView //展开 for outlineItem in tempOutlineItems { outlineItem.parent?.isItemExpanded = true tempOutlineView?.outlineView.expandItem(outlineItem.parent) } if BOTAOutlineView.isValidSearchMode() { BOTAOutlineView.reloadSearchChildren(item: BOTAOutlineView.data) } tempOutlineView?.outlineView.reloadData() //删除需要取消选中 tempOutlineView?.cancelSelect() //刷新nil数据 self.updateExtempViewState() } func handdler(_ handdler: KMNOutlineHanddler, didRename outline: CPDFOutline?, info: [String : Any]?) { let outlineItem = info?["data"] as? KMBOTAOutlineItem let tempOutlineView = self.BOTAOutlineView tempOutlineView?.outlineView.reloadItem(outlineItem) } func handdler(_ handdler: KMNOutlineHanddler, didChangeLocation outline: CPDFOutline?, info: [String : Any]?) { let outlineItem = info?["data"] as? KMBOTAOutlineItem let tempOutlineView = self.BOTAOutlineView tempOutlineView?.outlineView.reloadItem(outlineItem) } func handdler(_ handdler: KMNOutlineHanddler, didMove outline: CPDFOutline?, info: [String : Any]?) { guard let outlineItem = info?["data"] as? KMBOTAOutlineItem else { return } let parent = info?["parent"] as? KMBOTAOutlineItem let tempOutlineView = self.BOTAOutlineView let index = info?["index"] as? Int ?? 0 if BOTAOutlineView.isValidSearchMode() { BOTAOutlineView.reloadSearchChildren(item: BOTAOutlineView.data) } //显示数据刷新 outlineItem.parent?.children.removeObject(outlineItem) parent?.children.insert(outlineItem, at: index) outlineItem.parent = parent tempOutlineView?.outlineView.reloadData() tempOutlineView?.cancelSelect() //展开 outlineItem.isItemExpanded = true outlineItem.parent?.isItemExpanded = true tempOutlineView?.outlineView.expandItem(outlineItem) tempOutlineView?.outlineView.expandItem(outlineItem.parent) } } //MARK: - 快捷键 extension KMOutlineViewController { @IBAction func delete(_ sender: Any) { self.deleteItemAction() } } //MARK: - undoRedo extension KMOutlineViewController { func moveOutline(outlineItem: KMBOTAOutlineItem, index: NSInteger, parent: KMBOTAOutlineItem!) { handdler.moveOutline(outlineItem: outlineItem, index: index, parent: parent) } func changeLocation(outlineItem: KMBOTAOutlineItem, destination: CPDFDestination) { handdler.changeLocation(outlineItem: outlineItem, destination: destination) } func renamePDFOutline(outlineItem: KMBOTAOutlineItem!, label: String) { let tempOutlineView = self.BOTAOutlineView! self.view.window?.makeFirstResponder(tempOutlineView.outlineView) self.renameTextField.isEditable = false if outlineItem.outline.label == label { return } handdler.renamePDFOutline(outlineItem: outlineItem, label: label) } func deleteOutline(outlineItems: [KMBOTAOutlineItem]) { NSApp.mainWindow?.makeFirstResponder(self.BOTAOutlineView) let tempOutlineView = self.BOTAOutlineView! handdler.deleteOutline(outlineItems: outlineItems) } func addOutline(outlineItems: [KMBOTAOutlineItem]) { NSApp.mainWindow?.makeFirstResponder(self.BOTAOutlineView) let tempOutlineView = self.BOTAOutlineView! //先取消选中 tempOutlineView.cancelSelect() var tempOutlineItems: [KMBOTAOutlineItem] = outlineItems tempOutlineItems.sort(){$0.toIndex < $1.toIndex} handdler.addOutline(outlineItems: tempOutlineItems) } @IBAction func undo(_ sender: Any) { handdler.undo() } @IBAction func redo(_ sender: Any) { handdler.redo() } } //MARK: - ComponentDropdownDelegate extension KMOutlineViewController: ComponentDropdownDelegate { func componentDropdownDidShowMenuItem(dropdown: ComponentDropdown) { showGroupView() } } //MARK: - ComponentGroupDelegate extension KMOutlineViewController: ComponentGroupDelegate { func componentGroupDidDismiss(group: ComponentGroup?) { if group == groupView_ { removeGroupView() } else if group == menuGroupView_ { group?.removeFromSuperview() menuGroupView_ = nil } else if group == searchGroupView { // searchGroupView_ = nil searchGroupTarget?.properties.state = .normal searchGroupTarget?.reloadData() searchGroupTarget = nil } } func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) { if group == groupView_ { if let selItem = menuItemProperty { let index = group?.menuItemArr.firstIndex(of: selItem) if index == 0 { expandAllComments(item: NSMenuItem()) } else if index == 1 { collapseAllComments(item: NSMenuItem()) } else if index == 2 { removeAllOutlineItem(item: NSMenuItem()) } } } else if group == menuGroupView_ { if let selItem = menuItemProperty { let index = group?.menuItemArr.firstIndex(of: selItem) if index == 0 { addItemAction() } else if index == 1 { addChildItemAction() } else if index == 2 { addHigherItemAction() } else if index == 4 { deleteItemAction() } else if index == 6 { group?.removeFromSuperview() editItemAction() } else if index == 7 { renameItemAction() } else if index == 8 { changeItemAction() } else if index == 10 { promoteItemAction() } else if index == 11 { demoteItemAction() } group?.removeFromSuperview() } } else if group == searchGroupView { guard let menuI = menuItemProperty else { return } let idx = group?.menuItemArr.firstIndex(of: menuI) if idx == 0 { let key = KMNSearchKey.wholeWords.outline let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) BOTAOutlineView.wholeWords = !value } else if idx == 1 { let key = KMNSearchKey.caseSensitive.outline let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) BOTAOutlineView.caseSensitive = !value } } } }