// // KMTocOutlineView.swift // PDF Reader Pro // // Created by tangchao on 2023/11/17. // import Cocoa @objc protocol KMTocOutlineViewDelegate: KMCustomOutlineViewDelegate { @objc optional func outlineView(_ anOutlineView: NSOutlineView, highlightLevelForRow row: Int) -> Int @objc optional func outlineView(_ anOutlineView: NSOutlineView, imageContextForItem item: Any?) -> AnyObject? } class KMTocOutlineView: KMCustomOutlineView { weak var tocDelegate: KMTocOutlineViewDelegate? { didSet { self._rebuildTrackingAreas() } } var hasImageToolTips: Bool { get { return self._kmTrackingAreas != nil } set { if (newValue && self._kmTrackingAreas == nil) { self._kmTrackingAreas = [] if (self.window != nil) { self._rebuildTrackingAreas() } } else if (newValue == false && self._kmTrackingAreas != nil) { if self.window != nil { self._removeTrackingAreas() } } } } private var _kmTrackingAreas: [NSTrackingArea]? private let MAX_HIGHLIGHTS = 5 deinit { KMPrint("KMTocOutlineView deinit.") NotificationCenter.default.removeObserver(self) } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } override func drawRow(_ row: Int, clipRect: NSRect) { // if (RUNNING_BEFORE(10_10) && [[self delegate] respondsToSelector:@selector(outlineView:highlightLevelForRow:)] && // [self isRowSelected:row] == NO) { if let level = self.tocDelegate?.outlineView?(self, highlightLevelForRow: row), self.isRowSelected(row) == false { if (level < MAX_HIGHLIGHTS) { var color = NSColor(red: 0.556, green: 0.615, blue: 0.748, alpha: 1.0) let rect = self.rect(ofRow: row) if (NSIntersectsRect(rect, clipRect)) { NSGraphicsContext.saveGraphicsState() color.withAlphaComponent(0.1 * (CGFloat(MAX_HIGHLIGHTS) - level.cgFloat)).setFill() NSBezierPath.fill(rect) NSGraphicsContext.restoreGraphicsState() } } } super.drawRow(row, clipRect: clipRect) } override func highlightSelection(inClipRect clipRect: NSRect) { if self.selectionHighlightStyle == .regular { NSGraphicsContext.saveGraphicsState() NSColor(red: 0.556, green: 0.615, blue: 0.748, alpha: 1.0).setFill() for i in self.selectedRowIndexes { __NSRectFill(self.rect(ofRow: i)) } NSGraphicsContext.restoreGraphicsState() } else { super.highlightSelection(inClipRect: clipRect) } } override func preparedCell(atColumn column: Int, row: Int) -> NSCell? { let cell = super.preparedCell(atColumn: column, row: row) if self.selectionHighlightStyle == .regular && self.isRowSelected(row) && cell?.type == .textCellType { if var attrString = cell?.attributedStringValue.mutableCopy() as? NSMutableAttributedString { let font = NSFontManager.shared.convert(cell?.font ?? .systemFont(ofSize: 12), toHaveTrait: .boldFontMask) attrString.addAttributes([.font : font], range: NSMakeRange(0, attrString.length)) cell?.attributedStringValue = attrString } } return cell } override func selectRowIndexes(_ indexes: IndexSet, byExtendingSelection extend: Bool) { super.selectRowIndexes(indexes, byExtendingSelection: extend) self._handleHighlightsChanged() } override func deselectRow(_ row: Int) { super.deselectRow(row) self._handleHighlightsChanged() } override func becomeFirstResponder() -> Bool { if super.becomeFirstResponder() { self._handleHighlightsChanged() return true } return false } override func resignFirstResponder() -> Bool { if super.resignFirstResponder() { self._handleHighlightsChanged() return true } return false } override func updateTrackingAreas() { super.updateTrackingAreas() self._rebuildTrackingAreas() } override func noteNumberOfRowsChanged() { super.noteNumberOfRowsChanged() self._rebuildTrackingAreas() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if self.window != nil { self._handleKeyOrMainStateChanged(nil) } } override func viewWillMove(toWindow newWindow: NSWindow?) { let oldWindow = self.window let names = [NSWindow.didBecomeMainNotification, NSWindow.didResignMainNotification, NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] if oldWindow != nil { for name in names { NotificationCenter.default.removeObserver(self, name: name, object: oldWindow) } } if newWindow != nil { for name in names { NotificationCenter.default.addObserver(self, selector: #selector(_handleKeyOrMainStateChanged), name: name, object: newWindow) } } super.viewWillMove(toWindow: newWindow) } override func keyDown(with event: NSEvent) { let eventChar = event.firstCharacter() let modifiers = event.standardPDFListViewModifierFlags() if (eventChar == NSLeftArrowFunctionKey && modifiers == (NSEvent.ModifierFlags.command.rawValue + NSEvent.ModifierFlags.option.rawValue)) { self.collapseItem(nil, collapseChildren: true) } else if (eventChar == NSRightArrowFunctionKey && modifiers == (NSEvent.ModifierFlags.command.rawValue + NSEvent.ModifierFlags.option.rawValue)) { self.expandItem(nil, expandChildren: true) } else { super.keyDown(with: event) } } override func mouseEntered(with event: NSEvent) { if (self._kmTrackingAreas == nil) { return } let userInfo = event.trackingArea?.userInfo if let rowNumber = userInfo?["row"] as? Int { var item = self.item(atRow: rowNumber) if let context = self.tocDelegate?.outlineView?(self, imageContextForItem: item) as? KMImageToolTipContext { KMImageToolTipWindow.shared.showForImageContext(context, at: .zero) } } } override func mouseExited(with event: NSEvent) { let userInfo = event.trackingArea?.userInfo if let row = userInfo?["row"] { KMImageToolTipWindow.shared.fadeOut() } } } // MARK: - Private Methods extension KMTocOutlineView { private func _handleHighlightsChanged() { // if (RUNNING_BEFORE(10_10) && [[self delegate] respondsToSelector:@selector(outlineView:highlightLevelForRow:)]) if let _ = self.tocDelegate?.outlineView?(self, highlightLevelForRow: -1) { self.needsDisplay = true } } @objc private func _handleKeyOrMainStateChanged(_ note: NSNotification?) { if self.selectionHighlightStyle == .sourceList { self._handleHighlightsChanged() } else { if let win = self.window, win.isMainWindow || win.isKeyWindow { // self.backgroundColor = NSColor(calibratedRed: 0.839216, green: 0.866667, blue: 0.898039, alpha: 1) } else { // self.backgroundColor = .controlColor } self.needsDisplay = true } } private func _rebuildTrackingAreas() { if self._kmTrackingAreas == nil || self.tocDelegate?.outlineView?(self, imageContextForItem: nil) == nil { return } self._removeTrackingAreas() if (self.window != nil) { let visibleRect = self.visibleRect let rowRange = self.rows(in: visibleRect) // NSUInteger row; for row in rowRange.location ..< NSMaxRange(rowRange) { self._addTrackingAreaForRow(row) } } } private func _removeTrackingAreas() { if (self._kmTrackingAreas == nil) { return } for area in self._kmTrackingAreas! { self.removeTrackingArea(area) } self._kmTrackingAreas?.removeAll() } private func _addTrackingAreaForRow(_ row: Int) { if (self._kmTrackingAreas == nil) { return } let userInfo = ["row" : row] let area = NSTrackingArea(rect: self.rect(ofRow: row), options: [.mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: userInfo) self.addTrackingArea(area) self._kmTrackingAreas?.append(area) } }