// // 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 search = 1 case replace = 2 } 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() // , image: NSImage(named: "KMImageNameOutlineEmpty") 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() var searchResults : [KMBotaSearchSectionModel] = [] { didSet { self.updataLeftSideFindView() } } private var datas: [Any] = [] deinit { KMPrint("KMBotaSearchViewController deinit.") NotificationCenter.default.removeObserver(self) } 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 { } 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 } let isCase = KMDataManager.ud_bool(forKey: KMNSearchKey.caseSensitive.botaSearch) let isWholeWord = KMDataManager.ud_bool(forKey: KMNSearchKey.wholeWords.botaSearch) handdler.search(keyword: string, isCase: isCase, isWholeWord: isWholeWord) { [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() // self.searchLabel.stringValue = KMLocalizedString("Search") // self.searchLabel.textColor = KMAppearance.Layout.h0Color() // self.searchTips.stringValue = KMLocalizedString("Search") // self.searchTips.textColor = KMAppearance.Layout.h2Color() // self.searchResultsLabel.textColor = KMAppearance.Layout.h1Color() // self.pageLabel.stringValue = KMLocalizedString("Page") // self.pageLabel.textColor = KMAppearance.Layout.h1Color() // self.searchResultsView.isHidden = true // self.searchDomeButton.title = KMLocalizedString("Done") // self.searchDomeButton.toolTip = KMLocalizedString("Done") // self.searchDomeButton.setTitleColor(KMAppearance.Layout.w0Color()) // self.searchDomeButton.wantsLayer = true // self.searchDomeButton.layer?.backgroundColor = KMAppearance.Interactive.a0Color().cgColor // self.searchDomeButton.layer?.cornerRadius = 4.0 // self.searchDomeButton.hidden = YES; // self.searchField.wantsLayer = true // self.searchField.layer.backgroundColor = [KMAppearance KMColor_Layout_L1].CGColor; // self.searchField.layer?.cornerRadius = 1.0 // self.searchField.layer?.borderWidth = 1.0 // self.searchField.layer?.borderColor = KMAppearance.Interactive.a0Color().cgColor // self.searchBox.fillColor = KMAppearance.Interactive.s0Color() // self.searchField.hidden = YES; // self.searchBox.downCallback = { [unowned self] downEntered, box, _ in // if (downEntered) { // self.searchField.isHidden = false // self.searchDomeButton.isHidden = false // self.searchBox.isHidden = true // self.searchField.becomeFirstResponder() // } // } // self.searchBox.isHidden = true 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() } 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() } // var rowIndexes = self.findTableView.selectedRowIndexes // let row = self.findTableView.clickedRow // if (row != -1) { // if rowIndexes.contains(row) == false { // rowIndexes = IndexSet(integer: row) // } // // var selections: [CPDFSelection] = [] // for (i, data) in self.searchResults.enumerated() { // if rowIndexes.contains(i) { // selections.append(data.selection) // } // } // let hideNotes = self.hideNotes() // let allowsNotes = self.allowsNotes() // if hideNotes == false && allowsNotes { // item = menu.addItem(withTitle: KMLocalizedString("Add New Circle"), action: #selector(addAnnotationsForSelections), target: self, tag: CAnnotationType.circle.rawValue) // item?.representedObject = selections // item = menu.addItem(withTitle: KMLocalizedString("Add New Rectangle"), action: #selector(addAnnotationsForSelections), target: self, tag: CAnnotationType.square.rawValue) // item?.representedObject = selections // item = menu.addItem(withTitle: KMLocalizedString("Add New Highlight"), action: #selector(addAnnotationsForSelections), target: self, tag: CAnnotationType.highlight.rawValue) // item?.representedObject = selections // item = menu.addItem(withTitle: KMLocalizedString("Add New Underline"), action: #selector(addAnnotationsForSelections), target: self, tag: CAnnotationType.underline.rawValue) // item?.representedObject = selections // item = menu.addItem(withTitle: KMLocalizedString("Add New Strikethrough"), action: #selector(addAnnotationsForSelections), target: self, tag: CAnnotationType.strikeOut.rawValue) // item?.representedObject = selections // } // } } override func viewDidLoad() { super.viewDidLoad() self.tableView.delegate = self self.tableView.dataSource = self self.tableView.botaDelegate = self // self.tableView.menu?.delegate = self // self.mwcFlags.wholeWordSearch = KMDataManager.ud_integer(forKey: SKWholeWordSearchKey) // self.mwcFlags.caseInsensitiveSearch = KMDataManager.ud_integer(forKey: SKCaseInsensitiveSearchKey) // self.tableView.backgroundColor = KMAppearance.Layout.l0Color() self.tableView.tableColumn(withIdentifier: kPageColumnId)?.headerCell.title = KMLocalizedString("Page") } override func viewDidAppear() { super.viewDidAppear() // self.searchField.becomeFirstResponder() self.updateViewColor() DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged), name: NSApplication.interfaceThemeChangedNotification, object: nil) } 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 updateViewColor() { if (KMAppearance.isDarkMode()) { // self.searchField.layer?.backgroundColor = NSColor(red: 57.0/255.0, green: 60.0/255.0, blue: 62.0/255.0, alpha: 1).cgColor } else { // self.searchField.layer?.backgroundColor = .white } } 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 } // 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 } @objc func themeChanged(_ notification: NSNotification) { DispatchQueue.main.asyncAfter(deadline: .now()+0.3) { self.updateViewColor() } } @IBAction func searchDomeButtonAtion(_ sender: AnyObject) { self.searchField.isHidden = true self.searchDomeButton.isHidden = true self.searchBox.isHidden = false } @objc func toggleWholeWordSearch(_ sender: AnyObject?) { // if self.mwcFlags.wholeWordSearch == 1 { // self.mwcFlags.wholeWordSearch = 0 // } else { // self.mwcFlags.wholeWordSearch = 1 // } // if self.searchField.stringValue.isEmpty == false { // self.search(self.searchField) // } // KMDataManager.ud_set(self.mwcFlags.wholeWordSearch, forKey: SKWholeWordSearchKey) } @objc func toggleCaseInsensitiveSearch(_ sender: AnyObject?) { // if self.mwcFlags.caseInsensitiveSearch == 0 { // self.mwcFlags.caseInsensitiveSearch = 1 // } else { // self.mwcFlags.caseInsensitiveSearch = 0 // } // // if self.searchField.stringValue.isEmpty == false { // self.search(self.searchField) // } // KMDataManager.ud_set(self.mwcFlags.caseInsensitiveSearch, forKey: SKCaseInsensitiveSearchKey) } @objc func goToSelectedFindResults(_ sender: AnyObject?) { // guard let olView = sender as? NSTableView, olView.clickedRow != -1 else { // NSSound.beep() // return // } // self.updateFindResultHighlightsForDirection(.directSelection) } @objc func searchAction(_ sender: NSSearchField) { // if sender.stringValue.isEmpty { // self.applySearchTableHeader("") // } // self.delegate?.searchAction?(searchString: sender.stringValue, isCase: self.mwcFlags.caseInsensitiveSearch == 1) handdler.search(keyword: sender.stringValue, isCase: false, isWholeWord: false) { [weak self] results in self?.searchResults = results ?? [] self?.tableView.reloadData() } } @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 } } } // MARK: - NSTableViewDelegate, NSTableViewDataSource extension KMBotaSearchViewController: NSTableViewDelegate, NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { var datas: [Any] = [] for sectionM in self.handdler.searchResults { 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) { // if self.stopRepeatLoad == true { // // } else { // self.delegate?.controller?(controller: self, listViewSelectionDidChange: notification.object, info: nil) // } // [self updateFindResultHighlightsForDirection:NSDirectSelection]; let row = self.tableView.selectedRow if row >= 0 { let model = handdler.searchResults[row] // let isEditing = self.listView?.isEditing() ?? false // if isEditing { // self.mainViewController?.srHanddler.showSelection(model.selection) // return // } // self.listView?.go(to: model.selection, animated: true) // self.listView?.setHighlightedSelection(model.selection, animated: true) DispatchQueue.main.asyncAfter(deadline: .now()+0.3) { // self.listView?.setHighlightedSelections([model.selection]) // self.listView?.setNeedsDisplayAnnotationViewForVisiblePages() } } } func tableView(_ aTableView: NSTableView, copyRowsWithIndexes rowIndexes: IndexSet) { if IAPProductsManager.default().isAvailableAllFunction() == false { KMPurchaseCompareWindowController.sharedInstance().showWindow(nil) return } var string = "" for idx in rowIndexes { // let match = handdler.searchResults[idx].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) { // self.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 >= self.searchResults.count { return nil } // let model = self.searchResults[rowIndex] // 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: self.pdfDocument(), pageIndex: Int(model.selectionPageIndex), at: point, zoom: self.scaleFactor().cgFloat) return nil } } //MARK: - ComponentGroupDelegate extension KMBotaSearchViewController: ComponentGroupDelegate { func componentGroupDidDismiss(group: ComponentGroup?) { // if group == groupView_ { // removeGroupView() // } else if group == menuGroupView_ { // group?.removeFromSuperview() // menuGroupView_ = nil // } else if group == searchGroupView { // searchGroupView_ = nil searchGroupTarget?.properties.state = .normal searchGroupTarget?.reloadData() searchGroupTarget = nil } } func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) { // if group == groupView_ { // if let selItem = menuItemProperty { // let index = group?.menuItemArr.firstIndex(of: selItem) // if index == 0 { // expandAllComments(item: NSMenuItem()) // } else if index == 1 { // collapseAllComments(item: NSMenuItem()) // } else if index == 2 { // removeAllOutlineItem(item: NSMenuItem()) // } // } // } else 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) // BOTAOutlineView.wholeWords = !value } else if idx == 4 { let key = KMNSearchKey.caseSensitive.botaSearch let value = KMDataManager.ud_bool(forKey: key) KMDataManager.ud_set(!value, forKey: key) // BOTAOutlineView.caseSensitive = !value } } } }