KMSearchReplaceWindowController.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. //
  2. // KMSearchReplaceWindowController.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by User-Tangchao on 2024/8/7.
  6. //
  7. import Cocoa
  8. class KMSearchReplaceWindowController_Window: NSWindow {
  9. override var canBecomeMain: Bool {
  10. return true
  11. }
  12. override var canBecomeKey: Bool {
  13. return true
  14. }
  15. }
  16. @objc enum KMSearchReplaceType: Int {
  17. case none = 0
  18. case search = 1
  19. case replace = 2
  20. }
  21. class KMSearchReplaceWindowController: NSWindowController {
  22. @IBOutlet weak var titleBarBox: NSBox!
  23. @IBOutlet weak var closeButton: NSButton!
  24. @IBOutlet weak var tabBox: NSBox!
  25. @IBOutlet weak var searchTabButton: NSButton!
  26. @IBOutlet weak var replaceTabButton: NSButton!
  27. @IBOutlet weak var tabBottomLine: NSBox!
  28. @IBOutlet weak var tabSelectedLine: NSBox!
  29. @IBOutlet weak var tabSelectedLineLeftConst: NSLayoutConstraint!
  30. @IBOutlet weak var searchBox: NSBox!
  31. @IBOutlet weak var searchTitleLabel: NSTextField!
  32. @IBOutlet weak var searchInputBox: NSBox!
  33. @IBOutlet weak var searchInputView: NSTextField!
  34. @IBOutlet weak var matchWholeCheck: NSButton!
  35. @IBOutlet weak var caseSensitiveCheck: NSButton!
  36. @IBOutlet weak var previousButton: NSButton!
  37. @IBOutlet weak var nextButton: NSButton!
  38. @IBOutlet weak var replaceBox: NSBox!
  39. @IBOutlet weak var replaceTitleLabel: NSTextField!
  40. @IBOutlet weak var replaceInputBox: NSBox!
  41. @IBOutlet weak var replaceInputView: NSTextField!
  42. @IBOutlet weak var bottomBarBox: NSBox!
  43. @IBOutlet weak var replaceButton: NSButton!
  44. @IBOutlet weak var replaceAllButton: NSButton!
  45. var replaceCallback: (() -> Void)?
  46. private var _modalSession: NSApplication.ModalSession?
  47. private var handdler: KMSearchReplaceHanddler = KMSearchReplaceHanddler()
  48. private var type_: KMSearchReplaceType = .search
  49. private var currentSel: CPDFSelection?
  50. private var finding_ = false
  51. deinit {
  52. KMPrint("KMSearchReplaceWindowController deinit.")
  53. DistributedNotificationCenter.default().removeObserver(self)
  54. }
  55. convenience init(with pdfView: CPDFView?, type: KMSearchReplaceType) {
  56. self.init(windowNibName: "KMSearchReplaceWindowController")
  57. self.handdler.pdfView = pdfView
  58. self.type_ = type
  59. }
  60. override func windowDidLoad() {
  61. super.windowDidLoad()
  62. self.initDefaultValue()
  63. self.switchType(self.type_)
  64. self.updateViewColor()
  65. DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged), name: NSApplication.interfaceThemeChangedNotification, object: nil)
  66. }
  67. func initDefaultValue() {
  68. self.window?.isMovableByWindowBackground = true
  69. self.window?.contentView?.wantsLayer = true
  70. self.window?.contentView?.layer?.cornerRadius = 4
  71. self.window?.contentView?.layer?.masksToBounds = true
  72. self.window?.backgroundColor = .clear
  73. self.titleBarBox.boxType = .custom
  74. self.titleBarBox.borderWidth = 0
  75. self.closeButton.imagePosition = .imageOnly
  76. self.closeButton.image = NSImage(named: "KMImageNameUXIconBtnCloseNor")
  77. self.closeButton.target = self
  78. self.closeButton.action = #selector(_closeAction)
  79. self.searchTabButton.target = self
  80. self.searchTabButton.action = #selector(_searchTabAction)
  81. self.searchTabButton.title = " \(NSLocalizedString("Search", comment: ""))"
  82. self.searchTabButton.image = NSImage(named: "KMImageNameSearchIcon")
  83. self.searchTabButton.imagePosition = .imageLeft
  84. // self.searchTabButton.imageHugsTitle = true
  85. self.replaceTabButton.target = self
  86. self.replaceTabButton.action = #selector(_replaceTabAction)
  87. self.replaceTabButton.title = " \(NSLocalizedString("Replace", comment: ""))"
  88. self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceIcon")
  89. self.replaceTabButton.imagePosition = .imageLeft
  90. self.tabSelectedLine.borderWidth = 0
  91. self.tabSelectedLine.fillColor = NSColor(hex: "#4982E6")
  92. self.searchBox.borderWidth = 0
  93. // #0E1114
  94. self.searchTitleLabel.stringValue = NSLocalizedString("Search", comment: "")
  95. self.searchTitleLabel.font = NSFont.SFProTextBoldFont(14)
  96. self.searchInputBox.cornerRadius = 0
  97. self.searchInputView.drawsBackground = false
  98. self.searchInputView.isBordered = false
  99. self.searchInputView.delegate = self
  100. self.matchWholeCheck.title = NSLocalizedString("Whole Words Only", comment: "")
  101. self.matchWholeCheck.target = self
  102. self.matchWholeCheck.action = #selector(_checkAction)
  103. self.matchWholeCheck.state = .off
  104. self.caseSensitiveCheck.title = NSLocalizedString("Ignore Case", comment: "")
  105. self.caseSensitiveCheck.target = self
  106. self.caseSensitiveCheck.action = #selector(_checkAction)
  107. self.caseSensitiveCheck.state = .off
  108. self.previousButton.title = NSLocalizedString("Next", comment: "")
  109. self.previousButton.target = self
  110. self.previousButton.action = #selector(_nextAction)
  111. self.nextButton.title = NSLocalizedString("Previous", comment: "")
  112. self.nextButton.target = self
  113. self.nextButton.action = #selector(_previousAction)
  114. self.replaceBox.borderWidth = 0
  115. self.replaceTitleLabel.stringValue = NSLocalizedString("Replace with", comment: "")
  116. self.replaceTitleLabel.font = NSFont.SFProTextBoldFont(14)
  117. self.replaceInputBox.cornerRadius = 0
  118. self.replaceInputView.drawsBackground = false
  119. self.replaceInputView.isBordered = false
  120. self.replaceInputView.delegate = self
  121. self.bottomBarBox.borderWidth = 0
  122. self.replaceButton.title = NSLocalizedString("Replace", comment: "")
  123. self.replaceButton.target = self
  124. self.replaceButton.action = #selector(_replaceAction)
  125. self.replaceAllButton.title = NSLocalizedString("Replace All", comment: "")
  126. self.replaceAllButton.target = self
  127. self.replaceAllButton.action = #selector(_replaceAllAction)
  128. if self.searchInputView.stringValue.isEmpty {
  129. self.previousButton.isEnabled = false
  130. self.nextButton.isEnabled = false
  131. self.replaceButton.isEnabled = false
  132. self.replaceAllButton.isEnabled = false
  133. } else {
  134. self.previousButton.isEnabled = true
  135. self.nextButton.isEnabled = true
  136. self.replaceButton.isEnabled = true
  137. self.replaceAllButton.isEnabled = true
  138. self.currentSel = nil
  139. }
  140. }
  141. // MARK: - Actions
  142. @objc private func _closeAction(_ sender: NSButton) {
  143. self.endModal(sender)
  144. self.handdler.clearData()
  145. }
  146. @objc private func _previousAction(_ sender: NSButton) {
  147. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  148. if isEditing == false {
  149. guard let model = self.handdler.searchResults.safe_element(for: self.handdler.showIdx-1) as? KMSearchMode else {
  150. return
  151. }
  152. self.handdler.showIdx -= 1
  153. self.handdler.showSelection(model.selection)
  154. } else {
  155. if let _ = self.currentSel {
  156. self.currentSel = self.handdler.pdfView?.document.findForwardEditText()
  157. if let sel = self.currentSel {
  158. self.handdler.showSelection(sel)
  159. } else {
  160. let alert = NSAlert()
  161. alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  162. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  163. // alert.runModal()
  164. alert.beginSheetModal(for: (self.window)!)
  165. }
  166. } else {
  167. if self.finding_ {
  168. return
  169. }
  170. self.finding_ = true
  171. let searchS = self.searchInputView.stringValue
  172. let opt = self.fetchSearchOptions()
  173. self._beginLoading()
  174. DispatchQueue.global().async {
  175. let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt)
  176. DispatchQueue.main.async {
  177. self._endLoading()
  178. self.finding_ = false
  179. let sel = datas?.first?.first
  180. if sel == nil {
  181. let alert = NSAlert()
  182. alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  183. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  184. // alert.runModal()
  185. alert.beginSheetModal(for: (self.window)!)
  186. return
  187. }
  188. self.currentSel = sel
  189. self.handdler.showSelection(sel)
  190. }
  191. }
  192. }
  193. }
  194. }
  195. @objc private func _nextAction(_ sender: NSButton) {
  196. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  197. if isEditing == false {
  198. guard let model = self.handdler.searchResults.safe_element(for: self.handdler.showIdx+1) as? KMSearchMode else {
  199. return
  200. }
  201. self.handdler.showIdx += 1
  202. self.handdler.showSelection(model.selection)
  203. } else {
  204. if let _ = self.currentSel {
  205. self.currentSel = self.handdler.pdfView?.document.findBackwordEditText()
  206. if let sel = self.currentSel {
  207. self.handdler.showSelection(sel)
  208. } else {
  209. let alert = NSAlert()
  210. alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  211. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  212. // alert.runModal()
  213. alert.beginSheetModal(for: (self.window)!)
  214. }
  215. } else {
  216. if self.finding_ {
  217. return
  218. }
  219. self.finding_ = true
  220. let searchS = self.searchInputView.stringValue
  221. let opt = self.fetchSearchOptions()
  222. self._beginLoading()
  223. DispatchQueue.global().async {
  224. let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt)
  225. DispatchQueue.main.async {
  226. self._endLoading()
  227. self.finding_ = false
  228. let sel = datas?.first?.first
  229. if sel == nil {
  230. let alert = NSAlert()
  231. alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  232. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  233. // alert.runModal()
  234. alert.beginSheetModal(for: (self.window)!)
  235. return
  236. }
  237. self.currentSel = sel
  238. self.handdler.showSelection(sel)
  239. }
  240. }
  241. }
  242. }
  243. }
  244. @objc private func _checkAction(_ sender: NSButton) {
  245. self.currentSel = nil
  246. }
  247. @objc private func _searchTabAction(_ sender: NSButton) {
  248. self.switchType(.search, animate: true)
  249. }
  250. @objc private func _replaceTabAction(_ sender: NSButton) {
  251. self.switchType(.replace, animate: true)
  252. }
  253. @objc private func _replaceAction(_ sender: NSButton) {
  254. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  255. if isEditing == false {
  256. NSSound.beep()
  257. return
  258. }
  259. if let sel = self.currentSel {
  260. let searchS = self.searchInputView.stringValue
  261. let replaceS = self.replaceInputView.stringValue
  262. let success = self.handdler.replace(searchS: searchS, replaceS: replaceS, sel: sel) { [weak self] newSel in
  263. self?.handdler.showSelection(newSel)
  264. }
  265. if success {
  266. // self.handdler.showSelection(sel)
  267. }
  268. } else { // 先查找
  269. if self.finding_ {
  270. return
  271. }
  272. self.finding_ = true
  273. let searchS = self.searchInputView.stringValue
  274. let opt = self.fetchSearchOptions()
  275. self._beginLoading()
  276. DispatchQueue.global().async {
  277. let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt)
  278. DispatchQueue.main.async {
  279. self._endLoading()
  280. self.finding_ = false
  281. let sel = datas?.first?.first
  282. if sel == nil {
  283. let alert = NSAlert()
  284. alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  285. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  286. // alert.runModal()
  287. alert.beginSheetModal(for: (self.window)!)
  288. return
  289. }
  290. self.currentSel = sel
  291. self.handdler.showSelection(sel)
  292. }
  293. }
  294. }
  295. }
  296. @objc private func _replaceAllAction(_ sender: NSButton) {
  297. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  298. if isEditing == false {
  299. NSSound.beep()
  300. return
  301. }
  302. let datas = self.handdler.pdfView?.document.findEditSelections() ?? []
  303. if datas.isEmpty {
  304. let alert = NSAlert()
  305. alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  306. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  307. // alert.beginSheetModal(for: NSApp.mainWindow!)
  308. // alert.runModal()
  309. alert.beginSheetModal(for: (self.window)!)
  310. return
  311. }
  312. if self.finding_ {
  313. return
  314. }
  315. self.finding_ = true
  316. let searchS = self.searchInputView.stringValue
  317. let replaceS = self.replaceInputView.stringValue
  318. self._beginLoading()
  319. DispatchQueue.global().async {
  320. self.handdler.pdfView?.document.replaceAllEditText(with: searchS, toReplace: replaceS)
  321. self.currentSel = nil
  322. DispatchQueue.main.async {
  323. self._endLoading()
  324. self.finding_ = false
  325. self.handdler.pdfView?.setHighlightedSelection(nil, animated: false)
  326. self.handdler.pdfView?.setNeedsDisplayForVisiblePages()
  327. }
  328. }
  329. }
  330. private func fetchSearchOptions() -> CPDFSearchOptions {
  331. var opt = CPDFSearchOptions()
  332. let isCase = self.caseSensitiveCheck.state == .off
  333. if isCase {
  334. opt.insert(.caseSensitive)
  335. }
  336. let isWholeWord = self.matchWholeCheck.state == .on
  337. if isWholeWord {
  338. opt.insert(.matchWholeWord)
  339. }
  340. return opt
  341. }
  342. private func updateViewColor() {
  343. let isDark = KMAppearance.isDarkMode()
  344. if isDark {
  345. // self.window?.backgroundColor = NSColor(hex: "#393C3E")
  346. self.window?.contentView?.wantsLayer = true
  347. self.window?.contentView?.layer?.backgroundColor = NSColor(hex: "#393C3E").cgColor
  348. self.searchInputBox.borderColor = NSColor(hex: "#56585A")
  349. self.replaceInputBox.borderColor = NSColor(hex: "#56585A")
  350. } else {
  351. // self.window?.backgroundColor = .white
  352. self.window?.contentView?.wantsLayer = true
  353. self.window?.contentView?.layer?.backgroundColor = .white
  354. self.searchInputBox.borderColor = NSColor(hex: "#DADBDE")
  355. self.replaceInputBox.borderColor = NSColor(hex: "#DADBDE")
  356. }
  357. self.switchType(self.type_)
  358. }
  359. func switchType(_ type: KMSearchReplaceType, animate: Bool = false) {
  360. if type == .replace {
  361. if KMMemberInfo.shared.isMemberAllFunction == false {
  362. KMMemberInfo.shared.advancedFunctionUsage()
  363. return
  364. }
  365. }
  366. self.type_ = type
  367. let isDark = KMAppearance.isDarkMode()
  368. var selectedColor = NSColor(hex: "0E1114")
  369. var unSelectedColor = NSColor(hex: "757780")
  370. if isDark {
  371. selectedColor = .white
  372. unSelectedColor = NSColor(hex: "#7E7F85")
  373. }
  374. if type == .search { // 248
  375. self.tabSelectedLineLeftConst.animator().constant = 24
  376. self.searchTabButton.setTitleColor(selectedColor)
  377. self.searchTabButton.image = NSImage(named: "KMImageNameSearchIcon")
  378. self.replaceTabButton.setTitleColor(unSelectedColor)
  379. self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceUnselectedIcon")
  380. // DispatchQueue.main.async {
  381. self.replaceBox.isHidden = true
  382. self.bottomBarBox.isHidden = true
  383. // }
  384. var frame = self.window?.frame ?? .zero
  385. let height: CGFloat = 248+20
  386. let heightOffset = frame.size.height - height
  387. frame.origin.y += heightOffset
  388. frame.size.height = height
  389. self.window?.setFrame(frame, display: true, animate: animate)
  390. self.window?.minSize = frame.size
  391. self.window?.maxSize = frame.size
  392. } else if type == .replace { // 388
  393. self.tabSelectedLineLeftConst.animator().constant = 140
  394. self.searchTabButton.setTitleColor(unSelectedColor)
  395. self.searchTabButton.image = NSImage(named: "KMImageNameSearchUnselectedIcon")
  396. self.replaceTabButton.setTitleColor(selectedColor)
  397. self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceIcon")
  398. DispatchQueue.main.async {
  399. self.replaceBox.isHidden = false
  400. self.bottomBarBox.isHidden = false
  401. }
  402. var frame = self.window?.frame ?? .zero
  403. let height:CGFloat = 388
  404. let heightOffset = frame.size.height-height
  405. frame.origin.y += heightOffset
  406. frame.size.height = height
  407. self.window?.setFrame(frame, display: true, animate: animate)
  408. self.window?.minSize = frame.size
  409. self.window?.maxSize = frame.size
  410. // 将事件回调出去
  411. self.replaceCallback?()
  412. }
  413. }
  414. private func _beginLoading() {
  415. self.window?.contentView?.beginLoading()
  416. }
  417. private func _endLoading() {
  418. self.window?.contentView?.endLoading()
  419. }
  420. func startModal(_ sender: AnyObject?) {
  421. NSApp.stopModal()
  422. var modalCode: NSApplication.ModalResponse?
  423. if let _win = self.window {
  424. self._modalSession = NSApp.beginModalSession(for: _win)
  425. repeat {
  426. modalCode = NSApp.runModalSession(self._modalSession!)
  427. } while (modalCode == .continue)
  428. }
  429. }
  430. func endModal(_ sender: AnyObject?) {
  431. if let session = self._modalSession {
  432. NSApp.stopModal()
  433. NSApp.endModalSession(session)
  434. self.window?.orderOut(self)
  435. }
  436. if let winC = self.window?.kmCurrentWindowC, winC.isEqual(to: self) {
  437. self.window?.kmCurrentWindowC = nil
  438. }
  439. }
  440. // MARK: - Noti Methods
  441. @objc func themeChanged(_ notification: Notification) {
  442. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  443. self.updateViewColor()
  444. }
  445. }
  446. }
  447. extension KMSearchReplaceWindowController: NSTextFieldDelegate {
  448. func controlTextDidEndEditing(_ obj: Notification) {
  449. }
  450. func controlTextDidChange(_ obj: Notification) {
  451. if self.searchInputView.isEqual(to: obj.object) { // 搜索输入框
  452. if self.searchInputView.stringValue.isEmpty {
  453. self.previousButton.isEnabled = false
  454. self.nextButton.isEnabled = false
  455. self.replaceButton.isEnabled = false
  456. self.replaceAllButton.isEnabled = false
  457. } else {
  458. self.previousButton.isEnabled = true
  459. self.nextButton.isEnabled = true
  460. self.replaceButton.isEnabled = true
  461. self.replaceAllButton.isEnabled = true
  462. self.currentSel = nil
  463. }
  464. }
  465. }
  466. func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
  467. switch commandSelector {
  468. case #selector(NSResponder.insertNewline(_:)):
  469. if let inputView = control as? NSTextField {
  470. // //当当前TextField按下enter
  471. if inputView == self.searchInputView {
  472. let isCase = self.caseSensitiveCheck.state == .off
  473. let isWholeWord = self.matchWholeCheck.state == .on
  474. let isEditing = self.handdler.pdfView?.isEditing() ?? false
  475. if isEditing == false {
  476. if self.finding_ {
  477. return false
  478. }
  479. self.finding_ = true
  480. self._beginLoading()
  481. self.handdler.search(keyword: self.searchInputView.stringValue, isCase: isCase, isWholeWord: isWholeWord, callback: { [weak self] datas in
  482. self?.finding_ = false
  483. self?._endLoading()
  484. guard let sels = datas, sels.isEmpty == false else {
  485. let alert = NSAlert()
  486. alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  487. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  488. // alert.runModal()
  489. alert.beginSheetModal(for: (self?.window)!)
  490. return
  491. }
  492. if let sel = datas?.first?.selection {
  493. self?.handdler.showIdx = 0
  494. self?.handdler.showSelection(sel)
  495. }
  496. })
  497. } else {
  498. if self.finding_ {
  499. return false
  500. }
  501. self.finding_ = true
  502. let searchS = self.searchInputView.stringValue
  503. let opt = self.fetchSearchOptions()
  504. self._beginLoading()
  505. DispatchQueue.global().async {
  506. let datas = self.handdler.pdfView?.document.findEditAllPageString(searchS, with: opt) ?? []
  507. DispatchQueue.main.async {
  508. self.finding_ = false
  509. self._endLoading()
  510. if datas.isEmpty {
  511. let alert = NSAlert()
  512. alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "")
  513. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  514. // alert.beginSheetModal(for: NSApp.mainWindow!)
  515. // alert.runModal()
  516. alert.beginSheetModal(for: (self.window)!)
  517. return
  518. }
  519. self.currentSel = datas.first?.first
  520. if let sel = self.currentSel {
  521. self.handdler.showSelection(sel)
  522. }
  523. }
  524. }
  525. }
  526. }
  527. }
  528. return true
  529. default:
  530. return false
  531. }
  532. }
  533. }