// // KMBookMarkViewController.swift // PDF Reader Pro // // Created by lxy on 2022/10/10. // import Cocoa import KMComponentLibrary typealias KMBookMarkViewControllerBookMarkDidChange = (_ controller: KMBookMarkViewController, _ bookMarks: [KMBookMarkItem]) -> Void @objc protocol KMBookMarkViewControllerDelegate: NSObjectProtocol { @objc optional func bkControllerAddAction(controller: KMBookMarkViewController, bookmark: CPDFBookmark?, info: [String : Any]?) } class KMBookMarkViewController: KMNBaseViewController { @IBOutlet weak var addBookButton: NSButton! @IBOutlet weak var titleTextField: NSTextField! @IBOutlet weak var sortButton: NSButton! @IBOutlet var topSeplineView: NSView! @IBOutlet weak var bookTableView: KMBotaTableView! @IBOutlet weak var emptyView: NSView! @IBOutlet weak var bigTipLabel: NSTextField! @IBOutlet weak var tipLabel: NSTextField! var dataSource: [KMBookMarkItem] = [] var renameTextField: NSTextField? var renamePDFBook: KMBookMarkItem? var renameCellView: KMBookCellView? weak var document: CPDFDocument? var isLocalEvent: Bool = false //区分外部点击还是内部点击 var selectItems: [KMBookMarkItem] = [] var bookMarkDidChange: KMBookMarkViewControllerBookMarkDidChange? weak var delegate: KMBookMarkViewControllerDelegate? private lazy var handdler_ = KMNBookmarkHanddler() // 升序 private var sortType_: KMSortMode = .ascending private lazy var addButton_: ComponentButton = { let view = ComponentButton() view.properties = ComponentButtonProperty(type: .text_gray, size: .xxs, onlyIcon: true, icon: NSImage(named: "KMBookmarkAdd")) return view }() private var emptyView_: ComponentEmpty = { let view = ComponentEmpty() view.properties = ComponentEmptyProperty(emptyType: .noBookmark, state: .normal, image: NSImage(named: "KMBookmarkEmpty"), text: KMLocalizedString("No Bookmark"), subText: KMLocalizedString("Here is the description.")) return view }() var handdler: KMNBookmarkHanddler { get { return handdler_ } } private var groupView: ComponentGroup? = ComponentGroup.createFromNib(in: ComponentLibrary.shared.componentBundle()) convenience init() { self.init(nibName: "KMBookMarkViewController", bundle: nil) } override func viewDidLoad() { super.viewDidLoad() handdler.delegate = self titleTextField.font = ComponentLibrary.shared.getFontFromKey("mac/body-m-bold") addBookButton.addSubview(addButton_) addButton_.frame = addBookButton.bounds addButton_.autoresizingMask = [.width, .height] addButton_.setTarget(self, action: #selector(addBookmarkAction)) bookTableView.style = NSTableView.Style.plain bookTableView.allowsMultipleSelection = true bookTableView.doubleAction = #selector(renameBookAction) bookTableView.hasImageToolTips = true bookTableView.botaDelegate = self sortButton.image = NSImage(named: "KMImageNameBotaBookmarkSortIcon") sortButton.target = self sortButton.action = #selector(sortAction) emptyView.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.reloadData() } override func updateUILanguage() { super.updateUILanguage() KMMainThreadExecute { self.titleTextField.stringValue = KMLocalizedString("Bookmarks") } } override func updateUIThemeColor() { super.updateUIThemeColor() KMMainThreadExecute { self.view.wantsLayer = true let color = ComponentLibrary.shared.getComponentColorFromKey("colorBg/layout-middle") self.view.layer?.backgroundColor = color.cgColor self.topSeplineView.wantsLayer = true self.topSeplineView.layer?.backgroundColor = ComponentLibrary.shared.getComponentColorFromKey("colorBorder/divider").cgColor self.titleTextField.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/2") } } func reloadData() { let array = document?.bookmarks() ?? [CPDFBookmark]() var bookMarks: [KMBookMarkItem] = [] for bookMark in array { let item = KMBookMarkItem() item.bookMark = bookMark item.index = UInt(bookMark.pageIndex) item.label = bookMark.label bookMarks.append(item) } self.dataSource = bookMarks if self.sortType_ == .descending { self.dataSource.sort(){$0.bookMark.pageIndex >= $1.bookMark.pageIndex} } else { self.dataSource.sort(){$0.bookMark.pageIndex < $1.bookMark.pageIndex} } self.bookTableView.reloadData() self.updateAddBookMarkState() } func addBookMarkAndEdit(newBookMark: KMBookMarkItem) { _ = self.dataSource.contains { KMBookMarkItem in if KMBookMarkItem.bookMark == newBookMark.bookMark { let index = KMOCToolClass.arrayIndexOf(array: self.dataSource, item: KMBookMarkItem) ?? 0 self.didSelectItem(row: index, event: NSEvent()) self.renameBookWithRow(row: index) return true } return false } } override func addNotifations() { super.addNotifations() NotificationCenter.default.addObserver(self, selector: #selector(KMPDFViewCurrentPageDidChangedNotification), name: NSNotification.Name.init(rawValue: "KMPDFViewCurrentPageDidChanged"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(documentPageCountChangedNotification), name: NSNotification.Name.init(rawValue: "CPDFDocumentPageCountChangedNotification"), object: nil) } //MARK: - Menu Action @objc func renameBookAction() { if self.bookTableView.selectedRowIndexes.count == 1 { self.renameBookWithRow(row: self.bookTableView.selectedRowIndexes.first!) } else { __NSBeep() } } @IBAction func escButtonAction(_ sender: Any) { self.bookTableView.deselectAll(nil) } @IBAction func addBookmarkAction(_ sender: Any) { let currentPageIndex = handdler.currentPageIndex if let data = handdler.bookmark(for: currentPageIndex) { delegate?.bkControllerAddAction?(controller: self, bookmark: data, info: ["result" : false]) return } handdler.addCurrentBookmark(callback: { [unowned self] bookmark in self.delegate?.bkControllerAddAction?(controller: self, bookmark: bookmark, info: nil) self.reloadData() }) } @objc func changeLocationAction() { if self.bookTableView.selectedRowIndexes.count == 1 { let item = self.dataSource[self.bookTableView.selectedRowIndexes.first!] let alter = NSAlert() alter.alertStyle = NSAlert.Style.informational alter.messageText = KMLocalizedString("Are you sure you want to set the selected page as the bookmark location?", comment: "") alter.addButton(withTitle: KMLocalizedString("Yes", comment:"")) alter.addButton(withTitle: KMLocalizedString("No", comment:"")) let modlres = alter.runModal() if modlres == NSApplication.ModalResponse.alertFirstButtonReturn { let bookMark = KMBookMarkItem() bookMark.bookMark = item.bookMark bookMark.label = item.label bookMark.index = UInt(handdler.currentPageIndex) self.changeLocation(oldBookMark: item, newBookMark: bookMark) } } else { __NSBeep() } } @objc func deleteBookAction() { if self.bookTableView.selectedRowIndexes.count != 0 { var bookMarks:[KMBookMarkItem] = [] for index in self.bookTableView.selectedRowIndexes { let item = self.dataSource[index] bookMarks.append(item) } self.deleteBookMark(bookMarks: bookMarks) } else { __NSBeep() } } private func renameBookWithRow(row: Int) { self.renamePDFBook = self.dataSource[row] self.renameCellView = self.bookTableView.view(atColumn: 0, row: row, makeIfNecessary: true) as? KMBookCellView self.renameTextField = self.renameCellView?.inputTF self.renameTextField?.delegate = self self.renameTextField?.isEditable = true self.renameTextField?.becomeFirstResponder() } @objc func sortAction(_ sender: NSButton) { let type = self.sortType_ if type == .ascending { self.sortType_ = .descending } else { self.sortType_ = .ascending } self.reloadData() } //MARK: - Noti @objc func KMPDFViewCurrentPageDidChangedNotification(notification: NSNotification) { if notification.object is CPDFDocument { let pdfdocument : CPDFDocument = notification.object as! CPDFDocument if pdfdocument.isEqual(document) { if !isLocalEvent { var containSelIndex:Bool = false for (index, value) in self.dataSource.enumerated() { if value.bookMark == document?.bookmark(forPageIndex: UInt(handdler.currentPageIndex)) { containSelIndex = true self.didSelectItem(row: index, event: NSEvent()) break } } if !containSelIndex { self.cancelSelect() } } isLocalEvent = false } self.updateAddBookMarkState() } } @objc func documentPageCountChangedNotification(notification: NSNotification) { if notification.object is CPDFDocument { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { [weak self] in let pdfdocument : CPDFDocument = notification.object as! CPDFDocument if pdfdocument.isEqual(self?.document) { self?.reloadData() } } } } } // MARK: - NSTextFieldDelegate extension KMBookMarkViewController: NSTextFieldDelegate { func controlTextDidEndEditing(_ obj: Notification) { if (self.renameTextField!.isEqual(obj.object)) { let textField : NSTextField = obj.object as! NSTextField handdler.rename(bookmark: renamePDFBook!.bookMark, label: textField.stringValue) self.renameTextField?.isEditable = false } } } // MARK: - NSTableViewDelegate,NSTableViewDataSource extension KMBookMarkViewController : NSTableViewDelegate,NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { let count = self.dataSource.count if count == 0 { self.emptyView.isHidden = false } else { self.emptyView.isHidden = true } return count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if row < self.dataSource.count { let item: KMBookMarkItem = self.dataSource[row] let cell : KMBookCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "KMBookCellView"), owner: self) as! KMBookCellView cell.inputTF.font = ComponentLibrary.shared.getFontFromKey("mac/body-s-regular") cell.inputTF.textColor = ComponentLibrary.shared.getComponentColorFromKey("colorText/1") cell.inputTF.stringValue = item.bookMark.label cell.bookTitle.stringValue = "\(item.bookMark.pageIndex + 1)" cell.inputBox.borderWidth = 0 if let data = item.bookMark.date { cell.dateLabel.stringValue = KMTools.timeString(timeDate: data) } else { cell.dateLabel.stringValue = "" } cell.textFieldDidEndEditingCallback = { [weak self] textF in self?.renameCellView = cell self?.handdler.rename(bookmark: item.bookMark, label: textF.stringValue) } return cell } return nil } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = KMBookMarkTableRowView() rowView.selectionHighlightStyle = .none if row < self.dataSource.count { rowView.model = self.dataSource[row] rowView.menuClickedAction = { [weak self] point in let idxs = self?.bookTableView.selectedRowIndexes.count ?? 0 let tempView = self?.bookTableView.rowView(atRow: row, makeIfNecessary: false) var viewHeight: CGFloat = 0 let items: [String] = ["Delete", "Add", "Rename", "Delete All"] 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 } if idxs > 1 { (menuItemArr.safe_element(for: 1) as? ComponentMenuitemProperty)?.isDisabled = true (menuItemArr.safe_element(for: 2) as? ComponentMenuitemProperty)?.isDisabled = true } if self?.groupView != nil { self?.groupView?.clickedAutoHide = false self?.groupView?.groupDelegate = self self?.groupView?.frame = CGRectMake(0, 0, 180, viewHeight) self?.groupView?.updateGroupInfo(menuItemArr) self?.groupView?.showWithPoint(CGPoint(x: point.x, y: point.y - viewHeight), relativeTo: tempView) } return NSMenu() } rowView.mouseDownAction = { [unowned self] (view, event) in self.didSelectItem(row: row, event: event) } rowView.rightMouseDownAction = { [unowned self] (view, event) in if !KMOCToolClass.arrayContains(array: self.selectItems, annotation: rowView.model) || self.selectItems.count == 1 { self.selectIndex(index: row) } if row < 0 || row >= self.dataSource.count { return } } rowView.hoverCallback = { [unowned self] (mouseEntered, mouseBox) in if let value = ComponentLibrary.shared.getComponentValueFromKey("radius/xs") { let currentValue = value as? CGFloat ?? 0 rowView.box?.cornerRadius = currentValue } self.bookTableView.enumerateAvailableRowViews { view, row in if let data = view as? KMBookMarkTableRowView { data.model.hover = false data.reloadData() } } if mouseEntered { rowView.model.hover = true rowView.reloadData() } else { rowView.model.hover = false rowView.reloadData() } } } return rowView } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 36 } func tableView(_ tableView: NSTableView, shouldSelect tableColumn: NSTableColumn?) -> Bool { self.isLocalEvent = true return true } func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { self.isLocalEvent = true return true } func tableViewSelectionDidChange(_ notification: Notification) { // if self.bookTableView.selectedRow == -1 { // self.cancelSelect() // } } func selectIndex(index: Int) { if index < 0 || index >= self.dataSource.count { return } self.bookTableView.selectRowIndexes(IndexSet(integer: IndexSet.Element(index)), byExtendingSelection: false) self.didSelectItem(row: index, event: NSEvent(), needJump: false) } func didSelectItem(row: Int, event: NSEvent?, needJump: Bool = true) { //当选中一个时 if self.bookTableView.selectedRowIndexes.count == 1 || (event != nil && (!event!.modifierFlags.contains(NSEvent.ModifierFlags.command) && !event!.modifierFlags.contains(NSEvent.ModifierFlags.shift))) { self.bookTableView.selectRowIndexes(IndexSet(integer: IndexSet.Element(row)), byExtendingSelection: false) } //原始数据置空 for model in self.selectItems { self.dataSource.contains { KMBookMarkItem in if KMBookMarkItem.bookMark == model.bookMark { let index = KMOCToolClass.arrayIndexOf(array: self.dataSource, item: KMBookMarkItem) ?? 0 if index != nil { KMBookMarkItem.select = false if self.bookTableView.rowView(atRow: index, makeIfNecessary: false) != nil { let rowView: KMBookMarkTableRowView = self.bookTableView.rowView(atRow: index, makeIfNecessary: false) as! KMBookMarkTableRowView rowView.reloadData() } } return true } return false } } //获取最新数据 var items: [KMBookMarkItem] = [] for index in self.bookTableView.selectedRowIndexes { if index < self.dataSource.count { let model: KMBookMarkItem = self.dataSource[index] model.select = true if self.bookTableView.rowView(atRow: index, makeIfNecessary: false) != nil { let rowView: KMBookMarkTableRowView = self.bookTableView.rowView(atRow: index, makeIfNecessary: false) as! KMBookMarkTableRowView rowView.reloadData() } items.append(model) } } self.selectItems = items //刷新数据 if needJump { self.updateListViewData() } } func updateListViewData() { if self.bookTableView.selectedRowIndexes.count == 1 && self.bookTableView.selectedRowIndexes.first! < self.dataSource.count { let index = self.bookTableView.selectedRowIndexes.first let selectBookMark = self.dataSource[index!] // self.listView.go(toPageIndex: selectBookMark.bookMark.pageIndex, animated: true) } } func cancelSelect() { self.bookTableView.deselectAll(nil) for model in self.selectItems { model.select = false let index = self.dataSource.firstIndex(of: model) if index != nil { if (self.bookTableView.rowView(atRow: index!, makeIfNecessary: false) != nil) { let rowView: KMBookMarkTableRowView = self.bookTableView.rowView(atRow: index!, makeIfNecessary: false) as! KMBookMarkTableRowView rowView.reloadData() } } } } func updateAddBookMarkState() { addButton_.properties.isDisabled = !canAddBorkMark() addButton_.reloadData() } func canAddBorkMark() -> Bool { if document?.bookmarks() != nil && document?.bookmarks()?.count != 0 { for bookMark in document?.bookmarks() ?? [] { if bookMark.pageIndex == handdler.currentPageIndex { return false } } } return true } } // MARK: - KMBotaTableViewDelegate extension KMBookMarkViewController: KMBotaTableViewDelegate { func tableView(_ aTableView: NSTableView, imageContextForRow rowIndex: Int) -> AnyObject? { if aTableView.isEqual(to: self.bookTableView) { let cnt = self.dataSource.count if rowIndex >= cnt { return nil } let model = self.dataSource[rowIndex] return model.bookMark } return nil } } //MARK: - undoRedo extension KMBookMarkViewController { func changeLocation(oldBookMark: KMBookMarkItem, newBookMark: KMBookMarkItem) { document?.removeBookmark(forPageIndex: oldBookMark.index) document?.addBookmark(newBookMark.label, forPageIndex: newBookMark.index) reloadData() } func renamePDFBook(bookmark : KMBookMarkItem! , label:String) { if bookmark.bookMark.label == label { return } let temp = bookmark.bookMark.label bookmark.bookMark.label = label self.reloadData() var indexSet = IndexSet() indexSet.insert(self.bookTableView.row(for: self.renameCellView!)) self.bookTableView.selectRowIndexes(indexSet, byExtendingSelection: false) } func deleteBookMark(bookMarks: [KMBookMarkItem]) { for bookMark in bookMarks { if ((document?.removeBookmark(forPageIndex: bookMark.index)) != nil) { KMPrint("删除标签成功") } } self.reloadData() guard let callBack = bookMarkDidChange else { return } callBack(self, bookMarks) } func addBookMark(bookMarks: [KMBookMarkItem]) { for bookMark in bookMarks { document?.addBookmark(bookMark.label, forPageIndex: UInt(bookMark.index)) } self.reloadData() if bookMarks.count == 1 { DispatchQueue.main.async { [self] in if document?.bookmark(forPageIndex: UInt(bookMarks.first!.index)) != nil { let item = KMBookMarkItem() item.bookMark = (document?.bookmark(forPageIndex: UInt(bookMarks.first!.index)))! item.label = item.bookMark.label item.index = UInt(item.bookMark.pageIndex) self.addBookMarkAndEdit(newBookMark: item) } } } guard let callBack = bookMarkDidChange else { return } callBack(self, bookMarks) } @IBAction func undo(_ sender: Any) { handdler.undo() } @IBAction func redo(_ sender: Any) { handdler.redo() } } // MARK: - NSMenuDelegate, NSMenuItemValidation extension KMBookMarkViewController: NSMenuDelegate, NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { let action = menuItem.action if (action == #selector(undo)) { return handdler.canUndo() } if (action == #selector(redo)) { return handdler.canRedo() } if action == #selector(renameBookAction) || action == #selector(changeLocationAction) || action == #selector(deleteBookAction) { if self.bookTableView.selectedRowIndexes.count > 1 { if action == #selector(changeLocationAction) { return false } else if action == #selector(renameBookAction) { return false } } else if self.bookTableView.selectedRowIndexes.count == 1 { return true } else { if self.bookTableView.selectedRowIndexes.count == 0 { if action == #selector(changeLocationAction) {} } else { return false } } } return true } } extension KMBookMarkViewController: KMNBookmarkHanddlerDelegate { func handdler(_ handdler: KMNBookmarkHanddler, didAdd bookmark: CPDFBookmark?, info: [String : Any]?) { KMMainThreadExecute { self.reloadData() } } func handdler(_ handdler: KMNBookmarkHanddler, didRemove bookmark: CPDFBookmark?, info: [String : Any]?) { KMMainThreadExecute { self.reloadData() } } func handdler(_ handdler: KMNBookmarkHanddler, didRemoveAll info: [String : Any]?) { KMMainThreadExecute { self.reloadData() } } func handdler(_ handdler: KMNBookmarkHanddler, didRename bookmark: CPDFBookmark?, info: [String : Any]?) { KMMainThreadExecute { self.reloadData() } } } extension KMBookMarkViewController: ComponentGroupDelegate { func componentGroupDidSelect(group: ComponentGroup?, menuItemProperty: ComponentMenuitemProperty?) { if let selItem = menuItemProperty { let index = group?.menuItemArr.firstIndex(of: selItem) if index == 0 { group?.removeFromSuperview() var pageIndexs = IndexSet() for i in bookTableView.selectedRowIndexes { if let item = dataSource.safe_element(for: i) as? KMBookMarkItem { pageIndexs.insert(item.bookMark.pageIndex) } } _ = handdler.removeBookmarks(for: pageIndexs) } else if index == 1 { } else if index == 2 { group?.removeFromSuperview() renameBookAction() } else if index == 3 { Task { let resp = await KMAlertTool.runModel(message: KMLocalizedString("此操作将删除文档中的所有书签,是否继续?"), buttons: [KMLocalizedString("OK"), KMLocalizedString("No")]) if resp == .alertFirstButtonReturn { _ = handdler.removeAllBookmarks() } } } } } func componentGroupDidDismiss(group: ComponentGroup?) { self.groupView?.removeFromSuperview() } } class KMBookMarkItem: NSObject { var label: String = "" var index: UInt = 0 var bookMark: CPDFBookmark = CPDFBookmark() var select: Bool = false var hover: Bool = false }