KMSearchReplaceWindowController.swift 23 KB

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