// // KMSearchViewController.swift // PDF Reader Pro // // Created by lxy on 2022/11/17. // import Cocoa @objc protocol KMSearchViewControllerDelegate { @objc optional func searchDoneAction(viewController:KMSearchViewController) @objc optional func searchAction(searchString:String, isCase:Bool) } class CSearchFieldCustomCell : NSSearchFieldCell { required init(coder: NSCoder) { super.init(coder: coder) let cancelCell = self.cancelButtonCell let cancelImage = NSImage(named: "KMImageNameTriBtnClear") cancelImage?.size = NSMakeSize(16, 16) cancelCell?.image = cancelImage cancelCell?.alternateImage = cancelImage let searchCell = self.searchButtonCell let searchImage = NSImage(named: "KMImageNameSearchLeftImage") searchImage?.size = NSMakeSize(16, 16) searchCell?.image = searchImage searchCell?.alternateImage = searchImage } override func resetCancelButtonCell() { super.resetCancelButtonCell() } } class KMSearchViewController: NSViewController { let CPDFOfficeSearchHistoryKey = "CPDFOfficeSearchHistoryKey" @IBOutlet weak var findTipTextField: NSTextField! @IBOutlet weak var resultTextField: NSTextField! @IBOutlet weak var allTipTextField: NSTextField! @IBOutlet weak var lineView: NSView! @IBOutlet weak var searchCotentView: NSView! @IBOutlet weak var searchTextField: FocusAwareSearchTextField! @IBOutlet weak var doneButton: NSButton! @IBOutlet weak var outlineView: KMOutlineView! var isCase = false let searchFieldMenu = NSMenu() @IBOutlet weak var emptyView: NSView! @IBOutlet weak var tipTitleLabel: NSTextField! @IBOutlet weak var searResultLabel: NSTextField! //select 多选 var selectItems: [KMSearchMode] = [] var listView : CPDFListView! var searchResults : [KMSearchMode] = [] var sortResults : [KMSearchMode] = [] var isCancelCell : String = "" var previousSearchString: String = "" var previousCase: Bool = true open weak var delegate: KMSearchViewControllerDelegate? override func viewDidLoad() { super.viewDidLoad() // Do view setup here. self.setup() } override func viewDidAppear() { super.viewDidAppear() NSApplication.shared.mainWindow?.makeFirstResponder(self.searchTextField) } override func viewWillDisappear() { super.viewWillDisappear() self.cancelAllSearchModel() } override func viewWillAppear() { super.viewWillAppear() // self.selectAllSearchModel() self.searchDoneAction() } func setup() { self.view.backgroundColor(NSColor.km_init(hex: "#F7F8FA")) //空状态 self.emptyView.isHidden = true self.searResultLabel.stringValue = NSLocalizedString("No Search Results", comment: "") self.searResultLabel.font = NSFont.SFProTextRegularFont(14.0) self.searResultLabel.textColor = NSColor.km_init(hex: "#616469") self.tipTitleLabel.font = NSFont.SFProTextRegularFont(12.0) self.tipTitleLabel.textColor = NSColor.km_init(hex: "#94989C") let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.32 paragraphStyle.alignment = .center self.tipTitleLabel.attributedStringValue = NSMutableAttributedString(string: NSLocalizedString("Search text can be entered above", comment: ""), attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) self.lineView.backgroundColor(NSColor.km_init(hex: "#EDEEF0")) //搜索 self.searchCotentView.backgroundColor(NSColor.km_init(hex: "#FFFFFF")) self.searchCotentView.border() self.searchTextField.delegate = self self.searchTextField.border(NSColor.km_init(hex: "#FFFFFF"), 1, 0) self.searchTextField.textColor = NSColor.km_init(hex: "#252629") self.searchTextField.font = NSFont.SFProTextRegularFont(14.0) let search = UserDefaults.standard.object(forKey: "CPDFOfficeSearchIgnoreCaseKey") if search == nil { self.isCase = true } else { self.isCase = UserDefaults.standard.bool(forKey: "CPDFOfficeSearchIgnoreCaseKey") } self.previousCase = self.isCase self.findTipTextField.stringValue = NSLocalizedString("Search", comment: "") self.findTipTextField.font = NSFont.SFProTextSemiboldFont(14.0) self.findTipTextField.textColor = NSColor.km_init(hex: "#252629") self.doneButton.isHidden = true self.doneButton.title = NSLocalizedString("Done", comment: "") self.doneButton.backgroundColor(NSColor.km_init(hex: "#1770F4")) self.doneButton.font = NSFont.SFProTextRegularFont(12.0) self.doneButton.contentTintColor = NSColor.km_init(hex: "#FFFFFF") self.doneButton.border(NSColor.km_init(hex: "#1770F4"), 0, 4) //显示 self.allTipTextField.stringValue = NSLocalizedString("All", comment: "") self.allTipTextField.font = NSFont.SFProTextSemiboldFont(11.0) self.allTipTextField.textColor = NSColor.km_init(hex: "#252629") self.resultTextField.stringValue = NSLocalizedString("Results", comment: "") + ":" self.resultTextField.font = NSFont.SFProTextSemiboldFont(11.0) self.resultTextField.textColor = NSColor.km_init(hex: "#94989C") self.outlineView.allowsMultipleSelection = true self.outlineView.indentationPerLevel = 0 self.updateSearchMenu() (self.searchTextField.cell! as! NSSearchFieldCell).placeholderString = NSLocalizedString("Search PDF", comment: "") self.reloadData() } private func updateSearchMenu() { searchFieldMenu.removeAllItems() let item = searchFieldMenu.addItem(withTitle: NSLocalizedString("Ignore Case", comment: ""), action: #selector(caseSetAction), target: self) if self.isCase { item?.state = .on } else { item?.state = .off } let searchs : [String] = UserDefaults.standard.object(forKey: CPDFOfficeSearchHistoryKey) as? [String] ?? [] if searchs.count > 0 { searchFieldMenu.addItem(NSMenuItem.separator()) searchFieldMenu.addItem(withTitle: NSLocalizedString("Search History", comment: ""), action: nil, target: self) for search in searchs { searchFieldMenu.addItem(withTitle: search, action: #selector(searchHistoryAction), target: self) } searchFieldMenu.addItem(NSMenuItem.separator()) searchFieldMenu.addItem(withTitle: NSLocalizedString("Clear Search History", comment: ""), action: #selector(clearSearchHistoryAction), target: self) self.searchFieldMenu.font = NSFont.SFProTextRegularFont(13.0) } (self.searchTextField.cell! as! NSSearchFieldCell).searchMenuTemplate = searchFieldMenu // let menus : NSMenu = NSMenu(title: "") // menus.addItem(withTitle: NSLocalizedString("Add Crice", comment: ""), action: #selector(addAnonationStyle), target: self, tag: CAnnotationType.circle.rawValue) // menus.addItem(withTitle: NSLocalizedString("Add Square", comment: ""), action: #selector(addAnonationStyle), target: self, tag: CAnnotationType.square.rawValue) // menus.addItem(withTitle: NSLocalizedString("Add Highlight", comment: ""), action: #selector(addAnonationStyle), target: self, tag: CAnnotationType.highlight.rawValue) // menus.addItem(withTitle: NSLocalizedString("Add Underline", comment: ""), action: #selector(addAnonationStyle), target: self, tag: CAnnotationType.underline.rawValue) // menus.addItem(withTitle: NSLocalizedString("Add Strikeththrough", comment: ""), action: #selector(addAnonationStyle), target: self, tag: CAnnotationType.strikeOut.rawValue) // self.outlineView.menu = menus } func selectedRowIndexs() -> IndexSet { let clickRow = self.outlineView.clickedRow var selectedRowIndexs = self.outlineView.selectedRowIndexes if(clickRow != -1 && !selectedRowIndexs.contains(clickRow)) { selectedRowIndexs = [clickRow] } return selectedRowIndexs } public func reloadData() { self.sortResults = KMSearchMode.sortSearchResult(results: self.searchResults) if self.searchResults.count > 1 { self.resultTextField.stringValue = "Results:\(self.searchResults.count)" } else { self.resultTextField.stringValue = "Result:\(self.searchResults.count)" } self.listView.setHighlightedSelection(nil, animated: true) self.outlineView.reloadData() } } //MARK: Search extension KMSearchViewController: NSSearchFieldDelegate { private func searchDoneAction() { var searchs : [String] = UserDefaults.standard.object(forKey: CPDFOfficeSearchHistoryKey) as? [String] ?? [] let searchString = self.searchTextField.stringValue if searchString != "" && (self.previousSearchString != searchString || self.isCase != self.previousCase) { //缓存搜索词汇是否重复 if searchs.contains(searchString) { searchs.removeObject(searchString) } if searchs.count == 10 { searchs.remove(at: 9) } searchs.insert((self.searchTextField.stringValue), at: 0) UserDefaults.standard.set(searchs, forKey: CPDFOfficeSearchHistoryKey) self.updateSearchMenu() self.delegate?.searchAction?(searchString: self.searchTextField.stringValue,isCase:self.isCase) self.doneButton.isHidden = false for model in self.sortResults { model.select = true } self.outlineView.expandItem(nil, expandChildren: true) self.selectAllSearchModel() self.previousSearchString = searchString self.previousCase = self.isCase } //移除响应 NSApplication.shared.mainWindow?.makeFirstResponder(self) } func selectAllSearchModel() { //需要延时2s 不然不会高亮 // DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { [unowned self] in //高亮所有注释 var selections: [CPDFSelection] = [] for model in self.sortResults { for item in model.datas { item.selection.setColor(NSColor.km_init(hex: "#FFE600").withAlphaComponent(0.5)) selections.append(item.selection) } } self.listView.setHighlightedSelections(selections) self.listView.setNeedsDisplayAnnotationViewForVisiblePages() // } } func cancelAllSearchModel() { self.listView.setHighlightedSelections(nil) self.listView.setHighlightedSelection(nil, animated: true) self.listView.setNeedsDisplayAnnotationViewForVisiblePages() self.outlineView.deselectAll(nil) //清空搜索词汇 self.previousSearchString = "" } } //MARK: Action extension KMSearchViewController { @IBAction func addAnonationStyle(sender: NSMenuItem) { let selectRowIndexs = self.selectedRowIndexs() if selectRowIndexs.count > 0 { var newAnnonations : [CPDFAnnotation] = [] for selectRow in selectRowIndexs { let searchModel = self.outlineView.item(atRow: selectRow) as! KMSearchMode if searchModel.datas.count > 0 { //选了到了1页 for search in searchModel.datas { let selection = search.selection let annotation = self.listView.addAnnotation(with: CAnnotationType(rawValue: sender.tag) ?? CAnnotationType.unkown, selection: selection, page: selection.page, bounds: selection.bounds) self.listView.setNeedsDisplayAnnotationViewFor(selection.page) if annotation != nil { newAnnonations.append(annotation!) } } } else { //选到页码里的条数 let selection = searchModel.selection let annotation = self.listView.addAnnotation(with: CAnnotationType(rawValue: sender.tag) ?? CAnnotationType.unkown, selection: selection, page: selection.page, bounds: selection.bounds) self.listView.setNeedsDisplayAnnotationViewFor(selection.page) if annotation != nil { newAnnonations.append(annotation!) } } } self.listView.updateActiveAnnotations(newAnnonations) } } @IBAction func doneSearchAction(_ sender: Any) { self.searchTextField.stringValue = "" self.searchResults = []; self.reloadData() self.delegate?.searchDoneAction?(viewController: self) self.doneButton.isHidden = true //清空搜索词汇 self.previousSearchString = "" } @IBAction func searchHistoryAction(sender: NSMenuItem) { // self.delegate?.searchAction?(searchString: sender.title, isCase:self.isCase) self.searchTextField.stringValue = sender.title self.searchDoneAction() } @IBAction func caseSetAction(sender:Any) { self.isCase = !self.isCase UserDefaults.standard.set(self.isCase, forKey: "CPDFOfficeSearchIgnoreCaseKey") self.updateSearchMenu() self.searchDoneAction() } @IBAction func clearSearchHistoryAction(sender:Any) { UserDefaults.standard.removeObject(forKey: CPDFOfficeSearchHistoryKey) self.updateSearchMenu() } @IBAction func escButtonAction(_ sender: Any) { self.cancelSelect() } //MARK: 跳转 //跳转到指定位置 若存在多个 则跳转最后一个 func previewToSections() { if outlineView.selectedRowIndexes.count != 0 { var rows: [KMSearchMode] = [] for index in outlineView.selectedRowIndexes { let model: KMSearchMode = outlineView.item(atRow: index) as! KMSearchMode rows.append(model) } self.toSearchModes(rows) } } func toSearchModes(_ searchModes: [KMSearchMode]) { self.selectAllSearchModel() var selections: [CPDFSelection] = [] for model in searchModes { model.selection.setColor(NSColor.km_init(hex: "#FF5C00").withAlphaComponent(0.5)) selections.append(model.selection) } self.toSections(selections) } func toSections(_ selections: [CPDFSelection]) { self.listView.go(to: selections.last, animated: true) // if selections.count == 1 { // self.listView.setHighlightedSelection(selections.first, animated: true) // } else { // self.listView.setHighlightedSelections(selections) // } self.listView.setNeedsDisplayAnnotationViewForVisiblePages() } func didSelectItem(view: KMSearchTableRowView, event: NSEvent) { let rowView: KMSearchTableRowView = view if rowView.model.datas.count == 0 { //当选中一个时 if self.outlineView.selectedRowIndexes.count == 1 || (!event.modifierFlags.contains(NSEvent.ModifierFlags.command) && !event.modifierFlags.contains(NSEvent.ModifierFlags.shift)) { let index = self.outlineView.row(for: rowView) self.outlineView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false) } //原始数据置空 for model in self.selectItems { model.select = false self.outlineView.reloadItem(model) } //获取最新数据 var items: [KMSearchMode] = [] for index in self.outlineView.selectedRowIndexes { let model: KMSearchMode = self.outlineView.item(atRow: index) as! KMSearchMode model.select = true self.outlineView.reloadItem(model) items.append(model) } self.selectItems = items //刷新数据 self.previewToSections() } else { let expanded = outlineView.isItemExpanded(outlineView.item(atRow: outlineView.selectedRow)) if expanded { outlineView.collapseItem(outlineView.item(atRow: outlineView.selectedRow), collapseChildren: true) rowView.model.select = false outlineView.reloadItem(outlineView.item(atRow: outlineView.selectedRow)) } else { outlineView.expandItem(outlineView.item(atRow: outlineView.selectedRow), expandChildren: true) rowView.model.select = true outlineView.reloadItem(outlineView.item(atRow: outlineView.selectedRow)) } } } func cancelSelect() { self.outlineView.deselectAll(nil) for model in self.selectItems { model.select = false self.outlineView.reloadItem(model) } } } // MARK - NSOutlineViewDataSource,NSOutlineViewDelegate extension KMSearchViewController : NSOutlineViewDataSource,NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { let indexModel = item as? KMSearchMode var count = 0 if indexModel == nil { count = self.sortResults.count; } else { count = indexModel?.datas.count ?? 0 } if(count == 0) { //无数据时的图 self.emptyView.isHidden = false } else { self.emptyView.isHidden = true } return count } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { let indexModel = item as? KMSearchMode var child = KMSearchMode() if indexModel == nil { child = self.sortResults[index]; } else { child = indexModel?.datas[index] ?? KMSearchMode() } return child } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { let newitem = item as? KMSearchMode return newitem?.datas.count ?? 0 > 0 } func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item:Any) -> NSView? { let cell : KMSearchCellView = KMSearchCellView.init() let model : KMSearchMode = item as! KMSearchMode cell.model = model return cell } func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? { let rowView = KMSearchTableRowView() rowView.model = (item as! KMSearchMode) rowView.mouseDownCallback = { [unowned self] view, event in self.didSelectItem(view: view, event: event) } rowView.hoverCallback = { [unowned self] mouseEntered, mouseBox in self.outlineView.enumerateAvailableRowViews { view, row in if view is KMSearchTableRowView { (view as? KMSearchTableRowView)?.model.hover = false (view as? KMSearchTableRowView)?.reloadData() } } if mouseEntered { rowView.model.hover = true } else { rowView.model.hover = false } } return rowView } func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { let model : KMSearchMode = item as! KMSearchMode if model.datas.count != 0 { return 40 } else { let string: NSString = model.attributedString.string as NSString let text = string let size = CGSize(width: outlineView.frame.width - 32, height: 1000) let options = NSString.DrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.firstLineHeadIndent = 10.0 paragraphStyle.headIndent = 10.0 paragraphStyle.lineBreakMode = .byCharWrapping paragraphStyle.lineHeightMultiple = 1.32 let estimatedFrame = NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: NSFont.SFProTextRegularFont(14.0), NSAttributedString.Key.paragraphStyle: paragraphStyle], context: nil) return estimatedFrame.size.height + 16 } } // func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool { // return true // } func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { return true } func outlineViewSelectionDidChange(_ notification: Notification) { if self.outlineView.selectedRow == -1 { self.cancelSelect() } } func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool { if let item = item as? KMSearchMode { if !item.select && item.datas.count > 0 { item.select = true outlineView.animator().expandItem(item, expandChildren: true) return false } } return true } func outlineView(_ outlineView: NSOutlineView, shouldCollapseItem item: Any) -> Bool { if let item = item as? KMSearchMode { if item.select && item.datas.count > 0 { item.select = false outlineView.animator().collapseItem(item, collapseChildren: true) return false } } return true } // func outlineView(_ outlineView: NSOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool { // if self.outlineView.selectedRowIndexes.count > 1 { // return false // } // // let indexSet = [self.outlineView.clickedRow] // let indexSetData : Data = NSKeyedArchiver.archivedData(withRootObject: indexSet) as Data // pasteboard.declareTypes([NSPasteboard.PasteboardType(rawValue: "kKMPDFViewOutlineDragDataType")], owner: self) // pasteboard.setData(indexSetData, forType: NSPasteboard.PasteboardType(rawValue: NSPasteboard.PasteboardType.RawValue("kKMPDFViewOutlineDragDataType"))) // return true // } // // func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { // var dragOperation = NSDragOperation.init(rawValue: 0) // if index > 0 { // dragOperation = NSDragOperation.move // } // return dragOperation // } // // func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { // if ((index < 0)) { // return false // } // let outline = item as! CPDFOutline // if outline.parent == nil { // // } else { // // } // // return true // } } //MARK: ControlTextDelegate extension KMSearchViewController: NSTextFieldDelegate { func controlTextDidEndEditing(_ obj: Notification) { let object = obj.object as! NSTextField if object == self.searchTextField { // self.searchDoneAction() // self.sortResults = [] // self.reloadData() } } func controlTextDidChange(_ obj: Notification) { let object = obj.object as! NSTextField if object == self.searchTextField { if self.searchTextField.stringValue == "" { self.doneButton.isHidden = true self.searchResults = [] self.reloadData() self.cancelAllSearchModel() } } } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { switch commandSelector { case #selector(NSResponder.insertNewline(_:)): if let inputView = control as? NSTextField { //当当前TextField按下enter if inputView == searchTextField { self.searchDoneAction() } } return true default: return false } } } extension KMSearchViewController: NSMenuDelegate, NSMenuItemValidation { //MARK: NSMenuItemValidation func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { let action = menuItem.action if (action == #selector(addAnonationStyle)) { let model : KMSearchMode = self.outlineView.item(atRow: self.outlineView.clickedRow) as! KMSearchMode return model.datas.count == 0 } return true } } class FocusAwareSearchTextField: NSSearchField { var onFocus: () -> Void = {} var onUnfocus: () -> Void = {} override func becomeFirstResponder() -> Bool { onFocus() let textView = window?.fieldEditor(true, for: nil) as? NSTextView textView?.insertionPointColor = NSColor.km_init(hex: "#252629") return super.becomeFirstResponder() } override func resignFirstResponder() -> Bool { onUnfocus() return super.resignFirstResponder() } }