// // 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 outlineView: KMCustomOutlineView! var draggedBookmarks: [KMBookmark] = [] var recentDocuments: [[String: Any]] { get { return KMBookmarkManager.manager.recentDocuments } set { } } var bookmarkRoot: KMRootBookmark { get { return KMBookmarkManager.manager.rootBookmark } set { } } var toolbarItems: [String: NSToolbarItem] = [:] override func windowDidLoad() { super.windowDidLoad() setupToolbar() outlineView.delegate = self outlineView.dataSource = self outlineView.registerForDraggedTypes([kPasteboardTypeBookmarkRows, .fileURL, .string]) outlineView.doubleAction = #selector(doubleClickBookmark(_:)) } 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 = outlineView.clickedRow if let bm = (row != -1 ? outlineView.item(atRow: row) : nil) as? KMBookmark, [KMBookmarkType.bookmark, .session].contains(bm.bookmarkType) { bm.open() } } 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) } outlineView.reloadData() } @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) let row = outlineView.row(forItem: folder) outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) 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) 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).. [Any]? { // let row = outlineView.clickedRow // guard row != -1 else { return nil } // var indexes = outlineView.selectedRowIndexes // if !indexes.contains(row) { // indexes = IndexSet(integer: row) // } // return indexes.compactMap { outlineView.item(atRow: $0) } // } // // @IBAction func deleteBookmarks(_ sender: Any) { // guard let items = clickedBookmarks() as? [KMBookmark] else { return } // for item in items.reversed() { // if let parent = item.parent, let itemIndex = parent.children.index(of: item) { // parent.removeObject(fromChildrenAtIndex: itemIndex) // } // } // } // // @IBAction func openBookmarks(_ sender: Any) { // guard let items = clickedBookmarks() as? [KMBookmark] else { return } // for item in items.reversed() { // item.open() // } // } // // @IBAction func previewBookmarks(_ sender: Any) { // if QLPreviewPanel.sharedPreviewPanelExists() && QLPreviewPanel.shared().isVisible { // QLPreviewPanel.shared().orderOut(nil) // } else if let row = outlineView.clickedRow { // outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) // QLPreviewPanel.shared().makeKeyAndOrderFront(nil) // } // } // // // // MARK: - NSMenu delegate methods // // func addItemForBookmark(_ bookmark: KMBookmark, toMenu menu: NSMenu, isFolder: Bool, isAlternate: Bool) { // var item: NSMenuItem? // if isFolder { // item = menu.addItem(withSubmenuAndTitle: 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 = .alternate // item?.isAlternate = true // item?.setImageAndSize(bookmark.alternateIcon) // } else { // item?.setImageAndSize(bookmark.icon) // } // } // // func menuNeedsUpdate(_ menu: NSMenu) { // if menu == outlineView.menu { // let row = outlineView.clickedRow // menu.removeAllItems() // if row != -1 { // menu.addItem(withTitle: NSLocalizedString("Remove", comment: "Menu item title"), action: #selector(deleteBookmarks(_:)), 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 { // guard let supermenu = menu.supermenu, let idx = supermenu.indexOfItem(withSubmenu: menu), let bm = (supermenu == NSApp.mainMenu) ? bookmarkRoot : supermenu.item(at: idx)?.representedObject as? KMBookmark else { return } // // 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, let previousSession = previousSession { // 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) // } // } // } // } // // // avoid rebuilding the bookmarks menu on every key event // func menuHasKeyEquivalent(_ menu: NSMenu, for event: NSEvent, target: AutoreleasingUnsafeMutablePointer?, action: UnsafeMutablePointer?) -> Bool { 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 } } 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 [] } }