// // KMCustomOutlineView.swift // PDF Reader Pro // // Created by tangchao on 2023/11/17. // import Cocoa @objc protocol KMCustomOutlineViewDelegate: NSOutlineViewDelegate { @objc optional func outlineView(_ anOutlineView: NSOutlineView, deleteItems items: [Any]) @objc optional func outlineView(_ anOutlineView: NSOutlineView, canDeleteItems items: [Any]) -> Bool @objc optional func outlineView(_ anOutlineView: NSOutlineView, copyItems items: [Any]) @objc optional func outlineView(_ anOutlineView: NSOutlineView, canCopyItems items: [Any]) -> Bool @objc optional func outlineView(_ anOutlineView: NSOutlineView, pasteFromPasteboard pboard: NSPasteboard) @objc optional func outlineView(_ anOutlineView: NSOutlineView, canPasteFromPasteboard pboard: NSPasteboard) -> Bool @objc optional func outlineView(_ anOutlineView: NSOutlineView, typeSelectHelperSelectionStrings aTypeSelectHelper: SKTypeSelectHelper) -> NSArray @objc optional func outlineView(_ anOutlineView: NSOutlineView, typeSelectHelper aTypeSelectHelper: SKTypeSelectHelper, didFailToFindMatchForSearchString searchString: String) @objc optional func outlineView(_ anOutlineView: NSOutlineView, typeSelectHelper aTypeSelectHelper: SKTypeSelectHelper, updateSearchString searchString: String) } @objc protocol KMCustomOutlineViewDataSource: NSOutlineViewDataSource { @objc optional func outlineView(_ anOutlineView: NSOutlineView, dragEndedWithOperation dragEndedO: NSDragOperation) } open class KMCustomOutlineView: NSOutlineView { var supportsQuickLook = false private var _typeSelectHelper: SKTypeSelectHelper? var typeSelectHelper: SKTypeSelectHelper? { get { return self._typeSelectHelper } set { if self._typeSelectHelper != newValue { if self.isEqual(to: self._typeSelectHelper?.delegate) { self._typeSelectHelper?.delegate = nil } self._typeSelectHelper = newValue self._typeSelectHelper?.delegate = self } } } weak var botaDelegate: KMCustomOutlineViewDelegate? weak var botaDataSource: KMCustomOutlineViewDataSource? open override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. } open override func expandItem(_ item: Any?, expandChildren: Bool) { super.expandItem(item, expandChildren: expandChildren) self.typeSelectHelper?.rebuildTypeSelectSearchCache() } open override func collapseItem(_ item: Any?, collapseChildren: Bool) { super.collapseItem(item, collapseChildren: collapseChildren) self.typeSelectHelper?.rebuildTypeSelectSearchCache() } open override func reloadData() { super.reloadData() self.typeSelectHelper?.rebuildTypeSelectSearchCache() } open override func keyDown(with event: NSEvent) { let eventChar = event.firstCharacter() let modifierFlags = event.deviceIndependentModifierFlags() if ((eventChar == NSNewlineCharacter || eventChar == NSEnterCharacter || eventChar == NSCarriageReturnCharacter) && modifierFlags == 0) { if self.doubleValue == nil || self.sendAction(self.doubleAction, to: self.target) == false { NSSound.beep() } } else if ((eventChar == SKSpaceCharacter) && modifierFlags == 0) { if self.supportsQuickLook == false { self.enclosingScrollView?.pageDown(nil) } else if QLPreviewPanel.sharedPreviewPanelExists() && QLPreviewPanel.shared().isVisible { QLPreviewPanel.shared().orderOut(nil) } else { QLPreviewPanel.shared().makeKeyAndOrderFront(nil) } } else if ((eventChar == SKSpaceCharacter) && modifierFlags == NSEvent.ModifierFlags.shift.rawValue) { if self.supportsQuickLook == false { self.enclosingScrollView?.pageUp(nil) } } // else if (eventChar == NSHomeFunctionKey && (modifierFlags & ~NSFunctionKeyMask) == 0) { // [self scrollToBeginningOfDocument:nil]; // } else if (eventChar == NSEndFunctionKey && (modifierFlags & ~NSFunctionKeyMask) == 0) { // [self scrollToEndOfDocument:nil]; // } else if ((eventChar == NSDeleteCharacter || eventChar == NSDeleteFunctionKey) && modifierFlags == 0 && self.canDelete()) { self.delete(self) } else if let data = self.typeSelectHelper?.handleEvent(event), data == false { super.keyDown(with: event) } } /* - (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation { if ([[SKOutlineView superclass] instancesRespondToSelector:_cmd]) [super draggedImage:anImage endedAt:aPoint operation:operation]; if ([[self dataSource] respondsToSelector:@selector(outlineView:dragEndedWithOperation:)]) [[self dataSource] outlineView:self dragEndedWithOperation:operation]; } */ open override var font: NSFont? { get { for tc in self.tableColumns { if let cell = tc.dataCell as? NSCell { if cell.type == .textCellType { return cell.font } } } return nil } set { for tc in self.tableColumns { if let cell = tc.dataCell as? NSCell { if cell.type == .textCellType { cell.font = newValue } } } var rowHeight = font?.defaultViewLineHeight() ?? 34 if self.selectionHighlightStyle == .sourceList { rowHeight += 2.0 } self.rowHeight = rowHeight self.noteHeightOfRows(withIndexesChanged: IndexSet(0.. 0 { self.scrollRowToVisible(0) } } func scrollToEndOfDocument(_ sender: AnyObject?) { if self.numberOfRows > 0 { self.scrollRowToVisible(self.numberOfRows-1) } } func canPaste() -> Bool { return self.botaDelegate?.outlineView?(self, canPasteFromPasteboard: .general) ?? true } @objc func paste(_ sender: AnyObject?) { if self.canPaste() { self.botaDelegate?.outlineView?(self, pasteFromPasteboard: .general) } } func canCopy() -> Bool { let items = self.selectedItems() if items.count > 0 { return self.botaDelegate?.outlineView?(self, canCopyItems: items) ?? true } return false } @objc func copy(_ sender: AnyObject?) { if self.canCopy() { self.botaDelegate?.outlineView?(self, copyItems: self.selectedItems()) } } open func canDelete() -> Bool { let items = self.selectedItems() if items.count > 0 { return self.botaDelegate?.outlineView?(self, canDeleteItems: items) ?? true } return false } @objc func delete(_ sender: Any) { if self.canDelete() { self.botaDelegate?.outlineView?(self, deleteItems: self.selectedItems()) } } func selectedItems() -> [Any] { return self.itemsAtRowIndexes(self.selectedRowIndexes) } func itemsAtRowIndexes(_ indexes: IndexSet) -> [Any] { var items: [Any] = [] for i in indexes { if let item = self.item(atRow: i) { items.append(item) } } return items } } extension KMCustomOutlineView: NSMenuItemValidation { public func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { if (menuItem.action == #selector(delete)) { return self.canDelete() } else if (menuItem.action == KMSelectorCopy) { return self.canCopy() } else if (menuItem.action == #selector(paste)) { return self.canPaste() } else if (menuItem.action == #selector(selectAll)) { return self.allowsMultipleSelection } else if (menuItem.action == #selector(deselectAll)) { return self.allowsEmptySelection } // else if ([[SKOutlineView superclass] instancesRespondToSelector:@selector(validateMenuItem:)]) { // return [super validateMenuItem:menuItem]; // } return true } } extension KMCustomOutlineView: SKTypeSelectHelperDelegate { func typeSelectHelperSelectionStrings(_ typeSelectHelper: SKTypeSelectHelper) -> NSArray { return self.botaDelegate?.outlineView?(self, typeSelectHelperSelectionStrings: typeSelectHelper) ?? NSArray() } func typeSelectHelperCurrentlySelectedIndex(_ typeSelectHelper: SKTypeSelectHelper) -> Int { return self.selectedRowIndexes.last ?? NSNotFound } func typeSelectHelper(_ typeSelectHelper: SKTypeSelectHelper, selectItemAtIndex itemIndex: Int) { self.selectRowIndexes(NSIndexSet(index: itemIndex) as IndexSet, byExtendingSelection: false) self.scrollRowToVisible(itemIndex) } func typeSelectHelper(_ typeSelectHelper: SKTypeSelectHelper, didFailToFindMatchForSearchString searchString: String) { self.botaDelegate?.outlineView?(self, typeSelectHelper: typeSelectHelper, didFailToFindMatchForSearchString: searchString) } func typeSelectHelper(_ typeSelectHelper: SKTypeSelectHelper, updateSearchString searchString: String) { self.botaDelegate?.outlineView?(self, typeSelectHelper: typeSelectHelper, updateSearchString: searchString) } }