KMBotaSearchViewController.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  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 none = 0
  17. case search = 1
  18. case replace = 2
  19. }
  20. @objc protocol KMBotaSearchViewControllerDelegate: NSObjectProtocol {
  21. @objc optional func switchSearchPopWindow(controller: KMBotaSearchViewController)
  22. @objc optional func searchTypeDidChange(controller: KMBotaSearchViewController)
  23. }
  24. class KMBotaSearchViewController: KMNBotaBaseViewController {
  25. @IBOutlet weak var topView: NSBox!
  26. @IBOutlet weak var topHeightConst: NSLayoutConstraint!
  27. var contentView: NSView? {
  28. didSet {
  29. if let view = self.contentView {
  30. self.box.contentView = view
  31. }
  32. }
  33. }
  34. @IBOutlet weak var emptyBox: NSBox!
  35. @IBOutlet weak var box: NSBox!
  36. @IBOutlet weak var emptySearchLabel: NSTextField!
  37. // @IBOutlet weak var searchLabel: NSTextField!
  38. // @IBOutlet weak var searchTips: NSTextField!
  39. // @IBOutlet weak var pageLabel: NSTextField!
  40. @IBOutlet var scrollView: NSScrollView!
  41. @IBOutlet weak var tableView: KMBotaTableView!
  42. private lazy var topContentView_: KMNBotaSearchTopView? = {
  43. let view = KMNBotaSearchTopView.createFromNib()
  44. return view
  45. }()
  46. private var emptyView_: ComponentEmpty = {
  47. let view = ComponentEmpty()
  48. view.properties = ComponentEmptyProperty(emptyType: .noSearch, state: .normal, text: KMLocalizedString("No Results"), subText: KMLocalizedString(""))
  49. return view
  50. }()
  51. var previousButton: ComponentButton? {
  52. get {
  53. return topContentView_?.previousButton
  54. }
  55. }
  56. var nextButton: ComponentButton? {
  57. get {
  58. return topContentView_?.nextButton
  59. }
  60. }
  61. var replaceAllButton: ComponentButton? {
  62. get {
  63. return topContentView_?.replaceAllButton
  64. }
  65. }
  66. var replaceButton: ComponentButton? {
  67. get {
  68. return topContentView_?.replaceButton
  69. }
  70. }
  71. private var menuGroupView_: ComponentGroup?
  72. private var menuSections_: [CPDFSelection] = []
  73. private var menuType_: CAnnotationType = .circle
  74. var handdler = KMNSearchHanddler() {
  75. didSet {
  76. self.reloadData()
  77. }
  78. }
  79. weak var delegate: KMBotaSearchViewControllerDelegate?
  80. var searchResults : [KMBotaSearchSectionModel] = [] {
  81. didSet {
  82. self.updataLeftSideFindView()
  83. }
  84. }
  85. private var datas: [Any] = []
  86. private var searchResultIndex_: Int = -1
  87. private var currentSel: CPDFSelection?
  88. private var currentModel_: KMSearchMode?
  89. override func loadView() {
  90. super.loadView()
  91. topView.borderWidth = 0
  92. topView.fillColor = .clear
  93. topView.contentView = topContentView_
  94. topContentView_?.itemClick = { [unowned self] idx, params in
  95. if idx == KMNBotaSearchTopItemKey.search.rawValue {
  96. if let data = params.first as? ComponentButton {
  97. showSearchGroupView(sender: data)
  98. }
  99. } else if idx == KMNBotaSearchTopItemKey.replace.rawValue {
  100. if handdler.type == .search {
  101. showReplaceView(needSearch: true)
  102. } else {
  103. showSearchView(needSearch: true)
  104. }
  105. } else if idx == KMNBotaSearchTopItemKey.switch.rawValue {
  106. delegate?.switchSearchPopWindow?(controller: self)
  107. } else if idx == KMNBotaSearchTopItemKey.previous.rawValue {
  108. tableViewMoveUp(tableView)
  109. _previousAction(NSButton())
  110. } else if idx == KMNBotaSearchTopItemKey.next.rawValue {
  111. tableViewMoveDown(tableView)
  112. _nextAction(NSButton())
  113. } else if idx == KMNBotaSearchTopItemKey.replaceText.rawValue {
  114. _replaceAction(NSButton())
  115. } else if idx == KMNBotaSearchTopItemKey.replaceAllText.rawValue {
  116. _replaceAllAction(NSButton())
  117. }
  118. self.updateButtonState()
  119. }
  120. showSearchView()
  121. topContentView_?.valueDidChange = { [unowned self] sender, info in
  122. guard let string = info?[.newKey] as? String else {
  123. return
  124. }
  125. updateButtonState()
  126. currentSel = nil
  127. currentModel_ = nil
  128. self.search(keyword: string)
  129. }
  130. topContentView_?.replaceValueDidChange = { [unowned self] sender, info in
  131. guard let string = info?[.newKey] as? String else {
  132. return
  133. }
  134. handdler.replaceKey = string
  135. }
  136. emptyBox.contentView?.addSubview(emptyView_)
  137. emptyView_.km_add_top_constraint(constant: 232)
  138. emptyView_.km_add_bottom_constraint()
  139. emptyView_.km_add_leading_constraint()
  140. emptyView_.km_add_trailing_constraint()
  141. self.emptySearchLabel.stringValue = KMLocalizedString("")
  142. self.emptySearchLabel.textColor = KMAppearance.Layout.h1Color()
  143. contentView = tableView.enclosingScrollView
  144. tableView.menuClickedAction = { [unowned self] point in
  145. let idxs = self.tableView.selectedRowIndexes.count
  146. let convertP = self.tableView.convert(point, from: nil)
  147. let row = self.tableView.row(at: convertP)
  148. if row == -1 {
  149. return NSMenu()
  150. }
  151. let hideNotes = handdler.hideNotes()
  152. let allowsNotes = handdler.allowsNotes()
  153. if hideNotes || allowsNotes == false {
  154. return NSMenu()
  155. }
  156. guard let model = self.datas[row] as? KMSearchMode else {
  157. return NSMenu()
  158. }
  159. var viewHeight: CGFloat = 0
  160. let items: [String] = ["Add New Circle", "Add New Rectangle", "Add New Highlight", "Add New Underline", "Add New Strikethrough"]
  161. var menuItemArr: [ComponentMenuitemProperty] = []
  162. for value in items {
  163. let properties_Menuitem: ComponentMenuitemProperty = ComponentMenuitemProperty(multipleSelect: false,
  164. itemSelected: false,
  165. isDisabled: false,
  166. keyEquivalent: nil,
  167. text: KMLocalizedString(value),
  168. identifier: value)
  169. menuItemArr.append(properties_Menuitem)
  170. viewHeight += 36
  171. }
  172. self.menuGroupView_ = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle())
  173. self.menuGroupView_?.clickedAutoHide = true
  174. self.menuGroupView_?.groupDelegate = self
  175. self.menuGroupView_?.frame = CGRectMake(0, 0, 180, viewHeight)
  176. self.menuGroupView_?.updateGroupInfo(menuItemArr)
  177. self.menuSections_ = [model.selection]
  178. self.menuGroupView_?.showWithPoint(CGPoint(x: point.x, y: point.y - viewHeight), relativeTo: nil)
  179. return NSMenu()
  180. }
  181. }
  182. override func viewDidLoad() {
  183. super.viewDidLoad()
  184. tableView.delegate = self
  185. tableView.dataSource = self
  186. tableView.botaDelegate = self
  187. tableView.hasImageToolTips = true
  188. updateButtonState()
  189. }
  190. override func viewDidAppear() {
  191. super.viewDidAppear()
  192. view.window?.makeFirstResponder(topContentView_?.searchInput)
  193. }
  194. override func updateUILanguage() {
  195. super.updateUILanguage()
  196. KMMainThreadExecute {
  197. self.topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " " + "\(self.handdler.searchResults.count)"
  198. self.tableView.reloadData()
  199. }
  200. }
  201. override func updateUIThemeColor() {
  202. super.updateUIThemeColor()
  203. KMMainThreadExecute {
  204. self.view.wantsLayer = true
  205. let color = KMNColorTools.colorBg_layoutMiddle()
  206. self.view.layer?.backgroundColor = color.cgColor
  207. self.tableView.backgroundColor = color
  208. self.topContentView_?.replaceInput.properties.leftIcon = NSImage(named: "KMImagenameBotaSearchInputPrefiex")
  209. self.topContentView_?.replaceInput.reloadData()
  210. self.topContentView_?.resultLabel.textColor = KMNColorTools.colorText_3()
  211. self.topContentView_?.bottomLine.layer?.backgroundColor = KMNColorTools.colorPrimary_border1().cgColor
  212. self.tableView.reloadData()
  213. }
  214. }
  215. func reloadData() {
  216. let handdler = self.handdler
  217. topContentView_?.inputValue = handdler.searchKey ?? ""
  218. topContentView_?.replaceText = handdler.replaceKey ?? ""
  219. self.searchResults = handdler.searchSectionResults
  220. var index = handdler.showIdx
  221. var row = 0
  222. var searchModel = KMSearchMode()
  223. if handdler.showIdx != 0 {
  224. searchModel = self.handdler.searchResults[handdler.showIdx]
  225. self.handdler.showSelection(searchModel.selection)
  226. } else {
  227. // 显示第一个匹配项的选择框
  228. let sels = self.handdler.searchResults
  229. if let firstSelection = sels.first?.selection {
  230. searchModel = sels.first!
  231. self.handdler.showSelection(firstSelection)
  232. } else {
  233. self.handdler.showSelection(CPDFSelection())
  234. }
  235. index = 0
  236. }
  237. // 展开所有结果
  238. for model in handdler.searchSectionResults {
  239. model.isExpand = true
  240. }
  241. // 更新当前模型并刷新表格
  242. self.currentModel_ = searchModel
  243. self.tableView.reloadData()
  244. if handdler.showIdx != 0 {
  245. // 查找匹配的 model
  246. if let result = datas.firstIndex(where: {
  247. // 尝试将元素转换为 MyModel 类型
  248. if let model = $0 as? KMSearchMode {
  249. return model == searchModel // 如果转换成功,进行比较
  250. }
  251. return false
  252. }) {
  253. print("Found at index: \(index)")
  254. row = result
  255. } else {
  256. print("Model not found")
  257. row = 1
  258. }
  259. } else {
  260. row = 1
  261. }
  262. self.tableView.km_safe_selectRowIndexes(.init(integer: row), byExtendingSelection: false)
  263. self.tableView.scrollRowToVisible(self.tableView.selectedRow)
  264. self.showResult()
  265. updateButtonState()
  266. }
  267. func showSearchView(needSearch: Bool = false) {
  268. handdler.type = .search
  269. topContentView_?.showSearch()
  270. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0
  271. delegate?.searchTypeDidChange?(controller: self)
  272. if needSearch {
  273. if let keyword = topContentView_?.inputValue, keyword.isEmpty == false {
  274. DispatchQueue.main.async {
  275. self.search(keyword: keyword)
  276. }
  277. }
  278. }
  279. }
  280. func showReplaceView(needSearch: Bool = false) {
  281. if KMMemberInfo.shared.isLogin == false {
  282. KMLoginWindowsController.shared.showWindow(nil)
  283. return
  284. }
  285. handdler.type = .replace
  286. topContentView_?.showReplace()
  287. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0
  288. delegate?.searchTypeDidChange?(controller: self)
  289. if needSearch {
  290. if let keyword = topContentView_?.inputValue, keyword.isEmpty == false {
  291. DispatchQueue.main.async {
  292. self.search(keyword: keyword)
  293. }
  294. }
  295. }
  296. }
  297. func showResult() {
  298. topContentView_?.showResult(type: handdler.type)
  299. var count = 0
  300. if self.datas.isEmpty {
  301. topContentView_?.resultLabel.stringValue = ""
  302. } else {
  303. count = self.handdler.searchResults.count
  304. var i = 0
  305. var sectionCount = 0
  306. for model in self.datas {
  307. if i > self.tableView.selectedRow {
  308. break
  309. }
  310. if let data = model as? KMBotaSearchSectionModel {
  311. sectionCount += 1
  312. }
  313. i += 1
  314. }
  315. let theIdx = max(i-sectionCount, 1)
  316. topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " \(theIdx)/" + "\(count)"
  317. self.handdler.showIdx = theIdx - 1
  318. }
  319. topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: searchResults.isEmpty == false) ?? 0
  320. self.updateButtonState()
  321. }
  322. private func fetchSearchOptions() -> CPDFSearchOptions {
  323. var opt = CPDFSearchOptions()
  324. let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch)
  325. let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch)
  326. if isCase {
  327. opt.insert(.caseSensitive)
  328. }
  329. if isWholeWord {
  330. opt.insert(.matchWholeWord)
  331. }
  332. return opt
  333. }
  334. func updateButtonState() {
  335. let value = topContentView_?.inputValue ?? ""
  336. if value.isEmpty || self.handdler.searchResults.isEmpty{
  337. previousButton?.properties.isDisabled = true
  338. previousButton?.reloadData()
  339. nextButton?.properties.isDisabled = true
  340. nextButton?.reloadData()
  341. replaceButton?.properties.isDisabled = true
  342. replaceButton?.reloadData()
  343. replaceAllButton?.properties.isDisabled = true
  344. replaceAllButton?.reloadData()
  345. } else {
  346. replaceButton?.properties.isDisabled = false
  347. replaceButton?.reloadData()
  348. replaceAllButton?.properties.isDisabled = false
  349. replaceAllButton?.reloadData()
  350. previousButton?.properties.isDisabled = (self.handdler.showIdx == 0) ? true : false
  351. previousButton?.reloadData()
  352. nextButton?.properties.isDisabled = ((self.handdler.showIdx == self.handdler.resultCount - 1) || self.handdler.resultCount == 0)
  353. nextButton?.reloadData()
  354. }
  355. }
  356. private func _showNoResultsAlert() {
  357. _ = _showAlert(style: .critical, message: KMLocalizedString("No related content found, please change keyword."), info: "", buttons: [KMLocalizedString("OK", comment: "")])
  358. }
  359. private func _showAlert(style: NSAlert.Style, message: String, info: String, buttons: [String]) -> NSApplication.ModalResponse {
  360. let alert = NSAlert()
  361. alert.alertStyle = style
  362. alert.messageText = message
  363. alert.informativeText = info
  364. for button in buttons {
  365. alert.addButton(withTitle: button)
  366. }
  367. return alert.runModal()
  368. }
  369. func updataLeftSideFindView() {
  370. if (self.searchResults.count > 0) {
  371. self.emptyBox.isHidden = true
  372. } else {
  373. self.emptyBox.isHidden = false
  374. }
  375. }
  376. func updateFindResultHighlightsForDirection(_ direction: NSWindow.SelectionDirection) {
  377. let findResults: [KMSearchMode] = handdler.searchResults
  378. if (findResults.count == 0) {
  379. handdler.showSelection(nil)
  380. } else {
  381. if direction == .directSelection {
  382. self.searchResultIndex_ = 0
  383. } else if (direction == .selectingNext) {
  384. self.searchResultIndex_ += 1
  385. if self.searchResultIndex_ >= findResults.count {
  386. self.searchResultIndex_ = 0
  387. }
  388. } else if (direction == .selectingPrevious) {
  389. self.searchResultIndex_ -= 1
  390. if self.searchResultIndex_ < 0 {
  391. self.searchResultIndex_ = findResults.count-1
  392. }
  393. }
  394. let currentSel = findResults[self.searchResultIndex_].selection
  395. if currentSel.hasCharacters() {
  396. let page = currentSel.safeFirstPage()
  397. var rect = NSZeroRect
  398. for model in findResults {
  399. if let data = page, model.selection.pages().contains(data) {
  400. rect = NSUnionRect(rect, model.selection.bounds(for: data))
  401. }
  402. }
  403. let FIND_RESULT_MARGIN = 50.0
  404. rect = NSIntersectionRect(NSInsetRect(rect, -FIND_RESULT_MARGIN, -FIND_RESULT_MARGIN), page?.bounds(for: .cropBox) ?? .zero)
  405. handdler.pdfView?.go(to: page)
  406. handdler.pdfView?.go(to: rect, on: page)
  407. }
  408. if currentSel.hasCharacters() {
  409. let bColor = NSColor(red: 236/255.0, green: 241/255.0, blue: 83/255.0, alpha: 0.5)
  410. let color = NSColor(red: 219/255.0, green: 220/255.0, blue: 3/255.0, alpha: 0.5)
  411. handdler.pdfView?.setHighlight(currentSel, forBorderColor: .clear, fill: color, animated: true)
  412. handdler.pdfView?.go(to: currentSel, animated: true)
  413. handdler.pdfView?.setCurrentSelection(currentSel, animate: true)
  414. }
  415. }
  416. }
  417. }
  418. //MARK: Action
  419. extension KMBotaSearchViewController {
  420. @objc private func _previousAction(_ sender: NSButton) {
  421. let index = self.handdler.previous()
  422. if index < self.handdler.searchResults.count && index >= 0{
  423. // let model = self.handdler.searchResults[index]
  424. // self.handdler.showSelection(model.selection)
  425. self.updateButtonState()
  426. }
  427. }
  428. @objc private func _nextAction(_ sender: NSButton) {
  429. let index = self.handdler.next()
  430. if index < self.handdler.searchResults.count && index >= 0 {
  431. // let model = self.handdler.searchResults[index]
  432. // self.handdler.showSelection(model.selection)
  433. self.updateButtonState()
  434. }
  435. }
  436. @objc private func _replaceAction(_ sender: NSButton) {
  437. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  438. if isEditing == false {
  439. NSSound.beep()
  440. return
  441. }
  442. if let sel = self.currentSel {
  443. let searchS = self.topContentView_?.inputValue ?? ""
  444. let replaceS = self.topContentView_?.replaceText ?? ""
  445. self.handdler.replace(searchS: searchS, replaceS: replaceS, sel: sel) { [weak self] newSel in
  446. self?.handdler.showSelection(newSel)
  447. if let data = newSel {
  448. self?.currentModel_?.selection = data
  449. self?.tableView.reloadData()
  450. }
  451. }
  452. }
  453. }
  454. @objc private func _replaceAllAction(_ sender: NSButton) {
  455. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  456. if isEditing == false {
  457. NSSound.beep()
  458. return
  459. }
  460. let datas = self.handdler.searchSectionResults
  461. if datas.isEmpty {
  462. _showNoResultsAlert()
  463. return
  464. }
  465. let searchS = self.topContentView_?.inputValue ?? ""
  466. let replaceS = self.topContentView_?.replaceText ?? ""
  467. self.beginLoading()
  468. DispatchQueue.global().async {
  469. self.handdler.pdfView?.document.replaceAllEditText(with: searchS, toReplace: replaceS)
  470. self.currentSel = nil
  471. DispatchQueue.main.async {
  472. self.endLoading()
  473. self.handdler.pdfView?.setHighlightedSelection(nil, animated: false)
  474. self.handdler.pdfView?.setNeedsDisplayForVisiblePages()
  475. }
  476. }
  477. }
  478. }
  479. // MARK: - NSTableViewDelegate, NSTableViewDataSource
  480. extension KMBotaSearchViewController: NSTableViewDelegate, NSTableViewDataSource {
  481. func numberOfRows(in tableView: NSTableView) -> Int {
  482. var datas: [Any] = []
  483. for sectionM in self.handdler.searchSectionResults {
  484. if sectionM.items.count > 0 {
  485. datas.append(sectionM)
  486. if sectionM.isExpand == false {
  487. continue
  488. }
  489. for item in sectionM.items {
  490. datas.append(item)
  491. }
  492. }
  493. }
  494. self.datas = datas
  495. return datas.count
  496. }
  497. func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
  498. let model = self.datas[row]
  499. if let data = model as? KMBotaSearchSectionModel {
  500. var cell = tableView.makeView(withIdentifier: KMSectionCellView.km_identifier, owner: nil) as? KMSectionCellView
  501. if cell == nil {
  502. cell = KMSectionCellView.createFromNib()
  503. }
  504. cell?.titleLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium")
  505. cell?.countLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-regular")
  506. cell?.titleLabel.textColor = KMNColorTools.colorText_1()
  507. cell?.countLabel.textColor = KMNColorTools.colorText_3()
  508. cell?.titleLabel.isEditable = false
  509. cell?.isExpand = data.isExpand
  510. let pageIndex = data.pageIndex
  511. cell?.titleLabel.stringValue = KMLocalizedString("Page") + " \(pageIndex + 1)"
  512. cell?.countLabel.stringValue = "\(data.itemCount)"
  513. cell?.bottomLine.wantsLayer = true
  514. cell?.bottomLine.layer?.backgroundColor = KMNColorTools.colorBorder_divider().cgColor
  515. cell?.itemClick = { [weak self] idx, _ in
  516. if idx == 1 { // 收取 & 展开
  517. data.isExpand = !data.isExpand
  518. self?.tableView.reloadData()
  519. }
  520. }
  521. return cell
  522. }
  523. if let data = model as? KMSearchMode {
  524. var cell = tableView.makeView(withIdentifier: KMNBotaSearchCellView.km_identifier, owner: self) as? KMNBotaSearchCellView
  525. if cell == nil {
  526. cell = KMNBotaSearchCellView()
  527. }
  528. cell?.label.attributedStringValue = data.attributedString
  529. cell?.label.isEnabled = false
  530. return cell
  531. }
  532. return nil
  533. }
  534. func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
  535. let model = datas[row]
  536. if model is KMBotaSearchSectionModel {
  537. return 40.0
  538. }
  539. if let data = model as? KMSearchMode {
  540. let width = NSWidth(self.view.frame)
  541. let rect = data.attributedString.boundingRect(with: .init(width: width-24*2, height: CGFLOAT_MAX), options: [.usesLineFragmentOrigin, .usesLineFragmentOrigin])
  542. return rect.size.height + 12 * 2
  543. }
  544. return 40.0
  545. }
  546. func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
  547. let model = datas[row]
  548. if model is KMBotaSearchSectionModel {
  549. let rowView = KMTableRowView()
  550. rowView.selectionBackgroundColorBlock = {
  551. return .clear
  552. }
  553. return rowView
  554. }
  555. let rowView = KMTableRowView()
  556. rowView.selectionRadius = ComponentLibrary.shared.getComponentValueFromKey("radius/xs") as? CGFloat ?? 4
  557. rowView.selectionBackgroundColorBlock = {
  558. return KMNColorTools.colorPrimary_bgOpacityDark()
  559. }
  560. rowView.selectionInset = .init(top: 8, left: 16, bottom: 8, right: 16)
  561. return rowView
  562. }
  563. func tableViewSelectionDidChange(_ notification: Notification) {
  564. let row = self.tableView.selectedRow
  565. if row < 0 || row >= datas.count {
  566. return
  567. }
  568. guard let model = datas[row] as? KMSearchMode else {
  569. return
  570. }
  571. handdler.showSelection(model.selection)
  572. currentSel = model.selection
  573. currentModel_ = model
  574. self.showResult()
  575. }
  576. func tableView(_ aTableView: NSTableView, copyRowsWithIndexes rowIndexes: IndexSet) {
  577. if IAPProductsManager.default().isAvailableAllFunction() == false {
  578. KMPurchaseCompareWindowController.sharedInstance().showWindow(nil)
  579. return
  580. }
  581. var string = ""
  582. for idx in rowIndexes {
  583. if idx < 0 || idx >= datas.count {
  584. continue
  585. }
  586. guard let model = datas[idx] as? KMSearchMode else {
  587. continue
  588. }
  589. let match = model.selection
  590. string.append("* ")
  591. string = string.appendingFormat(KMLocalizedString("Page %@"), "\(match.safeFirstPage()?.pageIndex() ?? 0)")
  592. string = string.appendingFormat(": %@\n", match.string() ?? "")
  593. }
  594. let pboard = NSPasteboard.general
  595. pboard.clearContents()
  596. pboard.writeObjects([string as NSPasteboardWriting])
  597. }
  598. }
  599. // MARK: - KMBotaTableViewDelegate
  600. extension KMBotaSearchViewController: KMBotaTableViewDelegate {
  601. func tableView(_ aTableView: NSTableView, canCopyRowsWithIndexes rowIndexes: IndexSet) -> Bool {
  602. return rowIndexes.count > 0
  603. }
  604. func tableViewMoveRight(_ aTableView: NSTableView) {
  605. updateFindResultHighlightsForDirection(.selectingNext)
  606. }
  607. func tableViewMoveUp(_ aTableView: NSTableView) {
  608. var theRow = self.tableView.selectedRow-1
  609. if theRow < 0 || theRow >= self.datas.count {
  610. return
  611. }
  612. let model = self.datas[theRow]
  613. if let _ = model as? KMBotaSearchSectionModel {
  614. theRow -= 1
  615. }
  616. self.tableView.km_safe_selectRowIndexes(.init(integer: theRow), byExtendingSelection: false)
  617. self.tableView.scrollRowToVisible(self.tableView.selectedRow)
  618. }
  619. func tableViewMoveDown(_ aTableView: NSTableView) {
  620. var theRow = self.tableView.selectedRow + 1
  621. if theRow < 0 || theRow >= self.datas.count {
  622. return
  623. }
  624. let model = self.datas[theRow]
  625. if let _ = model as? KMBotaSearchSectionModel {
  626. theRow += 1
  627. }
  628. self.tableView.km_safe_selectRowIndexes(.init(integer: theRow), byExtendingSelection: false)
  629. self.tableView.scrollRowToVisible(self.tableView.selectedRow)
  630. }
  631. func tableView(_ aTableView: NSTableView, imageContextForRow rowIndex: Int) -> AnyObject? {
  632. if rowIndex < 0 || rowIndex >= datas.count {
  633. return nil
  634. }
  635. guard let model = datas[rowIndex] as? KMSearchMode else {
  636. return -1 as AnyObject
  637. }
  638. let selection = model.selection
  639. let x = selection.bounds.origin.x + NSWidth(selection.bounds) * 0.5
  640. let y = selection.bounds.origin.y + NSHeight(selection.bounds) * 0.5
  641. let point = NSPoint(x: x, y: y)
  642. return CPDFDestination(document: handdler.pdfDocument(), pageIndex: Int(model.selectionPageIndex), at: point, zoom: handdler.scaleFactor() ?? 0)
  643. }
  644. }
  645. //MARK: - ComponentGroupDelegate
  646. extension KMBotaSearchViewController: ComponentGroupDelegate {
  647. func showSearchGroupView(sender: ComponentButton) {
  648. var viewHeight: CGFloat = 8
  649. var menuItemArr: [ComponentMenuitemProperty] = []
  650. let titles = ["Search", "Find and Replace", "", "Whole Words", "Case Sensitive"]
  651. for i in titles {
  652. if i.isEmpty {
  653. let menuI = ComponentMenuitemProperty.divider()
  654. menuItemArr.append(menuI)
  655. viewHeight += 8
  656. } else {
  657. let menuI = ComponentMenuitemProperty(text: KMLocalizedString(i))
  658. menuItemArr.append(menuI)
  659. viewHeight += 36
  660. }
  661. }
  662. if handdler.type == .search {
  663. menuItemArr.first?.righticon = NSImage(named: "KMNImageNameMenuSelect")
  664. } else if handdler.type == .replace {
  665. let info = menuItemArr.safe_element(for: 1) as? ComponentMenuitemProperty
  666. info?.righticon = NSImage(named: "KMNImageNameMenuSelect")
  667. }
  668. if let info = menuItemArr.safe_element(for: 3) as? ComponentMenuitemProperty {
  669. if KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) {
  670. info.righticon = NSImage(named: "KMNImageNameMenuSelect")
  671. }
  672. }
  673. if let info = menuItemArr.last {
  674. if KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) {
  675. info.righticon = NSImage(named: "KMNImageNameMenuSelect")
  676. }
  677. }
  678. let groupView = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle())
  679. searchGroupView = groupView
  680. groupView?.groupDelegate = self
  681. groupView?.frame = CGRectMake(310, 0, 200, viewHeight)
  682. groupView?.updateGroupInfo(menuItemArr)
  683. var point = sender.convert(sender.frame.origin, to: nil)
  684. point.y -= viewHeight
  685. groupView?.showWithPoint(point, relativeTo: sender)
  686. searchGroupTarget = sender
  687. }
  688. func componentGroupDidDismiss(group: ComponentGroup?) {
  689. if group == menuGroupView_ {
  690. // group?.removeFromSuperview()
  691. // menuGroupView_ = nil
  692. } else if group == searchGroupView {
  693. searchGroupTarget?.properties.state = .normal
  694. searchGroupTarget?.reloadData()
  695. searchGroupTarget = nil
  696. }
  697. }
  698. func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) {
  699. if group == menuGroupView_ {
  700. if let selItem = menuItemProperty {
  701. let index = group?.menuItemArr.firstIndex(of: selItem)
  702. if index == 0 {
  703. menuType_ = .circle
  704. addAnnotationsForSelections(NSMenuItem())
  705. } else if index == 1 {
  706. menuType_ = .square
  707. addAnnotationsForSelections(NSMenuItem())
  708. } else if index == 2 {
  709. menuType_ = .highlight
  710. addAnnotationsForSelections(NSMenuItem())
  711. } else if index == 3 {
  712. menuType_ = .underline
  713. addAnnotationsForSelections(NSMenuItem())
  714. } else if index == 4 {
  715. menuType_ = .strikeOut
  716. addAnnotationsForSelections(NSMenuItem())
  717. }
  718. }
  719. } else if group == searchGroupView {
  720. guard let menuI = menuItemProperty else {
  721. return
  722. }
  723. let idx = group?.menuItemArr.firstIndex(of: menuI)
  724. if idx == 0 { // search
  725. showSearchView()
  726. } else if idx == 1 { // replace
  727. showReplaceView()
  728. } else if idx == 3 {
  729. let key = KMNSearchKey.wholeWords.botaSearch
  730. let value = KMDataManager.ud_bool(forKey: key)
  731. KMDataManager.ud_set(!value, forKey: key)
  732. currentSel = nil
  733. currentModel_ = nil
  734. if let data = topContentView_?.inputValue, data.isEmpty == false {
  735. search(keyword: data)
  736. }
  737. } else if idx == 4 {
  738. let key = KMNSearchKey.caseSensitive.botaSearch
  739. let value = KMDataManager.ud_bool(forKey: key)
  740. KMDataManager.ud_set(!value, forKey: key)
  741. currentSel = nil
  742. currentModel_ = nil
  743. if let data = topContentView_?.inputValue, data.isEmpty == false {
  744. search(keyword: data)
  745. }
  746. }
  747. }
  748. }
  749. @objc func addAnnotationsForSelections(_ sender: NSMenuItem) {
  750. let listView = handdler.pdfView as? CPDFListView
  751. for selection in menuSections_ {
  752. // if menuType_ == .circle {
  753. listView?.addAnnotation(with: menuType_, selection: selection, page: selection.page, bounds: selection.bounds)
  754. // }
  755. }
  756. }
  757. }
  758. //MARK: Search
  759. extension KMBotaSearchViewController {
  760. func search(keyword: String) {
  761. // 调用实际搜索函数
  762. let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch)
  763. let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch)
  764. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  765. handdler.search(keyword: keyword, isCase: isCase, isWholeWord: isWholeWord, isEdit: isEditing) { [weak self] datas in
  766. self?.reloadData()
  767. }
  768. }
  769. }
  770. //MARK: Load
  771. extension KMBotaSearchViewController {
  772. private func beginLoading() {
  773. self.view.beginLoading()
  774. }
  775. private func endLoading() {
  776. self.view.endLoading()
  777. }
  778. }