// // 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? var closeCallback: (() -> Void)? private var _modalSession: NSApplication.ModalSession? var handdler: KMNSearchHanddler = KMNSearchHanddler() { didSet { if handdler.searchKey?.count != 0 && handdler.searchResults.count == 0{ self.search(keyboard: handdler.searchKey ?? "") } else { self.reloadData() } } } private var currentSel: CPDFSelection? 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) } @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)") } 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_.itemClick = { [weak self] idx, _ in if idx == 1 { self?._closeAction(NSButton()) } else if idx == 2 { self?._closeAction(NSButton()) self?.itemClick?(1, self?.handdler) } else if idx == 3 { self?.switchType(.search, animate: true) self?.itemClick?(3, self?.handdler) self?.search(keyboard: self?.handdler.searchKey ?? "") } else if idx == 4 { self?.switchType(.replace, animate: true) self?.itemClick?(4, self?.handdler) self?.search(keyboard: self?.handdler.searchKey ?? "") } } 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) updateButtonStatus() } } 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() } override func updateUIThemeColor() { super.updateUIThemeColor() KMMainThreadExecute { 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 { replaceButton.properties.isDisabled = false replaceButton.reloadData() replaceAllButton.properties.isDisabled = false replaceAllButton.reloadData() previousButton.properties.isDisabled = (self.handdler.showIdx == 0) ? true : false previousButton.reloadData() nextButton.properties.isDisabled = ((self.handdler.showIdx == self.handdler.resultCount - 1) || self.handdler.resultCount == 0) ? true : false nextButton.reloadData() } } private func updateViewColor() { let isDark = KMAppearance.isDarkMode() if isDark { self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorBg/popup").cgColor self.window?.contentView?.border(ComponentLibrary.shared.getComponentColorFromKey("colorBorder/popUp"), 1, 8) } else { self.window?.contentView?.wantsLayer = true self.window?.contentView?.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorBg/popup").cgColor self.window?.contentView?.border(ComponentLibrary.shared.getComponentColorFromKey("colorBorder/popUp"), 1, 8) } self.switchType(handdler.type) } func switchType(_ type: KMNBotaSearchType, animate: Bool = false) { if type == .replace { if KMMemberInfo.shared.isLogin == false { KMLoginWindowsController.shared.showWindow(nil) return } } handdler.type = type self.titleBarView_.type = type if type == .search { // 248 112 self.replaceBox.isHidden = true var frame = self.window?.frame ?? .zero let height: CGFloat = 156 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 = 252 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?() } } 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 self._beginLoading() self.handdler.search(keyword: keyboard, isCase: isCase, isWholeWord: isWholeWord, isEdit: isEditing, callback: { [weak self] datas in self?._endLoading() self?.reloadData() }) } func reloadData() { let handdler = self.handdler searchItemView_.inputValue = handdler.searchKey ?? "" replaceItemView_.inputValue = handdler.replaceKey ?? "" if handdler.showIdx != 0 { let model = self.handdler.searchResults[handdler.showIdx] self.handdler.showSelection(model.selection) } else { let sels = handdler.searchResults if let sel = sels.first?.selection { handdler.showSelection(sel) } else { handdler.showSelection(CPDFSelection()) } } self._showIndexTip() self.updateButtonStatus() } } //MARK: Actions extension KMSearchReplaceWindowController { @objc private func _closeAction(_ sender: NSButton) { self.window?.orderOut(nil) self.handdler.clearData() self.closeCallback?() } @objc public func _previousAction(_ sender: NSButton?) { let index = self.handdler.previous() if index < self.handdler.searchResults.count && index >= 0{ let model = self.handdler.searchResults[index] self.handdler.showSelection(model.selection) self._showIndexTip() self.updateButtonStatus() } } @objc public func _nextAction(_ sender: NSButton?) { let index = self.handdler.next() if index < self.handdler.searchResults.count && index >= 0 { let model = self.handdler.searchResults[index] self.handdler.showSelection(model.selection) self._showIndexTip() self.updateButtonStatus() } } @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 } guard let selection = self.currentSel else { return } let searchS = self.searchItemView_.inputValue let replaceS = self.replaceItemView_.inputValue self.handdler.replace(searchS: searchS, replaceS: replaceS, sel: selection) { [weak self] newSel in self?.handdler.showSelection(newSel) } } @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 } 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.handdler.pdfView?.setHighlightedSelection(nil, animated: false) self.handdler.pdfView?.setNeedsDisplayForVisiblePages() } } } } //MARK: Alert extension KMSearchReplaceWindowController { 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() } } } //MARK: ComponentGroupDelegate extension KMSearchReplaceWindowController: ComponentGroupDelegate { 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 } 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 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 let data = searchItemView_.inputValue if data.isEmpty { search(keyboard: data) } } } } } //MARK: Load extension KMSearchReplaceWindowController { private func _beginLoading() { self.window?.contentView?.beginLoading() } private func _endLoading() { self.window?.contentView?.endLoading() } } //MARK: Modal extension KMSearchReplaceWindowController { 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 } } }