// // KMSearchReplaceWindowController.swift // PDF Reader Pro // // Created by User-Tangchao on 2024/8/7. // import Cocoa class KMSearchReplaceWindowController_Window: NSWindow { override var canBecomeMain: Bool { return true } override var canBecomeKey: Bool { return true } } @objc enum KMSearchReplaceType: Int { case none = 0 case search = 1 case replace = 2 } class KMSearchReplaceWindowController: NSWindowController { @IBOutlet weak var titleBarBox: NSBox! @IBOutlet weak var closeButton: NSButton! @IBOutlet weak var tabBox: NSBox! @IBOutlet weak var searchTabButton: NSButton! @IBOutlet weak var replaceTabButton: NSButton! @IBOutlet weak var tabBottomLine: NSBox! @IBOutlet weak var tabSelectedLine: NSBox! @IBOutlet weak var tabSelectedLineLeftConst: NSLayoutConstraint! @IBOutlet weak var searchBox: NSBox! @IBOutlet weak var searchTitleLabel: NSTextField! @IBOutlet weak var searchInputBox: NSBox! @IBOutlet weak var searchInputView: NSTextField! @IBOutlet weak var matchWholeCheck: NSButton! @IBOutlet weak var caseSensitiveCheck: NSButton! @IBOutlet weak var previousButton: NSButton! @IBOutlet weak var nextButton: NSButton! @IBOutlet weak var replaceBox: NSBox! @IBOutlet weak var replaceTitleLabel: NSTextField! @IBOutlet weak var replaceInputBox: NSBox! @IBOutlet weak var replaceInputView: NSTextField! @IBOutlet weak var bottomBarBox: NSBox! @IBOutlet weak var replaceButton: NSButton! @IBOutlet weak var replaceAllButton: NSButton! var replaceCallback: (() -> Void)? private var _modalSession: NSApplication.ModalSession? private var handdler: KMSearchReplaceHanddler = KMSearchReplaceHanddler() private var type_: KMSearchReplaceType = .search private var currentSel: CPDFSelection? private var finding_ = false deinit { KMPrint("KMSearchReplaceWindowController deinit.") DistributedNotificationCenter.default().removeObserver(self) } convenience init(with pdfView: CPDFView?, type: KMSearchReplaceType) { self.init(windowNibName: "KMSearchReplaceWindowController") self.handdler.pdfView = pdfView self.type_ = type } override func windowDidLoad() { super.windowDidLoad() self.initDefaultValue() self.switchType(self.type_) self.updateViewColor() DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged), name: NSApplication.interfaceThemeChangedNotification, object: nil) } func initDefaultValue() { self.window?.isMovableByWindowBackground = true self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.cornerRadius = 4 self.window?.contentView?.layer?.masksToBounds = true self.window?.backgroundColor = .clear self.titleBarBox.boxType = .custom self.titleBarBox.borderWidth = 0 self.closeButton.imagePosition = .imageOnly self.closeButton.image = NSImage(named: "KMImageNameUXIconBtnCloseNor") self.closeButton.target = self self.closeButton.action = #selector(_closeAction) self.searchTabButton.target = self self.searchTabButton.action = #selector(_searchTabAction) self.searchTabButton.title = " \(NSLocalizedString("Search", comment: ""))" self.searchTabButton.image = NSImage(named: "KMImageNameSearchIcon") self.searchTabButton.imagePosition = .imageLeft // self.searchTabButton.imageHugsTitle = true self.replaceTabButton.target = self self.replaceTabButton.action = #selector(_replaceTabAction) self.replaceTabButton.title = " \(NSLocalizedString("Replace", comment: ""))" self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceIcon") self.replaceTabButton.imagePosition = .imageLeft self.tabSelectedLine.borderWidth = 0 self.tabSelectedLine.fillColor = NSColor.km_init(hex: "#4982E6") self.searchBox.borderWidth = 0 // #0E1114 self.searchTitleLabel.stringValue = NSLocalizedString("Search", comment: "") self.searchTitleLabel.font = NSFont.SFProTextBoldFont(14) self.searchInputBox.cornerRadius = 0 self.searchInputView.drawsBackground = false self.searchInputView.isBordered = false self.searchInputView.delegate = self self.matchWholeCheck.title = NSLocalizedString("Whole Words Only", comment: "") self.matchWholeCheck.target = self self.matchWholeCheck.action = #selector(_checkAction) self.matchWholeCheck.state = .off self.caseSensitiveCheck.title = NSLocalizedString("Ignore Case", comment: "") self.caseSensitiveCheck.target = self self.caseSensitiveCheck.action = #selector(_checkAction) self.caseSensitiveCheck.state = .off self.previousButton.title = NSLocalizedString("Next", comment: "") self.previousButton.target = self self.previousButton.action = #selector(_nextAction) self.nextButton.title = NSLocalizedString("Previous", comment: "") self.nextButton.target = self self.nextButton.action = #selector(_previousAction) self.replaceBox.borderWidth = 0 self.replaceTitleLabel.stringValue = NSLocalizedString("Replace with", comment: "") self.replaceTitleLabel.font = NSFont.SFProTextBoldFont(14) self.replaceInputBox.cornerRadius = 0 self.replaceInputView.drawsBackground = false self.replaceInputView.isBordered = false self.replaceInputView.delegate = self self.bottomBarBox.borderWidth = 0 self.replaceButton.title = NSLocalizedString("Replace", comment: "") self.replaceButton.target = self self.replaceButton.action = #selector(_replaceAction) self.replaceAllButton.title = NSLocalizedString("Replace All", comment: "") self.replaceAllButton.target = self self.replaceAllButton.action = #selector(_replaceAllAction) if self.searchInputView.stringValue.isEmpty { self.previousButton.isEnabled = false self.nextButton.isEnabled = false self.replaceButton.isEnabled = false self.replaceAllButton.isEnabled = false } else { self.previousButton.isEnabled = true self.nextButton.isEnabled = true self.replaceButton.isEnabled = true self.replaceAllButton.isEnabled = true self.currentSel = nil } } // MARK: - Actions @objc private func _closeAction(_ sender: NSButton) { self.endModal(sender) self.handdler.clearData() } @objc private func _previousAction(_ sender: NSButton) { let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { guard let model = self.handdler.searchResults.safe_element(for: self.handdler.showIdx-1) as? KMSearchMode else { return } self.handdler.showIdx -= 1 self.handdler.showSelection(model.selection) } else { if let _ = self.currentSel { self.currentSel = self.handdler.pdfView?.document.findForwardEditText() if let sel = self.currentSel { self.handdler.showSelection(sel) } else { let alert = NSAlert() alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self.window)!) } } else { if self.finding_ { return } self.finding_ = true let searchS = self.searchInputView.stringValue let opt = self.fetchSearchOptions() self._beginLoading() DispatchQueue.global().async { let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt) DispatchQueue.main.async { self._endLoading() self.finding_ = false let sel = datas?.first?.first if sel == nil { let alert = NSAlert() alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self.window)!) return } self.currentSel = sel self.handdler.showSelection(sel) } } } } } @objc private func _nextAction(_ sender: NSButton) { let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { guard let model = self.handdler.searchResults.safe_element(for: self.handdler.showIdx+1) as? KMSearchMode else { return } self.handdler.showIdx += 1 self.handdler.showSelection(model.selection) } else { if let _ = self.currentSel { self.currentSel = self.handdler.pdfView?.document.findBackwordEditText() if let sel = self.currentSel { self.handdler.showSelection(sel) } else { let alert = NSAlert() alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self.window)!) } } else { if self.finding_ { return } self.finding_ = true let searchS = self.searchInputView.stringValue let opt = self.fetchSearchOptions() self._beginLoading() DispatchQueue.global().async { let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt) DispatchQueue.main.async { self._endLoading() self.finding_ = false let sel = datas?.first?.first if sel == nil { let alert = NSAlert() alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self.window)!) return } self.currentSel = sel self.handdler.showSelection(sel) } } } } } @objc private func _checkAction(_ sender: NSButton) { self.currentSel = nil } @objc private func _searchTabAction(_ sender: NSButton) { self.switchType(.search, animate: true) } @objc private func _replaceTabAction(_ sender: NSButton) { self.switchType(.replace, animate: true) } @objc private func _replaceAction(_ sender: NSButton) { let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { NSSound.beep() return } if let sel = self.currentSel { let searchS = self.searchInputView.stringValue let replaceS = self.replaceInputView.stringValue let success = self.handdler.replace(searchS: searchS, replaceS: replaceS, sel: sel) { [weak self] newSel in self?.handdler.showSelection(newSel) } if success { // self.handdler.showSelection(sel) } } else { // 先查找 if self.finding_ { return } self.finding_ = true let searchS = self.searchInputView.stringValue let opt = self.fetchSearchOptions() self._beginLoading() DispatchQueue.global().async { let datas = self.handdler.pdfView?.document.startFindEditText(from: nil, with: searchS, options: opt) DispatchQueue.main.async { self._endLoading() self.finding_ = false let sel = datas?.first?.first if sel == nil { let alert = NSAlert() alert.messageText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self.window)!) return } self.currentSel = sel self.handdler.showSelection(sel) } } } } @objc private func _replaceAllAction(_ sender: NSButton) { let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { NSSound.beep() return } let datas = self.handdler.pdfView?.document.findEditSelections() ?? [] if datas.isEmpty { let alert = NSAlert() alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.beginSheetModal(for: NSApp.mainWindow!) // alert.runModal() alert.beginSheetModal(for: (self.window)!) return } if self.finding_ { return } self.finding_ = true let searchS = self.searchInputView.stringValue let replaceS = self.replaceInputView.stringValue self._beginLoading() DispatchQueue.global().async { self.handdler.pdfView?.document.replaceAllEditText(with: searchS, toReplace: replaceS) self.currentSel = nil DispatchQueue.main.async { self._endLoading() self.finding_ = false self.handdler.pdfView?.setHighlightedSelection(nil, animated: false) self.handdler.pdfView?.setNeedsDisplayForVisiblePages() } } } private func fetchSearchOptions() -> CPDFSearchOptions { var opt = CPDFSearchOptions() let isCase = self.caseSensitiveCheck.state == .off if isCase { opt.insert(.caseSensitive) } let isWholeWord = self.matchWholeCheck.state == .on if isWholeWord { opt.insert(.matchWholeWord) } return opt } private func updateViewColor() { let isDark = KMAppearance.isDarkMode() if isDark { // self.window?.backgroundColor = NSColor(hex: "#393C3E") self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = NSColor.km_init(hex: "#393C3E").cgColor self.searchInputBox.borderColor = NSColor.km_init(hex: "#56585A") self.replaceInputBox.borderColor = NSColor.km_init(hex: "#56585A") } else { // self.window?.backgroundColor = .white self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = .white self.searchInputBox.borderColor = NSColor.km_init(hex: "#DADBDE") self.replaceInputBox.borderColor = NSColor.km_init(hex: "#DADBDE") } self.switchType(self.type_) } func switchType(_ type: KMSearchReplaceType, animate: Bool = false) { if type == .replace { if IAPProductsManager.default().isAvailableAllFunction() == false { // KMPurchaseCompareWindowController.sharedInstance().showWindow(nil) let winC = KMPurchaseCompareWindowController.sharedInstance() winC?.showWindow(nil) guard let win = winC?.window else { return } self.window?.addChildWindow(win, ordered: .above) return } } self.type_ = type let isDark = KMAppearance.isDarkMode() var selectedColor = NSColor.km_init(hex: "0E1114") var unSelectedColor = NSColor.km_init(hex: "757780") if isDark { selectedColor = .white unSelectedColor = NSColor.km_init(hex: "#7E7F85") } if type == .search { // 248 self.tabSelectedLineLeftConst.animator().constant = 24 self.searchTabButton.setTitleColor(selectedColor) self.searchTabButton.image = NSImage(named: "KMImageNameSearchIcon") self.replaceTabButton.setTitleColor(unSelectedColor) self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceUnselectedIcon") // DispatchQueue.main.async { self.replaceBox.isHidden = true self.bottomBarBox.isHidden = true // } var frame = self.window?.frame ?? .zero let height: CGFloat = 248+20 let heightOffset = frame.size.height - height frame.origin.y += heightOffset frame.size.height = height self.window?.setFrame(frame, display: true, animate: animate) self.window?.minSize = frame.size self.window?.maxSize = frame.size } else if type == .replace { // 388 self.tabSelectedLineLeftConst.animator().constant = 140 self.searchTabButton.setTitleColor(unSelectedColor) self.searchTabButton.image = NSImage(named: "KMImageNameSearchUnselectedIcon") self.replaceTabButton.setTitleColor(selectedColor) self.replaceTabButton.image = NSImage(named: "KMImageNameReplaceIcon") DispatchQueue.main.async { self.replaceBox.isHidden = false self.bottomBarBox.isHidden = false } var frame = self.window?.frame ?? .zero let height:CGFloat = 388 let heightOffset = frame.size.height-height frame.origin.y += heightOffset frame.size.height = height self.window?.setFrame(frame, display: true, animate: animate) self.window?.minSize = frame.size self.window?.maxSize = frame.size // 将事件回调出去 self.replaceCallback?() } } private func _beginLoading() { self.window?.contentView?.beginLoading() } private func _endLoading() { self.window?.contentView?.endLoading() } func startModal(_ sender: AnyObject?) { NSApp.stopModal() var modalCode: NSApplication.ModalResponse? if let _win = self.window { self._modalSession = NSApp.beginModalSession(for: _win) repeat { modalCode = NSApp.runModalSession(self._modalSession!) } while (modalCode == .continue) } } func endModal(_ sender: AnyObject?) { if let session = self._modalSession { NSApp.stopModal() NSApp.endModalSession(session) self.window?.orderOut(self) } if let winC = self.window?.kmCurrentWindowC, winC.isEqual(to: self) { self.window?.kmCurrentWindowC = nil } } // MARK: - Noti Methods @objc func themeChanged(_ notification: Notification) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.updateViewColor() } } } extension KMSearchReplaceWindowController: NSTextFieldDelegate { func controlTextDidEndEditing(_ obj: Notification) { } func controlTextDidChange(_ obj: Notification) { if self.searchInputView.isEqual(to: obj.object) { // 搜索输入框 if self.searchInputView.stringValue.isEmpty { self.previousButton.isEnabled = false self.nextButton.isEnabled = false self.replaceButton.isEnabled = false self.replaceAllButton.isEnabled = false } else { self.previousButton.isEnabled = true self.nextButton.isEnabled = true self.replaceButton.isEnabled = true self.replaceAllButton.isEnabled = true self.currentSel = nil } } } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { switch commandSelector { case #selector(NSResponder.insertNewline(_:)): if let inputView = control as? NSTextField { // //当当前TextField按下enter if inputView == self.searchInputView { let isCase = self.caseSensitiveCheck.state == .off let isWholeWord = self.matchWholeCheck.state == .on let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { if self.finding_ { return false } self.finding_ = true self._beginLoading() self.handdler.search(keyword: self.searchInputView.stringValue, isCase: isCase, isWholeWord: isWholeWord, callback: { [weak self] datas in self?.finding_ = false self?._endLoading() guard let sels = datas, sels.isEmpty == false else { let alert = NSAlert() alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.runModal() alert.beginSheetModal(for: (self?.window)!) return } if let sel = datas?.first?.selection { self?.handdler.showIdx = 0 self?.handdler.showSelection(sel) } }) } else { if self.finding_ { return false } self.finding_ = true let searchS = self.searchInputView.stringValue let opt = self.fetchSearchOptions() self._beginLoading() DispatchQueue.global().async { let datas = self.handdler.pdfView?.document.findEditAllPageString(searchS, with: opt) ?? [] DispatchQueue.main.async { self.finding_ = false self._endLoading() if datas.isEmpty { let alert = NSAlert() alert.informativeText = NSLocalizedString("No related content found, please change keyword.", comment: "") alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) // alert.beginSheetModal(for: NSApp.mainWindow!) // alert.runModal() alert.beginSheetModal(for: (self.window)!) return } self.currentSel = datas.first?.first if let sel = self.currentSel { self.handdler.showSelection(sel) } } } } } } return true default: return false } } }