From 0da8bb12c873c791a960dc7d28b6d3eb8c68097f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 24 Dec 2025 17:06:05 +0700 Subject: [PATCH 1/6] wip --- .../UserInterfaceState.xcuserstate | Bin 168242 -> 171022 bytes OpenTable/AppDelegate.swift | 19 +- OpenTable/Core/Storage/TabStateStorage.swift | 16 + OpenTable/OpenTableApp.swift | 3 + OpenTable/Views/MainContentView.swift | 285 ++++++++++++++++-- 5 files changed, 285 insertions(+), 38 deletions(-) diff --git a/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate b/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate index 589dc62cfff8a67d54fd143f4877c16fb8aeb70f..8e43b3f658ec615b53b1f8456f8eab2b7a0ba542 100644 GIT binary patch literal 171022 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 @State private var isRestoringTabs = false // Prevent circular sync during restoration + @State private var needsLazyLoad = false // Flag to trigger lazy load when connection becomes ready + @State private var saveDebounceTask: Task? // Debounce task for saving tabs + @State private var isDismissing = false // Prevent saving when view is being destroyed + @State private var justRestoredTab = false // Prevent lazy load duplicate execution after restore // Error alert state @State private var showErrorAlert = false @@ -201,23 +205,53 @@ struct MainContentView: View { // Dismiss all autocomplete windows to prevent duplicates NotificationCenter.default.post(name: NSNotification.Name("QueryTabDidChange"), object: nil) + // Skip save during restoration + guard !isRestoringTabs else { return } + + // CRITICAL: Skip save if view is being dismissed + guard !isDismissing else { + print("[MainContentView] Skipping selectedTabId save - view is being dismissed") + return + } + // Sync selected tab ID to session for persistence if let sessionId = DatabaseManager.shared.currentSessionId { DatabaseManager.shared.updateSession(sessionId) { session in session.selectedTabId = newTabId } + + // CRITICAL: Also persist to disk for restoration + TabStateStorage.shared.saveTabState( + connectionId: connection.id, + tabs: tabManager.tabs, + selectedTabId: newTabId + ) } } .onChange(of: tabManager.tabs) { _, newTabs in // Skip sync if we're currently restoring tabs from session (prevents circular updates) guard !isRestoringTabs else { return } + // CRITICAL: Skip save if view is being dismissed to prevent saving empty query + // When SwiftUI tears down the view, bindings may be reset causing empty saves + guard !isDismissing else { + print("[MainContentView] Skipping save - view is being dismissed") + return + } + // Sync tabs array to session for persistence if let sessionId = DatabaseManager.shared.currentSessionId { DatabaseManager.shared.updateSession(sessionId) { session in session.tabs = newTabs } + // CRITICAL: Persist tabs to disk so they can be restored when connection reopens + TabStateStorage.shared.saveTabState( + connectionId: connection.id, + tabs: newTabs, + selectedTabId: tabManager.selectedTabId + ) + // Clear saved state immediately when all tabs are closed if newTabs.isEmpty { TabStateStorage.shared.clearTabState(connectionId: connection.id) @@ -225,9 +259,7 @@ struct MainContentView: View { } } .onChange(of: currentTab?.resultColumns) { _, newColumns in - Task { @MainActor in - handleColumnsChange(newColumns: newColumns) - } + handleColumnsChange(newColumns: newColumns) } .onChange(of: currentTab?.errorMessage) { _, newError in // Show error alert when errorMessage is set @@ -236,6 +268,14 @@ struct MainContentView: View { showErrorAlert = true } } + .onChange(of: DatabaseManager.shared.currentSession?.isConnected) { _, isConnected in + // Auto-execute query when connection becomes ready and tab needs data + if isConnected == true && needsLazyLoad { + print("[MainContentView] Connection ready - executing lazy load") + needsLazyLoad = false + runQuery() + } + } } /// Separated to reduce type-checker complexity @@ -302,6 +342,27 @@ struct MainContentView: View { redoLastChange() } } + .onReceive(NotificationCenter.default.publisher(for: .mainWindowWillClose)) { _ in + // CRITICAL: Window is about to close - flush pending saves immediately + // This prevents query text from being lost when SwiftUI tears down the view + print("[MainContentView] Window will close - flushing pending saves") + + // Set flag to prevent further saves (view is being destroyed) + isDismissing = true + + // Cancel debounce task and save immediately + saveDebounceTask?.cancel() + + // Immediately save current state before view is destroyed + if let sessionId = DatabaseManager.shared.currentSessionId { + TabStateStorage.shared.saveTabState( + connectionId: connection.id, + tabs: tabManager.tabs, + selectedTabId: tabManager.selectedTabId + ) + print("[MainContentView] Flushed tab state for connection \(connection.id)") + } + } } /// First part of notifications - reduces type-checker complexity @@ -311,16 +372,81 @@ struct MainContentView: View { .task { await initializeView() - // Restore tabs from session if available (after DatabaseManager has loaded them) - if let sessionId = DatabaseManager.shared.currentSessionId, - let session = DatabaseManager.shared.activeSessions[sessionId], - !session.tabs.isEmpty { - // Set flag to prevent onChange(tabManager.tabs) from syncing back - // Use defer to ensure flag is always reset even if an error occurs + // Restore tabs from disk first (persists across app restarts) + // Fallback to session tabs (persists during app session only) + var didRestoreTabs = false + if let savedState = TabStateStorage.shared.loadTabState(connectionId: connection.id), + !savedState.tabs.isEmpty { + // Restore from disk isRestoringTabs = true defer { isRestoringTabs = false } + + let restoredTabs = savedState.tabs.map { QueryTab(from: $0) } + tabManager.tabs = restoredTabs + tabManager.selectedTabId = savedState.selectedTabId + didRestoreTabs = true + + print("[MainContentView] Restored \(restoredTabs.count) tabs from disk") + // Debug: Print query text to verify restoration + for (index, tab) in restoredTabs.enumerated() { + print("[MainContentView] Tab \(index): \"\(tab.title)\" - Query: \(tab.query.prefix(50))...") + } + } else if let sessionId = DatabaseManager.shared.currentSessionId, + let session = DatabaseManager.shared.activeSessions[sessionId], + !session.tabs.isEmpty { + // Fallback: Restore from session (for backward compatibility) + isRestoringTabs = true + defer { isRestoringTabs = false } + tabManager.tabs = session.tabs tabManager.selectedTabId = session.selectedTabId + didRestoreTabs = true + + print("[MainContentView] Restored \(session.tabs.count) tabs from session") + } + // CRITICAL: Execute query for table tabs to load data + // Query tabs only restore text without auto-execution + if didRestoreTabs { + if let selectedTab = tabManager.selectedTab { + print("[MainContentView] Restored tab: \(selectedTab.title), type: \(selectedTab.tabType), hasQuery: \(!selectedTab.query.isEmpty)") + + if selectedTab.tabType == .table, + !selectedTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + print("[MainContentView] Waiting for connection to be ready...") + + // CRITICAL: Wait for connection to be established + // Without this, query fails with "Not connected to database" + var retryCount = 0 + while retryCount < 50 { // Max 5 seconds + if let session = DatabaseManager.shared.currentSession, + session.isConnected { + print("[MainContentView] Connection ready! Executing query for restored table tab: \(selectedTab.title)") + + // Small delay to ensure everything is initialized + try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s + await MainActor.run { + justRestoredTab = true // Prevent lazy load from executing again + runQuery() + } + break + } + + // Wait 100ms and retry + try? await Task.sleep(nanoseconds: 100_000_000) + retryCount += 1 + } + + if retryCount >= 50 { + print("[MainContentView] Warning: Connection timeout, query not executed") + } + } else if selectedTab.tabType == .query { + print("[MainContentView] Restored query tab - skipping auto-execution") + } else { + print("[MainContentView] Restored table tab but no query - skipping auto-execution") + } + } else { + print("[MainContentView] No selected tab after restore") + } } } .onChange(of: selectedTables) { oldTables, newTables in @@ -459,11 +585,43 @@ struct MainContentView: View { queryText: Binding( get: { tab.query }, set: { newValue in - if let index = tabManager.selectedTabIndex { - tabManager.tabs[index].query = newValue + // CRITICAL: Bounds check to prevent crash on paste + guard let index = tabManager.selectedTabIndex, + index < tabManager.tabs.count else { + print("[MainContentView] Warning: Invalid tab index during query update") + return + } + + tabManager.tabs[index].query = newValue + + // Save as last query for this connection (TablePlus-style) + TabStateStorage.shared.saveLastQuery(newValue, for: connection.id) + + // CRITICAL: Debounce save to prevent race conditions + // Only save 500ms after user stops typing + // SKIP save during restoration or dismissal to prevent overwriting with empty values + if !isRestoringTabs && !isDismissing { + // Cancel previous debounce task + saveDebounceTask?.cancel() + + // CRITICAL: Capture current tabs STATE to prevent stale data + let tabsToSave = tabManager.tabs + let selectedId = tabManager.selectedTabId - // Save as last query for this connection (TablePlus-style) - TabStateStorage.shared.saveLastQuery(newValue, for: connection.id) + // Create new debounce task + saveDebounceTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s + + // Only save if not cancelled and view not being dismissed + guard !Task.isCancelled && !isDismissing else { return } + + // Save the captured tabs state (NOT current state which may have changed) + TabStateStorage.shared.saveTabState( + connectionId: connection.id, + tabs: tabsToSave, + selectedTabId: selectedId + ) + } } } ), @@ -733,7 +891,10 @@ struct MainContentView: View { } private func runQuery() { - guard let index = tabManager.selectedTabIndex else { return } + guard let index = tabManager.selectedTabIndex else { + print("[MainContentView] ⚠️ runQuery() called but selectedTabIndex is nil!") + return + } // Cancel any previous running query to prevent race conditions // This is critical for SSH connections where rapid sorting can cause @@ -1694,6 +1855,20 @@ struct MainContentView: View { /// Handle tab selection changes private func handleTabChange(oldTabId: UUID?, newTabId: UUID?) { + // CRITICAL: Flush pending debounced save to ensure last edit is saved + // Cancel and immediately execute if pending + if let task = saveDebounceTask, !task.isCancelled { + task.cancel() + // Immediately save current state before switching + if let sessionId = DatabaseManager.shared.currentSessionId, !isRestoringTabs, !isDismissing { + TabStateStorage.shared.saveTabState( + connectionId: connection.id, + tabs: tabManager.tabs, + selectedTabId: tabManager.selectedTabId + ) + } + } + // Save state to the old tab before switching if let oldId = oldTabId, let oldIndex = tabManager.tabs.firstIndex(where: { $0.id == oldId }) @@ -1705,32 +1880,72 @@ struct MainContentView: View { // sortState is already in tab, no need to save from local state } - // Restore state from the new tab + // Restore LIGHTWEIGHT state immediately (synchronous for instant UI update) if let newId = newTabId, let newIndex = tabManager.tabs.firstIndex(where: { $0.id == newId }) { let newTab = tabManager.tabs[newIndex] + print("[MainContentView] Tab change: \(oldTabId == nil ? "nil" : "tab") -> \(newTab.title) (type: \(newTab.tabType))") - // Restore pending changes - if newTab.pendingChanges.hasChanges { - changeManager.restoreState( - from: newTab.pendingChanges, tableName: newTab.tableName ?? "") - } else { - // Clear changeManager for tabs without pending changes (atomically) - changeManager.configureForTable( - tableName: newTab.tableName ?? "", - columns: newTab.resultColumns, - primaryKeyColumn: newTab.resultColumns.first, - databaseType: connection.type - ) - } - - // Restore row selection + // CRITICAL: Update these immediately for UI consistency selectedRowIndices = newTab.selectedRowIndices - // sortState is accessed via binding, no need to restore to local state - - // Update app state for menu item enabled state AppState.shared.isCurrentTabEditable = newTab.isEditable && newTab.tableName != nil + + // DEFER heavy changeManager operations to next run loop + // This prevents blocking the UI thread and gives instant tab switching + Task { @MainActor in + // This runs after SwiftUI updates the view + if newTab.pendingChanges.hasChanges { + changeManager.restoreState( + from: newTab.pendingChanges, tableName: newTab.tableName ?? "") + } else { + // Clear changeManager for tabs without pending changes (atomically) + changeManager.configureForTable( + tableName: newTab.tableName ?? "", + columns: newTab.resultColumns, + primaryKeyColumn: newTab.resultColumns.first, + databaseType: connection.type + ) + } + + // Reset flag BEFORE checking lazy load to ensure it's always reset + // Otherwise, if lazy load is skipped due to flag=true, flag never resets! + let shouldSkipLazyLoad = justRestoredTab + if justRestoredTab { + print("[MainContentView] Resetting justRestoredTab flag") + justRestoredTab = false + } + + // LAZY LOAD: Auto-execute query ONLY for table tabs + // Query tabs require manual execution by user (Cmd+Return) + // Skip if we just restored and executed this tab + + // DEBUG: Log all conditions + print("[MainContentView] Lazy load check for \(newTab.title):") + print(" - shouldSkipLazyLoad: \(shouldSkipLazyLoad)") + print(" - tabType: \(newTab.tabType)") + print(" - resultRows.isEmpty: \(newTab.resultRows.isEmpty)") + print(" - lastExecutedAt == nil: \(newTab.lastExecutedAt == nil)") + print(" - has query: \(!newTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)") + + if !shouldSkipLazyLoad && + newTab.tabType == .table && // Only auto-execute for table tabs + newTab.resultRows.isEmpty && + newTab.lastExecutedAt == nil && + !newTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + + // CRITICAL: Check connection before executing + if let session = DatabaseManager.shared.currentSession, session.isConnected { + print("[MainContentView] Lazy loading data for table tab: \(newTab.title)") + runQuery() + } else { + print("[MainContentView] Table tab needs data but not connected - setting flag") + needsLazyLoad = true + } + } else { + print("[MainContentView] Skipping lazy load (one or more conditions not met)") + } + } } else { // No tab selected AppState.shared.isCurrentTabEditable = false @@ -1746,6 +1961,10 @@ struct MainContentView: View { // Only update if columns have actually changed guard changeManager.columns != newColumns else { return } + + // IMPORTANT: Skip if tableName or columns don't match current tab + // This prevents duplicate updates when switching tabs (handleTabChange already handles it) + guard changeManager.tableName == tab.tableName ?? "" else { return } changeManager.configureForTable( tableName: tab.tableName ?? "", From 467d2f1c3f6268ec92ca9eca6a0fe2f59628012f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 24 Dec 2025 19:35:17 +0700 Subject: [PATCH 2/6] Fix: Table tabs now load data when opened as first tab Resolved race condition where opening a table tab as the first tab (no other tabs open) would not trigger data loading. Root cause: When two async runQuery() calls occurred (one from openTableData, one from lazy load in handleTabChange), the second call would cancel the first task via currentQueryTask?.cancel() before checking the isExecuting guard. This left the first query cancelled without starting a new one. Fix: Moved currentQueryTask?.cancel() after the isExecuting guard check, ensuring we only cancel previous tasks when actually starting a new query. Changes: - Moved task cancellation logic after isExecuting guard in runQuery() - Removed excessive debug logging added during investigation - Kept only critical warning logs for errors --- OpenTable/Views/MainContentView.swift | 110 ++++++++++---------------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/OpenTable/Views/MainContentView.swift b/OpenTable/Views/MainContentView.swift index 0820c064..2884e47d 100644 --- a/OpenTable/Views/MainContentView.swift +++ b/OpenTable/Views/MainContentView.swift @@ -385,12 +385,6 @@ struct MainContentView: View { tabManager.tabs = restoredTabs tabManager.selectedTabId = savedState.selectedTabId didRestoreTabs = true - - print("[MainContentView] Restored \(restoredTabs.count) tabs from disk") - // Debug: Print query text to verify restoration - for (index, tab) in restoredTabs.enumerated() { - print("[MainContentView] Tab \(index): \"\(tab.title)\" - Query: \(tab.query.prefix(50))...") - } } else if let sessionId = DatabaseManager.shared.currentSessionId, let session = DatabaseManager.shared.activeSessions[sessionId], !session.tabs.isEmpty { @@ -401,51 +395,35 @@ struct MainContentView: View { tabManager.tabs = session.tabs tabManager.selectedTabId = session.selectedTabId didRestoreTabs = true - - print("[MainContentView] Restored \(session.tabs.count) tabs from session") } - // CRITICAL: Execute query for table tabs to load data - // Query tabs only restore text without auto-execution + // Execute query for table tabs to load data if didRestoreTabs { - if let selectedTab = tabManager.selectedTab { - print("[MainContentView] Restored tab: \(selectedTab.title), type: \(selectedTab.tabType), hasQuery: \(!selectedTab.query.isEmpty)") + if let selectedTab = tabManager.selectedTab, + selectedTab.tabType == .table, + !selectedTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - if selectedTab.tabType == .table, - !selectedTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - print("[MainContentView] Waiting for connection to be ready...") - - // CRITICAL: Wait for connection to be established - // Without this, query fails with "Not connected to database" - var retryCount = 0 - while retryCount < 50 { // Max 5 seconds - if let session = DatabaseManager.shared.currentSession, - session.isConnected { - print("[MainContentView] Connection ready! Executing query for restored table tab: \(selectedTab.title)") - - // Small delay to ensure everything is initialized - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s - await MainActor.run { - justRestoredTab = true // Prevent lazy load from executing again - runQuery() - } - break + // Wait for connection to be established + var retryCount = 0 + while retryCount < 50 { // Max 5 seconds + if let session = DatabaseManager.shared.currentSession, + session.isConnected { + // Small delay to ensure everything is initialized + try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s + await MainActor.run { + justRestoredTab = true // Prevent lazy load from executing again + runQuery() } - - // Wait 100ms and retry - try? await Task.sleep(nanoseconds: 100_000_000) - retryCount += 1 + break } - if retryCount >= 50 { - print("[MainContentView] Warning: Connection timeout, query not executed") - } - } else if selectedTab.tabType == .query { - print("[MainContentView] Restored query tab - skipping auto-execution") - } else { - print("[MainContentView] Restored table tab but no query - skipping auto-execution") + // Wait 100ms and retry + try? await Task.sleep(nanoseconds: 100_000_000) + retryCount += 1 + } + + if retryCount >= 50 { + print("[MainContentView] ⚠️ Connection timeout, query not executed") } - } else { - print("[MainContentView] No selected tab after restore") } } } @@ -896,17 +874,19 @@ struct MainContentView: View { return } + guard !tabManager.tabs[index].isExecuting else { + return + } + // Cancel any previous running query to prevent race conditions - // This is critical for SSH connections where rapid sorting can cause - // multiple queries to return out of order, leading to EXC_BAD_ACCESS + // IMPORTANT: Only cancel AFTER checking isExecuting, otherwise we cancel + // a valid running query without starting a new one currentQueryTask?.cancel() // Increment generation - any query with a different generation will be ignored queryGeneration += 1 let capturedGeneration = queryGeneration - guard !tabManager.tabs[index].isExecuting else { return } - tabManager.tabs[index].isExecuting = true tabManager.tabs[index].executionTime = nil tabManager.tabs[index].errorMessage = nil @@ -1019,13 +999,16 @@ struct MainContentView: View { // Find tab by ID (index may have changed) - must update on main thread await MainActor.run { + // ALWAYS update toolbar state first - user should see query completion toolbarState.isExecuting = false toolbarState.lastQueryDuration = safeExecutionTime // Only update tab if this is still the most recent query // This prevents race conditions when navigating quickly between tables - guard capturedGeneration == queryGeneration else { return } + guard capturedGeneration == queryGeneration else { + return + } guard !Task.isCancelled else { return } if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { @@ -1885,7 +1868,6 @@ struct MainContentView: View { let newIndex = tabManager.tabs.firstIndex(where: { $0.id == newId }) { let newTab = tabManager.tabs[newIndex] - print("[MainContentView] Tab change: \(oldTabId == nil ? "nil" : "tab") -> \(newTab.title) (type: \(newTab.tabType))") // CRITICAL: Update these immediately for UI consistency selectedRowIndices = newTab.selectedRowIndices @@ -1912,21 +1894,9 @@ struct MainContentView: View { // Otherwise, if lazy load is skipped due to flag=true, flag never resets! let shouldSkipLazyLoad = justRestoredTab if justRestoredTab { - print("[MainContentView] Resetting justRestoredTab flag") justRestoredTab = false } - // LAZY LOAD: Auto-execute query ONLY for table tabs - // Query tabs require manual execution by user (Cmd+Return) - // Skip if we just restored and executed this tab - - // DEBUG: Log all conditions - print("[MainContentView] Lazy load check for \(newTab.title):") - print(" - shouldSkipLazyLoad: \(shouldSkipLazyLoad)") - print(" - tabType: \(newTab.tabType)") - print(" - resultRows.isEmpty: \(newTab.resultRows.isEmpty)") - print(" - lastExecutedAt == nil: \(newTab.lastExecutedAt == nil)") - print(" - has query: \(!newTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)") if !shouldSkipLazyLoad && newTab.tabType == .table && // Only auto-execute for table tabs @@ -1934,16 +1904,12 @@ struct MainContentView: View { newTab.lastExecutedAt == nil && !newTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - // CRITICAL: Check connection before executing + // Check connection before executing if let session = DatabaseManager.shared.currentSession, session.isConnected { - print("[MainContentView] Lazy loading data for table tab: \(newTab.title)") runQuery() } else { - print("[MainContentView] Table tab needs data but not connected - setting flag") needsLazyLoad = true } - } else { - print("[MainContentView] Skipping lazy load (one or more conditions not met)") } } } else { @@ -2280,7 +2246,15 @@ struct MainContentView: View { // For existing tabs, onChange will restore their saved selection if needsQuery { selectedRowIndices = [] - runQuery() + + // Execute query for new/replaced tabs + // IMPORTANT: Wrapped in Task to ensure SwiftUI processes tab property updates first + // - For NEW tabs: selectedTabId changes → onChange fires → lazy load also triggers + // (both will try to run query, but the second will be blocked by isExecuting guard) + // - For REPLACED tabs: selectedTabId stays same → onChange doesn't fire → we MUST call runQuery + Task { @MainActor in + runQuery() + } } } From 53f1ac7ceb909a12267afe882ba0495072b22056 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 24 Dec 2025 19:38:42 +0700 Subject: [PATCH 3/6] chore: Remove verbose debug logging Removed debug print statements from: - TabStateStorage: tab saving/loading logs - MainContentView: window close, connection ready, query update logs Kept only critical error/warning logs. --- .../UserInterfaceState.xcuserstate | Bin 171022 -> 172876 bytes OpenTable/Core/Storage/TabStateStorage.swift | 24 ++---------------- OpenTable/Views/MainContentView.swift | 13 +++------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate b/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate index 8e43b3f658ec615b53b1f8456f8eab2b7a0ba542..4e139f6638fb0b2684adc6a72d905f4c75af0534 100644 GIT binary patch literal 172876 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 Date: Wed, 24 Dec 2025 23:06:39 +0700 Subject: [PATCH 4/6] refactor: Address Copilot code quality feedback Improvements: - Extract magic numbers to named constants (tab save delay, connection check delay) - Clear currentQueryTask reference on completion to avoid stale references - Add isDismissing check in retry loop to prevent queries after view dismissal - Simplify lazy load logic (remove redundant if check) - Remove extra blank lines for consistency These changes improve code maintainability and prevent potential edge case bugs. --- OpenTable/Views/MainContentView.swift | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/OpenTable/Views/MainContentView.swift b/OpenTable/Views/MainContentView.swift index 488bebb5..96ad1ef6 100644 --- a/OpenTable/Views/MainContentView.swift +++ b/OpenTable/Views/MainContentView.swift @@ -43,6 +43,12 @@ struct MainContentView: View { @State private var saveDebounceTask: Task? // Debounce task for saving tabs @State private var isDismissing = false // Prevent saving when view is being destroyed @State private var justRestoredTab = false // Prevent lazy load duplicate execution after restore + + // MARK: - Constants + + private static let tabSaveDebounceDelay: UInt64 = 500_000_000 // 500ms in nanoseconds + private static let connectionCheckDelay: UInt64 = 100_000_000 // 100ms in nanoseconds + private static let maxConnectionRetries = 50 // Max retries for connection check (5 seconds total) // Error alert state @State private var showErrorAlert = false @@ -399,11 +405,14 @@ struct MainContentView: View { // Wait for connection to be established var retryCount = 0 - while retryCount < 50 { // Max 5 seconds + while retryCount < Self.maxConnectionRetries { + // Stop waiting if view is being dismissed + guard !isDismissing else { break } + if let session = DatabaseManager.shared.currentSession, session.isConnected { // Small delay to ensure everything is initialized - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s + try? await Task.sleep(nanoseconds: Self.connectionCheckDelay) await MainActor.run { justRestoredTab = true // Prevent lazy load from executing again runQuery() @@ -992,6 +1001,8 @@ struct MainContentView: View { // Find tab by ID (index may have changed) - must update on main thread await MainActor.run { + // Clear task reference to avoid stale references + currentQueryTask = nil // ALWAYS update toolbar state first - user should see query completion toolbarState.isExecuting = false @@ -1053,6 +1064,9 @@ struct MainContentView: View { // MUST run on MainActor for SwiftUI onChange to fire await MainActor.run { + // Clear task reference + currentQueryTask = nil + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { tabManager.tabs[idx].errorMessage = error.localizedDescription tabManager.tabs[idx].isExecuting = false @@ -1886,17 +1900,13 @@ struct MainContentView: View { // Reset flag BEFORE checking lazy load to ensure it's always reset // Otherwise, if lazy load is skipped due to flag=true, flag never resets! let shouldSkipLazyLoad = justRestoredTab - if justRestoredTab { - justRestoredTab = false - } - + justRestoredTab = false if !shouldSkipLazyLoad && newTab.tabType == .table && // Only auto-execute for table tabs newTab.resultRows.isEmpty && newTab.lastExecutedAt == nil && !newTab.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - // Check connection before executing if let session = DatabaseManager.shared.currentSession, session.isConnected { runQuery() From bbd852666a74f02431749c5fc2e7ec4394308692 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 24 Dec 2025 23:21:44 +0700 Subject: [PATCH 5/6] wip --- .../UserInterfaceState.xcuserstate | Bin 172876 -> 172842 bytes OpenTable/AppDelegate.swift | 9 +- OpenTable/Views/MainContentView.swift | 130 +++++++++--------- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate b/OpenTable.xcodeproj/project.xcworkspace/xcuserdata/ngoquocdat.xcuserdatad/UserInterfaceState.xcuserstate index 4e139f6638fb0b2684adc6a72d905f4c75af0534..039de63fccca18b3e4c11e8d70cd81d8f9fd869b 100644 GIT binary patch literal 172842 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 50 { + + if retryCount >= Self.maxConnectionRetries { print("[MainContentView] ⚠️ Connection timeout, query not executed") } } @@ -462,7 +517,7 @@ struct MainContentView: View { // Load query from history/bookmark panel into current tab Task { @MainActor in guard let query = notification.object as? String else { return } - + // Load into the current tab (which was just created by .newTab) if let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count { @@ -493,67 +548,18 @@ struct MainContentView: View { } else { // Cancel any running query to prevent race conditions currentQueryTask?.cancel() - + // Rebuild query for table tabs to ensure fresh data if let tabIndex = tabManager.selectedTabIndex, tabManager.tabs[tabIndex].tabType == .table { rebuildTableQuery(at: tabIndex) } - + // Fetch fresh data from database runQuery() } } } - .onReceive(NotificationCenter.default.publisher(for: .deleteSelectedRows)) { _ in - // Delete rows or mark table for deletion - Task { @MainActor in - // First check if we have row selection in data grid - if !selectedRowIndices.isEmpty { - deleteSelectedRows() - } - // Otherwise check if tables are selected in sidebar - else if !selectedTables.isEmpty { - // Batch update to avoid stale copy issues with @Binding - var updatedDeletes = pendingDeletes - var updatedTruncates = pendingTruncates - - for table in selectedTables { - updatedTruncates.remove(table.name) - if updatedDeletes.contains(table.name) { - updatedDeletes.remove(table.name) - } else { - updatedDeletes.insert(table.name) - } - } - - pendingTruncates = updatedTruncates - pendingDeletes = updatedDeletes - } - } - } - .onReceive(NotificationCenter.default.publisher(for: .databaseDidConnect)) { _ in - // Load schema and update toolbar when connection is established (fixes race condition) - Task { @MainActor in - await loadSchema() - // Update version after connection is fully established - if let driver = DatabaseManager.shared.activeDriver { - toolbarState.databaseVersion = driver.serverVersion - } - } - } - .onReceive(NotificationCenter.default.publisher(for: .showAllTables)) { _ in - // Show all tables metadata when user clicks "Tables" heading in sidebar - Task { @MainActor in - showAllTablesMetadata() - } - } - .onReceive(NotificationCenter.default.publisher(for: .addNewRow)) { _ in - // Add row menu item (Cmd+I) - Task { @MainActor in - addNewRow() - } - } } // MARK: - Query Tab Content @@ -590,7 +596,7 @@ struct MainContentView: View { // Create new debounce task saveDebounceTask = Task { @MainActor in - try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s + try? await Task.sleep(nanoseconds: Self.tabSaveDebounceDelay) // Only save if not cancelled and view not being dismissed guard !Task.isCancelled && !isDismissing else { return } From 6bdf01b41f7ddd824df78927c7304a1e86def4d9 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 25 Dec 2025 08:36:30 +0700 Subject: [PATCH 6/6] refactor: Extract services and child views from complex files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactoring to improve code organization and maintainability: Phase 1 - DataChange.swift (1,095 → 4 files): - SQLStatementGenerator: SQL generation logic - DataChangeUndoManager: Undo/redo stack management - DataChangeModels: Pure data models - DataChangeManager: Refactored change tracking Phase 2 - FilterPanelView.swift (758 → 260 lines): - FilterRowView: Single filter row component - SQLPreviewSheet: SQL preview modal - FilterSettingsPopover: Settings popover - QuickSearchField: Quick search component Phase 3 - DataGridView.swift (1,706 → 700 lines): - KeyHandlingTableView: NSTableView subclass - TableRowViewWithMenu: Row view with context menu - CellTextField: Custom text field - DataGridCellFactory: Cell creation factory Phase 4 - HistoryListViewController.swift (1,325 → 550 lines): - HistoryDataProvider: Data fetching/filtering - HistoryRowView: Table cell view - HistoryTableView: Custom NSTableView Phase 5 - MainContentView.swift (2,365 → 2,113 lines): - QueryExecutionService: Query execution logic - RowOperationsManager: Row operations (add/delete/undo) - MainStatusBarView: Status bar component - QueryTabContentView/TableTabContentView: Tab content views --- .../ChangeTracking/DataChangeManager.swift | 596 ++++++++ .../ChangeTracking/DataChangeModels.swift | 90 ++ .../DataChangeUndoManager.swift | 83 ++ .../SQLStatementGenerator.swift | 345 +++++ .../Core/Services/QueryExecutionService.swift | 251 ++++ .../Core/Services/RowOperationsManager.swift | 312 ++++ OpenTable/Models/DataChange.swift | 1095 -------------- .../Editor/HistoryListViewController.swift | 1325 ----------------- OpenTable/Views/Filter/FilterPanelView.swift | 325 ++++ OpenTable/Views/Filter/FilterRowView.swift | 290 ++++ .../Views/Filter/FilterSettingsPopover.swift | 45 + OpenTable/Views/Filter/QuickSearchField.swift | 47 + OpenTable/Views/Filter/SQLPreviewSheet.swift | 85 ++ .../Views/History/HistoryDataProvider.swift | 138 ++ .../History/HistoryListViewController.swift | 756 ++++++++++ OpenTable/Views/History/HistoryRowView.swift | 158 ++ .../Views/History/HistoryTableView.swift | 100 ++ .../Views/Main/Child/MainStatusBarView.swift | 91 ++ .../Main/Child/QueryTabContentView.swift | 78 + .../Main/Child/TableTabContentView.swift | 90 ++ OpenTable/Views/MainContentView.swift | 407 +---- OpenTable/Views/Results/CellTextField.swift | 82 + .../Views/Results/DataGridCellFactory.swift | 207 +++ OpenTable/Views/Results/DataGridView.swift | 1257 ++-------------- OpenTable/Views/Results/FilterPanelView.swift | 758 ---------- .../Views/Results/KeyHandlingTableView.swift | 297 ++++ .../Views/Results/TableRowViewWithMenu.swift | 156 ++ 27 files changed, 4827 insertions(+), 4637 deletions(-) create mode 100644 OpenTable/Core/ChangeTracking/DataChangeManager.swift create mode 100644 OpenTable/Core/ChangeTracking/DataChangeModels.swift create mode 100644 OpenTable/Core/ChangeTracking/DataChangeUndoManager.swift create mode 100644 OpenTable/Core/ChangeTracking/SQLStatementGenerator.swift create mode 100644 OpenTable/Core/Services/QueryExecutionService.swift create mode 100644 OpenTable/Core/Services/RowOperationsManager.swift delete mode 100644 OpenTable/Models/DataChange.swift delete mode 100644 OpenTable/Views/Editor/HistoryListViewController.swift create mode 100644 OpenTable/Views/Filter/FilterPanelView.swift create mode 100644 OpenTable/Views/Filter/FilterRowView.swift create mode 100644 OpenTable/Views/Filter/FilterSettingsPopover.swift create mode 100644 OpenTable/Views/Filter/QuickSearchField.swift create mode 100644 OpenTable/Views/Filter/SQLPreviewSheet.swift create mode 100644 OpenTable/Views/History/HistoryDataProvider.swift create mode 100644 OpenTable/Views/History/HistoryListViewController.swift create mode 100644 OpenTable/Views/History/HistoryRowView.swift create mode 100644 OpenTable/Views/History/HistoryTableView.swift create mode 100644 OpenTable/Views/Main/Child/MainStatusBarView.swift create mode 100644 OpenTable/Views/Main/Child/QueryTabContentView.swift create mode 100644 OpenTable/Views/Main/Child/TableTabContentView.swift create mode 100644 OpenTable/Views/Results/CellTextField.swift create mode 100644 OpenTable/Views/Results/DataGridCellFactory.swift delete mode 100644 OpenTable/Views/Results/FilterPanelView.swift create mode 100644 OpenTable/Views/Results/KeyHandlingTableView.swift create mode 100644 OpenTable/Views/Results/TableRowViewWithMenu.swift diff --git a/OpenTable/Core/ChangeTracking/DataChangeManager.swift b/OpenTable/Core/ChangeTracking/DataChangeManager.swift new file mode 100644 index 00000000..38f29947 --- /dev/null +++ b/OpenTable/Core/ChangeTracking/DataChangeManager.swift @@ -0,0 +1,596 @@ +// +// DataChangeManager.swift +// OpenTable +// +// Manager for tracking data changes with O(1) lookups. +// Delegates SQL generation to SQLStatementGenerator. +// Delegates undo/redo stack management to DataChangeUndoManager. +// + +import Combine +import Foundation + +/// Manager for tracking and applying data changes +/// @MainActor ensures thread-safe access - critical for avoiding EXC_BAD_ACCESS +/// when multiple queries complete simultaneously (e.g., rapid sorting over SSH tunnel) +@MainActor +final class DataChangeManager: ObservableObject { + @Published var changes: [RowChange] = [] + @Published var hasChanges: Bool = false + @Published var reloadVersion: Int = 0 // Incremented to trigger table reload + + var tableName: String = "" + var primaryKeyColumn: String? + var databaseType: DatabaseType = .mysql + + // Simple storage with explicit deep copy to avoid memory corruption + private var _columnsStorage: [String] = [] + var columns: [String] { + get { _columnsStorage } + set { _columnsStorage = newValue.map { String($0) } } + } + + // MARK: - Cached Lookups for O(1) Performance + + /// Set of row indices that are marked for deletion - O(1) lookup + private var deletedRowIndices: Set = [] + + /// Set of row indices that are newly inserted - O(1) lookup + private(set) var insertedRowIndices: Set = [] + + /// Set of "rowIndex-colIndex" strings for modified cells - O(1) lookup + private var modifiedCells: Set = [] + + /// Lazy storage for inserted row values - avoids creating CellChange objects until needed + private var insertedRowData: [Int: [String?]] = [:] + + /// Undo/redo manager + private let undoManager = DataChangeUndoManager() + + // MARK: - Undo/Redo Properties + + var canUndo: Bool { undoManager.canUndo } + var canRedo: Bool { undoManager.canRedo } + + // MARK: - Helper Methods + + private func cellKey(rowIndex: Int, columnIndex: Int) -> String { + "\(rowIndex)-\(columnIndex)" + } + + // MARK: - Configuration + + /// Clear all changes (called after successful save) + func clearChanges() { + changes.removeAll() + deletedRowIndices.removeAll() + insertedRowIndices.removeAll() + modifiedCells.removeAll() + insertedRowData.removeAll() + undoManager.clearAll() + hasChanges = false + reloadVersion += 1 + } + + /// Atomically configure the manager for a new table + func configureForTable( + tableName: String, + columns: [String], + primaryKeyColumn: String?, + databaseType: DatabaseType = .mysql + ) { + self.tableName = tableName + self.columns = columns + self.primaryKeyColumn = primaryKeyColumn + self.databaseType = databaseType + + deletedRowIndices.removeAll() + insertedRowIndices.removeAll() + modifiedCells.removeAll() + insertedRowData.removeAll() + undoManager.clearAll() + + changes.removeAll() + hasChanges = false + reloadVersion += 1 + } + + // MARK: - Change Tracking + + func recordCellChange( + rowIndex: Int, + columnIndex: Int, + columnName: String, + oldValue: String?, + newValue: String?, + originalRow: [String?]? = nil + ) { + guard oldValue != newValue else { return } + + let cellChange = CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: oldValue, + newValue: newValue + ) + + let key = cellKey(rowIndex: rowIndex, columnIndex: columnIndex) + + // Check if this is an edit to an INSERTED row + if let insertIndex = changes.firstIndex(where: { + $0.rowIndex == rowIndex && $0.type == .insert + }) { + // Update stored values directly + if var storedValues = insertedRowData[rowIndex] { + if columnIndex < storedValues.count { + storedValues[columnIndex] = newValue + insertedRowData[rowIndex] = storedValues + } + } + + // Update/create CellChange for this column + if let cellIndex = changes[insertIndex].cellChanges.firstIndex(where: { + $0.columnIndex == columnIndex + }) { + changes[insertIndex].cellChanges[cellIndex] = CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: nil, + newValue: newValue + ) + } else { + changes[insertIndex].cellChanges.append(CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: nil, + newValue: newValue + )) + } + pushUndo(.cellEdit( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + previousValue: oldValue, + newValue: newValue + )) + hasChanges = !changes.isEmpty + return + } + + // Find existing UPDATE row change or create new one + if let existingIndex = changes.firstIndex(where: { + $0.rowIndex == rowIndex && $0.type == .update + }) { + if let cellIndex = changes[existingIndex].cellChanges.firstIndex(where: { + $0.columnIndex == columnIndex + }) { + let originalOldValue = changes[existingIndex].cellChanges[cellIndex].oldValue + changes[existingIndex].cellChanges[cellIndex] = CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: originalOldValue, + newValue: newValue + ) + + // If value is back to original, remove the change + if originalOldValue == newValue { + changes[existingIndex].cellChanges.remove(at: cellIndex) + modifiedCells.remove(key) + if changes[existingIndex].cellChanges.isEmpty { + changes.remove(at: existingIndex) + } + } + } else { + changes[existingIndex].cellChanges.append(cellChange) + modifiedCells.insert(key) + } + } else { + let rowChange = RowChange( + rowIndex: rowIndex, + type: .update, + cellChanges: [cellChange], + originalRow: originalRow + ) + changes.append(rowChange) + modifiedCells.insert(key) + } + + pushUndo(.cellEdit( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + previousValue: oldValue, + newValue: newValue + )) + hasChanges = !changes.isEmpty + } + + func recordRowDeletion(rowIndex: Int, originalRow: [String?]) { + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .update } + modifiedCells = modifiedCells.filter { !$0.hasPrefix("\(rowIndex)-") } + + let rowChange = RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow) + changes.append(rowChange) + deletedRowIndices.insert(rowIndex) + pushUndo(.rowDeletion(rowIndex: rowIndex, originalRow: originalRow)) + hasChanges = true + reloadVersion += 1 + } + + func recordBatchRowDeletion(rows: [(rowIndex: Int, originalRow: [String?])]) { + guard rows.count > 1 else { + if let row = rows.first { + recordRowDeletion(rowIndex: row.rowIndex, originalRow: row.originalRow) + } + return + } + + var batchData: [(rowIndex: Int, originalRow: [String?])] = [] + + for (rowIndex, originalRow) in rows { + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .update } + modifiedCells = modifiedCells.filter { !$0.hasPrefix("\(rowIndex)-") } + + let rowChange = RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow) + changes.append(rowChange) + deletedRowIndices.insert(rowIndex) + batchData.append((rowIndex: rowIndex, originalRow: originalRow)) + } + + pushUndo(.batchRowDeletion(rows: batchData)) + hasChanges = true + reloadVersion += 1 + } + + func recordRowInsertion(rowIndex: Int, values: [String?]) { + insertedRowData[rowIndex] = values + let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: []) + changes.append(rowChange) + insertedRowIndices.insert(rowIndex) + pushUndo(.rowInsertion(rowIndex: rowIndex)) + hasChanges = true + } + + // MARK: - Undo Operations + + func undoRowDeletion(rowIndex: Int) { + guard deletedRowIndices.contains(rowIndex) else { return } + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .delete } + deletedRowIndices.remove(rowIndex) + hasChanges = !changes.isEmpty + reloadVersion += 1 + } + + func undoRowInsertion(rowIndex: Int) { + guard insertedRowIndices.contains(rowIndex) else { return } + + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } + insertedRowIndices.remove(rowIndex) + insertedRowData.removeValue(forKey: rowIndex) + + // Shift down indices for rows after the removed row + var shiftedInsertedIndices = Set() + for idx in insertedRowIndices { + shiftedInsertedIndices.insert(idx > rowIndex ? idx - 1 : idx) + } + insertedRowIndices = shiftedInsertedIndices + + for i in 0.. rowIndex { + changes[i].rowIndex -= 1 + } + } + + hasChanges = !changes.isEmpty + } + + func undoBatchRowInsertion(rowIndices: [Int]) { + guard !rowIndices.isEmpty else { return } + + let validRows = rowIndices.filter { insertedRowIndices.contains($0) } + guard !validRows.isEmpty else { return } + + // Collect row values for undo/redo + var rowValues: [[String?]] = [] + for rowIndex in validRows { + if let insertChange = changes.first(where: { $0.rowIndex == rowIndex && $0.type == .insert }) { + let values = insertChange.cellChanges.sorted { $0.columnIndex < $1.columnIndex } + .map { $0.newValue } + rowValues.append(values) + } else { + rowValues.append(Array(repeating: nil, count: columns.count)) + } + } + + for rowIndex in validRows { + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } + insertedRowIndices.remove(rowIndex) + insertedRowData.removeValue(forKey: rowIndex) + } + + pushUndo(.batchRowInsertion(rowIndices: validRows, rowValues: rowValues)) + + for deletedIndex in validRows.reversed() { + var shiftedIndices = Set() + for idx in insertedRowIndices { + shiftedIndices.insert(idx > deletedIndex ? idx - 1 : idx) + } + insertedRowIndices = shiftedIndices + + for i in 0.. deletedIndex { + changes[i].rowIndex -= 1 + } + } + } + + hasChanges = !changes.isEmpty + } + + // MARK: - Undo/Redo Stack Management + + func pushUndo(_ action: UndoAction) { + undoManager.push(action) + } + + func popUndo() -> UndoAction? { + undoManager.popUndo() + } + + func clearUndoStack() { + undoManager.clearUndo() + } + + func clearRedoStack() { + undoManager.clearRedo() + } + + /// Undo the last change and return details needed to update the UI + func undoLastChange() -> (action: UndoAction, needsRowRemoval: Bool, needsRowRestore: Bool, restoreRow: [String?]?)? { + guard let action = popUndo() else { return nil } + + undoManager.moveToRedo(action) + + switch action { + case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, _): + if let changeIndex = changes.firstIndex(where: { + $0.rowIndex == rowIndex && ($0.type == .update || $0.type == .insert) + }) { + if let cellIndex = changes[changeIndex].cellChanges.firstIndex(where: { + $0.columnIndex == columnIndex + }) { + if changes[changeIndex].type == .update { + let originalValue = changes[changeIndex].cellChanges[cellIndex].oldValue + if previousValue == originalValue { + changes[changeIndex].cellChanges.remove(at: cellIndex) + modifiedCells.remove(cellKey(rowIndex: rowIndex, columnIndex: columnIndex)) + if changes[changeIndex].cellChanges.isEmpty { + changes.remove(at: changeIndex) + } + } else { + let originalOldValue = changes[changeIndex].cellChanges[cellIndex].oldValue + changes[changeIndex].cellChanges[cellIndex] = CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: originalOldValue, + newValue: previousValue + ) + } + } else if changes[changeIndex].type == .insert { + changes[changeIndex].cellChanges[cellIndex] = CellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: nil, + newValue: previousValue + ) + } + } + } + hasChanges = !changes.isEmpty + reloadVersion += 1 + return (action, false, false, nil) + + case .rowInsertion(let rowIndex): + undoRowInsertion(rowIndex: rowIndex) + return (action, true, false, nil) + + case .rowDeletion(let rowIndex, let originalRow): + undoRowDeletion(rowIndex: rowIndex) + return (action, false, true, originalRow) + + case .batchRowDeletion(let rows): + for (rowIndex, _) in rows.reversed() { + undoRowDeletion(rowIndex: rowIndex) + } + return (action, false, true, nil) + + case .batchRowInsertion(let rowIndices, let rowValues): + for (index, rowIndex) in rowIndices.enumerated().reversed() { + guard index < rowValues.count else { continue } + let values = rowValues[index] + + let cellChanges = values.enumerated().map { colIndex, value in + CellChange( + rowIndex: rowIndex, + columnIndex: colIndex, + columnName: columns[safe: colIndex] ?? "", + oldValue: nil, + newValue: value + ) + } + let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges) + changes.append(rowChange) + insertedRowIndices.insert(rowIndex) + } + + hasChanges = !changes.isEmpty + reloadVersion += 1 + return (action, true, false, nil) + } + } + + /// Redo the last undone change + func redoLastChange() -> (action: UndoAction, needsRowInsert: Bool, needsRowDelete: Bool)? { + guard let action = undoManager.popRedo() else { return nil } + + undoManager.moveToUndo(action) + + switch action { + case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, let newValue): + recordCellChange( + rowIndex: rowIndex, + columnIndex: columnIndex, + columnName: columnName, + oldValue: previousValue, + newValue: newValue + ) + _ = undoManager.popUndo() // Remove extra undo + reloadVersion += 1 + return (action, false, false) + + case .rowInsertion(let rowIndex): + insertedRowIndices.insert(rowIndex) + let cellChanges = columns.enumerated().map { index, columnName in + CellChange( + rowIndex: rowIndex, + columnIndex: index, + columnName: columnName, + oldValue: nil, + newValue: nil + ) + } + let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges) + changes.append(rowChange) + hasChanges = true + reloadVersion += 1 + return (action, true, false) + + case .rowDeletion(let rowIndex, let originalRow): + recordRowDeletion(rowIndex: rowIndex, originalRow: originalRow) + _ = undoManager.popUndo() + return (action, false, true) + + case .batchRowDeletion(let rows): + for (rowIndex, originalRow) in rows { + recordRowDeletion(rowIndex: rowIndex, originalRow: originalRow) + _ = undoManager.popUndo() + } + return (action, false, true) + + case .batchRowInsertion(let rowIndices, _): + for rowIndex in rowIndices { + changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } + insertedRowIndices.remove(rowIndex) + } + hasChanges = !changes.isEmpty + reloadVersion += 1 + return (action, true, false) + } + } + + // MARK: - SQL Generation + + func generateSQL() -> [String] { + let generator = SQLStatementGenerator( + tableName: tableName, + columns: columns, + primaryKeyColumn: primaryKeyColumn, + databaseType: databaseType + ) + return generator.generateStatements( + from: changes, + insertedRowData: insertedRowData, + deletedRowIndices: deletedRowIndices, + insertedRowIndices: insertedRowIndices + ) + } + + // MARK: - Actions + + func getOriginalValues() -> [(rowIndex: Int, columnIndex: Int, value: String?)] { + var originals: [(rowIndex: Int, columnIndex: Int, value: String?)] = [] + + for change in changes { + if change.type == .update { + for cellChange in change.cellChanges { + originals.append(( + rowIndex: change.rowIndex, + columnIndex: cellChange.columnIndex, + value: cellChange.oldValue + )) + } + } + } + + return originals + } + + func discardChanges() { + changes.removeAll() + deletedRowIndices.removeAll() + insertedRowIndices.removeAll() + modifiedCells.removeAll() + insertedRowData.removeAll() + hasChanges = false + reloadVersion += 1 + } + + // MARK: - Per-Tab State Management + + func saveState() -> TabPendingChanges { + var state = TabPendingChanges() + state.changes = changes + state.deletedRowIndices = deletedRowIndices + state.insertedRowIndices = insertedRowIndices + state.modifiedCells = modifiedCells + state.insertedRowData = insertedRowData + state.primaryKeyColumn = primaryKeyColumn + state.columns = columns + return state + } + + func restoreState(from state: TabPendingChanges, tableName: String) { + self.tableName = tableName + self.changes = state.changes + self.deletedRowIndices = state.deletedRowIndices + self.insertedRowIndices = state.insertedRowIndices + self.modifiedCells = state.modifiedCells + self.insertedRowData = state.insertedRowData + self.primaryKeyColumn = state.primaryKeyColumn + self.columns = state.columns + self.hasChanges = !state.changes.isEmpty + } + + // MARK: - O(1) Lookups + + func isRowDeleted(_ rowIndex: Int) -> Bool { + deletedRowIndices.contains(rowIndex) + } + + func isRowInserted(_ rowIndex: Int) -> Bool { + insertedRowIndices.contains(rowIndex) + } + + func isCellModified(rowIndex: Int, columnIndex: Int) -> Bool { + modifiedCells.contains(cellKey(rowIndex: rowIndex, columnIndex: columnIndex)) + } + + func getModifiedColumnsForRow(_ rowIndex: Int) -> Set { + var result: Set = [] + let prefix = "\(rowIndex)-" + for key in modifiedCells { + if key.hasPrefix(prefix) { + if let colIndex = Int(key.dropFirst(prefix.count)) { + result.insert(colIndex) + } + } + } + return result + } +} diff --git a/OpenTable/Core/ChangeTracking/DataChangeModels.swift b/OpenTable/Core/ChangeTracking/DataChangeModels.swift new file mode 100644 index 00000000..c8af3050 --- /dev/null +++ b/OpenTable/Core/ChangeTracking/DataChangeModels.swift @@ -0,0 +1,90 @@ +// +// DataChangeModels.swift +// OpenTable +// +// Pure data models for tracking data changes. +// No business logic - just structures for representing change state. +// + +import Foundation + +/// Represents a type of data change +enum ChangeType: Equatable { + case update + case insert + case delete +} + +/// Represents a single cell change +struct CellChange: Identifiable, Equatable { + let id: UUID + let rowIndex: Int + let columnIndex: Int + let columnName: String + let oldValue: String? + let newValue: String? + + init( + rowIndex: Int, + columnIndex: Int, + columnName: String, + oldValue: String?, + newValue: String? + ) { + self.id = UUID() + self.rowIndex = rowIndex + self.columnIndex = columnIndex + self.columnName = columnName + self.oldValue = oldValue + self.newValue = newValue + } +} + +/// Represents a row-level change +struct RowChange: Identifiable, Equatable { + let id: UUID + var rowIndex: Int + let type: ChangeType + var cellChanges: [CellChange] + let originalRow: [String?]? + + init( + rowIndex: Int, + type: ChangeType, + cellChanges: [CellChange] = [], + originalRow: [String?]? = nil + ) { + self.id = UUID() + self.rowIndex = rowIndex + self.type = type + self.cellChanges = cellChanges + self.originalRow = originalRow + } +} + +/// Represents an action that can be undone +enum UndoAction { + case cellEdit( + rowIndex: Int, + columnIndex: Int, + columnName: String, + previousValue: String?, + newValue: String? + ) + case rowInsertion(rowIndex: Int) + case rowDeletion(rowIndex: Int, originalRow: [String?]) + /// Batch deletion of multiple rows (for undo as a single action) + case batchRowDeletion(rows: [(rowIndex: Int, originalRow: [String?])]) + /// Batch insertion undo - when user deletes multiple inserted rows at once + case batchRowInsertion(rowIndices: [Int], rowValues: [[String?]]) +} + +// Note: TabPendingChanges is defined in QueryTab.swift + +// MARK: - Array Extension + +extension Array { + subscript(safe index: Int) -> Element? { + indices.contains(index) ? self[index] : nil + } +} diff --git a/OpenTable/Core/ChangeTracking/DataChangeUndoManager.swift b/OpenTable/Core/ChangeTracking/DataChangeUndoManager.swift new file mode 100644 index 00000000..afcd1aff --- /dev/null +++ b/OpenTable/Core/ChangeTracking/DataChangeUndoManager.swift @@ -0,0 +1,83 @@ +// +// DataChangeUndoManager.swift +// OpenTable +// +// Manages undo/redo stacks for data changes. +// Extracted from DataChangeManager to improve separation of concerns. +// + +import Foundation + +/// Manages undo/redo stacks for data changes +final class DataChangeUndoManager { + /// Undo stack for reversing changes (LIFO) + private var undoStack: [UndoAction] = [] + + /// Redo stack for re-applying undone changes (LIFO) + private var redoStack: [UndoAction] = [] + + // MARK: - Public API + + /// Check if there are any undo actions available + var canUndo: Bool { + !undoStack.isEmpty + } + + /// Check if there are any redo actions available + var canRedo: Bool { + !redoStack.isEmpty + } + + /// Push an undo action onto the stack + /// Clears the redo stack since new changes invalidate redo history + func push(_ action: UndoAction) { + undoStack.append(action) + // Don't clear redo here - let caller decide when to clear + } + + /// Pop the last undo action from the stack + func popUndo() -> UndoAction? { + undoStack.popLast() + } + + /// Pop the last redo action from the stack + func popRedo() -> UndoAction? { + redoStack.popLast() + } + + /// Move an action from undo to redo stack + func moveToRedo(_ action: UndoAction) { + redoStack.append(action) + } + + /// Move an action from redo to undo stack + func moveToUndo(_ action: UndoAction) { + undoStack.append(action) + } + + /// Clear the undo stack + func clearUndo() { + undoStack.removeAll() + } + + /// Clear the redo stack (called when new changes are made) + func clearRedo() { + redoStack.removeAll() + } + + /// Clear both stacks + func clearAll() { + undoStack.removeAll() + redoStack.removeAll() + } + + /// Get the count of undo actions + var undoCount: Int { + undoStack.count + } + + /// Get the count of redo actions + var redoCount: Int { + redoStack.count + } +} diff --git a/OpenTable/Core/ChangeTracking/SQLStatementGenerator.swift b/OpenTable/Core/ChangeTracking/SQLStatementGenerator.swift new file mode 100644 index 00000000..7c0f75b5 --- /dev/null +++ b/OpenTable/Core/ChangeTracking/SQLStatementGenerator.swift @@ -0,0 +1,345 @@ +// +// SQLStatementGenerator.swift +// OpenTable +// +// Generates SQL statements (INSERT, UPDATE, DELETE) from tracked changes. +// Extracted from DataChangeManager to improve separation of concerns. +// + +import Foundation + +/// Generates SQL statements from data changes +struct SQLStatementGenerator { + let tableName: String + let columns: [String] + let primaryKeyColumn: String? + let databaseType: DatabaseType + + // MARK: - Public API + + /// Generate all SQL statements from changes + /// - Parameters: + /// - changes: Array of row changes to process + /// - insertedRowData: Lazy storage for inserted row values + /// - deletedRowIndices: Set of deleted row indices for validation + /// - insertedRowIndices: Set of inserted row indices for validation + /// - Returns: Array of SQL statement strings + func generateStatements( + from changes: [RowChange], + insertedRowData: [Int: [String?]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [String] { + var statements: [String] = [] + + // Collect UPDATE and DELETE changes to batch them + var updateChanges: [RowChange] = [] + var deleteChanges: [RowChange] = [] + + for change in changes { + switch change.type { + case .update: + updateChanges.append(change) + case .insert: + // SAFETY: Verify the row is still marked as inserted + guard insertedRowIndices.contains(change.rowIndex) else { + continue + } + if let sql = generateInsertSQL(for: change, insertedRowData: insertedRowData) { + statements.append(sql) + } + case .delete: + // SAFETY: Verify the row is still marked as deleted + guard deletedRowIndices.contains(change.rowIndex) else { + continue + } + deleteChanges.append(change) + } + } + + // Generate batched UPDATE statements (group by same columns being updated) + if !updateChanges.isEmpty { + let batchedUpdates = generateBatchUpdateSQL(for: updateChanges) + statements.append(contentsOf: batchedUpdates) + } + + // Generate batched DELETE statement (single DELETE with OR conditions) + if !deleteChanges.isEmpty { + if let sql = generateBatchDeleteSQL(for: deleteChanges) { + statements.append(sql) + } + } + + return statements + } + + // MARK: - INSERT Generation + + private func generateInsertSQL(for change: RowChange, insertedRowData: [Int: [String?]]) -> String? { + // OPTIMIZATION: Get values from lazy storage instead of cellChanges + if let values = insertedRowData[change.rowIndex] { + return generateInsertSQLFromStoredData(rowIndex: change.rowIndex, values: values) + } + + // Fallback: use cellChanges if stored data not available (backward compatibility) + return generateInsertSQLFromCellChanges(for: change) + } + + /// Generate INSERT SQL from lazy-stored row data (optimized path) + private func generateInsertSQLFromStoredData(rowIndex: Int, values: [String?]) -> String? { + var nonDefaultColumns: [String] = [] + var nonDefaultValues: [String] = [] + + for (index, value) in values.enumerated() { + // Skip DEFAULT columns - let DB handle them + if value == "__DEFAULT__" { continue } + + guard index < columns.count else { continue } + let columnName = columns[index] + + nonDefaultColumns.append(databaseType.quoteIdentifier(columnName)) + + if let val = value { + if isSQLFunctionExpression(val) { + nonDefaultValues.append(val.trimmingCharacters(in: .whitespaces).uppercased()) + } else { + nonDefaultValues.append("'\(escapeSQLString(val))'") + } + } else { + nonDefaultValues.append("NULL") + } + } + + // If all columns are DEFAULT, don't generate INSERT + guard !nonDefaultColumns.isEmpty else { return nil } + + let columnList = nonDefaultColumns.joined(separator: ", ") + let valueList = nonDefaultValues.joined(separator: ", ") + + return "INSERT INTO \(databaseType.quoteIdentifier(tableName)) (\(columnList)) VALUES (\(valueList))" + } + + /// Generate INSERT SQL from cellChanges (fallback for backward compatibility) + private func generateInsertSQLFromCellChanges(for change: RowChange) -> String? { + guard !change.cellChanges.isEmpty else { return nil } + + // Filter out DEFAULT columns - let DB handle them + let nonDefaultChanges = change.cellChanges.filter { + $0.newValue != "__DEFAULT__" + } + + // If all columns are DEFAULT, don't generate INSERT + guard !nonDefaultChanges.isEmpty else { return nil } + + let columnNames = nonDefaultChanges.map { + databaseType.quoteIdentifier($0.columnName) + }.joined(separator: ", ") + + let values = nonDefaultChanges.map { cellChange -> String in + if let newValue = cellChange.newValue { + if isSQLFunctionExpression(newValue) { + return newValue.trimmingCharacters(in: .whitespaces).uppercased() + } + return "'\(escapeSQLString(newValue))'" + } + return "NULL" + }.joined(separator: ", ") + + return "INSERT INTO \(databaseType.quoteIdentifier(tableName)) (\(columnNames)) VALUES (\(values))" + } + + // MARK: - UPDATE Generation + + /// Generate batched UPDATE statements grouped by columns being updated + /// Example: UPDATE table SET col1 = CASE WHEN id=1 THEN 'val1' WHEN id=2 THEN 'val2' END WHERE id IN (1,2) + private func generateBatchUpdateSQL(for changes: [RowChange]) -> [String] { + guard !changes.isEmpty else { return [] } + guard let pkColumn = primaryKeyColumn else { + // Fallback to individual UPDATEs if no PK + return changes.compactMap { generateUpdateSQL(for: $0) } + } + guard let pkIndex = columns.firstIndex(of: pkColumn) else { + return changes.compactMap { generateUpdateSQL(for: $0) } + } + + // Group changes by set of columns being updated + var grouped: [[String]: [RowChange]] = [:] + for change in changes { + let columnNames = change.cellChanges.map { $0.columnName }.sorted() + grouped[columnNames, default: []].append(change) + } + + var statements: [String] = [] + + for (columnNames, groupedChanges) in grouped { + // Build CASE statements for each column + var caseClauses: [String] = [] + + for columnName in columnNames { + var whenClauses: [String] = [] + + for change in groupedChanges { + guard let originalRow = change.originalRow, + pkIndex < originalRow.count, + let cellChange = change.cellChanges.first(where: { $0.columnName == columnName }) else { + continue + } + + let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" + + // Generate value + let value: String + if cellChange.newValue == "__DEFAULT__" { + value = "DEFAULT" + } else if let newValue = cellChange.newValue { + if isSQLFunctionExpression(newValue) { + value = newValue.trimmingCharacters(in: .whitespaces).uppercased() + } else { + value = "'\(escapeSQLString(newValue))'" + } + } else { + value = "NULL" + } + + whenClauses.append("WHEN \(databaseType.quoteIdentifier(pkColumn)) = \(pkValue) THEN \(value)") + } + + if !whenClauses.isEmpty { + let caseExpr = "CASE \(whenClauses.joined(separator: " ")) END" + caseClauses.append("\(databaseType.quoteIdentifier(columnName)) = \(caseExpr)") + } + } + + // Build WHERE IN clause with all PKs + var pkValues: [String] = [] + for change in groupedChanges { + guard let originalRow = change.originalRow, + pkIndex < originalRow.count else { + continue + } + let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" + pkValues.append(pkValue) + } + + if !caseClauses.isEmpty && !pkValues.isEmpty { + let whereClause = "\(databaseType.quoteIdentifier(pkColumn)) IN (\(pkValues.joined(separator: ", ")))" + let sql = "UPDATE \(databaseType.quoteIdentifier(tableName)) SET \(caseClauses.joined(separator: ", ")) WHERE \(whereClause)" + statements.append(sql) + } + } + + return statements + } + + /// Generate individual UPDATE statement for a single row (fallback) + private func generateUpdateSQL(for change: RowChange) -> String? { + guard !change.cellChanges.isEmpty else { return nil } + + let setClauses = change.cellChanges.map { cellChange -> String in + let value: String + if cellChange.newValue == "__DEFAULT__" { + value = "DEFAULT" + } else if let newValue = cellChange.newValue { + if isSQLFunctionExpression(newValue) { + value = newValue.trimmingCharacters(in: .whitespaces).uppercased() + } else { + value = "'\(escapeSQLString(newValue))'" + } + } else { + value = "NULL" + } + return "\(databaseType.quoteIdentifier(cellChange.columnName)) = \(value)" + }.joined(separator: ", ") + + // Use primary key for WHERE clause + var whereClause = "1=1" // Fallback - dangerous but necessary without PK + + if let pkColumn = primaryKeyColumn, + let pkColumnIndex = columns.firstIndex(of: pkColumn) { + // Try to get PK value from originalRow first + if let originalRow = change.originalRow, pkColumnIndex < originalRow.count { + let pkValue = originalRow[pkColumnIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" + whereClause = "\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)" + } + // Otherwise try from cellChanges (if PK column was edited) + else if let pkChange = change.cellChanges.first(where: { $0.columnName == pkColumn }) { + let pkValue = pkChange.oldValue.map { "'\(escapeSQLString($0))'" } ?? "NULL" + whereClause = "\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)" + } + } + + return "UPDATE \(databaseType.quoteIdentifier(tableName)) SET \(setClauses) WHERE \(whereClause)" + } + + // MARK: - DELETE Generation + + /// Generate a batched DELETE statement combining multiple rows with OR conditions + /// Example: DELETE FROM table WHERE id = 1 OR id = 2 OR id = 3 + private func generateBatchDeleteSQL(for changes: [RowChange]) -> String? { + guard !changes.isEmpty else { return nil } + guard let pkColumn = primaryKeyColumn else { return nil } + guard let pkIndex = columns.firstIndex(of: pkColumn) else { return nil } + + // Build OR conditions for all rows + var conditions: [String] = [] + + for change in changes { + guard let originalRow = change.originalRow, + pkIndex < originalRow.count else { + continue + } + + let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" + conditions.append("\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)") + } + + guard !conditions.isEmpty else { return nil } + + // Combine all conditions with OR + let whereClause = conditions.joined(separator: " OR ") + return "DELETE FROM \(databaseType.quoteIdentifier(tableName)) WHERE \(whereClause)" + } + + // MARK: - Helper Functions + + /// Check if a string is a SQL function expression that should not be quoted + private func isSQLFunctionExpression(_ value: String) -> Bool { + let trimmed = value.trimmingCharacters(in: .whitespaces).uppercased() + + // Common SQL functions for datetime/timestamps + let sqlFunctions = [ + "NOW()", + "CURRENT_TIMESTAMP()", + "CURRENT_TIMESTAMP", + "CURDATE()", + "CURTIME()", + "UTC_TIMESTAMP()", + "UTC_DATE()", + "UTC_TIME()", + "LOCALTIME()", + "LOCALTIME", + "LOCALTIMESTAMP()", + "LOCALTIMESTAMP", + "SYSDATE()", + "UNIX_TIMESTAMP()", + "CURRENT_DATE()", + "CURRENT_DATE", + "CURRENT_TIME()", + "CURRENT_TIME", + ] + + return sqlFunctions.contains(trimmed) + } + + /// Escape characters that can break SQL strings + private func escapeSQLString(_ str: String) -> String { + var result = str + result = result.replacingOccurrences(of: "\\", with: "\\\\") // Backslash first + result = result.replacingOccurrences(of: "'", with: "''") // Single quote + result = result.replacingOccurrences(of: "\n", with: "\\n") // Newline + result = result.replacingOccurrences(of: "\r", with: "\\r") // Carriage return + result = result.replacingOccurrences(of: "\t", with: "\\t") // Tab + result = result.replacingOccurrences(of: "\0", with: "\\0") // Null byte + return result + } +} diff --git a/OpenTable/Core/Services/QueryExecutionService.swift b/OpenTable/Core/Services/QueryExecutionService.swift new file mode 100644 index 00000000..cea85a1e --- /dev/null +++ b/OpenTable/Core/Services/QueryExecutionService.swift @@ -0,0 +1,251 @@ +// +// QueryExecutionService.swift +// OpenTable +// +// Service responsible for query execution, parsing, and SQL statement extraction. +// Extracted from MainContentView for better separation of concerns. +// + +import Combine +import Foundation + +/// Service for executing database queries and parsing SQL +@MainActor +final class QueryExecutionService: ObservableObject { + + // MARK: - Published State + + @Published var isExecuting: Bool = false + @Published var executionTime: TimeInterval? + @Published var errorMessage: String? + + // MARK: - Private State + + private var currentTask: Task? + private var queryGeneration: Int = 0 + + // MARK: - Query Execution + + /// Execute a query and return results via callbacks + /// - Parameters: + /// - sql: The SQL query to execute + /// - connection: Database connection configuration + /// - tableName: Optional table name for editable queries + /// - onSuccess: Callback with query result + /// - onError: Callback with error + func execute( + sql: String, + connection: DatabaseConnection, + tableName: String?, + onSuccess: @escaping (QueryExecutionResult) async -> Void, + onError: @escaping (Error) async -> Void + ) { + // Don't execute empty queries + guard !sql.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + isExecuting = false + return + } + + // Cancel any previous running query + currentTask?.cancel() + + // Increment generation for race condition prevention + queryGeneration += 1 + let capturedGeneration = queryGeneration + + isExecuting = true + executionTime = nil + errorMessage = nil + + let isEditable = tableName != nil + + currentTask = Task { + do { + let result = try await DatabaseManager.shared.execute(query: sql) + + // Fetch column defaults and total row count if editable table + var columnDefaults: [String: String?] = [:] + var totalRowCount: Int? = nil + + if isEditable, let tableName = tableName { + if let driver = DatabaseManager.shared.activeDriver { + // Execute both queries in parallel for better performance + async let columnInfoTask = driver.fetchColumns(table: tableName) + async let countTask: QueryResult = { + let quotedTable = connection.type.quoteIdentifier(tableName) + return try await DatabaseManager.shared.execute(query: "SELECT COUNT(*) FROM \(quotedTable)") + }() + + let (columnInfo, countResult) = try await (columnInfoTask, countTask) + + for col in columnInfo { + columnDefaults[col.name] = col.defaultValue + } + + if let firstRow = countResult.rows.first, + let countStr = firstRow.first as? String, + let count = Int(countStr) { + totalRowCount = count + } + } + } + + // Deep copy all data to prevent C buffer retention issues + // result.rows is [[String?]] - raw arrays, not QueryResultRow + var safeRows: [QueryResultRow] = [] + for row in result.rows { + var safeValues: [String?] = [] + for val in row { + if let v = val { + safeValues.append(String(v)) + } else { + safeValues.append(nil) + } + } + safeRows.append(QueryResultRow(values: safeValues)) + } + + let safeResult = QueryExecutionResult( + columns: result.columns.map { String($0) }, + rows: safeRows, + executionTime: result.executionTime, + columnDefaults: columnDefaults.mapValues { $0.map { String($0) } }, + totalRowCount: totalRowCount, + tableName: tableName.map { String($0) }, + isEditable: isEditable + ) + + // Check for cancellation + guard !Task.isCancelled else { + await MainActor.run { + isExecuting = false + executionTime = safeResult.executionTime + } + return + } + + // Check generation for race conditions + guard capturedGeneration == queryGeneration else { + return + } + + await MainActor.run { + isExecuting = false + executionTime = safeResult.executionTime + } + + await onSuccess(safeResult) + + } catch { + guard capturedGeneration == queryGeneration else { return } + + await MainActor.run { + isExecuting = false + errorMessage = error.localizedDescription + } + + await onError(error) + } + } + } + + /// Cancel any running query + func cancel() { + currentTask?.cancel() + currentTask = nil + isExecuting = false + } + + // MARK: - SQL Parsing + + /// Extract the SQL statement at the cursor position (semicolon-delimited) + /// Enables TablePlus-like behavior: execute only the current query + func extractQueryAtCursor(from fullQuery: String, at position: Int) -> String { + let trimmed = fullQuery.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return trimmed } + + // If no semicolons, return the entire query + guard trimmed.contains(";") else { return trimmed } + + // Split by semicolon but keep track of positions + var statements: [(text: String, range: Range)] = [] + var currentStart = 0 + var inString = false + var stringChar: Character = "\"" + + for (i, char) in fullQuery.enumerated() { + // Track string literals to avoid splitting on semicolons inside strings + if char == "'" || char == "\"" { + if !inString { + inString = true + stringChar = char + } else if char == stringChar { + inString = false + } + } + + // Found a statement delimiter + if char == ";" && !inString { + let statement = String( + fullQuery[ + fullQuery.index(fullQuery.startIndex, offsetBy: currentStart).. String? { + let pattern = #"(?i)^\s*SELECT\s+.+?\s+FROM\s+[`"]?(\w+)[`"]?\s*(?:WHERE|ORDER|LIMIT|GROUP|HAVING|$|;)"# + + guard let regex = try? NSRegularExpression(pattern: pattern, options: []), + let match = regex.firstMatch(in: sql, options: [], range: NSRange(sql.startIndex..., in: sql)), + let range = Range(match.range(at: 1), in: sql) + else { + return nil + } + + return String(sql[range]) + } +} + +// MARK: - Query Execution Result + +/// Result of a query execution with all necessary data +struct QueryExecutionResult { + let columns: [String] + let rows: [QueryResultRow] + let executionTime: TimeInterval + let columnDefaults: [String: String?] + let totalRowCount: Int? + let tableName: String? + let isEditable: Bool +} diff --git a/OpenTable/Core/Services/RowOperationsManager.swift b/OpenTable/Core/Services/RowOperationsManager.swift new file mode 100644 index 00000000..5acfea58 --- /dev/null +++ b/OpenTable/Core/Services/RowOperationsManager.swift @@ -0,0 +1,312 @@ +// +// RowOperationsManager.swift +// OpenTable +// +// Service responsible for row operations: add, delete, duplicate, undo/redo. +// Extracted from MainContentView for better separation of concerns. +// + +import AppKit +import Foundation + +/// Manager for row operations in the data grid +@MainActor +final class RowOperationsManager { + + // MARK: - Dependencies + + private let changeManager: DataChangeManager + + // MARK: - Initialization + + init(changeManager: DataChangeManager) { + self.changeManager = changeManager + } + + // MARK: - Add Row + + /// Add a new row to a table tab + /// - Parameters: + /// - columns: Column names + /// - columnDefaults: Column default values + /// - resultRows: Current rows (will be mutated) + /// - Returns: Tuple of (newRowIndex, newRowValues) or nil if failed + func addNewRow( + columns: [String], + columnDefaults: [String: String?], + resultRows: inout [QueryResultRow] + ) -> (rowIndex: Int, values: [String?])? { + // Create new row values with DEFAULT markers + var newRowValues: [String?] = [] + for column in columns { + if let defaultValue = columnDefaults[column], defaultValue != nil { + // Use __DEFAULT__ marker so generateInsertSQL skips this column + newRowValues.append("__DEFAULT__") + } else { + // NULL for columns without defaults + newRowValues.append(nil) + } + } + + // Add to resultRows + let newRow = QueryResultRow(values: newRowValues) + resultRows.append(newRow) + + // Get the new row index + let newRowIndex = resultRows.count - 1 + + // Record in change manager as pending INSERT + changeManager.recordRowInsertion(rowIndex: newRowIndex, values: newRowValues) + + return (newRowIndex, newRowValues) + } + + // MARK: - Duplicate Row + + /// Duplicate a row with new primary key + /// - Parameters: + /// - sourceRowIndex: Index of row to duplicate + /// - columns: Column names + /// - resultRows: Current rows (will be mutated) + /// - Returns: Tuple of (newRowIndex, newRowValues) or nil if failed + func duplicateRow( + sourceRowIndex: Int, + columns: [String], + resultRows: inout [QueryResultRow] + ) -> (rowIndex: Int, values: [String?])? { + guard sourceRowIndex < resultRows.count else { return nil } + + // Copy values from selected row + let sourceRow = resultRows[sourceRowIndex] + var newValues = sourceRow.values + + // Set primary key column to DEFAULT so DB auto-generates + if let pkColumn = changeManager.primaryKeyColumn, + let pkIndex = columns.firstIndex(of: pkColumn) { + newValues[pkIndex] = "__DEFAULT__" + } + + // Add the duplicated row + let newRow = QueryResultRow(values: newValues) + resultRows.append(newRow) + + // Get the new row index + let newRowIndex = resultRows.count - 1 + + // Record in change manager as pending INSERT + changeManager.recordRowInsertion(rowIndex: newRowIndex, values: newValues) + + return (newRowIndex, newValues) + } + + // MARK: - Delete Rows + + /// Delete selected rows + /// - Parameters: + /// - selectedIndices: Indices of rows to delete + /// - resultRows: Current rows (will be mutated) + /// - Returns: Next row index to select after deletion, or -1 if no rows left + func deleteSelectedRows( + selectedIndices: Set, + resultRows: inout [QueryResultRow] + ) -> Int { + guard !selectedIndices.isEmpty else { return -1 } + + // Separate inserted rows from existing rows + var insertedRowsToDelete: [Int] = [] + var existingRowsToDelete: [(rowIndex: Int, originalRow: [String?])] = [] + + // Find the lowest selected row index for selection movement + let minSelectedRow = selectedIndices.min() ?? 0 + let maxSelectedRow = selectedIndices.max() ?? 0 + + // Categorize rows (process in descending order to maintain correct indices) + for rowIndex in selectedIndices.sorted(by: >) { + if changeManager.isRowInserted(rowIndex) { + insertedRowsToDelete.append(rowIndex) + } else if !changeManager.isRowDeleted(rowIndex) { + if rowIndex < resultRows.count { + let originalRow = resultRows[rowIndex].values + existingRowsToDelete.append((rowIndex: rowIndex, originalRow: originalRow)) + } + } + } + + // Process inserted rows deletion + if !insertedRowsToDelete.isEmpty { + let sortedInsertedRows = insertedRowsToDelete.sorted(by: >) + + // Remove from resultRows first (descending order) + for rowIndex in sortedInsertedRows { + guard rowIndex < resultRows.count else { continue } + resultRows.remove(at: rowIndex) + } + + // Update changeManager for ALL deleted inserted rows at once + changeManager.undoBatchRowInsertion(rowIndices: sortedInsertedRows) + } + + // Record batch deletion for existing rows (single undo action for all rows) + if !existingRowsToDelete.isEmpty { + changeManager.recordBatchRowDeletion(rows: existingRowsToDelete) + } + + // Calculate next row selection, accounting for deleted inserted rows + let totalRows = resultRows.count + let rowsDeleted = insertedRowsToDelete.count + let adjustedMaxRow = maxSelectedRow - rowsDeleted + let adjustedMinRow = minSelectedRow - insertedRowsToDelete.filter { $0 < minSelectedRow }.count + + if adjustedMaxRow + 1 < totalRows { + return min(adjustedMaxRow + 1, totalRows - 1) + } else if adjustedMinRow > 0 { + return adjustedMinRow - 1 + } else if totalRows > 0 { + return 0 + } else { + return -1 + } + } + + // MARK: - Undo/Redo + + /// Undo the last change + /// - Parameter resultRows: Current rows (will be mutated) + /// - Returns: Updated selection indices + func undoLastChange(resultRows: inout [QueryResultRow]) -> Set? { + guard let result = changeManager.undoLastChange() else { return nil } + + var adjustedSelection: Set? = nil + + switch result.action { + case .cellEdit(let rowIndex, let columnIndex, _, let previousValue, _): + if rowIndex < resultRows.count { + resultRows[rowIndex].values[columnIndex] = previousValue + } + + case .rowInsertion(let rowIndex): + if rowIndex < resultRows.count { + resultRows.remove(at: rowIndex) + adjustedSelection = Set() + } + + case .rowDeletion(_, _): + // Row is restored in changeManager - visual indicator will be removed + break + + case .batchRowDeletion(_): + // All rows are restored in changeManager + break + + case .batchRowInsertion(let rowIndices, let rowValues): + // Restore deleted inserted rows - add them back to resultRows + for (index, rowIndex) in rowIndices.enumerated().reversed() { + guard index < rowValues.count else { continue } + guard rowIndex <= resultRows.count else { continue } + + let values = rowValues[index] + let newRow = QueryResultRow(values: values) + resultRows.insert(newRow, at: rowIndex) + } + } + + return adjustedSelection + } + + /// Redo the last undone change + /// - Parameters: + /// - resultRows: Current rows (will be mutated) + /// - columns: Column names for new row creation + /// - Returns: Updated selection indices + func redoLastChange(resultRows: inout [QueryResultRow], columns: [String]) -> Set? { + guard let result = changeManager.redoLastChange() else { return nil } + + switch result.action { + case .cellEdit(let rowIndex, let columnIndex, _, _, let newValue): + if rowIndex < resultRows.count { + resultRows[rowIndex].values[columnIndex] = newValue + } + + case .rowInsertion(let rowIndex): + let newValues = [String?](repeating: nil, count: columns.count) + let newRow = QueryResultRow(values: newValues) + if rowIndex <= resultRows.count { + resultRows.insert(newRow, at: rowIndex) + } + + case .rowDeletion(_, _): + // Row is re-marked as deleted in changeManager + break + + case .batchRowDeletion(_): + // Rows are re-marked as deleted + break + + case .batchRowInsertion(let rowIndices, _): + // Redo the deletion - remove the rows from resultRows again + for rowIndex in rowIndices.sorted(by: >) { + guard rowIndex < resultRows.count else { continue } + resultRows.remove(at: rowIndex) + } + } + + return nil + } + + // MARK: - Undo Insert Row + + /// Remove a row that was inserted (called by undo context menu) + /// - Parameters: + /// - rowIndex: Index of the inserted row + /// - resultRows: Current rows (will be mutated) + /// - selectedIndices: Current selection (will be adjusted) + /// - Returns: Adjusted selection indices + func undoInsertRow( + at rowIndex: Int, + resultRows: inout [QueryResultRow], + selectedIndices: Set + ) -> Set { + guard rowIndex >= 0 && rowIndex < resultRows.count else { return selectedIndices } + + // Remove the row from resultRows + resultRows.remove(at: rowIndex) + + // Adjust selection indices + var adjustedSelection = Set() + for idx in selectedIndices { + if idx == rowIndex { + continue // Skip the removed row + } else if idx > rowIndex { + adjustedSelection.insert(idx - 1) + } else { + adjustedSelection.insert(idx) + } + } + + return adjustedSelection + } + + // MARK: - Copy Rows + + /// Copy selected rows to clipboard as tab-separated values + /// - Parameters: + /// - selectedIndices: Indices of rows to copy + /// - resultRows: Current rows + func copySelectedRowsToClipboard(selectedIndices: Set, resultRows: [QueryResultRow]) { + guard !selectedIndices.isEmpty else { return } + + let sortedIndices = selectedIndices.sorted() + var lines: [String] = [] + + for rowIndex in sortedIndices { + guard rowIndex < resultRows.count else { continue } + let row = resultRows[rowIndex] + let line = row.values.map { $0 ?? "NULL" }.joined(separator: "\t") + lines.append(line) + } + + let text = lines.joined(separator: "\n") + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + } +} diff --git a/OpenTable/Models/DataChange.swift b/OpenTable/Models/DataChange.swift deleted file mode 100644 index 11af1af2..00000000 --- a/OpenTable/Models/DataChange.swift +++ /dev/null @@ -1,1095 +0,0 @@ -// -// DataChange.swift -// OpenTable -// -// Models for tracking data changes -// - -import Combine -import Foundation - -/// Represents a type of data change -enum ChangeType: Equatable { - case update - case insert - case delete -} - -/// Represents a single cell change -struct CellChange: Identifiable, Equatable { - let id: UUID - let rowIndex: Int - let columnIndex: Int - let columnName: String - let oldValue: String? - let newValue: String? - - init(rowIndex: Int, columnIndex: Int, columnName: String, oldValue: String?, newValue: String?) - { - self.id = UUID() - self.rowIndex = rowIndex - self.columnIndex = columnIndex - self.columnName = columnName - self.oldValue = oldValue - self.newValue = newValue - } -} - -/// Represents a row-level change -struct RowChange: Identifiable, Equatable { - let id: UUID - var rowIndex: Int - let type: ChangeType - var cellChanges: [CellChange] - let originalRow: [String?]? - - init( - rowIndex: Int, type: ChangeType, cellChanges: [CellChange] = [], - originalRow: [String?]? = nil - ) { - self.id = UUID() - self.rowIndex = rowIndex - self.type = type - self.cellChanges = cellChanges - self.originalRow = originalRow - } -} - -/// Manager for tracking and applying data changes -/// @MainActor ensures thread-safe access to most properties - critical for avoiding EXC_BAD_ACCESS -/// when multiple queries complete simultaneously (e.g., rapid sorting over SSH tunnel) - -/// Represents an action that can be undone -enum UndoAction { - case cellEdit(rowIndex: Int, columnIndex: Int, columnName: String, previousValue: String?, newValue: String?) - case rowInsertion(rowIndex: Int) - case rowDeletion(rowIndex: Int, originalRow: [String?]) - /// Batch deletion of multiple rows (for undo as a single action) - case batchRowDeletion(rows: [(rowIndex: Int, originalRow: [String?])]) - /// Batch insertion undo - when user deletes multiple inserted rows at once - case batchRowInsertion(rowIndices: [Int], rowValues: [[String?]]) -} - -@MainActor -final class DataChangeManager: ObservableObject { - @Published var changes: [RowChange] = [] - @Published var hasChanges: Bool = false - @Published var reloadVersion: Int = 0 // Incremented to trigger table reload - - var tableName: String = "" - var primaryKeyColumn: String? - var databaseType: DatabaseType = .mysql // For database-specific SQL generation - - // Simple storage with explicit deep copy to avoid memory corruption - private var _columnsStorage: [String] = [] - var columns: [String] { - get { - return _columnsStorage - } - set { - // Create explicit deep copy to ensure independence - _columnsStorage = newValue.map { String($0) } - } - } - - // MARK: - Cached Lookups for O(1) Performance - - /// Set of row indices that are marked for deletion - O(1) lookup - private var deletedRowIndices: Set = [] - - /// Set of row indices that are newly inserted - O(1) lookup - private(set) var insertedRowIndices: Set = [] - - /// Set of "rowIndex-colIndex" strings for modified cells - O(1) lookup - private var modifiedCells: Set = [] - - /// Lazy storage for inserted row values - avoids creating CellChange objects until needed - /// Maps rowIndex -> column values array for newly inserted rows - /// This dramatically improves add row performance for tables with many columns - private var insertedRowData: [Int: [String?]] = [:] - - /// Undo stack for reversing changes (LIFO) - private var undoStack: [UndoAction] = [] - - /// Redo stack for re-applying undone changes (LIFO) - private var redoStack: [UndoAction] = [] - - /// Helper to create a cache key for modified cells - private func cellKey(rowIndex: Int, columnIndex: Int) -> String { - "\(rowIndex)-\(columnIndex)" - } - - /// Clear all changes (called after successful save) - func clearChanges() { - changes.removeAll() - deletedRowIndices.removeAll() - insertedRowIndices.removeAll() - modifiedCells.removeAll() - insertedRowData.removeAll() // Clear lazy storage - undoStack.removeAll() // Clear undo stack too - hasChanges = false - reloadVersion += 1 // Trigger table reload - } - - /// Atomically configure the manager for a new table - /// This batches all updates and only triggers @Published changes at the end - /// to prevent race conditions where SwiftUI reads properties mid-update - func configureForTable( - tableName: String, columns: [String], primaryKeyColumn: String?, - databaseType: DatabaseType = .mysql - ) { - // First, update non-published properties (no SwiftUI notifications) - self.tableName = tableName - self.columns = columns // Uses deep copy setter to avoid memory corruption - self.primaryKeyColumn = primaryKeyColumn - self.databaseType = databaseType - - // Clear caches - deletedRowIndices.removeAll() - insertedRowIndices.removeAll() - modifiedCells.removeAll() - insertedRowData.removeAll() // Clear lazy storage - - // Now update @Published properties - triggers ONE view update - changes.removeAll() - hasChanges = false - reloadVersion += 1 - } - - /// Rebuilds the caches from the changes array (used after complex modifications) - private func rebuildCaches() { - deletedRowIndices.removeAll() - insertedRowIndices.removeAll() - modifiedCells.removeAll() - - for change in changes { - if change.type == .delete { - deletedRowIndices.insert(change.rowIndex) - } else if change.type == .insert { - insertedRowIndices.insert(change.rowIndex) - } else if change.type == .update { - for cellChange in change.cellChanges { - modifiedCells.insert( - cellKey(rowIndex: change.rowIndex, columnIndex: cellChange.columnIndex)) - } - } - } - } - - // MARK: - Change Tracking - - func recordCellChange( - rowIndex: Int, columnIndex: Int, columnName: String, oldValue: String?, newValue: String?, - originalRow: [String?]? = nil - ) { - guard oldValue != newValue else { return } - - let cellChange = CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: oldValue, - newValue: newValue - ) - - let key = cellKey(rowIndex: rowIndex, columnIndex: columnIndex) - - // Check if this is an edit to an INSERTED row - // If so, update the INSERT record's cell values instead of creating UPDATE - if let insertIndex = changes.firstIndex(where: { - $0.rowIndex == rowIndex && $0.type == .insert - }) { - // OPTIMIZATION: Update the stored values directly first - if var storedValues = insertedRowData[rowIndex] { - if columnIndex < storedValues.count { - storedValues[columnIndex] = newValue - insertedRowData[rowIndex] = storedValues - } - } - - // Also update/create CellChange for this specific column - // (Lazy build - only for edited columns, not all columns) - if let cellIndex = changes[insertIndex].cellChanges.firstIndex(where: { - $0.columnIndex == columnIndex - }) { - // Update existing cell in INSERT - changes[insertIndex].cellChanges[cellIndex] = CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: nil, // INSERT doesn't have oldValue - newValue: newValue - ) - } else { - // Add new cell to INSERT (lazy - only for this edited column) - changes[insertIndex].cellChanges.append(CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: nil, - newValue: newValue - )) - } - // Push undo action for inserted row cell edit - pushUndo(.cellEdit(rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName, previousValue: oldValue, newValue: newValue)) - hasChanges = !changes.isEmpty - return - } - - // Find existing UPDATE row change or create new one - if let existingIndex = changes.firstIndex(where: { - $0.rowIndex == rowIndex && $0.type == .update - }) { - // Check if this column was already changed - if let cellIndex = changes[existingIndex].cellChanges.firstIndex(where: { - $0.columnIndex == columnIndex - }) { - // Update existing cell change, keeping original oldValue - let originalOldValue = changes[existingIndex].cellChanges[cellIndex].oldValue - changes[existingIndex].cellChanges[cellIndex] = CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: originalOldValue, - newValue: newValue - ) - - // If value is back to original, remove the change - if originalOldValue == newValue { - changes[existingIndex].cellChanges.remove(at: cellIndex) - modifiedCells.remove(key) // Remove from cache - if changes[existingIndex].cellChanges.isEmpty { - changes.remove(at: existingIndex) - } - } - } else { - changes[existingIndex].cellChanges.append(cellChange) - modifiedCells.insert(key) // Add to cache - } - } else { - // Create new RowChange with originalRow for WHERE clause PK lookup - let rowChange = RowChange( - rowIndex: rowIndex, type: .update, cellChanges: [cellChange], - originalRow: originalRow) - changes.append(rowChange) - modifiedCells.insert(key) // Add to cache - } - - // Push undo action for cell edit - pushUndo(.cellEdit(rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName, previousValue: oldValue, newValue: newValue)) - hasChanges = !changes.isEmpty - } - - func recordRowDeletion(rowIndex: Int, originalRow: [String?]) { - // Remove any pending updates for this row - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .update } - - // Clear modified cells cache for this row - modifiedCells = modifiedCells.filter { !$0.hasPrefix("\(rowIndex)-") } - - let rowChange = RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow) - changes.append(rowChange) - deletedRowIndices.insert(rowIndex) // Add to cache - pushUndo(.rowDeletion(rowIndex: rowIndex, originalRow: originalRow)) // Push undo action - hasChanges = true - reloadVersion += 1 // Trigger table reload to show red background - } - - /// Record multiple row deletions as a single undo action - /// - Parameter rows: Array of (rowIndex, originalRow) tuples, sorted by row index descending - func recordBatchRowDeletion(rows: [(rowIndex: Int, originalRow: [String?])]) { - guard rows.count > 1 else { - // Single row, use normal method - if let row = rows.first { - recordRowDeletion(rowIndex: row.rowIndex, originalRow: row.originalRow) - } - return - } - - // Collect data for batch undo before modifying state - var batchData: [(rowIndex: Int, originalRow: [String?])] = [] - - for (rowIndex, originalRow) in rows { - // Remove any pending updates for this row - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .update } - - // Clear modified cells cache for this row - modifiedCells = modifiedCells.filter { !$0.hasPrefix("\(rowIndex)-") } - - let rowChange = RowChange(rowIndex: rowIndex, type: .delete, originalRow: originalRow) - changes.append(rowChange) - deletedRowIndices.insert(rowIndex) - - batchData.append((rowIndex: rowIndex, originalRow: originalRow)) - } - - // Push a single batch undo action - pushUndo(.batchRowDeletion(rows: batchData)) - hasChanges = true - reloadVersion += 1 - } - - func recordRowInsertion(rowIndex: Int, values: [String?]) { - // OPTIMIZATION: Store row data directly without creating CellChange objects - // This eliminates expensive enumerated().map() for every column - // CellChanges will be built lazily only when needed (SQL generation or cell edits) - insertedRowData[rowIndex] = values - - // Lightweight RowChange marker with empty cellChanges array - let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: []) - changes.append(rowChange) - insertedRowIndices.insert(rowIndex) - pushUndo(.rowInsertion(rowIndex: rowIndex)) - hasChanges = true - } - - /// Undo a pending row deletion - func undoRowDeletion(rowIndex: Int) { - // SAFETY: Only process if this row is actually marked as deleted - guard deletedRowIndices.contains(rowIndex) else { - print("⚠️ undoRowDeletion called for row \(rowIndex) but it's not in deletedRowIndices") - return - } - - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .delete } - deletedRowIndices.remove(rowIndex) - hasChanges = !changes.isEmpty - reloadVersion += 1 // Trigger table reload to remove red background - } - - /// Undo a pending row insertion - func undoRowInsertion(rowIndex: Int) { - // SAFETY: Only process if this row is actually marked as inserted - guard insertedRowIndices.contains(rowIndex) else { - print("⚠️ undoRowInsertion: row \(rowIndex) not in insertedRowIndices") - return - } - - // Remove the INSERT change from the changes array - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } - insertedRowIndices.remove(rowIndex) - insertedRowData.removeValue(forKey: rowIndex) // Clear lazy storage - - // Shift down indices for rows after the removed row - var shiftedInsertedIndices = Set() - for idx in insertedRowIndices { - if idx > rowIndex { - shiftedInsertedIndices.insert(idx - 1) - } else { - shiftedInsertedIndices.insert(idx) - } - } - insertedRowIndices = shiftedInsertedIndices - - // Also update row indices in changes array for all changes after this row - for i in 0.. rowIndex { - changes[i].rowIndex -= 1 - } - } - - hasChanges = !changes.isEmpty - } - - /// Undo multiple row insertions at once (for batch deletion) - /// This is more efficient than calling undoRowInsertion multiple times - /// - Parameter rowIndices: Array of row indices to undo, MUST be sorted in descending order - func undoBatchRowInsertion(rowIndices: [Int]) { - guard !rowIndices.isEmpty else { return } - - // Verify all rows are inserted - let validRows = rowIndices.filter { insertedRowIndices.contains($0) } - - if validRows.count != rowIndices.count { - let invalidRows = Set(rowIndices).subtracting(validRows) - print("⚠️ undoBatchRowInsertion: rows \(invalidRows) not in insertedRowIndices") - } - - guard !validRows.isEmpty else { return } - - // Collect row values BEFORE removing changes (for undo/redo) - var rowValues: [[String?]] = [] - for rowIndex in validRows { - if let insertChange = changes.first(where: { $0.rowIndex == rowIndex && $0.type == .insert }) { - let values = insertChange.cellChanges.sorted { $0.columnIndex < $1.columnIndex } - .map { $0.newValue } - rowValues.append(values) - } else { - rowValues.append(Array(repeating: nil, count: columns.count)) - } - } - - // Remove all INSERT changes for these rows - for rowIndex in validRows { - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } - insertedRowIndices.remove(rowIndex) - insertedRowData.removeValue(forKey: rowIndex) // Clear lazy storage - } - - // Push undo action so user can undo this deletion - pushUndo(.batchRowInsertion(rowIndices: validRows, rowValues: rowValues)) - - // Shift indices for all remaining rows - for deletedIndex in validRows.reversed() { - var shiftedIndices = Set() - for idx in insertedRowIndices { - if idx > deletedIndex { - shiftedIndices.insert(idx - 1) - } else { - shiftedIndices.insert(idx) - } - } - insertedRowIndices = shiftedIndices - - for i in 0.. deletedIndex { - changes[i].rowIndex -= 1 - } - } - } - - hasChanges = !changes.isEmpty - } - - // MARK: - Undo Stack Management - - /// Push an undo action onto the stack - func pushUndo(_ action: UndoAction) { - undoStack.append(action) - } - - /// Pop the last undo action from the stack - func popUndo() -> UndoAction? { - undoStack.popLast() - } - - /// Clear the undo stack (called after save or discard) - func clearUndoStack() { - undoStack.removeAll() - } - - /// Clear the redo stack (called when new changes are made) - func clearRedoStack() { - redoStack.removeAll() - } - - /// Check if there are any undo actions available - var canUndo: Bool { - !undoStack.isEmpty - } - - /// Check if there are any redo actions available - var canRedo: Bool { - !redoStack.isEmpty - } - - /// Undo the last change and return details needed to update the UI - /// Returns: (action, needsRowRemoval, needsRowRestore, restoreRow) - func undoLastChange() -> (action: UndoAction, needsRowRemoval: Bool, needsRowRestore: Bool, restoreRow: [String?]?)? { - guard let action = popUndo() else { return nil } - - // Push to redo stack so we can redo this action - redoStack.append(action) - - switch action { - case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, _): - // Find and revert the cell change - if let changeIndex = changes.firstIndex(where: { - $0.rowIndex == rowIndex && ($0.type == .update || $0.type == .insert) - }) { - if let cellIndex = changes[changeIndex].cellChanges.firstIndex(where: { - $0.columnIndex == columnIndex - }) { - // For updates, restore the original value - if changes[changeIndex].type == .update { - let originalValue = changes[changeIndex].cellChanges[cellIndex].oldValue - if previousValue == originalValue { - // Value is back to original, remove the cell change - changes[changeIndex].cellChanges.remove(at: cellIndex) - modifiedCells.remove(cellKey(rowIndex: rowIndex, columnIndex: columnIndex)) - if changes[changeIndex].cellChanges.isEmpty { - changes.remove(at: changeIndex) - } - } else { - // Update cell change with previous value - let originalOldValue = changes[changeIndex].cellChanges[cellIndex].oldValue - changes[changeIndex].cellChanges[cellIndex] = CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: originalOldValue, - newValue: previousValue - ) - } - } else if changes[changeIndex].type == .insert { - // For inserts, just update the cell value - changes[changeIndex].cellChanges[cellIndex] = CellChange( - rowIndex: rowIndex, - columnIndex: columnIndex, - columnName: columnName, - oldValue: nil, - newValue: previousValue - ) - } - } - } - hasChanges = !changes.isEmpty - reloadVersion += 1 - return (action, false, false, nil) - - case .rowInsertion(let rowIndex): - // Undo the insertion by removing the row - undoRowInsertion(rowIndex: rowIndex) - return (action, true, false, nil) - - case .rowDeletion(let rowIndex, let originalRow): - // Undo the deletion by restoring the row - undoRowDeletion(rowIndex: rowIndex) - return (action, false, true, originalRow) - - case .batchRowDeletion(let rows): - // Undo all deletions in the batch (restore all rows) - // Process in reverse order to maintain correct indices - for (rowIndex, _) in rows.reversed() { - undoRowDeletion(rowIndex: rowIndex) - } - return (action, false, true, nil) - - case .batchRowInsertion(let rowIndices, let rowValues): - // Undo the deletion of inserted rows - restore them as INSERT changes - // Process in reverse order (ascending) to maintain correct indices when re-inserting - for (index, rowIndex) in rowIndices.enumerated().reversed() { - guard index < rowValues.count else { continue } - let values = rowValues[index] - - // Re-create INSERT change - let cellChanges = values.enumerated().map { colIndex, value in - CellChange( - rowIndex: rowIndex, - columnIndex: colIndex, - columnName: columns[safe: colIndex] ?? "", - oldValue: nil, - newValue: value - ) - } - let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges) - changes.append(rowChange) - insertedRowIndices.insert(rowIndex) - } - - hasChanges = !changes.isEmpty - reloadVersion += 1 - // Return true for needsRowInsert so MainContentView knows to restore to resultRows - return (action, true, false, nil) - } - } - - /// Redo the last undone change and return details needed to update the UI - /// Returns: (action, needsRowInsert, needsRowDelete) - func redoLastChange() -> (action: UndoAction, needsRowInsert: Bool, needsRowDelete: Bool)? { - guard !redoStack.isEmpty else { return nil } - let action = redoStack.removeLast() - - // Push back to undo stack so we can undo again - undoStack.append(action) - - switch action { - case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, let newValue): - // Re-apply the cell edit (previousValue becomes oldValue for the new edit) - recordCellChange(rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName, oldValue: previousValue, newValue: newValue) - // Remove the extra undo action that recordCellChange pushed - _ = undoStack.popLast() - reloadVersion += 1 - return (action, false, false) - - case .rowInsertion(let rowIndex): - // Re-apply the row insertion - we need to restore the full INSERT change - // Note: We don't have the original cell values in the UndoAction, - // so we need the caller (MainContentView) to provide them when re-inserting the row - // For now, just mark as inserted and let the caller handle cell values - insertedRowIndices.insert(rowIndex) - - // Create empty INSERT change - caller should update with actual values - // The row should already exist in resultRows from the redo handler in MainContentView - let cellChanges = columns.enumerated().map { index, columnName in - CellChange( - rowIndex: rowIndex, - columnIndex: index, - columnName: columnName, - oldValue: nil, - newValue: nil // Will be updated by caller - ) - } - let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges) - changes.append(rowChange) - - hasChanges = true - reloadVersion += 1 - return (action, true, false) - - case .rowDeletion(let rowIndex, let originalRow): - // Re-apply the deletion - recordRowDeletion(rowIndex: rowIndex, originalRow: originalRow) - // Remove the extra undo action that recordRowDeletion pushed - _ = undoStack.popLast() - return (action, false, true) - - case .batchRowDeletion(let rows): - // Re-apply all deletions in the batch - for (rowIndex, originalRow) in rows { - recordRowDeletion(rowIndex: rowIndex, originalRow: originalRow) - // Remove the extra undo action - _ = undoStack.popLast() - } - return (action, false, true) - - case .batchRowInsertion(let rowIndices, _): - // Redo the deletion of inserted rows - remove them again - // This is called when user: delete inserted rows -> undo -> redo - // We need to remove the rows from changes and insertedRowIndices again - for rowIndex in rowIndices { - changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert } - insertedRowIndices.remove(rowIndex) - } - hasChanges = !changes.isEmpty - reloadVersion += 1 - // Return true for needsRowInsert to signal MainContentView to remove from resultRows - // (We repurpose this flag since the logic is similar - rows need to be removed) - return (action, true, false) - } - } - - // MARK: - SQL Generation - - func generateSQL() -> [String] { - var statements: [String] = [] - - // Collect UPDATE and DELETE changes to batch them - var updateChanges: [RowChange] = [] - var deleteChanges: [RowChange] = [] - - for change in changes { - switch change.type { - case .update: - updateChanges.append(change) - case .insert: - // SAFETY: Verify the row is still marked as inserted - guard insertedRowIndices.contains(change.rowIndex) else { - print("⚠️ Skipping INSERT for row \(change.rowIndex) - not in insertedRowIndices") - continue - } - if let sql = generateInsertSQL(for: change) { - statements.append(sql) - } - case .delete: - // SAFETY: Verify the row is still marked as deleted - guard deletedRowIndices.contains(change.rowIndex) else { - print("⚠️ Skipping DELETE for row \(change.rowIndex) - not in deletedRowIndices") - continue - } - deleteChanges.append(change) - } - } - - // Generate batched UPDATE statements (group by same columns being updated) - if !updateChanges.isEmpty { - let batchedUpdates = generateBatchUpdateSQL(for: updateChanges) - statements.append(contentsOf: batchedUpdates) - } - - // Generate batched DELETE statement (TablePlus style: single DELETE with OR conditions) - if !deleteChanges.isEmpty { - if let sql = generateBatchDeleteSQL(for: deleteChanges) { - statements.append(sql) - } - } - - return statements - } - - /// Check if a string is a SQL function expression that should not be quoted - private func isSQLFunctionExpression(_ value: String) -> Bool { - let trimmed = value.trimmingCharacters(in: .whitespaces).uppercased() - - // Common SQL functions for datetime/timestamps - let sqlFunctions = [ - "NOW()", - "CURRENT_TIMESTAMP()", - "CURRENT_TIMESTAMP", - "CURDATE()", - "CURTIME()", - "UTC_TIMESTAMP()", - "UTC_DATE()", - "UTC_TIME()", - "LOCALTIME()", - "LOCALTIME", - "LOCALTIMESTAMP()", - "LOCALTIMESTAMP", - "SYSDATE()", - "UNIX_TIMESTAMP()", - "CURRENT_DATE()", - "CURRENT_DATE", - "CURRENT_TIME()", - "CURRENT_TIME", - ] - - return sqlFunctions.contains(trimmed) - } - - /// Generate batched UPDATE statements grouped by columns being updated - /// Example: UPDATE table SET col1 = CASE WHEN id=1 THEN 'val1' WHEN id=2 THEN 'val2' END WHERE id IN (1,2) - /// This is much more efficient than individual UPDATE statements - private func generateBatchUpdateSQL(for changes: [RowChange]) -> [String] { - guard !changes.isEmpty else { return [] } - guard let pkColumn = primaryKeyColumn else { - // Fallback to individual UPDATEs if no PK - return changes.compactMap { generateUpdateSQL(for: $0) } - } - guard let pkIndex = columns.firstIndex(of: pkColumn) else { - return changes.compactMap { generateUpdateSQL(for: $0) } - } - - // Group changes by set of columns being updated - var grouped: [[String]: [RowChange]] = [:] - for change in changes { - let columnNames = change.cellChanges.map { $0.columnName }.sorted() - grouped[columnNames, default: []].append(change) - } - - var statements: [String] = [] - - for (columnNames, groupedChanges) in grouped { - // Build CASE statements for each column - var caseClauses: [String] = [] - - for columnName in columnNames { - var whenClauses: [String] = [] - - for change in groupedChanges { - guard let originalRow = change.originalRow, - pkIndex < originalRow.count, - let cellChange = change.cellChanges.first(where: { $0.columnName == columnName }) else { - continue - } - - let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" - - // Generate value - let value: String - if cellChange.newValue == "__DEFAULT__" { - value = "DEFAULT" - } else if let newValue = cellChange.newValue { - if isSQLFunctionExpression(newValue) { - value = newValue.trimmingCharacters(in: .whitespaces).uppercased() - } else { - value = "'\(escapeSQLString(newValue))'" - } - } else { - value = "NULL" - } - - whenClauses.append("WHEN \(databaseType.quoteIdentifier(pkColumn)) = \(pkValue) THEN \(value)") - } - - if !whenClauses.isEmpty { - let caseExpr = "CASE \(whenClauses.joined(separator: " ")) END" - caseClauses.append("\(databaseType.quoteIdentifier(columnName)) = \(caseExpr)") - } - } - - // Build WHERE IN clause with all PKs - var pkValues: [String] = [] - for change in groupedChanges { - guard let originalRow = change.originalRow, - pkIndex < originalRow.count else { - continue - } - let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" - pkValues.append(pkValue) - } - - if !caseClauses.isEmpty && !pkValues.isEmpty { - let whereClause = "\(databaseType.quoteIdentifier(pkColumn)) IN (\(pkValues.joined(separator: ", ")))" - let sql = "UPDATE \(databaseType.quoteIdentifier(tableName)) SET \(caseClauses.joined(separator: ", ")) WHERE \(whereClause)" - statements.append(sql) - } - } - - return statements - } - - /// Generate individual UPDATE statement for a single row (fallback) - private func generateUpdateSQL(for change: RowChange) -> String? { - guard !change.cellChanges.isEmpty else { return nil } - - let setClauses = change.cellChanges.map { cellChange -> String in - let value: String - if cellChange.newValue == "__DEFAULT__" { - value = "DEFAULT" // SQL DEFAULT keyword - } else if let newValue = cellChange.newValue { - // Check if it's a SQL function expression - if isSQLFunctionExpression(newValue) { - value = newValue.trimmingCharacters(in: .whitespaces).uppercased() - } else { - value = "'\(escapeSQLString(newValue))'" - } - } else { - value = "NULL" - } - return "\(databaseType.quoteIdentifier(cellChange.columnName)) = \(value)" - }.joined(separator: ", ") - - // Use primary key for WHERE clause - var whereClause = "1=1" // Fallback - dangerous but necessary without PK - - if let pkColumn = primaryKeyColumn, - let pkColumnIndex = columns.firstIndex(of: pkColumn) - { - // Try to get PK value from originalRow first - if let originalRow = change.originalRow, pkColumnIndex < originalRow.count { - let pkValue = - originalRow[pkColumnIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" - whereClause = "\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)" - } - // Otherwise try from cellChanges (if PK column was edited) - else if let pkChange = change.cellChanges.first(where: { $0.columnName == pkColumn }) { - let pkValue = pkChange.oldValue.map { "'\(escapeSQLString($0))'" } ?? "NULL" - whereClause = "\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)" - } - } - - return - "UPDATE \(databaseType.quoteIdentifier(tableName)) SET \(setClauses) WHERE \(whereClause)" - } - - private func generateInsertSQL(for change: RowChange) -> String? { - // OPTIMIZATION: Get values from lazy storage instead of cellChanges - if let values = insertedRowData[change.rowIndex] { - return generateInsertSQLFromStoredData(rowIndex: change.rowIndex, values: values) - } - - // Fallback: use cellChanges if stored data not available (backward compatibility) - return generateInsertSQLFromCellChanges(for: change) - } - - /// Generate INSERT SQL from lazy-stored row data (new optimized path) - private func generateInsertSQLFromStoredData(rowIndex: Int, values: [String?]) -> String? { - var nonDefaultColumns: [String] = [] - var nonDefaultValues: [String] = [] - - for (index, value) in values.enumerated() { - // Skip DEFAULT columns - let DB handle them - if value == "__DEFAULT__" { continue } - - guard index < columns.count else { continue } - let columnName = columns[index] - - nonDefaultColumns.append(databaseType.quoteIdentifier(columnName)) - - if let val = value { - // Check if it's a SQL function expression - if isSQLFunctionExpression(val) { - nonDefaultValues.append(val.trimmingCharacters(in: .whitespaces).uppercased()) - } else { - nonDefaultValues.append("'\(escapeSQLString(val))'") - } - } else { - nonDefaultValues.append("NULL") - } - } - - // If all columns are DEFAULT, don't generate INSERT - guard !nonDefaultColumns.isEmpty else { return nil } - - let columnList = nonDefaultColumns.joined(separator: ", ") - let valueList = nonDefaultValues.joined(separator: ", ") - - return "INSERT INTO \(databaseType.quoteIdentifier(tableName)) (\(columnList)) VALUES (\(valueList))" - } - - /// Generate INSERT SQL from cellChanges (fallback for backward compatibility) - private func generateInsertSQLFromCellChanges(for change: RowChange) -> String? { - guard !change.cellChanges.isEmpty else { return nil } - - // Filter out DEFAULT columns - let DB handle them - let nonDefaultChanges = change.cellChanges.filter { - $0.newValue != "__DEFAULT__" - } - - // If all columns are DEFAULT, don't generate INSERT - // (user hasn't modified anything - just added empty row) - guard !nonDefaultChanges.isEmpty else { return nil } - - let columnNames = nonDefaultChanges.map { - databaseType.quoteIdentifier($0.columnName) - }.joined(separator: ", ") - - let values = nonDefaultChanges.map { cellChange -> String in - if let newValue = cellChange.newValue { - // Check if it's a SQL function expression - if isSQLFunctionExpression(newValue) { - return newValue.trimmingCharacters(in: .whitespaces).uppercased() - } - return "'\(escapeSQLString(newValue))'" - } - return "NULL" - }.joined(separator: ", ") - - return - "INSERT INTO \(databaseType.quoteIdentifier(tableName)) (\(columnNames)) VALUES (\(values))" - } - - /// Generate a batched DELETE statement combining multiple rows with OR conditions - /// Example: DELETE FROM table WHERE id = 1 OR id = 2 OR id = 3 - /// This is much more efficient than individual DELETE statements - private func generateBatchDeleteSQL(for changes: [RowChange]) -> String? { - guard !changes.isEmpty else { return nil } - guard let pkColumn = primaryKeyColumn else { return nil } - guard let pkIndex = columns.firstIndex(of: pkColumn) else { return nil } - - // Build OR conditions for all rows - var conditions: [String] = [] - - for change in changes { - guard let originalRow = change.originalRow, - pkIndex < originalRow.count else { - continue - } - - let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" - conditions.append("\(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)") - } - - guard !conditions.isEmpty else { return nil } - - // Combine all conditions with OR - let whereClause = conditions.joined(separator: " OR ") - return "DELETE FROM \(databaseType.quoteIdentifier(tableName)) WHERE \(whereClause)" - } - - /// Generate individual DELETE statement for a single row (kept for compatibility) - private func generateDeleteSQL(for change: RowChange) -> String? { - guard let pkColumn = primaryKeyColumn, - let originalRow = change.originalRow, - let pkIndex = columns.firstIndex(of: pkColumn), - pkIndex < originalRow.count - else { - return nil - } - - let pkValue = originalRow[pkIndex].map { "'\(escapeSQLString($0))'" } ?? "NULL" - return - "DELETE FROM \(databaseType.quoteIdentifier(tableName)) WHERE \(databaseType.quoteIdentifier(pkColumn)) = \(pkValue)" - } - - private func escapeSQLString(_ str: String) -> String { - // Escape characters that can break SQL strings - var result = str - result = result.replacingOccurrences(of: "\\", with: "\\\\") // Backslash first - result = result.replacingOccurrences(of: "'", with: "''") // Single quote - result = result.replacingOccurrences(of: "\n", with: "\\n") // Newline - result = result.replacingOccurrences(of: "\r", with: "\\r") // Carriage return - result = result.replacingOccurrences(of: "\t", with: "\\t") // Tab - result = result.replacingOccurrences(of: "\0", with: "\\0") // Null byte - return result - } - - // MARK: - Actions - - /// Returns all original cell values that need to be restored - /// Format: [(rowIndex, columnIndex, originalValue)] - func getOriginalValues() -> [(rowIndex: Int, columnIndex: Int, value: String?)] { - var originals: [(rowIndex: Int, columnIndex: Int, value: String?)] = [] - - for change in changes { - if change.type == .update { - for cellChange in change.cellChanges { - originals.append( - ( - rowIndex: change.rowIndex, - columnIndex: cellChange.columnIndex, - value: cellChange.oldValue - )) - } - } - } - - return originals - } - - func discardChanges() { - changes.removeAll() - deletedRowIndices.removeAll() // Clear cache - insertedRowIndices.removeAll() // Clear cache - modifiedCells.removeAll() // Clear cache - insertedRowData.removeAll() // Clear lazy storage - hasChanges = false - reloadVersion += 1 // Trigger table reload - } - - // MARK: - Per-Tab State Management - - /// Save current state to a TabPendingChanges struct for storage in a tab - func saveState() -> TabPendingChanges { - var state = TabPendingChanges() - state.changes = changes - state.deletedRowIndices = deletedRowIndices - state.insertedRowIndices = insertedRowIndices - state.modifiedCells = modifiedCells - state.insertedRowData = insertedRowData // Save lazy storage - state.primaryKeyColumn = primaryKeyColumn - state.columns = columns - return state - } - - /// Restore state from a TabPendingChanges struct - func restoreState(from state: TabPendingChanges, tableName: String) { - self.tableName = tableName - self.changes = state.changes - self.deletedRowIndices = state.deletedRowIndices - self.insertedRowIndices = state.insertedRowIndices - self.modifiedCells = state.modifiedCells - self.insertedRowData = state.insertedRowData // Restore lazy storage - self.primaryKeyColumn = state.primaryKeyColumn - self.columns = state.columns - self.hasChanges = !state.changes.isEmpty - } - - /// O(1) lookup for deleted rows using cached Set - func isRowDeleted(_ rowIndex: Int) -> Bool { - deletedRowIndices.contains(rowIndex) - } - - /// O(1) lookup for inserted rows using cached Set - func isRowInserted(_ rowIndex: Int) -> Bool { - insertedRowIndices.contains(rowIndex) - } - - /// O(1) lookup for modified cells using cached Set - func isCellModified(rowIndex: Int, columnIndex: Int) -> Bool { - modifiedCells.contains(cellKey(rowIndex: rowIndex, columnIndex: columnIndex)) - } - - /// Returns a Set of column indices that are modified for a given row - /// Used for efficient batch lookup in TableRowView - func getModifiedColumnsForRow(_ rowIndex: Int) -> Set { - var result: Set = [] - let prefix = "\(rowIndex)-" - for key in modifiedCells { - if key.hasPrefix(prefix) { - if let colIndex = Int(key.dropFirst(prefix.count)) { - result.insert(colIndex) - } - } - } - return result - } -} - -// MARK: - Array Extension - -extension Array { - subscript(safe index: Int) -> Element? { - indices.contains(index) ? self[index] : nil - } -} diff --git a/OpenTable/Views/Editor/HistoryListViewController.swift b/OpenTable/Views/Editor/HistoryListViewController.swift deleted file mode 100644 index fee62ead..00000000 --- a/OpenTable/Views/Editor/HistoryListViewController.swift +++ /dev/null @@ -1,1325 +0,0 @@ -// -// HistoryListViewController.swift -// OpenTable -// -// Left pane controller for history/bookmark list with search and filtering -// - -import AppKit - -// MARK: - Delegate Protocol - -protocol HistoryListViewControllerDelegate: AnyObject { - func historyListViewController(_ controller: HistoryListViewController, didSelectHistoryEntry entry: QueryHistoryEntry) - func historyListViewController(_ controller: HistoryListViewController, didSelectBookmark bookmark: QueryBookmark) - func historyListViewController(_ controller: HistoryListViewController, didDoubleClickHistoryEntry entry: QueryHistoryEntry) - func historyListViewController(_ controller: HistoryListViewController, didDoubleClickBookmark bookmark: QueryBookmark) - func historyListViewControllerDidClearSelection(_ controller: HistoryListViewController) -} - -// MARK: - Display Mode - -enum HistoryDisplayMode: Int { - case history = 0 - case bookmarks = 1 -} - -// MARK: - UI Date Filter (maps to DateFilter from QueryHistoryStorage) - -enum UIDateFilter: Int { - case today = 0 - case week = 1 - case month = 2 - case all = 3 - - var title: String { - switch self { - case .today: return "Today" - case .week: return "This Week" - case .month: return "This Month" - case .all: return "All Time" - } - } - - /// Convert to storage DateFilter - var toDateFilter: DateFilter { - switch self { - case .today: return .today - case .week: return .thisWeek - case .month: return .thisMonth - case .all: return .all - } - } -} - -// MARK: - HistoryListViewController - -final class HistoryListViewController: NSViewController, NSMenuItemValidation { - - // MARK: - Properties - - weak var delegate: HistoryListViewControllerDelegate? - - private var displayMode: HistoryDisplayMode = .history { - didSet { - if oldValue != displayMode { - updateFilterVisibility() - loadData() - } - } - } - - private var dateFilter: UIDateFilter = .all { - didSet { - if oldValue != dateFilter { - loadData() - } - } - } - - private var searchText: String = "" { - didSet { - scheduleSearch() - } - } - - private var historyEntries: [QueryHistoryEntry] = [] - private var bookmarks: [QueryBookmark] = [] - - private var searchTask: DispatchWorkItem? - private let searchDebounceInterval: TimeInterval = 0.15 - - // Track pending deletion for smart selection - private var pendingDeletionRow: Int? - private var pendingDeletionCount: Int? - - // MARK: - UI Components - - private let headerView: NSVisualEffectView = { - let view = NSVisualEffectView() - view.material = .headerView - view.blendingMode = .withinWindow - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private lazy var modeSegment: NSSegmentedControl = { - let segment = NSSegmentedControl(labels: ["History", "Bookmarks"], trackingMode: .selectOne, target: self, action: #selector(modeChanged(_:))) - segment.selectedSegment = 0 - segment.translatesAutoresizingMaskIntoConstraints = false - segment.controlSize = .small - return segment - }() - - private lazy var searchField: NSSearchField = { - let field = NSSearchField() - field.placeholderString = "Search queries..." - field.delegate = self - field.translatesAutoresizingMaskIntoConstraints = false - field.controlSize = .small - return field - }() - - private lazy var filterButton: NSPopUpButton = { - let button = NSPopUpButton(frame: .zero, pullsDown: false) - button.controlSize = .small - button.translatesAutoresizingMaskIntoConstraints = false - - for filter in [UIDateFilter.today, .week, .month, .all] { - button.addItem(withTitle: filter.title) - } - button.selectItem(at: UIDateFilter.all.rawValue) - button.target = self - button.action = #selector(filterChanged(_:)) - return button - }() - - private lazy var clearAllButton: NSButton = { - let button = NSButton() - button.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Clear All") - button.bezelStyle = .shadowlessSquare - button.isBordered = false - button.imagePosition = .imageOnly - button.translatesAutoresizingMaskIntoConstraints = false - button.target = self - button.action = #selector(clearAllClicked(_:)) - button.toolTip = "Clear all \(displayMode == .history ? "history" : "bookmarks")" - return button - }() - - - private let scrollView: NSScrollView = { - let scroll = NSScrollView() - scroll.hasVerticalScroller = true - scroll.hasHorizontalScroller = false - scroll.autohidesScrollers = true - scroll.borderType = .noBorder - scroll.translatesAutoresizingMaskIntoConstraints = false - scroll.drawsBackground = false - return scroll - }() - - private lazy var tableView: HistoryTableView = { - let table = HistoryTableView() - table.style = .plain - table.headerView = nil - table.rowHeight = 56 - table.intercellSpacing = NSSize(width: 0, height: 1) - table.backgroundColor = .clear - table.usesAlternatingRowBackgroundColors = false - table.allowsMultipleSelection = false - table.delegate = self - table.dataSource = self - table.doubleAction = #selector(tableViewDoubleClick(_:)) - table.target = self - table.keyboardDelegate = self // Set keyboard delegate - - let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("MainColumn")) - column.width = 300 - table.addTableColumn(column) - - return table - }() - - private lazy var emptyStateView: NSView = { - let container = NSView() - container.translatesAutoresizingMaskIntoConstraints = false - container.isHidden = true - - let stackView = NSStackView() - stackView.orientation = .vertical - stackView.alignment = .centerX - stackView.spacing = 8 - stackView.translatesAutoresizingMaskIntoConstraints = false - - let imageView = NSImageView() - imageView.imageScaling = .scaleProportionallyUpOrDown - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 48), - imageView.heightAnchor.constraint(equalToConstant: 48) - ]) - imageView.contentTintColor = .tertiaryLabelColor - self.emptyImageView = imageView - - let titleLabel = NSTextField(labelWithString: "") - titleLabel.font = .systemFont(ofSize: 14, weight: .medium) - titleLabel.textColor = .secondaryLabelColor - titleLabel.alignment = .center - self.emptyTitleLabel = titleLabel - - let subtitleLabel = NSTextField(labelWithString: "") - subtitleLabel.font = .systemFont(ofSize: 12) - subtitleLabel.textColor = .tertiaryLabelColor - subtitleLabel.alignment = .center - subtitleLabel.maximumNumberOfLines = 2 - subtitleLabel.lineBreakMode = .byWordWrapping - subtitleLabel.preferredMaxLayoutWidth = 200 - self.emptySubtitleLabel = subtitleLabel - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(subtitleLabel) - - container.addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: container.centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: container.centerYAnchor) - ]) - - return container - }() - - private weak var emptyImageView: NSImageView? - private weak var emptyTitleLabel: NSTextField? - private weak var emptySubtitleLabel: NSTextField? - - // MARK: - Lifecycle - - override func loadView() { - view = NSView() - view.wantsLayer = true - } - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupNotifications() - restoreState() - loadData() - } - - // MARK: - Setup - - private func setupUI() { - // Header - view.addSubview(headerView) - - let headerStack = NSStackView() - headerStack.orientation = .vertical - headerStack.spacing = 8 - headerStack.translatesAutoresizingMaskIntoConstraints = false - headerStack.edgeInsets = NSEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) - - let topRow = NSStackView(views: [modeSegment, NSView(), clearAllButton, filterButton]) - topRow.distribution = .fill - topRow.spacing = 8 - - headerStack.addArrangedSubview(topRow) - headerStack.addArrangedSubview(searchField) - - headerView.addSubview(headerStack) - - // Divider - let divider = NSBox() - divider.boxType = .separator - divider.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(divider) - - // Scroll view with table - scrollView.documentView = tableView - view.addSubview(scrollView) - - // Empty state (overlays scroll view) - view.addSubview(emptyStateView) - - NSLayoutConstraint.activate([ - // Header - headerView.topAnchor.constraint(equalTo: view.topAnchor), - headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - headerStack.topAnchor.constraint(equalTo: headerView.topAnchor), - headerStack.leadingAnchor.constraint(equalTo: headerView.leadingAnchor), - headerStack.trailingAnchor.constraint(equalTo: headerView.trailingAnchor), - headerStack.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), - - // Divider - divider.topAnchor.constraint(equalTo: headerView.bottomAnchor), - divider.leadingAnchor.constraint(equalTo: view.leadingAnchor), - divider.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - // Scroll view - scrollView.topAnchor.constraint(equalTo: divider.bottomAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - // Empty state - emptyStateView.topAnchor.constraint(equalTo: scrollView.topAnchor), - emptyStateView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - emptyStateView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - emptyStateView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) - ]) - - updateFilterVisibility() - } - - private func setupNotifications() { - NotificationCenter.default.addObserver( - self, - selector: #selector(historyDidUpdate), - name: .queryHistoryDidUpdate, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(bookmarksDidUpdate), - name: .queryBookmarksDidUpdate, - object: nil - ) - } - - // MARK: - State Persistence - - private func restoreState() { - let savedMode = UserDefaults.standard.integer(forKey: "HistoryPanel.displayMode") - let savedFilter = UserDefaults.standard.integer(forKey: "HistoryPanel.dateFilter") - - if let mode = HistoryDisplayMode(rawValue: savedMode) { - displayMode = mode - modeSegment.selectedSegment = mode.rawValue - } - - if let filter = UIDateFilter(rawValue: savedFilter) { - dateFilter = filter - filterButton.selectItem(at: filter.rawValue) - } - } - - private func saveState() { - UserDefaults.standard.set(displayMode.rawValue, forKey: "HistoryPanel.displayMode") - UserDefaults.standard.set(dateFilter.rawValue, forKey: "HistoryPanel.dateFilter") - } - - // MARK: - Data Loading - - private func loadData() { - switch displayMode { - case .history: - loadHistory() - case .bookmarks: - loadBookmarks() - } - } - - private func loadHistory() { - historyEntries = QueryHistoryManager.shared.fetchHistory( - limit: 500, - offset: 0, - connectionId: nil, - searchText: searchText.isEmpty ? nil : searchText, - dateFilter: dateFilter.toDateFilter - ) - - - tableView.reloadData() - updateEmptyState() - - // Handle pending deletion selection - if let deletedRow = pendingDeletionRow, let countBefore = pendingDeletionCount { - selectRowAfterDeletion(deletedRow: deletedRow, countBefore: countBefore) - pendingDeletionRow = nil - pendingDeletionCount = nil - } else if tableView.selectedRow < 0 { - // Clear preview if no selection - delegate?.historyListViewControllerDidClearSelection(self) - } - } - - private func loadBookmarks() { - bookmarks = QueryHistoryManager.shared.fetchBookmarks( - searchText: searchText.isEmpty ? nil : searchText, - tag: nil - ) - - - tableView.reloadData() - updateEmptyState() - - // Handle pending deletion selection - if let deletedRow = pendingDeletionRow, let countBefore = pendingDeletionCount { - selectRowAfterDeletion(deletedRow: deletedRow, countBefore: countBefore) - pendingDeletionRow = nil - pendingDeletionCount = nil - } else if tableView.selectedRow < 0 { - // Clear preview if no selection - delegate?.historyListViewControllerDidClearSelection(self) - } - } - - // MARK: - Search - - private func scheduleSearch() { - searchTask?.cancel() - - let task = DispatchWorkItem { [weak self] in - self?.loadData() - } - searchTask = task - - DispatchQueue.main.asyncAfter(deadline: .now() + searchDebounceInterval, execute: task) - } - - // MARK: - Actions - - @objc private func modeChanged(_ sender: NSSegmentedControl) { - if let mode = HistoryDisplayMode(rawValue: sender.selectedSegment) { - displayMode = mode - saveState() - } - } - - @objc private func filterChanged(_ sender: NSPopUpButton) { - if let filter = UIDateFilter(rawValue: sender.indexOfSelectedItem) { - dateFilter = filter - saveState() - } - } - - @objc private func tableViewDoubleClick(_ sender: Any) { - let row = tableView.clickedRow - guard row >= 0 else { return } - - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - delegate?.historyListViewController(self, didDoubleClickHistoryEntry: historyEntries[row]) - case .bookmarks: - guard row < bookmarks.count else { return } - delegate?.historyListViewController(self, didDoubleClickBookmark: bookmarks[row]) - } - } - - @objc private func historyDidUpdate() { - if displayMode == .history { - loadData() - } - } - - @objc private func bookmarksDidUpdate() { - if displayMode == .bookmarks { - loadData() - } - } - - // MARK: - UI Updates - - private func updateFilterVisibility() { - filterButton.isHidden = displayMode == .bookmarks - searchField.placeholderString = displayMode == .history ? "Search queries..." : "Search bookmarks..." - } - - private func updateEmptyState() { - let isEmpty: Bool - switch displayMode { - case .history: - isEmpty = historyEntries.isEmpty - case .bookmarks: - isEmpty = bookmarks.isEmpty - } - - emptyStateView.isHidden = !isEmpty - scrollView.isHidden = isEmpty - - guard isEmpty else { return } - - let isSearching = !searchText.isEmpty - - if isSearching { - emptyImageView?.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "No results") - emptyTitleLabel?.stringValue = "No Matching Queries" - emptySubtitleLabel?.stringValue = "Try adjusting your search terms\nor date filter." - } else { - switch displayMode { - case .history: - emptyImageView?.image = NSImage(systemSymbolName: "clock.arrow.circlepath", accessibilityDescription: "No history") - emptyTitleLabel?.stringValue = "No Query History Yet" - emptySubtitleLabel?.stringValue = "Your executed queries will\nappear here for quick access." - case .bookmarks: - emptyImageView?.image = NSImage(systemSymbolName: "bookmark", accessibilityDescription: "No bookmarks") - emptyTitleLabel?.stringValue = "No Bookmarks Yet" - emptySubtitleLabel?.stringValue = "Save frequently used queries\nusing Cmd+Shift+B." - } - } - } - - // MARK: - Context Menu - - private func buildContextMenu(for row: Int) -> NSMenu { - let menu = NSMenu() - - let copyItem = NSMenuItem(title: "Copy Query", action: #selector(copyQuery(_:)), keyEquivalent: "c") - copyItem.keyEquivalentModifierMask = .command - copyItem.tag = row - menu.addItem(copyItem) - - let runItem = NSMenuItem(title: "Run in New Tab", action: #selector(runInNewTab(_:)), keyEquivalent: "\r") - runItem.tag = row - menu.addItem(runItem) - - menu.addItem(NSMenuItem.separator()) - - switch displayMode { - case .history: - let bookmarkItem = NSMenuItem(title: "Save as Bookmark...", action: #selector(saveAsBookmark(_:)), keyEquivalent: "") - bookmarkItem.tag = row - menu.addItem(bookmarkItem) - case .bookmarks: - let editItem = NSMenuItem(title: "Edit Bookmark...", action: #selector(editBookmark(_:)), keyEquivalent: "e") - editItem.keyEquivalentModifierMask = .command - editItem.tag = row - menu.addItem(editItem) - } - - menu.addItem(NSMenuItem.separator()) - - let deleteItem = NSMenuItem(title: "Delete", action: #selector(deleteEntry(_:)), keyEquivalent: "\u{8}") - deleteItem.keyEquivalentModifierMask = [] - deleteItem.tag = row - menu.addItem(deleteItem) - - return menu - } - - @objc private func copyQuery(_ sender: NSMenuItem) { - let row = sender.tag - let query: String - - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - query = historyEntries[row].query - case .bookmarks: - guard row < bookmarks.count else { return } - query = bookmarks[row].query - } - - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(query, forType: .string) - } - - @objc private func runInNewTab(_ sender: NSMenuItem) { - let row = sender.tag - let query: String - - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - query = historyEntries[row].query - case .bookmarks: - guard row < bookmarks.count else { return } - query = bookmarks[row].query - QueryHistoryManager.shared.markBookmarkUsed(id: bookmarks[row].id) - } - - NotificationCenter.default.post(name: .newTab, object: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NotificationCenter.default.post( - name: .loadQueryIntoEditor, - object: nil, - userInfo: ["query": query] - ) - } - } - - @objc private func saveAsBookmark(_ sender: NSMenuItem) { - let row = sender.tag - guard row < historyEntries.count else { return } - let entry = historyEntries[row] - - let editor = BookmarkEditorController( - bookmark: nil, - query: entry.query, - connectionId: entry.connectionId - ) - - editor.onSave = { [weak self] bookmark in - let success = QueryHistoryManager.shared.saveBookmark( - name: bookmark.name, - query: bookmark.query, - connectionId: bookmark.connectionId, - tags: bookmark.tags, - notes: bookmark.notes - ) - - if success { - } else { - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Failed to Save Bookmark" - alert.informativeText = "Could not save the bookmark to storage. Please try again." - alert.alertStyle = .warning - alert.addButton(withTitle: "OK") - alert.runModal() - } - } - } - - view.window?.contentViewController?.presentAsSheet(editor) - } - - @objc private func editBookmark(_ sender: NSMenuItem) { - let row = sender.tag - guard row < bookmarks.count else { return } - let bookmark = bookmarks[row] - - let editorView = BookmarkEditorView( - bookmark: bookmark, - query: bookmark.query, - connectionId: bookmark.connectionId - ) { [weak self] updatedBookmark in - let success = QueryHistoryManager.shared.updateBookmark(updatedBookmark) - - if success { - } else { - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Failed to Update Bookmark" - alert.informativeText = "Could not save changes to the bookmark. Please try again." - alert.alertStyle = .warning - alert.addButton(withTitle: "OK") - alert.runModal() - } - } - } - - presentAsSheet(editorView) - } - - @objc private func deleteEntry(_ sender: NSMenuItem) { - let row = sender.tag - - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - let entry = historyEntries[row] - QueryHistoryManager.shared.deleteHistory(id: entry.id) - case .bookmarks: - guard row < bookmarks.count else { return } - let bookmark = bookmarks[row] - QueryHistoryManager.shared.deleteBookmark(id: bookmark.id) - } - } - - @objc private func clearAllClicked(_ sender: Any?) { - let count: Int - let itemName: String - - switch displayMode { - case .history: - count = historyEntries.count - itemName = count == 1 ? "history entry" : "history entries" - case .bookmarks: - count = bookmarks.count - itemName = count == 1 ? "bookmark" : "bookmarks" - } - - guard count > 0 else { return } - - let alert = NSAlert() - alert.messageText = "Clear All \(displayMode == .history ? "History" : "Bookmarks")?" - alert.informativeText = "This will permanently delete \(count) \(itemName). This action cannot be undone." - alert.alertStyle = .warning - alert.addButton(withTitle: "Clear All") - alert.addButton(withTitle: "Cancel") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - switch displayMode { - case .history: - _ = QueryHistoryManager.shared.clearAllHistory() - case .bookmarks: - _ = QueryHistoryManager.shared.clearAllBookmarks() - } - } - } -} - -// MARK: - NSTableViewDataSource - -extension HistoryListViewController: NSTableViewDataSource { - - func numberOfRows(in tableView: NSTableView) -> Int { - switch displayMode { - case .history: - return historyEntries.count - case .bookmarks: - return bookmarks.count - } - } -} - -// MARK: - NSTableViewDelegate - -extension HistoryListViewController: NSTableViewDelegate { - - func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - switch displayMode { - case .history: - return historyCell(for: row) - case .bookmarks: - return bookmarkCell(for: row) - } - } - - private func historyCell(for row: Int) -> NSView? { - guard row < historyEntries.count else { return nil } - let entry = historyEntries[row] - - let identifier = NSUserInterfaceItemIdentifier("HistoryCell") - let cell = tableView.makeView(withIdentifier: identifier, owner: nil) as? HistoryRowView - ?? HistoryRowView() - cell.identifier = identifier - cell.configureForHistory(entry) - return cell - } - - private func bookmarkCell(for row: Int) -> NSView? { - guard row < bookmarks.count else { return nil } - let bookmark = bookmarks[row] - - let identifier = NSUserInterfaceItemIdentifier("BookmarkCell") - let cell = tableView.makeView(withIdentifier: identifier, owner: nil) as? HistoryRowView - ?? HistoryRowView() - cell.identifier = identifier - cell.configureForBookmark(bookmark) - return cell - } - - func tableViewSelectionDidChange(_ notification: Notification) { - let row = tableView.selectedRow - guard row >= 0 else { - delegate?.historyListViewControllerDidClearSelection(self) - return - } - - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - delegate?.historyListViewController(self, didSelectHistoryEntry: historyEntries[row]) - case .bookmarks: - guard row < bookmarks.count else { return } - delegate?.historyListViewController(self, didSelectBookmark: bookmarks[row]) - } - } - - func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableView.RowActionEdge) -> [NSTableViewRowAction] { - if edge == .trailing { - let delete = NSTableViewRowAction(style: .destructive, title: "Delete") { [weak self] _, row in - self?.deleteEntryAtRow(row) - } - return [delete] - } - return [] - } - - private func deleteEntryAtRow(_ row: Int) { - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - QueryHistoryManager.shared.deleteHistory(id: historyEntries[row].id) - case .bookmarks: - guard row < bookmarks.count else { return } - QueryHistoryManager.shared.deleteBookmark(id: bookmarks[row].id) - } - } -} - -// MARK: - NSSearchFieldDelegate - -extension HistoryListViewController: NSSearchFieldDelegate { - - func controlTextDidChange(_ obj: Notification) { - if let field = obj.object as? NSSearchField { - searchText = field.stringValue - } - } - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - if commandSelector == #selector(cancelOperation(_:)) { - if !searchText.isEmpty { - searchField.stringValue = "" - searchText = "" - return true - } - } - return false - } -} - -// MARK: - NSMenuDelegate (Context Menu) - -extension HistoryListViewController { - - override func rightMouseDown(with event: NSEvent) { - let point = tableView.convert(event.locationInWindow, from: nil) - let row = tableView.row(at: point) - - if row >= 0 { - tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) - let menu = buildContextMenu(for: row) - NSMenu.popUpContextMenu(menu, with: event, for: tableView) - } - } - - // MARK: - Helper Methods - - func handleDeleteKey() { - deleteSelectedRow() - } - - /// Standard delete action for menu integration - @objc func delete(_ sender: Any?) { - deleteSelectedRow() - } - - /// Standard copy action for menu integration (Cmd+C) - @objc func copy(_ sender: Any?) { - copyQueryForSelectedRow() - } - - /// Validate menu items - func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { - if menuItem.action == #selector(delete(_:)) { - let hasSelection = tableView.selectedRow >= 0 - let hasItems = displayMode == .history ? historyEntries.count > 0 : bookmarks.count > 0 - return hasSelection && hasItems - } - if menuItem.action == #selector(copy(_:)) { - return tableView.selectedRow >= 0 - } - return true - } - - // MARK: - Keyboard Actions - - /// Handle Return/Enter key - open selected item in new tab - func handleReturnKey() { - runInNewTabForSelectedRow() - } - - /// Handle Space key - toggle preview (currently just copies to show it's working) - func handleSpaceKey() { - // TODO: Implement preview panel toggle - // For now, just show the query text in a temporary way - let row = tableView.selectedRow - guard row >= 0 else { return } - - let query: String - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - query = historyEntries[row].query - case .bookmarks: - guard row < bookmarks.count else { return } - query = bookmarks[row].query - } - - // Preview panel will be implemented in a future update - } - - /// Handle Cmd+E - edit bookmark - func handleEditBookmark() { - guard displayMode == .bookmarks else { return } - editBookmarkForSelectedRow() - } - - /// Handle Escape key - clear search or selection - func handleEscapeKey() { - // If search field has text, clear it - if !searchText.isEmpty { - searchField.stringValue = "" - searchText = "" - searchField.window?.makeFirstResponder(tableView) - } else if tableView.selectedRow >= 0 { - // Otherwise clear selection - tableView.deselectAll(nil) - } - } - - // MARK: - Keyboard Shortcut Helpers - - private func copyQueryForSelectedRow() { - let row = tableView.selectedRow - guard row >= 0 else { return } - - - let query: String - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - query = historyEntries[row].query - case .bookmarks: - guard row < bookmarks.count else { return } - query = bookmarks[row].query - } - - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(query, forType: .string) - } - - private func runInNewTabForSelectedRow() { - let row = tableView.selectedRow - guard row >= 0 else { - return - } - - - let query: String - switch displayMode { - case .history: - guard row < historyEntries.count else { - return - } - query = historyEntries[row].query - case .bookmarks: - guard row < bookmarks.count else { - return - } - query = bookmarks[row].query - QueryHistoryManager.shared.markBookmarkUsed(id: bookmarks[row].id) - } - - NotificationCenter.default.post(name: .newTab, object: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NotificationCenter.default.post( - name: .loadQueryIntoEditor, - object: nil, - userInfo: ["query": query] - ) - } - } - - private func saveAsBookmarkForSelectedRow() { - let row = tableView.selectedRow - guard displayMode == .history, row >= 0, row < historyEntries.count else { return } - - let entry = historyEntries[row] - let editorView = BookmarkEditorView( - query: entry.query, - connectionId: entry.connectionId - ) { [weak self] bookmark in - let success = QueryHistoryManager.shared.saveBookmark( - name: bookmark.name, - query: bookmark.query, - connectionId: bookmark.connectionId, - tags: bookmark.tags, - notes: bookmark.notes - ) - - if success { - } else { - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Failed to Save Bookmark" - alert.informativeText = "Could not save the bookmark to storage. Please try again." - alert.alertStyle = .warning - alert.addButton(withTitle: "OK") - alert.runModal() - } - } - } - - presentAsSheet(editorView) - } - - private func editBookmarkForSelectedRow() { - let row = tableView.selectedRow - guard displayMode == .bookmarks, row >= 0, row < bookmarks.count else { return } - - let bookmark = bookmarks[row] - let editorView = BookmarkEditorView( - bookmark: bookmark, - query: bookmark.query, - connectionId: bookmark.connectionId - ) { [weak self] updatedBookmark in - let success = QueryHistoryManager.shared.updateBookmark(updatedBookmark) - - if success { - } else { - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Failed to Update Bookmark" - alert.informativeText = "Could not save changes to the bookmark. Please try again." - alert.alertStyle = .warning - alert.addButton(withTitle: "OK") - alert.runModal() - } - } - } - - presentAsSheet(editorView) - } - - func deleteSelectedRow() { - let row = tableView.selectedRow - guard row >= 0 else { return } - - - // Store the count before deletion and row for smart selection - let countBeforeDeletion: Int - switch displayMode { - case .history: - countBeforeDeletion = historyEntries.count - case .bookmarks: - countBeforeDeletion = bookmarks.count - } - - // Store for selection after reload - pendingDeletionRow = row - pendingDeletionCount = countBeforeDeletion - - // Perform deletion - switch displayMode { - case .history: - guard row < historyEntries.count else { return } - let entryId = historyEntries[row].id - QueryHistoryManager.shared.deleteHistory(id: entryId) - // Selection will happen in loadHistory() after notification - - case .bookmarks: - guard row < bookmarks.count else { return } - let bookmarkId = bookmarks[row].id - let bookmarkName = bookmarks[row].name - QueryHistoryManager.shared.deleteBookmark(id: bookmarkId) - // Selection will happen in loadBookmarks() if notification triggers, - // otherwise do it manually - } - } - - /// Select an appropriate row after deletion - /// Selects the next row, or the previous row if the last item was deleted - private func selectRowAfterDeletion(deletedRow: Int, countBefore: Int) { - let currentCount: Int - switch displayMode { - case .history: - currentCount = historyEntries.count - case .bookmarks: - currentCount = bookmarks.count - } - - // If list is now empty, clear selection and delegate - guard currentCount > 0 else { - tableView.deselectAll(nil) - delegate?.historyListViewControllerDidClearSelection(self) - return - } - - // Select next item if available, otherwise select previous - let newSelection: Int - if deletedRow < currentCount { - // Next item moved into this position - newSelection = deletedRow - } else { - // Deleted last item, select new last item - newSelection = currentCount - 1 - } - - tableView.selectRowIndexes(IndexSet(integer: newSelection), byExtendingSelection: false) - tableView.scrollRowToVisible(newSelection) - - // Notify delegate of new selection - switch displayMode { - case .history: - if newSelection < historyEntries.count { - delegate?.historyListViewController(self, didSelectHistoryEntry: historyEntries[newSelection]) - } - case .bookmarks: - if newSelection < bookmarks.count { - delegate?.historyListViewController(self, didSelectBookmark: bookmarks[newSelection]) - } - } - } -} - -// MARK: - HistoryRowView - -final class HistoryRowView: NSTableCellView { - - private let statusIcon: NSImageView = { - let imageView = NSImageView() - imageView.imageScaling = .scaleProportionallyUpOrDown - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private let queryLabel: NSTextField = { - let label = NSTextField(labelWithString: "") - label.font = .monospacedSystemFont(ofSize: 11, weight: .regular) - label.textColor = .labelColor - label.lineBreakMode = .byTruncatingTail - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private let secondaryLabel: NSTextField = { - let label = NSTextField(labelWithString: "") - label.font = .systemFont(ofSize: 10) - label.textColor = .secondaryLabelColor - label.lineBreakMode = .byTruncatingTail - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private let timeLabel: NSTextField = { - let label = NSTextField(labelWithString: "") - label.font = .systemFont(ofSize: 10) - label.textColor = .tertiaryLabelColor - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private let durationLabel: NSTextField = { - let label = NSTextField(labelWithString: "") - label.font = .systemFont(ofSize: 10) - label.textColor = .tertiaryLabelColor - label.alignment = .right - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var isSetup = false - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setupViews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupViews() - } - - private func setupViews() { - guard !isSetup else { return } - isSetup = true - - addSubview(statusIcon) - addSubview(queryLabel) - addSubview(secondaryLabel) - addSubview(timeLabel) - addSubview(durationLabel) - - NSLayoutConstraint.activate([ - // Status icon - statusIcon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), - statusIcon.topAnchor.constraint(equalTo: topAnchor, constant: 10), - statusIcon.widthAnchor.constraint(equalToConstant: 14), - statusIcon.heightAnchor.constraint(equalToConstant: 14), - - // Query label (first line) - queryLabel.leadingAnchor.constraint(equalTo: statusIcon.trailingAnchor, constant: 8), - queryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), - queryLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), - - // Secondary label (second line - database/tags) - secondaryLabel.leadingAnchor.constraint(equalTo: queryLabel.leadingAnchor), - secondaryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), - secondaryLabel.topAnchor.constraint(equalTo: queryLabel.bottomAnchor, constant: 2), - - // Time label (third line left) - timeLabel.leadingAnchor.constraint(equalTo: queryLabel.leadingAnchor), - timeLabel.topAnchor.constraint(equalTo: secondaryLabel.bottomAnchor, constant: 2), - - // Duration label (third line right) - durationLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), - durationLabel.centerYAnchor.constraint(equalTo: timeLabel.centerYAnchor), - durationLabel.leadingAnchor.constraint(greaterThanOrEqualTo: timeLabel.trailingAnchor, constant: 8) - ]) - } - - func configureForHistory(_ entry: QueryHistoryEntry) { - // Status icon - let imageName = entry.wasSuccessful ? "checkmark.circle.fill" : "xmark.circle.fill" - statusIcon.image = NSImage(systemSymbolName: imageName, accessibilityDescription: entry.wasSuccessful ? "Success" : "Error") - statusIcon.contentTintColor = entry.wasSuccessful ? .systemGreen : .systemRed - - // Query preview - queryLabel.stringValue = entry.queryPreview - - // Database - secondaryLabel.stringValue = entry.databaseName - - // Relative time - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .abbreviated - timeLabel.stringValue = formatter.localizedString(for: entry.executedAt, relativeTo: Date()) - - // Duration - durationLabel.stringValue = entry.formattedExecutionTime - } - - func configureForBookmark(_ bookmark: QueryBookmark) { - // Bookmark icon - statusIcon.image = NSImage(systemSymbolName: "bookmark.fill", accessibilityDescription: "Bookmark") - statusIcon.contentTintColor = .systemYellow - - // Bookmark name - queryLabel.stringValue = bookmark.name - queryLabel.font = .systemFont(ofSize: 12, weight: .medium) - - // Tags - secondaryLabel.stringValue = bookmark.hasTags ? bookmark.formattedTags : "No tags" - - // Created date - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none - timeLabel.stringValue = dateFormatter.string(from: bookmark.createdAt) - - // Clear duration - durationLabel.stringValue = "" - } - - override func prepareForReuse() { - super.prepareForReuse() - queryLabel.font = .monospacedSystemFont(ofSize: 11, weight: .regular) - statusIcon.image = nil - queryLabel.stringValue = "" - secondaryLabel.stringValue = "" - timeLabel.stringValue = "" - durationLabel.stringValue = "" - } -} - -// MARK: - Custom TableView for Keyboard Handling - -/// Custom table view for keyboard delegation -private class HistoryTableView: NSTableView, NSMenuItemValidation { - weak var keyboardDelegate: HistoryListViewController? - - override var acceptsFirstResponder: Bool { - return true - } - - override func mouseDown(with event: NSEvent) { - super.mouseDown(with: event) - // Ensure we become first responder for keyboard shortcuts - window?.makeFirstResponder(self) - } - - // MARK: - Standard Responder Actions - - @objc func delete(_ sender: Any?) { - keyboardDelegate?.deleteSelectedRow() - } - - @objc func copy(_ sender: Any?) { - keyboardDelegate?.copy(sender) - } - - func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { - if menuItem.action == #selector(delete(_:)) { - return keyboardDelegate?.validateMenuItem(menuItem) ?? false - } - if menuItem.action == #selector(copy(_:)) { - return selectedRow >= 0 - } - return false - } - - // MARK: - Keyboard Event Handling - - override func keyDown(with event: NSEvent) { - let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask) - - // Return/Enter key - open in new tab - if (event.keyCode == 36 || event.keyCode == 76) && modifiers.isEmpty { - if selectedRow >= 0 { - keyboardDelegate?.handleReturnKey() - return - } - } - - // Space key - toggle preview - if event.keyCode == 49 && modifiers.isEmpty { - if selectedRow >= 0 { - keyboardDelegate?.handleSpaceKey() - return - } - } - - // Cmd+E - edit bookmark - if event.keyCode == 14 && modifiers == .command { - keyboardDelegate?.handleEditBookmark() - return - } - - // Escape key - clear search or selection - if event.keyCode == 53 && modifiers.isEmpty { - keyboardDelegate?.handleEscapeKey() - return - } - - // Delete key (bare, not Cmd+Delete which goes through menu) - if event.keyCode == 51 && modifiers.isEmpty { - if selectedRow >= 0 { - keyboardDelegate?.handleDeleteKey() - return - } - } - - super.keyDown(with: event) - } -} diff --git a/OpenTable/Views/Filter/FilterPanelView.swift b/OpenTable/Views/Filter/FilterPanelView.swift new file mode 100644 index 00000000..c95bd0ce --- /dev/null +++ b/OpenTable/Views/Filter/FilterPanelView.swift @@ -0,0 +1,325 @@ +// +// FilterPanelView.swift +// OpenTable +// +// Bottom filter panel for table data filtering. +// Child views extracted to separate files for maintainability. +// + +import SwiftUI + +/// Bottom filter panel for table data filtering +struct FilterPanelView: View { + @ObservedObject var filterState: FilterStateManager + let columns: [String] + let primaryKeyColumn: String? + let databaseType: DatabaseType + let onApply: ([TableFilter]) -> Void + let onUnset: () -> Void + let onQuickSearch: ((String) -> Void)? + + @State private var showSQLSheet = false + @State private var showSettingsPopover = false + @State private var generatedSQL = "" + @State private var showSavePresetAlert = false + @State private var newPresetName = "" + @State private var savedPresets: [FilterPreset] = [] + + var body: some View { + VStack(spacing: 0) { + filterHeader + + Divider() + .foregroundStyle(Color(nsColor: .separatorColor)) + + // Quick Search field (when no filters or alongside filters) + if filterState.hasActiveQuickSearch || filterState.filters.isEmpty { + QuickSearchField( + searchText: $filterState.quickSearchText, + hasActiveSearch: filterState.hasActiveQuickSearch, + onSubmit: { onQuickSearch?(filterState.quickSearchText) }, + onClear: { filterState.clearQuickSearch() } + ) + Divider() + .foregroundStyle(Color(nsColor: .separatorColor)) + } + + // Filter rows + if filterState.filters.isEmpty { + if !filterState.hasActiveQuickSearch { + emptyState + } + } else { + filterList + } + + Divider() + .foregroundStyle(Color(nsColor: .separatorColor)) + + filterFooter + } + .background(Color(nsColor: .windowBackgroundColor)) + .sheet(isPresented: $showSQLSheet) { + SQLPreviewSheet(sql: generatedSQL, tableName: "", databaseType: databaseType) + } + } + + // MARK: - Header + + private var filterHeader: some View { + HStack(spacing: 8) { + Text("Filters") + .font(.system(size: 12, weight: .medium)) + + if filterState.hasAppliedFilters { + Text("(\(filterState.appliedFilters.count) active)") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + + Spacer() + + // AND/OR Logic Toggle + Picker("", selection: $filterState.filterLogicMode) { + Text("AND").tag(FilterLogicMode.and) + Text("OR").tag(FilterLogicMode.or) + } + .pickerStyle(.segmented) + .frame(width: 80) + .help("Match ALL filters (AND) or ANY filter (OR)") + + presetsMenu + + // Settings button + Button(action: { showSettingsPopover.toggle() }) { + Image(systemName: "gearshape") + .font(.system(size: 12)) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .help("Filter Settings") + .popover(isPresented: $showSettingsPopover, arrowEdge: .bottom) { + FilterSettingsPopover() + } + + // Add filter button + Button(action: { + filterState.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn) + }) { + Image(systemName: "plus") + .font(.system(size: 12)) + } + .buttonStyle(.borderless) + .foregroundColor(.accentColor) + .help("Add Filter (Cmd+Shift+F)") + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(Color(nsColor: .controlBackgroundColor)) + .contentShape(Rectangle()) + .onTapGesture { filterState.focusedFilterId = nil } + .alert("Save Filter Preset", isPresented: $showSavePresetAlert) { + TextField("Preset Name", text: $newPresetName) + Button("Cancel", role: .cancel) {} + Button("Save") { + if !newPresetName.isEmpty { + filterState.saveAsPreset(name: newPresetName) + loadPresets() + } + } + } message: { + Text("Enter a name for this filter preset") + } + } + + // MARK: - Presets Menu + + private var presetsMenu: some View { + Menu { + if !savedPresets.isEmpty { + ForEach(savedPresets) { preset in + Button(preset.name) { + filterState.loadPreset(preset) + } + } + Divider() + } + + Button("Save as Preset...") { + newPresetName = "" + showSavePresetAlert = true + } + .disabled(filterState.filters.isEmpty) + + if !savedPresets.isEmpty { + Menu("Delete Preset") { + ForEach(savedPresets) { preset in + Button(preset.name, role: .destructive) { + filterState.deletePreset(preset) + loadPresets() + } + } + } + } + } label: { + Image(systemName: "folder") + .font(.system(size: 12)) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .help("Save and load filter presets") + .onAppear { + loadPresets() + } + } + + // MARK: - Empty State + + private var emptyState: some View { + VStack(spacing: 12) { + Image(systemName: "line.3.horizontal.decrease.circle") + .font(.system(size: 32)) + .foregroundStyle(.tertiary) + + Text("No filters active") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(.secondary) + + HStack(spacing: 8) { + Button("Add Filter") { + filterState.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn) + } + .buttonStyle(.bordered) + .controlSize(.small) + + Text("or use Quick Search above") + .font(.system(size: 11)) + .foregroundStyle(.tertiary) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + } + + // MARK: - Filter List + + private var filterList: some View { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(spacing: 2) { + ForEach(filterState.filters) { filter in + FilterRowView( + filter: filterState.binding(for: filter), + columns: columns, + isFocused: filterState.focusedFilterId == filter.id, + onDuplicate: { filterState.duplicateFilter(filter) }, + onRemove: { filterState.removeFilter(filter) }, + onApply: { applySingleFilter(filter) }, + onFocus: { filterState.focusedFilterId = filter.id } + ) + .id(filter.id) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + } + .frame(maxHeight: min(CGFloat(filterState.filters.count) * 40 + 8, 160)) + .onChange(of: filterState.focusedFilterId) { _, newFocusedId in + if let focusedId = newFocusedId { + withAnimation(.easeInOut(duration: 0.25)) { + proxy.scrollTo(focusedId, anchor: .bottom) + } + } + } + } + } + + // MARK: - Footer + + private var filterFooter: some View { + HStack(spacing: 8) { + Toggle("Select All", isOn: selectAllBinding) + .toggleStyle(.checkbox) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .disabled(filterState.filters.isEmpty) + + Spacer() + + Button("Unset") { + filterState.clearAppliedFilters() + onUnset() + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(!filterState.hasAppliedFilters) + + Button("SQL") { + generatedSQL = filterState.generatePreviewSQL(databaseType: databaseType) + showSQLSheet = true + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(filterState.filters.isEmpty) + + Button("Apply All") { + applySelectedFilters() + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + .disabled(!filterState.hasSelectedFilters) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .contentShape(Rectangle()) + .onTapGesture { filterState.focusedFilterId = nil } + } + + // MARK: - Helpers + + private var selectAllBinding: Binding { + Binding( + get: { filterState.allFiltersSelected }, + set: { filterState.selectAll($0) } + ) + } + + private func applySingleFilter(_ filter: TableFilter) { + guard filter.isValid else { return } + filterState.applySingleFilter(filter) + onApply([filter]) + } + + private func applySelectedFilters() { + filterState.applySelectedFilters() + onApply(filterState.appliedFilters) + } + + private func loadPresets() { + savedPresets = filterState.loadAllPresets() + } +} + +// MARK: - Preview + +#Preview("Filter Panel") { + FilterPanelView( + filterState: { + let state = FilterStateManager() + Task { @MainActor in + state.filters = [ + TableFilter(columnName: "name", filterOperator: .contains, value: "John"), + TableFilter(columnName: "age", filterOperator: .greaterThan, value: "18") + ] + } + return state + }(), + columns: ["id", "name", "age", "email"], + primaryKeyColumn: "id", + databaseType: .mysql, + onApply: { _ in }, + onUnset: { }, + onQuickSearch: { _ in } + ) + .frame(width: 600) +} diff --git a/OpenTable/Views/Filter/FilterRowView.swift b/OpenTable/Views/Filter/FilterRowView.swift new file mode 100644 index 00000000..78eb8408 --- /dev/null +++ b/OpenTable/Views/Filter/FilterRowView.swift @@ -0,0 +1,290 @@ +// +// FilterRowView.swift +// OpenTable +// +// Single filter row view with native macOS styling. +// Extracted from FilterPanelView for better maintainability. +// + +import SwiftUI + +/// Single filter row view with native macOS styling +struct FilterRowView: View { + @Binding var filter: TableFilter + let columns: [String] + let isFocused: Bool + let onDuplicate: () -> Void + let onRemove: () -> Void + let onApply: () -> Void + let onFocus: () -> Void + + @State private var isHovered: Bool = false + + /// Display name for the column (handles raw SQL and empty) + private var displayColumnName: String { + if filter.columnName == TableFilter.rawSQLColumn { + return "Raw SQL" + } else if filter.columnName.isEmpty { + return "Column" + } else { + return filter.columnName + } + } + + /// Dynamic background color based on state + private var backgroundFillColor: Color { + if isFocused { + return Color.accentColor.opacity(0.06) + } else if isHovered { + return Color(nsColor: .controlBackgroundColor) + } else { + return Color.clear + } + } + + /// Dynamic border color based on state + private var borderColor: Color { + if isFocused { + return Color.accentColor.opacity(0.3) + } else if isHovered { + return Color(nsColor: .separatorColor).opacity(0.5) + } else { + return Color.clear + } + } + + var body: some View { + HStack(spacing: 8) { + // Checkbox for multi-select + Toggle("", isOn: $filter.isSelected) + .toggleStyle(.checkbox) + .labelsHidden() + + // Column dropdown - native Menu style + columnMenu + .frame(width: 120) + + // Operator dropdown (hidden for raw SQL) + if !filter.isRawSQL { + operatorMenu + .frame(width: 110) + } + + // Value field(s) + valueFields + + Spacer(minLength: 0) + + // Action buttons + actionButtons + } + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(backgroundFillColor) + ) + .overlay( + RoundedRectangle(cornerRadius: 4) + .strokeBorder(borderColor, lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { onFocus() } + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovered = hovering + } + } + .animation(.easeInOut(duration: 0.2), value: isFocused) + } + + // MARK: - Column Menu + + private var columnMenu: some View { + Menu { + Button(action: { filter.columnName = TableFilter.rawSQLColumn }) { + if filter.columnName == TableFilter.rawSQLColumn { + Label("Raw SQL", systemImage: "checkmark") + } else { + Text("Raw SQL") + } + } + + if !columns.isEmpty { + Divider() + ForEach(columns, id: \.self) { column in + Button(action: { filter.columnName = column }) { + if filter.columnName == column { + Label(column, systemImage: "checkmark") + } else { + Text(column) + } + } + } + } + } label: { + HStack(spacing: 4) { + Text(displayColumnName) + .font(.system(size: 12)) + .lineLimit(1) + .truncationMode(.tail) + Spacer(minLength: 0) + Image(systemName: "chevron.up.chevron.down") + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color(nsColor: .controlBackgroundColor)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + } + .menuStyle(.borderlessButton) + .simultaneousGesture(TapGesture().onEnded { onFocus() }) + } + + // MARK: - Operator Menu + + private var operatorMenu: some View { + Menu { + ForEach(FilterOperator.allCases) { op in + Button(action: { filter.filterOperator = op }) { + if filter.filterOperator == op { + Label(op.displayName, systemImage: "checkmark") + } else { + Text(op.displayName) + } + } + } + } label: { + HStack(spacing: 4) { + Text(filter.filterOperator.displayName) + .font(.system(size: 12)) + .lineLimit(1) + Spacer(minLength: 0) + Image(systemName: "chevron.up.chevron.down") + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color(nsColor: .controlBackgroundColor)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + } + .menuStyle(.borderlessButton) + .simultaneousGesture(TapGesture().onEnded { onFocus() }) + } + + // MARK: - Value Fields + + @ViewBuilder + private var valueFields: some View { + if filter.isRawSQL { + // Raw SQL input + TextField("WHERE clause...", text: Binding( + get: { filter.rawSQL ?? "" }, + set: { filter.rawSQL = $0 } + )) + .textFieldStyle(.plain) + .font(.system(size: 12)) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color(nsColor: .textBackgroundColor)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + .onSubmit { onApply() } + .simultaneousGesture(TapGesture().onEnded { onFocus() }) + } else if filter.filterOperator.requiresValue { + // Standard value input + TextField("Value", text: $filter.value) + .textFieldStyle(.plain) + .font(.system(size: 12)) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color(nsColor: .textBackgroundColor)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + .frame(minWidth: 80) + .onSubmit { onApply() } + .simultaneousGesture(TapGesture().onEnded { onFocus() }) + + // Second value for BETWEEN + if filter.filterOperator.requiresSecondValue { + Text("and") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + + TextField("Value", text: Binding( + get: { filter.secondValue ?? "" }, + set: { filter.secondValue = $0 } + )) + .textFieldStyle(.plain) + .font(.system(size: 12)) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color(nsColor: .textBackgroundColor)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + .frame(minWidth: 80) + .onSubmit { onApply() } + .simultaneousGesture(TapGesture().onEnded { onFocus() }) + } + } else { + // No value needed (IS NULL, etc.) - show indicator + Text("—") + .font(.system(size: 12)) + .foregroundStyle(.tertiary) + .frame(minWidth: 80, alignment: .leading) + } + } + + // MARK: - Action Buttons + + private var actionButtons: some View { + HStack(spacing: 8) { + // Apply single filter + Button(action: onApply) { + Image(systemName: "play.fill") + .font(.system(size: 11)) + } + .buttonStyle(.borderless) + .foregroundStyle(filter.isValid ? Color(nsColor: .systemGreen) : Color.secondary) + .disabled(!filter.isValid) + .help("Apply This Filter") + + // Duplicate + Button(action: onDuplicate) { + Image(systemName: "doc.on.doc") + .font(.system(size: 11)) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .help("Duplicate Filter") + + // Remove + Button(action: onRemove) { + Image(systemName: "xmark") + .font(.system(size: 11)) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .help("Remove Filter") + } + } +} diff --git a/OpenTable/Views/Filter/FilterSettingsPopover.swift b/OpenTable/Views/Filter/FilterSettingsPopover.swift new file mode 100644 index 00000000..42a9f1f3 --- /dev/null +++ b/OpenTable/Views/Filter/FilterSettingsPopover.swift @@ -0,0 +1,45 @@ +// +// FilterSettingsPopover.swift +// OpenTable +// +// Popover for filter default settings. +// Extracted from FilterPanelView for better maintainability. +// + +import SwiftUI + +/// Popover for filter default settings +struct FilterSettingsPopover: View { + @State private var settings: FilterSettings + + init() { + _settings = State(initialValue: FilterSettingsStorage.shared.loadSettings()) + } + + var body: some View { + Form { + Picker("Default Column", selection: $settings.defaultColumn) { + ForEach(FilterDefaultColumn.allCases) { option in + Text(option.displayName).tag(option) + } + } + + Picker("Default Operator", selection: $settings.defaultOperator) { + ForEach(FilterDefaultOperator.allCases) { option in + Text(option.displayName).tag(option) + } + } + + Picker("Panel State", selection: $settings.panelState) { + ForEach(FilterPanelDefaultState.allCases) { option in + Text(option.displayName).tag(option) + } + } + } + .formStyle(.grouped) + .frame(width: 280) + .onChange(of: settings) { _, newValue in + FilterSettingsStorage.shared.saveSettings(newValue) + } + } +} diff --git a/OpenTable/Views/Filter/QuickSearchField.swift b/OpenTable/Views/Filter/QuickSearchField.swift new file mode 100644 index 00000000..a366eddf --- /dev/null +++ b/OpenTable/Views/Filter/QuickSearchField.swift @@ -0,0 +1,47 @@ +// +// QuickSearchField.swift +// OpenTable +// +// Quick search field component for filtering across all columns. +// Extracted from FilterPanelView for better maintainability. +// + +import SwiftUI + +/// Quick search field for filtering across all columns +struct QuickSearchField: View { + @Binding var searchText: String + let hasActiveSearch: Bool + let onSubmit: () -> Void + let onClear: () -> Void + + var body: some View { + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.system(size: 12)) + .foregroundStyle(.secondary) + + TextField("Quick search across all columns...", text: $searchText) + .textFieldStyle(.plain) + .font(.system(size: 12)) + .onSubmit { + if !searchText.isEmpty { + onSubmit() + } + } + + if hasActiveSearch { + Button(action: onClear) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + .buttonStyle(.borderless) + .help("Clear Search") + } + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color(nsColor: .textBackgroundColor)) + } +} diff --git a/OpenTable/Views/Filter/SQLPreviewSheet.swift b/OpenTable/Views/Filter/SQLPreviewSheet.swift new file mode 100644 index 00000000..93355439 --- /dev/null +++ b/OpenTable/Views/Filter/SQLPreviewSheet.swift @@ -0,0 +1,85 @@ +// +// SQLPreviewSheet.swift +// OpenTable +// +// Modal sheet to display generated SQL from filters. +// Extracted from FilterPanelView for better maintainability. +// + +import SwiftUI + +/// Modal sheet to display generated SQL +struct SQLPreviewSheet: View { + let sql: String + let tableName: String + let databaseType: DatabaseType + @Environment(\.dismiss) private var dismiss + @State private var copied = false + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("Generated WHERE Clause") + .font(.system(size: 13, weight: .semibold)) + Spacer() + Button(action: { dismiss() }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 14)) + .foregroundStyle(.tertiary) + } + .buttonStyle(.borderless) + } + + ScrollView { + Text(sql.isEmpty ? "(no conditions)" : sql) + .font(.system(size: 12, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + } + .frame(maxHeight: 180) + .background(Color(nsColor: .textBackgroundColor)) + .cornerRadius(6) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + + HStack { + Button(action: copyToClipboard) { + HStack(spacing: 4) { + Image(systemName: copied ? "checkmark" : "doc.on.doc") + .font(.system(size: 11)) + Text(copied ? "Copied!" : "Copy") + .font(.system(size: 12)) + } + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(sql.isEmpty) + + Spacer() + + Button("Close") { + dismiss() + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + .keyboardShortcut(.escape) + } + } + .padding(16) + .frame(width: 480, height: 300) + } + + private func copyToClipboard() { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(sql, forType: .string) + copied = true + + // Reset after delay + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + copied = false + } + } +} diff --git a/OpenTable/Views/History/HistoryDataProvider.swift b/OpenTable/Views/History/HistoryDataProvider.swift new file mode 100644 index 00000000..e72b5cfc --- /dev/null +++ b/OpenTable/Views/History/HistoryDataProvider.swift @@ -0,0 +1,138 @@ +// +// HistoryDataProvider.swift +// OpenTable +// +// Data provider for history and bookmark entries. +// Extracted from HistoryListViewController for better separation of concerns. +// + +import Foundation + +/// Data provider for history and bookmark entries +final class HistoryDataProvider { + + // MARK: - Properties + + private(set) var historyEntries: [QueryHistoryEntry] = [] + private(set) var bookmarks: [QueryBookmark] = [] + + var displayMode: HistoryDisplayMode = .history + var dateFilter: UIDateFilter = .all + var searchText: String = "" + + private var searchTask: DispatchWorkItem? + private let searchDebounceInterval: TimeInterval = 0.15 + + /// Callback when data changes + var onDataChanged: (() -> Void)? + + // MARK: - Computed Properties + + var count: Int { + switch displayMode { + case .history: + return historyEntries.count + case .bookmarks: + return bookmarks.count + } + } + + var isEmpty: Bool { + count == 0 + } + + // MARK: - Data Loading + + func loadData() { + switch displayMode { + case .history: + loadHistory() + case .bookmarks: + loadBookmarks() + } + } + + private func loadHistory() { + historyEntries = QueryHistoryManager.shared.fetchHistory( + limit: 500, + offset: 0, + connectionId: nil, + searchText: searchText.isEmpty ? nil : searchText, + dateFilter: dateFilter.toDateFilter + ) + } + + private func loadBookmarks() { + bookmarks = QueryHistoryManager.shared.fetchBookmarks( + searchText: searchText.isEmpty ? nil : searchText, + tag: nil + ) + } + + // MARK: - Search + + func scheduleSearch(completion: @escaping () -> Void) { + searchTask?.cancel() + + let task = DispatchWorkItem { [weak self] in + self?.loadData() + completion() + } + searchTask = task + + DispatchQueue.main.asyncAfter(deadline: .now() + searchDebounceInterval, execute: task) + } + + // MARK: - Item Access + + func historyEntry(at index: Int) -> QueryHistoryEntry? { + guard index >= 0 && index < historyEntries.count else { return nil } + return historyEntries[index] + } + + func bookmark(at index: Int) -> QueryBookmark? { + guard index >= 0 && index < bookmarks.count else { return nil } + return bookmarks[index] + } + + func query(at index: Int) -> String? { + switch displayMode { + case .history: + return historyEntry(at: index)?.query + case .bookmarks: + return bookmark(at: index)?.query + } + } + + // MARK: - Deletion + + func deleteItem(at index: Int) -> Bool { + switch displayMode { + case .history: + guard let entry = historyEntry(at: index) else { return false } + QueryHistoryManager.shared.deleteHistory(id: entry.id) + return true + case .bookmarks: + guard let bookmark = bookmark(at: index) else { return false } + QueryHistoryManager.shared.deleteBookmark(id: bookmark.id) + return true + } + } + + func clearAll() -> Bool { + switch displayMode { + case .history: + return QueryHistoryManager.shared.clearAllHistory() + case .bookmarks: + return QueryHistoryManager.shared.clearAllBookmarks() + } + } + + // MARK: - Bookmark Operations + + func markBookmarkUsed(at index: Int) { + guard displayMode == .bookmarks, + let bookmark = bookmark(at: index) else { return } + QueryHistoryManager.shared.markBookmarkUsed(id: bookmark.id) + } +} diff --git a/OpenTable/Views/History/HistoryListViewController.swift b/OpenTable/Views/History/HistoryListViewController.swift new file mode 100644 index 00000000..4484a7c6 --- /dev/null +++ b/OpenTable/Views/History/HistoryListViewController.swift @@ -0,0 +1,756 @@ +// +// HistoryListViewController.swift +// OpenTable +// +// Left pane controller for history/bookmark list with search and filtering. +// Child views and data provider extracted to separate files. +// + +import AppKit + +// MARK: - Delegate Protocol + +protocol HistoryListViewControllerDelegate: AnyObject { + func historyListViewController(_ controller: HistoryListViewController, didSelectHistoryEntry entry: QueryHistoryEntry) + func historyListViewController(_ controller: HistoryListViewController, didSelectBookmark bookmark: QueryBookmark) + func historyListViewController(_ controller: HistoryListViewController, didDoubleClickHistoryEntry entry: QueryHistoryEntry) + func historyListViewController(_ controller: HistoryListViewController, didDoubleClickBookmark bookmark: QueryBookmark) + func historyListViewControllerDidClearSelection(_ controller: HistoryListViewController) +} + +// MARK: - Display Mode + +enum HistoryDisplayMode: Int { + case history = 0 + case bookmarks = 1 +} + +// MARK: - UI Date Filter + +enum UIDateFilter: Int { + case today = 0 + case week = 1 + case month = 2 + case all = 3 + + var title: String { + switch self { + case .today: return "Today" + case .week: return "This Week" + case .month: return "This Month" + case .all: return "All Time" + } + } + + var toDateFilter: DateFilter { + switch self { + case .today: return .today + case .week: return .thisWeek + case .month: return .thisMonth + case .all: return .all + } + } +} + +// MARK: - HistoryListViewController + +final class HistoryListViewController: NSViewController, NSMenuItemValidation { + + // MARK: - Properties + + weak var delegate: HistoryListViewControllerDelegate? + + private let dataProvider = HistoryDataProvider() + + private var displayMode: HistoryDisplayMode = .history { + didSet { + if oldValue != displayMode { + dataProvider.displayMode = displayMode + updateFilterVisibility() + loadData() + } + } + } + + private var dateFilter: UIDateFilter = .all { + didSet { + if oldValue != dateFilter { + dataProvider.dateFilter = dateFilter + loadData() + } + } + } + + private var searchText: String = "" { + didSet { + dataProvider.searchText = searchText + scheduleSearch() + } + } + + private var pendingDeletionRow: Int? + private var pendingDeletionCount: Int? + + // MARK: - UI Components + + private let headerView: NSVisualEffectView = { + let view = NSVisualEffectView() + view.material = .headerView + view.blendingMode = .withinWindow + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var modeSegment: NSSegmentedControl = { + let segment = NSSegmentedControl(labels: ["History", "Bookmarks"], trackingMode: .selectOne, target: self, action: #selector(modeChanged(_:))) + segment.selectedSegment = 0 + segment.translatesAutoresizingMaskIntoConstraints = false + segment.controlSize = .small + return segment + }() + + private lazy var searchField: NSSearchField = { + let field = NSSearchField() + field.placeholderString = "Search queries..." + field.delegate = self + field.translatesAutoresizingMaskIntoConstraints = false + field.controlSize = .small + return field + }() + + private lazy var filterButton: NSPopUpButton = { + let button = NSPopUpButton(frame: .zero, pullsDown: false) + button.controlSize = .small + button.translatesAutoresizingMaskIntoConstraints = false + for filter in [UIDateFilter.today, .week, .month, .all] { + button.addItem(withTitle: filter.title) + } + button.selectItem(at: UIDateFilter.all.rawValue) + button.target = self + button.action = #selector(filterChanged(_:)) + return button + }() + + private lazy var clearAllButton: NSButton = { + let button = NSButton() + button.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Clear All") + button.bezelStyle = .shadowlessSquare + button.isBordered = false + button.imagePosition = .imageOnly + button.translatesAutoresizingMaskIntoConstraints = false + button.target = self + button.action = #selector(clearAllClicked(_:)) + button.toolTip = "Clear all \(displayMode == .history ? "history" : "bookmarks")" + return button + }() + + private let scrollView: NSScrollView = { + let scroll = NSScrollView() + scroll.hasVerticalScroller = true + scroll.hasHorizontalScroller = false + scroll.autohidesScrollers = true + scroll.borderType = .noBorder + scroll.translatesAutoresizingMaskIntoConstraints = false + scroll.drawsBackground = false + return scroll + }() + + private lazy var tableView: HistoryTableView = { + let table = HistoryTableView() + table.style = .plain + table.headerView = nil + table.rowHeight = 56 + table.intercellSpacing = NSSize(width: 0, height: 1) + table.backgroundColor = .clear + table.usesAlternatingRowBackgroundColors = false + table.allowsMultipleSelection = false + table.delegate = self + table.dataSource = self + table.doubleAction = #selector(tableViewDoubleClick(_:)) + table.target = self + table.keyboardDelegate = self + + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("MainColumn")) + column.width = 300 + table.addTableColumn(column) + + return table + }() + + private lazy var emptyStateView: NSView = { + let container = NSView() + container.translatesAutoresizingMaskIntoConstraints = false + container.isHidden = true + + let stackView = NSStackView() + stackView.orientation = .vertical + stackView.alignment = .centerX + stackView.spacing = 8 + stackView.translatesAutoresizingMaskIntoConstraints = false + + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 48), + imageView.heightAnchor.constraint(equalToConstant: 48) + ]) + imageView.contentTintColor = .tertiaryLabelColor + self.emptyImageView = imageView + + let titleLabel = NSTextField(labelWithString: "") + titleLabel.font = .systemFont(ofSize: 14, weight: .medium) + titleLabel.textColor = .secondaryLabelColor + titleLabel.alignment = .center + self.emptyTitleLabel = titleLabel + + let subtitleLabel = NSTextField(labelWithString: "") + subtitleLabel.font = .systemFont(ofSize: 12) + subtitleLabel.textColor = .tertiaryLabelColor + subtitleLabel.alignment = .center + subtitleLabel.maximumNumberOfLines = 2 + subtitleLabel.lineBreakMode = .byWordWrapping + subtitleLabel.preferredMaxLayoutWidth = 200 + self.emptySubtitleLabel = subtitleLabel + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + + container.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: container.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: container.centerYAnchor) + ]) + + return container + }() + + private weak var emptyImageView: NSImageView? + private weak var emptyTitleLabel: NSTextField? + private weak var emptySubtitleLabel: NSTextField? + + // MARK: - Lifecycle + + override func loadView() { + view = NSView() + view.wantsLayer = true + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupNotifications() + restoreState() + loadData() + } + + // MARK: - Setup + + private func setupUI() { + view.addSubview(headerView) + + let headerStack = NSStackView() + headerStack.orientation = .vertical + headerStack.spacing = 8 + headerStack.translatesAutoresizingMaskIntoConstraints = false + headerStack.edgeInsets = NSEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) + + let topRow = NSStackView(views: [modeSegment, NSView(), clearAllButton, filterButton]) + topRow.distribution = .fill + topRow.spacing = 8 + + headerStack.addArrangedSubview(topRow) + headerStack.addArrangedSubview(searchField) + + headerView.addSubview(headerStack) + + let divider = NSBox() + divider.boxType = .separator + divider.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(divider) + + scrollView.documentView = tableView + view.addSubview(scrollView) + view.addSubview(emptyStateView) + + NSLayoutConstraint.activate([ + headerView.topAnchor.constraint(equalTo: view.topAnchor), + headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + headerStack.topAnchor.constraint(equalTo: headerView.topAnchor), + headerStack.leadingAnchor.constraint(equalTo: headerView.leadingAnchor), + headerStack.trailingAnchor.constraint(equalTo: headerView.trailingAnchor), + headerStack.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + + divider.topAnchor.constraint(equalTo: headerView.bottomAnchor), + divider.leadingAnchor.constraint(equalTo: view.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + scrollView.topAnchor.constraint(equalTo: divider.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + emptyStateView.topAnchor.constraint(equalTo: scrollView.topAnchor), + emptyStateView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + emptyStateView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + emptyStateView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + ]) + + updateFilterVisibility() + } + + private func setupNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(historyDidUpdate), name: .queryHistoryDidUpdate, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(bookmarksDidUpdate), name: .queryBookmarksDidUpdate, object: nil) + } + + // MARK: - State Persistence + + private func restoreState() { + let savedMode = UserDefaults.standard.integer(forKey: "HistoryPanel.displayMode") + let savedFilter = UserDefaults.standard.integer(forKey: "HistoryPanel.dateFilter") + + if let mode = HistoryDisplayMode(rawValue: savedMode) { + displayMode = mode + modeSegment.selectedSegment = mode.rawValue + } + + if let filter = UIDateFilter(rawValue: savedFilter) { + dateFilter = filter + filterButton.selectItem(at: filter.rawValue) + } + } + + private func saveState() { + UserDefaults.standard.set(displayMode.rawValue, forKey: "HistoryPanel.displayMode") + UserDefaults.standard.set(dateFilter.rawValue, forKey: "HistoryPanel.dateFilter") + } + + // MARK: - Data Loading + + private func loadData() { + dataProvider.loadData() + tableView.reloadData() + updateEmptyState() + + if let deletedRow = pendingDeletionRow, let countBefore = pendingDeletionCount { + selectRowAfterDeletion(deletedRow: deletedRow, countBefore: countBefore) + pendingDeletionRow = nil + pendingDeletionCount = nil + } else if tableView.selectedRow < 0 { + delegate?.historyListViewControllerDidClearSelection(self) + } + } + + // MARK: - Search + + private func scheduleSearch() { + dataProvider.scheduleSearch { [weak self] in + self?.tableView.reloadData() + self?.updateEmptyState() + } + } + + // MARK: - Actions + + @objc private func modeChanged(_ sender: NSSegmentedControl) { + if let mode = HistoryDisplayMode(rawValue: sender.selectedSegment) { + displayMode = mode + saveState() + } + } + + @objc private func filterChanged(_ sender: NSPopUpButton) { + if let filter = UIDateFilter(rawValue: sender.indexOfSelectedItem) { + dateFilter = filter + saveState() + } + } + + @objc private func tableViewDoubleClick(_ sender: Any) { + let row = tableView.clickedRow + guard row >= 0 else { return } + + switch displayMode { + case .history: + guard let entry = dataProvider.historyEntry(at: row) else { return } + delegate?.historyListViewController(self, didDoubleClickHistoryEntry: entry) + case .bookmarks: + guard let bookmark = dataProvider.bookmark(at: row) else { return } + delegate?.historyListViewController(self, didDoubleClickBookmark: bookmark) + } + } + + @objc private func historyDidUpdate() { + if displayMode == .history { loadData() } + } + + @objc private func bookmarksDidUpdate() { + if displayMode == .bookmarks { loadData() } + } + + @objc private func clearAllClicked(_ sender: Any?) { + let count = dataProvider.count + let itemName = count == 1 ? (displayMode == .history ? "history entry" : "bookmark") : (displayMode == .history ? "history entries" : "bookmarks") + + guard count > 0 else { return } + + let alert = NSAlert() + alert.messageText = "Clear All \(displayMode == .history ? "History" : "Bookmarks")?" + alert.informativeText = "This will permanently delete \(count) \(itemName). This action cannot be undone." + alert.alertStyle = .warning + alert.addButton(withTitle: "Clear All") + alert.addButton(withTitle: "Cancel") + + if alert.runModal() == .alertFirstButtonReturn { + _ = dataProvider.clearAll() + } + } + + // MARK: - UI Updates + + private func updateFilterVisibility() { + filterButton.isHidden = displayMode == .bookmarks + searchField.placeholderString = displayMode == .history ? "Search queries..." : "Search bookmarks..." + } + + private func updateEmptyState() { + let isEmpty = dataProvider.isEmpty + emptyStateView.isHidden = !isEmpty + scrollView.isHidden = isEmpty + + guard isEmpty else { return } + + let isSearching = !searchText.isEmpty + + if isSearching { + emptyImageView?.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "No results") + emptyTitleLabel?.stringValue = "No Matching Queries" + emptySubtitleLabel?.stringValue = "Try adjusting your search terms\nor date filter." + } else { + switch displayMode { + case .history: + emptyImageView?.image = NSImage(systemSymbolName: "clock.arrow.circlepath", accessibilityDescription: "No history") + emptyTitleLabel?.stringValue = "No Query History Yet" + emptySubtitleLabel?.stringValue = "Your executed queries will\nappear here for quick access." + case .bookmarks: + emptyImageView?.image = NSImage(systemSymbolName: "bookmark", accessibilityDescription: "No bookmarks") + emptyTitleLabel?.stringValue = "No Bookmarks Yet" + emptySubtitleLabel?.stringValue = "Save frequently used queries\nusing Cmd+Shift+B." + } + } + } + + // MARK: - Context Menu + + private func buildContextMenu(for row: Int) -> NSMenu { + let menu = NSMenu() + + let copyItem = NSMenuItem(title: "Copy Query", action: #selector(copyQuery(_:)), keyEquivalent: "c") + copyItem.keyEquivalentModifierMask = .command + copyItem.tag = row + menu.addItem(copyItem) + + let runItem = NSMenuItem(title: "Run in New Tab", action: #selector(runInNewTab(_:)), keyEquivalent: "\r") + runItem.tag = row + menu.addItem(runItem) + + menu.addItem(NSMenuItem.separator()) + + switch displayMode { + case .history: + let bookmarkItem = NSMenuItem(title: "Save as Bookmark...", action: #selector(saveAsBookmark(_:)), keyEquivalent: "") + bookmarkItem.tag = row + menu.addItem(bookmarkItem) + case .bookmarks: + let editItem = NSMenuItem(title: "Edit Bookmark...", action: #selector(editBookmark(_:)), keyEquivalent: "e") + editItem.keyEquivalentModifierMask = .command + editItem.tag = row + menu.addItem(editItem) + } + + menu.addItem(NSMenuItem.separator()) + + let deleteItem = NSMenuItem(title: "Delete", action: #selector(deleteEntry(_:)), keyEquivalent: "\u{8}") + deleteItem.keyEquivalentModifierMask = [] + deleteItem.tag = row + menu.addItem(deleteItem) + + return menu + } + + @objc private func copyQuery(_ sender: NSMenuItem) { + guard let query = dataProvider.query(at: sender.tag) else { return } + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(query, forType: .string) + } + + @objc private func runInNewTab(_ sender: NSMenuItem) { + guard let query = dataProvider.query(at: sender.tag) else { return } + + if displayMode == .bookmarks { + dataProvider.markBookmarkUsed(at: sender.tag) + } + + NotificationCenter.default.post(name: .newTab, object: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post(name: .loadQueryIntoEditor, object: query) + } + } + + @objc private func saveAsBookmark(_ sender: NSMenuItem) { + guard let entry = dataProvider.historyEntry(at: sender.tag) else { return } + + let editor = BookmarkEditorController(bookmark: nil, query: entry.query, connectionId: entry.connectionId) + editor.onSave = { bookmark in + _ = QueryHistoryManager.shared.saveBookmark( + name: bookmark.name, + query: bookmark.query, + connectionId: bookmark.connectionId, + tags: bookmark.tags, + notes: bookmark.notes + ) + } + view.window?.contentViewController?.presentAsSheet(editor) + } + + @objc private func editBookmark(_ sender: NSMenuItem) { + guard let bookmark = dataProvider.bookmark(at: sender.tag) else { return } + + let editorView = BookmarkEditorView(bookmark: bookmark, query: bookmark.query, connectionId: bookmark.connectionId) { updatedBookmark in + _ = QueryHistoryManager.shared.updateBookmark(updatedBookmark) + } + presentAsSheet(editorView) + } + + @objc private func deleteEntry(_ sender: NSMenuItem) { + _ = dataProvider.deleteItem(at: sender.tag) + } + + // MARK: - Selection After Deletion + + private func selectRowAfterDeletion(deletedRow: Int, countBefore: Int) { + let currentCount = dataProvider.count + + guard currentCount > 0 else { + tableView.deselectAll(nil) + delegate?.historyListViewControllerDidClearSelection(self) + return + } + + let newSelection = deletedRow < currentCount ? deletedRow : currentCount - 1 + tableView.selectRowIndexes(IndexSet(integer: newSelection), byExtendingSelection: false) + tableView.scrollRowToVisible(newSelection) + + switch displayMode { + case .history: + if let entry = dataProvider.historyEntry(at: newSelection) { + delegate?.historyListViewController(self, didSelectHistoryEntry: entry) + } + case .bookmarks: + if let bookmark = dataProvider.bookmark(at: newSelection) { + delegate?.historyListViewController(self, didSelectBookmark: bookmark) + } + } + } +} + +// MARK: - NSTableViewDataSource + +extension HistoryListViewController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + dataProvider.count + } +} + +// MARK: - NSTableViewDelegate + +extension HistoryListViewController: NSTableViewDelegate { + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + switch displayMode { + case .history: + return historyCell(for: row) + case .bookmarks: + return bookmarkCell(for: row) + } + } + + private func historyCell(for row: Int) -> NSView? { + guard let entry = dataProvider.historyEntry(at: row) else { return nil } + let identifier = NSUserInterfaceItemIdentifier("HistoryCell") + let cell = tableView.makeView(withIdentifier: identifier, owner: nil) as? HistoryRowView ?? HistoryRowView() + cell.identifier = identifier + cell.configureForHistory(entry) + return cell + } + + private func bookmarkCell(for row: Int) -> NSView? { + guard let bookmark = dataProvider.bookmark(at: row) else { return nil } + let identifier = NSUserInterfaceItemIdentifier("BookmarkCell") + let cell = tableView.makeView(withIdentifier: identifier, owner: nil) as? HistoryRowView ?? HistoryRowView() + cell.identifier = identifier + cell.configureForBookmark(bookmark) + return cell + } + + func tableViewSelectionDidChange(_ notification: Notification) { + let row = tableView.selectedRow + guard row >= 0 else { + delegate?.historyListViewControllerDidClearSelection(self) + return + } + + switch displayMode { + case .history: + if let entry = dataProvider.historyEntry(at: row) { + delegate?.historyListViewController(self, didSelectHistoryEntry: entry) + } + case .bookmarks: + if let bookmark = dataProvider.bookmark(at: row) { + delegate?.historyListViewController(self, didSelectBookmark: bookmark) + } + } + } + + func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableView.RowActionEdge) -> [NSTableViewRowAction] { + if edge == .trailing { + let delete = NSTableViewRowAction(style: .destructive, title: "Delete") { [weak self] _, row in + _ = self?.dataProvider.deleteItem(at: row) + } + return [delete] + } + return [] + } +} + +// MARK: - NSSearchFieldDelegate + +extension HistoryListViewController: NSSearchFieldDelegate { + + func controlTextDidChange(_ obj: Notification) { + if let field = obj.object as? NSSearchField { + searchText = field.stringValue + } + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if commandSelector == #selector(cancelOperation(_:)) { + if !searchText.isEmpty { + searchField.stringValue = "" + searchText = "" + return true + } + } + return false + } +} + +// MARK: - Context Menu + +extension HistoryListViewController { + + override func rightMouseDown(with event: NSEvent) { + let point = tableView.convert(event.locationInWindow, from: nil) + let row = tableView.row(at: point) + + if row >= 0 { + tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) + let menu = buildContextMenu(for: row) + NSMenu.popUpContextMenu(menu, with: event, for: tableView) + } + } +} + +// MARK: - HistoryTableViewKeyboardDelegate + +extension HistoryListViewController: HistoryTableViewKeyboardDelegate { + + func handleDeleteKey() { + deleteSelectedRow() + } + + @objc func delete(_ sender: Any?) { + deleteSelectedRow() + } + + @objc func copy(_ sender: Any?) { + copyQueryForSelectedRow() + } + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.action == #selector(delete(_:)) { + return tableView.selectedRow >= 0 && dataProvider.count > 0 + } + if menuItem.action == #selector(copy(_:)) { + return tableView.selectedRow >= 0 + } + return true + } + + func handleReturnKey() { + runInNewTabForSelectedRow() + } + + func handleSpaceKey() { + // Preview panel - future implementation + } + + func handleEditBookmark() { + guard displayMode == .bookmarks else { return } + editBookmarkForSelectedRow() + } + + func handleEscapeKey() { + if !searchText.isEmpty { + searchField.stringValue = "" + searchText = "" + searchField.window?.makeFirstResponder(tableView) + } else if tableView.selectedRow >= 0 { + tableView.deselectAll(nil) + } + } + + func deleteSelectedRow() { + let row = tableView.selectedRow + guard row >= 0 else { return } + + pendingDeletionRow = row + pendingDeletionCount = dataProvider.count + _ = dataProvider.deleteItem(at: row) + } + + private func copyQueryForSelectedRow() { + let row = tableView.selectedRow + guard row >= 0, let query = dataProvider.query(at: row) else { return } + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(query, forType: .string) + } + + private func runInNewTabForSelectedRow() { + let row = tableView.selectedRow + guard row >= 0, let query = dataProvider.query(at: row) else { return } + + if displayMode == .bookmarks { + dataProvider.markBookmarkUsed(at: row) + } + + NotificationCenter.default.post(name: .newTab, object: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post(name: .loadQueryIntoEditor, object: query) + } + } + + private func editBookmarkForSelectedRow() { + let row = tableView.selectedRow + guard let bookmark = dataProvider.bookmark(at: row) else { return } + + let editorView = BookmarkEditorView(bookmark: bookmark, query: bookmark.query, connectionId: bookmark.connectionId) { updatedBookmark in + _ = QueryHistoryManager.shared.updateBookmark(updatedBookmark) + } + presentAsSheet(editorView) + } +} diff --git a/OpenTable/Views/History/HistoryRowView.swift b/OpenTable/Views/History/HistoryRowView.swift new file mode 100644 index 00000000..357ba97d --- /dev/null +++ b/OpenTable/Views/History/HistoryRowView.swift @@ -0,0 +1,158 @@ +// +// HistoryRowView.swift +// OpenTable +// +// Table cell view for history and bookmark entries. +// Extracted from HistoryListViewController for better maintainability. +// + +import AppKit + +/// Table cell view for history and bookmark entries +final class HistoryRowView: NSTableCellView { + + private let statusIcon: NSImageView = { + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let queryLabel: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = .monospacedSystemFont(ofSize: 11, weight: .regular) + label.textColor = .labelColor + label.lineBreakMode = .byTruncatingTail + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let secondaryLabel: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = .systemFont(ofSize: 10) + label.textColor = .secondaryLabelColor + label.lineBreakMode = .byTruncatingTail + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let timeLabel: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = .systemFont(ofSize: 10) + label.textColor = .tertiaryLabelColor + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let durationLabel: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = .systemFont(ofSize: 10) + label.textColor = .tertiaryLabelColor + label.alignment = .right + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var isSetup = false + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + private func setupViews() { + guard !isSetup else { return } + isSetup = true + + addSubview(statusIcon) + addSubview(queryLabel) + addSubview(secondaryLabel) + addSubview(timeLabel) + addSubview(durationLabel) + + NSLayoutConstraint.activate([ + // Status icon + statusIcon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + statusIcon.topAnchor.constraint(equalTo: topAnchor, constant: 10), + statusIcon.widthAnchor.constraint(equalToConstant: 14), + statusIcon.heightAnchor.constraint(equalToConstant: 14), + + // Query label (first line) + queryLabel.leadingAnchor.constraint(equalTo: statusIcon.trailingAnchor, constant: 8), + queryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + queryLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + + // Secondary label (second line - database/tags) + secondaryLabel.leadingAnchor.constraint(equalTo: queryLabel.leadingAnchor), + secondaryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + secondaryLabel.topAnchor.constraint(equalTo: queryLabel.bottomAnchor, constant: 2), + + // Time label (third line left) + timeLabel.leadingAnchor.constraint(equalTo: queryLabel.leadingAnchor), + timeLabel.topAnchor.constraint(equalTo: secondaryLabel.bottomAnchor, constant: 2), + + // Duration label (third line right) + durationLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + durationLabel.centerYAnchor.constraint(equalTo: timeLabel.centerYAnchor), + durationLabel.leadingAnchor.constraint(greaterThanOrEqualTo: timeLabel.trailingAnchor, constant: 8) + ]) + } + + func configureForHistory(_ entry: QueryHistoryEntry) { + // Status icon + let imageName = entry.wasSuccessful ? "checkmark.circle.fill" : "xmark.circle.fill" + statusIcon.image = NSImage(systemSymbolName: imageName, accessibilityDescription: entry.wasSuccessful ? "Success" : "Error") + statusIcon.contentTintColor = entry.wasSuccessful ? .systemGreen : .systemRed + + // Query preview + queryLabel.stringValue = entry.queryPreview + + // Database + secondaryLabel.stringValue = entry.databaseName + + // Relative time + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + timeLabel.stringValue = formatter.localizedString(for: entry.executedAt, relativeTo: Date()) + + // Duration + durationLabel.stringValue = entry.formattedExecutionTime + } + + func configureForBookmark(_ bookmark: QueryBookmark) { + // Bookmark icon + statusIcon.image = NSImage(systemSymbolName: "bookmark.fill", accessibilityDescription: "Bookmark") + statusIcon.contentTintColor = .systemYellow + + // Bookmark name + queryLabel.stringValue = bookmark.name + queryLabel.font = .systemFont(ofSize: 12, weight: .medium) + + // Tags + secondaryLabel.stringValue = bookmark.hasTags ? bookmark.formattedTags : "No tags" + + // Created date + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + timeLabel.stringValue = dateFormatter.string(from: bookmark.createdAt) + + // Clear duration + durationLabel.stringValue = "" + } + + override func prepareForReuse() { + super.prepareForReuse() + queryLabel.font = .monospacedSystemFont(ofSize: 11, weight: .regular) + statusIcon.image = nil + queryLabel.stringValue = "" + secondaryLabel.stringValue = "" + timeLabel.stringValue = "" + durationLabel.stringValue = "" + } +} diff --git a/OpenTable/Views/History/HistoryTableView.swift b/OpenTable/Views/History/HistoryTableView.swift new file mode 100644 index 00000000..120088b8 --- /dev/null +++ b/OpenTable/Views/History/HistoryTableView.swift @@ -0,0 +1,100 @@ +// +// HistoryTableView.swift +// OpenTable +// +// Custom NSTableView with keyboard handling for history panel. +// Extracted from HistoryListViewController for better maintainability. +// + +import AppKit + +/// Protocol for keyboard event delegation +protocol HistoryTableViewKeyboardDelegate: AnyObject { + func handleDeleteKey() + func handleReturnKey() + func handleSpaceKey() + func handleEditBookmark() + func handleEscapeKey() + func deleteSelectedRow() + func copy(_ sender: Any?) + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool +} + +/// Custom table view for keyboard delegation in history panel +final class HistoryTableView: NSTableView, NSMenuItemValidation { + weak var keyboardDelegate: HistoryTableViewKeyboardDelegate? + + override var acceptsFirstResponder: Bool { + return true + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + // Ensure we become first responder for keyboard shortcuts + window?.makeFirstResponder(self) + } + + // MARK: - Standard Responder Actions + + @objc func delete(_ sender: Any?) { + keyboardDelegate?.deleteSelectedRow() + } + + @objc func copy(_ sender: Any?) { + keyboardDelegate?.copy(sender) + } + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.action == #selector(delete(_:)) { + return keyboardDelegate?.validateMenuItem(menuItem) ?? false + } + if menuItem.action == #selector(copy(_:)) { + return selectedRow >= 0 + } + return false + } + + // MARK: - Keyboard Event Handling + + override func keyDown(with event: NSEvent) { + let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + + // Return/Enter key - open in new tab + if (event.keyCode == 36 || event.keyCode == 76) && modifiers.isEmpty { + if selectedRow >= 0 { + keyboardDelegate?.handleReturnKey() + return + } + } + + // Space key - toggle preview + if event.keyCode == 49 && modifiers.isEmpty { + if selectedRow >= 0 { + keyboardDelegate?.handleSpaceKey() + return + } + } + + // Cmd+E - edit bookmark + if event.keyCode == 14 && modifiers == .command { + keyboardDelegate?.handleEditBookmark() + return + } + + // Escape key - clear search or selection + if event.keyCode == 53 && modifiers.isEmpty { + keyboardDelegate?.handleEscapeKey() + return + } + + // Delete key (bare, not Cmd+Delete which goes through menu) + if event.keyCode == 51 && modifiers.isEmpty { + if selectedRow >= 0 { + keyboardDelegate?.handleDeleteKey() + return + } + } + + super.keyDown(with: event) + } +} diff --git a/OpenTable/Views/Main/Child/MainStatusBarView.swift b/OpenTable/Views/Main/Child/MainStatusBarView.swift new file mode 100644 index 00000000..6cdb37fb --- /dev/null +++ b/OpenTable/Views/Main/Child/MainStatusBarView.swift @@ -0,0 +1,91 @@ +// +// MainStatusBarView.swift +// OpenTable +// +// Created by Ngo Quoc Dat on 24/12/25. +// + +import SwiftUI + +/// Status bar at the bottom of the results section +struct MainStatusBarView: View { + let tab: QueryTab? + let filterStateManager: FilterStateManager + let selectedRowIndices: Set + @Binding var showStructure: Bool + + var body: some View { + HStack { + // Left: Data/Structure toggle for table tabs + if let tab = tab, tab.tabType == .table, tab.tableName != nil { + Picker("", selection: $showStructure) { + Label("Data", systemImage: "tablecells").tag(false) + Label("Structure", systemImage: "list.bullet.rectangle").tag(true) + } + .pickerStyle(.segmented) + .frame(width: 180) + .controlSize(.small) + .offset(x: -26) + } + + Spacer() + + // Center: Row info (pagination/selection) + if let tab = tab, !tab.resultRows.isEmpty { + Text(rowInfoText(for: tab)) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + // Right: Filters toggle button + if let tab = tab, tab.tabType == .table, tab.tableName != nil { + Toggle(isOn: Binding( + get: { filterStateManager.isVisible }, + set: { _ in filterStateManager.toggle() } + )) { + HStack(spacing: 4) { + Image(systemName: filterStateManager.hasAppliedFilters + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease.circle") + Text("Filters") + if filterStateManager.hasAppliedFilters { + Text("(\(filterStateManager.appliedFilters.count))") + .foregroundStyle(.secondary) + } + } + } + .toggleStyle(.button) + .controlSize(.small) + .help("Toggle Filters (Cmd+F)") + } + } + .padding(.horizontal, 12) + .padding(.vertical, 4) + .background(Color(nsColor: .controlBackgroundColor)) + } + + /// Generate row info text based on selection and pagination state + private func rowInfoText(for tab: QueryTab) -> String { + let loadedCount = tab.resultRows.count + // Use selectedRowIndices parameter instead of tab.selectedRowIndices + let selectedCount = selectedRowIndices.count + let total = tab.pagination.totalRowCount + + if selectedCount > 0 { + // Selection mode + if selectedCount == loadedCount { + return "All \(loadedCount) rows selected" + } else { + return "\(selectedCount) of \(loadedCount) rows selected" + } + } else if let total = total, total > loadedCount { + // Pagination mode: "1-100 of 5000 rows" + return "1-\(loadedCount) of \(total) rows" + } else { + // Simple mode: "100 rows" + return "\(loadedCount) rows" + } + } +} diff --git a/OpenTable/Views/Main/Child/QueryTabContentView.swift b/OpenTable/Views/Main/Child/QueryTabContentView.swift new file mode 100644 index 00000000..4cd1841a --- /dev/null +++ b/OpenTable/Views/Main/Child/QueryTabContentView.swift @@ -0,0 +1,78 @@ +// +// QueryTabContentView.swift +// OpenTable +// +// Created by Ngo Quoc Dat on 24/12/25. +// + +import SwiftUI + +/// Content view for query tabs (editor + results split) +struct QueryTabContentView: View { + let tab: QueryTab + let connection: DatabaseConnection + let changeManager: DataChangeManager + let filterStateManager: FilterStateManager + @Binding var queryText: String + @Binding var cursorPosition: Int + @Binding var selectedRowIndices: Set + @Binding var editingCell: CellPosition? + + let schemaProvider: SQLSchemaProvider + let onExecute: () -> Void + + // Callbacks + let onCommit: (String) -> Void + let onRefresh: () -> Void + let onCellEdit: (Int, Int, String?) -> Void + let onSort: (Int, Bool) -> Void + let onAddRow: () -> Void + let onUndoInsert: (Int) -> Void + let onFilterColumn: (String) -> Void + let onApplyFilters: ([TableFilter]) -> Void + let onClearFilters: () -> Void + let onQuickSearch: (String) -> Void + let sortedRows: [QueryResultRow] + + @Binding var sortState: SortState + @Binding var showStructure: Bool + + var body: some View { + VSplitView { + // Query Editor (top) + VStack(spacing: 0) { + QueryEditorView( + queryText: $queryText, + cursorPosition: $cursorPosition, + onExecute: onExecute, + schemaProvider: schemaProvider + ) + } + .frame(minHeight: 100, idealHeight: 200) + + // Results Table (bottom) + TableTabContentView( + tab: tab, + connection: connection, + changeManager: changeManager, + filterStateManager: filterStateManager, + selectedRowIndices: $selectedRowIndices, + editingCell: $editingCell, + onCommit: onCommit, + onRefresh: onRefresh, + onCellEdit: onCellEdit, + onSort: onSort, + onAddRow: onAddRow, + onUndoInsert: onUndoInsert, + onFilterColumn: onFilterColumn, + onApplyFilters: onApplyFilters, + onClearFilters: onClearFilters, + onQuickSearch: onQuickSearch, + sortedRows: sortedRows, + sortState: $sortState, + showStructure: $showStructure + ) + .frame(minHeight: 150) + } + } +} diff --git a/OpenTable/Views/Main/Child/TableTabContentView.swift b/OpenTable/Views/Main/Child/TableTabContentView.swift new file mode 100644 index 00000000..e7eab5b0 --- /dev/null +++ b/OpenTable/Views/Main/Child/TableTabContentView.swift @@ -0,0 +1,90 @@ +// +// TableTabContentView.swift +// OpenTable +// +// Created by Ngo Quoc Dat on 24/12/25. +// + +import SwiftUI + +/// Content view for table tabs (results only, no editor) +struct TableTabContentView: View { + let tab: QueryTab + let connection: DatabaseConnection + let changeManager: DataChangeManager + let filterStateManager: FilterStateManager + @Binding var selectedRowIndices: Set + @Binding var editingCell: CellPosition? + + // Callbacks + let onCommit: (String) -> Void + let onRefresh: () -> Void + let onCellEdit: (Int, Int, String?) -> Void + let onSort: (Int, Bool) -> Void + let onAddRow: () -> Void + let onUndoInsert: (Int) -> Void + let onFilterColumn: (String) -> Void + let onApplyFilters: ([TableFilter]) -> Void + let onClearFilters: () -> Void + let onQuickSearch: (String) -> Void + let sortedRows: [QueryResultRow] + + @Binding var sortState: SortState + @Binding var showStructure: Bool + + var body: some View { + VStack(spacing: 0) { + // Show structure view or data view based on toggle + if showStructure, let tableName = tab.tableName { + TableStructureView(tableName: tableName, connection: connection) + .frame(maxHeight: .infinity) + } else { + DataGridView( + rowProvider: InMemoryRowProvider( + rows: sortedRows, + columns: tab.resultColumns, + columnDefaults: tab.columnDefaults + ), + changeManager: changeManager, + isEditable: tab.isEditable, + onCommit: onCommit, + onRefresh: onRefresh, + onCellEdit: onCellEdit, + onSort: onSort, + onAddRow: onAddRow, + onUndoInsert: onUndoInsert, + onFilterColumn: onFilterColumn, + selectedRowIndices: $selectedRowIndices, + sortState: $sortState, + editingCell: $editingCell + ) + .frame(maxHeight: .infinity, alignment: .top) + } + + // Filter panel (collapsible, at bottom) + if filterStateManager.isVisible && tab.tabType == .table { + Divider() + FilterPanelView( + filterState: filterStateManager, + columns: tab.resultColumns, + primaryKeyColumn: changeManager.primaryKeyColumn, + databaseType: connection.type, + onApply: onApplyFilters, + onUnset: onClearFilters, + onQuickSearch: onQuickSearch + ) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + + // Status bar + MainStatusBarView( + tab: tab, + filterStateManager: filterStateManager, + selectedRowIndices: selectedRowIndices, + showStructure: $showStructure + ) + } + .frame(minHeight: 150) + .animation(.easeInOut(duration: 0.2), value: filterStateManager.isVisible) + } +} diff --git a/OpenTable/Views/MainContentView.swift b/OpenTable/Views/MainContentView.swift index 4cbed22a..9cb1cd36 100644 --- a/OpenTable/Views/MainContentView.swift +++ b/OpenTable/Views/MainContentView.swift @@ -21,6 +21,12 @@ struct MainContentView: View { @StateObject private var tabManager = QueryTabManager() @StateObject private var changeManager = DataChangeManager() @StateObject private var filterStateManager = FilterStateManager() + @StateObject private var queryService = QueryExecutionService() + + // Lazy-initialized row operations manager + private var rowOperationsManager: RowOperationsManager { + RowOperationsManager(changeManager: changeManager) + } @State private var selectedRowIndices: Set = [] @State private var editingCell: CellPosition? = nil @@ -742,90 +748,24 @@ struct MainContentView: View { // MARK: - Status Bar private var statusBar: some View { - HStack { - // Left: Data/Structure toggle for table tabs - if let tab = currentTab, tab.tabType == .table, tab.tableName != nil { - Picker( - "", - selection: Binding( - get: { tab.showStructure ? "structure" : "data" }, - set: { newValue in - DispatchQueue.main.async { - if let index = tabManager.selectedTabIndex { - tabManager.tabs[index].showStructure = (newValue == "structure") - } - } - } - ) - ) { - Label("Data", systemImage: "tablecells").tag("data") - Label("Structure", systemImage: "list.bullet.rectangle").tag("structure") - } - .pickerStyle(.segmented) - .frame(width: 180) - .controlSize(.small) - .offset(x: -26) - } - - Spacer() - - // Center: Row info (pagination/selection) - if let tab = currentTab, !tab.resultRows.isEmpty { - Text(rowInfoText(for: tab)) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - // Right: Filters toggle button - if let tab = currentTab, tab.tabType == .table, tab.tableName != nil { - Toggle(isOn: Binding( - get: { filterStateManager.isVisible }, - set: { _ in filterStateManager.toggle() } - )) { - HStack(spacing: 4) { - Image(systemName: filterStateManager.hasAppliedFilters - ? "line.3.horizontal.decrease.circle.fill" - : "line.3.horizontal.decrease.circle") - Text("Filters") - if filterStateManager.hasAppliedFilters { - Text("(\(filterStateManager.appliedFilters.count))") - .foregroundStyle(.secondary) - } - } - } - .toggleStyle(.button) - .controlSize(.small) - .help("Toggle Filters (Cmd+F)") - } - } - .padding(.horizontal, 12) - .padding(.vertical, 4) - .background(Color(nsColor: .controlBackgroundColor)) + MainStatusBarView( + tab: currentTab, + filterStateManager: filterStateManager, + selectedRowIndices: selectedRowIndices, + showStructure: showStructureBinding + ) } - /// Generate row info text based on selection and pagination state - private func rowInfoText(for tab: QueryTab) -> String { - let loadedCount = tab.resultRows.count - // Use local selectedRowIndices state (not tab.selectedRowIndices which is only synced on tab switch) - let selectedCount = selectedRowIndices.count - let total = tab.pagination.totalRowCount - - if selectedCount > 0 { - // Selection mode - if selectedCount == loadedCount { - return "All \(loadedCount) rows selected" - } else { - return "\(selectedCount) of \(loadedCount) rows selected" + /// Binding for showStructure state + private var showStructureBinding: Binding { + Binding( + get: { currentTab?.showStructure ?? false }, + set: { newValue in + if let index = tabManager.selectedTabIndex { + tabManager.tabs[index].showStructure = newValue + } } - } else if let total = total, total > loadedCount { - // Pagination mode: "1-100 of 5000 rows" - return "1-\(loadedCount) of \(total) rows" - } else { - // Simple mode: "100 rows" - return "\(loadedCount) rows" - } + ) } // MARK: - Actions @@ -1202,73 +1142,14 @@ struct MainContentView: View { !selectedRowIndices.isEmpty else { return } - // Separate inserted rows from existing rows - var insertedRowsToDelete: [Int] = [] - var existingRowsToDelete: [(rowIndex: Int, originalRow: [String?])] = [] - - // Find the lowest selected row index for selection movement - let minSelectedRow = selectedRowIndices.min() ?? 0 - let maxSelectedRow = selectedRowIndices.max() ?? 0 - - // Categorize rows (process in descending order to maintain correct indices) - for rowIndex in selectedRowIndices.sorted(by: >) { - if changeManager.isRowInserted(rowIndex) { - // Collect inserted rows to delete - insertedRowsToDelete.append(rowIndex) - } else if !changeManager.isRowDeleted(rowIndex) { - // Collect existing rows for batch deletion - if rowIndex < tabManager.tabs[tabIndex].resultRows.count { - let originalRow = tabManager.tabs[tabIndex].resultRows[rowIndex].values - existingRowsToDelete.append((rowIndex: rowIndex, originalRow: originalRow)) - } - } - } - - // Process inserted rows deletion - if !insertedRowsToDelete.isEmpty { - // Sort descending so removing higher indices first doesn't affect lower indices - let sortedInsertedRows = insertedRowsToDelete.sorted(by: >) - - // Remove from resultRows first (descending order) - for rowIndex in sortedInsertedRows { - guard rowIndex < tabManager.tabs[tabIndex].resultRows.count else { continue } - tabManager.tabs[tabIndex].resultRows.remove(at: rowIndex) - } - - // Update changeManager for ALL deleted inserted rows at once - // This prevents index shifting issues from calling undoRowInsertion multiple times - changeManager.undoBatchRowInsertion(rowIndices: sortedInsertedRows) - } - - // Record batch deletion for existing rows (single undo action for all rows) - if !existingRowsToDelete.isEmpty { - changeManager.recordBatchRowDeletion(rows: existingRowsToDelete) - } + // Use RowOperationsManager to delete rows + let nextRow = rowOperationsManager.deleteSelectedRows( + selectedIndices: selectedRowIndices, + resultRows: &tabManager.tabs[tabIndex].resultRows + ) - // Move selection to next available row after deletion - let totalRows = tabManager.tabs[tabIndex].resultRows.count - - // Calculate next row selection, accounting for deleted inserted rows - let rowsDeleted = insertedRowsToDelete.count - let adjustedMaxRow = maxSelectedRow - rowsDeleted - let adjustedMinRow = minSelectedRow - insertedRowsToDelete.filter { $0 < minSelectedRow }.count - - let nextRow: Int - if adjustedMaxRow + 1 < totalRows { - // Select row after the deleted range - nextRow = min(adjustedMaxRow + 1, totalRows - 1) - } else if adjustedMinRow > 0 { - // Deleted rows at end, select previous row - nextRow = adjustedMinRow - 1 - } else if totalRows > 0 { - // Select first row if available - nextRow = 0 - } else { - // All rows deleted - nextRow = -1 - } - - if nextRow >= 0 && nextRow < totalRows { + // Update selection + if nextRow >= 0 && nextRow < tabManager.tabs[tabIndex].resultRows.count { selectedRowIndices = [nextRow] } else { selectedRowIndices.removeAll() @@ -1292,21 +1173,12 @@ struct MainContentView: View { private func copySelectedRowsToClipboard() { guard let index = tabManager.selectedTabIndex, !selectedRowIndices.isEmpty else { return } - + let tab = tabManager.tabs[index] - let sortedIndices = selectedRowIndices.sorted() - var lines: [String] = [] - - for rowIndex in sortedIndices { - guard rowIndex < tab.resultRows.count else { continue } - let row = tab.resultRows[rowIndex] - let line = row.values.map { $0 ?? "NULL" }.joined(separator: "\t") - lines.append(line) - } - - let text = lines.joined(separator: "\n") - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) + rowOperationsManager.copySelectedRowsToClipboard( + selectedIndices: selectedRowIndices, + resultRows: tab.resultRows + ) } // MARK: - Filters @@ -1619,45 +1491,25 @@ struct MainContentView: View { private func addNewRow() { guard let tabIndex = tabManager.selectedTabIndex else { return } guard tabIndex < tabManager.tabs.count else { return } - + let tab = tabManager.tabs[tabIndex] - + // Only add rows to editable table tabs guard tab.isEditable, tab.tableName != nil else { return } - - let columns = tab.resultColumns - let columnDefaults = tab.columnDefaults - - // Create new row values with DEFAULT markers - // These will be filtered out during INSERT generation, - // letting the database use actual defaults - var newRowValues: [String?] = [] - for column in columns { - if let defaultValue = columnDefaults[column], defaultValue != nil { - // Use __DEFAULT__ marker so generateInsertSQL skips this column - newRowValues.append("__DEFAULT__") - } else { - // NULL for columns without defaults - newRowValues.append(nil) - } - } - - // Add to tab's resultRows - let newRow = QueryResultRow(values: newRowValues) - tabManager.tabs[tabIndex].resultRows.append(newRow) - - // Get the new row index - let newRowIndex = tabManager.tabs[tabIndex].resultRows.count - 1 - - // Record in change manager as pending INSERT - changeManager.recordRowInsertion(rowIndex: newRowIndex, values: newRowValues) - + + // Use RowOperationsManager to add the row + guard let result = rowOperationsManager.addNewRow( + columns: tab.resultColumns, + columnDefaults: tab.columnDefaults, + resultRows: &tabManager.tabs[tabIndex].resultRows + ) else { return } + // Select the new row (scrolls to it) - selectedRowIndices = [newRowIndex] - + selectedRowIndices = [result.rowIndex] + // Auto-focus first cell instantly (TablePlus behavior) - editingCell = CellPosition(row: newRowIndex, column: 0) - + editingCell = CellPosition(row: result.rowIndex, column: 0) + // Mark tab as having user interaction tabManager.tabs[tabIndex].hasUserInteraction = true } @@ -1679,31 +1531,18 @@ struct MainContentView: View { selectedRowIndices.count == 1, selectedIndex < tab.resultRows.count else { return } - // Copy values from selected row - let sourceRow = tab.resultRows[selectedIndex] - var newValues = sourceRow.values - - // Set primary key column to DEFAULT so DB auto-generates - if let pkColumn = changeManager.primaryKeyColumn, - let pkIndex = tab.resultColumns.firstIndex(of: pkColumn) { - newValues[pkIndex] = "__DEFAULT__" - } - - // Add the duplicated row - let newRow = QueryResultRow(values: newValues) - tabManager.tabs[tabIndex].resultRows.append(newRow) - - // Get the new row index - let newRowIndex = tabManager.tabs[tabIndex].resultRows.count - 1 - - // Record in change manager as pending INSERT - changeManager.recordRowInsertion(rowIndex: newRowIndex, values: newValues) + // Use RowOperationsManager to duplicate the row + guard let result = rowOperationsManager.duplicateRow( + sourceRowIndex: selectedIndex, + columns: tab.resultColumns, + resultRows: &tabManager.tabs[tabIndex].resultRows + ) else { return } // Select the new row (scrolls to it) - selectedRowIndices = [newRowIndex] + selectedRowIndices = [result.rowIndex] // Auto-focus first cell (TablePlus behavior) - editingCell = CellPosition(row: newRowIndex, column: 0) + editingCell = CellPosition(row: result.rowIndex, column: 0) // Mark tab as having user interaction tabManager.tabs[tabIndex].hasUserInteraction = true @@ -1713,26 +1552,13 @@ struct MainContentView: View { private func undoInsertRow(at rowIndex: Int) { guard let tabIndex = tabManager.selectedTabIndex else { return } guard tabIndex < tabManager.tabs.count else { return } - guard rowIndex >= 0 && rowIndex < tabManager.tabs[tabIndex].resultRows.count else { return } - - // Remove the row from resultRows - tabManager.tabs[tabIndex].resultRows.remove(at: rowIndex) - - // Clear selection since the row no longer exists - if selectedRowIndices.contains(rowIndex) { - selectedRowIndices.remove(rowIndex) - } - - // Adjust selection indices for rows that shifted down - var adjustedSelection = Set() - for idx in selectedRowIndices { - if idx > rowIndex { - adjustedSelection.insert(idx - 1) - } else { - adjustedSelection.insert(idx) - } - } - selectedRowIndices = adjustedSelection + + // Use RowOperationsManager to undo the insertion + selectedRowIndices = rowOperationsManager.undoInsertRow( + at: rowIndex, + resultRows: &tabManager.tabs[tabIndex].resultRows, + selectedIndices: selectedRowIndices + ) } /// Undo the last change (Cmd+Z) @@ -1740,62 +1566,14 @@ struct MainContentView: View { private func undoLastChange() { guard let tabIndex = tabManager.selectedTabIndex else { return } guard tabIndex < tabManager.tabs.count else { return } - - // Get the undo result from changeManager - guard let result = changeManager.undoLastChange() else { return } - - switch result.action { - case .cellEdit(let rowIndex, let columnIndex, _, let previousValue, _): - // Restore the cell value in resultRows - if rowIndex < tabManager.tabs[tabIndex].resultRows.count { - tabManager.tabs[tabIndex].resultRows[rowIndex].values[columnIndex] = previousValue - } - - case .rowInsertion(let rowIndex): - // Remove the inserted row from resultRows - if rowIndex < tabManager.tabs[tabIndex].resultRows.count { - tabManager.tabs[tabIndex].resultRows.remove(at: rowIndex) - - // Clear selection if it was on the removed row - if selectedRowIndices.contains(rowIndex) { - selectedRowIndices.remove(rowIndex) - } - - // Adjust selection indices for rows that shifted down - var adjustedSelection = Set() - for idx in selectedRowIndices { - if idx > rowIndex { - adjustedSelection.insert(idx - 1) - } else { - adjustedSelection.insert(idx) - } - } - selectedRowIndices = adjustedSelection - } - - case .rowDeletion(_, _): - // Row is restored in changeManager - visual indicator will be removed - // No need to modify resultRows since deletion was just a visual indicator - break - - case .batchRowDeletion(_): - // All rows are restored in changeManager - visual indicators will be removed - // No need to modify resultRows since deletions were just visual indicators - break - - case .batchRowInsertion(let rowIndices, let rowValues): - // Restore deleted inserted rows - add them back to resultRows - // Process in reverse order (ascending) to maintain correct indices - for (index, rowIndex) in rowIndices.enumerated().reversed() { - guard index < rowValues.count else { continue } - guard rowIndex <= tabManager.tabs[tabIndex].resultRows.count else { continue } - - let values = rowValues[index] - let newRow = QueryResultRow(values: values) - tabManager.tabs[tabIndex].resultRows.insert(newRow, at: rowIndex) - } + + // Use RowOperationsManager to undo + if let adjustedSelection = rowOperationsManager.undoLastChange( + resultRows: &tabManager.tabs[tabIndex].resultRows + ) { + selectedRowIndices = adjustedSelection } - + // Mark tab as having user interaction tabManager.tabs[tabIndex].hasUserInteraction = true } @@ -1805,44 +1583,15 @@ struct MainContentView: View { private func redoLastChange() { guard let tabIndex = tabManager.selectedTabIndex else { return } guard tabIndex < tabManager.tabs.count else { return } - - // Get the redo result from changeManager - guard let result = changeManager.redoLastChange() else { return } - - switch result.action { - case .cellEdit(let rowIndex, let columnIndex, _, _, let newValue): - // Re-apply the cell value in resultRows - if rowIndex < tabManager.tabs[tabIndex].resultRows.count { - tabManager.tabs[tabIndex].resultRows[rowIndex].values[columnIndex] = newValue - } - - case .rowInsertion(let rowIndex): - // Re-insert the row into resultRows - var newValues = [String?](repeating: nil, count: changeManager.columns.count) - let newRow = QueryResultRow(values: newValues) - if rowIndex <= tabManager.tabs[tabIndex].resultRows.count { - tabManager.tabs[tabIndex].resultRows.insert(newRow, at: rowIndex) - } - - case .rowDeletion(_, _): - // Row is re-marked as deleted in changeManager - // No need to modify resultRows since deletion is just a visual indicator - break - - case .batchRowDeletion(_): - // Rows are re-marked as deleted in changeManager - // No need to modify resultRows since deletions are just visual indicators - break - - case .batchRowInsertion(let rowIndices, _): - // Redo the deletion - remove the rows from resultRows again - // Remove in descending order to avoid index shifting issues - for rowIndex in rowIndices.sorted(by: >) { - guard rowIndex < tabManager.tabs[tabIndex].resultRows.count else { continue } - tabManager.tabs[tabIndex].resultRows.remove(at: rowIndex) - } - } - + + let tab = tabManager.tabs[tabIndex] + + // Use RowOperationsManager to redo + _ = rowOperationsManager.redoLastChange( + resultRows: &tabManager.tabs[tabIndex].resultRows, + columns: tab.resultColumns + ) + // Mark tab as having user interaction tabManager.tabs[tabIndex].hasUserInteraction = true } diff --git a/OpenTable/Views/Results/CellTextField.swift b/OpenTable/Views/Results/CellTextField.swift new file mode 100644 index 00000000..b6db011f --- /dev/null +++ b/OpenTable/Views/Results/CellTextField.swift @@ -0,0 +1,82 @@ +// +// CellTextField.swift +// OpenTable +// +// Custom text field that delegates context menu to row view. +// Extracted from DataGridView for better maintainability. +// + +import AppKit + +/// NSTextField subclass that shows row context menu instead of text editing menu +final class CellTextField: NSTextField { + + override class var cellClass: AnyClass? { + get { CellTextFieldCell.self } + set { } + } + + /// Override right mouse down to end editing and show row context menu + override func rightMouseDown(with event: NSEvent) { + window?.makeFirstResponder(nil) + + var view: NSView? = self + while let parent = view?.superview { + if let rowView = parent as? TableRowViewWithMenu { + if let menu = rowView.menu(for: event) { + NSMenu.popUpContextMenu(menu, with: event, for: self) + } + return + } + view = parent + } + } + + override func menu(for event: NSEvent) -> NSMenu? { + window?.makeFirstResponder(nil) + + var view: NSView? = self + while let parent = view?.superview { + if let rowView = parent as? TableRowViewWithMenu { + return rowView.menu(for: event) + } + view = parent + } + + return nil + } +} + +/// Custom text field cell that provides a field editor with custom context menu behavior +final class CellTextFieldCell: NSTextFieldCell { + + private class CellFieldEditor: NSTextView { + + override func rightMouseDown(with event: NSEvent) { + window?.makeFirstResponder(nil) + + var view: NSView? = self + while let parent = view?.superview { + if let cellTextField = parent as? CellTextField { + cellTextField.rightMouseDown(with: event) + return + } + view = parent + } + } + + override func menu(for event: NSEvent) -> NSMenu? { + return nil + } + } + + private var customFieldEditor: CellFieldEditor? + + override func fieldEditor(for controlView: NSView) -> NSTextView? { + if customFieldEditor == nil { + customFieldEditor = CellFieldEditor() + customFieldEditor?.isFieldEditor = true + } + return customFieldEditor + } +} diff --git a/OpenTable/Views/Results/DataGridCellFactory.swift b/OpenTable/Views/Results/DataGridCellFactory.swift new file mode 100644 index 00000000..646b9823 --- /dev/null +++ b/OpenTable/Views/Results/DataGridCellFactory.swift @@ -0,0 +1,207 @@ +// +// DataGridCellFactory.swift +// OpenTable +// +// Factory for creating and configuring data grid cells. +// Extracted from DataGridView coordinator for better maintainability. +// + +import AppKit + +/// Factory for creating data grid cell views +final class DataGridCellFactory { + private let cellIdentifier = NSUserInterfaceItemIdentifier("DataCell") + private let rowNumberCellIdentifier = NSUserInterfaceItemIdentifier("RowNumberCell") + + /// Large dataset threshold - above this, disable expensive visual features + private let largeDatasetThreshold = 5000 + + // MARK: - Row Number Cell + + func makeRowNumberCell( + tableView: NSTableView, + row: Int, + cachedRowCount: Int, + visualState: RowVisualState + ) -> NSView { + let cellViewId = NSUserInterfaceItemIdentifier("RowNumberCellView") + let cellView: NSTableCellView + let cell: NSTextField + + if let reused = tableView.makeView(withIdentifier: cellViewId, owner: nil) as? NSTableCellView, + let textField = reused.textField { + cellView = reused + cell = textField + } else { + cellView = NSTableCellView() + cellView.identifier = cellViewId + + cell = NSTextField(labelWithString: "") + cell.alignment = .right + cell.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) + cell.textColor = .secondaryLabelColor + cell.translatesAutoresizingMaskIntoConstraints = false + + cellView.textField = cell + cellView.addSubview(cell) + + NSLayoutConstraint.activate([ + cell.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 4), + cell.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -4), + cell.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + ]) + } + + guard row >= 0 && row < cachedRowCount else { + cell.stringValue = "" + return cellView + } + + cell.stringValue = "\(row + 1)" + cell.textColor = visualState.isDeleted ? .systemRed.withAlphaComponent(0.5) : .secondaryLabelColor + + return cellView + } + + // MARK: - Data Cell + + func makeDataCell( + tableView: NSTableView, + row: Int, + columnIndex: Int, + value: String?, + visualState: RowVisualState, + isEditable: Bool, + isLargeDataset: Bool, + isFocused: Bool, + delegate: NSTextFieldDelegate + ) -> NSView { + let cellViewId = NSUserInterfaceItemIdentifier("DataCellView") + let cellView: NSTableCellView + let cell: NSTextField + let isNewCell: Bool + + if let reused = tableView.makeView(withIdentifier: cellViewId, owner: nil) as? NSTableCellView, + let textField = reused.textField { + cellView = reused + cell = textField + isNewCell = false + } else { + cellView = NSTableCellView() + cellView.identifier = cellViewId + cellView.wantsLayer = true + + cell = CellTextField() + cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + cell.drawsBackground = false + cell.isBordered = false + cell.focusRingType = .none + cell.lineBreakMode = .byTruncatingTail + cell.cell?.truncatesLastVisibleLine = true + cell.translatesAutoresizingMaskIntoConstraints = false + + cellView.textField = cell + cellView.addSubview(cell) + + NSLayoutConstraint.activate([ + cell.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 4), + cell.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -4), + cell.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + ]) + isNewCell = true + } + + cell.isEditable = isEditable + cell.delegate = delegate + cell.identifier = cellIdentifier + + let isDeleted = visualState.isDeleted + let isInserted = visualState.isInserted + let isModified = visualState.modifiedColumns.contains(columnIndex) + + // Update text content + cell.placeholderString = nil + + if value == nil { + cell.stringValue = "" + if !isLargeDataset { + cell.placeholderString = "NULL" + cell.textColor = .secondaryLabelColor + if isNewCell || cell.font?.fontDescriptor.symbolicTraits.contains(.italic) != true { + cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular).withTraits(.italic) + } + } else { + cell.textColor = .secondaryLabelColor + } + } else if value == "__DEFAULT__" { + cell.stringValue = "" + if !isLargeDataset { + cell.placeholderString = "DEFAULT" + cell.textColor = .systemBlue + cell.font = .monospacedSystemFont(ofSize: 13, weight: .medium) + } else { + cell.textColor = .systemBlue + } + } else if value == "" { + cell.stringValue = "" + if !isLargeDataset { + cell.placeholderString = "Empty" + cell.textColor = .secondaryLabelColor + if isNewCell || cell.font?.fontDescriptor.symbolicTraits.contains(.italic) != true { + cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular).withTraits(.italic) + } + } else { + cell.textColor = .secondaryLabelColor + } + } else { + cell.stringValue = value ?? "" + cell.textColor = .labelColor + if cell.font?.fontDescriptor.symbolicTraits.contains(.italic) == true || + cell.font?.fontDescriptor.symbolicTraits.contains(.bold) == true { + cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + } + } + + // Update background color + if isDeleted { + cellView.layer?.backgroundColor = NSColor.systemRed.withAlphaComponent(0.15).cgColor + } else if isInserted { + cellView.layer?.backgroundColor = NSColor.systemGreen.withAlphaComponent(0.15).cgColor + } else if isModified && !isLargeDataset { + cellView.layer?.backgroundColor = NSColor.systemYellow.withAlphaComponent(0.3).cgColor + } else { + cellView.layer?.backgroundColor = nil + } + + // Focus ring + if isLargeDataset { + cellView.layer?.borderWidth = 0 + } else if isFocused { + cellView.layer?.borderWidth = 2 + cellView.layer?.borderColor = NSColor.selectedControlColor.cgColor + } else { + cellView.layer?.borderWidth = 0 + } + + return cellView + } + + // MARK: - Column Width Calculation + + func calculateColumnWidth(for columnName: String) -> CGFloat { + let font = NSFont.systemFont(ofSize: 13, weight: .semibold) + let attributes: [NSAttributedString.Key: Any] = [.font: font] + let size = (columnName as NSString).size(withAttributes: attributes) + let width = size.width + 48 + return max(width, 30) + } +} + +// MARK: - NSFont Extension + +extension NSFont { + func withTraits(_ traits: NSFontDescriptor.SymbolicTraits) -> NSFont { + let descriptor = fontDescriptor.withSymbolicTraits(traits) + return NSFont(descriptor: descriptor, size: pointSize) ?? self + } +} diff --git a/OpenTable/Views/Results/DataGridView.swift b/OpenTable/Views/Results/DataGridView.swift index ee791f97..ed2066bd 100644 --- a/OpenTable/Views/Results/DataGridView.swift +++ b/OpenTable/Views/Results/DataGridView.swift @@ -2,7 +2,8 @@ // DataGridView.swift // OpenTable // -// High-performance NSTableView wrapper for SwiftUI +// High-performance NSTableView wrapper for SwiftUI. +// Custom views extracted to separate files for maintainability. // import AppKit @@ -14,37 +15,34 @@ struct CellPosition: Equatable { let column: Int } -// MARK: - Row Visual State Cache - -/// Cached visual state for a row - avoids repeated changeManager lookups during cell rendering -/// Computed once per row, read by all cells in that row +/// Cached visual state for a row - avoids repeated changeManager lookups struct RowVisualState { let isDeleted: Bool let isInserted: Bool let modifiedColumns: Set - /// Empty state for rows with no changes static let empty = RowVisualState(isDeleted: false, isInserted: false, modifiedColumns: []) } /// High-performance table view using AppKit NSTableView -/// Wrapped for SwiftUI via NSViewRepresentable struct DataGridView: NSViewRepresentable { let rowProvider: InMemoryRowProvider @ObservedObject var changeManager: DataChangeManager let isEditable: Bool var onCommit: ((String) -> Void)? var onRefresh: (() -> Void)? - var onCellEdit: ((Int, Int, String?) -> Void)? // (rowIndex, columnIndex, newValue) - var onDeleteRows: ((Set) -> Void)? // Called when Delete key pressed - var onSort: ((Int, Bool) -> Void)? // Called when column header clicked (columnIndex, ascending) - var onAddRow: (() -> Void)? // Called when user triggers add row (Cmd+N) - var onUndoInsert: ((Int) -> Void)? // Called when user undoes row insertion (rowIndex) - var onFilterColumn: ((String) -> Void)? // Called when user selects "Filter with column" from header context menu + var onCellEdit: ((Int, Int, String?) -> Void)? + var onDeleteRows: ((Set) -> Void)? + var onSort: ((Int, Bool) -> Void)? + var onAddRow: (() -> Void)? + var onUndoInsert: ((Int) -> Void)? + var onFilterColumn: ((String) -> Void)? @Binding var selectedRowIndices: Set @Binding var sortState: SortState - @Binding var editingCell: CellPosition? // Triggers editing of specific cell + @Binding var editingCell: CellPosition? + + private let cellFactory = DataGridCellFactory() // MARK: - NSViewRepresentable @@ -55,7 +53,6 @@ struct DataGridView: NSViewRepresentable { scrollView.autohidesScrollers = true scrollView.borderType = .noBorder - // Use custom table view that handles Delete key let tableView = KeyHandlingTableView() tableView.coordinator = context.coordinator tableView.style = .plain @@ -68,42 +65,31 @@ struct DataGridView: NSViewRepresentable { tableView.intercellSpacing = NSSize(width: 1, height: 0) tableView.rowHeight = 24 - // Set delegate and data source tableView.delegate = context.coordinator tableView.dataSource = context.coordinator // Add row number column - let rowNumberColumn = NSTableColumn( - identifier: NSUserInterfaceItemIdentifier("__rowNumber__")) + let rowNumberColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("__rowNumber__")) rowNumberColumn.title = "#" rowNumberColumn.width = 40 rowNumberColumn.minWidth = 40 rowNumberColumn.maxWidth = 60 rowNumberColumn.isEditable = false - rowNumberColumn.resizingMask = [] // Disable resizing + rowNumberColumn.resizingMask = [] tableView.addTableColumn(rowNumberColumn) // Add data columns for (index, columnName) in rowProvider.columns.enumerated() { let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("col_\(index)")) column.title = columnName - - // Auto-size column width to fit header text - let calculatedWidth = calculateColumnWidth(for: columnName) - column.width = calculatedWidth + column.width = cellFactory.calculateColumnWidth(for: columnName) column.minWidth = 30 - // Don't set maxWidth - let column stay at calculated width column.resizingMask = .userResizingMask column.isEditable = isEditable - // Use stable key for native sort descriptors (not column title which may change) - // AppKit will automatically show native sort indicators when user clicks header column.sortDescriptorPrototype = NSSortDescriptor(key: "col_\(index)", ascending: true) - tableView.addTableColumn(column) } - - // Use default NSTableHeaderView - 100% native sorting behavior - // Set up context menu using NSMenuDelegate (no subclassing needed) + if let headerView = tableView.headerView { let headerMenu = NSMenu() headerMenu.delegate = context.coordinator @@ -112,55 +98,28 @@ struct DataGridView: NSViewRepresentable { scrollView.documentView = tableView context.coordinator.tableView = tableView - + context.coordinator.cellFactory = cellFactory return scrollView } - - /// Calculate column width based on header text length - private func calculateColumnWidth(for columnName: String) -> CGFloat { - // Use header font (system default for table headers) - let font = NSFont.systemFont(ofSize: 13, weight: .semibold) - let attributes: [NSAttributedString.Key: Any] = [.font: font] - let size = (columnName as NSString).size(withAttributes: attributes) - - // Add generous padding: 12px left + text + 24px for sort indicator + 12px right - let width = size.width + 48 - - // Min 30px, no max (always fit full header text) - return max(width, 30) - } func updateNSView(_ scrollView: NSScrollView, context: Context) { guard let tableView = scrollView.documentView as? NSTableView else { return } let coordinator = context.coordinator - // Don't update while editing - this would cancel the edit - if tableView.editedRow >= 0 { - return - } + if tableView.editedRow >= 0 { return } - // PERF: Version check for change tracking (increments on clear/save) let versionChanged = coordinator.lastReloadVersion != changeManager.reloadVersion - - // PERF: Use cached values - avoids potential issues with deallocated provider let oldRowCount = coordinator.cachedRowCount let oldColumnCount = coordinator.cachedColumnCount let newRowCount = rowProvider.totalRowCount let newColumnCount = rowProvider.columns.count - // PERF: Only reload on structural changes, NOT on sort - // Sorting changes row order but not count - NSTableView handles this internally - // Removed: rowDataChanged comparison that caused O(n) overhead per update - let needsReload = - oldRowCount != newRowCount - || oldColumnCount != newColumnCount - || versionChanged + let needsReload = oldRowCount != newRowCount || oldColumnCount != newColumnCount || versionChanged - // Update coordinator references (but not version tracker yet - see below) coordinator.rowProvider = rowProvider - coordinator.updateCache() // Update cached counts after provider change + coordinator.updateCache() coordinator.changeManager = changeManager coordinator.isEditable = isEditable coordinator.onCommit = onCommit @@ -171,57 +130,37 @@ struct DataGridView: NSViewRepresentable { coordinator.onUndoInsert = onUndoInsert coordinator.onFilterColumn = onFilterColumn - // PERF: Rebuild visual state cache once per update cycle - // Cells read from this cache instead of calling changeManager directly coordinator.rebuildVisualStateCache() - // Check if columns changed - compare actual column names, not just count - let currentDataColumns = tableView.tableColumns.dropFirst() // Skip row number column + // Check if columns changed + let currentDataColumns = tableView.tableColumns.dropFirst() let currentColumnNames = currentDataColumns.map { $0.title } - - // Only rebuild if columns actually changed AND we have columns to show let columnsChanged = !rowProvider.columns.isEmpty && (currentColumnNames != rowProvider.columns) - + if columnsChanged { - // Rebuild columns - remove ALL data columns (keep only row number column) - let columnsToRemove = tableView.tableColumns.filter { - $0.identifier.rawValue != "__rowNumber__" - } + let columnsToRemove = tableView.tableColumns.filter { $0.identifier.rawValue != "__rowNumber__" } for column in columnsToRemove { tableView.removeTableColumn(column) } for (index, columnName) in rowProvider.columns.enumerated() { - let column = NSTableColumn( - identifier: NSUserInterfaceItemIdentifier("col_\(index)")) - + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("col_\(index)")) column.title = columnName - let calculatedWidth = calculateColumnWidth(for: columnName) - column.width = calculatedWidth + column.width = cellFactory.calculateColumnWidth(for: columnName) column.minWidth = 30 - // Don't set maxWidth - let column stay at calculated width column.resizingMask = .userResizingMask column.isEditable = isEditable - - // Use stable key for native sort descriptors column.sortDescriptorPrototype = NSSortDescriptor(key: "col_\(index)", ascending: true) - tableView.addTableColumn(column) } - - // Force header to recalculate layout after column changes - // Default NSTableHeaderView handles native sort indicators automatically tableView.sizeToFit() } - // Sync SwiftUI sort state → NSTableView (one-way) - // AppKit handles drawing native sort indicators automatically - // Use flag to prevent delegate from triggering infinite loop + // Sync sort state coordinator.isSyncingSortDescriptors = true defer { coordinator.isSyncingSortDescriptors = false } - + if !sortState.isSorting { - // No sort active - clear sort descriptors if !tableView.sortDescriptors.isEmpty { tableView.sortDescriptors = [] } @@ -229,48 +168,36 @@ struct DataGridView: NSViewRepresentable { columnIndex >= 0 && columnIndex < rowProvider.columns.count { let key = "col_\(columnIndex)" let ascending = sortState.direction == .ascending - - // Only update if different to avoid unnecessary updates let currentDescriptor = tableView.sortDescriptors.first if currentDescriptor?.key != key || currentDescriptor?.ascending != ascending { tableView.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)] } } - // Only reload if data actually changed if needsReload { tableView.reloadData() } - - // CRITICAL: Update version tracker AFTER reload check - // This ensures versionChanged is true when changeManager.reloadVersion increments - // (e.g., when clearChanges() is called after discarding or saving changes) + coordinator.lastReloadVersion = changeManager.reloadVersion // Sync selection let currentSelection = tableView.selectedRowIndexes let targetSelection = IndexSet(selectedRowIndices) - if currentSelection != targetSelection { tableView.selectRowIndexes(targetSelection, byExtendingSelection: false) } - - // Handle editingCell - start editing the specified cell + + // Handle editingCell if let cell = editingCell { - let tableColumn = cell.column + 1 // +1 to skip row number column + let tableColumn = cell.column + 1 if cell.row < tableView.numberOfRows && tableColumn < tableView.numberOfColumns { - // Scroll to the row first tableView.scrollRowToVisible(cell.row) - - // Select the row and start editing after a brief delay (allows scroll to complete) DispatchQueue.main.async { [weak tableView] in guard let tableView = tableView else { return } tableView.selectRowIndexes(IndexSet(integer: cell.row), byExtendingSelection: false) tableView.editColumn(tableColumn, row: cell.row, with: nil, select: true) } } - - // Clear the binding after handling DispatchQueue.main.async { self.editingCell = nil } @@ -278,7 +205,7 @@ struct DataGridView: NSViewRepresentable { } func makeCoordinator() -> TableViewCoordinator { - let coordinator = TableViewCoordinator( + TableViewCoordinator( rowProvider: rowProvider, changeManager: changeManager, isEditable: isEditable, @@ -287,11 +214,6 @@ struct DataGridView: NSViewRepresentable { onRefresh: onRefresh, onCellEdit: onCellEdit ) - - // onColumnResize callback will be set by coordinator property directly - // Coordinator will update columnWidths via binding when column is resized - - return coordinator } } @@ -307,38 +229,26 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData var onCommit: ((String) -> Void)? var onRefresh: (() -> Void)? var onCellEdit: ((Int, Int, String?) -> Void)? + var onSort: ((Int, Bool) -> Void)? + var onAddRow: (() -> Void)? + var onUndoInsert: ((Int) -> Void)? + var onFilterColumn: ((String) -> Void)? weak var tableView: NSTableView? + var cellFactory: DataGridCellFactory? @Binding var selectedRowIndices: Set - // Track reload version to detect changes cleared var lastReloadVersion: Int = 0 - - // Cache column count and row count to avoid accessing potentially invalid provider private(set) var cachedRowCount: Int = 0 private(set) var cachedColumnCount: Int = 0 - - // Guard flag to prevent infinite loop when syncing sort descriptors - // Set to true when programmatically setting sortDescriptors from updateNSView var isSyncingSortDescriptors: Bool = false - // Cell reuse identifiers private let cellIdentifier = NSUserInterfaceItemIdentifier("DataCell") - private let rowNumberCellIdentifier = NSUserInterfaceItemIdentifier("RowNumberCell") - - // MARK: - Row Visual State Cache - // Caches per-row visual state (deleted/inserted/modified) to avoid repeated changeManager lookups - // Rebuilt on each updateNSView cycle, read during cell rendering private var rowVisualStateCache: [Int: RowVisualState] = [:] - - /// Large dataset threshold - above this, disable expensive visual features private let largeDatasetThreshold = 5000 - /// Whether current dataset is "large" and should use simplified rendering - var isLargeDataset: Bool { - cachedRowCount > largeDatasetThreshold - } + var isLargeDataset: Bool { cachedRowCount > largeDatasetThreshold } init( rowProvider: InMemoryRowProvider, @@ -360,7 +270,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData updateCache() } - /// Update cached counts from current rowProvider func updateCache() { cachedRowCount = rowProvider.totalRowCount cachedColumnCount = rowProvider.columns.count @@ -368,16 +277,10 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData // MARK: - Row Visual State Cache - /// Rebuild visual state cache from changeManager - /// Called once per updateNSView cycle - O(changes) not O(rows) func rebuildVisualStateCache() { rowVisualStateCache.removeAll(keepingCapacity: true) - - // Skip cache building for large datasets with no changes guard changeManager.hasChanges else { return } - // Build cache from changeManager's efficient O(1) lookups - // Only cache rows that have changes (sparse cache) for change in changeManager.changes { let rowIndex = change.rowIndex let isDeleted = change.type == .delete @@ -394,115 +297,67 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData } } - /// Get cached visual state for a row - O(1) dictionary lookup - /// Returns .empty for rows without changes (no cache entry) func visualState(for row: Int) -> RowVisualState { rowVisualStateCache[row] ?? .empty } - /// Clear visual state cache (called when data changes significantly) - func clearVisualStateCache() { - rowVisualStateCache.removeAll() - } - - /// Callback when column header clicked for sorting: (columnIndex, ascending) - var onSort: ((Int, Bool) -> Void)? - - /// Callback when user triggers add row (Cmd+N) - var onAddRow: (() -> Void)? - - /// Callback when user undoes row insertion - var onUndoInsert: ((Int) -> Void)? - - /// Callback when user selects "Filter with column" from header context menu - var onFilterColumn: ((String) -> Void)? - // MARK: - NSTableViewDataSource func numberOfRows(in tableView: NSTableView) -> Int { - // Use cached count for safety - updated when provider changes - return cachedRowCount + cachedRowCount } - - // MARK: - Native Sorting via NSTableViewDelegate - - /// Called by AppKit when user clicks column header to sort - /// This is the native NSTableView sorting mechanism + + // MARK: - Native Sorting + func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) { - // CRITICAL: Ignore if we're programmatically syncing from SwiftUI - // This prevents infinite loop: updateNSView → sortDescriptorsDidChange → onSort → updateNSView guard !isSyncingSortDescriptors else { return } - - // Get the new primary sort descriptor + guard let sortDescriptor = tableView.sortDescriptors.first, let key = sortDescriptor.key, key.hasPrefix("col_"), - let columnIndex = Int(key.dropFirst(4)) else { + let columnIndex = Int(key.dropFirst(4)), + columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return } - - // Validate column index - guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { - return - } - - // Call parent's sort handler with column index AND direction from AppKit - // This ensures parent uses the exact direction AppKit determined + onSort?(columnIndex, sortDescriptor.ascending) } - + // MARK: - NSMenuDelegate (Header Context Menu) - - /// Dynamically populate header context menu based on clicked column + func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - + guard let tableView = tableView, let headerView = tableView.headerView, - let window = tableView.window else { - return - } - - // Get mouse location in header coordinates + let window = tableView.window else { return } + let mouseLocation = window.mouseLocationOutsideOfEventStream let pointInHeader = headerView.convert(mouseLocation, from: nil) let columnIndex = headerView.column(at: pointInHeader) - - guard columnIndex >= 0 && columnIndex < tableView.tableColumns.count else { - return - } - + + guard columnIndex >= 0 && columnIndex < tableView.tableColumns.count else { return } + let column = tableView.tableColumns[columnIndex] - - // Skip row number column - if column.identifier.rawValue == "__rowNumber__" { - return - } - - let copyItem = NSMenuItem( - title: "Copy Column Name", - action: #selector(copyColumnName(_:)), - keyEquivalent: "") + if column.identifier.rawValue == "__rowNumber__" { return } + + let copyItem = NSMenuItem(title: "Copy Column Name", action: #selector(copyColumnName(_:)), keyEquivalent: "") copyItem.representedObject = column.title copyItem.target = self menu.addItem(copyItem) - - // Add "Filter with column" menu item - let filterItem = NSMenuItem( - title: "Filter with column", - action: #selector(filterWithColumn(_:)), - keyEquivalent: "") + + let filterItem = NSMenuItem(title: "Filter with column", action: #selector(filterWithColumn(_:)), keyEquivalent: "") filterItem.representedObject = column.title filterItem.target = self menu.addItem(filterItem) } - + @objc private func copyColumnName(_ sender: NSMenuItem) { guard let columnName = sender.representedObject as? String else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(columnName, forType: .string) } - + @objc private func filterWithColumn(_ sender: NSMenuItem) { guard let columnName = sender.representedObject as? String else { return } onFilterColumn?(columnName) @@ -510,228 +365,52 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData // MARK: - NSTableViewDelegate - func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) - -> NSView? - { + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard let column = tableColumn else { return nil } let columnId = column.identifier.rawValue - // Row number column if columnId == "__rowNumber__" { - return makeRowNumberCell(tableView: tableView, row: row) - } - - // Data column - guard columnId.hasPrefix("col_"), - let columnIndex = Int(columnId.dropFirst(4)) - else { - return nil - } - - return makeDataCell(tableView: tableView, row: row, columnIndex: columnIndex) - } - - private func makeRowNumberCell(tableView: NSTableView, row: Int) -> NSView { - // PERF: Reuse cell views, configure once - let cellViewId = NSUserInterfaceItemIdentifier("RowNumberCellView") - let cellView: NSTableCellView - let cell: NSTextField - - if let reused = tableView.makeView(withIdentifier: cellViewId, owner: nil) - as? NSTableCellView, - let textField = reused.textField - { - cellView = reused - cell = textField - } else { - // PERF: Configure once - font, alignment, constraints - cellView = NSTableCellView() - cellView.identifier = cellViewId - - cell = NSTextField(labelWithString: "") - cell.alignment = .right - cell.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) - cell.textColor = .secondaryLabelColor - cell.translatesAutoresizingMaskIntoConstraints = false - - cellView.textField = cell - cellView.addSubview(cell) - - NSLayoutConstraint.activate([ - cell.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 4), - cell.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -4), - cell.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), - ]) - } - - // Boundary check - guard row >= 0 && row < cachedRowCount else { - cell.stringValue = "" - return cellView - } - - // PERF: Update only text and color on reuse - cell.stringValue = "\(row + 1)" - - // PERF: Read from cached visual state instead of changeManager - let state = visualState(for: row) - cell.textColor = state.isDeleted ? .systemRed.withAlphaComponent(0.5) : .secondaryLabelColor - - return cellView - } - - private func makeDataCell(tableView: NSTableView, row: Int, columnIndex: Int) -> NSView { - // PERF: Reuse cells - only configure once, update text+background on reuse - let cellViewId = NSUserInterfaceItemIdentifier("DataCellView") - let cellView: NSTableCellView - let cell: NSTextField - let isNewCell: Bool - - if let reused = tableView.makeView(withIdentifier: cellViewId, owner: nil) - as? NSTableCellView, - let textField = reused.textField - { - cellView = reused - cell = textField - isNewCell = false - } else { - // PERF: Configure once - fonts, layers, constraints are set only on creation - cellView = NSTableCellView() - cellView.identifier = cellViewId - cellView.wantsLayer = true // Set once, never toggle - - cell = CellTextField() - cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular) - cell.drawsBackground = false // Set once - background via layer - cell.isBordered = false - cell.focusRingType = .none - cell.lineBreakMode = .byTruncatingTail - cell.cell?.truncatesLastVisibleLine = true - cell.translatesAutoresizingMaskIntoConstraints = false - - cellView.textField = cell - cellView.addSubview(cell) - - // PERF: Constraints set once, never modified - NSLayoutConstraint.activate([ - cell.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 4), - cell.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -4), - cell.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), - ]) - isNewCell = true + return cellFactory?.makeRowNumberCell( + tableView: tableView, + row: row, + cachedRowCount: cachedRowCount, + visualState: visualState(for: row) + ) } - // Set editable/delegate on reuse (cheap operations) - cell.isEditable = isEditable - cell.delegate = self - cell.identifier = cellIdentifier + guard columnId.hasPrefix("col_"), let columnIndex = Int(columnId.dropFirst(4)) else { return nil } - // Boundary check - return empty cell if out of bounds guard row >= 0 && row < cachedRowCount, columnIndex >= 0 && columnIndex < cachedColumnCount, - let rowData = rowProvider.row(at: row) - else { - cell.stringValue = "" - cell.placeholderString = nil - cell.textColor = .labelColor - cellView.layer?.backgroundColor = nil - cellView.layer?.borderWidth = 0 - return cellView + let rowData = rowProvider.row(at: row) else { + return nil } let value = rowData.value(at: columnIndex) - - // PERF: Read from cached visual state instead of changeManager - // visualState is computed once per updateNSView cycle, shared by all cells in row let state = visualState(for: row) - let isDeleted = state.isDeleted - let isInserted = state.isInserted - let isModified = state.modifiedColumns.contains(columnIndex) - - // PERF: Update text content (always needed on reuse) - cell.placeholderString = nil - - if value == nil { - cell.stringValue = "" - // PERF: For large datasets, skip placeholder styling - if !isLargeDataset { - cell.placeholderString = "NULL" - cell.textColor = .secondaryLabelColor - if isNewCell || cell.font?.fontDescriptor.symbolicTraits.contains(.italic) != true { - cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular).withTraits(.italic) - } - } else { - cell.textColor = .secondaryLabelColor - } - } else if value == "__DEFAULT__" { - cell.stringValue = "" - if !isLargeDataset { - cell.placeholderString = "DEFAULT" - cell.textColor = .systemBlue - cell.font = .monospacedSystemFont(ofSize: 13, weight: .medium) - } else { - cell.textColor = .systemBlue - } - } else if value == "" { - cell.stringValue = "" - if !isLargeDataset { - cell.placeholderString = "Empty" - cell.textColor = .secondaryLabelColor - if isNewCell || cell.font?.fontDescriptor.symbolicTraits.contains(.italic) != true { - cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular).withTraits(.italic) - } - } else { - cell.textColor = .secondaryLabelColor - } - } else { - cell.stringValue = value ?? "" - cell.textColor = .labelColor - // Only reset font if it was changed (avoid font allocation) - if cell.font?.fontDescriptor.symbolicTraits.contains(.italic) == true || - cell.font?.fontDescriptor.symbolicTraits.contains(.bold) == true { - cell.font = .monospacedSystemFont(ofSize: 13, weight: .regular) - } - } - // PERF: Update background color (priority: deleted > inserted > modified) - // For large datasets, skip modified cell highlighting - if isDeleted { - cellView.layer?.backgroundColor = NSColor.systemRed.withAlphaComponent(0.15).cgColor - } else if isInserted { - cellView.layer?.backgroundColor = NSColor.systemGreen.withAlphaComponent(0.15).cgColor - } else if isModified && !isLargeDataset { - cellView.layer?.backgroundColor = NSColor.systemYellow.withAlphaComponent(0.3).cgColor - } else { - cellView.layer?.backgroundColor = nil - } - - // PERF: Focus ring - skip for large datasets - if isLargeDataset { - cellView.layer?.borderWidth = 0 - } else { - let tableColumnIndex = columnIndex + 1 - let isFocused: Bool = { - guard let keyTableView = tableView as? KeyHandlingTableView, - keyTableView.focusedRow == row, // Use focusedRow, not selectedRow - keyTableView.focusedColumn == tableColumnIndex - else { return false } - return true - }() - - if isFocused { - cellView.layer?.borderWidth = 2 - cellView.layer?.borderColor = NSColor.selectedControlColor.cgColor - } else { - cellView.layer?.borderWidth = 0 - } - } + let tableColumnIndex = columnIndex + 1 + let isFocused: Bool = { + guard let keyTableView = tableView as? KeyHandlingTableView, + keyTableView.focusedRow == row, + keyTableView.focusedColumn == tableColumnIndex else { return false } + return true + }() - return cellView + return cellFactory?.makeDataCell( + tableView: tableView, + row: row, + columnIndex: columnIndex, + value: value, + visualState: state, + isEditable: isEditable && !state.isDeleted, + isLargeDataset: isLargeDataset, + isFocused: isFocused, + delegate: self + ) } - // MARK: - Row View (for context menu) - func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = TableRowViewWithMenu() rowView.coordinator = self @@ -750,8 +429,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData self.selectedRowIndices = newSelection } } - - // Clear focus if selection is empty + if let keyTableView = tableView as? KeyHandlingTableView { if newSelection.isEmpty { keyTableView.focusedRow = -1 @@ -762,43 +440,30 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData // MARK: - Editing - func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) - -> Bool - { + func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool { guard isEditable, - let columnId = tableColumn?.identifier.rawValue, - columnId != "__rowNumber__", - !changeManager.isRowDeleted(row) - else { - return false - } + let columnId = tableColumn?.identifier.rawValue, + columnId != "__rowNumber__", + !changeManager.isRowDeleted(row) else { return false } return true } func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { - guard let textField = control as? NSTextField, - let tableView = tableView - else { - return true - } + guard let textField = control as? NSTextField, let tableView = tableView else { return true } let row = tableView.row(for: textField) let column = tableView.column(for: textField) - guard row >= 0, column > 0 else { return true } // column 0 is row number + guard row >= 0, column > 0 else { return true } - let columnIndex = column - 1 // Adjust for row number column - // Keep empty string as empty (not NULL) - use context menu "Set NULL" for NULL + let columnIndex = column - 1 let newValue: String? = textField.stringValue - // Get old value guard let rowData = rowProvider.row(at: row) else { return true } let oldValue = rowData.value(at: columnIndex) - // Skip if no change guard oldValue != newValue else { return true } - // Record change with entire row for WHERE clause PK lookup let columnName = rowProvider.columns[columnIndex] changeManager.recordCellChange( rowIndex: row, @@ -809,125 +474,93 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData originalRow: rowData.values ) - // Update local data rowProvider.updateValue(newValue, at: row, columnIndex: columnIndex) - - // Notify parent view to update tab.resultRows onCellEdit?(row, columnIndex, newValue) - // Reload the edited cell to show yellow background DispatchQueue.main.async { - tableView.reloadData( - forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column)) + tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column)) } return true } - - /// Handle Tab/Shift+Tab navigation between cells during editing + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { guard let tableView = tableView else { return false } - + let currentRow = tableView.row(for: control) let currentColumn = tableView.column(for: control) - + guard currentRow >= 0, currentColumn >= 0 else { return false } - - // Tab key - move to next cell + if commandSelector == #selector(NSResponder.insertTab(_:)) { - // End current editing first (value will be saved by textShouldEndEditing) tableView.window?.makeFirstResponder(tableView) - - // Calculate next cell position + var nextColumn = currentColumn + 1 var nextRow = currentRow - - // Skip to next row if at end of columns + if nextColumn >= tableView.numberOfColumns { - nextColumn = 1 // Skip row number column (column 0) + nextColumn = 1 nextRow += 1 } - - // If at end of table, stay on last cell if nextRow >= tableView.numberOfRows { nextRow = tableView.numberOfRows - 1 nextColumn = tableView.numberOfColumns - 1 } - - // Start editing next cell after a brief delay + DispatchQueue.main.async { tableView.selectRowIndexes(IndexSet(integer: nextRow), byExtendingSelection: false) tableView.editColumn(nextColumn, row: nextRow, with: nil, select: true) } - return true } - - // Shift+Tab - move to previous cell + if commandSelector == #selector(NSResponder.insertBacktab(_:)) { - // End current editing first tableView.window?.makeFirstResponder(tableView) - - // Calculate previous cell position + var prevColumn = currentColumn - 1 var prevRow = currentRow - - // Skip to previous row if at start of columns - if prevColumn < 1 { // Column 0 is row number + + if prevColumn < 1 { prevColumn = tableView.numberOfColumns - 1 prevRow -= 1 } - - // If at start of table, stay on first cell if prevRow < 0 { prevRow = 0 prevColumn = 1 } - - // Start editing previous cell after a brief delay + DispatchQueue.main.async { tableView.selectRowIndexes(IndexSet(integer: prevRow), byExtendingSelection: false) tableView.editColumn(prevColumn, row: prevRow, with: nil, select: true) } - return true } - - // Return key - end editing, stay on row (don't move to next row) + if commandSelector == #selector(NSResponder.insertNewline(_:)) { tableView.window?.makeFirstResponder(tableView) return true } - - // Escape key - cancel editing + if commandSelector == #selector(NSResponder.cancelOperation(_:)) { tableView.window?.makeFirstResponder(tableView) return true } - + return false } // MARK: - Row Actions - /// Delete a single row (used from context menu) - /// For multiple rows, use deleteRows(at:) directly for better performance func deleteRow(at index: Int) { - // Reuse batch logic for consistency deleteRows(at: [index]) } - - /// Delete multiple rows at once (batch operation for performance) - /// This is much faster than calling deleteRow(at:) in a loop because it: - /// - Processes all deletions before UI updates - /// - Triggers a single reloadData() call instead of N calls + func deleteRows(at indices: Set) { guard !indices.isEmpty else { return } - - // Separate inserted rows from existing rows + var insertedRowsToDelete: [Int] = [] var existingRowsToDelete: [(rowIndex: Int, originalRow: [String?])] = [] - + for rowIndex in indices { if changeManager.isRowInserted(rowIndex) { insertedRowsToDelete.append(rowIndex) @@ -936,86 +569,57 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData existingRowsToDelete.append((rowIndex: rowIndex, originalRow: rowData.values)) } } - - // Process inserted rows deletion (removes from data) + if !insertedRowsToDelete.isEmpty { - // Sort descending so removing higher indices first doesn't affect lower indices let sortedInsertedRows = insertedRowsToDelete.sorted(by: >) - - // Remove from rowProvider and changeManager for rowIndex in sortedInsertedRows { rowProvider.removeRow(at: rowIndex) } - - // Batch update changeManager changeManager.undoBatchRowInsertion(rowIndices: sortedInsertedRows) - - // Update cached counts updateCache() } - - // Record batch deletion for existing rows (single undo action) + if !existingRowsToDelete.isEmpty { changeManager.recordBatchRowDeletion(rows: existingRowsToDelete) } - - // Calculate new selection based on deleted rows + let minSelectedRow = indices.min() ?? 0 let maxSelectedRow = indices.max() ?? 0 let totalRows = cachedRowCount let insertedRowsDeletedCount = insertedRowsToDelete.count - - // Adjust for inserted rows that were removed (they shift indices) + let adjustedMaxRow = maxSelectedRow - insertedRowsDeletedCount let adjustedMinRow = minSelectedRow - insertedRowsToDelete.filter { $0 < minSelectedRow }.count - + var newSelection = Set() if adjustedMaxRow + 1 < totalRows { - // Select row after the deleted range newSelection.insert(min(adjustedMaxRow + 1, totalRows - 1)) } else if adjustedMinRow > 0 { - // Deleted rows at end, select previous row newSelection.insert(adjustedMinRow - 1) } else if totalRows > 0 { - // Select first row if available newSelection.insert(0) } - - // Update selection + self.selectedRowIndices = newSelection - - // Single reload for all changes - massive performance improvement! tableView?.reloadData() } - func undoDeleteRow(at index: Int) { changeManager.undoRowDeletion(rowIndex: index) tableView?.reloadData( forRowIndexes: IndexSet(integer: index), columnIndexes: IndexSet(integersIn: 0..<(tableView?.numberOfColumns ?? 0))) } - - /// Trigger adding a new row (calls parent's onAddRow callback) + func addNewRow() { onAddRow?() } - - /// Undo a row insertion (remove from data and change tracking) + func undoInsertRow(at index: Int) { - // Notify parent to remove from resultRows FIRST (before indices change) onUndoInsert?(index) - - // Remove from change manager changeManager.undoRowInsertion(rowIndex: index) - - // Remove from row provider rowProvider.removeRow(at: index) - - // Update cached counts updateCache() - - // Reload entire table since row indices shifted tableView?.reloadData() } @@ -1034,49 +638,38 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData NSPasteboard.general.setString(text, forType: .string) } - /// Set a cell value (for Set NULL / Set Empty actions - legacy, uses selected column) func setCellValue(_ value: String?, at rowIndex: Int) { guard let tableView = tableView else { return } - - // Get selected column (default to first data column) var columnIndex = max(0, tableView.selectedColumn - 1) if columnIndex < 0 { columnIndex = 0 } - setCellValueAtColumn(value, at: rowIndex, columnIndex: columnIndex) } - /// Set a cell value at specific column func setCellValueAtColumn(_ value: String?, at rowIndex: Int, columnIndex: Int) { guard let tableView = tableView else { return } guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return } let columnName = rowProvider.columns[columnIndex] let oldValue = rowProvider.row(at: rowIndex)?.value(at: columnIndex) - - // Get the full original row for WHERE clause generation let originalRow = rowProvider.row(at: rowIndex)?.values - // Record the change WITH original row for proper WHERE clause changeManager.recordCellChange( rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName, oldValue: oldValue, newValue: value, - originalRow: originalRow // CRITICAL: Pass original row to avoid WHERE 1=1 + originalRow: originalRow ) - // Update local data rowProvider.updateValue(value, at: rowIndex, columnIndex: columnIndex) - // Reload only the specific cell that was changed (columnIndex + 1 for row number column) let tableColumnIndex = columnIndex + 1 tableView.reloadData( forRowIndexes: IndexSet(integer: rowIndex), columnIndexes: IndexSet(integer: tableColumnIndex)) } - /// Copy cell value to clipboard func copyCellValue(at rowIndex: Int, columnIndex: Int) { guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return } @@ -1088,257 +681,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData } } -// MARK: - Custom Row View with Context Menu - -final class TableRowViewWithMenu: NSTableRowView { - weak var coordinator: TableViewCoordinator? - var rowIndex: Int = 0 - - override func menu(for event: NSEvent) -> NSMenu? { - guard let coordinator = coordinator, - let tableView = coordinator.tableView - else { return nil } - - // Determine which column was clicked - let locationInRow = convert(event.locationInWindow, from: nil) - let locationInTable = tableView.convert(locationInRow, from: self) - let clickedColumn = tableView.column(at: locationInTable) - - // Adjust for row number column (index 0) - let dataColumnIndex = clickedColumn > 0 ? clickedColumn - 1 : -1 - - let menu = NSMenu() - - if coordinator.changeManager.isRowDeleted(rowIndex) { - menu.addItem( - withTitle: "Undo Delete", action: #selector(undoDeleteRow), keyEquivalent: "" - ).target = self - } - - // Normal row menu (or additional items for inserted rows) - if !coordinator.changeManager.isRowDeleted(rowIndex) { - // Edit actions (if editable) - if coordinator.isEditable && dataColumnIndex >= 0 { - let setValueMenu = NSMenu() - - let emptyItem = NSMenuItem( - title: "Empty", action: #selector(setEmptyValue(_:)), keyEquivalent: "") - emptyItem.representedObject = dataColumnIndex - emptyItem.target = self - setValueMenu.addItem(emptyItem) - - let nullItem = NSMenuItem( - title: "NULL", action: #selector(setNullValue(_:)), keyEquivalent: "") - nullItem.representedObject = dataColumnIndex - nullItem.target = self - setValueMenu.addItem(nullItem) - - let defaultItem = NSMenuItem( - title: "Default", action: #selector(setDefaultValue(_:)), keyEquivalent: "") - defaultItem.representedObject = dataColumnIndex - defaultItem.target = self - setValueMenu.addItem(defaultItem) - - let setValueItem = NSMenuItem(title: "Set Value", action: nil, keyEquivalent: "") - setValueItem.submenu = setValueMenu - menu.addItem(setValueItem) - - menu.addItem(NSMenuItem.separator()) - } - - // Copy actions - if dataColumnIndex >= 0 { - let copyCellItem = NSMenuItem( - title: "Copy Cell Value", action: #selector(copyCellValue(_:)), - keyEquivalent: "") - copyCellItem.representedObject = dataColumnIndex - copyCellItem.target = self - menu.addItem(copyCellItem) - } - - let copyItem = NSMenuItem( - title: "Copy", action: #selector(copySelectedOrCurrentRow), keyEquivalent: "c") - copyItem.keyEquivalentModifierMask = .command - copyItem.target = self - menu.addItem(copyItem) - - if coordinator.isEditable { - menu.addItem(NSMenuItem.separator()) - - let duplicateItem = NSMenuItem( - title: "Duplicate", action: #selector(duplicateRow), keyEquivalent: "d") - duplicateItem.keyEquivalentModifierMask = .command - duplicateItem.target = self - menu.addItem(duplicateItem) - - let deleteItem = NSMenuItem( - title: "Delete", action: #selector(deleteRow), keyEquivalent: String(Character(UnicodeScalar(NSBackspaceCharacter)!))) - deleteItem.keyEquivalentModifierMask = [] - deleteItem.target = self - menu.addItem(deleteItem) - } - } - - return menu - } - - @objc private func deleteRow() { - coordinator?.deleteRow(at: rowIndex) - } - - @objc private func duplicateRow() { - // Post notification to duplicate the selected row - NotificationCenter.default.post(name: .duplicateRow, object: nil) - } - - @objc private func undoDeleteRow() { - coordinator?.undoDeleteRow(at: rowIndex) - } - - @objc private func undoInsertRow() { - coordinator?.undoInsertRow(at: rowIndex) - } - - @objc private func copyRow() { - coordinator?.copyRows(at: [rowIndex]) - } - - @objc private func copySelectedRows() { - guard let selectedIndices = coordinator?.selectedRowIndices else { return } - coordinator?.copyRows(at: selectedIndices) - } - - @objc private func copySelectedOrCurrentRow() { - guard let coordinator = coordinator else { return } - // If rows are selected, copy all selected; otherwise copy current row - if !coordinator.selectedRowIndices.isEmpty { - coordinator.copyRows(at: coordinator.selectedRowIndices) - } else { - coordinator.copyRows(at: [rowIndex]) - } - } - - @objc private func copyCellValue(_ sender: NSMenuItem) { - guard let columnIndex = sender.representedObject as? Int else { return } - coordinator?.copyCellValue(at: rowIndex, columnIndex: columnIndex) - } - - @objc private func setNullValue(_ sender: NSMenuItem) { - guard let columnIndex = sender.representedObject as? Int else { return } - coordinator?.setCellValueAtColumn(nil, at: rowIndex, columnIndex: columnIndex) - } - - @objc private func setEmptyValue(_ sender: NSMenuItem) { - guard let columnIndex = sender.representedObject as? Int else { return } - coordinator?.setCellValueAtColumn("", at: rowIndex, columnIndex: columnIndex) - } - - @objc private func setDefaultValue(_ sender: NSMenuItem) { - guard let columnIndex = sender.representedObject as? Int else { return } - coordinator?.setCellValueAtColumn("__DEFAULT__", at: rowIndex, columnIndex: columnIndex) - } - - // Column resize tracking removed - too complex for current implementation -} - -// MARK: - Custom TextField that delegates context menu to row view - -/// NSTextField subclass that shows row context menu instead of text editing menu -/// This ensures our custom menu (Undo Insert, Set Value, etc.) works even when editing -final class CellTextField: NSTextField { - - /// Override to provide our custom cell that handles context menu - override class var cellClass: AnyClass? { - get { CellTextFieldCell.self } - set { } - } - - /// Override right mouse down to end editing and show row context menu - /// The field editor (NSTextView) normally handles right-click during editing, - /// so we intercept here before it gets to the field editor - override func rightMouseDown(with event: NSEvent) { - // End editing first - window?.makeFirstResponder(nil) - - // Find the row view and show its menu - var view: NSView? = self - while let parent = view?.superview { - if let rowView = parent as? TableRowViewWithMenu { - if let menu = rowView.menu(for: event) { - NSMenu.popUpContextMenu(menu, with: event, for: self) - } - return - } - view = parent - } - } - - override func menu(for event: NSEvent) -> NSMenu? { - // End editing first so the menu shows correctly - window?.makeFirstResponder(nil) - - // Find the row view and delegate to it - var view: NSView? = self - while let parent = view?.superview { - if let rowView = parent as? TableRowViewWithMenu { - return rowView.menu(for: event) - } - view = parent - } - - // Fallback to no menu (don't show system text editing menu) - return nil - } -} - -/// Custom text field cell that provides a field editor with custom context menu behavior -final class CellTextFieldCell: NSTextFieldCell { - - /// Custom field editor that forwards right-click to parent text field - private class CellFieldEditor: NSTextView { - - override func rightMouseDown(with event: NSEvent) { - // End editing and find parent CellTextField - window?.makeFirstResponder(nil) - - // Find the CellTextField and let it handle the menu - var view: NSView? = self - while let parent = view?.superview { - if let cellTextField = parent as? CellTextField { - cellTextField.rightMouseDown(with: event) - return - } - view = parent - } - } - - override func menu(for event: NSEvent) -> NSMenu? { - // Don't show system text editing menu - return nil - } - } - - /// Lazy field editor instance - private var customFieldEditor: CellFieldEditor? - - override func fieldEditor(for controlView: NSView) -> NSTextView? { - if customFieldEditor == nil { - customFieldEditor = CellFieldEditor() - customFieldEditor?.isFieldEditor = true - } - return customFieldEditor - } -} - -// MARK: - NSFont Extension - -extension NSFont { - func withTraits(_ traits: NSFontDescriptor.SymbolicTraits) -> NSFont { - let descriptor = fontDescriptor.withSymbolicTraits(traits) - return NSFont(descriptor: descriptor, size: pointSize) ?? self - } -} - // MARK: - Preview #Preview { @@ -1359,348 +701,3 @@ extension NSFont { ) .frame(width: 600, height: 400) } - -// MARK: - Custom TableView with Key Handling - -/// NSTableView subclass that handles Delete key to mark rows for deletion -/// Also implements TablePlus-style cell focus on click -final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { - weak var coordinator: TableViewCoordinator? - - /// Currently focused row index (-1 = no focus) - /// Tracked separately from selectedRow to avoid async timing bugs - var focusedRow: Int = -1 { - didSet { - if oldValue != focusedRow && oldValue >= 0 { - // Clear focus border from old row - if focusedColumn >= 0 && focusedColumn < numberOfColumns && oldValue < numberOfRows { - reloadData(forRowIndexes: IndexSet(integer: oldValue), - columnIndexes: IndexSet(integer: focusedColumn)) - } - } - } - } - - /// Currently focused column index (-1 = no focus, 0 = row number column) - var focusedColumn: Int = -1 { - didSet { - if oldValue != focusedColumn { - // Capture current focusedRow to avoid async timing bug - let rowToUpdate = focusedRow - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - // Clear old column's border using captured row - if oldValue >= 0 && oldValue < self.numberOfColumns && rowToUpdate >= 0 && rowToUpdate < self.numberOfRows { - self.reloadData(forRowIndexes: IndexSet(integer: rowToUpdate), - columnIndexes: IndexSet(integer: oldValue)) - } - // Draw new column's border using current focusedRow - if self.focusedColumn >= 0 && self.focusedColumn < self.numberOfColumns && self.focusedRow >= 0 && self.focusedRow < self.numberOfRows { - self.reloadData(forRowIndexes: IndexSet(integer: self.focusedRow), - columnIndexes: IndexSet(integer: self.focusedColumn)) - } - } - } - } - } - - /// Anchor row for Shift+Arrow range selection (-1 = no anchor) - var selectionAnchor: Int = -1 - - /// Current pivot row for Shift+Arrow navigation (where the user is navigating to) - var selectionPivot: Int = -1 - - // MARK: - TablePlus-Style Cell Focus - - override func mouseDown(with event: NSEvent) { - // Capture clicked location before super changes selection - let point = convert(event.locationInWindow, from: nil) - let clickedRow = row(at: point) - let clickedColumn = column(at: point) - - // Double-click in empty area (no row) adds a new row (TablePlus behavior) - if event.clickCount == 2 && clickedRow == -1 && coordinator?.isEditable == true { - NotificationCenter.default.post(name: .addNewRow, object: nil) - return - } - - // Reset anchor/pivot when clicking without Shift (starting new selection) - if clickedRow >= 0 && !event.modifierFlags.contains(.shift) { - selectionAnchor = clickedRow - selectionPivot = clickedRow - } - - // Let super handle row selection - super.mouseDown(with: event) - - // After selection, focus the specific cell (TablePlus behavior) - // This makes Enter key edit that cell, not column 0 - guard clickedRow >= 0, - clickedColumn >= 0, - clickedColumn < numberOfColumns, - selectedRowIndexes.contains(clickedRow) else { - return - } - - // Skip row number column (not editable) - let column = tableColumns[clickedColumn] - if column.identifier.rawValue == "__rowNumber__" { - focusedRow = -1 - focusedColumn = -1 - return - } - - // Track focused row and column for keyboard navigation - focusedRow = clickedRow - focusedColumn = clickedColumn - - // Focus the cell without opening editor (select: false) - // This is the native AppKit way to set cell focus - // When user presses Enter, this cell will be edited - editColumn(clickedColumn, row: clickedRow, with: nil, select: false) - } - - // MARK: - Standard Edit Menu Actions - - /// Respond to Edit > Delete menu item - @objc func delete(_ sender: Any?) { - guard coordinator?.isEditable == true else { return } - let selectedIndices = Set(selectedRowIndexes.map { $0 }) - guard !selectedIndices.isEmpty else { return } - - // Batch delete all selected rows (single UI reload) - coordinator?.deleteRows(at: selectedIndices) - } - - /// Enable/disable Edit menu items based on state - func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { - if menuItem.action == #selector(delete(_:)) { - // Enable Delete when rows are selected and table is editable - return coordinator?.isEditable == true && !selectedRowIndexes.isEmpty - } - // For other items, check if we can respond to them - if let action = menuItem.action { - return responds(to: action) - } - return false - } - - // MARK: - Keyboard Handling - - /// Override to catch Delete/Backspace before menu items can intercept - override func performKeyEquivalent(with event: NSEvent) -> Bool { - // Delete (keyCode 51) or Forward Delete (keyCode 117) - if event.keyCode == 51 || event.keyCode == 117 { - let selectedIndices = Set(selectedRowIndexes.map { $0 }) - if !selectedIndices.isEmpty && coordinator?.isEditable == true { - // Batch delete all selected rows (single UI reload) - coordinator?.deleteRows(at: selectedIndices) - return true // We handled it - } - } - return super.performKeyEquivalent(with: event) - } - - override func keyDown(with event: NSEvent) { - // Note: Cmd+N is captured by app menu (New Connection) - // Use File > Add Row (Cmd+I) for adding rows - - let row = selectedRow - let isShiftHeld = event.modifierFlags.contains(.shift) - - switch event.keyCode { - case 126: // Up arrow - move to previous row (Shift extends selection) - handleUpArrow(currentRow: row, isShiftHeld: isShiftHeld) - return - - case 125: // Down arrow - move to next row (Shift extends selection) - handleDownArrow(currentRow: row, isShiftHeld: isShiftHeld) - return - - case 123: // Left arrow - move to previous column - if focusedColumn > 1 { // Skip row number column (index 0) - focusedColumn -= 1 - if row >= 0 { - scrollColumnToVisible(focusedColumn) - } - } else if focusedColumn == -1 && numberOfColumns > 1 { - // No focus yet, start at last column - focusedColumn = numberOfColumns - 1 - if row >= 0 { - scrollColumnToVisible(focusedColumn) - } - } - return - - case 124: // Right arrow - move to next column - if focusedColumn >= 1 && focusedColumn < numberOfColumns - 1 { - focusedColumn += 1 - if row >= 0 { - scrollColumnToVisible(focusedColumn) - } - } else if focusedColumn == -1 && numberOfColumns > 1 { - // No focus yet, start at first data column - focusedColumn = 1 - if row >= 0 { - scrollColumnToVisible(focusedColumn) - } - } - return - - case 36: // Enter/Return - edit focused cell - if row >= 0 && focusedColumn >= 1 && coordinator?.isEditable == true { - editColumn(focusedColumn, row: row, with: nil, select: true) - } - return - - case 53: // Escape - clear focus and selection - focusedRow = -1 - focusedColumn = -1 - NotificationCenter.default.post(name: .clearSelection, object: nil) - return - - case 51, 117: // Delete or Backspace key - // Post notification to trigger batched deletion in MainContentView - // This enables undoing all deletions at once - if !selectedRowIndexes.isEmpty { - NotificationCenter.default.post(name: .deleteSelectedRows, object: nil) - return - } - - case 48: // Tab - move to next cell - if row >= 0 && focusedColumn >= 1 { - var nextColumn = focusedColumn + 1 - var nextRow = row - - if nextColumn >= numberOfColumns { - nextColumn = 1 // Skip row number column - nextRow += 1 - } - if nextRow >= numberOfRows { - nextRow = numberOfRows - 1 - nextColumn = numberOfColumns - 1 - } - - selectRowIndexes(IndexSet(integer: nextRow), byExtendingSelection: false) - focusedRow = nextRow // Update focusedRow when moving to next cell - focusedColumn = nextColumn - scrollRowToVisible(nextRow) - scrollColumnToVisible(nextColumn) - } - return - - default: - break - } - - super.keyDown(with: event) - } - - // MARK: - Arrow Key Selection Helpers - - /// Handle Up arrow key with optional Shift for range selection - private func handleUpArrow(currentRow: Int, isShiftHeld: Bool) { - guard numberOfRows > 0 else { return } - - if currentRow == -1 { - // No selection, select last row - let targetRow = numberOfRows - 1 - selectionAnchor = targetRow - selectionPivot = targetRow - focusedRow = targetRow // Track focused row - selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) - scrollRowToVisible(targetRow) - return - } - - if isShiftHeld { - // Shift+Up: extend/shrink selection - if selectionAnchor == -1 { - selectionAnchor = currentRow - selectionPivot = currentRow - } - - // Use pivot for navigation, not selectedRow - let currentPivot = selectionPivot >= 0 ? selectionPivot : currentRow - let targetRow = max(0, currentPivot - 1) - selectionPivot = targetRow - - // Select range from anchor to pivot - let startRow = min(selectionAnchor, selectionPivot) - let endRow = max(selectionAnchor, selectionPivot) - let range = IndexSet(integersIn: startRow...endRow) - selectRowIndexes(range, byExtendingSelection: false) - scrollRowToVisible(targetRow) - } else { - // Normal Up: move to previous row, single selection - let targetRow = max(0, currentRow - 1) - selectionAnchor = targetRow - selectionPivot = targetRow - focusedRow = targetRow // Track focused row - selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) - scrollRowToVisible(targetRow) - } - } - - /// Handle Down arrow key with optional Shift for range selection - private func handleDownArrow(currentRow: Int, isShiftHeld: Bool) { - guard numberOfRows > 0 else { return } - - if currentRow == -1 { - // No selection, select first row - selectionAnchor = 0 - selectionPivot = 0 - focusedRow = 0 // Track focused row - selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - scrollRowToVisible(0) - return - } - - if isShiftHeld { - // Shift+Down: extend/shrink selection - if selectionAnchor == -1 { - selectionAnchor = currentRow - selectionPivot = currentRow - } - - // Use pivot for navigation, not selectedRow - let currentPivot = selectionPivot >= 0 ? selectionPivot : currentRow - let targetRow = min(numberOfRows - 1, currentPivot + 1) - selectionPivot = targetRow - - // Select range from anchor to pivot - let startRow = min(selectionAnchor, selectionPivot) - let endRow = max(selectionAnchor, selectionPivot) - let range = IndexSet(integersIn: startRow...endRow) - selectRowIndexes(range, byExtendingSelection: false) - scrollRowToVisible(targetRow) - } else { - // Normal Down: move to next row, single selection - let targetRow = min(numberOfRows - 1, currentRow + 1) - selectionAnchor = targetRow - selectionPivot = targetRow - focusedRow = targetRow // Track focused row - selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) - scrollRowToVisible(targetRow) - } - } - - override func menu(for event: NSEvent) -> NSMenu? { - let point = convert(event.locationInWindow, from: nil) - let clickedRow = row(at: point) - - // If clicked on a valid row, get its row view's menu - if clickedRow >= 0, - let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) - as? TableRowViewWithMenu - { - // Select the row if not already selected - if !selectedRowIndexes.contains(clickedRow) { - selectRowIndexes(IndexSet(integer: clickedRow), byExtendingSelection: false) - } - return rowView.menu(for: event) - } - - return super.menu(for: event) - } -} diff --git a/OpenTable/Views/Results/FilterPanelView.swift b/OpenTable/Views/Results/FilterPanelView.swift deleted file mode 100644 index a006a96b..00000000 --- a/OpenTable/Views/Results/FilterPanelView.swift +++ /dev/null @@ -1,758 +0,0 @@ -// -// FilterPanelView.swift -// OpenTable -// -// Filter panel UI for table data filtering -// - -import SwiftUI - -/// Bottom filter panel for table data filtering -struct FilterPanelView: View { - @ObservedObject var filterState: FilterStateManager - let columns: [String] - let primaryKeyColumn: String? - let databaseType: DatabaseType - let onApply: ([TableFilter]) -> Void - let onUnset: () -> Void - let onQuickSearch: ((String) -> Void)? // New callback for Quick Search - - @State private var showSQLSheet = false - @State private var showSettingsPopover = false - @State private var generatedSQL = "" - @State private var showSavePresetAlert = false - @State private var newPresetName = "" - @State private var savedPresets: [FilterPreset] = [] - - var body: some View { - VStack(spacing: 0) { - // Header with title and action buttons - filterHeader - - Divider() - .foregroundStyle(Color(nsColor: .separatorColor)) - - // Quick Search field (when no filters or alongside filters) - if filterState.hasActiveQuickSearch || filterState.filters.isEmpty { - quickSearchField - Divider() - .foregroundStyle(Color(nsColor: .separatorColor)) - } - - // Filter rows - if filterState.filters.isEmpty { - if !filterState.hasActiveQuickSearch { - emptyState - } - } else { - filterList - } - - Divider() - .foregroundStyle(Color(nsColor: .separatorColor)) - - // Footer with Apply All, Unset, SQL buttons - filterFooter - } - .background(Color(nsColor: .windowBackgroundColor)) - .sheet(isPresented: $showSQLSheet) { - SQLPreviewSheet(sql: generatedSQL, tableName: "", databaseType: databaseType) - } - } - - // MARK: - Header - - private var filterHeader: some View { - HStack(spacing: 8) { - Text("Filters") - .font(.system(size: 12, weight: .medium)) - - if filterState.hasAppliedFilters { - Text("(\(filterState.appliedFilters.count) active)") - .font(.system(size: 11)) - .foregroundStyle(.secondary) - } - - Spacer() - - // AND/OR Logic Toggle - Picker("", selection: $filterState.filterLogicMode) { - Text("AND").tag(FilterLogicMode.and) - Text("OR").tag(FilterLogicMode.or) - } - .pickerStyle(.segmented) - .frame(width: 80) - .help("Match ALL filters (AND) or ANY filter (OR)") - - // Presets Menu - Menu { - // Load preset section - if !savedPresets.isEmpty { - ForEach(savedPresets) { preset in - Button(preset.name) { - filterState.loadPreset(preset) - } - } - Divider() - } - - // Save current filters - Button("Save as Preset...") { - newPresetName = "" - showSavePresetAlert = true - } - .disabled(filterState.filters.isEmpty) - - // Delete presets - if !savedPresets.isEmpty { - Menu("Delete Preset") { - ForEach(savedPresets) { preset in - Button(preset.name, role: .destructive) { - filterState.deletePreset(preset) - loadPresets() - } - } - } - } - } label: { - Image(systemName: "folder") - .font(.system(size: 12)) - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - .help("Save and load filter presets") - .onAppear { - loadPresets() - } - - // Settings button (gear icon) - Button(action: { showSettingsPopover.toggle() }) { - Image(systemName: "gearshape") - .font(.system(size: 12)) - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - .help("Filter Settings") - .popover(isPresented: $showSettingsPopover, arrowEdge: .bottom) { - FilterSettingsPopover() - } - - // Add filter button - Button(action: { - filterState.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn) - }) { - Image(systemName: "plus") - .font(.system(size: 12)) - } - .buttonStyle(.borderless) - .foregroundColor(.accentColor) - .help("Add Filter (Cmd+Shift+F)") - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .background(Color(nsColor: .controlBackgroundColor)) - .contentShape(Rectangle()) - .onTapGesture { filterState.focusedFilterId = nil } - .alert("Save Filter Preset", isPresented: $showSavePresetAlert) { - TextField("Preset Name", text: $newPresetName) - Button("Cancel", role: .cancel) {} - Button("Save") { - if !newPresetName.isEmpty { - filterState.saveAsPreset(name: newPresetName) - loadPresets() - } - } - } message: { - Text("Enter a name for this filter preset") - } - } - - // MARK: - Quick Search - - private var quickSearchField: some View { - HStack(spacing: 8) { - Image(systemName: "magnifyingglass") - .font(.system(size: 12)) - .foregroundStyle(.secondary) - - TextField("Quick search across all columns...", text: $filterState.quickSearchText) - .textFieldStyle(.plain) - .font(.system(size: 12)) - .onSubmit { - // Apply quick search on Enter - if !filterState.quickSearchText.isEmpty { - onQuickSearch?(filterState.quickSearchText) - } - } - - if filterState.hasActiveQuickSearch { - Button(action: { filterState.clearQuickSearch() }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 12)) - .foregroundStyle(.secondary) - } - .buttonStyle(.borderless) - .help("Clear Search") - } - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color(nsColor: .textBackgroundColor)) - } - - // MARK: - Empty State - - private var emptyState: some View { - VStack(spacing: 12) { - Image(systemName: "line.3.horizontal.decrease.circle") - .font(.system(size: 32)) - .foregroundStyle(.tertiary) - - Text("No filters active") - .font(.system(size: 13, weight: .medium)) - .foregroundStyle(.secondary) - - HStack(spacing: 8) { - Button("Add Filter") { - filterState.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn) - } - .buttonStyle(.bordered) - .controlSize(.small) - - Text("or use Quick Search above") - .font(.system(size: 11)) - .foregroundStyle(.tertiary) - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 24) - } - - // MARK: - Filter List - - private var filterList: some View { - ScrollViewReader { proxy in - ScrollView { - LazyVStack(spacing: 2) { - ForEach(filterState.filters) { filter in - FilterRowView( - filter: filterState.binding(for: filter), - columns: columns, - isFocused: filterState.focusedFilterId == filter.id, - onDuplicate: { filterState.duplicateFilter(filter) }, - onRemove: { filterState.removeFilter(filter) }, - onApply: { applySingleFilter(filter) }, - onFocus: { filterState.focusedFilterId = filter.id } - ) - .id(filter.id) // Make each row identifiable for scrollTo - } - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - } - // Dynamic height: ~40pt per row, max 4 rows visible before scrolling - .frame(maxHeight: min(CGFloat(filterState.filters.count) * 40 + 8, 160)) - .onChange(of: filterState.focusedFilterId) { _, newFocusedId in - // Auto-scroll to the focused filter (newly added or explicitly focused) - if let focusedId = newFocusedId { - withAnimation(.easeInOut(duration: 0.25)) { - proxy.scrollTo(focusedId, anchor: .bottom) - } - } - } - } - } - - // MARK: - Footer - - private var filterFooter: some View { - HStack(spacing: 8) { - // Select all checkbox - Toggle("Select All", isOn: selectAllBinding) - .toggleStyle(.checkbox) - .font(.system(size: 11)) - .foregroundStyle(.secondary) - .disabled(filterState.filters.isEmpty) - - Spacer() - - // Unset button - Button("Unset") { - filterState.clearAppliedFilters() - onUnset() - } - .buttonStyle(.bordered) - .controlSize(.small) - .disabled(!filterState.hasAppliedFilters) - - // SQL button - now uses extracted method - Button("SQL") { - generatedSQL = filterState.generatePreviewSQL(databaseType: databaseType) - showSQLSheet = true - } - .buttonStyle(.bordered) - .controlSize(.small) - .disabled(filterState.filters.isEmpty) - - // Apply All button (for selected filters) - Button("Apply All") { - applySelectedFilters() - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .disabled(!filterState.hasSelectedFilters) - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .contentShape(Rectangle()) - .onTapGesture { filterState.focusedFilterId = nil } - } - - // MARK: - Helpers - - private var selectAllBinding: Binding { - Binding( - get: { filterState.allFiltersSelected }, - set: { filterState.selectAll($0) } - ) - } - - private func applySingleFilter(_ filter: TableFilter) { - guard filter.isValid else { return } - filterState.applySingleFilter(filter) - onApply([filter]) - } - - private func applySelectedFilters() { - filterState.applySelectedFilters() - onApply(filterState.appliedFilters) - } - - private func loadPresets() { - savedPresets = filterState.loadAllPresets() - } -} - -// MARK: - Filter Row View - -/// Single filter row view with native macOS styling -struct FilterRowView: View { - @Binding var filter: TableFilter - let columns: [String] - let isFocused: Bool - let onDuplicate: () -> Void - let onRemove: () -> Void - let onApply: () -> Void - let onFocus: () -> Void - - @State private var isHovered: Bool = false - - - /// Display name for the column (handles raw SQL and empty) - private var displayColumnName: String { - if filter.columnName == TableFilter.rawSQLColumn { - return "Raw SQL" - } else if filter.columnName.isEmpty { - return "Column" - } else { - return filter.columnName - } - } - - /// Dynamic background color based on state - private var backgroundFillColor: Color { - if isFocused { - return Color.accentColor.opacity(0.06) - } else if isHovered { - return Color(nsColor: .controlBackgroundColor) - } else { - return Color.clear - } - } - - /// Dynamic border color based on state - private var borderColor: Color { - if isFocused { - return Color.accentColor.opacity(0.3) - } else if isHovered { - return Color(nsColor: .separatorColor).opacity(0.5) - } else { - return Color.clear - } - } - - var body: some View { - HStack(spacing: 8) { - // Checkbox for multi-select - Toggle("", isOn: $filter.isSelected) - .toggleStyle(.checkbox) - .labelsHidden() - - // Column dropdown - native Menu style - columnMenu - .frame(width: 120) - - // Operator dropdown (hidden for raw SQL) - if !filter.isRawSQL { - operatorMenu - .frame(width: 110) - } - - // Value field(s) - valueFields - - Spacer(minLength: 0) - - // Action buttons - actionButtons - } - .padding(.vertical, 6) - .padding(.horizontal, 8) - .background( - RoundedRectangle(cornerRadius: 4) - .fill(backgroundFillColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 4) - .strokeBorder(borderColor, lineWidth: 1) - ) - .contentShape(Rectangle()) - .onTapGesture { onFocus() } - .onHover { hovering in - withAnimation(.easeInOut(duration: 0.15)) { - isHovered = hovering - } - } - .animation(.easeInOut(duration: 0.2), value: isFocused) - } - - // MARK: - Column Menu - - private var columnMenu: some View { - Menu { - Button(action: { filter.columnName = TableFilter.rawSQLColumn }) { - if filter.columnName == TableFilter.rawSQLColumn { - Label("Raw SQL", systemImage: "checkmark") - } else { - Text("Raw SQL") - } - } - - if !columns.isEmpty { - Divider() - ForEach(columns, id: \.self) { column in - Button(action: { filter.columnName = column }) { - if filter.columnName == column { - Label(column, systemImage: "checkmark") - } else { - Text(column) - } - } - } - } - } label: { - HStack(spacing: 4) { - Text(displayColumnName) - .font(.system(size: 12)) - .lineLimit(1) - .truncationMode(.tail) - Spacer(minLength: 0) - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color(nsColor: .controlBackgroundColor)) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - } - .menuStyle(.borderlessButton) - .simultaneousGesture(TapGesture().onEnded { onFocus() }) - } - - // MARK: - Operator Menu - - private var operatorMenu: some View { - Menu { - ForEach(FilterOperator.allCases) { op in - Button(action: { filter.filterOperator = op }) { - if filter.filterOperator == op { - Label(op.displayName, systemImage: "checkmark") - } else { - Text(op.displayName) - } - } - } - } label: { - HStack(spacing: 4) { - Text(filter.filterOperator.displayName) - .font(.system(size: 12)) - .lineLimit(1) - Spacer(minLength: 0) - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color(nsColor: .controlBackgroundColor)) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - } - .menuStyle(.borderlessButton) - .simultaneousGesture(TapGesture().onEnded { onFocus() }) - } - - // MARK: - Value Fields - - @ViewBuilder - private var valueFields: some View { - if filter.isRawSQL { - // Raw SQL input - TextField("WHERE clause...", text: Binding( - get: { filter.rawSQL ?? "" }, - set: { filter.rawSQL = $0 } - )) - .textFieldStyle(.plain) - .font(.system(size: 12)) - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color(nsColor: .textBackgroundColor)) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - .onSubmit { onApply() } - .simultaneousGesture(TapGesture().onEnded { onFocus() }) - } else if filter.filterOperator.requiresValue { - // Standard value input - TextField("Value", text: $filter.value) - .textFieldStyle(.plain) - .font(.system(size: 12)) - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color(nsColor: .textBackgroundColor)) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - .frame(minWidth: 80) - .onSubmit { onApply() } - .simultaneousGesture(TapGesture().onEnded { onFocus() }) - - // Second value for BETWEEN - if filter.filterOperator.requiresSecondValue { - Text("and") - .font(.system(size: 11)) - .foregroundStyle(.secondary) - - TextField("Value", text: Binding( - get: { filter.secondValue ?? "" }, - set: { filter.secondValue = $0 } - )) - .textFieldStyle(.plain) - .font(.system(size: 12)) - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color(nsColor: .textBackgroundColor)) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - .frame(minWidth: 80) - .onSubmit { onApply() } - .simultaneousGesture(TapGesture().onEnded { onFocus() }) - } - } else { - // No value needed (IS NULL, etc.) - show indicator - Text("—") - .font(.system(size: 12)) - .foregroundStyle(.tertiary) - .frame(minWidth: 80, alignment: .leading) - } - } - - // MARK: - Action Buttons - - private var actionButtons: some View { - HStack(spacing: 8) { - // Apply single filter - Button(action: onApply) { - Image(systemName: "play.fill") - .font(.system(size: 11)) - } - .buttonStyle(.borderless) - .foregroundStyle(filter.isValid ? Color(nsColor: .systemGreen) : Color.secondary) - .disabled(!filter.isValid) - .help("Apply This Filter") - - // Duplicate - Button(action: onDuplicate) { - Image(systemName: "doc.on.doc") - .font(.system(size: 11)) - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - .help("Duplicate Filter") - - // Remove - Button(action: onRemove) { - Image(systemName: "xmark") - .font(.system(size: 11)) - } - .buttonStyle(.borderless) - .foregroundStyle(.secondary) - .help("Remove Filter") - } - } -} - -// MARK: - SQL Preview Sheet - -/// Modal sheet to display generated SQL -struct SQLPreviewSheet: View { - let sql: String - let tableName: String - let databaseType: DatabaseType - @Environment(\.dismiss) private var dismiss - @State private var copied = false - - var body: some View { - VStack(spacing: 16) { - HStack { - Text("Generated WHERE Clause") - .font(.system(size: 13, weight: .semibold)) - Spacer() - Button(action: { dismiss() }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 14)) - .foregroundStyle(.tertiary) - } - .buttonStyle(.borderless) - } - - ScrollView { - Text(sql.isEmpty ? "(no conditions)" : sql) - .font(.system(size: 12, design: .monospaced)) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(12) - } - .frame(maxHeight: 180) - .background(Color(nsColor: .textBackgroundColor)) - .cornerRadius(6) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) - ) - - HStack { - Button(action: copyToClipboard) { - HStack(spacing: 4) { - Image(systemName: copied ? "checkmark" : "doc.on.doc") - .font(.system(size: 11)) - Text(copied ? "Copied!" : "Copy") - .font(.system(size: 12)) - } - } - .buttonStyle(.bordered) - .controlSize(.small) - .disabled(sql.isEmpty) - - Spacer() - - Button("Close") { - dismiss() - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .keyboardShortcut(.escape) - } - } - .padding(16) - .frame(width: 480, height: 300) - } - - private func copyToClipboard() { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(sql, forType: .string) - copied = true - - // Reset after delay - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - copied = false - } - } -} - -// MARK: - Filter Settings Popover - -/// Popover for filter default settings -struct FilterSettingsPopover: View { - @State private var settings: FilterSettings - - init() { - _settings = State(initialValue: FilterSettingsStorage.shared.loadSettings()) - } - - var body: some View { - Form { - Picker("Default Column", selection: $settings.defaultColumn) { - ForEach(FilterDefaultColumn.allCases) { option in - Text(option.displayName).tag(option) - } - } - - Picker("Default Operator", selection: $settings.defaultOperator) { - ForEach(FilterDefaultOperator.allCases) { option in - Text(option.displayName).tag(option) - } - } - - Picker("Panel State", selection: $settings.panelState) { - ForEach(FilterPanelDefaultState.allCases) { option in - Text(option.displayName).tag(option) - } - } - } - .formStyle(.grouped) - .frame(width: 280) - .onChange(of: settings) { _, newValue in - FilterSettingsStorage.shared.saveSettings(newValue) - } - } -} - -// MARK: - Preview - -#Preview("Filter Panel") { - FilterPanelView( - filterState: { - let state = FilterStateManager() - Task { @MainActor in - state.filters = [ - TableFilter(columnName: "name", filterOperator: .contains, value: "John"), - TableFilter(columnName: "age", filterOperator: .greaterThan, value: "18") - ] - } - return state - }(), - columns: ["id", "name", "age", "email"], - primaryKeyColumn: "id", - databaseType: .mysql, - onApply: { _ in }, - onUnset: { }, - onQuickSearch: { _ in } - ) - .frame(width: 600) -} diff --git a/OpenTable/Views/Results/KeyHandlingTableView.swift b/OpenTable/Views/Results/KeyHandlingTableView.swift new file mode 100644 index 00000000..7deb194e --- /dev/null +++ b/OpenTable/Views/Results/KeyHandlingTableView.swift @@ -0,0 +1,297 @@ +// +// KeyHandlingTableView.swift +// OpenTable +// +// NSTableView subclass that handles Delete key and TablePlus-style cell focus. +// Extracted from DataGridView for better maintainability. +// + +import AppKit + +/// NSTableView subclass that handles Delete key to mark rows for deletion +/// Also implements TablePlus-style cell focus on click +final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { + weak var coordinator: TableViewCoordinator? + + /// Currently focused row index (-1 = no focus) + var focusedRow: Int = -1 { + didSet { + if oldValue != focusedRow && oldValue >= 0 { + if focusedColumn >= 0 && focusedColumn < numberOfColumns && oldValue < numberOfRows { + reloadData(forRowIndexes: IndexSet(integer: oldValue), + columnIndexes: IndexSet(integer: focusedColumn)) + } + } + } + } + + /// Currently focused column index (-1 = no focus, 0 = row number column) + var focusedColumn: Int = -1 { + didSet { + if oldValue != focusedColumn { + let rowToUpdate = focusedRow + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if oldValue >= 0 && oldValue < self.numberOfColumns && rowToUpdate >= 0 && rowToUpdate < self.numberOfRows { + self.reloadData(forRowIndexes: IndexSet(integer: rowToUpdate), + columnIndexes: IndexSet(integer: oldValue)) + } + if self.focusedColumn >= 0 && self.focusedColumn < self.numberOfColumns && self.focusedRow >= 0 && self.focusedRow < self.numberOfRows { + self.reloadData(forRowIndexes: IndexSet(integer: self.focusedRow), + columnIndexes: IndexSet(integer: self.focusedColumn)) + } + } + } + } + } + + /// Anchor row for Shift+Arrow range selection (-1 = no anchor) + var selectionAnchor: Int = -1 + + /// Current pivot row for Shift+Arrow navigation + var selectionPivot: Int = -1 + + // MARK: - TablePlus-Style Cell Focus + + override func mouseDown(with event: NSEvent) { + let point = convert(event.locationInWindow, from: nil) + let clickedRow = row(at: point) + let clickedColumn = column(at: point) + + // Double-click in empty area adds a new row + if event.clickCount == 2 && clickedRow == -1 && coordinator?.isEditable == true { + NotificationCenter.default.post(name: .addNewRow, object: nil) + return + } + + // Reset anchor/pivot when clicking without Shift + if clickedRow >= 0 && !event.modifierFlags.contains(.shift) { + selectionAnchor = clickedRow + selectionPivot = clickedRow + } + + super.mouseDown(with: event) + + guard clickedRow >= 0, + clickedColumn >= 0, + clickedColumn < numberOfColumns, + selectedRowIndexes.contains(clickedRow) else { + return + } + + let column = tableColumns[clickedColumn] + if column.identifier.rawValue == "__rowNumber__" { + focusedRow = -1 + focusedColumn = -1 + return + } + + focusedRow = clickedRow + focusedColumn = clickedColumn + editColumn(clickedColumn, row: clickedRow, with: nil, select: false) + } + + // MARK: - Standard Edit Menu Actions + + @objc func delete(_ sender: Any?) { + guard coordinator?.isEditable == true else { return } + let selectedIndices = Set(selectedRowIndexes.map { $0 }) + guard !selectedIndices.isEmpty else { return } + coordinator?.deleteRows(at: selectedIndices) + } + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.action == #selector(delete(_:)) { + return coordinator?.isEditable == true && !selectedRowIndexes.isEmpty + } + if let action = menuItem.action { + return responds(to: action) + } + return false + } + + // MARK: - Keyboard Handling + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + if event.keyCode == 51 || event.keyCode == 117 { + let selectedIndices = Set(selectedRowIndexes.map { $0 }) + if !selectedIndices.isEmpty && coordinator?.isEditable == true { + coordinator?.deleteRows(at: selectedIndices) + return true + } + } + return super.performKeyEquivalent(with: event) + } + + override func keyDown(with event: NSEvent) { + let row = selectedRow + let isShiftHeld = event.modifierFlags.contains(.shift) + + switch event.keyCode { + case 126: // Up arrow + handleUpArrow(currentRow: row, isShiftHeld: isShiftHeld) + return + + case 125: // Down arrow + handleDownArrow(currentRow: row, isShiftHeld: isShiftHeld) + return + + case 123: // Left arrow + if focusedColumn > 1 { + focusedColumn -= 1 + if row >= 0 { scrollColumnToVisible(focusedColumn) } + } else if focusedColumn == -1 && numberOfColumns > 1 { + focusedColumn = numberOfColumns - 1 + if row >= 0 { scrollColumnToVisible(focusedColumn) } + } + return + + case 124: // Right arrow + if focusedColumn >= 1 && focusedColumn < numberOfColumns - 1 { + focusedColumn += 1 + if row >= 0 { scrollColumnToVisible(focusedColumn) } + } else if focusedColumn == -1 && numberOfColumns > 1 { + focusedColumn = 1 + if row >= 0 { scrollColumnToVisible(focusedColumn) } + } + return + + case 36: // Enter/Return + if row >= 0 && focusedColumn >= 1 && coordinator?.isEditable == true { + editColumn(focusedColumn, row: row, with: nil, select: true) + } + return + + case 53: // Escape + focusedRow = -1 + focusedColumn = -1 + NotificationCenter.default.post(name: .clearSelection, object: nil) + return + + case 51, 117: // Delete or Backspace + if !selectedRowIndexes.isEmpty { + NotificationCenter.default.post(name: .deleteSelectedRows, object: nil) + return + } + + case 48: // Tab + if row >= 0 && focusedColumn >= 1 { + var nextColumn = focusedColumn + 1 + var nextRow = row + + if nextColumn >= numberOfColumns { + nextColumn = 1 + nextRow += 1 + } + if nextRow >= numberOfRows { + nextRow = numberOfRows - 1 + nextColumn = numberOfColumns - 1 + } + + selectRowIndexes(IndexSet(integer: nextRow), byExtendingSelection: false) + focusedRow = nextRow + focusedColumn = nextColumn + scrollRowToVisible(nextRow) + scrollColumnToVisible(nextColumn) + } + return + + default: + break + } + + super.keyDown(with: event) + } + + // MARK: - Arrow Key Selection Helpers + + private func handleUpArrow(currentRow: Int, isShiftHeld: Bool) { + guard numberOfRows > 0 else { return } + + if currentRow == -1 { + let targetRow = numberOfRows - 1 + selectionAnchor = targetRow + selectionPivot = targetRow + focusedRow = targetRow + selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) + scrollRowToVisible(targetRow) + return + } + + if isShiftHeld { + if selectionAnchor == -1 { + selectionAnchor = currentRow + selectionPivot = currentRow + } + + let currentPivot = selectionPivot >= 0 ? selectionPivot : currentRow + let targetRow = max(0, currentPivot - 1) + selectionPivot = targetRow + + let startRow = min(selectionAnchor, selectionPivot) + let endRow = max(selectionAnchor, selectionPivot) + let range = IndexSet(integersIn: startRow...endRow) + selectRowIndexes(range, byExtendingSelection: false) + scrollRowToVisible(targetRow) + } else { + let targetRow = max(0, currentRow - 1) + selectionAnchor = targetRow + selectionPivot = targetRow + focusedRow = targetRow + selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) + scrollRowToVisible(targetRow) + } + } + + private func handleDownArrow(currentRow: Int, isShiftHeld: Bool) { + guard numberOfRows > 0 else { return } + + if currentRow == -1 { + selectionAnchor = 0 + selectionPivot = 0 + focusedRow = 0 + selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + scrollRowToVisible(0) + return + } + + if isShiftHeld { + if selectionAnchor == -1 { + selectionAnchor = currentRow + selectionPivot = currentRow + } + + let currentPivot = selectionPivot >= 0 ? selectionPivot : currentRow + let targetRow = min(numberOfRows - 1, currentPivot + 1) + selectionPivot = targetRow + + let startRow = min(selectionAnchor, selectionPivot) + let endRow = max(selectionAnchor, selectionPivot) + let range = IndexSet(integersIn: startRow...endRow) + selectRowIndexes(range, byExtendingSelection: false) + scrollRowToVisible(targetRow) + } else { + let targetRow = min(numberOfRows - 1, currentRow + 1) + selectionAnchor = targetRow + selectionPivot = targetRow + focusedRow = targetRow + selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false) + scrollRowToVisible(targetRow) + } + } + + override func menu(for event: NSEvent) -> NSMenu? { + let point = convert(event.locationInWindow, from: nil) + let clickedRow = row(at: point) + + if clickedRow >= 0, + let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) as? TableRowViewWithMenu { + if !selectedRowIndexes.contains(clickedRow) { + selectRowIndexes(IndexSet(integer: clickedRow), byExtendingSelection: false) + } + return rowView.menu(for: event) + } + + return super.menu(for: event) + } +} diff --git a/OpenTable/Views/Results/TableRowViewWithMenu.swift b/OpenTable/Views/Results/TableRowViewWithMenu.swift new file mode 100644 index 00000000..d2286785 --- /dev/null +++ b/OpenTable/Views/Results/TableRowViewWithMenu.swift @@ -0,0 +1,156 @@ +// +// TableRowViewWithMenu.swift +// OpenTable +// +// Custom row view with context menu support. +// Extracted from DataGridView for better maintainability. +// + +import AppKit + +/// Custom row view that provides context menu for row operations +final class TableRowViewWithMenu: NSTableRowView { + weak var coordinator: TableViewCoordinator? + var rowIndex: Int = 0 + + override func menu(for event: NSEvent) -> NSMenu? { + guard let coordinator = coordinator, + let tableView = coordinator.tableView else { return nil } + + // Determine which column was clicked + let locationInRow = convert(event.locationInWindow, from: nil) + let locationInTable = tableView.convert(locationInRow, from: self) + let clickedColumn = tableView.column(at: locationInTable) + + // Adjust for row number column (index 0) + let dataColumnIndex = clickedColumn > 0 ? clickedColumn - 1 : -1 + + let menu = NSMenu() + + if coordinator.changeManager.isRowDeleted(rowIndex) { + menu.addItem( + withTitle: "Undo Delete", action: #selector(undoDeleteRow), keyEquivalent: "" + ).target = self + } + + // Normal row menu (or additional items for inserted rows) + if !coordinator.changeManager.isRowDeleted(rowIndex) { + // Edit actions (if editable) + if coordinator.isEditable && dataColumnIndex >= 0 { + let setValueMenu = NSMenu() + + let emptyItem = NSMenuItem( + title: "Empty", action: #selector(setEmptyValue(_:)), keyEquivalent: "") + emptyItem.representedObject = dataColumnIndex + emptyItem.target = self + setValueMenu.addItem(emptyItem) + + let nullItem = NSMenuItem( + title: "NULL", action: #selector(setNullValue(_:)), keyEquivalent: "") + nullItem.representedObject = dataColumnIndex + nullItem.target = self + setValueMenu.addItem(nullItem) + + let defaultItem = NSMenuItem( + title: "Default", action: #selector(setDefaultValue(_:)), keyEquivalent: "") + defaultItem.representedObject = dataColumnIndex + defaultItem.target = self + setValueMenu.addItem(defaultItem) + + let setValueItem = NSMenuItem(title: "Set Value", action: nil, keyEquivalent: "") + setValueItem.submenu = setValueMenu + menu.addItem(setValueItem) + + menu.addItem(NSMenuItem.separator()) + } + + // Copy actions + if dataColumnIndex >= 0 { + let copyCellItem = NSMenuItem( + title: "Copy Cell Value", action: #selector(copyCellValue(_:)), + keyEquivalent: "") + copyCellItem.representedObject = dataColumnIndex + copyCellItem.target = self + menu.addItem(copyCellItem) + } + + let copyItem = NSMenuItem( + title: "Copy", action: #selector(copySelectedOrCurrentRow), keyEquivalent: "c") + copyItem.keyEquivalentModifierMask = .command + copyItem.target = self + menu.addItem(copyItem) + + if coordinator.isEditable { + menu.addItem(NSMenuItem.separator()) + + let duplicateItem = NSMenuItem( + title: "Duplicate", action: #selector(duplicateRow), keyEquivalent: "d") + duplicateItem.keyEquivalentModifierMask = .command + duplicateItem.target = self + menu.addItem(duplicateItem) + + let deleteItem = NSMenuItem( + title: "Delete", action: #selector(deleteRow), keyEquivalent: String(Character(UnicodeScalar(NSBackspaceCharacter)!))) + deleteItem.keyEquivalentModifierMask = [] + deleteItem.target = self + menu.addItem(deleteItem) + } + } + + return menu + } + + @objc private func deleteRow() { + coordinator?.deleteRow(at: rowIndex) + } + + @objc private func duplicateRow() { + NotificationCenter.default.post(name: .duplicateRow, object: nil) + } + + @objc private func undoDeleteRow() { + coordinator?.undoDeleteRow(at: rowIndex) + } + + @objc private func undoInsertRow() { + coordinator?.undoInsertRow(at: rowIndex) + } + + @objc private func copyRow() { + coordinator?.copyRows(at: [rowIndex]) + } + + @objc private func copySelectedRows() { + guard let selectedIndices = coordinator?.selectedRowIndices else { return } + coordinator?.copyRows(at: selectedIndices) + } + + @objc private func copySelectedOrCurrentRow() { + guard let coordinator = coordinator else { return } + if !coordinator.selectedRowIndices.isEmpty { + coordinator.copyRows(at: coordinator.selectedRowIndices) + } else { + coordinator.copyRows(at: [rowIndex]) + } + } + + @objc private func copyCellValue(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + coordinator?.copyCellValue(at: rowIndex, columnIndex: columnIndex) + } + + @objc private func setNullValue(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + coordinator?.setCellValueAtColumn(nil, at: rowIndex, columnIndex: columnIndex) + } + + @objc private func setEmptyValue(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + coordinator?.setCellValueAtColumn("", at: rowIndex, columnIndex: columnIndex) + } + + @objc private func setDefaultValue(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + coordinator?.setCellValueAtColumn("__DEFAULT__", at: rowIndex, columnIndex: columnIndex) + } +}