KMSearchReplaceWindowController.swift 21 KB

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