KMBotaSearchViewController.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. //
  2. // KMBotaSearchViewController.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by tangchao on 2023/11/16.
  6. //
  7. import Cocoa
  8. import KMComponentLibrary
  9. extension KMNSearchKey.wholeWords {
  10. static let botaSearch = "BotaSearchWholeWordsKey"
  11. }
  12. extension KMNSearchKey.caseSensitive {
  13. static let botaSearch = "BotaSearchCaseSensitiveKey"
  14. }
  15. enum KMNBotaSearchType: Int {
  16. case search = 1
  17. case replace = 2
  18. }
  19. class KMBotaSearchViewController: KMNBotaBaseViewController {
  20. @IBOutlet weak var searchField: NSSearchField!
  21. @IBOutlet weak var segmentedControl: KMSegmentedControl!
  22. @IBOutlet weak var topView: NSBox!
  23. @IBOutlet weak var topHeightConst: NSLayoutConstraint!
  24. var contentView: NSView? {
  25. didSet {
  26. if let view = self.contentView {
  27. self.box.contentView = view
  28. }
  29. }
  30. }
  31. @IBOutlet weak var emptyBox: NSBox!
  32. @IBOutlet weak var searchBox: KMBox!
  33. @IBOutlet weak var searchResultsView: NSView!
  34. @IBOutlet weak var searchResultsLabel: NSTextField!
  35. @IBOutlet weak var searchDomeButton: NSButton!
  36. @IBOutlet weak var box: NSBox!
  37. @IBOutlet weak var emptySearchLabel: NSTextField!
  38. @IBOutlet weak var searchLabel: NSTextField!
  39. @IBOutlet weak var searchTips: NSTextField!
  40. @IBOutlet weak var pageLabel: NSTextField!
  41. @IBOutlet var scrollView: NSScrollView!
  42. @IBOutlet weak var tableView: KMBotaTableView!
  43. private lazy var topContentView_: KMNBotaSearchTopView? = {
  44. let view = KMNBotaSearchTopView.createFromNib()
  45. return view
  46. }()
  47. var handdler = KMNSearchHanddler()
  48. var searchResults : [KMSearchMode] = [] {
  49. didSet {
  50. self.updataLeftSideFindView()
  51. }
  52. }
  53. deinit {
  54. KMPrint("KMBotaSearchViewController deinit.")
  55. NotificationCenter.default.removeObserver(self)
  56. }
  57. override func loadView() {
  58. super.loadView()
  59. topView.borderWidth = 0
  60. topView.fillColor = .clear
  61. topView.contentView = topContentView_
  62. topContentView_?.itemClick = { [unowned self] idx, params in
  63. if idx == KMNBotaSearchTopItemKey.search.rawValue {
  64. if let data = params.first as? ComponentButton {
  65. showSearchGroupView(sender: data)
  66. }
  67. } else if idx == KMNBotaSearchTopItemKey.replace.rawValue {
  68. if handdler.type == .search {
  69. handdler.type = .replace
  70. showReplaceView()
  71. } else {
  72. handdler.type = .search
  73. showSearchView()
  74. }
  75. } else if idx == KMNBotaSearchTopItemKey.switch.rawValue {
  76. } else if idx == KMNBotaSearchTopItemKey.previous.rawValue {
  77. tableViewMoveUp(tableView)
  78. } else if idx == KMNBotaSearchTopItemKey.next.rawValue {
  79. tableViewMoveDown(tableView)
  80. }
  81. }
  82. showSearchView()
  83. topContentView_?.valueDidChange = { [unowned self] sender, info in
  84. guard let string = info?[.newKey] as? String else {
  85. return
  86. }
  87. let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch)
  88. let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch)
  89. handdler.search(keyword: string, isCase: isCase, isWholeWord: isWholeWord) { [unowned self] results in
  90. searchResults = results ?? []
  91. showResult()
  92. tableView.reloadData()
  93. }
  94. }
  95. self.emptySearchLabel.stringValue = KMLocalizedString("No Results")
  96. self.emptySearchLabel.textColor = KMAppearance.Layout.h1Color()
  97. self.emptyBox.fillColor = KMAppearance.Layout.l0Color()
  98. // self.searchLabel.stringValue = KMLocalizedString("Search")
  99. // self.searchLabel.textColor = KMAppearance.Layout.h0Color()
  100. // self.searchTips.stringValue = KMLocalizedString("Search")
  101. // self.searchTips.textColor = KMAppearance.Layout.h2Color()
  102. // self.searchResultsLabel.textColor = KMAppearance.Layout.h1Color()
  103. // self.pageLabel.stringValue = KMLocalizedString("Page")
  104. // self.pageLabel.textColor = KMAppearance.Layout.h1Color()
  105. // self.searchResultsView.isHidden = true
  106. // self.searchDomeButton.title = KMLocalizedString("Done")
  107. // self.searchDomeButton.toolTip = KMLocalizedString("Done")
  108. // self.searchDomeButton.setTitleColor(KMAppearance.Layout.w0Color())
  109. // self.searchDomeButton.wantsLayer = true
  110. // self.searchDomeButton.layer?.backgroundColor = KMAppearance.Interactive.a0Color().cgColor
  111. // self.searchDomeButton.layer?.cornerRadius = 4.0
  112. // self.searchDomeButton.hidden = YES;
  113. // self.searchField.wantsLayer = true
  114. // self.searchField.layer.backgroundColor = [KMAppearance KMColor_Layout_L1].CGColor;
  115. // self.searchField.layer?.cornerRadius = 1.0
  116. // self.searchField.layer?.borderWidth = 1.0
  117. // self.searchField.layer?.borderColor = KMAppearance.Interactive.a0Color().cgColor
  118. // self.searchBox.fillColor = KMAppearance.Interactive.s0Color()
  119. // self.searchField.hidden = YES;
  120. // self.searchBox.downCallback = { [unowned self] downEntered, box, _ in
  121. // if (downEntered) {
  122. // self.searchField.isHidden = false
  123. // self.searchDomeButton.isHidden = false
  124. // self.searchBox.isHidden = true
  125. // self.searchField.becomeFirstResponder()
  126. // }
  127. // }
  128. // self.searchBox.isHidden = true
  129. contentView = tableView.enclosingScrollView
  130. // let menu = NSMenu()
  131. // _ = menu.addItem(title: KMLocalizedString("Whole Words Only"), action: #selector(toggleWholeWordSearch), target: self)
  132. // _ = menu.addItem(title: KMLocalizedString("Ignore Case"), action: #selector(toggleCaseInsensitiveSearch), target: self)
  133. // (self.searchField.cell as? NSSearchFieldCell)?.searchMenuTemplate = menu
  134. // (self.searchField.cell as? NSSearchFieldCell)?.placeholderString = KMLocalizedString("Search PDF")
  135. //
  136. // self.searchField.target = self
  137. // self.searchField.action = #selector(searchAction)
  138. }
  139. override func viewDidLoad() {
  140. super.viewDidLoad()
  141. self.tableView.delegate = self
  142. self.tableView.dataSource = self
  143. self.tableView.botaDelegate = self
  144. // self.tableView.menu?.delegate = self
  145. // self.mwcFlags.wholeWordSearch = KMDataManager.ud_integer(forKey: SKWholeWordSearchKey)
  146. // self.mwcFlags.caseInsensitiveSearch = KMDataManager.ud_integer(forKey: SKCaseInsensitiveSearchKey)
  147. // self.tableView.backgroundColor = KMAppearance.Layout.l0Color()
  148. self.tableView.tableColumn(withIdentifier: kPageColumnId)?.headerCell.title = KMLocalizedString("Page")
  149. }
  150. override func viewDidAppear() {
  151. super.viewDidAppear()
  152. // self.searchField.becomeFirstResponder()
  153. self.updateViewColor()
  154. DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged), name: NSApplication.interfaceThemeChangedNotification, object: nil)
  155. }
  156. override func updateUILanguage() {
  157. super.updateUILanguage()
  158. KMMainThreadExecute {
  159. self.topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " " + "\(self.handdler.searchResults.count)"
  160. }
  161. }
  162. override func updateUIThemeColor() {
  163. super.updateUIThemeColor()
  164. KMMainThreadExecute {
  165. self.view.wantsLayer = true
  166. let color = KMNColorTools.colorBg_layoutMiddle()
  167. self.view.layer?.backgroundColor = color.cgColor
  168. self.tableView.backgroundColor = color
  169. self.topContentView_?.resultLabel.textColor = KMNColorTools.colorText_3()
  170. }
  171. }
  172. func updateViewColor() {
  173. if (KMAppearance.isDarkMode()) {
  174. // self.searchField.layer?.backgroundColor = NSColor(red: 57.0/255.0, green: 60.0/255.0, blue: 62.0/255.0, alpha: 1).cgColor
  175. } else {
  176. // self.searchField.layer?.backgroundColor = .white
  177. }
  178. }
  179. func showSearchView() {
  180. topContentView_?.showSearch()
  181. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0
  182. }
  183. func showReplaceView() {
  184. topContentView_?.showReplace()
  185. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0
  186. }
  187. func showResult() {
  188. topContentView_?.showResult(type: handdler.type)
  189. topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " " + "\(self.handdler.searchResults.count)"
  190. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: searchResults.isEmpty == false) ?? 0
  191. }
  192. // MARK: - Group View
  193. func showSearchGroupView(sender: ComponentButton) {
  194. var viewHeight: CGFloat = 8
  195. var menuItemArr: [ComponentMenuitemProperty] = []
  196. let titles = ["Search", "Find and Replace", "", "Whole Words", "Case Sensitive"]
  197. for i in titles {
  198. if i.isEmpty {
  199. let menuI = ComponentMenuitemProperty.divider()
  200. menuItemArr.append(menuI)
  201. viewHeight += 8
  202. } else {
  203. let menuI = ComponentMenuitemProperty(text: KMLocalizedString(i))
  204. menuItemArr.append(menuI)
  205. viewHeight += 36
  206. }
  207. }
  208. if handdler.type == .search {
  209. menuItemArr.first?.righticon = NSImage(named: "KMNImageNameMenuSelect")
  210. } else if handdler.type == .replace {
  211. let info = menuItemArr.safe_element(for: 1) as? ComponentMenuitemProperty
  212. info?.righticon = NSImage(named: "KMNImageNameMenuSelect")
  213. }
  214. if let info = menuItemArr.safe_element(for: 3) as? ComponentMenuitemProperty {
  215. if KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) {
  216. info.righticon = NSImage(named: "KMNImageNameMenuSelect")
  217. }
  218. }
  219. if let info = menuItemArr.last {
  220. if KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) {
  221. info.righticon = NSImage(named: "KMNImageNameMenuSelect")
  222. }
  223. }
  224. let groupView = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle())
  225. searchGroupView = groupView
  226. groupView?.groupDelegate = self
  227. groupView?.frame = CGRectMake(310, 0, 200, viewHeight)
  228. groupView?.updateGroupInfo(menuItemArr)
  229. var point = sender.convert(sender.frame.origin, to: nil)
  230. point.y -= viewHeight
  231. groupView?.showWithPoint(point, relativeTo: sender)
  232. searchGroupTarget = sender
  233. }
  234. @objc func themeChanged(_ notification: NSNotification) {
  235. DispatchQueue.main.asyncAfter(deadline: .now()+0.3) {
  236. self.updateViewColor()
  237. }
  238. }
  239. @IBAction func searchDomeButtonAtion(_ sender: AnyObject) {
  240. self.searchField.isHidden = true
  241. self.searchDomeButton.isHidden = true
  242. self.searchBox.isHidden = false
  243. }
  244. @objc func toggleWholeWordSearch(_ sender: AnyObject?) {
  245. // if self.mwcFlags.wholeWordSearch == 1 {
  246. // self.mwcFlags.wholeWordSearch = 0
  247. // } else {
  248. // self.mwcFlags.wholeWordSearch = 1
  249. // }
  250. // if self.searchField.stringValue.isEmpty == false {
  251. // self.search(self.searchField)
  252. // }
  253. // KMDataManager.ud_set(self.mwcFlags.wholeWordSearch, forKey: SKWholeWordSearchKey)
  254. }
  255. @objc func toggleCaseInsensitiveSearch(_ sender: AnyObject?) {
  256. // if self.mwcFlags.caseInsensitiveSearch == 0 {
  257. // self.mwcFlags.caseInsensitiveSearch = 1
  258. // } else {
  259. // self.mwcFlags.caseInsensitiveSearch = 0
  260. // }
  261. //
  262. // if self.searchField.stringValue.isEmpty == false {
  263. // self.search(self.searchField)
  264. // }
  265. // KMDataManager.ud_set(self.mwcFlags.caseInsensitiveSearch, forKey: SKCaseInsensitiveSearchKey)
  266. }
  267. @objc func goToSelectedFindResults(_ sender: AnyObject?) {
  268. // guard let olView = sender as? NSTableView, olView.clickedRow != -1 else {
  269. // NSSound.beep()
  270. // return
  271. // }
  272. // self.updateFindResultHighlightsForDirection(.directSelection)
  273. }
  274. @objc func searchAction(_ sender: NSSearchField) {
  275. // if sender.stringValue.isEmpty {
  276. // self.applySearchTableHeader("")
  277. // }
  278. // self.delegate?.searchAction?(searchString: sender.stringValue, isCase: self.mwcFlags.caseInsensitiveSearch == 1)
  279. handdler.search(keyword: sender.stringValue, isCase: false, isWholeWord: false) { [weak self] results in
  280. self?.searchResults = results ?? []
  281. self?.tableView.reloadData()
  282. }
  283. }
  284. func updataLeftSideFindView() {
  285. if (self.searchResults.count > 0) {
  286. self.emptyBox.isHidden = true
  287. // self.searchResultsView.isHidden = false
  288. // self.searchResultsLabel.stringValue = String(format: KMLocalizedString("%ld Results"), self.searchResults.count)
  289. } else {
  290. self.emptyBox.isHidden = false
  291. // self.searchResultsView.isHidden = true
  292. }
  293. }
  294. }
  295. // MARK: - NSTableViewDelegate, NSTableViewDataSource
  296. extension KMBotaSearchViewController: NSTableViewDelegate, NSTableViewDataSource {
  297. func numberOfRows(in tableView: NSTableView) -> Int {
  298. return self.handdler.searchResults.count
  299. }
  300. func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
  301. let cell = tableView.makeView(withIdentifier: KMFindTableviewCell.km_identifier, owner: self) as! KMFindTableviewCell
  302. if handdler.searchResults.count > row {
  303. let selection = handdler.searchResults[row]
  304. if let data = tableColumn?.identifier.rawValue, data == kResultsColumnId.rawValue {
  305. cell.resultLabel.attributedStringValue = selection.attributedString
  306. cell.resultLabel.textColor = KMAppearance.Layout.h0Color()
  307. } else if let data = tableColumn?.identifier.rawValue, data == kPageColumnId.rawValue {
  308. cell.resultLabel.stringValue = "\(Int(selection.selectionPageIndex) + 1)"
  309. cell.resultLabel.textColor = KMAppearance.Layout.h2Color()
  310. }
  311. }
  312. return cell
  313. }
  314. func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
  315. return 40.0
  316. }
  317. func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
  318. let rowView = KMBotaTableRowView()
  319. return rowView
  320. }
  321. func tableViewSelectionDidChange(_ notification: Notification) {
  322. // if self.stopRepeatLoad == true {
  323. //
  324. // } else {
  325. // self.delegate?.controller?(controller: self, listViewSelectionDidChange: notification.object, info: nil)
  326. // }
  327. // [self updateFindResultHighlightsForDirection:NSDirectSelection];
  328. let row = self.tableView.selectedRow
  329. if row >= 0 {
  330. let model = handdler.searchResults[row]
  331. // let isEditing = self.listView?.isEditing() ?? false
  332. // if isEditing {
  333. // self.mainViewController?.srHanddler.showSelection(model.selection)
  334. // return
  335. // }
  336. // self.listView?.go(to: model.selection, animated: true)
  337. // self.listView?.setHighlightedSelection(model.selection, animated: true)
  338. DispatchQueue.main.asyncAfter(deadline: .now()+0.3) {
  339. // self.listView?.setHighlightedSelections([model.selection])
  340. // self.listView?.setNeedsDisplayAnnotationViewForVisiblePages()
  341. }
  342. }
  343. }
  344. func tableView(_ aTableView: NSTableView, copyRowsWithIndexes rowIndexes: IndexSet) {
  345. if IAPProductsManager.default().isAvailableAllFunction() == false {
  346. KMPurchaseCompareWindowController.sharedInstance().showWindow(nil)
  347. return
  348. }
  349. var string = ""
  350. for idx in rowIndexes {
  351. let match = handdler.searchResults[idx].selection
  352. string.append("* ")
  353. // [string appendFormat:NSLocalizedString(@"Page %@", @""), [match firstPageLabel]];
  354. string = string.appendingFormat(KMLocalizedString("Page %@"), "\(match.safeFirstPage()?.pageIndex() ?? 0)")
  355. // [string appendFormat:@"", [[match contextString] string]];
  356. string = string.appendingFormat(": %@\n", match.string() ?? "")
  357. }
  358. let pboard = NSPasteboard.general
  359. pboard.clearContents()
  360. pboard.writeObjects([string as NSPasteboardWriting])
  361. }
  362. }
  363. // MARK: - KMBotaTableViewDelegate
  364. extension KMBotaSearchViewController: KMBotaTableViewDelegate {
  365. func tableView(_ aTableView: NSTableView, canCopyRowsWithIndexes rowIndexes: IndexSet) -> Bool {
  366. return rowIndexes.count > 0
  367. }
  368. func tableViewMoveRight(_ aTableView: NSTableView) {
  369. // self.updateFindResultHighlightsForDirection(.selectingNext)
  370. }
  371. func tableViewMoveUp(_ aTableView: NSTableView) {
  372. self.tableView.km_safe_selectRowIndexes(.init(integer: self.tableView.selectedRow-1), byExtendingSelection: false)
  373. self.tableView.scrollRowToVisible(self.tableView.selectedRow)
  374. }
  375. func tableViewMoveDown(_ aTableView: NSTableView) {
  376. self.tableView.km_safe_selectRowIndexes(.init(integer: self.tableView.selectedRow+1), byExtendingSelection: false)
  377. self.tableView.scrollRowToVisible(self.tableView.selectedRow)
  378. }
  379. func tableView(_ aTableView: NSTableView, imageContextForRow rowIndex: Int) -> AnyObject? {
  380. if rowIndex >= self.searchResults.count {
  381. return nil
  382. }
  383. let model = self.searchResults[rowIndex]
  384. let selection = model.selection
  385. let x = selection.bounds.origin.x + NSWidth(selection.bounds) * 0.5
  386. let y = selection.bounds.origin.y + NSHeight(selection.bounds) * 0.5
  387. let point = NSPoint(x: x, y: y)
  388. // return CPDFDestination(document: self.pdfDocument(), pageIndex: Int(model.selectionPageIndex), at: point, zoom: self.scaleFactor().cgFloat)
  389. return nil
  390. }
  391. }
  392. //MARK: - ComponentGroupDelegate
  393. extension KMBotaSearchViewController: ComponentGroupDelegate {
  394. func componentGroupDidDismiss(group: ComponentGroup?) {
  395. // if group == groupView_ {
  396. // removeGroupView()
  397. // } else if group == menuGroupView_ {
  398. // group?.removeFromSuperview()
  399. // menuGroupView_ = nil
  400. // } else
  401. if group == searchGroupView {
  402. // searchGroupView_ = nil
  403. searchGroupTarget?.properties.state = .normal
  404. searchGroupTarget?.reloadData()
  405. searchGroupTarget = nil
  406. }
  407. }
  408. func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) {
  409. // if group == groupView_ {
  410. // if let selItem = menuItemProperty {
  411. // let index = group?.menuItemArr.firstIndex(of: selItem)
  412. // if index == 0 {
  413. // expandAllComments(item: NSMenuItem())
  414. // } else if index == 1 {
  415. // collapseAllComments(item: NSMenuItem())
  416. // } else if index == 2 {
  417. // removeAllOutlineItem(item: NSMenuItem())
  418. // }
  419. // }
  420. // } else if group == menuGroupView_ {
  421. // if let selItem = menuItemProperty {
  422. // let index = group?.menuItemArr.firstIndex(of: selItem)
  423. // if index == 0 {
  424. // addItemAction()
  425. // } else if index == 1 {
  426. // addChildItemAction()
  427. // } else if index == 2 {
  428. // addHigherItemAction()
  429. // } else if index == 4 {
  430. // deleteItemAction()
  431. // } else if index == 6 {
  432. // group?.removeFromSuperview()
  433. //
  434. // editItemAction()
  435. // } else if index == 7 {
  436. // renameItemAction()
  437. // } else if index == 8 {
  438. // changeItemAction()
  439. // } else if index == 10 {
  440. // promoteItemAction()
  441. // } else if index == 11 {
  442. // demoteItemAction()
  443. // }
  444. //
  445. // group?.removeFromSuperview()
  446. // }
  447. // } else
  448. if group == searchGroupView {
  449. guard let menuI = menuItemProperty else {
  450. return
  451. }
  452. let idx = group?.menuItemArr.firstIndex(of: menuI)
  453. if idx == 0 { // search
  454. } else if idx == 1 { // replace
  455. } else if idx == 3 {
  456. let key = KMNSearchKey.wholeWords.botaSearch
  457. let value = KMDataManager.ud_bool(forKey: key)
  458. KMDataManager.ud_set(!value, forKey: key)
  459. // BOTAOutlineView.wholeWords = !value
  460. } else if idx == 4 {
  461. let key = KMNSearchKey.caseSensitive.botaSearch
  462. let value = KMDataManager.ud_bool(forKey: key)
  463. KMDataManager.ud_set(!value, forKey: key)
  464. // BOTAOutlineView.caseSensitive = !value
  465. }
  466. }
  467. }
  468. }