// // KMBookMarkViewController.swift // PDF Reader Pro // // Created by lxy on 2022/10/10. // import Cocoa import PDFKit typealias KMBookMarkViewControllerBookMarkDidChange = (_ controller: KMBookMarkViewController, _ bookMarks: [KMBookMarkItem]) -> Void class KMBookMarkViewController: NSViewController, NSTextFieldDelegate { @IBOutlet weak var addBookButton: NSButton! @IBOutlet weak var titleTextField: NSTextField! @IBOutlet var topSeplineView: NSView! @IBOutlet weak var bookTableView: NSTableView! @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! var listView: CPDFListView! let pdfView = PDFView.init() var isLocalEvent: Bool = false //区分外部点击还是内部点击 var selectItems: [KMBookMarkItem] = [] var bookMarkDidChange: KMBookMarkViewControllerBookMarkDidChange? func dealloc() { NotificationCenter.default.removeObserver(self) } deinit { NotificationCenter.default.removeObserver(self) } override func viewDidLoad() { super.viewDidLoad() self.view.wantsLayer = true self.view.layer?.backgroundColor = NSColor(red: 247.0/255.0, green: 248.0/255.0, blue: 250.0/255.0, alpha: 1).cgColor self.topSeplineView.wantsLayer = true self.topSeplineView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.1).cgColor self.bookTableView.backgroundColor = NSColor.km_init(hex: "#F7F8FA") if #available(macOS 11, *) { self.bookTableView.style = NSTableView.Style.plain } self.bookTableView.allowsMultipleSelection = true self.bookTableView.doubleAction = #selector(renameBookAction) // self.bookTableView.selectionHighlightStyle = NSTableView.SelectionHighlightStyle.none; self.refreshUI() self.reloadData() self.initNotification() } func refreshUI() { self.titleTextField.font = NSFont.SFProTextSemiboldFont(14.0) self.titleTextField.textColor = NSColor.km_init(hex: "#252629") self.titleTextField.stringValue = NSLocalizedString("Bookmarks", comment: "") self.addBookButton.toolTip = NSLocalizedString("Add Bookmark", comment: "") self.bigTipLabel.font = NSFont.SFProTextRegularFont(14.0) self.bigTipLabel.textColor = NSColor.km_init(hex: "#616469") self.bigTipLabel.stringValue = NSLocalizedString("No Bookmarks", comment: "") let title = NSLocalizedString("To create a bookmark, please right-click on the selected page and choose \"Add Bookmark\", or click \"Add\" button in the upper right corner.", comment: "") let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.32 paragraphStyle.alignment = .center self.tipLabel.attributedStringValue = NSMutableAttributedString(string: title, attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle, .foregroundColor : NSColor.km_init(hex: "#94989C")]) } func reloadData() { let array = self.listView.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 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 } } func initNotification() { 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) } func removeNotification() { NotificationCenter.default.removeObserver(self) } private func addMenuTitle(view: NSView, event: NSEvent) { let menus : NSMenu = NSMenu(title: "") menus.delegate = self let addItem = self.menuItemWithTitle(title: NSLocalizedString("Rename", comment: ""), action: #selector(renameBookAction)) let addChildItem = self.menuItemWithTitle(title: NSLocalizedString("Change Destination", comment: ""), action: #selector(changeLocationAction)) let addHigherItem = self.menuItemWithTitle(title: NSLocalizedString("Delete", comment: ""), action: #selector(deleteBookAction)) menus.addItem(addItem) menus.addItem(addChildItem) menus.addItem(addHigherItem) let point = view.convert(event.locationInWindow, from: nil) menus.popUp(positioning: nil, at: point, in: view) // self.bookTableView.menu = menus } func menuItemWithTitle(title:String, action:Selector?) -> NSMenuItem { let menuItem = NSMenuItem.init(title: title as String, action: action, keyEquivalent: "") menuItem.target = self return menuItem } //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) { if self.listView.document.bookmark(forPageIndex: UInt(self.listView.currentPageIndex)) == nil { let label = "\(NSLocalizedString("Page", comment:"")) \(self.listView.currentPageIndex + 1)" let bookMark = KMBookMarkItem() bookMark.label = label bookMark.index = UInt(self.listView.currentPageIndex) self.addBookMark(bookMarks: [bookMark]) } else { let bookMark = self.listView.document.bookmark(forPageIndex: UInt(self.listView.currentPageIndex)) self.dataSource.contains { KMBookMarkItem in if KMBookMarkItem.bookMark == bookMark { let index = Int(KMBookMarkItem.index) self.bookTableView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false) self.didSelectItem(row: index, event: NSEvent()) return true } return false } } } @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 = NSLocalizedString("Are you sure you want to set the target location of the selected outline to the current location", comment: "") alter.addButton(withTitle: NSLocalizedString("YES", comment:"")) alter.addButton(withTitle: NSLocalizedString("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(self.listView.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.bookTitle self.renameTextField.delegate = self self.renameTextField.isEditable = true self.renameTextField.becomeFirstResponder() } //MARK: Noti @objc func KMPDFViewCurrentPageDidChangedNotification(notification: NSNotification) { if notification.object is CPDFDocument { let pdfdocument : CPDFDocument = notification.object as! CPDFDocument if pdfdocument.isEqual(self.listView.document) { if !isLocalEvent { var containSelIndex:Bool = false for (index, value) in self.dataSource.enumerated() { if value.bookMark == self.listView.document.bookmark(forPageIndex: UInt(self.listView.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) { [unowned self] in let pdfdocument : CPDFDocument = notification.object as! CPDFDocument if pdfdocument.isEqual(self.listView.document) { self.reloadData() } } } } //MARK: - NSTextFieldDelegate func controlTextDidEndEditing(_ obj: Notification) { if (self.renameTextField.isEqual(obj.object)) { let textField : NSTextField = obj.object as! NSTextField self.renamePDFBook(bookmark: self.renamePDFBook, label: textField.stringValue) self.renameTextField.isEditable = false } } } extension KMBookMarkViewController : NSTableViewDelegate,NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { let count = self.dataSource?.count ?? 0 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.bookTitle.stringValue = item.bookMark.label return cell } return nil } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = KMBookMarkTableRowView() if row < self.dataSource.count { rowView.model = self.dataSource[row] 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 self.bookTableView.rowView(atRow: row, makeIfNecessary: false) != nil { let tempView = self.bookTableView.rowView(atRow: row, makeIfNecessary: false) self.addMenuTitle(view: tempView!, event: event) } } rowView.hoverCallback = { [unowned self] (mouseEntered, mouseBox) in self.bookTableView.enumerateAvailableRowViews { view, row in if view is KMBookMarkTableRowView { (view as? KMBookMarkTableRowView)?.model.hover = false (view as? KMBookMarkTableRowView)?.reloadData() } } if mouseEntered { rowView.model.hover = true } else { rowView.model.hover = false } } } return rowView } 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) { 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() { // self.addBookButton.isEnabled = self.canAddBorkMark() } func canAddBorkMark() -> Bool { if self.listView.document.bookmarks() != nil && self.listView.document.bookmarks()?.count != 0 { for bookMark in self.listView.document.bookmarks() { if bookMark.pageIndex == self.listView.currentPageIndex { return false } } } return true } } //MARK: undoRedo extension KMBookMarkViewController { func changeLocation(oldBookMark: KMBookMarkItem, newBookMark: KMBookMarkItem) { self.listView.document.removeBookmark(forPageIndex: oldBookMark.index) self.listView.document.addBookmark(newBookMark.label, forPageIndex: newBookMark.index) self.reloadData() self.listView.setNeedsDisplayForVisiblePages() self.listView.undoManager?.registerUndo(withTarget: self) { [unowned self] targetType in self.changeLocation(oldBookMark: newBookMark, newBookMark: oldBookMark) } } 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) self.listView.undoManager?.registerUndo(withTarget: self) { [unowned self] targetType in bookmark.bookMark.label = label self.renamePDFBook(bookmark: bookmark, label: temp ?? bookmark.label) } } func deleteBookMark(bookMarks: [KMBookMarkItem]) { for bookMark in bookMarks { if self.listView.document.removeBookmark(forPageIndex: bookMark.index) { KMPrint("删除标签成功") } } self.listView.setNeedsDisplayForVisiblePages() self.reloadData() //undo redo var saveBooks:[KMBookMarkItem] = bookMarks self.listView.undoManager?.registerUndo(withTarget: self) { [unowned self] targetType in saveBooks.sort(){$0.index > $1.index} self.addBookMark(bookMarks: bookMarks) } guard let callBack = bookMarkDidChange else { return } callBack(self, bookMarks) } func addBookMark(bookMarks: [KMBookMarkItem]) { for bookMark in bookMarks { self.listView.document.addBookmark(bookMark.label, forPageIndex: UInt(bookMark.index)) } self.listView.setNeedsDisplayForVisiblePages() self.reloadData() if bookMarks.count == 1 { DispatchQueue.main.async { if self.listView.document.bookmark(forPageIndex: UInt(bookMarks.first!.index)) != nil { let item = KMBookMarkItem() item.bookMark = self.listView.document.bookmark(forPageIndex: UInt(bookMarks.first!.index)) item.label = item.bookMark.label item.index = UInt(item.bookMark.pageIndex) self.addBookMarkAndEdit(newBookMark: item) } } } //undo redo var saveBooks:[KMBookMarkItem] = bookMarks self.listView.undoManager?.registerUndo(withTarget: self) { [unowned self] targetType in saveBooks.sort(){$0.index > $1.index} self.deleteBookMark(bookMarks: saveBooks) } guard let callBack = bookMarkDidChange else { return } callBack(self, bookMarks) } @IBAction func undo(_ sender: Any) { if (self.listView.undoManager?.canUndo ?? false) { self.listView.undoManager?.undo() } } @IBAction func redo(_ sender: Any) { if (self.listView.undoManager?.canRedo ?? false) { self.listView.undoManager?.redo() } } } extension KMBookMarkViewController: NSMenuDelegate, NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { let action = menuItem.action if (action == #selector(undo)) { return self.listView.undoManager?.canUndo ?? false } if (action == #selector(redo)) { return self.listView.undoManager?.canRedo ?? false } 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) { // if self.listView.document.bookmark(forPageIndex: UInt(self.listView.currentPageIndex)) != nil { // return false // } } } else { return false } } } return true } } class KMBookMarkItem: NSObject { var label: String = "" var index: UInt = 0 var bookMark: CPDFBookmark = CPDFBookmark() var select: Bool = false var hover: Bool = false }