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 @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use sheet presentation for all file open/save panels instead of free-floating dialogs
- Replace event monitor with native SwiftUI .onKeyPress() in connection switcher
- Extract reusable SearchFieldView component from 4 custom search field implementations
- Replace timing hacks with structured signals (polling loops, arbitrary delays)

### Changed

Expand Down
16 changes: 12 additions & 4 deletions TablePro/AppDelegate+ConnectionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,18 @@ extension AppDelegate {
Task { @MainActor [weak self] in
defer { self?.isProcessingQueuedURLs = false }

var ready = false
for _ in 0..<25 {
if WindowOpener.shared.openWindow != nil { ready = true; break }
try? await Task.sleep(for: .milliseconds(200))
let ready = await withTaskGroup(of: Bool.self) { group in
group.addTask {
await WindowOpener.shared.waitUntilReady()
return true
}
group.addTask {
try? await Task.sleep(for: .seconds(5))
return false
}
let result = await group.next() ?? false
group.cancelAll()
return result
}
guard let self else { return }
if !ready {
Expand Down
6 changes: 2 additions & 4 deletions TablePro/AppDelegate+FileOpen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,8 @@ extension AppDelegate {

private func handleConnectionShareFile(_ url: URL) {
openWelcomeWindow()
// Delay to ensure WelcomeWindowView's .onReceive is registered after window renders
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
NotificationCenter.default.post(name: .connectionShareFileOpened, object: url)
}
pendingConnectionShareURL = url
NotificationCenter.default.post(name: .connectionShareFileOpened, object: url)
}

// MARK: - Plugin Install
Expand Down
16 changes: 0 additions & 16 deletions TablePro/AppDelegate+WindowConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,6 @@ extension AppDelegate {
NotificationCenter.default.post(name: .openWelcomeWindow, object: nil)
}

func configureWelcomeWindow() {
Task { @MainActor [weak self] in
for _ in 0 ..< 5 {
guard let self else { return }
let found = NSApp.windows.contains(where: { self.isWelcomeWindow($0) })
if found {
for window in NSApp.windows where self.isWelcomeWindow(window) {
self.configureWelcomeWindowStyle(window)
}
return
}
try? await Task.sleep(for: .milliseconds(50))
}
}
}

private func configureWelcomeWindowStyle(_ window: NSWindow) {
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
Expand Down
5 changes: 3 additions & 2 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
/// Prevents duplicate connections when the same file is opened twice rapidly.
var connectingFilePaths = Set<String>()

/// Connection share file URL pending consumption by WelcomeViewModel.setUp()
var pendingConnectionShareURL: URL?

// MARK: - NSApplicationDelegate

func application(_ application: NSApplication, open urls: [URL]) {
Expand Down Expand Up @@ -105,8 +108,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
_ = QueryHistoryStorage.shared
}

configureWelcomeWindow()

let settings = AppSettingsStorage.shared.loadGeneral()
if settings.startupBehavior == .reopenLast,
let lastConnectionId = AppSettingsStorage.shared.loadLastConnectionId() {
Expand Down
23 changes: 22 additions & 1 deletion TablePro/Core/Services/Infrastructure/WindowOpener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,30 @@ internal final class WindowOpener {

internal static let shared = WindowOpener()

private var readyContinuation: CheckedContinuation<Void, Never>?

/// Set on appear by ContentView, WelcomeViewModel, or ConnectionFormView.
/// Safe to store — OpenWindowAction is app-scoped, not view-scoped.
internal var openWindow: OpenWindowAction?
internal var openWindow: OpenWindowAction? {
didSet {
if openWindow != nil {
readyContinuation?.resume()
readyContinuation = nil
}
}
}

/// Suspends until openWindow is set. Returns immediately if already available.
internal func waitUntilReady() async {
if openWindow != nil { return }
await withCheckedContinuation { continuation in
if openWindow != nil {
continuation.resume()
} else {
readyContinuation = continuation
}
}
}

/// Ordered queue of pending payloads — windows requested via openNativeTab
/// but not yet acknowledged by MainContentView.configureWindow.
Expand Down
32 changes: 24 additions & 8 deletions TablePro/ViewModels/WelcomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ final class WelcomeViewModel {

loadConnections()
linkedConnections = LinkedFolderWatcher.shared.linkedConnections

if let appDelegate = NSApp.delegate as? AppDelegate,
let pendingURL = appDelegate.pendingConnectionShareURL {
appDelegate.pendingConnectionShareURL = nil
activeSheet = .importFile(pendingURL)
}
}

deinit {
Expand Down Expand Up @@ -504,15 +510,25 @@ final class WelcomeViewModel {
}

func focusConnectionFormWindow() {
if let window = NSApp.windows.first(where: { $0.identifier?.rawValue == "connection-form" }) {
window.makeKeyAndOrderFront(nil)
return
}

var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: nil,
queue: .main
) { notification in
guard let window = notification.object as? NSWindow,
window.identifier?.rawValue == "connection-form" else { return }
if let observer { NotificationCenter.default.removeObserver(observer) }
}

Task { @MainActor in
for _ in 0..<10 {
for window in NSApp.windows where
window.identifier?.rawValue == "connection-form" {
window.makeKeyAndOrderFront(nil)
return
}
try? await Task.sleep(for: .milliseconds(20))
}
try? await Task.sleep(for: .milliseconds(500))
if let observer { NotificationCenter.default.removeObserver(observer) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,10 @@ extension MainContentCoordinator {
) {
let isNonSQL = PluginManager.shared.editorLanguage(for: connectionType) != .sql

// Phase 2a: Exact row count
// Phase 2a: Exact row count (background priority to let Phase 1 render first)
// Redis/non-SQL drivers don't support SELECT COUNT(*); use approximate count instead.
Task { [weak self] in
Task(priority: .background) { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: 200_000_000)
guard !self.isTearingDown else { return }
guard let mainDriver = DatabaseManager.shared.driver(for: connectionId) else { return }

Expand Down Expand Up @@ -289,9 +288,8 @@ extension MainContentCoordinator {
// Phase 2b: Fetch enum/set values (not applicable for non-SQL databases)
guard !isNonSQL else { return }
guard let enumDriver = DatabaseManager.shared.driver(for: connectionId) else { return }
Task { [weak self] in
Task(priority: .background) { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: 200_000_000)
guard !self.isTearingDown else { return }

// Use schema if available, otherwise fetch column info for enum parsing
Expand Down
3 changes: 1 addition & 2 deletions TablePro/Views/Main/MainContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,7 @@ struct MainContentView: View {
.onChange(of: tabManager.selectedTabId) { _, newTabId in
pendingTabSwitch?.cancel()
pendingTabSwitch = Task { @MainActor in
// Let other onChange handlers (tabs, resultColumns) settle first
try? await Task.sleep(for: .milliseconds(16))
await Task.yield()
guard !Task.isCancelled else { return }
handleTabSelectionChange(from: previousSelectedTabId, to: newTabId)
previousSelectedTabId = newTabId
Expand Down
Loading