From 5eda2c85e368a0289ecce4a363aa7b90b8c43746 Mon Sep 17 00:00:00 2001 From: timl Date: Tue, 29 Jul 2025 16:07:04 -0700 Subject: [PATCH 1/4] Add HighlightedTextManager and pull highlighted text logic into it --- .../Notifications/AccessibilityNotificationsDelegate.swift | 1 + .../PanelStateManager/Tethered/PanelStateTetheredManager.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift index 0ca9872c..c1292dab 100644 --- a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift +++ b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Accessibility @MainActor protocol AccessibilityNotificationsDelegate: AnyObject { func accessibilityManager(_ manager: AccessibilityNotificationsManager, didActivateWindow window: TrackedWindow) diff --git a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift index 24b06405..d4084cd3 100644 --- a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift +++ b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift @@ -6,6 +6,7 @@ // @preconcurrency import AppKit +import Accessibility import Combine import Defaults import PostHog From 6b9d25cd789c44423b834d2fa5e6b71b264f3ceb Mon Sep 17 00:00:00 2001 From: timl Date: Tue, 29 Jul 2025 16:44:45 -0700 Subject: [PATCH 2/4] Move pendingInput code to OnitPanelState --- .../PanelStateBaseManager.swift | 3 ++ .../Pinned/PanelStatePinnedManager.swift | 3 +- .../Tethered/PanelStateTetheredManager.swift | 6 ++-- .../HighlightedTextDelegate.swift | 17 +++++++++ .../HighlightedTextManager.swift | 35 ++++++++++++++----- ...e.swift => OnitPanelState+Delegates.swift} | 19 ++++++++++ .../Onit/UI/Panels/State/OnitPanelState.swift | 11 ++++++ .../Onit/UI/QuickEdit/QuickEditManager.swift | 2 +- 8 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift rename macos/Onit/UI/Panels/State/{OnitPanelState+NSWindowDelegate.swift => OnitPanelState+Delegates.swift} (54%) diff --git a/macos/Onit/PanelStateManager/PanelStateBaseManager.swift b/macos/Onit/PanelStateManager/PanelStateBaseManager.swift index f43dda0b..0d029e08 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.unsubscribeFromDelegates() + } state = defaultState tetherButtonPanelState = nil diff --git a/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift b/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift index 77897e65..0e69cf09 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.subscribeToDelegates() 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.unsubscribeFromDelegates() state.removeDelegate(self) hintYRelativePosition = nil diff --git a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift index d4084cd3..af1021a8 100644 --- a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift +++ b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift @@ -81,7 +81,9 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { dragManager.stopMonitoring() dragManagerCancellable?.cancel() draggingState = nil - + for (_, state) in statesByWindow { + state.unsubscribeFromDelegates() + } super.stop() statesByWindow = [:] @@ -241,7 +243,7 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { panelState = activeState } else { panelState = OnitPanelState(trackedWindow: trackedWindow) - + panelState.subscribeToDelegates() statesByWindow[trackedWindow] = panelState } diff --git a/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift b/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift new file mode 100644 index 00000000..cc70f337 --- /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 highlightedTextDidChange(selectedText: String?, application: String?) +} diff --git a/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift b/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift index c7328965..44081ce1 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(selectedText: String?, application: String?) { + for case let delegate as HighlightedTextDelegate in delegates.allObjects { + delegate.highlightedTextDidChange(selectedText: selectedText, application: application) + } + } + func setCurrentSource(_ source: String?) { currentSource = source } @@ -97,22 +117,19 @@ 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(selectedText: 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(selectedText: selectedText, application: currentSource) } func handleCaretPositionChange(for element: AXUIElement) { diff --git a/macos/Onit/UI/Panels/State/OnitPanelState+NSWindowDelegate.swift b/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift similarity index 54% rename from macos/Onit/UI/Panels/State/OnitPanelState+NSWindowDelegate.swift rename to macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift index b5da29f4..6d5b1a41 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,21 @@ extension OnitPanelState: NSWindowDelegate { } } } + +extension OnitPanelState: HighlightedTextDelegate { + func highlightedTextDidChange(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 3c1b5d30..4cb47ea4 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 subscribeToDelegates() { + HighlightedTextManager.shared.addDelegate(self) + } + + public func unsubscribeFromDelegates() { + 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 8aa1da88..e513dd77 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() } } From 2c6544b2a22d66c9f55f407303df8f9d44617634 Mon Sep 17 00:00:00 2001 From: Niduank Date: Thu, 31 Jul 2025 15:23:53 +0200 Subject: [PATCH 3/4] Review fixes --- .../PanelStateManager/PanelStateBaseManager.swift | 2 +- .../Pinned/PanelStatePinnedManager.swift | 4 ++-- .../Tethered/PanelStateTetheredManager.swift | 4 ++-- .../HighlightedText/HighlightedTextDelegate.swift | 2 +- .../HighlightedText/HighlightedTextManager.swift | 12 ++++++++---- .../UI/Panels/State/OnitPanelState+Delegates.swift | 3 ++- macos/Onit/UI/Panels/State/OnitPanelState.swift | 4 ++-- 7 files changed, 18 insertions(+), 13 deletions(-) diff --git a/macos/Onit/PanelStateManager/PanelStateBaseManager.swift b/macos/Onit/PanelStateManager/PanelStateBaseManager.swift index 0d029e08..f3aa146f 100644 --- a/macos/Onit/PanelStateManager/PanelStateBaseManager.swift +++ b/macos/Onit/PanelStateManager/PanelStateBaseManager.swift @@ -74,7 +74,7 @@ class PanelStateBaseManager: PanelStateManagerLogic { closePanels() hideTetherWindow() for state in states { - state.unsubscribeFromDelegates() + state.unsubscribeAsHighlightedTextDelegate() } state = defaultState diff --git a/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift b/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift index 0e69cf09..38ea3aea 100644 --- a/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift +++ b/macos/Onit/PanelStateManager/Pinned/PanelStatePinnedManager.swift @@ -54,7 +54,7 @@ class PanelStatePinnedManager: PanelStateBaseManager, ObservableObject { self.state = state states = [state] - state.subscribeToDelegates() + state.subscribeAsHighlightedTextDelegate() globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { [weak self] event in guard let self = self else { return } @@ -115,7 +115,7 @@ class PanelStatePinnedManager: PanelStateBaseManager, ObservableObject { dragManager.stopMonitoring() dragManagerCancellable?.cancel() draggingWindow = nil - state.unsubscribeFromDelegates() + state.unsubscribeAsHighlightedTextDelegate() state.removeDelegate(self) hintYRelativePosition = nil diff --git a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift index af1021a8..b4405831 100644 --- a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift +++ b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift @@ -82,7 +82,7 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { dragManagerCancellable?.cancel() draggingState = nil for (_, state) in statesByWindow { - state.unsubscribeFromDelegates() + state.unsubscribeAsHighlightedTextDelegate() } super.stop() @@ -243,7 +243,7 @@ class PanelStateTetheredManager: PanelStateBaseManager, ObservableObject { panelState = activeState } else { panelState = OnitPanelState(trackedWindow: trackedWindow) - panelState.subscribeToDelegates() + panelState.subscribeAsHighlightedTextDelegate() statesByWindow[trackedWindow] = panelState } diff --git a/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift b/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift index cc70f337..de892ab8 100644 --- a/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift +++ b/macos/Onit/StateManagers/HighlightedText/HighlightedTextDelegate.swift @@ -13,5 +13,5 @@ protocol HighlightedTextDelegate: AnyObject { /// - Parameters: /// - selectedText: The newly selected text, or nil if text was deselected /// - application: The name of the application where the text was selected - func highlightedTextDidChange(selectedText: String?, application: String?) + 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 44081ce1..9a3b36f7 100644 --- a/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift +++ b/macos/Onit/StateManagers/HighlightedText/HighlightedTextManager.swift @@ -53,9 +53,9 @@ class HighlightedTextManager: ObservableObject { delegates.remove(delegate) } - private func notifyDelegates(selectedText: String?, application: String?) { + private func notifyDelegates(_ notification: (HighlightedTextDelegate) -> Void) { for case let delegate as HighlightedTextDelegate in delegates.allObjects { - delegate.highlightedTextDidChange(selectedText: selectedText, application: application) + notification(delegate) } } @@ -121,7 +121,9 @@ class HighlightedTextManager: ObservableObject { self.selectedText = nil // Notify delegates that text was deselected - notifyDelegates(selectedText: nil, application: currentSource) + notifyDelegates { + $0.highlightedTextManager(self, didChange: nil, application: currentSource) + } return } @@ -129,7 +131,9 @@ class HighlightedTextManager: ObservableObject { self.selectedText = selectedText // Notify delegates about the text change - notifyDelegates(selectedText: selectedText, application: currentSource) + notifyDelegates { + $0.highlightedTextManager(self, didChange: selectedText, application: currentSource) + } } func handleCaretPositionChange(for element: AXUIElement) { diff --git a/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift b/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift index 6d5b1a41..c5ca4521 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState+Delegates.swift @@ -32,7 +32,8 @@ extension OnitPanelState: NSWindowDelegate { } extension OnitPanelState: HighlightedTextDelegate { - func highlightedTextDidChange(selectedText: String?, application: String?) { + + func highlightedTextManager(_ manager: HighlightedTextManager, didChange selectedText: String?, application: String?) { if let selectedText = selectedText { let input = Input(selectedText: selectedText, application: application ?? "") diff --git a/macos/Onit/UI/Panels/State/OnitPanelState.swift b/macos/Onit/UI/Panels/State/OnitPanelState.swift index 4cb47ea4..4b156ec5 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState.swift @@ -204,11 +204,11 @@ class OnitPanelState: NSObject { // MARK: - Setup - public func subscribeToDelegates() { + public func subscribeAsHighlightedTextDelegate() { HighlightedTextManager.shared.addDelegate(self) } - public func unsubscribeFromDelegates() { + public func unsubscribeAsHighlightedTextDelegate() { HighlightedTextManager.shared.removeDelegate(self) } From 8cf268a5898ac5eafa5a2a7e6df5f32117036fda Mon Sep 17 00:00:00 2001 From: Niduank Date: Thu, 31 Jul 2025 15:26:47 +0200 Subject: [PATCH 4/4] Remove import Accessibility --- .../Notifications/AccessibilityNotificationsDelegate.swift | 1 - .../PanelStateManager/Tethered/PanelStateTetheredManager.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift index c1292dab..0ca9872c 100644 --- a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift +++ b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsDelegate.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Accessibility @MainActor protocol AccessibilityNotificationsDelegate: AnyObject { func accessibilityManager(_ manager: AccessibilityNotificationsManager, didActivateWindow window: TrackedWindow) diff --git a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift index b4405831..f7551197 100644 --- a/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift +++ b/macos/Onit/PanelStateManager/Tethered/PanelStateTetheredManager.swift @@ -6,7 +6,6 @@ // @preconcurrency import AppKit -import Accessibility import Combine import Defaults import PostHog