diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9657acb..3bfb30458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Safe mode no longer jumps to the first tab after you confirm a query. The confirmation now stays on the tab you ran the query from. (#1781) - Switching between sidebar tables no longer leaves extra blank space above the list. (#1675) - SSH tunnels no longer pin a CPU core after the connection drops. A dropped tunnel is now detected and torn down instead of spinning in its relay loop. (#1769) - Restored table tabs now load with the current page size instead of the page size from the previous session. diff --git a/TablePro/Core/Services/Execution/Providers/OperationConfirming.swift b/TablePro/Core/Services/Execution/Providers/OperationConfirming.swift index 962fb2322..4596d9077 100644 --- a/TablePro/Core/Services/Execution/Providers/OperationConfirming.swift +++ b/TablePro/Core/Services/Execution/Providers/OperationConfirming.swift @@ -14,7 +14,7 @@ internal struct AlertOperationConfirming: OperationConfirming { @MainActor func confirm(sql: String, operationDescription: String, connectionId: UUID, isDestructive: Bool) async -> Bool { NSApp.activate(ignoringOtherApps: true) - let window = WindowLifecycleMonitor.shared.findWindow(for: connectionId) + let window = WindowLifecycleMonitor.shared.activeWindow(for: connectionId, preferring: NSApp.keyWindow) let preview = Self.preview(of: sql) if isDestructive { diff --git a/TablePro/Core/Services/Infrastructure/WindowLifecycleMonitor.swift b/TablePro/Core/Services/Infrastructure/WindowLifecycleMonitor.swift index 192675436..bff1b623b 100644 --- a/TablePro/Core/Services/Infrastructure/WindowLifecycleMonitor.swift +++ b/TablePro/Core/Services/Infrastructure/WindowLifecycleMonitor.swift @@ -115,6 +115,19 @@ internal final class WindowLifecycleMonitor { .first { $0.isVisible } } + /// The active window for a connection, preferring `candidate` (typically the + /// key window) when it belongs to this connection. Each editor tab is a + /// separate window in a native tab group, so `findWindow` returns an + /// arbitrary tab's window; anchoring a connection-scoped sheet there would + /// switch the selected tab. Preferring the key window keeps the user on the + /// tab they triggered the action from. + internal func activeWindow(for connectionId: UUID, preferring candidate: NSWindow?) -> NSWindow? { + if let candidate, self.connectionId(forWindow: candidate) == connectionId { + return candidate + } + return findWindow(for: connectionId) + } + /// Look up the connectionId for a given windowId. internal func connectionId(for windowId: UUID) -> UUID? { purgeStaleEntries() diff --git a/TableProTests/Core/Services/WindowLifecycleMonitorTests.swift b/TableProTests/Core/Services/WindowLifecycleMonitorTests.swift index 4c5012263..98161e25d 100644 --- a/TableProTests/Core/Services/WindowLifecycleMonitorTests.swift +++ b/TableProTests/Core/Services/WindowLifecycleMonitorTests.swift @@ -167,6 +167,48 @@ struct WindowLifecycleMonitorTests { #expect(monitor.findWindow(for: UUID()) == nil) } + // MARK: - activeWindow + + @Test("activeWindow prefers the candidate when it belongs to the connection") + func activeWindowPrefersCandidate() { + let windowId = UUID() + let connectionId = UUID() + let window = NSWindow() + + monitor.register(window: window, connectionId: connectionId, windowId: windowId) + defer { cleanup(windowId) } + + #expect(monitor.activeWindow(for: connectionId, preferring: window) === window) + } + + @Test("activeWindow ignores a candidate from a different connection") + func activeWindowIgnoresForeignCandidate() { + let windowIdA = UUID() + let windowIdB = UUID() + let connectionA = UUID() + let connectionB = UUID() + let windowA = NSWindow() + let windowB = NSWindow() + + monitor.register(window: windowA, connectionId: connectionA, windowId: windowIdA) + monitor.register(window: windowB, connectionId: connectionB, windowId: windowIdB) + defer { cleanup(windowIdA, windowIdB) } + + #expect(monitor.activeWindow(for: connectionA, preferring: windowB) !== windowB) + } + + @Test("activeWindow falls back to findWindow when the candidate is nil") + func activeWindowNilCandidateFallsBack() { + let windowId = UUID() + let connectionId = UUID() + let window = NSWindow() + + monitor.register(window: window, connectionId: connectionId, windowId: windowId) + defer { cleanup(windowId) } + + #expect(monitor.activeWindow(for: connectionId, preferring: nil) === monitor.findWindow(for: connectionId)) + } + // MARK: - windows(for:) empty for unknown @Test("windows(for:) returns empty array for unknown connectionId")