123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- //
- // 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 = ""
- var wholeWords = false {
- didSet {
- if isValidSearchMode() == false {
- return
- }
-
- reloadData()
- outlineView.expandItem(nil, expandChildren: true)
- }
- }
-
- var caseSensitive = false {
- didSet {
- if isValidSearchMode() == false {
- return
- }
-
- reloadData()
- outlineView.expandItem(nil, expandChildren: true)
- }
- }
-
- 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
- if isValidSearchMode() {
- self.reloadSearchChildren(item: self.data)
- }
-
- self.outlineView.reloadData()
- }
-
- self.delegate?.BOTAOutlineView(self, didReloadData: self.data ?? KMBOTAOutlineItem())
- }
-
- // 递归处理
-
- func reloadSearchChildren(item: KMBOTAOutlineItem?) {
- guard let theItem = item else {
- return
- }
-
- // 处理当前 item
- let models = self.fetchOutlines(for: theItem, searchString: searchKey)
- // 搜索数据
- theItem.searchChildren = models
-
- theItem.isItemExpanded = true
-
- if theItem.children.isEmpty { // 递归退出条件
- return
- }
-
- // 处理 childItem
- for childM in theItem.children {
- self.reloadSearchChildren(item: childM)
- }
- }
-
- 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 caseSensitive == false {
- label = label.lowercased()
- searchLabel = searchLabel.lowercased()
- }
-
-
- if label.contains(searchLabel) {
- if wholeWords {
- let words = label.words()
- return words.contains(searchLabel)
- }
- 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
- }
-
- func isValidSearchMode() -> Bool {
- if isSearchMode == false {
- return false
- }
- if searchKey.isEmpty {
- return false
- }
- return true
- }
- }
- //MARK: NSOutlineViewDataSource,NSOutlineViewDelegate
- extension KMBOTAOutlineView : NSOutlineViewDataSource,NSOutlineViewDelegate {
- func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
- guard let rootModel = self.data else {
- return 0
- }
-
- guard let model = item as? KMBOTAOutlineItem else {
- if isValidSearchMode() { // 是否为搜索模块
- if self.hasContainString(searchKey, rootOutline: rootModel.outline) == false {
- return 0
- }
- return rootModel.searchChildren.count
- }
- return Int(rootModel.outline.numberOfChildren)
- }
- if isValidSearchMode() {
- return model.searchChildren.count
- }
-
- return model.children.count
- }
-
- func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
- guard let rootModel = self.data else {
- return ""
- }
-
- guard let model = item as? KMBOTAOutlineItem else {
- if isValidSearchMode() { // 是否为搜索模块
- if self.hasContainString(searchKey, rootOutline: rootModel.outline) == false {
- return ""
- }
- return rootModel.searchChildren.safe_element(for: index) as Any
- }
- return rootModel.children[index] as Any
- }
- if isValidSearchMode() {
- return model.searchChildren.safe_element(for: index) as Any
- }
- return model.children[index] as Any
- }
-
- func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
- guard let model = item as? KMBOTAOutlineItem else {
- return false
- }
- if isValidSearchMode() {
- let datas = self.fetchOutlines(for: model, searchString: searchKey)
- return !datas.isEmpty
- }
- return !model.children.isEmpty
- }
-
- 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? {
- guard let model = item as? KMBOTAOutlineItem else {
- return nil
- }
-
- let cell : KMBOTAOutlineCellView = KMBOTAOutlineCellView.init()
-
- if isValidSearchMode() {
- var label = model.outline.label ?? ""
- let attri = NSMutableAttributedString(string: label, attributes: [
- .font : NSFont.SFProTextRegularFont(13),
- .foregroundColor : KMNColorTools.colorText_1()])
-
- var _searchKey = searchKey
- if caseSensitive == false {
- label = label.lowercased()
- _searchKey = _searchKey.lowercased()
- }
-
- let ranges = label.ranges(of: _searchKey)
- for range in ranges.nsRnage {
- attri.addAttributes([.font : NSFont.SFProTextBoldFont(13), .foregroundColor: KMNColorTools.colorPrimary_textLight()], range: range)
- }
- cell.titleLabel.attributedStringValue = attri
-
- cell.iconButton.isHidden = model.searchChildren.isEmpty
- } else {
- cell.titleLabel.stringValue = model.outline.label ?? ""
-
- if model.outline.numberOfChildren > 0 {
- cell.iconButton.isHidden = false
- } else {
- cell.iconButton.isHidden = true
- }
- }
-
- let isItemExpanded = model.isItemExpanded
- if isItemExpanded {
- cell.iconButton.image = NSImage(named: "KMImageNameArrowDown")
- } else {
- cell.iconButton.image = NSImage(named: "KMImageNameArrowRight")
- }
-
- 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))
- }
- }
- }
|