Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions TablePro/Core/Services/Infrastructure/WindowLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
42 changes: 42 additions & 0 deletions TableProTests/Core/Services/WindowLifecycleMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading