Skip to content

Commit d4eaff5

Browse files
committed
fix: correct window management and event handling patterns
- VIEW-1.1 (high): replace title-matching window discovery with WindowAccessor NSViewRepresentable - VIEW-1.2 (high): replace all NSApp.keyWindow (22 occurrences) with coordinator.window - VIEW-8.2 (medium): extract onAppear into coordinator.configureWindow/registerWindowLifecycle - VIEW-6.1 (high): replace NSEvent.addLocalMonitorForEvents with .onKeyPress in ConnectionSwitcherPopover - APP-2.3 (high): remove openWindow from WindowOpener singleton, use NotificationCenter - MODEL-1.2 (high): simplify QueryTab equality to id-based, add contentHash for change detection - MODEL-1.8 (medium): replace UUID() on ColumnInfo/IndexInfo/ForeignKeyInfo with deterministic ids
1 parent f597c50 commit d4eaff5

25 files changed

+20924
-20901
lines changed

TablePro/AppDelegate+ConnectionHandler.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension AppDelegate {
2323
// MARK: - Database URL Handler
2424

2525
func handleDatabaseURL(_ url: URL) {
26-
guard WindowOpener.shared.openWindow != nil else {
26+
guard WindowOpener.shared.isReady else {
2727
queuedURLEntries.append(.databaseURL(url))
2828
scheduleQueuedURLProcessing()
2929
return
@@ -86,7 +86,7 @@ extension AppDelegate {
8686
// MARK: - SQLite File Handler
8787

8888
func handleSQLiteFile(_ url: URL) {
89-
guard WindowOpener.shared.openWindow != nil else {
89+
guard WindowOpener.shared.isReady else {
9090
queuedURLEntries.append(.sqliteFile(url))
9191
scheduleQueuedURLProcessing()
9292
return
@@ -131,7 +131,7 @@ extension AppDelegate {
131131
// MARK: - DuckDB File Handler
132132

133133
func handleDuckDBFile(_ url: URL) {
134-
guard WindowOpener.shared.openWindow != nil else {
134+
guard WindowOpener.shared.isReady else {
135135
queuedURLEntries.append(.duckdbFile(url))
136136
scheduleQueuedURLProcessing()
137137
return
@@ -176,7 +176,7 @@ extension AppDelegate {
176176
// MARK: - Generic Database File Handler
177177

178178
func handleGenericDatabaseFile(_ url: URL, type dbType: DatabaseType) {
179-
guard WindowOpener.shared.openWindow != nil else {
179+
guard WindowOpener.shared.isReady else {
180180
queuedURLEntries.append(.genericDatabaseFile(url, dbType))
181181
scheduleQueuedURLProcessing()
182182
return
@@ -229,7 +229,7 @@ extension AppDelegate {
229229

230230
var ready = false
231231
for _ in 0..<25 {
232-
if WindowOpener.shared.openWindow != nil { ready = true; break }
232+
if WindowOpener.shared.isReady { ready = true; break }
233233
try? await Task.sleep(for: .milliseconds(200))
234234
}
235235
guard let self else { return }

TablePro/AppDelegate+FileOpen.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,7 @@ extension AppDelegate {
193193
ConnectionStorage.shared.addConnection(connection)
194194
NotificationCenter.default.post(name: .connectionUpdated, object: nil)
195195

196-
if let openWindow = WindowOpener.shared.openWindow {
197-
openWindow(id: "connection-form", value: connection.id)
198-
}
196+
NotificationCenter.default.post(name: .openConnectionFormWindow, object: connection.id)
199197
}
200198

201199
// MARK: - Plugin Install

TablePro/Core/SchemaTracking/StructureChangeManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ final class StructureChangeManager {
7878
self.currentForeignKeys = groupedFKs.keys.sorted().compactMap { name -> EditableForeignKeyDefinition? in
7979
guard let fkInfos = groupedFKs[name], let first = fkInfos.first else { return nil }
8080
return EditableForeignKeyDefinition(
81-
id: first.id,
81+
id: UUID(),
8282
name: first.name,
8383
columns: fkInfos.map { $0.column },
8484
referencedTable: first.referencedTable,

TablePro/Core/Services/Infrastructure/WindowOpener.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,20 @@ internal final class WindowOpener {
1515

1616
internal static let shared = WindowOpener()
1717

18-
/// Set by ContentView when it appears. Safe to store — OpenWindowAction is app-scoped, not view-scoped.
19-
internal var openWindow: OpenWindowAction?
18+
/// True once any SwiftUI scene has appeared and stored `openWindow`.
19+
/// Used as a readiness check by AppDelegate cold-start queue.
20+
internal var isReady: Bool = false
2021

2122
/// The connectionId for the next window about to be opened.
2223
/// Set by `openNativeTab` before calling `openWindow`, consumed by
2324
/// `AppDelegate.windowDidBecomeKey` to set the correct `tabbingIdentifier`.
2425
internal var pendingConnectionId: UUID?
2526

26-
/// Opens a new native window tab with the given payload.
27-
/// Stores the connectionId so AppDelegate can set the correct tabbingIdentifier.
27+
/// Opens a new native window tab by posting a notification.
28+
/// The `OpenWindowHandler` in TableProApp receives it and calls `openWindow`.
2829
internal func openNativeTab(_ payload: EditorTabPayload) {
2930
pendingConnectionId = payload.connectionId
30-
guard let openWindow else {
31-
Self.logger.warning("openNativeTab called before openWindow was set — payload dropped")
32-
return
33-
}
34-
openWindow(id: "main", value: payload)
31+
NotificationCenter.default.post(name: .openMainWindow, object: payload)
3532
}
3633

3734
/// Returns and clears the pending connectionId (consume-once pattern).

TablePro/Models/Query/QueryResult.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ struct TableInfo: Identifiable, Hashable {
113113

114114
/// Information about a table column
115115
struct ColumnInfo: Identifiable, Hashable {
116-
let id = UUID()
116+
var id: String { name }
117117
let name: String
118118
let dataType: String
119119
let isNullable: Bool
@@ -127,7 +127,7 @@ struct ColumnInfo: Identifiable, Hashable {
127127

128128
/// Information about a table index
129129
struct IndexInfo: Identifiable, Hashable {
130-
let id = UUID()
130+
var id: String { "\(name)_\(columns.joined(separator: ","))" }
131131
let name: String
132132
let columns: [String]
133133
let isUnique: Bool
@@ -137,7 +137,7 @@ struct IndexInfo: Identifiable, Hashable {
137137

138138
/// Information about a foreign key relationship
139139
struct ForeignKeyInfo: Identifiable, Hashable {
140-
let id = UUID()
140+
var id: String { "\(column)_\(referencedTable)_\(referencedColumn)" }
141141
let name: String
142142
let column: String
143143
let referencedTable: String

TablePro/Models/Query/QueryTab.swift

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,10 @@ struct QueryTab: Identifiable, Equatable {
279279
var tabType: TabType
280280

281281
// Results — stored in a reference-type buffer to avoid CoW duplication
282-
// of large data when the struct is mutated (MEM-1 fix)
282+
// of large data when the struct is mutated (MEM-1 fix).
283+
// Note: When QueryTab is copied (struct CoW), copies share the same RowBuffer
284+
// instance. This is intentional — RowBuffer is a reference type specifically to
285+
// avoid duplicating large result arrays on every struct mutation.
283286
var rowBuffer: RowBuffer
284287

285288
// Backward-compatible computed accessors for result data
@@ -492,22 +495,35 @@ struct QueryTab: Identifiable, Equatable {
492495
)
493496
}
494497

498+
// Identity-based equality: two QueryTabs are equal if they represent the same tab.
499+
// Content changes (query text, results, filters, etc.) are tracked via resultVersion,
500+
// metadataVersion, and SwiftUI's observation system — not via Equatable.
495501
static func == (lhs: QueryTab, rhs: QueryTab) -> Bool {
496502
lhs.id == rhs.id
497-
&& lhs.title == rhs.title
498-
&& lhs.isExecuting == rhs.isExecuting
499-
&& lhs.errorMessage == rhs.errorMessage
500-
&& lhs.executionTime == rhs.executionTime
501-
&& lhs.resultVersion == rhs.resultVersion
502-
&& lhs.pagination == rhs.pagination
503-
&& lhs.sortState == rhs.sortState
504-
&& lhs.showStructure == rhs.showStructure
505-
&& lhs.isEditable == rhs.isEditable
506-
&& lhs.isView == rhs.isView
507-
&& lhs.tabType == rhs.tabType
508-
&& lhs.rowsAffected == rhs.rowsAffected
509-
&& lhs.isPreview == rhs.isPreview
510-
&& lhs.hasUserInteraction == rhs.hasUserInteraction
503+
}
504+
505+
/// Hash of content fields that matter for change detection.
506+
/// Use this when you need to know if a tab's visible state has changed
507+
/// (e.g., for caching or diff purposes), rather than relying on Equatable.
508+
var contentHash: Int {
509+
var hasher = Hasher()
510+
hasher.combine(id)
511+
hasher.combine(title)
512+
hasher.combine(query)
513+
hasher.combine(tableName)
514+
hasher.combine(isExecuting)
515+
hasher.combine(errorMessage)
516+
hasher.combine(executionTime)
517+
hasher.combine(resultVersion)
518+
hasher.combine(metadataVersion)
519+
hasher.combine(showStructure)
520+
hasher.combine(isEditable)
521+
hasher.combine(isView)
522+
hasher.combine(tabType)
523+
hasher.combine(rowsAffected)
524+
hasher.combine(isPreview)
525+
hasher.combine(hasUserInteraction)
526+
return hasher.finalize()
511527
}
512528
}
513529

TablePro/Models/Schema/ColumnDefinition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ struct EditableColumnDefinition: Hashable, Codable, Identifiable {
5252
/// Create from existing ColumnInfo
5353
static func from(_ columnInfo: ColumnInfo) -> EditableColumnDefinition {
5454
EditableColumnDefinition(
55-
id: columnInfo.id,
55+
id: UUID(),
5656
name: columnInfo.name,
5757
dataType: columnInfo.dataType,
5858
isNullable: columnInfo.isNullable,

TablePro/Models/Schema/ForeignKeyDefinition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable {
4949
/// Create from existing ForeignKeyInfo
5050
static func from(_ fkInfo: ForeignKeyInfo) -> EditableForeignKeyDefinition {
5151
EditableForeignKeyDefinition(
52-
id: fkInfo.id,
52+
id: UUID(),
5353
name: fkInfo.name,
5454
columns: [fkInfo.column],
5555
referencedTable: fkInfo.referencedTable,

TablePro/Models/Schema/IndexDefinition.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct EditableIndexDefinition: Hashable, Codable, Identifiable {
4949
/// Create from existing IndexInfo
5050
static func from(_ indexInfo: IndexInfo) -> EditableIndexDefinition {
5151
EditableIndexDefinition(
52-
id: indexInfo.id,
52+
id: UUID(),
5353
name: indexInfo.name,
5454
columns: indexInfo.columns,
5555
type: IndexType(rawValue: indexInfo.type.uppercased()) ?? .btree,

0 commit comments

Comments
 (0)