KMLeftSideViewController+Outline.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. //
  2. // KMLeftSideViewController+Outline.swift
  3. // PDF Master
  4. //
  5. // Created by tangchao on 2023/12/22.
  6. //
  7. import Foundation
  8. enum KMOutlineViewMenuItemTag: Int {
  9. case addEntry = 0
  10. case addChild
  11. case addAunt
  12. case remove
  13. case edit
  14. case setDestination
  15. case rename
  16. case promote
  17. case demote
  18. }
  19. extension KMLeftSideViewController {
  20. func updateOutlineSelection() {
  21. // if ([[pdfView document] outlineRoot] == nil || mwcFlags.updatingOutlineSelection)
  22. if self.listView.document.outlineRoot() == nil {
  23. return
  24. }
  25. // mwcFlags.updatingOutlineSelection = YES;
  26. let numRows = self.tocOutlineView.numberOfRows
  27. var arr = NSMutableArray()
  28. for i in 0 ..< numRows {
  29. guard let tPDFOutline = self.tocOutlineView.item(atRow: i) as? CPDFOutline else {
  30. continue
  31. }
  32. let tPage = tPDFOutline.destination.page()
  33. if (tPage == nil) {
  34. continue
  35. }
  36. let tDict = NSDictionary(object: tPage as Any, forKey: "\(i)" as NSCopying)
  37. arr.add(tDict)
  38. }
  39. let currentPage = self.listView.currentPage()
  40. var hasExistInOutlineView = false
  41. for dict in arr {
  42. guard let _dict = dict as? NSDictionary else {
  43. continue
  44. }
  45. let page = _dict.allValues.last as? CPDFPage
  46. // NSInteger index = [dict.allKeys.lastObject integerValue];
  47. let index = Int(_dict.allKeys.last as? String ?? "0") ?? 0
  48. if let data = page?.isEqual(to: currentPage), data {
  49. self.tocOutlineView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false)
  50. self.tocOutlineView.scrollRowToVisible(index)
  51. hasExistInOutlineView = true
  52. break
  53. }
  54. }
  55. if (!hasExistInOutlineView) {
  56. self.tocOutlineView.deselectRow(self.tocOutlineView.selectedRow)
  57. }
  58. // mwcFlags.updatingOutlineSelection = NO;
  59. }
  60. func newAddOutlineEntryEditingMode(_ index: Int) {
  61. self.renamePDFOutline = self.tocOutlineView.item(atRow: self.tocOutlineView.selectedRow) as? CPDFOutline
  62. let row = self.tocOutlineView.selectedRow
  63. let viewS = self.tocOutlineView.view(atColumn: 0, row: row, makeIfNecessary: true)
  64. let targrtTextField = viewS?.subviews.first as? NSTextField
  65. self.renamePDFOutlineTextField = targrtTextField
  66. targrtTextField?.delegate = self
  67. targrtTextField?.isEditable = true
  68. targrtTextField?.becomeFirstResponder()
  69. }
  70. func loadUnfoldDate(_ foldType: KMFoldType) {
  71. self.allFoldNotes.removeAll()
  72. var mutableArray: [CPDFAnnotation] = []
  73. for note in self.notes {
  74. if note is CPDFMarkupAnnotation {
  75. mutableArray.append(note)
  76. }
  77. }
  78. self.canFoldNotes = mutableArray
  79. self.allFoldNotes = []
  80. }
  81. func selectedRowIndexes() -> IndexSet {
  82. var selectedIndexes = self.tocOutlineView.selectedRowIndexes
  83. let clickedRow = self.tocOutlineView.clickedRow
  84. if clickedRow != -1 && selectedIndexes.contains(clickedRow) == false {
  85. var indexes = IndexSet(integer: clickedRow)
  86. selectedIndexes = indexes
  87. }
  88. return selectedIndexes
  89. }
  90. func editOutlineUI(_ editVC: KMOutlineEditViewController) {
  91. if editVC.pageButton.state == .on {
  92. let numberString = editVC.outlineTargetPageIndexTextField.stringValue
  93. let idx = Int(numberString) ?? 1
  94. let newPage = editVC.pdfView?.document.page(at: UInt(idx-1))
  95. let originalPage = editVC.originalDestination?.page()
  96. if let data = newPage?.pageIndex(), data != editVC.originalPageIndex {
  97. // if let data = newPage?.isEqual(to: originalPage), data {
  98. //新page不存在
  99. if (newPage == nil) {
  100. } else {
  101. if let outL = editVC.outline {
  102. let pageSize = newPage?.bounds(for: .cropBox).size ?? .zero
  103. if let destination = CPDFDestination(page: newPage, at: CGPointMake(pageSize.width, pageSize.height)) {
  104. self.changePDFOutlineDestination(destination, PDFoutline: outL)
  105. }
  106. }
  107. }
  108. }
  109. } else if editVC.urlButton.state == .on {
  110. var urlString = editVC.outlineURLTextField.stringValue
  111. let tLowerUrl = urlString.lowercased()
  112. if tLowerUrl.hasPrefix("https://") == false && tLowerUrl.hasPrefix("ftp://") == false && tLowerUrl.hasPrefix("http://") == false && urlString.isEmpty == false {
  113. urlString = "http://\(urlString)"
  114. }
  115. if let urlAction = CPDFURLAction(url: urlString), editVC.outline != nil {
  116. if editVC.originalURLString != editVC.outlineURLTextField.stringValue {
  117. self.changePDFAction(urlAction, PDFOutline: editVC.outline!)
  118. }
  119. }
  120. } else if editVC.mailButton.state == .on {
  121. var mailString = editVC.mailAddressTextField.stringValue
  122. let tLowerStr = mailString.lowercased()
  123. if tLowerStr.hasPrefix("mailto:") == false {
  124. mailString = "mailto:\(mailString)"
  125. }
  126. if var urlAction = CPDFURLAction(url: mailString) {
  127. if urlAction.url() == nil {
  128. urlAction = CPDFURLAction(url: "mailto:")
  129. }
  130. if mailString != editVC.originalURLString && editVC.outline != nil {
  131. self.changePDFAction(urlAction, PDFOutline: editVC.outline!)
  132. }
  133. }
  134. }
  135. if editVC.outlineNameTextView.string == editVC.originalLabel {
  136. return
  137. }
  138. if let outL = editVC.outline {
  139. self.renamePDFOutline(outL, label: editVC.outlineNameTextView.string)
  140. }
  141. }
  142. @objc func outlineContextMenuItemClicked_AddEntry(_ sender: AnyObject?) {
  143. let PDFOutlineArray = NSMutableArray()
  144. let rowSet = self.selectedRowIndexes()
  145. for idx in rowSet {
  146. PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx))
  147. }
  148. if (PDFOutlineArray.count == 0) {
  149. var lastPDFLine = self.tocOutlineView.item(atRow: self.tocOutlineView.numberOfRows-1) as? CPDFOutline
  150. var rootPDFOutline: CPDFOutline?
  151. if (lastPDFLine != nil) {
  152. while (lastPDFLine!.parent != nil) {
  153. lastPDFLine = lastPDFLine?.parent
  154. }
  155. rootPDFOutline = lastPDFLine
  156. } else {
  157. rootPDFOutline = self.listView.document.outlineRoot()
  158. if ((rootPDFOutline == nil)) {
  159. // rootPDFOutline = CPDFOutline()
  160. // self.listView.document.setOutlineRoot(rootPDFOutline)
  161. rootPDFOutline = self.listView.document.setNewOutlineRoot()
  162. }
  163. }
  164. let addOutLine = CPDFOutline()
  165. addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1)
  166. addOutLine.destination = self.listView.currentDestination
  167. self.addoutline(parent: rootPDFOutline, addOutline: addOutLine, index: Int(rootPDFOutline?.numberOfChildren ?? 0), needExpand: false)
  168. } else {
  169. let currentPDFline = PDFOutlineArray.lastObject as? CPDFOutline
  170. let currentIndex = currentPDFline?.index ?? 0
  171. var parent: CPDFOutline?
  172. parent = currentPDFline?.parent
  173. let addOutLine = CPDFOutline()
  174. addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1)
  175. addOutLine.destination = self.listView.currentDestination
  176. self.addoutline(parent: parent, addOutline: addOutLine, index: Int(currentIndex) + 1, needExpand: false)
  177. self.tocOutlineView.scrollRowToVisible(Int(currentIndex) + 1)
  178. self.tocOutlineView.deselectRow(Int(currentIndex)+1)
  179. }
  180. }
  181. }
  182. // MARK: - Undo & Redo
  183. extension KMLeftSideViewController {
  184. @objc dynamic func changePDFOutlineDestination(_ destination: CPDFDestination?, PDFoutline outline: CPDFOutline) {
  185. if let des = destination {
  186. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).changePDFOutlineDestination(outline.destination, PDFoutline: outline)
  187. let newDes = CPDFDestination(document: des.document, pageIndex: des.pageIndex, at: des.point, zoom: des.zoom)
  188. outline.destination = newDes
  189. self.tocOutlineView.reloadItem(outline)
  190. }
  191. }
  192. @objc dynamic func changePDFAction(_ action: CPDFAction, PDFOutline outline: CPDFOutline) {
  193. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).changePDFAction(outline.action, PDFOutline: outline)
  194. outline.action = action
  195. }
  196. @objc dynamic func renamePDFOutline(_ outline: CPDFOutline, label: String) {
  197. if (self.isRenameNoteOutline) {
  198. if outline is CPDFAnnotation {
  199. // if([outline isKindOfClass:[PDFAnnotation class]]) {
  200. // PDFAnnotation *annotation = (PDFAnnotation *)outline;
  201. // annotation.string = label;
  202. // [rightSideController.noteOutlineView reloadData];
  203. // [rightSideController.noteOutlineView selectRowIndexes:[[[NSIndexSet alloc] initWithIndex:[rightSideController.noteOutlineView rowForItem:outline]] autorelease] byExtendingSelection:NO];
  204. }
  205. } else {
  206. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).renamePDFOutline(outline, label: outline.label)
  207. outline.label = label
  208. self.tocOutlineView.reloadData()
  209. self.tocOutlineView.km_selectItem(outline, byExtendingSelection: false)
  210. }
  211. }
  212. @objc dynamic func demoteOutlineWithGrandParent(_ grandParentOutline: CPDFOutline, demoteOutline: CPDFOutline, index: Int) {
  213. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).promoteOutlineWithGrandParent(grandParentOutline, promoteOutline: demoteOutline, rowIndex: index)
  214. if grandParentOutline.isEqual(to: demoteOutline.parent) {
  215. let demoteIndex = demoteOutline.index
  216. let previousOutline = grandParentOutline.child(at: demoteIndex-1)
  217. demoteOutline.removeFromParent()
  218. previousOutline?.insertChild(demoteOutline, at: UInt(index))
  219. self.tocOutlineView.reloadData()
  220. self.tocOutlineView.expandItem(previousOutline)
  221. } else {
  222. demoteOutline.removeFromParent()
  223. grandParentOutline.insertChild(demoteOutline, at: grandParentOutline.numberOfChildren)
  224. self.tocOutlineView.reloadData()
  225. self.tocOutlineView.expandItem(grandParentOutline)
  226. }
  227. self.tocOutlineView.km_selectItem(demoteOutline, byExtendingSelection: false)
  228. }
  229. @objc dynamic func promoteOutlineWithGrandParent(_ grandParentOutline: CPDFOutline, promoteOutline: CPDFOutline, rowIndex: Int) {
  230. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).demoteOutlineWithGrandParent(grandParentOutline, demoteOutline: promoteOutline, index: rowIndex)
  231. let index = promoteOutline.parent?.index ?? 0
  232. if grandParentOutline.isEqual(to: promoteOutline.parent) {
  233. promoteOutline.removeFromParent()
  234. grandParentOutline.parent.insertChild(promoteOutline, at: index+1)
  235. } else {
  236. promoteOutline.removeFromParent()
  237. grandParentOutline.insertChild(promoteOutline, at: index+1)
  238. }
  239. self.tocOutlineView.reloadData()
  240. self.tocOutlineView.km_selectItem(promoteOutline, byExtendingSelection: false)
  241. }
  242. @objc dynamic func addoutline(parent parentOutline: CPDFOutline?, addOutline: CPDFOutline, index: Int, needExpand: Bool) {
  243. var tempO: CPDFOutline? = addOutline
  244. if addOutline.label != nil {
  245. parentOutline?.insertChild(addOutline, at: UInt(index))
  246. } else {
  247. let outline = parentOutline?.insertChild(at: UInt(index))
  248. outline?.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), self.listView.currentPageIndex+1)
  249. outline?.destination = self.listView.currentDestination
  250. tempO = outline
  251. }
  252. guard let outline = tempO else {
  253. return
  254. }
  255. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).removeOutline(parent: parentOutline, removeOutline: outline, index: index, needExpand: needExpand)
  256. self.view.window?.makeFirstResponder(nil)
  257. Task { @MainActor in
  258. self.tocOutlineView.reloadData()
  259. if (needExpand) {
  260. self.tocOutlineView.expandItem(parentOutline)
  261. }
  262. let idx = self.tocOutlineView.row(forItem: outline)
  263. self.tocOutlineView.selectRowIndexes(IndexSet(integer: idx), byExtendingSelection: false)
  264. self.newAddOutlineEntryEditingMode(index)
  265. }
  266. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  267. self.tocOutlineView.scrollRowToVisible(self.tocOutlineView.row(forItem: outline))
  268. }
  269. }
  270. @objc dynamic func removeOutline(parent parentOutline: CPDFOutline?, removeOutline: CPDFOutline, index: Int, needExpand: Bool) {
  271. (self.listView.undoManager?.prepare(withInvocationTarget: self) as AnyObject).addoutline(parent: parentOutline, addOutline: removeOutline, index: index, needExpand: true)
  272. removeOutline.removeFromParent()
  273. if (needExpand) {
  274. self.tocOutlineView.expandItem(parentOutline)
  275. }
  276. Task { @MainActor in
  277. self.tocOutlineView.reloadData()
  278. }
  279. }
  280. }
  281. // MARK: - Menu Actions
  282. extension KMLeftSideViewController {
  283. //添加子节点
  284. @objc func outlineContextMenuItemClicked_AddChildEntry(_ sender: AnyObject?) {
  285. let PDFOutlineArray = NSMutableArray()
  286. let rowSet = self.selectedRowIndexes()
  287. for idx in rowSet {
  288. PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx))
  289. }
  290. let currentPDFline = PDFOutlineArray.lastObject as? CPDFOutline
  291. let addOutLine = CPDFOutline()
  292. addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.listView.currentPageIndex + 1)")
  293. addOutLine.destination = self.listView.currentDestination
  294. self.addoutline(parent: currentPDFline, addOutline: addOutLine, index: Int(currentPDFline?.numberOfChildren ?? 0), needExpand: true)
  295. }
  296. //添加上一级节点
  297. @objc func outlineContextMenuItemClicked_AddAuntEntry(_ sender: AnyObject?) {
  298. let PDFOutlineArray = NSMutableArray()
  299. let rowSet = self.selectedRowIndexes()
  300. for idx in rowSet {
  301. PDFOutlineArray.add(self.tocOutlineView.item(atRow: idx))
  302. }
  303. let clickedOutline = PDFOutlineArray.lastObject as? CPDFOutline
  304. let fatherOutLine = clickedOutline?.parent
  305. let grandfatherOutLine = fatherOutLine?.parent
  306. if (grandfatherOutLine != nil) {
  307. let addOutLine = CPDFOutline()
  308. addOutLine.label = String(format: "%@ %ld", KMLocalizedString("Page", nil), "\(self.listView.currentPageIndex + 1)")
  309. addOutLine.destination = self.listView.currentDestination
  310. self.addoutline(parent: grandfatherOutLine, addOutline: addOutLine, index: Int(fatherOutLine?.index ?? 0) + 1, needExpand: false)
  311. }
  312. }
  313. //移除节点
  314. @objc func outlineContextMenuItemClicked_RemoveEntry(_ sender: AnyObject?) {
  315. let set = self.selectedRowIndexes()
  316. var selectedPDFOutlineArr = NSMutableArray()
  317. for idx in set {
  318. selectedPDFOutlineArr.add(self.tocOutlineView.item(atRow: idx))
  319. }
  320. //整体移除多选
  321. for tOutline in selectedPDFOutlineArr {
  322. guard let outL = tOutline as? CPDFOutline else {
  323. continue
  324. }
  325. self.removeOutline(parent: outL.parent , removeOutline: outL, index: Int(outL.index), needExpand: false)
  326. }
  327. }
  328. //重置目的
  329. @objc func outlineContextMenuItemClicked_SetDestination(_ sender: AnyObject?) {
  330. guard let setPDFOutline = self.tocOutlineView.item(atRow: self.tocOutlineView.clickedRow) as? CPDFOutline else {
  331. return
  332. }
  333. Task {
  334. let modalRes = await KMAlertTool.runModel(style: .informational, message: KMLocalizedString("Are you sure you want to set the destination as the current location?", nil), buttons: [KMLocalizedString("Yes", nil), KMLocalizedString("No", nil)])
  335. if (modalRes == .alertFirstButtonReturn) {
  336. self.changePDFOutlineDestination(self.listView.currentDestination, PDFoutline: setPDFOutline)
  337. self.tocOutlineView.reloadData()
  338. let idx = self.tocOutlineView.row(forItem: setPDFOutline)
  339. self.tocOutlineView.selectRowIndexes(IndexSet(integer: idx), byExtendingSelection: false)
  340. }
  341. }
  342. }
  343. //弹出菜单
  344. @objc func outlineContextMenuItemClicked_Edit(_ sender: AnyObject?) {
  345. let popover = NSPopover()
  346. popover.delegate = self
  347. let targetOutline: CPDFOutline? = self.tocOutlineView.km.clickedItem()
  348. let outlineEditViewController = KMOutlineEditViewController(outline: targetOutline, document: self.listView)
  349. let cell = self.tocOutlineView.rowView(atRow: self.tocOutlineView.clickedRow, makeIfNecessary: true)
  350. popover.contentViewController = outlineEditViewController
  351. popover.animates = true
  352. popover.behavior = .transient
  353. popover.show(relativeTo: cell?.bounds ?? .zero, of: cell!, preferredEdge: .minX)
  354. }
  355. //重命名
  356. @objc func outlineContextMenuItemClicked_Rename(_ sender: AnyObject?) {
  357. if (self.tocOutlineView.clickedRow < 0) {
  358. return
  359. }
  360. self.renamePDFOutline = self.tocOutlineView.km.clickedItem()
  361. let viewS = self.tocOutlineView.view(atColumn: 0, row: self.tocOutlineView.clickedRow, makeIfNecessary: true)
  362. let targrtTextField = viewS?.subviews.first as? NSTextField
  363. self.renamePDFOutlineTextField = targrtTextField
  364. targrtTextField?.delegate = self
  365. targrtTextField?.isEditable = true
  366. targrtTextField?.becomeFirstResponder()
  367. }
  368. //降级节点
  369. @objc func outlineContextMenuItemClicked_Demote(_ sender: AnyObject?) {
  370. guard let currentOutline: CPDFOutline = self.tocOutlineView.km.clickedItem() else {
  371. return
  372. }
  373. let parentOutLine = currentOutline.parent
  374. let newParentOutLine = parentOutLine?.child(at: currentOutline.index-1)
  375. var newIndex = 0
  376. let newParentOutLineExpandState = self.tocOutlineView.isItemExpanded(newParentOutLine)
  377. if (newParentOutLineExpandState) {
  378. newIndex = self.tocOutlineView.clickedRow
  379. } else {
  380. newIndex = self.tocOutlineView.clickedRow + Int(newParentOutLine?.numberOfChildren ?? 0)
  381. }
  382. let currentIndex = currentOutline.index
  383. currentOutline.removeFromParent()
  384. self.demoteOutlineWithGrandParent(newParentOutLine!, demoteOutline: currentOutline, index: Int(currentIndex))
  385. }
  386. //升级节点
  387. @objc func outlineContextMenuItemClicked_Promote(_ sender: AnyObject?) {
  388. guard let currentOutline: CPDFOutline = self.tocOutlineView.km.clickedItem() else {
  389. return
  390. }
  391. let parentOutLine = currentOutline.parent
  392. if let grandParentOutLine = parentOutLine?.parent {
  393. self.promoteOutlineWithGrandParent(grandParentOutLine, promoteOutline: currentOutline, rowIndex:Int(currentOutline.index))
  394. }
  395. }
  396. }