// // KMSearchReplaceWindowController.swift // PDF Reader Pro // // Created by User-Tangchao on 2024/8/7. // import Cocoa import KMComponentLibrary class KMSearchReplaceWindowController_Window: NSWindow { override var canBecomeMain: Bool { return true } override var canBecomeKey: Bool { return true } } class KMSearchReplaceWindowController: KMNBaseWindowController { @IBOutlet weak var titleBarBox: NSBox! @IBOutlet weak var tabBox: NSBox! @IBOutlet weak var searchBox: NSBox! @IBOutlet weak var replaceBox: NSBox! var replaceCallback: (() -> Void)? var itemClick: KMCommonClickBlock? private var _modalSession: NSApplication.ModalSession? private var handdler = KMNSearchHanddler() private var type_: KMNBotaSearchType = .search private var currentSel: CPDFSelection? private var finding_ = false private lazy var titleBarView_: KMNSearchReplaceTitleBarView = { let view = KMNSearchReplaceTitleBarView() return view }() private lazy var searchItemView_: KMNSearchReplaceSearchItemView = { let view = KMNSearchReplaceSearchItemView() return view }() private lazy var replaceItemView_: KMNSearchReplacePopItemView = { let view = KMNSearchReplacePopItemView() return view }() var previousButton: ComponentButton { get { return searchItemView_.previousButton } } var nextButton: ComponentButton { get { return searchItemView_.nextButton } } var replaceAllButton: ComponentButton { get { return replaceItemView_.replaceAllButton } } var replaceButton: ComponentButton { get { return replaceItemView_.replaceButton } } convenience init(with pdfView: CPDFView?, type: KMNBotaSearchType) { self.init(windowNibName: "KMSearchReplaceWindowController") self.handdler.pdfView = pdfView self.type_ = type } override func windowDidLoad() { super.windowDidLoad() self.initDefaultValue() self.switchType(self.type_) } func initDefaultValue() { window?.isMovableByWindowBackground = true window?.contentView?.wantsLayer = true window?.contentView?.layer?.cornerRadius = ComponentLibrary.shared.getComponentValueFromKey("radius/m") as? CGFloat ?? 8 window?.contentView?.layer?.masksToBounds = true window?.backgroundColor = .clear titleBarBox.boxType = .custom titleBarBox.borderWidth = 0 titleBarBox.contentView = titleBarView_ titleBarView_.titleLabel.font = .SFProTextRegularFont(14) titleBarView_.itemClick = { [unowned self] idx, _ in if idx == 1 { _closeAction(NSButton()) } else if idx == 2 { _closeAction(NSButton()) itemClick?(1, handdler) } } searchBox.borderWidth = 0 searchBox.contentView = searchItemView_ searchItemView_.itemClick = { [unowned self] idx, _ in if idx == 1 { // Previous _previousAction(NSButton()) } else if idx == 2 { // next _nextAction(NSButton()) } } searchItemView_.valueDidChange = { [unowned self] value, _ in if let data = value as? String { handdler.searchKey = data search(keyboard: data) } } searchItemView_.inputDidEditBlock = { [unowned self] in updateButtonStatus() let value = searchItemView_.inputValue if value.isEmpty { } else { currentSel = nil } } replaceBox.borderWidth = 0 replaceBox.contentView = replaceItemView_ replaceItemView_.itemClick = { [unowned self] idx, _ in if idx == 1 { _replaceAllAction(NSButton()) } else if idx == 2 { _replaceAction(NSButton()) } } replaceItemView_.valueDidChange = { [unowned self] value, _ in if let data = value as? String { handdler.replaceKey = data } } updateButtonStatus() if searchItemView_.inputValue.isEmpty { } else { self.currentSel = nil } } override func updateUILanguage() { super.updateUILanguage() KMMainThreadExecute { self.titleBarView_.titleLabel.stringValue = KMLocalizedString("Search") } } override func updateUIThemeColor() { super.updateUIThemeColor() KMMainThreadExecute { self.titleBarView_.titleLabel.textColor = KMNColorTools.colorText_1() self.updateViewColor() } } func updateButtonStatus() { let value = searchItemView_.inputValue if value.isEmpty { previousButton.properties.isDisabled = true previousButton.reloadData() nextButton.properties.isDisabled = true nextButton.reloadData() replaceButton.properties.isDisabled = true replaceButton.reloadData() replaceAllButton.properties.isDisabled = true replaceAllButton.reloadData() } else { previousButton.properties.isDisabled = false previousButton.reloadData() nextButton.properties.isDisabled = false nextButton.reloadData() replaceButton.properties.isDisabled = false replaceButton.reloadData() replaceAllButton.properties.isDisabled = false replaceAllButton.reloadData() } } func update(keyborad: String?, replaceKey: String?, results: [KMSearchMode]) { searchItemView_.inputValue = keyborad ?? "" replaceItemView_.inputValue = replaceKey ?? "" if results.isEmpty == false { handdler.searchResults = results self.currentSel = results.first?.selection if let sel = self.currentSel { self.handdler.showSelection(sel) } } updateButtonStatus() } // MARK: - Actions @objc private func _closeAction(_ sender: NSButton) { self.window?.orderOut(nil) 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 { _showNoResultsAlert() return } } else { if self.finding_ { return } self.finding_ = true let searchS = self.searchItemView_.inputValue 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 { self._showNoResultsAlert() 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 { _showNoResultsAlert() } } else { if self.finding_ { return } self.finding_ = true let searchS = self.searchItemView_.inputValue 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 { self._showNoResultsAlert() 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.searchItemView_.inputValue let replaceS = self.replaceItemView_.inputValue 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.searchItemView_.inputValue 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 { self._showNoResultsAlert() 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 { _showNoResultsAlert() return } if self.finding_ { return } self.finding_ = true let searchS = self.searchItemView_.inputValue let replaceS = self.replaceItemView_.inputValue 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 _showNoResultsAlert() { _ = _showAlert(style: .critical, message: KMLocalizedString("No related content found, please change keyword."), info: "", buttons: [KMLocalizedString("OK", comment: "")]) } private func _showAlert(style: NSAlert.Style, message: String, info: String, buttons: [String]) -> NSApplication.ModalResponse { let alert = NSAlert() alert.alertStyle = style alert.messageText = message alert.informativeText = info for button in buttons { alert.addButton(withTitle: button) } return alert.runModal() } private func fetchSearchOptions() -> CPDFSearchOptions { var opt = CPDFSearchOptions() let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) if isCase { opt.insert(.caseSensitive) } if isWholeWord { opt.insert(.matchWholeWord) } return opt } private func updateViewColor() { let isDark = KMAppearance.isDarkMode() if isDark { self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = NSColor(hex: "#393C3E").cgColor } else { self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = .white } self.switchType(self.type_) } func switchType(_ type: KMNBotaSearchType, animate: Bool = false) { if type == .replace { if IAPProductsManager.default().isAvailableAllFunction() == false { 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(hex: "0E1114") var unSelectedColor = NSColor(hex: "757780") if isDark { selectedColor = .white unSelectedColor = NSColor(hex: "#7E7F85") } if type == .search { // 248 112 self.replaceBox.isHidden = true var frame = self.window?.frame ?? .zero let height: CGFloat = 112 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 208 DispatchQueue.main.async { self.replaceBox.isHidden = false } var frame = self.window?.frame ?? .zero let height:CGFloat = 208 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 } } func search(keyboard: String) { let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) let isEditing = self.handdler.pdfView?.isEditing() ?? false if isEditing == false { if self.finding_ { return } self.finding_ = true self._beginLoading() self.handdler.search(keyword: keyboard, isCase: isCase, isWholeWord: isWholeWord, callback: { [weak self] datas in self?.finding_ = false self?._endLoading() guard let sels = self?.handdler.searchResults, sels.isEmpty == false else { self?._showNoResultsAlert() return } if let sel = sels.first?.selection { self?.handdler.showIdx = 0 self?.handdler.showSelection(sel) } }) } else { if self.finding_ { return } self.finding_ = true let searchS = keyboard 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 { self._showNoResultsAlert() return } self.currentSel = datas.first?.first if let sel = self.currentSel { self.handdler.showSelection(sel) } } } } } }