// // KMBotaSearchViewController.swift // PDF Reader Pro // // Created by tangchao on 2023/11/16. // import Cocoa import KMComponentLibrary extension KMNSearchKey.wholeWords { static let botaSearch = "BotaSearchWholeWordsKey" } extension KMNSearchKey.caseSensitive { static let botaSearch = "BotaSearchCaseSensitiveKey" } enum KMNBotaSearchType: Int { case none = 0 case search = 1 case replace = 2 } @objc protocol KMBotaSearchViewControllerDelegate: NSObjectProtocol { @objc optional func switchSearchPopWindow(controller: KMBotaSearchViewController) } class KMBotaSearchViewController: KMNBotaBaseViewController { @IBOutlet weak var searchField: NSSearchField! @IBOutlet weak var segmentedControl: KMSegmentedControl! @IBOutlet weak var topView: NSBox! @IBOutlet weak var topHeightConst: NSLayoutConstraint! var contentView: NSView? { didSet { if let view = self.contentView { self.box.contentView = view } } } @IBOutlet weak var emptyBox: NSBox! @IBOutlet weak var searchBox: KMBox! @IBOutlet weak var searchResultsView: NSView! @IBOutlet weak var searchResultsLabel: NSTextField! @IBOutlet weak var searchDomeButton: NSButton! @IBOutlet weak var box: NSBox! @IBOutlet weak var emptySearchLabel: NSTextField! @IBOutlet weak var searchLabel: NSTextField! @IBOutlet weak var searchTips: NSTextField! @IBOutlet weak var pageLabel: NSTextField! @IBOutlet var scrollView: NSScrollView! @IBOutlet weak var tableView: KMBotaTableView! private lazy var topContentView_: KMNBotaSearchTopView? = { let view = KMNBotaSearchTopView.createFromNib() return view }() private var emptyView_: ComponentEmpty = { let view = ComponentEmpty() view.properties = ComponentEmptyProperty(emptyType: .noSearch, state: .normal, text: KMLocalizedString("No Results"), subText: KMLocalizedString("")) return view }() private var menuGroupView_: ComponentGroup? private var menuSections_: [CPDFSelection] = [] private var menuType_: CAnnotationType = .circle var handdler = KMNSearchHanddler() weak var delegate: KMBotaSearchViewControllerDelegate? var searchResults : [KMBotaSearchSectionModel] = [] { didSet { self.updataLeftSideFindView() } } private var datas: [Any] = [] private var searchResultIndex_: Int = -1 override func loadView() { super.loadView() topView.borderWidth = 0 topView.fillColor = .clear topView.contentView = topContentView_ topContentView_?.itemClick = { [unowned self] idx, params in if idx == KMNBotaSearchTopItemKey.search.rawValue { if let data = params.first as? ComponentButton { showSearchGroupView(sender: data) } } else if idx == KMNBotaSearchTopItemKey.replace.rawValue { if handdler.type == .search { handdler.type = .replace showReplaceView() } else { handdler.type = .search showSearchView() } } else if idx == KMNBotaSearchTopItemKey.switch.rawValue { delegate?.switchSearchPopWindow?(controller: self) } else if idx == KMNBotaSearchTopItemKey.previous.rawValue { tableViewMoveUp(tableView) } else if idx == KMNBotaSearchTopItemKey.next.rawValue { tableViewMoveDown(tableView) } } showSearchView() topContentView_?.valueDidChange = { [unowned self] sender, info in guard let string = info?[.newKey] as? String else { return } self.search(keyword: string) { [unowned self] results in searchResults = results ?? [] showResult() tableView.reloadData() } } emptyBox.contentView?.addSubview(emptyView_) emptyView_.km_add_top_constraint(constant: 232) emptyView_.km_add_bottom_constraint() emptyView_.km_add_leading_constraint() emptyView_.km_add_trailing_constraint() self.emptySearchLabel.stringValue = KMLocalizedString("") self.emptySearchLabel.textColor = KMAppearance.Layout.h1Color() self.emptyBox.fillColor = KMAppearance.Layout.l0Color() contentView = tableView.enclosingScrollView tableView.menuClickedAction = { [unowned self] point in let idxs = self.tableView.selectedRowIndexes.count let convertP = self.tableView.convert(point, from: nil) let row = self.tableView.row(at: convertP) if row == -1 { return NSMenu() } let hideNotes = handdler.hideNotes() let allowsNotes = handdler.allowsNotes() if hideNotes || allowsNotes == false { return NSMenu() } guard let model = self.datas[row] as? KMSearchMode else { return NSMenu() } var viewHeight: CGFloat = 0 let items: [String] = ["Add New Circle", "Add New Rectangle", "Add New Highlight", "Add New Underline", "Add New Strikethrough"] var menuItemArr: [ComponentMenuitemProperty] = [] for value in items { let properties_Menuitem: ComponentMenuitemProperty = ComponentMenuitemProperty(multipleSelect: false, itemSelected: false, isDisabled: false, keyEquivalent: nil, text: KMLocalizedString(value), identifier: value) menuItemArr.append(properties_Menuitem) viewHeight += 36 } self.menuGroupView_ = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle()) self.menuGroupView_?.clickedAutoHide = false self.menuGroupView_?.groupDelegate = self self.menuGroupView_?.frame = CGRectMake(0, 0, 180, viewHeight) self.menuGroupView_?.updateGroupInfo(menuItemArr) self.menuSections_ = [model.selection] self.menuGroupView_?.showWithPoint(CGPoint(x: point.x, y: point.y - viewHeight), relativeTo: self.tableView) return NSMenu() } } override func viewDidLoad() { super.viewDidLoad() self.tableView.delegate = self self.tableView.dataSource = self self.tableView.botaDelegate = self } override func viewDidAppear() { super.viewDidAppear() // self.searchField.becomeFirstResponder() } override func updateUILanguage() { super.updateUILanguage() KMMainThreadExecute { self.topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " " + "\(self.handdler.searchResults.count)" self.tableView.reloadData() } } override func updateUIThemeColor() { super.updateUIThemeColor() KMMainThreadExecute { self.view.wantsLayer = true let color = KMNColorTools.colorBg_layoutMiddle() self.view.layer?.backgroundColor = color.cgColor self.tableView.backgroundColor = color self.topContentView_?.resultLabel.textColor = KMNColorTools.colorText_3() self.tableView.reloadData() } } func showSearchView() { topContentView_?.showSearch() topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0 } func showReplaceView() { topContentView_?.showReplace() topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: handdler.searchResults.isEmpty == false) ?? 0 } func showResult() { topContentView_?.showResult(type: handdler.type) topContentView_?.resultLabel.stringValue = KMLocalizedString("Result:") + " " + "\(self.handdler.searchResults.count)" topHeightConst.constant = topContentView_?.fetchContentHeight(type: handdler.type, hasResult: searchResults.isEmpty == false) ?? 0 } 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() } func search(keyword: String, callback: @escaping (([KMBotaSearchSectionModel]?) -> Void)) { let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) handdler.search(keyword: keyword, isCase: isCase, isWholeWord: isWholeWord, callback: callback) } // 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) var point = sender.convert(sender.frame.origin, to: nil) point.y -= viewHeight groupView?.showWithPoint(point, relativeTo: sender) searchGroupTarget = sender } @IBAction func searchDomeButtonAtion(_ sender: AnyObject) { self.searchField.isHidden = true self.searchDomeButton.isHidden = true self.searchBox.isHidden = false } @objc func goToSelectedFindResults(_ sender: AnyObject?) { // guard let olView = sender as? NSTableView, olView.clickedRow != -1 else { // NSSound.beep() // return // } // self.updateFindResultHighlightsForDirection(.directSelection) } @objc func addAnnotationsForSelections(_ sender: NSMenuItem) { for selection in menuSections_ { // self.listView?.addAnnotation(with: CAnnotationType(rawValue: sender.tag) ?? .circle, selection: selection, page: selection.page, bounds: selection.bounds) } } func updataLeftSideFindView() { if (self.searchResults.count > 0) { self.emptyBox.isHidden = true // self.searchResultsView.isHidden = false // self.searchResultsLabel.stringValue = String(format: KMLocalizedString("%ld Results"), self.searchResults.count) } else { self.emptyBox.isHidden = false // self.searchResultsView.isHidden = true } } func updateFindResultHighlightsForDirection(_ direction: NSWindow.SelectionDirection) { var findResults: [KMSearchMode] = handdler.searchResults if (findResults.count == 0) { handdler.showSelection(nil) } else { if direction == .directSelection { self.searchResultIndex_ = 0 } else if (direction == .selectingNext) { self.searchResultIndex_ += 1 if self.searchResultIndex_ >= findResults.count { self.searchResultIndex_ = 0 } } else if (direction == .selectingPrevious) { self.searchResultIndex_ -= 1 if self.searchResultIndex_ < 0 { self.searchResultIndex_ = findResults.count-1 } } let currentSel = findResults[self.searchResultIndex_].selection if currentSel.hasCharacters() { let page = currentSel.safeFirstPage() var rect = NSZeroRect for model in findResults { if let data = page, model.selection.pages().contains(data) { rect = NSUnionRect(rect, model.selection.bounds(for: data)) } } let FIND_RESULT_MARGIN = 50.0 rect = NSIntersectionRect(NSInsetRect(rect, -FIND_RESULT_MARGIN, -FIND_RESULT_MARGIN), page?.bounds(for: .cropBox) ?? .zero) handdler.pdfView?.go(to: page) handdler.pdfView?.go(to: rect, on: page) } if currentSel.hasCharacters() { let bColor = NSColor(red: 236/255.0, green: 241/255.0, blue: 83/255.0, alpha: 0.5) let color = NSColor(red: 219/255.0, green: 220/255.0, blue: 3/255.0, alpha: 0.5) handdler.pdfView?.setHighlight(currentSel, forBorderColor: .clear, fill: color, animated: true) handdler.pdfView?.go(to: currentSel, animated: true) handdler.pdfView?.setCurrentSelection(currentSel, animate: true) } // let mode = self.listView?.toolMode ?? .none // if mode == .moveToolMode || mode == .magnifyToolMode || mode == .selectToolMode { // self.listView?.setCurrentSelection(nil, animate: false) // } } } } // MARK: - NSTableViewDelegate, NSTableViewDataSource extension KMBotaSearchViewController: NSTableViewDelegate, NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { var datas: [Any] = [] for sectionM in self.handdler.searchSectionResults { if sectionM.items.count > 0 { datas.append(sectionM) if sectionM.isExpand == false { continue } for item in sectionM.items { datas.append(item) } } } self.datas = datas return datas.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let model = self.datas[row] if let data = model as? KMBotaSearchSectionModel { var cell = tableView.makeView(withIdentifier: KMSectionCellView.km_identifier, owner: nil) as? KMSectionCellView if cell == nil { cell = KMSectionCellView.createFromNib() } cell?.titleLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-medium") cell?.titleLabel.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-regular") cell?.titleLabel.textColor = KMNColorTools.colorText_1() cell?.countLabel.textColor = KMNColorTools.colorText_3() cell?.isExpand = data.isExpand let pageIndex = data.pageIndex cell?.titleLabel.stringValue = KMLocalizedString("Page") + " \(pageIndex + 1)" cell?.countLabel.stringValue = "\(data.itemCount)" cell?.bottomLine.wantsLayer = true cell?.bottomLine.layer?.backgroundColor = KMNColorTools.colorBorder_divider().cgColor cell?.itemClick = { [weak self] idx, _ in if idx == 1 { // 收取 & 展开 data.isExpand = !data.isExpand self?.tableView.reloadData() } } return cell } if let data = model as? KMSearchMode { var cell = tableView.makeView(withIdentifier: KMNBotaSearchCellView.km_identifier, owner: self) as? KMNBotaSearchCellView if cell == nil { cell = KMNBotaSearchCellView() } cell?.label.attributedStringValue = data.attributedString return cell } return nil } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { let model = datas[row] if model is KMBotaSearchSectionModel { return 40.0 } if let data = model as? KMSearchMode { let width = NSWidth(self.view.frame) let rect = data.attributedString.boundingRect(with: .init(width: width-24*2, height: CGFLOAT_MAX), options: [.usesLineFragmentOrigin, .usesLineFragmentOrigin]) return rect.size.height + 12 * 2 } return 40.0 } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = KMBotaTableRowView() return rowView } func tableViewSelectionDidChange(_ notification: Notification) { let row = self.tableView.selectedRow if row < 0 || row >= datas.count { return } guard let model = datas[row] as? KMSearchMode else { return } let isEditing = handdler.pdfView?.isEditing() ?? false if isEditing { handdler.showSelection(model.selection) return } DispatchQueue.main.asyncAfter(deadline: .now()+0.3) { self.handdler.showSelection(model.selection) } } func tableView(_ aTableView: NSTableView, copyRowsWithIndexes rowIndexes: IndexSet) { if IAPProductsManager.default().isAvailableAllFunction() == false { KMPurchaseCompareWindowController.sharedInstance().showWindow(nil) return } var string = "" for idx in rowIndexes { if idx < 0 || idx >= datas.count { continue } guard let model = datas[idx] as? KMSearchMode else { continue } let match = model.selection string.append("* ") // [string appendFormat:NSLocalizedString(@"Page %@", @""), [match firstPageLabel]]; string = string.appendingFormat(KMLocalizedString("Page %@"), "\(match.safeFirstPage()?.pageIndex() ?? 0)") // [string appendFormat:@"", [[match contextString] string]]; string = string.appendingFormat(": %@\n", match.string() ?? "") } let pboard = NSPasteboard.general pboard.clearContents() pboard.writeObjects([string as NSPasteboardWriting]) } } // MARK: - KMBotaTableViewDelegate extension KMBotaSearchViewController: KMBotaTableViewDelegate { func tableView(_ aTableView: NSTableView, canCopyRowsWithIndexes rowIndexes: IndexSet) -> Bool { return rowIndexes.count > 0 } func tableViewMoveRight(_ aTableView: NSTableView) { updateFindResultHighlightsForDirection(.selectingNext) } func tableViewMoveUp(_ aTableView: NSTableView) { self.tableView.km_safe_selectRowIndexes(.init(integer: self.tableView.selectedRow-1), byExtendingSelection: false) self.tableView.scrollRowToVisible(self.tableView.selectedRow) } func tableViewMoveDown(_ aTableView: NSTableView) { self.tableView.km_safe_selectRowIndexes(.init(integer: self.tableView.selectedRow+1), byExtendingSelection: false) self.tableView.scrollRowToVisible(self.tableView.selectedRow) } func tableView(_ aTableView: NSTableView, imageContextForRow rowIndex: Int) -> AnyObject? { if rowIndex < 0 || rowIndex >= datas.count { return nil } guard let model = datas[rowIndex] as? KMSearchMode else { return nil } let selection = model.selection let x = selection.bounds.origin.x + NSWidth(selection.bounds) * 0.5 let y = selection.bounds.origin.y + NSHeight(selection.bounds) * 0.5 let point = NSPoint(x: x, y: y) return CPDFDestination(document: handdler.pdfDocument(), pageIndex: Int(model.selectionPageIndex), at: point, zoom: handdler.scaleFactor() ?? 0) } } //MARK: - ComponentGroupDelegate extension KMBotaSearchViewController: ComponentGroupDelegate { func componentGroupDidDismiss(group: ComponentGroup?) { if group == menuGroupView_ { group?.removeFromSuperview() menuGroupView_ = nil } else if group == searchGroupView { searchGroupTarget?.properties.state = .normal searchGroupTarget?.reloadData() searchGroupTarget = nil } } func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) { if group == menuGroupView_ { if let selItem = menuItemProperty { let index = group?.menuItemArr.firstIndex(of: selItem) if index == 0 { menuType_ = .circle addAnnotationsForSelections(NSMenuItem()) } else if index == 1 { menuType_ = .square addAnnotationsForSelections(NSMenuItem()) } else if index == 2 { menuType_ = .highlight addAnnotationsForSelections(NSMenuItem()) } else if index == 3 { menuType_ = .underline addAnnotationsForSelections(NSMenuItem()) } else if index == 4 { menuType_ = .strikeOut addAnnotationsForSelections(NSMenuItem()) } group?.removeFromSuperview() } } else if group == searchGroupView { guard let menuI = menuItemProperty else { return } let idx = group?.menuItemArr.firstIndex(of: menuI) if idx == 0 { // search } else if idx == 1 { // replace } else if idx == 3 { let key = KMNSearchKey.wholeWords.botaSearch let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) if let data = topContentView_?.inputValue, data.isEmpty { search(keyword: data) { [weak self] results in self?.searchResults = results ?? [] self?.showResult() self?.tableView.reloadData() } } } else if idx == 4 { let key = KMNSearchKey.caseSensitive.botaSearch let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) if let data = topContentView_?.inputValue, data.isEmpty { search(keyword: data) { [weak self] results in self?.searchResults = results ?? [] self?.showResult() self?.tableView.reloadData() } } } } } }