diff --git a/macos/Onit/PanelStateManager/PanelStateBaseManager.swift b/macos/Onit/PanelStateManager/PanelStateBaseManager.swift index f43dda0b5..f3aa146fd 100644 --- a/macos/Onit/PanelStateManager/PanelStateBaseManager.swift +++ b/macos/Onit/PanelStateManager/PanelStateBaseManager.swift @@ -73,6 +73,9 @@ class PanelStateBaseManager: PanelStateManagerLogic { } closePanels() hideTetherWindow() + for state in states { + state.unsubscribeAsHighlightedTextDelegate() + } state = defaultState tetherButtonPanelState = nil diff --git a/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift b/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift index 77897e658..38ea3aea6 100644 --- a/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift +++ b/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift @@ -54,6 +54,7 @@ class PanelStatePinnedManager: PanelStateBaseManager, ObservableObject { self.state = state states = [state] + state.subscribeAsHighlightedTextDelegate() globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { [weak self] event in guard let self = self else { return } @@ -114,7 +115,7 @@ class PanelStatePinnedManager: PanelStateBaseManager, ObservableObject { dragManager.stopMonitoring() dragManagerCancellable?.cancel() draggingWindow = nil - + state.unsubscribeAsHighlightedTextDelegate() state.removeDelegate(self) hintYRelativePosition = nil diff --git a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift index 24b064051..f75511979 100644 --- a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift +++ b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift @@ -80,7 +80,9 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { dragManager.stopMonitoring() dragManagerCancellable?.cancel() draggingState = nil - + for (_, state) in statesByWindow { + state.unsubscribeAsHighlightedTextDelegate() + } super.stop() statesByWindow = [:] @@ -240,7 +242,7 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { panelState = activeState } else { panelState = OnitPanelState(trackedWindow: trackedWindow) - + panelState.subscribeAsHighlightedTextDelegate() statesByWindow[trackedWindow] = panelState } diff --git a/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift b/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift new file mode 100644 index 000000000..de892ab86 --- /dev/null +++ b/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift @@ -0,0 +1,17 @@ +// +// HighlightedTextDelegate.swift +// Onit +// +// Created by TimL on 07/29/2025. +// + +import Foundation + +@MainActor +protocol HighlightedTextDelegate: AnyObject { + /// Called when highlighted text has changed + /// - Parameters: + /// - selectedText: The newly selected text, or nil if text was deselected + /// - application: The name of the application where the text was selected + func highlightedTextManager(_ manager: HighlightedTextManager, didChange selectedText: String?, application: String?) +} diff --git a/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift b/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift index c7328965a..9a3b36f7e 100644 --- a/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift +++ b/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift @@ -26,6 +26,10 @@ class HighlightedTextManager: ObservableObject { // Published property for selected text that QuickEditManager can observe @Published var selectedText: String? + // MARK: - Delegates + + private var delegates = NSHashTable.weakObjects() + // MARK: - Private initializer private init() { @@ -39,6 +43,22 @@ class HighlightedTextManager: ObservableObject { // MARK: - Functions + // MARK: - Delegate Management + + func addDelegate(_ delegate: HighlightedTextDelegate) { + delegates.add(delegate) + } + + func removeDelegate(_ delegate: HighlightedTextDelegate) { + delegates.remove(delegate) + } + + private func notifyDelegates(_ notification: (HighlightedTextDelegate) -> Void) { + for case let delegate as HighlightedTextDelegate in delegates.allObjects { + notification(delegate) + } + } + func setCurrentSource(_ source: String?) { currentSource = source } @@ -97,21 +117,22 @@ class HighlightedTextManager: ObservableObject { let selectedText = text, HighlightedTextValidator.isValid(text: selectedText) else { - PanelStateCoordinator.shared.state.pendingInput = nil - PanelStateCoordinator.shared.state.trackedPendingInput = nil + // Update the published selectedText property self.selectedText = nil + + // Notify delegates that text was deselected + notifyDelegates { + $0.highlightedTextManager(self, didChange: nil, application: currentSource) + } return } // Update the published selectedText property self.selectedText = selectedText - let input = Input(selectedText: selectedText, application: currentSource ?? "") - - if Defaults[.autoAddHighlightedTextToContext] { - PanelStateCoordinator.shared.state.pendingInput = input - } else { - PanelStateCoordinator.shared.state.trackedPendingInput = input + // Notify delegates about the text change + notifyDelegates { + $0.highlightedTextManager(self, didChange: selectedText, application: currentSource) } } diff --git a/macos/Onit/UI/Panels/State/OnitPanelState+NSWindowDelegate.swift b/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift similarity index 52% rename from macos/Onit/UI/Panels/State/OnitPanelState+NSWindowDelegate.swift rename to macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift index b5da29f43..c5ca45216 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState+NSWindowDelegate.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift @@ -6,6 +6,7 @@ // import AppKit +import Defaults extension OnitPanelState: NSWindowDelegate { @@ -29,3 +30,22 @@ extension OnitPanelState: NSWindowDelegate { } } } + +extension OnitPanelState: HighlightedTextDelegate { + + func highlightedTextManager(_ manager: HighlightedTextManager, didChange selectedText: String?, application: String?) { + if let selectedText = selectedText { + let input = Input(selectedText: selectedText, application: application ?? "") + + if Defaults[.autoAddHighlightedTextToContext] { + pendingInput = input + } else { + trackedPendingInput = input + } + } else { + // Text was deselected + pendingInput = nil + trackedPendingInput = nil + } + } +} diff --git a/macos/Onit/UI/Panels/State/OnitPanelState.swift b/macos/Onit/UI/Panels/State/OnitPanelState.swift index 3c1b5d30c..4b156ec55 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState.swift @@ -202,6 +202,17 @@ class OnitPanelState: NSObject { currentAnimationTask = nil } + // MARK: - Setup + + public func subscribeAsHighlightedTextDelegate() { + HighlightedTextManager.shared.addDelegate(self) + } + + public func unsubscribeAsHighlightedTextDelegate() { + HighlightedTextManager.shared.removeDelegate(self) + } + + // MARK: - Delegates func addDelegate(_ delegate: OnitPanelStateDelegate) { diff --git a/macos/Onit/UI/QuickEdit/QuickEditManager.swift b/macos/Onit/UI/QuickEdit/QuickEditManager.swift index 8aa1da888..e513dd779 100644 --- a/macos/Onit/UI/QuickEdit/QuickEditManager.swift +++ b/macos/Onit/UI/QuickEdit/QuickEditManager.swift @@ -284,7 +284,7 @@ extension QuickEditManager { } func caretDidDisappear() { - if !hasTextSelection(PanelStateCoordinator.shared.state.pendingInput?.selectedText ?? PanelStateCoordinator.shared.state.trackedPendingInput?.selectedText) { + if !hasTextSelection(highlightedTextManager.selectedText) { hideHint() } }