From 78bda0d803c9c88bc9a48e45ece884e38e7c25fe Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 19:24:24 -0500 Subject: [PATCH 1/6] Added close active tab while hovering over sidebar --- .../Representables/ClickDetector.swift | 96 +++++++++++++++++++ ora/Core/Utilities/SettingsStore.swift | 12 +++ .../Sections/GeneralSettingsView.swift | 18 ++++ ora/Features/Sidebar/Views/SidebarView.swift | 7 ++ 4 files changed, 133 insertions(+) create mode 100644 ora/Core/Platform/Representables/ClickDetector.swift diff --git a/ora/Core/Platform/Representables/ClickDetector.swift b/ora/Core/Platform/Representables/ClickDetector.swift new file mode 100644 index 00000000..fec03223 --- /dev/null +++ b/ora/Core/Platform/Representables/ClickDetector.swift @@ -0,0 +1,96 @@ +// +// ClickDetector.swift +// ora +// +// Created by Aryan Rogye on 3/8/26. +// + +import AppKit +import SwiftUI + +enum ClickConfig: String, CaseIterable { + case none = "None" + case middleMouse = "Middle Mouse" + case optionClick = "Option Click" +} + +struct ClickDetector: NSViewRepresentable { + var config: ClickConfig + var onClick: () -> Void + + func makeNSView(context: Context) -> NSView { + let view = NSView() + /// uncomment to see border around the click area +// view.wantsLayer = true +// view.layer?.borderColor = NSColor.red.cgColor +// view.layer?.borderWidth = 2 + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { + context.coordinator.view = nsView + guard context.coordinator.currentConfig != config else { return } + context.coordinator.update(config: config, onClick: onClick, view: nsView) + } + + func makeCoordinator() -> Coordinator { + Coordinator(config: config, onClick: onClick) + } + + class Coordinator: NSObject { + var monitor: Any? + var lastFired: Date = .distantPast + let throttleInterval: TimeInterval = 0.5 + var currentConfig: ClickConfig = .none + weak var view: NSView? + + init(config: ClickConfig, onClick: @escaping () -> Void) { + super.init() + update(config: config, onClick: onClick, view: nil) + } + + func update(config: ClickConfig, onClick: @escaping () -> Void, view: NSView?) { + self.view = view + currentConfig = config + if let monitor { NSEvent.removeMonitor(monitor) } + monitor = nil + + switch config { + case .none: + break + case .middleMouse: + monitor = NSEvent.addLocalMonitorForEvents(matching: .otherMouseDown) { [weak self] event in + guard event.buttonNumber == 2 else { return event } + guard self?.isOverView(event) == true else { return event } + self?.fire(onClick) + return event + } + case .optionClick: + monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in + guard event.modifierFlags.contains(.option) else { return event } + guard self?.isOverView(event) == true else { return event } + self?.fire(onClick) + return event + } + } + } + + private func isOverView(_ event: NSEvent) -> Bool { + guard let view, let window = view.window else { return false } + let locationInWindow = event.locationInWindow + let locationInView = view.convert(locationInWindow, from: nil) + return view.bounds.contains(locationInView) + } + + private func fire(_ onClick: @escaping () -> Void) { + let now = Date() + guard now.timeIntervalSince(lastFired) >= throttleInterval else { return } + lastFired = now + onClick() + } + + deinit { + if let monitor { NSEvent.removeMonitor(monitor) } + } + } +} diff --git a/ora/Core/Utilities/SettingsStore.swift b/ora/Core/Utilities/SettingsStore.swift index 48d27e03..b085020d 100644 --- a/ora/Core/Utilities/SettingsStore.swift +++ b/ora/Core/Utilities/SettingsStore.swift @@ -155,6 +155,7 @@ class SettingsStore: ObservableObject { private let tabRemovalTimeoutKey = "settings.tabRemovalTimeout" private let maxRecentTabsKey = "settings.maxRecentTabs" private let autoPiPEnabledKey = "settings.autoPiPEnabled" + private let clickConfigKey = "settings.clickConfig" private let passwordsEnabledKey = "settings.passwords.enabled" private let passwordManagerProviderKey = "settings.passwords.provider" private let passwordAutofillEnabledKey = "settings.passwords.autofillEnabled" @@ -223,6 +224,10 @@ class SettingsStore: ObservableObject { @Published var maxRecentTabs: Int { didSet { defaults.set(maxRecentTabs, forKey: maxRecentTabsKey) } } + + @Published var clickConfig: ClickConfig { + didSet { defaults.set(clickConfig.rawValue, forKey: clickConfigKey) } + } @Published var autoPiPEnabled: Bool { didSet { defaults.set(autoPiPEnabled, forKey: autoPiPEnabledKey) } @@ -322,6 +327,13 @@ class SettingsStore: ObservableObject { passwordSavePromptsEnabled = defaults.object(forKey: passwordSavePromptsEnabledKey) as? Bool ?? true suppressedPasswordSavePromptHosts = Set(defaults .stringArray(forKey: suppressedPasswordSavePromptHostsKey) ?? []) + if let raw = defaults.string(forKey: clickConfigKey), + let config = ClickConfig(rawValue: raw) + { + clickConfig = config + } else { + clickConfig = .none + } } // MARK: - Per-container helpers diff --git a/ora/Features/Settings/Sections/GeneralSettingsView.swift b/ora/Features/Settings/Sections/GeneralSettingsView.swift index 505e6a94..fc08d3c8 100644 --- a/ora/Features/Settings/Sections/GeneralSettingsView.swift +++ b/ora/Features/Settings/Sections/GeneralSettingsView.swift @@ -96,6 +96,24 @@ struct GeneralSettingsView: View { .font(.caption2) .foregroundColor(.secondary) } + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Close active tab with:") + Spacer() + Picker("Sidebar hover close trigger", selection: $settings.clickConfig) { + Text("Disabled").tag(ClickConfig.none) + Text("Middle mouse click").tag(ClickConfig.middleMouse) + Text("Option + click").tag(ClickConfig.optionClick) + } + .labelsHidden() + .frame(width: 180) + } + + Text("Note: This only works when your pointer is over the sidebar.") + .font(.caption2) + .foregroundColor(.secondary) + } Toggle("Auto Picture-in-Picture on tab switch", isOn: $settings.autoPiPEnabled) } diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index 6f493b0d..cedd2229 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -14,6 +14,8 @@ struct SidebarView: View { @EnvironmentObject var media: MediaController @EnvironmentObject var sidebarManager: SidebarManager @EnvironmentObject var toolbarManager: ToolbarManager + + @StateObject private var settings = SettingsStore.shared @Query var containers: [TabContainer] @Query(filter: nil, sort: [.init(\History.lastAccessedAt, order: .reverse)]) @@ -102,6 +104,11 @@ struct SidebarView: View { toggleMaximizeWindow() } .enableInjection() + .background( + ClickDetector(config: settings.clickConfig) { + tabManager.closeActiveTab() + } + ) } private func onContainerSelected(container: TabContainer) { From 57123c4781874a7c897c644e86139b4e3c9e2584 Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 19:38:25 -0500 Subject: [PATCH 2/6] update to return nil --- ora/Core/Platform/Representables/ClickDetector.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ora/Core/Platform/Representables/ClickDetector.swift b/ora/Core/Platform/Representables/ClickDetector.swift index fec03223..2f68ae46 100644 --- a/ora/Core/Platform/Representables/ClickDetector.swift +++ b/ora/Core/Platform/Representables/ClickDetector.swift @@ -63,14 +63,14 @@ struct ClickDetector: NSViewRepresentable { guard event.buttonNumber == 2 else { return event } guard self?.isOverView(event) == true else { return event } self?.fire(onClick) - return event + return nil } case .optionClick: monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in guard event.modifierFlags.contains(.option) else { return event } guard self?.isOverView(event) == true else { return event } self?.fire(onClick) - return event + return nil } } } From 1670fcf487149a724caf7d7d9bd9420f1caa3ef3 Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 19:42:22 -0500 Subject: [PATCH 3/6] Fixing lint issues --- .../Sections/GeneralSettingsView.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ora/Features/Settings/Sections/GeneralSettingsView.swift b/ora/Features/Settings/Sections/GeneralSettingsView.swift index fc08d3c8..29a9f96e 100644 --- a/ora/Features/Settings/Sections/GeneralSettingsView.swift +++ b/ora/Features/Settings/Sections/GeneralSettingsView.swift @@ -7,7 +7,7 @@ struct GeneralSettingsView: View { @StateObject private var settings = SettingsStore.shared @StateObject private var defaultBrowserManager = DefaultBrowserManager.shared @Environment(\.theme) var theme - + var body: some View { SettingsContainer(maxContentWidth: 760) { Form { @@ -22,7 +22,7 @@ struct GeneralSettingsView: View { .font(.subheadline) .foregroundColor(.secondary) } - + Text("Fast, secure, and beautiful browser built for macOS") .font(.caption) .foregroundColor(.secondary) @@ -30,7 +30,7 @@ struct GeneralSettingsView: View { .padding(12) .background(theme.solidWindowBackgroundColor) .cornerRadius(8) - + if !defaultBrowserManager.isDefault { HStack { Text("Born for your Mac. Make Ora your default browser.") @@ -42,17 +42,17 @@ struct GeneralSettingsView: View { .background(theme.solidWindowBackgroundColor) .cornerRadius(8) } - + AppearanceSelector(selection: $appearanceManager.appearance) VStack(alignment: .leading, spacing: 12) { Text("Tab Management") .font(.headline) - + VStack(alignment: .leading, spacing: 8) { Text("Automatically clean up old tabs to preserve memory.") .font(.caption) .foregroundColor(.secondary) - + HStack { Text("Destroy web views after:") Spacer() @@ -66,7 +66,7 @@ struct GeneralSettingsView: View { } .frame(width: 120) } - + HStack { Text("Remove tabs completely after:") Spacer() @@ -80,7 +80,7 @@ struct GeneralSettingsView: View { } .frame(width: 120) } - + HStack { Text("Maximum recent tabs to keep in view:") Spacer() @@ -91,7 +91,7 @@ struct GeneralSettingsView: View { } .frame(width: 80) } - + Text("Note: Pinned and favorite tabs are never automatically removed.") .font(.caption2) .foregroundColor(.secondary) @@ -109,53 +109,53 @@ struct GeneralSettingsView: View { .labelsHidden() .frame(width: 180) } - + Text("Note: This only works when your pointer is over the sidebar.") .font(.caption2) .foregroundColor(.secondary) } - + Toggle("Auto Picture-in-Picture on tab switch", isOn: $settings.autoPiPEnabled) } .padding(.vertical, 8) VStack(alignment: .leading, spacing: 12) { Text("Updates") .font(.headline) - + Toggle("Automatically check for updates", isOn: $settings.autoUpdateEnabled) - + VStack(alignment: .leading, spacing: 8) { HStack { Button("Check for Updates") { updateService.checkForUpdates() } - + if updateService.isCheckingForUpdates { ProgressView() .scaleEffect(0.5) .frame(width: 16, height: 16) } - + if updateService.updateAvailable { Text("Update available!") .foregroundColor(.green) .font(.caption) } } - + if let result = updateService.lastCheckResult { Text(result) .font(.caption) .foregroundColor(updateService.updateAvailable ? .green : .secondary) } - + // Show current app version if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { Text("Current version: \(appVersion)") .font(.caption2) .foregroundColor(.secondary) } - + // Show last check time if let lastCheck = updateService.lastCheckDate { Text("Last checked: \(lastCheck.formatted(date: .abbreviated, time: .shortened))") @@ -168,7 +168,7 @@ struct GeneralSettingsView: View { } } } - + private func getAppVersion() -> String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" From cffb28fb37a08c006e861e1a92ccd9ca6118173e Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 19:44:55 -0500 Subject: [PATCH 4/6] fixed liniting --- .../Representables/ClickDetector.swift | 20 +++++----- ora/Core/Utilities/SettingsStore.swift | 5 ++- .../PasswordAutofillCoordinator.swift | 3 +- .../Passwords/Views/PasswordsWindow.swift | 3 +- .../Sections/GeneralSettingsView.swift | 40 +++++++++---------- .../Sections/PasswordsSettingsView.swift | 3 +- .../Views/BottomOption/ContainerForm.swift | 3 +- ora/Features/Sidebar/Views/SidebarView.swift | 2 +- 8 files changed, 42 insertions(+), 37 deletions(-) diff --git a/ora/Core/Platform/Representables/ClickDetector.swift b/ora/Core/Platform/Representables/ClickDetector.swift index 2f68ae46..c35f0a46 100644 --- a/ora/Core/Platform/Representables/ClickDetector.swift +++ b/ora/Core/Platform/Representables/ClickDetector.swift @@ -17,7 +17,7 @@ enum ClickConfig: String, CaseIterable { struct ClickDetector: NSViewRepresentable { var config: ClickConfig var onClick: () -> Void - + func makeNSView(context: Context) -> NSView { let view = NSView() /// uncomment to see border around the click area @@ -26,35 +26,35 @@ struct ClickDetector: NSViewRepresentable { // view.layer?.borderWidth = 2 return view } - + func updateNSView(_ nsView: NSView, context: Context) { context.coordinator.view = nsView guard context.coordinator.currentConfig != config else { return } context.coordinator.update(config: config, onClick: onClick, view: nsView) } - + func makeCoordinator() -> Coordinator { Coordinator(config: config, onClick: onClick) } - + class Coordinator: NSObject { var monitor: Any? var lastFired: Date = .distantPast let throttleInterval: TimeInterval = 0.5 var currentConfig: ClickConfig = .none weak var view: NSView? - + init(config: ClickConfig, onClick: @escaping () -> Void) { super.init() update(config: config, onClick: onClick, view: nil) } - + func update(config: ClickConfig, onClick: @escaping () -> Void, view: NSView?) { self.view = view currentConfig = config if let monitor { NSEvent.removeMonitor(monitor) } monitor = nil - + switch config { case .none: break @@ -74,21 +74,21 @@ struct ClickDetector: NSViewRepresentable { } } } - + private func isOverView(_ event: NSEvent) -> Bool { guard let view, let window = view.window else { return false } let locationInWindow = event.locationInWindow let locationInView = view.convert(locationInWindow, from: nil) return view.bounds.contains(locationInView) } - + private func fire(_ onClick: @escaping () -> Void) { let now = Date() guard now.timeIntervalSince(lastFired) >= throttleInterval else { return } lastFired = now onClick() } - + deinit { if let monitor { NSEvent.removeMonitor(monitor) } } diff --git a/ora/Core/Utilities/SettingsStore.swift b/ora/Core/Utilities/SettingsStore.swift index b085020d..006a1587 100644 --- a/ora/Core/Utilities/SettingsStore.swift +++ b/ora/Core/Utilities/SettingsStore.swift @@ -224,7 +224,7 @@ class SettingsStore: ObservableObject { @Published var maxRecentTabs: Int { didSet { defaults.set(maxRecentTabs, forKey: maxRecentTabsKey) } } - + @Published var clickConfig: ClickConfig { didSet { defaults.set(clickConfig.rawValue, forKey: clickConfigKey) } } @@ -326,7 +326,8 @@ class SettingsStore: ObservableObject { passwordAutofillSubmitEnabled = defaults.object(forKey: passwordAutofillSubmitEnabledKey) as? Bool ?? true passwordSavePromptsEnabled = defaults.object(forKey: passwordSavePromptsEnabledKey) as? Bool ?? true suppressedPasswordSavePromptHosts = Set(defaults - .stringArray(forKey: suppressedPasswordSavePromptHostsKey) ?? []) + .stringArray(forKey: suppressedPasswordSavePromptHostsKey) ?? [] + ) if let raw = defaults.string(forKey: clickConfigKey), let config = ClickConfig(rawValue: raw) { diff --git a/ora/Features/Passwords/Services/PasswordAutofillCoordinator.swift b/ora/Features/Passwords/Services/PasswordAutofillCoordinator.swift index d66997fa..f65a2a03 100644 --- a/ora/Features/Passwords/Services/PasswordAutofillCoordinator.swift +++ b/ora/Features/Passwords/Services/PasswordAutofillCoordinator.swift @@ -644,7 +644,8 @@ final class PasswordAutofillCoordinator { emailSuggestions: filteredEmailSuggestions, generatedPassword: filteredGeneratedPassword, selectedSuggestionIndex: (savedPasswordEntries.isEmpty && filteredEmailSuggestions - .isEmpty && filteredGeneratedPassword == nil) ? -1 : 0 + .isEmpty && filteredGeneratedPassword == nil + ) ? -1 : 0 ) } } diff --git a/ora/Features/Passwords/Views/PasswordsWindow.swift b/ora/Features/Passwords/Views/PasswordsWindow.swift index 679bf1c6..7d8e34eb 100644 --- a/ora/Features/Passwords/Views/PasswordsWindow.swift +++ b/ora/Features/Passwords/Views/PasswordsWindow.swift @@ -87,7 +87,8 @@ private struct PasswordsWindowView: View { if filteredEntries.isEmpty { emptyState(message: searchText - .isEmpty ? "No saved passwords yet." : "No saved passwords match that search.") + .isEmpty ? "No saved passwords yet." : "No saved passwords match that search." + ) } else { passwordsTable } diff --git a/ora/Features/Settings/Sections/GeneralSettingsView.swift b/ora/Features/Settings/Sections/GeneralSettingsView.swift index 29a9f96e..c9774941 100644 --- a/ora/Features/Settings/Sections/GeneralSettingsView.swift +++ b/ora/Features/Settings/Sections/GeneralSettingsView.swift @@ -7,7 +7,7 @@ struct GeneralSettingsView: View { @StateObject private var settings = SettingsStore.shared @StateObject private var defaultBrowserManager = DefaultBrowserManager.shared @Environment(\.theme) var theme - + var body: some View { SettingsContainer(maxContentWidth: 760) { Form { @@ -22,7 +22,7 @@ struct GeneralSettingsView: View { .font(.subheadline) .foregroundColor(.secondary) } - + Text("Fast, secure, and beautiful browser built for macOS") .font(.caption) .foregroundColor(.secondary) @@ -30,7 +30,7 @@ struct GeneralSettingsView: View { .padding(12) .background(theme.solidWindowBackgroundColor) .cornerRadius(8) - + if !defaultBrowserManager.isDefault { HStack { Text("Born for your Mac. Make Ora your default browser.") @@ -42,17 +42,17 @@ struct GeneralSettingsView: View { .background(theme.solidWindowBackgroundColor) .cornerRadius(8) } - + AppearanceSelector(selection: $appearanceManager.appearance) VStack(alignment: .leading, spacing: 12) { Text("Tab Management") .font(.headline) - + VStack(alignment: .leading, spacing: 8) { Text("Automatically clean up old tabs to preserve memory.") .font(.caption) .foregroundColor(.secondary) - + HStack { Text("Destroy web views after:") Spacer() @@ -66,7 +66,7 @@ struct GeneralSettingsView: View { } .frame(width: 120) } - + HStack { Text("Remove tabs completely after:") Spacer() @@ -80,7 +80,7 @@ struct GeneralSettingsView: View { } .frame(width: 120) } - + HStack { Text("Maximum recent tabs to keep in view:") Spacer() @@ -91,12 +91,12 @@ struct GeneralSettingsView: View { } .frame(width: 80) } - + Text("Note: Pinned and favorite tabs are never automatically removed.") .font(.caption2) .foregroundColor(.secondary) } - + VStack(alignment: .leading, spacing: 6) { HStack { Text("Close active tab with:") @@ -109,53 +109,53 @@ struct GeneralSettingsView: View { .labelsHidden() .frame(width: 180) } - + Text("Note: This only works when your pointer is over the sidebar.") .font(.caption2) .foregroundColor(.secondary) } - + Toggle("Auto Picture-in-Picture on tab switch", isOn: $settings.autoPiPEnabled) } .padding(.vertical, 8) VStack(alignment: .leading, spacing: 12) { Text("Updates") .font(.headline) - + Toggle("Automatically check for updates", isOn: $settings.autoUpdateEnabled) - + VStack(alignment: .leading, spacing: 8) { HStack { Button("Check for Updates") { updateService.checkForUpdates() } - + if updateService.isCheckingForUpdates { ProgressView() .scaleEffect(0.5) .frame(width: 16, height: 16) } - + if updateService.updateAvailable { Text("Update available!") .foregroundColor(.green) .font(.caption) } } - + if let result = updateService.lastCheckResult { Text(result) .font(.caption) .foregroundColor(updateService.updateAvailable ? .green : .secondary) } - + // Show current app version if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { Text("Current version: \(appVersion)") .font(.caption2) .foregroundColor(.secondary) } - + // Show last check time if let lastCheck = updateService.lastCheckDate { Text("Last checked: \(lastCheck.formatted(date: .abbreviated, time: .shortened))") @@ -168,7 +168,7 @@ struct GeneralSettingsView: View { } } } - + private func getAppVersion() -> String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" diff --git a/ora/Features/Settings/Sections/PasswordsSettingsView.swift b/ora/Features/Settings/Sections/PasswordsSettingsView.swift index 6a561486..fab65085 100644 --- a/ora/Features/Settings/Sections/PasswordsSettingsView.swift +++ b/ora/Features/Settings/Sections/PasswordsSettingsView.swift @@ -155,7 +155,8 @@ struct PasswordsSettingsView: View { if filteredEntries.isEmpty { emptyState(message: searchText - .isEmpty ? "No saved passwords yet." : "No saved passwords match that search.") + .isEmpty ? "No saved passwords yet." : "No saved passwords match that search." + ) } else { passwordsTable } diff --git a/ora/Features/Sidebar/Views/BottomOption/ContainerForm.swift b/ora/Features/Sidebar/Views/BottomOption/ContainerForm.swift index 113f96d8..cc66646c 100644 --- a/ora/Features/Sidebar/Views/BottomOption/ContainerForm.swift +++ b/ora/Features/Sidebar/Views/BottomOption/ContainerForm.swift @@ -37,7 +37,8 @@ struct ContainerForm: View { value: emoji.isEmpty ) .background(isEmojiPickerHovering ? theme.mutedBackground.opacity(0.8) - : theme.mutedBackground) + : theme.mutedBackground + ) .cornerRadius(ContainerConstants.UI.cornerRadius) if emoji.isEmpty { diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index cedd2229..de27d2f1 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -14,7 +14,7 @@ struct SidebarView: View { @EnvironmentObject var media: MediaController @EnvironmentObject var sidebarManager: SidebarManager @EnvironmentObject var toolbarManager: ToolbarManager - + @StateObject private var settings = SettingsStore.shared @Query var containers: [TabContainer] From f43636f7806c9b847327a3d65ea7244c51bfc766 Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 21:21:41 -0500 Subject: [PATCH 5/6] Updated to only close the tab that is getting hovered on --- .../Representables/ClickDetector.swift | 12 ++++++---- .../Sections/GeneralSettingsView.swift | 24 +++++++------------ .../Sidebar/Views/ContainerView.swift | 13 ++++++++++ ora/Features/Sidebar/Views/SidebarView.swift | 7 ------ .../Views/TabList/NormalTabsList.swift | 8 +++++++ 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/ora/Core/Platform/Representables/ClickDetector.swift b/ora/Core/Platform/Representables/ClickDetector.swift index c35f0a46..50aa60d4 100644 --- a/ora/Core/Platform/Representables/ClickDetector.swift +++ b/ora/Core/Platform/Representables/ClickDetector.swift @@ -29,7 +29,6 @@ struct ClickDetector: NSViewRepresentable { func updateNSView(_ nsView: NSView, context: Context) { context.coordinator.view = nsView - guard context.coordinator.currentConfig != config else { return } context.coordinator.update(config: config, onClick: onClick, view: nsView) } @@ -42,6 +41,7 @@ struct ClickDetector: NSViewRepresentable { var lastFired: Date = .distantPast let throttleInterval: TimeInterval = 0.5 var currentConfig: ClickConfig = .none + var onClick: (() -> Void)? weak var view: NSView? init(config: ClickConfig, onClick: @escaping () -> Void) { @@ -51,6 +51,8 @@ struct ClickDetector: NSViewRepresentable { func update(config: ClickConfig, onClick: @escaping () -> Void, view: NSView?) { self.view = view + self.onClick = onClick + guard currentConfig != config else { return } currentConfig = config if let monitor { NSEvent.removeMonitor(monitor) } monitor = nil @@ -62,14 +64,14 @@ struct ClickDetector: NSViewRepresentable { monitor = NSEvent.addLocalMonitorForEvents(matching: .otherMouseDown) { [weak self] event in guard event.buttonNumber == 2 else { return event } guard self?.isOverView(event) == true else { return event } - self?.fire(onClick) + self?.fire() return nil } case .optionClick: monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in guard event.modifierFlags.contains(.option) else { return event } guard self?.isOverView(event) == true else { return event } - self?.fire(onClick) + self?.fire() return nil } } @@ -82,11 +84,11 @@ struct ClickDetector: NSViewRepresentable { return view.bounds.contains(locationInView) } - private func fire(_ onClick: @escaping () -> Void) { + private func fire() { let now = Date() guard now.timeIntervalSince(lastFired) >= throttleInterval else { return } lastFired = now - onClick() + onClick?() } deinit { diff --git a/ora/Features/Settings/Sections/GeneralSettingsView.swift b/ora/Features/Settings/Sections/GeneralSettingsView.swift index c9774941..351995a9 100644 --- a/ora/Features/Settings/Sections/GeneralSettingsView.swift +++ b/ora/Features/Settings/Sections/GeneralSettingsView.swift @@ -97,22 +97,16 @@ struct GeneralSettingsView: View { .foregroundColor(.secondary) } - VStack(alignment: .leading, spacing: 6) { - HStack { - Text("Close active tab with:") - Spacer() - Picker("Sidebar hover close trigger", selection: $settings.clickConfig) { - Text("Disabled").tag(ClickConfig.none) - Text("Middle mouse click").tag(ClickConfig.middleMouse) - Text("Option + click").tag(ClickConfig.optionClick) - } - .labelsHidden() - .frame(width: 180) + HStack { + Text("Close tab with:") + Spacer() + Picker("", selection: $settings.clickConfig) { + Text("Disabled").tag(ClickConfig.none) + Text("Middle mouse click").tag(ClickConfig.middleMouse) + Text("Option + click").tag(ClickConfig.optionClick) } - - Text("Note: This only works when your pointer is over the sidebar.") - .font(.caption2) - .foregroundColor(.secondary) + .labelsHidden() + .frame(width: 180) } Toggle("Auto Picture-in-Picture on tab switch", isOn: $settings.autoPiPEnabled) diff --git a/ora/Features/Sidebar/Views/ContainerView.swift b/ora/Features/Sidebar/Views/ContainerView.swift index 43df9368..d50804c7 100644 --- a/ora/Features/Sidebar/Views/ContainerView.swift +++ b/ora/Features/Sidebar/Views/ContainerView.swift @@ -9,9 +9,12 @@ struct ContainerView: View { @EnvironmentObject var toolbarManager: ToolbarManager @EnvironmentObject var tabManager: TabManager @EnvironmentObject var privacyMode: PrivacyMode + + @StateObject private var settings = SettingsStore.shared @State var isDragging = false @State private var draggedItem: UUID? + @State private var hoverTab: UUID? @State private var editingURLString: String = "" var body: some View { @@ -78,6 +81,7 @@ struct ContainerView: View { NormalTabsList( tabs: normalTabs, draggedItem: $draggedItem, + hoverTab: $hoverTab, onDrag: dragTab, onSelect: selectTab, onPinToggle: togglePin, @@ -91,6 +95,15 @@ struct ContainerView: View { } } .modifier(OraWindowDragGesture(isDragging: $isDragging)) + .background( + ClickDetector(config: settings.clickConfig) { + if let hoverTab { + if let tab = normalTabs.first(where: { $0.id == hoverTab }) { + tabManager.closeTab(tab: tab) + } + } + } + ) } private var favoriteTabs: [Tab] { diff --git a/ora/Features/Sidebar/Views/SidebarView.swift b/ora/Features/Sidebar/Views/SidebarView.swift index de27d2f1..6f493b0d 100644 --- a/ora/Features/Sidebar/Views/SidebarView.swift +++ b/ora/Features/Sidebar/Views/SidebarView.swift @@ -15,8 +15,6 @@ struct SidebarView: View { @EnvironmentObject var sidebarManager: SidebarManager @EnvironmentObject var toolbarManager: ToolbarManager - @StateObject private var settings = SettingsStore.shared - @Query var containers: [TabContainer] @Query(filter: nil, sort: [.init(\History.lastAccessedAt, order: .reverse)]) var histories: [History] @@ -104,11 +102,6 @@ struct SidebarView: View { toggleMaximizeWindow() } .enableInjection() - .background( - ClickDetector(config: settings.clickConfig) { - tabManager.closeActiveTab() - } - ) } private func onContainerSelected(container: TabContainer) { diff --git a/ora/Features/Sidebar/Views/TabList/NormalTabsList.swift b/ora/Features/Sidebar/Views/TabList/NormalTabsList.swift index 00c4794e..2534ec5d 100644 --- a/ora/Features/Sidebar/Views/TabList/NormalTabsList.swift +++ b/ora/Features/Sidebar/Views/TabList/NormalTabsList.swift @@ -4,6 +4,7 @@ import SwiftUI struct NormalTabsList: View { let tabs: [Tab] @Binding var draggedItem: UUID? + @Binding var hoverTab: UUID? let onDrag: (UUID) -> NSItemProvider let onSelect: (Tab) -> Void let onPinToggle: (Tab) -> Void @@ -45,6 +46,13 @@ struct NormalTabsList: View { targetSection: .normal ) ) + .onHover { hovering in + if hovering { + hoverTab = tab.id + } else { + hoverTab = nil + } + } .transition(.asymmetric( insertion: .opacity.combined(with: .move(edge: .bottom)), removal: .opacity.combined(with: .move(edge: .top)) From 38443142b5885176152bb6f9f231c099dfc28896 Mon Sep 17 00:00:00 2001 From: AryanRogye Date: Sun, 8 Mar 2026 21:27:41 -0500 Subject: [PATCH 6/6] Fix lint --- ora/Core/Platform/Representables/ClickDetector.swift | 7 ++++--- ora/Features/Sidebar/Views/ContainerView.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ora/Core/Platform/Representables/ClickDetector.swift b/ora/Core/Platform/Representables/ClickDetector.swift index 50aa60d4..b4683ed1 100644 --- a/ora/Core/Platform/Representables/ClickDetector.swift +++ b/ora/Core/Platform/Representables/ClickDetector.swift @@ -19,12 +19,13 @@ struct ClickDetector: NSViewRepresentable { var onClick: () -> Void func makeNSView(context: Context) -> NSView { - let view = NSView() - /// uncomment to see border around the click area + // Uncomment to visualize the click area bounds when debugging. +// let view = NSView() // view.wantsLayer = true // view.layer?.borderColor = NSColor.red.cgColor // view.layer?.borderWidth = 2 - return view +// return view + return NSView() } func updateNSView(_ nsView: NSView, context: Context) { diff --git a/ora/Features/Sidebar/Views/ContainerView.swift b/ora/Features/Sidebar/Views/ContainerView.swift index d50804c7..f53f8c0f 100644 --- a/ora/Features/Sidebar/Views/ContainerView.swift +++ b/ora/Features/Sidebar/Views/ContainerView.swift @@ -9,7 +9,7 @@ struct ContainerView: View { @EnvironmentObject var toolbarManager: ToolbarManager @EnvironmentObject var tabManager: TabManager @EnvironmentObject var privacyMode: PrivacyMode - + @StateObject private var settings = SettingsStore.shared @State var isDragging = false