// // 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 } // override func setFrame(from string: NSWindow.PersistableFrameDescriptor) { // super.setFrame(from: string) // } // // override func setFrame(_ frameRect: NSRect, display flag: Bool) { // let frame = frameRect //// let supFrame = self.parent?.frame ?? .zero // //// KMPrint("frame: \(frame)") //// KMPrint("supFrame: \(supFrame)") // // var theFrame = frame // theFrame.origin.x = max(frame.origin.x, 100) // theFrame.origin.y = max(frame.origin.y, 100) // // super.setFrame(theFrame, display: flag) // } // // override func setFrame(_ frameRect: NSRect, display displayFlag: Bool, animate animateFlag: Bool) { // super.setFrame(frameRect, display: displayFlag, animate: animateFlag) // } // // override func setFrameOrigin(_ point: NSPoint) { // super.setFrameOrigin(point) // } } 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 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 } } private var searchGroupView_: ComponentGroup? convenience init(with pdfView: CPDFView?, type: KMNBotaSearchType) { self.init(windowNibName: "KMSearchReplaceWindowController") self.handdler.pdfView = pdfView handdler.type = type } override func windowDidLoad() { super.windowDidLoad() self.initDefaultValue() self.switchType(handdler.type) // NotificationCenter.default.addObserver(self, selector: #selector(didMoveNotification), name: NSWindow.didMoveNotification, object: self.window) // NotificationCenter.default.addObserver(self, selector: #selector(didMoveNotification), name: NSWindow.willMoveNotification, object: self.window) } @objc func didMoveNotification(_ noti: Notification) { let window = self.window let frame = window?.frame ?? .zero let supFrame = window?.parent?.frame ?? .zero KMPrint("frame: \(frame)") KMPrint("supFrame: \(supFrame)") // var theFrame = frame // theFrame.origin.x = max(frame.origin.x, supFrame.origin.x) // theFrame.origin.y = max(frame.origin.y, supFrame.origin.y) // // self.window?.setFrame(theFrame, display: true) // self.window?.setFrameOrigin(theFrame.origin) } 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()) } else if idx == 3 { showSearchGroupView(sender: ComponentButton()) } } 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 } } searchItemView_.input.properties.showSuffix = false searchItemView_.input.reloadData() 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.searchItemView_.input.properties.leftIcon = NSImage(named: "KMImageNameBotaSearch") self.searchItemView_.input.reloadData() self.replaceItemView_.input.properties.leftIcon = NSImage(named: "KMImagenameBotaSearchInputPrefiex") self.replaceItemView_.input.reloadData() 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 ?? "" handdler.searchKey = keyborad handdler.replaceKey = replaceKey if results.isEmpty == false { handdler.searchResults = results self.currentSel = results.first?.selection if let sel = self.currentSel { self.handdler.showSelection(sel) } } updateButtonStatus() } // MARK: - Group View func showSearchGroupView(sender: ComponentButton) { var viewHeight: CGFloat = 8 var menuItemArr: [ComponentMenuitemProperty] = [] let titles = ["Search", "Find and Replace", "", "Whole Words", "Case Sensitive"] for i in titles { if i.isEmpty { let menuI = ComponentMenuitemProperty.divider() menuItemArr.append(menuI) viewHeight += 8 } else { let menuI = ComponentMenuitemProperty(text: KMLocalizedString(i)) menuItemArr.append(menuI) viewHeight += 36 } } if handdler.type == .search { menuItemArr.first?.righticon = NSImage(named: "KMNImageNameMenuSelect") } else if handdler.type == .replace { let info = menuItemArr.safe_element(for: 1) as? ComponentMenuitemProperty info?.righticon = NSImage(named: "KMNImageNameMenuSelect") } if let info = menuItemArr.safe_element(for: 3) as? ComponentMenuitemProperty { if KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) { info.righticon = NSImage(named: "KMNImageNameMenuSelect") } } if let info = menuItemArr.last { if KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) { info.righticon = NSImage(named: "KMNImageNameMenuSelect") } } let groupView = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle()) searchGroupView_ = groupView groupView?.groupDelegate = self groupView?.frame = CGRectMake(310, 0, 200, viewHeight) groupView?.updateGroupInfo(menuItemArr) let senderView = self.searchItemView_.input var point = senderView.convert(senderView.frame.origin, to: nil) point.y -= viewHeight point.y -= 30 groupView?.showWithPoint(point, relativeTo: senderView) // searchGroupTarget = sender } // 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) self._showIndexTip() } else { if let _ = self.currentSel { self.currentSel = self.handdler.pdfView?.document.findForwardEditText() if let sel = self.currentSel { self.handdler.showIdx -= 1 self.handdler.showSelection(sel) self._showIndexTip() } 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.showIdx = 0 var count = 0 for i in datas ?? [] { count += i.count } self.handdler.resultCount = count self._showIndexTip() 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) self._showIndexTip() } else { if let _ = self.currentSel { self.currentSel = self.handdler.pdfView?.document.findBackwordEditText() if let sel = self.currentSel { self.handdler.showIdx += 1 self.handdler.showSelection(sel) self._showIndexTip() } 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.handdler.showIdx = 0 var count = 0 for i in datas ?? [] { count += i.count } self.handdler.resultCount = count self._showIndexTip() 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 _showIndexTip() { DispatchQueue.main.async { if self.handdler.resultCount == 0 { self.searchItemView_.input.properties.rightText = "" } else { self.searchItemView_.input.properties.rightText = "\(self.handdler.showIdx+1)/\(self.handdler.resultCount)" } self.searchItemView_.input.reloadData() } } 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(handdler.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 } } handdler.type = type 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) { if keyboard.isEmpty { handdler.resultCount = 0 _showIndexTip() return } 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.resultCount = sels.count self?._showIndexTip() 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 self.handdler.showIdx = 0 var count = 0 for i in datas { count += i.count } self.handdler.resultCount = count self._showIndexTip() if let sel = self.currentSel { self.handdler.showSelection(sel) } } } } } } extension KMSearchReplaceWindowController: ComponentGroupDelegate { func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) { if group == searchGroupView_ { guard let menuI = menuItemProperty else { return } let idx = group?.menuItemArr.firstIndex(of: menuI) if idx == 0 { // search switchType(.search) } else if idx == 1 { // replace switchType(.replace) } else if idx == 3 { let key = KMNSearchKey.wholeWords.botaSearch let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) currentSel = nil // currentModel_ = nil let data = searchItemView_.inputValue if data.isEmpty { search(keyboard: data) } } else if idx == 4 { let key = KMNSearchKey.caseSensitive.botaSearch let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) currentSel = nil // currentModel_ = nil let data = searchItemView_.inputValue if data.isEmpty { search(keyboard: data) } } } } }