KMBOTAOutlineView.swift 20 KB

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