// // KMBOTAOutlineView.swift // PDF Reader Pro // // Created by lizhe on 2023/4/2. // import Cocoa protocol KMBOTAOutlineViewDelegate: NSObjectProtocol { func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didReloadData: KMBOTAOutlineItem) func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didSelectItem: [KMBOTAOutlineItem]) func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, rightDidMoseDown: KMBOTAOutlineItem, event: NSEvent) func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool } class KMBOTAOutlineView: BaseXibView { @IBOutlet weak var outlineView: KMOutlineView! @IBOutlet weak var scrollView: NSScrollView! weak var delegate: KMBOTAOutlineViewDelegate? var inputData: CPDFOutline? { didSet { self.reloadData(expandItemType: .none) } } var data: KMBOTAOutlineItem? var selectItems: [KMBOTAOutlineItem]? var dragPDFOutline: KMBOTAOutlineItem! var isSearchMode = false var searchKey = "" override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. } override func awakeFromNib() { super.awakeFromNib() self.setup() } func setup() { self.scrollView.backgroundColor(NSColor.km_init(hex: "#F2F9FF")) self.outlineView.registerForDraggedTypes([NSPasteboard.PasteboardType(rawValue: "kKMPDFViewOutlineDragDataType")]) self.outlineView.delegate = self self.outlineView.dataSource = self self.outlineView.selectionHighlightStyle = NSTableView.SelectionHighlightStyle.none; self.outlineView.allowsMultipleSelection = true // self.outlineView.indentationPerLevel = 0 outlineView.tocDelegate = self outlineView.hasImageToolTips = true } func reloadData(expandItemType: KMOutlineViewExpandItemType = .none) { if self.inputData != nil { //获取数据 var tempData: KMBOTAOutlineItem = KMBOTAOutlineItem() if self.inputData!.numberOfChildren > 0 { let outline: CPDFOutline = self.inputData! tempData = self.addOutlineItem(outlineItem:tempData, outline: outline, expandItemType: expandItemType) } else { tempData.outline = CPDFOutline() if expandItemType == .collapse { tempData.isItemExpanded = false } else if (expandItemType == .expand) { tempData.isItemExpanded = true } } tempData.parent = nil self.data = tempData self.outlineView.reloadData() } self.delegate?.BOTAOutlineView(self, didReloadData: self.data ?? KMBOTAOutlineItem()) } func addOutlineItem(outlineItem: KMBOTAOutlineItem?, outline: CPDFOutline, expandItemType: KMOutlineViewExpandItemType = .none) -> KMBOTAOutlineItem { //添加根节点 let item: KMBOTAOutlineItem = KMBOTAOutlineItem() item.outline = outline item.parent = outlineItem var items: [KMBOTAOutlineItem] = [] if outline.numberOfChildren > 0 { for index in 0...outline.numberOfChildren - 1 { let children: CPDFOutline = outline.child(at: index) let childrenItem = self.addOutlineItem(outlineItem: item, outline: children, expandItemType: expandItemType) if expandItemType == .collapse { childrenItem.isItemExpanded = false } else if (expandItemType == .expand) { childrenItem.isItemExpanded = true } items.append(childrenItem) } } item.children = items return item } func updateUI() { } func updateLanguage() { } func hasContainString(_ searchString: String, rootOutline outline: CPDFOutline) -> Bool { var label = outline.label ?? "" var searchLabel = searchString // if self.outlineIgnoreCaseFlag { // label = label.lowercased() // searchLabel = searchLabel.lowercased() // } if label.contains(searchLabel) { // if ([outline.label rangeOfString:searchString options:self.outlineIgnoreCaseFlag?NSCaseInsensitiveSearch:0].location != NSNotFound){ return true } else { var subHas = false for i in 0 ..< outline.numberOfChildren { if let subOutline = outline.child(at: i) { subHas = self.hasContainString(searchString, rootOutline: subOutline) } else { continue } if (subHas) { break } } return subHas } } func fetchOutlines(for item: KMBOTAOutlineItem, searchString: String) -> [KMBOTAOutlineItem] { var items: [KMBOTAOutlineItem] = [] for childI in item.children { if self.hasContainString(searchString, rootOutline: childI.outline) { items.append(childI) } } return items } } //MARK: NSOutlineViewDataSource,NSOutlineViewDelegate extension KMBOTAOutlineView : NSOutlineViewDataSource,NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { let outline = item as? KMBOTAOutlineItem if outline != nil { if isSearchMode { let ols = self.fetchOutlines(for: outline!, searchString: searchKey) return ols.count } return outline?.children.count ?? 0 } else { if self.isSearchMode { // 是否为搜索模块 if self.hasContainString(searchKey, rootOutline: outline!.outline) == false { // self.showSearchOutlineBlankState(true) return 0 } // self.showSearchOutlineBlankState(false) let ols = self.fetchOutlines(for: outline!, searchString: searchKey) return ols.count } return Int(self.data?.outline.numberOfChildren ?? 0) } } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { let outline = item as? KMBOTAOutlineItem if outline != nil { if isSearchMode { let ols = self.fetchOutlines(for: outline!, searchString: searchKey) return ols.safe_element(for: index) as Any } return outline?.children[index] as Any } else { if self.isSearchMode { // 是否为搜索模块 if self.hasContainString(searchKey, rootOutline: outline!.outline) == false { // self.showSearchOutlineBlankState(true) return 0 } // self.showSearchOutlineBlankState(false) let ols = self.fetchOutlines(for: outline!, searchString: searchKey) return ols.count } return self.data?.children[index] as Any } } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { let outline = item as? KMBOTAOutlineItem if outline != nil && outline?.children.count != 0 { return true } return false } func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool { if let item = item as? KMBOTAOutlineItem { if !item.isItemExpanded { item.isItemExpanded = true outlineView.animator().expandItem(item, expandChildren: true) return false } } return true } func outlineView(_ outlineView: NSOutlineView, shouldCollapseItem item: Any) -> Bool { if let item = item as? KMBOTAOutlineItem { if item.isItemExpanded { item.isItemExpanded = false outlineView.animator().collapseItem(item, collapseChildren: true) return false } } return true } func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { let cell : KMBOTAOutlineCellView = KMBOTAOutlineCellView.init() cell.model = item as? KMBOTAOutlineItem cell.iconAction = { [unowned self] view in let rowIndex = outlineView.row(forItem: item) let rowView = outlineView.rowView(atRow: rowIndex, makeIfNecessary: false) self.didSelectItem(view: (rowView as? KMBOTAOutlineRowView), event: NSEvent()) if self.selectItems?.count == 1 { self.needOpenOrCloseItem(oulineItem: (self.selectItems?.first)!) } } return cell } func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? { let rowView = KMBOTAOutlineRowView() rowView.model = item as? KMBOTAOutlineItem rowView.mouseDownCallback = { [unowned self] (view, event) in self.didSelectItem(view: view, event: event) } rowView.rightMouseCallback = { [unowned self] (view, event) in if !KMOCToolClass.arrayContains(array: self.selectItems, annotation: item) || self.selectItems!.count == 1 { self.selectItem(outlineItem: item as! KMBOTAOutlineItem) } self.delegate?.BOTAOutlineView(self, rightDidMoseDown: item as! KMBOTAOutlineItem, event: event) } rowView.hoverCallback = { [unowned self] (mouseEntered, mouseBox) in self.outlineView.enumerateAvailableRowViews { view, row in if view is KMBOTAOutlineRowView { (view as? KMBOTAOutlineRowView)?.model.hover = false (view as? KMBOTAOutlineRowView)?.reloadData() } } if mouseEntered { rowView.model.hover = true } else { rowView.model.hover = false } } return rowView } func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { if item is KMBOTAOutlineItem { let tempItem: KMBOTAOutlineItem = item as! KMBOTAOutlineItem let string: NSString = tempItem.outline.label as NSString let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.32 paragraphStyle.alignment = .left let attributes = [NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.font : NSFont.SFProTextRegularFont(14.0)] let size = string.boundingRect(with: NSMakeSize(outlineView.frame.size.width - 30, 200), options: NSString.DrawingOptions(rawValue: 3), attributes: attributes) return max(40, size.height + 16) } return 40 } func outlineView(_ outlineView: NSOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool { guard let callBack = self.delegate else { return false} return callBack.BOTAOutlineView(self, writeItems: items, to: pasteboard) } func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { guard let callBack = self.delegate else { return NSDragOperation.init(rawValue: 0)} return callBack.BOTAOutlineView(self, validateDrop: info, proposedItem: item, proposedChildIndex: index) } func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { guard let callBack = self.delegate else { return false} return callBack.BOTAOutlineView(self, acceptDrop: info, item: item, childIndex: index) } func outlineViewSelectionDidChange(_ notification: Notification) { // if self.outlineView.selectedRow == -1 { // self.cancelSelect() // } } } extension KMBOTAOutlineView: KMTocOutlineViewDelegate { func outlineView(_ anOutlineView: NSOutlineView, imageContextForItem item: Any?) -> AnyObject? { if item == nil { return true as AnyObject } if let data = item as? KMBOTAOutlineItem { return data.outline.destination } return nil } } //MARK: Action extension KMBOTAOutlineView { @objc func expandAllComments(item: NSMenuItem) { self.reloadData(expandItemType: .expand) self.outlineView.reloadData() self.outlineView.expandItem(nil, expandChildren: true) } @objc func collapseAllComments(item: NSMenuItem) { self.reloadData(expandItemType: .collapse) self.outlineView.reloadData() self.outlineView.collapseItem(nil, collapseChildren: true) } func selectItem(outlineItem: KMBOTAOutlineItem) { let index = self.outlineView.row(forItem: outlineItem) self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false) self.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false) } func selectIndex(index: Int) { self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false) self.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false) } func cancelSelect() { guard let items = self.selectItems else { return } self.outlineView.deselectAll(nil) for model in items { model.select = false self.outlineView.reloadItem(model) } } func didSelectItem(view: KMBOTAOutlineRowView?, event: NSEvent, isNeedDelegate: Bool = true) { //当选中一个时 if view != nil && (self.outlineView.selectedRowIndexes.count == 1 || (!event.modifierFlags.contains(NSEvent.ModifierFlags.command) && !event.modifierFlags.contains(NSEvent.ModifierFlags.shift))) { let rowView: KMBOTAOutlineRowView = view! let index = self.outlineView.row(for: rowView) self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false) } //原始数据置空 if self.selectItems != nil { for item in self.selectItems! { item.select = false let index = self.outlineView.row(forItem: item) if index != -1 { if self.outlineView.rowView(atRow: index, makeIfNecessary: false) != nil { let rowView: KMBOTAOutlineRowView = self.outlineView.rowView(atRow: index, makeIfNecessary: false) as! KMBOTAOutlineRowView rowView.reloadData() } } } } //获取最新数据 var items: [KMBOTAOutlineItem] = [] for index in self.outlineView.selectedRowIndexes { if index != -1 { let item: KMBOTAOutlineItem = self.outlineView.item(atRow: index) as! KMBOTAOutlineItem item.select = true items.append(item) //刷新数据 if self.outlineView.rowView(atRow: index, makeIfNecessary: false) != nil { let rowView: KMBOTAOutlineRowView = self.outlineView.rowView(atRow: index, makeIfNecessary: false) as! KMBOTAOutlineRowView rowView.reloadData() self.outlineView.reloadItem(item, reloadChildren: true) } } } self.selectItems = items // if self.selectItems?.count == 1 { // self.needOpenOrCloseItem(oulineItem: (self.selectItems?.first)!) // } if self.selectItems != nil && isNeedDelegate { self.delegate?.BOTAOutlineView(self, didSelectItem: self.selectItems!) } } func needOpenOrCloseItem(oulineItem: KMBOTAOutlineItem) { //只有一个选中项时开启关闭item if self.selectItems?.count == 1 { if self.outlineView.isItemExpanded(oulineItem) { self.outlineView.collapseItem(oulineItem) oulineItem.isItemExpanded = false } else { self.outlineView.expandItem(oulineItem) oulineItem.isItemExpanded = true } let row = self.outlineView.row(forItem: oulineItem) self.outlineView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 0)) } } }