// // KMBookmarkController.swift // PDF Reader Pro // // Created by lizhe on 2024/2/5. // import Cocoa private let kLabelIdentifier = NSUserInterfaceItemIdentifier("label") private let kFileIdentifier = NSUserInterfaceItemIdentifier("file") private let kPageIdentifier = NSUserInterfaceItemIdentifier("page") let kTextWithIconStringKey = "string"; let kTextWithIconImageKey = "image"; let kBookmarksToolbarIdentifier = "BookmarksToolbarIdentifier" let kBookmarksNewFolderToolbarItemIdentifier = "BookmarksNewFolderToolbarItemIdentifier" let kBookmarksNewSeparatorToolbarItemIdentifier = "BookmarksNewSeparatorToolbarItemIdentifier" let kBookmarksDeleteToolbarItemIdentifier = "BookmarksDeleteToolbarItemIdentifier" let kPasteboardTypeBookmarkRows = NSPasteboard.PasteboardType(rawValue: "pasteboard.bookmarkrows") class KMBookmarkController: NSWindowController { @IBOutlet weak var bookmarkOutlineView: KMBookmarkOutlineView! @IBOutlet weak var outlineView: KMCustomOutlineView! var previousSession: KMBookmark? var draggedBookmarks: [KMBookmark] = [] var recentDocuments: [[String: Any]] { get { return KMBookmarkManager.manager.recentDocuments } set { } } var bookmarkRoot: KMRootBookmark { get { if let lastOpenFiles = UserDefaults.standard.array(forKey: SKLastOpenFileNamesKey), !lastOpenFiles.isEmpty { previousSession = KMSessionBookmark.bookmarkSession(setups: lastOpenFiles as NSArray, label: NSLocalizedString("Restore Previous Session", comment: "Menu item title")) } return KMBookmarkManager.manager.rootBookmark } set { } } var toolbarItems: [String: NSToolbarItem] = [:] override func windowDidLoad() { super.windowDidLoad() self.window?.title = NSLocalizedString("Bookmarks", comment: ""); setupToolbar() bookmarkOutlineView.outlineView.menu = NSMenu() bookmarkOutlineView.outlineView.menu?.delegate = self bookmarkOutlineView.data = self.bookmarkRoot bookmarkOutlineView.doubleClickAction = { [unowned self] view in self.doubleClickBookmark(nil) } } func updateStatus() { let row = outlineView.selectedRow var message = "" if row != -1 { if let bookmark = outlineView.item(atRow: row) as? KMBookmark { switch bookmark.bookmarkType { case .bookmark: message = bookmark.fileURL?.path ?? "" case .folder: let count = bookmark.children.count message = count == 1 ? NSLocalizedString("1 item", comment: "Bookmark folder description") : String(format: NSLocalizedString("%ld items", comment: "Bookmark folder description"), count) default: break } } } // statusBar.leftStringValue = message } // static func showBookmarkController() -> KMBookmarkController { let controller = KMBookmarkController.init(windowNibName: "KMBookmarkController") NSWindow.currentWindow().addChildWindow(controller.window!, ordered: NSWindow.OrderingMode.above) controller.window?.center() return controller } // // // //MARK: Recent Documents func recentDocumentInfo(at fileURL: URL) -> [String: Any]? { let path = fileURL.path for info in recentDocuments { // if let aliasData = info[ALIASDATA_KEY] as? Data, // let alias = SKAlias(aliasData), // alias.fileURLNoUI?.path.caseInsensitiveCompare(path) == .orderedSame { // return info // } } return nil } // func addRecentDocument(for fileURL: URL, pageIndex: UInt, scaleFactor factor: CGFloat, snapshots setups: [Any]?) { // if let info = recentDocumentInfo(at: fileURL) { // recentDocuments.removeObject(info) // } // // if let alias = SKAlias(url: fileURL) { // var bm: [String: Any] = [ // PAGEINDEX_KEY: pageIndex, // SCALE_KEY: factor, // ALIASDATA_KEY: alias.data, // ALIAS_KEY: alias, // SNAPSHOTS_KEY: setups ?? [] // ] // recentDocuments.insert(bm, at: 0) // if recentDocuments.count > maxRecentDocumentsCount { // recentDocuments.removeLastObject() // } // } } // // func pageIndex(forRecentDocumentAt fileURL: URL) -> UInt { // guard let fileURL = fileURL else { return UInt.max } // if let pageIndex = recentDocumentInfo(at: fileURL)?[PAGEINDEX_KEY] as? UInt { // return pageIndex // } // return UInt.max // } // // func scaleFactor(forRecentDocumentAt fileURL: URL) -> CGFloat { // guard let fileURL = fileURL else { return 0 } // if let scaleFactor = recentDocumentInfo(at: fileURL)?[SCALE_KEY] as? CGFloat { // return scaleFactor // } // return 0 // } // // func snapshots(forRecentDocumentAt fileURL: URL) -> [Any]? { // guard let fileURL = fileURL else { return nil } // if let setups = recentDocumentInfo(at: fileURL)?[SNAPSHOTS_KEY] as? [Any], !setups.isEmpty { // return setups // } // return nil // } // // //MARK: Bookmarks support func getInsertionFolder(_ bookmarkPtr: inout KMBookmark?, childIndex indexPtr: inout Int) { let rowIndex = outlineView.clickedRow var indexes = outlineView.selectedRowIndexes if rowIndex != -1 && !indexes.contains(rowIndex) { indexes = IndexSet(integer: rowIndex) } let rowIdx = indexes.last ?? NSNotFound var item = KMBookmarkManager.manager.rootBookmark var idx = item.children.count if rowIdx != NSNotFound { if let selectedItem = outlineView.item(atRow: rowIdx) as? KMBookmark { if outlineView.isItemExpanded(selectedItem) { item = selectedItem as! KMRootBookmark idx = item.children.count } else if let parent = selectedItem.parent, let itemIdx = parent.children.firstIndex(of: selectedItem) { item = parent as! KMRootBookmark idx = itemIdx + 1 } } } bookmarkPtr = item indexPtr = idx } @IBAction func openBookmark(_ sender: Any) { if let bookmark = (sender as AnyObject).representedObject as? KMBookmark { bookmark.open() } } @IBAction func doubleClickBookmark(_ sender: Any?) { let row = bookmarkOutlineView.outlineView.clickedRow if let bm = (row != -1 ? bookmarkOutlineView.outlineView.item(atRow: row) : nil) as? KMBookmark, [KMBookmarkType.bookmark, .session, .file].contains(bm.bookmarkType) { bm.open() } } @objc func deleteBookmarks(bookmarks: [KMBookmark]) { for item in minimumCoverForBookmarks(bookmarks).reversed() { guard let parent = item.parent, let itemIndex = parent.children.firstIndex(of: item) else { continue } parent.removeObjectFromChildren(index: itemIndex) } bookmarkOutlineView.reloadData() KMBookmarkManager.manager.saveData() } @IBAction func insertBookmarkFolder(_ sender: Any) { let folder = KMFolderBookmark.folderBookmark(label: NSLocalizedString("Folder", comment: "default folder name")) var item: KMBookmark? var idx: Int = 0 getInsertionFolder(&item, childIndex: &idx) item?.insert(child: folder, atIndex: idx) bookmarkOutlineView.outlineView.reloadData() let row = bookmarkOutlineView.outlineView.row(forItem: folder) if row > 0 { bookmarkOutlineView.outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) bookmarkOutlineView.outlineView.editColumn(0, row: row, with: nil, select: true) } } @IBAction func insertBookmarkSeparator(_ sender: Any) { let separator = KMSeparatorBookmark() var item: KMBookmark? var idx: Int = 0 getInsertionFolder(&item, childIndex: &idx) item?.insert(child: separator, atIndex: idx) bookmarkOutlineView.outlineView.reloadData() // let row = outlineView.row(forItem: separator) // outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) } @IBAction func addBookmark(_ sender: Any) { let openPanel = NSOpenPanel() var types = [String]() for docClass in NSDocumentController.shared.documentClassNames { if let docClass = NSClassFromString(docClass) as? NSDocument.Type { types += docClass.readableTypes } } openPanel.allowsMultipleSelection = true openPanel.canChooseDirectories = true openPanel.allowedFileTypes = types openPanel.beginSheetModal(for: self.window!) { (result) in guard result == .OK else { return } let newBookmarks = KMBookmark.bookmarks(urls: openPanel.urls) if newBookmarks != nil { var item: KMBookmark? var index: Int = 0 self.getInsertionFolder(&item, childIndex: &index) var indexes = IndexSet(integersIn: Int(index).. 0 { bookmarkOutlineView.outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) QLPreviewPanel.shared().makeKeyAndOrderFront(nil) } } } // // avoid rebuilding the bookmarks menu on every key event func menuHasKeyEquivalent(_ menu: NSMenu, for event: NSEvent, target: AutoreleasingUnsafeMutablePointer?, action: UnsafeMutablePointer?) -> Bool { return false } // MARK: - Toolbar func setupToolbar() { // Create a new toolbar instance, and attach it to our document window let toolbar = NSToolbar(identifier: kBookmarksToolbarIdentifier) var dict = [String: NSToolbarItem]() // Set up toolbar properties: Allow customization, give a default display mode, and remember state in user defaults toolbar.allowsUserCustomization = true toolbar.autosavesConfiguration = true toolbar.displayMode = .default // We are the delegate toolbar.delegate = self // Add template toolbar items var item = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(kBookmarksNewFolderToolbarItemIdentifier)) item.label = NSLocalizedString("New Folder", comment: "Toolbar item label") item.paletteLabel = NSLocalizedString("New Folder", comment: "Toolbar item label") item.toolTip = NSLocalizedString("Add a New Folder", comment: "Tool tip message") // item.image = NSImage(named: "NewFolder") item.image = NSImage(named: NSImage.folderName)! item.target = self item.action = #selector(insertBookmarkFolder(_:)) dict[kBookmarksNewFolderToolbarItemIdentifier] = item item = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(kBookmarksNewSeparatorToolbarItemIdentifier)) item.label = NSLocalizedString("New Separator", comment: "Toolbar item label") item.paletteLabel = NSLocalizedString("New Separator", comment: "Toolbar item label") item.toolTip = NSLocalizedString("Add a New Separator", comment: "Tool tip message") // item.image = NSImage(named: "NewSeparator") item.image = NSImage(named: NSImage.shareTemplateName)! item.target = self item.action = #selector(insertBookmarkSeparator(_:)) dict[kBookmarksNewSeparatorToolbarItemIdentifier] = item item = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(kBookmarksDeleteToolbarItemIdentifier)) item.label = NSLocalizedString("Delete", comment: "Toolbar item label") item.paletteLabel = NSLocalizedString("Delete", comment: "Toolbar item label") item.toolTip = NSLocalizedString("Delete Selected Items", comment: "Tool tip message") item.image = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kToolbarDeleteIcon))) item.target = self item.action = #selector(deleteBookmark(_:)) dict[kBookmarksDeleteToolbarItemIdentifier] = item toolbarItems = dict // Attach the toolbar to the window self.window?.toolbar = toolbar } // // // MARK: - Quick Look Panel Support // // func acceptsPreviewPanelControl(_ panel: QLPreviewPanel) -> Bool { // return true // } // // func beginPreviewPanelControl(_ panel: QLPreviewPanel) { // panel.delegate = self // panel.dataSource = self // } // // func endPreviewPanelControl(_ panel: QLPreviewPanel) { // } // // func previewItems() -> [KMBookmark] { // var items = [KMBookmark]() // // outlineView.selectedRowIndexes.enumerated().forEach { (idx, _) in // if let item = outlineView.item(atRow: idx) as? KMBookmark { // if item.bookmarkType == .bookmark { // items.append(item) // } else if item.bookmarkType == .session { // items.append(contentsOf: item.children) // } // } // } // return items // } // // func numberOfPreviewItems(in panel: QLPreviewPanel) -> Int { // return previewItems().count // } // // func previewPanel(_ panel: QLPreviewPanel, previewItemAt anIndex: Int) -> QLPreviewItem { // return previewItems()[anIndex] // } // // func previewPanel(_ panel: QLPreviewPanel, sourceFrameOnScreenForPreviewItem item: QLPreviewItem) -> NSRect { // var item = item // if let parent = (item as? KMBookmark)?.parent, parent.bookmarkType == .session { // item = parent // } // let row = outlineView.row(forItem: item) // var iconRect = NSZeroRect // if let item = item as? KMBookmark, row != -1 { // let cell = outlineView.preparedCell(atColumn: 0, row: row) as? SKTextWithIconCell // iconRect = cell?.iconRect(forBounds: outlineView.frameOfCell(atColumn: 0, row: row)) ?? NSZeroRect // if outlineView.visibleRect.intersects(iconRect) { // iconRect = outlineView.convert(iconRect, to: nil) // } else { // iconRect = NSZeroRect // } // } // return iconRect // } // // func previewPanel(_ panel: QLPreviewPanel, transitionImageForPreviewItem item: QLPreviewItem, contentRect: UnsafeMutablePointer) -> NSImage? { // var item = item // if let parent = (item as? KMBookmark)?.parent, parent.bookmarkType == .session { // item = parent // } // return (item as? KMBookmark)?.icon // } // // func previewPanel(_ panel: QLPreviewPanel, handle event: NSEvent) -> Bool { // if event.type == .keyDown { // outlineView.keyDown(with: event) // return true // } // return false // } // } //extension KMBookmarkController: NSOutlineViewDelegate, NSOutlineViewDataSource { // //MARK: NSOutlineViewDataSource // // func minimumCoverForBookmarks(_ items: [KMBookmark]) -> [KMBookmark] { // var lastBm: KMBookmark? // var minimalCover = [KMBookmark]() // // for bm in items { // if !(bm.isDescendant(of: lastBm)) { // minimalCover.append(bm) // lastBm = bm // } // } // return minimalCover // } // // func outlineView(_ ov: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { // let bookmark = item as? KMBookmark ?? bookmarkRoot // return bookmark.bookmarkType == .folder ? bookmark.children.count : 0 // } // // func outlineView(_ ov: NSOutlineView, isItemExpandable item: Any) -> Bool { // let bookmark = item as! KMBookmark // return bookmark.bookmarkType == .folder // } // // func outlineView(_ ov: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { // let bookmark = (item as? KMBookmark) ?? bookmarkRoot // return bookmark.objectOfChidren(index: index) // } // // func outlineView(_ ov: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? { // guard let column = tableColumn else { return nil } // guard let bm = item as? KMBookmark else { return nil } // let tcID = column.identifier // // switch tcID { // case kLabelIdentifier: // return [kTextWithIconStringKey: bm.label, kTextWithIconImageKey: bm.icon] // case kFileIdentifier: // if bm.bookmarkType == .folder || bm.bookmarkType == .session { // let count = bm.children.count // return count == 1 ? NSLocalizedString("1 item", comment: "Bookmark folder description") : String.localizedStringWithFormat(NSLocalizedString("%ld items", comment: "Bookmark folder description"), count) // } else { // return bm.fileURL?.path ?? "" // } // case kPageIdentifier: // return bm.pageNumber // default: // return nil // } // } // // func outlineView(_ ov: NSOutlineView, setObjectValue object: Any?, for tableColumn: NSTableColumn?, byItem item: Any?) { // guard let column = tableColumn else { return } // guard let bm = item as? KMBookmark else { return } // let tcID = column.identifier // // switch tcID { // case kLabelIdentifier: // if let newLabel = (object as? [String: Any])?[kTextWithIconStringKey] as? String, newLabel != bm.label { // bm.label = newLabel // } // case kPageIdentifier: // if let newPageNumber = object as? Int, newPageNumber != bm.pageNumber.intValue { // bm.pageNumber = newPageNumber as NSNumber // } // default: // break // } // } // // func outlineView(_ ov: NSOutlineView, writeItems items: [Any], to pboard: NSPasteboard) -> Bool { // draggedBookmarks = minimumCoverForBookmarks(items as! [KMBookmark]) // pboard.clearContents() // pboard.setData(Data(), forType: kPasteboardTypeBookmarkRows) // return true // } // // func outlineView(_ ov: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { // guard index != NSOutlineViewDropOnItemIndex else { return [] } // // let pboard = info.draggingPasteboard // // if pboard.canReadItem(withDataConformingToTypes: [kPasteboardTypeBookmarkRows.rawValue]) && info.draggingSource as? NSOutlineView == ov { // return .move // } else if NSURL.canReadFileURL(from: pboard) { // return .every // } // return [] // } // // func outlineView(_ ov: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { // let pboard = info.draggingPasteboard // // if pboard.canReadItem(withDataConformingToTypes: [kPasteboardTypeBookmarkRows.rawValue]) && info.draggingSource as? NSOutlineView == ov { // var movedBookmarks = [KMBookmark]() // var indexes = IndexSet() // var insertionIndex = index // // let targetItem = item as? KMBookmark ?? bookmarkRoot // // for bookmark in draggedBookmarks { // guard let parent = bookmark.parent else { continue } // guard let bookmarkIndex = parent.children.firstIndex(of: bookmark) else { continue } // // if targetItem == parent { // if insertionIndex > bookmarkIndex { // insertionIndex -= 1 // } // if insertionIndex == bookmarkIndex { // continue // } // } // parent.removeObjectFromChildren(index: bookmarkIndex) // targetItem.insert(child: bookmark, atIndex: insertionIndex) // movedBookmarks.append(bookmark) // insertionIndex += 1 // } // // for bookmark in movedBookmarks { // let row = ov.row(forItem: bookmark) // if row != -1 { // indexes.insert(row) // } // } // if !indexes.isEmpty { // ov.selectRowIndexes(indexes, byExtendingSelection: false) // } // return true // } else { // let urls = NSURL.readFileURLs(from: pboard) // // let newBookmarks = KMBookmark.bookmarks(urls: urls) // if !newBookmarks.isEmpty { // var indexes = IndexSet(integersIn: index..<(index + newBookmarks.count)) // (item as? KMBookmark ?? bookmarkRoot).mutableArrayValue(forKey: "children").insert(newBookmarks, at: indexes) // if (item as? KMBookmark ?? bookmarkRoot) === bookmarkRoot || ov.isItemExpanded(item) { // if (item as? KMBookmark ?? bookmarkRoot) !== bookmarkRoot { // indexes.shift(startingAt: 0, by: ov.row(forItem: item) + 1) // } // ov.selectRowIndexes(indexes, byExtendingSelection: false) // } // return true // } // return false // } // } // // func outlineView(_ ov: NSOutlineView, dragEndedWith operation: NSDragOperation) { // draggedBookmarks.removeAll() // } // // // // MARK: NSOutlineViewDelegate // // func outlineView(_ ov: NSOutlineView, dataCellFor tableColumn: NSTableColumn?, item: Any) -> Any? { // if tableColumn == nil { // return (item as? KMBookmark)?.bookmarkType == .separator ? KMSeparatorCell() : nil // } // return tableColumn?.dataCell(forRow: ov.row(forItem: item)) // } // // func outlineView(_ ov: NSOutlineView, willDisplayCell cell: Any, for tableColumn: NSTableColumn?, item: Any) { // guard let column = tableColumn else { return } // guard let cell = cell as? NSCell else { return } // // if column.identifier == kFileIdentifier { // if let bm = item as? KMBookmark { //// if bm.bookmarkType == .folder || bm.bookmarkType == .session { //// cell.textColor = .disabledControlTextColor //// } else { //// cell.textColor = .controlTextColor //// } // } // } // } // // func outlineView(_ ov: NSOutlineView, shouldEdit tableColumn: NSTableColumn?, item: Any) -> Bool { // guard let column = tableColumn else { return false } // guard let bm = item as? KMBookmark else { return false } // // let tcID = column.identifier // switch tcID { // case kLabelIdentifier: // return bm.bookmarkType != .separator // case kPageIdentifier: // return bm.pageIndex != NSNotFound // default: // return false // } // } // // func outlineView(_ ov: NSOutlineView, toolTipFor cell: NSCell, rect: UnsafeMutablePointer, tableColumn tc: NSTableColumn?, item: Any, mouseLocation: NSPoint) -> String { // guard let column = tc else { return "" } // guard let bm = item as? KMBookmark else { return "" } // // let tcID = column.identifier // switch tcID { // case kLabelIdentifier: // return bm.label // case kFileIdentifier: // if bm.bookmarkType == .session { // return "" //// return bm.children.map { $0.path ?? "" }.joined(separator: "\n") // } else if bm.bookmarkType == .folder { // let count = bm.children.count // return count == 1 ? NSLocalizedString("1 item", comment: "Bookmark folder description") : String.localizedStringWithFormat(NSLocalizedString("%ld items", comment: "Bookmark folder description"), count) // } else { // return bm.fileURL?.path ?? "" // } // case kPageIdentifier: // return bm.pageNumber.stringValue // default: // return "" // } // } // // func outlineViewSelectionDidChange(_ notification: Notification) { // updateStatus() // if QLPreviewPanel.sharedPreviewPanelExists(), let previewPanel = QLPreviewPanel.shared(), previewPanel.isVisible, previewPanel.dataSource === self { // previewPanel.reloadData() // } // } // // func outlineView(_ ov: NSOutlineView, deleteItems items: [Any]) { // for item in minimumCoverForBookmarks(items as! [KMBookmark]).reversed() { // guard let parent = item.parent, let itemIndex = parent.children.firstIndex(of: item) else { continue } // parent.removeObjectFromChildren(index: itemIndex) // } // } // // func outlineView(_ ov: NSOutlineView, canDeleteItems items: [Any]) -> Bool { // return !items.isEmpty // } // // func outlineView(_ ov: NSOutlineView, copyItems items: [Any]) { // var urls = [URL]() // addBookmarkURLsToArray(minimumCoverForBookmarks(items as! [KMBookmark]), &urls) // if !urls.isEmpty { // let pboard = NSPasteboard.general // pboard.clearContents() // pboard.writeObjects(urls as [NSPasteboardWriting]) // } // } // // func outlineView(_ ov: NSOutlineView, canCopyItems items: [Any]) -> Bool { // return !items.isEmpty // } // // func outlineView(_ ov: NSOutlineView, pasteFromPasteboard pboard: NSPasteboard) { // let urls = NSURL.readFileURLs(from: pboard) // // let newBookmarks = KMBookmark.bookmarks(urls: urls) // if !newBookmarks.isEmpty { // var item: KMBookmark? // var anIndex = 0 // getInsertionFolder(&item, childIndex: &anIndex) // var indexes = IndexSet(integersIn: anIndex..<(anIndex + newBookmarks.count)) // (item ?? bookmarkRoot).mutableArrayValue(forKey: "children").insert(newBookmarks, at: indexes) // if item === bookmarkRoot || ov.isItemExpanded(item) { // if item !== bookmarkRoot { // indexes.shift(startingAt: 0, by: ov.row(forItem: item) + 1) // } // ov.selectRowIndexes(indexes, byExtendingSelection: false) // } // } // } // // func outlineView(_ ov: NSOutlineView, canPasteFromPasteboard pboard: NSPasteboard) -> Bool { // return NSURL.canReadFileURL(from: pboard) // } // // func outlineView(_ ov: NSOutlineView, typeSelectHelperSelectionStrings typeSelectHelper: SKTypeSelectHelper) -> [String] { // let count = ov.numberOfRows // var labels = [String]() // for i in 0.. [NSToolbarItem.Identifier] { // return [.flexibleSpace, .yourItem1, .yourItem2, .yourItem3] return [ NSToolbarItem.Identifier(kBookmarksNewFolderToolbarItemIdentifier), NSToolbarItem.Identifier(kBookmarksNewSeparatorToolbarItemIdentifier), NSToolbarItem.Identifier(kBookmarksDeleteToolbarItemIdentifier) ] } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { // return [.yourItem1, .yourItem2, .yourItem3, .flexibleSpace, .space] return [ NSToolbarItem.Identifier(kBookmarksNewFolderToolbarItemIdentifier), NSToolbarItem.Identifier(kBookmarksNewSeparatorToolbarItemIdentifier), NSToolbarItem.Identifier(kBookmarksDeleteToolbarItemIdentifier), .flexibleSpace, .space ] } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { return toolbarItems[itemIdentifier.rawValue] } func validateToolbarItem(_ item: NSToolbarItem) -> Bool { // guard let toolbar = self.window?.toolbar else { return false } // // if toolbar.customizationPaletteIsRunning { // return false // } else if toolbarItem.itemIdentifier == kBookmarksDeleteToolbarItemIdentifier { // return outlineView.canDelete // } return true } } extension KMBookmarkController: NSMenuDelegate, NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { // if menuItem.action == #selector(toggleStatusBar(_:)) { // if statusBar.isVisible { // menuItem.title = NSLocalizedString("Hide Status Bar", comment: "Menu item title") // } else { // menuItem.title = NSLocalizedString("Show Status Bar", comment: "Menu item title") // } // return true // } else if menuItem.action == #selector(addBookmark(_:)) { // return menuItem.tag == 0 // } return true } // MARK: - NSMenu delegate methods func addItemForBookmark(_ bookmark: KMBookmark, toMenu menu: NSMenu, isFolder: Bool, isAlternate: Bool) { var item: NSMenuItem? if isFolder { item = menu.addItemWithSubmenuAndTitle(bookmark.label) item?.submenu?.delegate = self } else { item = menu.addItem(withTitle: bookmark.label, action: #selector(openBookmark(_:)), target: self) } item?.representedObject = bookmark if isAlternate { item?.keyEquivalentModifierMask = NSEvent.ModifierFlags.option item?.isAlternate = true item?.setImageAndSize(bookmark.alternateIcon) } else { item?.setImageAndSize(bookmark.icon) } } // func menuNeedsUpdate(_ menu: NSMenu) { if bookmarkOutlineView != nil && menu == bookmarkOutlineView.outlineView.menu { let row = bookmarkOutlineView.outlineView.clickedRow menu.removeAllItems() if row != -1 { menu.addItem(withTitle: NSLocalizedString("Remove", comment: "Menu item title"), action: #selector(deleteMenuBookmarks), target: self) menu.addItem(withTitle: NSLocalizedString("Open", comment: "Menu item title"), action: #selector(openBookmarks), target: self) menu.addItem(withTitle: NSLocalizedString("Quick Look", comment: "Menu item title"), action: #selector(previewBookmarks), target: self) menu.addItem(.separator()) } menu.addItem(withTitle: NSLocalizedString("New Folder", comment: "Menu item title"), action: #selector(insertBookmarkFolder), target: self) menu.addItem(withTitle: NSLocalizedString("New Separator", comment: "Menu item title"), action: #selector(insertBookmarkSeparator), target: self) } else { let supermenu = menu.supermenu if supermenu != nil { let idx = supermenu!.indexOfItem(withSubmenu: menu) let bm = (supermenu == NSApp.mainMenu) ? bookmarkRoot : supermenu!.item(at: idx)?.representedObject as? KMBookmark let bookmarks = bm!.children var i = menu.numberOfItems while i > 0 { if let menuItem = menu.item(at: i - 1), menuItem.isSeparatorItem || menuItem.representedObject != nil { menu.removeItem(menuItem) } i -= 1 } if supermenu == NSApp.mainMenu && previousSession != nil { menu.addItem(.separator()) addItemForBookmark(previousSession!, toMenu: menu, isFolder: false, isAlternate: false) addItemForBookmark(previousSession!, toMenu: menu, isFolder: true, isAlternate: true) } if menu.numberOfItems > 0, bookmarks.count > 0 { menu.addItem(.separator()) } for bm in bookmarks { switch bm.bookmarkType { case .folder: addItemForBookmark(bm, toMenu: menu, isFolder: true, isAlternate: false) addItemForBookmark(bm, toMenu: menu, isFolder: false, isAlternate: true) case .session: addItemForBookmark(bm, toMenu: menu, isFolder: false, isAlternate: false) addItemForBookmark(bm, toMenu: menu, isFolder: true, isAlternate: true) case .separator: menu.addItem(.separator()) default: addItemForBookmark(bm, toMenu: menu, isFolder: false, isAlternate: false) } } } } } } extension NSURL { static func canReadFileURL(from pboard: NSPasteboard) -> Bool { let canReadFileURLsOnly = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly: true] let canReadClasses = [NSURL.self] return pboard.canReadObject(forClasses: canReadClasses, options: canReadFileURLsOnly) || pboard.canReadItem(withDataConformingToTypes: [NSPasteboard.PasteboardType.fileURL.rawValue]) } static func readFileURLs(from pboard: NSPasteboard) -> [URL] { if let fileURLs = pboard.readObjects(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) as? [URL], !fileURLs.isEmpty { return fileURLs } else if ((pboard.types?.contains(.fileURL)) != nil) { if let filenames = pboard.propertyList(forType: .fileURL) as? [String] { return filenames.compactMap { URL(fileURLWithPath: $0) } } } return [] } } extension KMBookmarkController { func minimumCoverForBookmarks(_ items: [KMBookmark]) -> [KMBookmark] { var lastBm: KMBookmark? var minimalCover = [KMBookmark]() for bm in items { if !(bm.isDescendant(of: lastBm)) { minimalCover.append(bm) lastBm = bm } } return minimalCover } func clickedBookmarks() -> [Any]? { let row = bookmarkOutlineView.outlineView.clickedRow guard row != -1 else { return nil } var indexes = bookmarkOutlineView.outlineView.selectedRowIndexes if !indexes.contains(row) { indexes = IndexSet(integer: row) } return indexes.compactMap { bookmarkOutlineView.outlineView.item(atRow: $0) } } }