diff --git a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift index 94dca9da..240586bc 100644 --- a/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift +++ b/macos/Onit/Accessibility/Notifications/AccessibilityNotificationsManager.swift @@ -595,6 +595,24 @@ class AccessibilityNotificationsManager: ObservableObject { showDebug() } + + private func clearPendingInputsOnInvalidSelectedText() { + let currentHighlightedTextIsInView = PanelStateCoordinator.shared.state.selectedPendingInput == PanelStateCoordinator.shared.state.unpinnedPendingInput + + + /// Clearing `InputView` when it's showing the current highlighted text that's about to be cleared. + if currentHighlightedTextIsInView { + PanelStateCoordinator.shared.state.selectedPendingInput = nil + } + + PanelStateCoordinator.shared.state.unpinnedPendingInput = nil + PanelStateCoordinator.shared.state.trackedPendingInput = nil + } + + private func addSelectedTextToContextAndShowInView(_ input: Input) { + PanelStateCoordinator.shared.state.selectedPendingInput = input + PanelStateCoordinator.shared.state.unpinnedPendingInput = input + } private func processSelectedText(_ text: String?) { guard Defaults[.autoContextFromHighlights], @@ -602,8 +620,7 @@ class AccessibilityNotificationsManager: ObservableObject { HighlightedTextValidator.isValid(text: selectedText) else { screenResult.userInteraction.selectedText = nil - PanelStateCoordinator.shared.state.pendingInput = nil - PanelStateCoordinator.shared.state.trackedPendingInput = nil + clearPendingInputsOnInvalidSelectedText() return } @@ -612,7 +629,7 @@ class AccessibilityNotificationsManager: ObservableObject { let input = Input(selectedText: selectedText, application: currentSource ?? "") if Defaults[.autoAddHighlightedTextToContext] { - PanelStateCoordinator.shared.state.pendingInput = input + addSelectedTextToContextAndShowInView(input) } else { PanelStateCoordinator.shared.state.trackedPendingInput = input } diff --git a/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json b/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json new file mode 100644 index 00000000..3dad0e97 --- /dev/null +++ b/macos/Onit/Assets.xcassets/Icons/pin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "pin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg b/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg new file mode 100644 index 00000000..e14da339 --- /dev/null +++ b/macos/Onit/Assets.xcassets/Icons/pin.imageset/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/macos/Onit/Data/Structures/Input.swift b/macos/Onit/Data/Structures/Input.swift index 7024dcee..13219bff 100644 --- a/macos/Onit/Data/Structures/Input.swift +++ b/macos/Onit/Data/Structures/Input.swift @@ -7,7 +7,7 @@ import Foundation -struct Input: Codable, Equatable { +struct Input: Codable, Equatable, Hashable { var selectedText: String var application: String? } diff --git a/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift b/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift index 1d6b6e9e..37b706cf 100644 --- a/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift +++ b/macos/Onit/KeyboardShortcuts/KeyboardShortcutsManager.swift @@ -106,8 +106,8 @@ struct KeyboardShortcutsManager { if state.panel != nil { if state.showContextMenuBrowserTabs { state.showContextMenuBrowserTabs = false - } else if state.pendingInput != nil { - state.pendingInput = nil + } else if state.hasHighlightedText { + state.clearHighlightedTextStates() } else { PanelStateCoordinator.shared.closePanel() } diff --git a/macos/Onit/UI/Components/ContextTag.swift b/macos/Onit/UI/Components/ContextTag.swift index 2a19b3f4..c8b6dc94 100644 --- a/macos/Onit/UI/Components/ContextTag.swift +++ b/macos/Onit/UI/Components/ContextTag.swift @@ -19,10 +19,12 @@ struct ContextTag: View { private let shouldFadeIn: Bool private let iconBundleURL: URL? private let iconView: (any View)? + private let iconViewCornerIcon: ImageResource? private let caption: String? private let tooltip: String? private let errorDotColor: Color? private let action: (() -> Void)? + private let pinAction: (() -> Void)? private let removeAction: (() -> Void)? init( @@ -37,10 +39,12 @@ struct ContextTag: View { shouldFadeIn: Bool = false, iconBundleURL: URL? = nil, iconView: (any View)? = nil, + iconViewCornerIcon: ImageResource? = nil, caption: String? = nil, tooltip: String? = nil, errorDotColor: Color? = nil, action: (() -> Void)? = nil, + pinAction: (() -> Void)? = nil, removeAction: (() -> Void)? = nil ) { self.text = text @@ -54,16 +58,18 @@ struct ContextTag: View { self.shouldFadeIn = shouldFadeIn self.iconBundleURL = iconBundleURL self.iconView = iconView + self.iconViewCornerIcon = iconViewCornerIcon self.caption = caption self.tooltip = tooltip self.errorDotColor = errorDotColor self.action = action + self.pinAction = pinAction self.removeAction = removeAction } @State private var isHoveredBody: Bool = false @State private var isPressedBody: Bool = false - @State private var isHoveredRemove: Bool = false + @State private var isHoveredAction: Bool = false private let height: CGFloat = 24 @@ -72,6 +78,10 @@ struct ContextTag: View { return NSWorkspace.shared.icon(forFile: bundleUrl.path) } + private var hasHoverActions: Bool { + pinAction != nil || removeAction != nil + } + var body: some View { ZStack(alignment: .leading) { HStack(alignment: .center, spacing: 6) { @@ -97,7 +107,21 @@ struct ContextTag: View { } if let iconView = iconView { - AnyView(iconView) + ZStack(alignment: .bottomTrailing) { + AnyView(iconView) + + if let cornerIcon = iconViewCornerIcon { + ZStack(alignment: .center) { + Circle() + .fill(isHoveredBody ? hoverBackground : background) + .frame(width: 13, height: 13) + + Image(cornerIcon) + .addIconStyles(iconSize: 7.45) + } + .offset(x: 4, y: 4) + } + } } if isLoading { textView.shimmering() } @@ -114,16 +138,29 @@ struct ContextTag: View { } } - HStack(spacing: 0) { - Spacer() - - if let removeAction = removeAction { + if hasHoverActions { + HStack(spacing: 0) { + Spacer() + FadeHorizontal(color: hoverBackground) - removeButton(removeAction) + + HStack(spacing: 6) { + if let pinAction = pinAction { + hoverActionButton(icon: .pin) { + pinAction() + } + } + + if let removeAction = removeAction { + hoverActionButton(icon: .cross) { + removeAction() + } + } + } } + .frame(height: height) + .opacity(isHoveredBody ? 1 : 0) } - .frame(height: height) - .opacity(isHoveredBody ? 1 : 0) } .padding(.leading, 4) .padding(.trailing, 6) @@ -173,26 +210,26 @@ extension ContextTag { Text(text) .styleText( size: 12, - color: isHoveredRemove ? .T_3 : isHoveredBody ? hoverTextColor : textColor + color: isHoveredAction ? .T_3 : isHoveredBody ? hoverTextColor : textColor ) .truncateText() - .addAnimation(dependency: [isHoveredBody, isHoveredRemove]) + .addAnimation(dependency: [isHoveredBody, isHoveredAction]) } - private func removeButton(_ removeAction: @escaping () -> Void) -> some View { + private func hoverActionButton(icon: ImageResource, hoverAction: @escaping () -> Void) -> some View { Button { - removeAction() + hoverAction() } label: { - Image(.cross) + Image(icon) .addIconStyles( - foregroundColor: isHoveredRemove ? .white : .gray100, + foregroundColor: isHoveredAction ? Color.primary : .gray100, iconSize: 9 ) - .addAnimation(dependency: isHoveredRemove) + .addAnimation(dependency: isHoveredAction) } .background(hoverBackground) .onHover { isHovering in - isHoveredRemove = isHovering + isHoveredAction = isHovering } } } diff --git a/macos/Onit/UI/Content/ExternalTetheredButton.swift b/macos/Onit/UI/Content/ExternalTetheredButton.swift index 82163f38..9ac2c414 100644 --- a/macos/Onit/UI/Content/ExternalTetheredButton.swift +++ b/macos/Onit/UI/Content/ExternalTetheredButton.swift @@ -54,7 +54,7 @@ struct ExternalTetheredButton: View { } private var capturedHighlightedText: Bool { - return windowState?.pendingInput != nil + return windowState?.unpinnedPendingInput != nil || windowState?.trackedPendingInput != nil } private var foregroundWindowIcon: NSImage? { diff --git a/macos/Onit/UI/Panels/State/OnitPanelState.swift b/macos/Onit/UI/Panels/State/OnitPanelState.swift index cba07a14..037587f6 100644 --- a/macos/Onit/UI/Panels/State/OnitPanelState.swift +++ b/macos/Onit/UI/Panels/State/OnitPanelState.swift @@ -136,6 +136,10 @@ class OnitPanelState: NSObject { } var pinnedPendingInputs: [Input] = [] + + var hasHighlightedText: Bool { + selectedPendingInput != nil || trackedPendingInput != nil || unpinnedPendingInput != nil || !pinnedPendingInputs.isEmpty + } func clearHighlightedTextStates() { selectedPendingInput = nil diff --git a/macos/Onit/UI/Prompt/FinalContextView.swift b/macos/Onit/UI/Prompt/FinalContextView.swift index 0fc57b05..be38c41a 100644 --- a/macos/Onit/UI/Prompt/FinalContextView.swift +++ b/macos/Onit/UI/Prompt/FinalContextView.swift @@ -25,7 +25,7 @@ struct FinalContextView: View { let prompt: Prompt var usingContextOrInput: Bool { - usingContext || prompt.input != nil + usingContext || !prompt.inputs.isEmpty } var usingContext: Bool { @@ -164,8 +164,10 @@ struct FinalContextView: View { .buttonStyle(.plain) if isExpanded { - if let input = prompt.input { - InputView(input: input, isEditing: false) + if !prompt.inputs.isEmpty { + ForEach(prompt.inputs, id: \.self) { input in + InputView(input: input, isEditing: false) + } } if usingContext { diff --git a/macos/Onit/UI/Prompt/Generated/GeneratedView.swift b/macos/Onit/UI/Prompt/Generated/GeneratedView.swift index ecac38a4..61e05b20 100644 --- a/macos/Onit/UI/Prompt/Generated/GeneratedView.swift +++ b/macos/Onit/UI/Prompt/Generated/GeneratedView.swift @@ -36,7 +36,7 @@ struct GeneratedView: View { #if DEBUG #Preview { var prompt = Prompt.sample - prompt.input = Input(selectedText: "blablabla", application: "Xcode") + prompt.inputs = [Input(selectedText: "blablabla", application: "Xcode")] return GeneratedView(prompt: prompt) } diff --git a/macos/Onit/UI/Prompt/Input/InputButtons.swift b/macos/Onit/UI/Prompt/Input/InputButtons.swift index e8672a58..31d56678 100644 --- a/macos/Onit/UI/Prompt/Input/InputButtons.swift +++ b/macos/Onit/UI/Prompt/Input/InputButtons.swift @@ -17,9 +17,11 @@ struct InputButtons: View { var body: some View { Group { - if let state = state, input == state.pendingInput { + if let state = state, input == state.selectedPendingInput { Button { - state.pendingInput = nil + clearHighlightedText(state: state) + + state.selectedPendingInput = nil } label: { Image(.smallRemove) .renderingMode(.template) @@ -57,6 +59,14 @@ struct InputButtons: View { Defaults[.showHighlightedTextInput] = false } } + + // MARK: - Private Functions + + private func clearHighlightedText(state: OnitPanelState) { + if state.selectedPendingInput == state.unpinnedPendingInput { + state.unpinnedPendingInput = nil + } + } } #if DEBUG diff --git a/macos/Onit/UI/Prompt/PromptCore.swift b/macos/Onit/UI/Prompt/PromptCore.swift index 2db00dc4..88d8cc7f 100644 --- a/macos/Onit/UI/Prompt/PromptCore.swift +++ b/macos/Onit/UI/Prompt/PromptCore.swift @@ -70,8 +70,8 @@ struct PromptCore: View { var body: some View { VStack(spacing: 0) { - if let windowState = windowState, let pendingInput = windowState.pendingInput { - InputView(input: pendingInput) + if let windowState = windowState, let selectedPendingInput = windowState.selectedPendingInput { + InputView(input: selectedPendingInput) } VStack(spacing: 6) {