KMBOTAOutlineView.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. //
  2. // KMBOTAOutlineView.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by lizhe on 2023/4/2.
  6. //
  7. import Cocoa
  8. protocol KMBOTAOutlineViewDelegate: NSObjectProtocol {
  9. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didReloadData: KMBOTAOutlineItem)
  10. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, didSelectItem: [KMBOTAOutlineItem])
  11. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, rightDidMoseDown: KMBOTAOutlineItem, event: NSEvent)
  12. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool
  13. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation
  14. func BOTAOutlineView(_ outlineView: KMBOTAOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool
  15. }
  16. class KMBOTAOutlineView: BaseXibView {
  17. @IBOutlet weak var outlineView: KMOutlineView!
  18. @IBOutlet weak var scrollView: NSScrollView!
  19. weak var delegate: KMBOTAOutlineViewDelegate?
  20. var inputData: CPDFOutline? {
  21. didSet {
  22. self.reloadData(expandItemType: .none)
  23. }
  24. }
  25. var data: KMBOTAOutlineItem?
  26. var selectItems: [KMBOTAOutlineItem] = []
  27. var dragPDFOutline: KMBOTAOutlineItem!
  28. var isSearchMode = false
  29. var searchKey = ""
  30. var wholeWords: Bool {
  31. set {
  32. if isValidSearchMode() == false {
  33. return
  34. }
  35. reloadData()
  36. outlineView.expandItem(nil, expandChildren: true)
  37. }
  38. get {
  39. let key = KMNSearchKey.wholeWords.botaSearch
  40. let value = KMDataManager.ud_bool(forKey: key)
  41. return value
  42. }
  43. }
  44. var caseSensitive: Bool {
  45. set {
  46. if isValidSearchMode() == false {
  47. return
  48. }
  49. reloadData()
  50. outlineView.expandItem(nil, expandChildren: true)
  51. }
  52. get {
  53. let key = KMNSearchKey.caseSensitive.botaSearch
  54. let value = KMDataManager.ud_bool(forKey: key)
  55. return value
  56. }
  57. }
  58. override func draw(_ dirtyRect: NSRect) {
  59. super.draw(dirtyRect)
  60. // Drawing code here.
  61. }
  62. override func awakeFromNib() {
  63. super.awakeFromNib()
  64. self.setup()
  65. }
  66. override func setup() {
  67. self.scrollView.backgroundColor(NSColor.km_init(hex: "#F2F9FF"))
  68. self.outlineView.registerForDraggedTypes([NSPasteboard.PasteboardType(rawValue: "kKMPDFViewOutlineDragDataType")])
  69. self.outlineView.delegate = self
  70. self.outlineView.dataSource = self
  71. self.outlineView.selectionHighlightStyle = NSTableView.SelectionHighlightStyle.none;
  72. self.outlineView.allowsMultipleSelection = true
  73. // self.outlineView.indentationPerLevel = 0
  74. outlineView.tocDelegate = self
  75. outlineView.hasImageToolTips = true
  76. }
  77. func reloadData(expandItemType: KMOutlineViewExpandItemType = .none) {
  78. if self.inputData != nil {
  79. //获取数据
  80. var tempData: KMBOTAOutlineItem = KMBOTAOutlineItem()
  81. if self.inputData!.numberOfChildren > 0 {
  82. let outline: CPDFOutline = self.inputData!
  83. tempData = self.addOutlineItem(outlineItem:tempData, outline: outline, expandItemType: expandItemType)
  84. } else {
  85. tempData.outline = CPDFOutline()
  86. if expandItemType == .collapse {
  87. tempData.isItemExpanded = false
  88. } else if (expandItemType == .expand) {
  89. tempData.isItemExpanded = true
  90. }
  91. }
  92. tempData.parent = nil
  93. self.data = tempData
  94. if isValidSearchMode() {
  95. self.reloadSearchChildren(item: self.data)
  96. }
  97. self.outlineView.reloadData()
  98. }
  99. self.delegate?.BOTAOutlineView(self, didReloadData: self.data ?? KMBOTAOutlineItem())
  100. }
  101. // 递归处理
  102. func reloadSearchChildren(item: KMBOTAOutlineItem?) {
  103. guard let theItem = item else {
  104. return
  105. }
  106. // 处理当前 item
  107. let models = self.fetchOutlines(for: theItem, searchString: searchKey)
  108. // 搜索数据
  109. theItem.searchChildren = models
  110. theItem.isItemExpanded = true
  111. if theItem.children.isEmpty { // 递归退出条件
  112. return
  113. }
  114. // 处理 childItem
  115. for childM in theItem.children {
  116. self.reloadSearchChildren(item: childM)
  117. }
  118. }
  119. func addOutlineItem(outlineItem: KMBOTAOutlineItem?, outline: CPDFOutline, expandItemType: KMOutlineViewExpandItemType = .none) -> KMBOTAOutlineItem {
  120. //添加根节点
  121. let item: KMBOTAOutlineItem = KMBOTAOutlineItem()
  122. item.outline = outline
  123. item.parent = outlineItem
  124. var items: [KMBOTAOutlineItem] = []
  125. if outline.numberOfChildren > 0 {
  126. for index in 0...outline.numberOfChildren - 1 {
  127. let children: CPDFOutline = outline.child(at: index)
  128. let childrenItem = self.addOutlineItem(outlineItem: item, outline: children, expandItemType: expandItemType)
  129. if expandItemType == .collapse {
  130. childrenItem.isItemExpanded = false
  131. } else if (expandItemType == .expand) {
  132. childrenItem.isItemExpanded = true
  133. }
  134. items.append(childrenItem)
  135. }
  136. }
  137. item.children = items
  138. return item
  139. }
  140. func updateUI() {
  141. }
  142. func updateLanguage() {
  143. }
  144. func hasContainString(_ searchString: String, rootOutline outline: CPDFOutline) -> Bool {
  145. var label = outline.label ?? ""
  146. var searchLabel = searchString
  147. if caseSensitive == false {
  148. label = label.lowercased()
  149. searchLabel = searchLabel.lowercased()
  150. }
  151. if label.contains(searchLabel) {
  152. if wholeWords {
  153. let words = label.words()
  154. return words.contains(searchLabel)
  155. }
  156. return true
  157. } else {
  158. var subHas = false
  159. for i in 0 ..< outline.numberOfChildren {
  160. if let subOutline = outline.child(at: i) {
  161. subHas = self.hasContainString(searchString, rootOutline: subOutline)
  162. } else {
  163. continue
  164. }
  165. if (subHas) {
  166. break
  167. }
  168. }
  169. return subHas
  170. }
  171. }
  172. func fetchOutlines(for item: KMBOTAOutlineItem, searchString: String) -> [KMBOTAOutlineItem] {
  173. var items: [KMBOTAOutlineItem] = []
  174. for childI in item.children {
  175. if self.hasContainString(searchString, rootOutline: childI.outline) {
  176. items.append(childI)
  177. }
  178. }
  179. return items
  180. }
  181. func isValidSearchMode() -> Bool {
  182. if isSearchMode == false {
  183. return false
  184. }
  185. if searchKey.isEmpty {
  186. return false
  187. }
  188. return true
  189. }
  190. }
  191. //MARK: NSOutlineViewDataSource,NSOutlineViewDelegate
  192. extension KMBOTAOutlineView : NSOutlineViewDataSource,NSOutlineViewDelegate {
  193. func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
  194. guard let rootModel = self.data else {
  195. return 0
  196. }
  197. guard let model = item as? KMBOTAOutlineItem else {
  198. if isValidSearchMode() { // 是否为搜索模块
  199. if self.hasContainString(searchKey, rootOutline: rootModel.outline) == false {
  200. return 0
  201. }
  202. return rootModel.searchChildren.count
  203. }
  204. return Int(rootModel.outline.numberOfChildren)
  205. }
  206. if isValidSearchMode() {
  207. return model.searchChildren.count
  208. }
  209. return model.children.count
  210. }
  211. func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
  212. guard let rootModel = self.data else {
  213. return ""
  214. }
  215. guard let model = item as? KMBOTAOutlineItem else {
  216. if isValidSearchMode() { // 是否为搜索模块
  217. if self.hasContainString(searchKey, rootOutline: rootModel.outline) == false {
  218. return ""
  219. }
  220. return rootModel.searchChildren.safe_element(for: index) as Any
  221. }
  222. return rootModel.children[index] as Any
  223. }
  224. if isValidSearchMode() {
  225. return model.searchChildren.safe_element(for: index) as Any
  226. }
  227. return model.children[index] as Any
  228. }
  229. func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
  230. guard let model = item as? KMBOTAOutlineItem else {
  231. return false
  232. }
  233. if isValidSearchMode() {
  234. let datas = self.fetchOutlines(for: model, searchString: searchKey)
  235. return !datas.isEmpty
  236. }
  237. return !model.children.isEmpty
  238. }
  239. func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool {
  240. if let item = item as? KMBOTAOutlineItem {
  241. if !item.isItemExpanded {
  242. item.isItemExpanded = true
  243. outlineView.animator().expandItem(item, expandChildren: true)
  244. return false
  245. }
  246. }
  247. return true
  248. }
  249. func outlineView(_ outlineView: NSOutlineView, shouldCollapseItem item: Any) -> Bool {
  250. if let item = item as? KMBOTAOutlineItem {
  251. if item.isItemExpanded {
  252. item.isItemExpanded = false
  253. outlineView.animator().collapseItem(item, collapseChildren: true)
  254. return false
  255. }
  256. }
  257. return true
  258. }
  259. func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
  260. guard let model = item as? KMBOTAOutlineItem else {
  261. return nil
  262. }
  263. let cell : KMBOTAOutlineCellView = KMBOTAOutlineCellView.createFromNib() ?? KMBOTAOutlineCellView.init()
  264. if isValidSearchMode() {
  265. var label = model.outline.label ?? ""
  266. let attri = NSMutableAttributedString(string: label, attributes: [
  267. .font : NSFont.SFProTextRegularFont(13),
  268. .foregroundColor : KMNColorTools.colorText_1()])
  269. var _searchKey = searchKey
  270. if caseSensitive == false {
  271. label = label.lowercased()
  272. _searchKey = _searchKey.lowercased()
  273. }
  274. if wholeWords {
  275. let escapedKey = NSRegularExpression.escapedPattern(for: _searchKey)
  276. let pattern = "\\b\(escapedKey)\\b"
  277. do {
  278. let regex = try NSRegularExpression(pattern: pattern, options: caseSensitive ? [] : [.caseInsensitive])
  279. let matches = regex.matches(in: label, range: NSRange(location: 0, length: label.utf16.count))
  280. for match in matches {
  281. attri.addAttributes([
  282. .font : NSFont.SFProTextBoldFont(13),
  283. .foregroundColor: KMNColorTools.colorPrimary_textLight()
  284. ], range: match.range)
  285. }
  286. } catch {
  287. }
  288. } else {
  289. let ranges = label.ranges(of: _searchKey)
  290. for range in ranges.nsRnage {
  291. attri.addAttributes([.font : NSFont.SFProTextBoldFont(13), .foregroundColor: KMNColorTools.colorPrimary_textLight()], range: range)
  292. }
  293. }
  294. cell.titleLabel.attributedStringValue = attri
  295. cell.iconButton.isHidden = model.searchChildren.isEmpty
  296. } else {
  297. cell.titleLabel.stringValue = model.outline.label ?? ""
  298. if model.outline.numberOfChildren > 0 {
  299. cell.iconButton.isHidden = false
  300. } else {
  301. cell.iconButton.isHidden = true
  302. }
  303. }
  304. cell.model = model
  305. cell.iconAction = { [unowned self] view in
  306. let rowIndex = outlineView.row(forItem: item)
  307. let rowView = outlineView.rowView(atRow: rowIndex, makeIfNecessary: false)
  308. self.didSelectItem(view: (rowView as? KMBOTAOutlineRowView), event: NSEvent())
  309. if self.selectItems.count == 1 {
  310. self.needOpenOrCloseItem(oulineItem: (self.selectItems.first)!)
  311. }
  312. }
  313. return cell
  314. }
  315. func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
  316. let rowView = KMBOTAOutlineRowView()
  317. rowView.model = item as? KMBOTAOutlineItem
  318. rowView.mouseDownCallback = { [unowned self] (view, event) in
  319. self.didSelectItem(view: view, event: event)
  320. }
  321. rowView.rightMouseCallback = { [unowned self] (view, event) in
  322. if !KMOCToolClass.arrayContains(array: self.selectItems, annotation: item) ||
  323. self.selectItems.count == 1 {
  324. self.selectItem(outlineItem: item as! KMBOTAOutlineItem)
  325. }
  326. self.delegate?.BOTAOutlineView(self, rightDidMoseDown: item as! KMBOTAOutlineItem, event: event)
  327. }
  328. rowView.hoverCallback = { [unowned self] (mouseEntered, mouseBox) in
  329. self.outlineView.enumerateAvailableRowViews { view, row in
  330. if view is KMBOTAOutlineRowView {
  331. (view as? KMBOTAOutlineRowView)?.model?.hover = false
  332. (view as? KMBOTAOutlineRowView)?.reloadData()
  333. }
  334. }
  335. if mouseEntered {
  336. rowView.model?.hover = true
  337. } else {
  338. rowView.model?.hover = false
  339. }
  340. }
  341. return rowView
  342. }
  343. func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
  344. if item is KMBOTAOutlineItem {
  345. let tempItem: KMBOTAOutlineItem = item as! KMBOTAOutlineItem
  346. let string: NSString = tempItem.outline.label as NSString
  347. let paragraphStyle = NSMutableParagraphStyle()
  348. paragraphStyle.lineHeightMultiple = 1.32
  349. paragraphStyle.alignment = .left
  350. let attributes = [NSAttributedString.Key.paragraphStyle: paragraphStyle,
  351. NSAttributedString.Key.font : NSFont.SFProTextRegularFont(14.0)]
  352. let size = string.boundingRect(with: NSMakeSize(outlineView.frame.size.width - 30, 200), options: NSString.DrawingOptions(rawValue: 3), attributes: attributes)
  353. return max(40, size.height + 16)
  354. }
  355. return 40
  356. }
  357. func outlineView(_ outlineView: NSOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool {
  358. guard let callBack = self.delegate else { return false}
  359. return callBack.BOTAOutlineView(self, writeItems: items, to: pasteboard)
  360. }
  361. func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
  362. guard let callBack = self.delegate else { return NSDragOperation.init(rawValue: 0)}
  363. return callBack.BOTAOutlineView(self, validateDrop: info, proposedItem: item, proposedChildIndex: index)
  364. }
  365. func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
  366. guard let callBack = self.delegate else { return false}
  367. return callBack.BOTAOutlineView(self, acceptDrop: info, item: item, childIndex: index)
  368. }
  369. func outlineViewSelectionDidChange(_ notification: Notification) {
  370. // if self.outlineView.selectedRow == -1 {
  371. // self.cancelSelect()
  372. // }
  373. }
  374. }
  375. extension KMBOTAOutlineView: KMTocOutlineViewDelegate {
  376. func outlineView(_ anOutlineView: NSOutlineView, imageContextForItem item: Any?) -> AnyObject? {
  377. if item == nil {
  378. return true as AnyObject
  379. }
  380. if let data = item as? KMBOTAOutlineItem {
  381. return data.outline.destination
  382. }
  383. return nil
  384. }
  385. }
  386. //MARK: Action
  387. extension KMBOTAOutlineView {
  388. @objc func expandAllComments(item: NSMenuItem) {
  389. self.reloadData(expandItemType: .expand)
  390. self.outlineView.reloadData()
  391. self.outlineView.expandItem(nil, expandChildren: true)
  392. }
  393. @objc func collapseAllComments(item: NSMenuItem) {
  394. self.reloadData(expandItemType: .collapse)
  395. self.outlineView.reloadData()
  396. self.outlineView.collapseItem(nil, collapseChildren: true)
  397. }
  398. func selectItem(outlineItem: KMBOTAOutlineItem) {
  399. let index = self.outlineView.row(forItem: outlineItem)
  400. self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false)
  401. self.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false)
  402. }
  403. func selectIndex(index: Int) {
  404. self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false)
  405. self.didSelectItem(view: nil, event: NSEvent(), isNeedDelegate: false)
  406. }
  407. func cancelSelect() {
  408. self.outlineView.deselectAll(nil)
  409. for model in self.selectItems {
  410. model.select = false
  411. self.outlineView.reloadItem(model)
  412. }
  413. }
  414. func selectAll() {
  415. let totalRows = self.outlineView.numberOfRows
  416. //outline全选
  417. var allIndexes = IndexSet()
  418. for row in 0..<totalRows {
  419. allIndexes.insert(row)
  420. }
  421. outlineView.selectRowIndexes(allIndexes, byExtendingSelection: false)
  422. //数据全选
  423. var items: [KMBOTAOutlineItem] = []
  424. for row in 0..<totalRows {
  425. let item: KMBOTAOutlineItem = self.outlineView.item(atRow: row) as! KMBOTAOutlineItem
  426. item.select = true
  427. items.append(item)
  428. //刷新数据
  429. if self.outlineView.rowView(atRow: row, makeIfNecessary: false) != nil {
  430. let rowView: KMBOTAOutlineRowView = self.outlineView.rowView(atRow: row, makeIfNecessary: false) as! KMBOTAOutlineRowView
  431. rowView.reloadData()
  432. self.outlineView.reloadItem(item, reloadChildren: true)
  433. }
  434. }
  435. self.selectItems = items
  436. }
  437. func didSelectItem(view: KMBOTAOutlineRowView?, event: NSEvent, isNeedDelegate: Bool = true) {
  438. //当选中一个时
  439. if view != nil && (self.outlineView.selectedRowIndexes.count == 1 ||
  440. (!event.modifierFlags.contains(NSEvent.ModifierFlags.command) &&
  441. !event.modifierFlags.contains(NSEvent.ModifierFlags.shift))) {
  442. let rowView: KMBOTAOutlineRowView = view!
  443. let index = self.outlineView.row(for: rowView)
  444. self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false)
  445. }
  446. //原始数据置空
  447. for item in self.selectItems {
  448. item.select = false
  449. let index = self.outlineView.row(forItem: item)
  450. if index != -1 {
  451. if self.outlineView.rowView(atRow: index, makeIfNecessary: false) != nil {
  452. let rowView: KMBOTAOutlineRowView = self.outlineView.rowView(atRow: index, makeIfNecessary: false) as! KMBOTAOutlineRowView
  453. rowView.reloadData()
  454. }
  455. }
  456. }
  457. //获取最新数据
  458. var items: [KMBOTAOutlineItem] = []
  459. for index in self.outlineView.selectedRowIndexes {
  460. if index != -1 {
  461. let item: KMBOTAOutlineItem = self.outlineView.item(atRow: index) as! KMBOTAOutlineItem
  462. item.select = true
  463. items.append(item)
  464. //刷新数据
  465. if self.outlineView.rowView(atRow: index, makeIfNecessary: false) != nil {
  466. let rowView: KMBOTAOutlineRowView = self.outlineView.rowView(atRow: index, makeIfNecessary: false) as! KMBOTAOutlineRowView
  467. self.outlineView.reloadItem(item, reloadChildren: true)
  468. rowView.reloadData()
  469. }
  470. }
  471. }
  472. self.selectItems = items
  473. // if self.selectItems?.count == 1 {
  474. // self.needOpenOrCloseItem(oulineItem: (self.selectItems?.first)!)
  475. // }
  476. if isNeedDelegate {
  477. self.delegate?.BOTAOutlineView(self, didSelectItem: self.selectItems)
  478. }
  479. }
  480. func needOpenOrCloseItem(oulineItem: KMBOTAOutlineItem) {
  481. //只有一个选中项时开启关闭item
  482. if self.selectItems.count == 1 {
  483. if self.outlineView.isItemExpanded(oulineItem) {
  484. self.outlineView.collapseItem(oulineItem)
  485. oulineItem.isItemExpanded = false
  486. } else {
  487. self.outlineView.expandItem(oulineItem)
  488. oulineItem.isItemExpanded = true
  489. }
  490. let row = self.outlineView.row(forItem: oulineItem)
  491. self.outlineView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 0))
  492. }
  493. }
  494. }