diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index 14195846c..fc7168491 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -31,7 +31,6 @@ struct ContentView: View { @State private var windowTitle: String @Environment(\.openWindow) private var openWindow - @Environment(AppState.self) private var appState private let storage = ConnectionStorage.shared diff --git a/TablePro/Models/Connection/ConnectionToolbarState.swift b/TablePro/Models/Connection/ConnectionToolbarState.swift index ecfd8ccb3..c7707b771 100644 --- a/TablePro/Models/Connection/ConnectionToolbarState.swift +++ b/TablePro/Models/Connection/ConnectionToolbarState.swift @@ -183,6 +183,15 @@ final class ConnectionToolbarState { /// Whether there are pending data grid changes (for SQL preview button) var hasDataPendingChanges: Bool = false + /// Whether the structure view has pending schema changes + var hasStructureChanges: Bool = false + + /// Whether the current editor has non-empty query text + var hasQueryText: Bool = false + + /// Whether the history panel is visible + var isHistoryPanelVisible: Bool = false + /// Whether the SQL review popover is showing var showSQLReviewPopover: Bool = false diff --git a/TablePro/TableProApp.swift b/TablePro/TableProApp.swift index f24752dfb..890379aef 100644 --- a/TablePro/TableProApp.swift +++ b/TablePro/TableProApp.swift @@ -36,7 +36,6 @@ final class AppState { /// Custom Commands struct for pasteboard operations struct PasteboardCommands: Commands { - var appState: AppState var settingsManager: AppSettingsManager @FocusedValue(\.commandActions) var actions: MainContentCommandActions? @@ -55,8 +54,8 @@ struct PasteboardCommands: Commands { Button("Copy") { let action = PasteboardActionRouter.resolveCopyAction( firstResponder: NSApp.keyWindow?.firstResponder, - hasRowSelection: appState.hasRowSelection, - hasTableSelection: appState.hasTableSelection + hasRowSelection: actions?.hasRowSelection ?? false, + hasTableSelection: actions?.hasTableSelection ?? false ) switch action { case .textCopy: @@ -73,18 +72,18 @@ struct PasteboardCommands: Commands { actions?.copySelectedRowsWithHeaders() } .optionalKeyboardShortcut(shortcut(for: .copyWithHeaders)) - .disabled(!appState.hasRowSelection) + .disabled(!(actions?.hasRowSelection ?? false)) Button("Copy as JSON") { actions?.copySelectedRowsAsJson() } .optionalKeyboardShortcut(shortcut(for: .copyAsJson)) - .disabled(!appState.hasRowSelection) + .disabled(!(actions?.hasRowSelection ?? false)) Button("Paste") { let action = PasteboardActionRouter.resolvePasteAction( firstResponder: NSApp.keyWindow?.firstResponder, - isCurrentTabEditable: appState.isCurrentTabEditable + isCurrentTabEditable: actions?.isCurrentTabEditable ?? false ) switch action { case .textPaste: @@ -99,7 +98,7 @@ struct PasteboardCommands: Commands { actions?.deleteSelectedRows() } .optionalKeyboardShortcut(shortcut(for: .delete)) - .disabled(!appState.isCurrentTabEditable && !appState.hasTableSelection) + .disabled(!(actions?.isCurrentTabEditable ?? false) && !(actions?.hasTableSelection ?? false)) Divider() @@ -122,7 +121,6 @@ struct PasteboardCommands: Commands { /// All menu commands extracted into a separate Commands struct so that AppState /// changes only re-evaluate the menu items — NOT the Scene body / WindowGroups. struct AppMenuCommands: Commands { - var appState: AppState var settingsManager: AppSettingsManager var updaterBridge: UpdaterBridge @FocusedValue(\.commandActions) var actions: MainContentCommandActions? @@ -169,36 +167,36 @@ struct AppMenuCommands: Commands { actions?.newTab() } .optionalKeyboardShortcut(shortcut(for: .newTab)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("New View...") { actions?.createView() } - .disabled(!appState.isConnected || appState.isReadOnly) + .disabled(!(actions?.isConnected ?? false) || actions?.isReadOnly ?? false) Button("Open Database...") { actions?.openDatabaseSwitcher() } .optionalKeyboardShortcut(shortcut(for: .openDatabase)) - .disabled(!appState.isConnected || !appState.supportsDatabaseSwitching) + .disabled(!(actions?.isConnected ?? false) || !(actions?.supportsDatabaseSwitching ?? false)) Button(String(localized: "Open File...")) { actions?.openSQLFile() } .optionalKeyboardShortcut(shortcut(for: .openFile)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Switch Connection...") { NotificationCenter.default.post(name: .openConnectionSwitcher, object: nil) } .optionalKeyboardShortcut(shortcut(for: .switchConnection)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Quick Switcher...") { actions?.openQuickSwitcher() } .optionalKeyboardShortcut(shortcut(for: .quickSwitcher)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Divider() @@ -206,25 +204,25 @@ struct AppMenuCommands: Commands { actions?.saveChanges() } .optionalKeyboardShortcut(shortcut(for: .saveChanges)) - .disabled(!appState.isConnected || appState.isReadOnly) + .disabled(!(actions?.isConnected ?? false) || actions?.isReadOnly ?? false) Button(String(localized: "Save As...")) { actions?.saveFileAs() } .optionalKeyboardShortcut(shortcut(for: .saveAs)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button { actions?.previewSQL() } label: { - if let dbType = appState.currentDatabaseType { + if let dbType = actions?.currentDatabaseType { Text(String(format: String(localized: "Preview %@"), PluginManager.shared.queryLanguageName(for: dbType))) } else { Text("Preview SQL") } } .optionalKeyboardShortcut(shortcut(for: .previewSQL)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Close Tab") { if let actions { @@ -241,13 +239,13 @@ struct AppMenuCommands: Commands { NotificationCenter.default.post(name: .refreshData, object: nil) } .optionalKeyboardShortcut(shortcut(for: .refresh)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Explain Query") { actions?.explainQuery() } .optionalKeyboardShortcut(shortcut(for: .explainQuery)) - .disabled(!appState.isConnected || !appState.hasQueryText) + .disabled(!(actions?.isConnected ?? false) || !(actions?.hasQueryText ?? false)) Divider() @@ -265,19 +263,19 @@ struct AppMenuCommands: Commands { actions?.exportTables() } .optionalKeyboardShortcut(shortcut(for: .export)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Export Results...") { actions?.exportQueryResults() } - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) - if appState.currentDatabaseType.map({ PluginManager.shared.supportsImport(for: $0) }) ?? true { + if actions.map({ PluginManager.shared.supportsImport(for: $0.currentDatabaseType) }) ?? true { Button("Import...") { actions?.importTables() } .optionalKeyboardShortcut(shortcut(for: .importData)) - .disabled(!appState.isConnected || appState.isReadOnly) + .disabled(!(actions?.isConnected ?? false) || actions?.isReadOnly ?? false) } } @@ -312,7 +310,7 @@ struct AppMenuCommands: Commands { } // Edit menu - pasteboard commands with FocusedValue support - PasteboardCommands(appState: appState, settingsManager: settingsManager) + PasteboardCommands(settingsManager: settingsManager) // Edit menu - row operations (after pasteboard) CommandGroup(after: .pasteboard) { @@ -322,13 +320,13 @@ struct AppMenuCommands: Commands { actions?.addNewRow() } .optionalKeyboardShortcut(shortcut(for: .addRow)) - .disabled(!appState.isCurrentTabEditable || appState.isReadOnly) + .disabled(!(actions?.isCurrentTabEditable ?? false) || actions?.isReadOnly ?? false) Button("Duplicate Row") { actions?.duplicateRow() } .optionalKeyboardShortcut(shortcut(for: .duplicateRow)) - .disabled(!appState.isCurrentTabEditable || appState.isReadOnly) + .disabled(!(actions?.isCurrentTabEditable ?? false) || actions?.isReadOnly ?? false) Divider() @@ -337,7 +335,7 @@ struct AppMenuCommands: Commands { actions?.truncateTables() } .optionalKeyboardShortcut(shortcut(for: .truncateTable)) - .disabled(!appState.hasTableSelection || appState.isReadOnly) + .disabled(!(actions?.hasTableSelection ?? false) || actions?.isReadOnly ?? false) } // View menu @@ -346,13 +344,13 @@ struct AppMenuCommands: Commands { NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .toggleTableBrowser)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Toggle Inspector") { actions?.toggleRightSidebar() } .optionalKeyboardShortcut(shortcut(for: .toggleInspector)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Divider() @@ -360,13 +358,13 @@ struct AppMenuCommands: Commands { actions?.toggleFilterPanel() } .optionalKeyboardShortcut(shortcut(for: .toggleFilters)) - .disabled(!appState.isConnected || !appState.isTableTab) + .disabled(!(actions?.isConnected ?? false) || !(actions?.isTableTab ?? false)) Button("Toggle History") { actions?.toggleHistoryPanel() } .optionalKeyboardShortcut(shortcut(for: .toggleHistory)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Divider() @@ -374,25 +372,25 @@ struct AppMenuCommands: Commands { actions?.toggleResults() } .optionalKeyboardShortcut(shortcut(for: .toggleResults)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Previous Result") { actions?.previousResultTab() } .optionalKeyboardShortcut(shortcut(for: .previousResultTab)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Next Result") { actions?.nextResultTab() } .optionalKeyboardShortcut(shortcut(for: .nextResultTab)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Button("Close Result Tab") { actions?.closeResultTab() } .optionalKeyboardShortcut(shortcut(for: .closeResultTab)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) Divider() @@ -418,7 +416,7 @@ struct AppMenuCommands: Commands { KeyEquivalent(Character(String(number))), modifiers: .command ) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) } Divider() @@ -428,28 +426,28 @@ struct AppMenuCommands: Commands { NSApp.sendAction(#selector(NSWindow.selectPreviousTab(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .showPreviousTabBrackets)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) // Next tab (Cmd+Shift+]) — delegate to native macOS tab switching Button("Show Next Tab") { NSApp.sendAction(#selector(NSWindow.selectNextTab(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .showNextTabBrackets)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) // Previous tab (Cmd+Option+Left) Button("Previous Tab") { NSApp.sendAction(#selector(NSWindow.selectPreviousTab(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .previousTabArrows)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) // Next tab (Cmd+Option+Right) Button("Next Tab") { NSApp.sendAction(#selector(NSWindow.selectNextTab(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .nextTabArrows)) - .disabled(!appState.isConnected) + .disabled(!(actions?.isConnected ?? false)) } // Help menu @@ -514,7 +512,6 @@ struct TableProApp: App { // Each native window-tab gets its own ContentView with independent state. WindowGroup(id: "main", for: EditorTabPayload.self) { $payload in ContentView(payload: payload) - .environment(AppState.shared) .background(OpenWindowHandler()) } .windowStyle(.automatic) @@ -529,7 +526,6 @@ struct TableProApp: App { .commands { AppMenuCommands( - appState: AppState.shared, settingsManager: AppSettingsManager.shared, updaterBridge: updaterBridge ) diff --git a/TablePro/Views/Editor/QueryEditorView.swift b/TablePro/Views/Editor/QueryEditorView.swift index 98be84613..b360c3cc7 100644 --- a/TablePro/Views/Editor/QueryEditorView.swift +++ b/TablePro/Views/Editor/QueryEditorView.swift @@ -14,7 +14,6 @@ import TableProPluginKit struct QueryEditorView: View { private static let logger = Logger(subsystem: "com.TablePro", category: "QueryEditorView") - @Environment(AppState.self) private var appState @Binding var queryText: String @Binding var cursorPositions: [CursorPosition] @@ -33,7 +32,7 @@ struct QueryEditorView: View { @State private var vimMode: VimMode = .normal var body: some View { - let hasQuery = appState.hasQueryText + let hasQuery = !queryText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty VStack(alignment: .leading, spacing: 0) { // Editor header with toolbar (above editor, higher z-index) diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 6b0bf54d6..51cbde30d 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -77,7 +77,6 @@ struct MainEditorContentView: View { // MARK: - Environment - @Environment(AppState.self) private var appState /// Returns the cached AnyChangeManager, creating it on first access. private var currentChangeManager: AnyChangeManager { @@ -92,7 +91,7 @@ struct MainEditorContentView: View { // MARK: - Body var body: some View { - let isHistoryVisible = appState.isHistoryPanelVisible + let isHistoryVisible = coordinator.toolbarState.isHistoryPanelVisible VStack(spacing: 0) { // Native macOS window tabs replace the custom tab bar. @@ -237,10 +236,10 @@ struct MainEditorContentView: View { private func updateHasQueryText() { if let tab = tabManager.selectedTab, tab.tabType == .query { - appState.hasQueryText = !tab.query.trimmingCharacters(in: .whitespacesAndNewlines) + coordinator.toolbarState.hasQueryText = !tab.query.trimmingCharacters(in: .whitespacesAndNewlines) .isEmpty } else { - appState.hasQueryText = false + coordinator.toolbarState.hasQueryText = false } } diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 82c6561b3..4352ac629 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -12,6 +12,7 @@ import Foundation import Observation import os import SwiftUI +import TableProPluginKit /// Provides command actions for MainContentView, accessible via @FocusedValue @MainActor @@ -263,6 +264,48 @@ final class MainContentCommandActions { } } + // MARK: - Per-Window State (replaces AppState.shared for menu enablement) + + var isConnected: Bool { coordinator != nil } + + var safeModeLevel: SafeModeLevel { connection.safeModeLevel } + + var isReadOnly: Bool { safeModeLevel.blocksAllWrites } + + var editorLanguage: EditorLanguage { + PluginManager.shared.editorLanguage(for: connection.type) + } + + var currentDatabaseType: DatabaseType { connection.type } + + var supportsDatabaseSwitching: Bool { + PluginManager.shared.supportsDatabaseSwitching(for: connection.type) + } + + var isCurrentTabEditable: Bool { + coordinator?.tabManager.selectedTab?.isEditable == true + } + + var isTableTab: Bool { + coordinator?.toolbarState.isTableTab ?? false + } + + var hasRowSelection: Bool { + !selectedRowIndices.wrappedValue.isEmpty + } + + var hasTableSelection: Bool { + !selectedTables.wrappedValue.isEmpty + } + + var hasQueryText: Bool { + !(coordinator?.tabManager.selectedTab?.query.isEmpty ?? true) + } + + var hasStructureChanges: Bool { + coordinator?.toolbarState.hasStructureChanges ?? false + } + // MARK: - Unsaved Changes Check private var hasUnsavedChanges: Bool { @@ -339,9 +382,7 @@ final class MainContentCommandActions { } coordinator?.tabManager.tabs.removeAll() coordinator?.tabManager.selectedTabId = nil - AppState.shared.isCurrentTabEditable = false coordinator?.toolbarState.isTableTab = false - AppState.shared.isTableTab = false } } @@ -545,7 +586,7 @@ final class MainContentCommandActions { // MARK: - UI Operations (Group A — Called Directly) func toggleHistoryPanel() { - AppState.shared.isHistoryPanelVisible.toggle() + coordinator?.toolbarState.isHistoryPanelVisible.toggle() } func toggleRightSidebar() { diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index b5c94519c..be03b9acb 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -61,7 +61,6 @@ struct MainContentView: View { // MARK: - Environment - @Environment(AppState.self) private var appState // MARK: - Initialization @@ -187,7 +186,7 @@ struct MainContentView: View { hasDataChanges: changeManager.hasChanges, pendingTruncates: pendingTruncates, pendingDeletes: pendingDeletes, - hasStructureChanges: appState.hasStructureChanges, + hasStructureChanges: toolbarState.hasStructureChanges, isFileDirty: tabManager.selectedTab?.isFileDirty ?? false ) } @@ -581,7 +580,7 @@ struct MainContentView: View { changeManager.hasChanges || !pendingTruncates.isEmpty || !pendingDeletes.isEmpty - || AppState.shared.hasStructureChanges + || toolbarState.hasStructureChanges let hasFileChanges = tabManager.selectedTab?.isFileDirty ?? false toolbarState.hasDataPendingChanges = hasDataChanges toolbarState.hasPendingChanges = hasDataChanges || hasFileChanges diff --git a/TablePro/Views/Sidebar/SidebarContextMenu.swift b/TablePro/Views/Sidebar/SidebarContextMenu.swift index 6f72bf376..a59024241 100644 --- a/TablePro/Views/Sidebar/SidebarContextMenu.swift +++ b/TablePro/Views/Sidebar/SidebarContextMenu.swift @@ -101,7 +101,7 @@ struct SidebarContextMenu: View { if SidebarContextMenuLogic.importVisible( isView: isView, supportsImport: PluginManager.shared.supportsImport( - for: AppState.shared.currentDatabaseType ?? .mysql + for: coordinator?.connection.type ?? .mysql ) ) { Button("Import...") { diff --git a/TablePro/Views/Sidebar/SidebarView.swift b/TablePro/Views/Sidebar/SidebarView.swift index 6150975b7..8b301837e 100644 --- a/TablePro/Views/Sidebar/SidebarView.swift +++ b/TablePro/Views/Sidebar/SidebarView.swift @@ -235,7 +235,7 @@ struct SidebarView: View { SidebarContextMenu( clickedTable: table, selectedTables: selectedTablesBinding, - isReadOnly: AppState.shared.safeModeLevel.blocksAllWrites, + isReadOnly: coordinator?.safeModeLevel.blocksAllWrites ?? false, onBatchToggleTruncate: { viewModel.batchToggleTruncate() }, onBatchToggleDelete: { viewModel.batchToggleDelete() }, coordinator: coordinator @@ -281,7 +281,7 @@ struct SidebarView: View { SidebarContextMenu( clickedTable: nil, selectedTables: selectedTablesBinding, - isReadOnly: AppState.shared.safeModeLevel.blocksAllWrites, + isReadOnly: coordinator?.safeModeLevel.blocksAllWrites ?? false, onBatchToggleTruncate: { viewModel.batchToggleTruncate() }, onBatchToggleDelete: { viewModel.batchToggleDelete() }, coordinator: coordinator diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index e7c1592cf..5b26418ec 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -74,7 +74,7 @@ struct TableStructureView: View { .onAppear { AppState.shared.isCurrentTabEditable = (selectedTab != .ddl) AppState.shared.hasRowSelection = !selectedRows.isEmpty - AppState.shared.hasStructureChanges = structureChangeManager.hasChanges + coordinator?.toolbarState.hasStructureChanges = structureChangeManager.hasChanges // Wire action handler for direct coordinator calls actionHandler.saveChanges = { @@ -92,11 +92,11 @@ struct TableStructureView: View { .onDisappear { AppState.shared.isCurrentTabEditable = false AppState.shared.hasRowSelection = false - AppState.shared.hasStructureChanges = false + coordinator?.toolbarState.hasStructureChanges = false coordinator?.structureActions = nil } .onChange(of: structureChangeManager.hasChanges) { _, newValue in - AppState.shared.hasStructureChanges = newValue + coordinator?.toolbarState.hasStructureChanges = newValue } .onReceive(NotificationCenter.default.publisher(for: .refreshData), perform: onRefreshData) }