@@ -17,6 +17,81 @@ final class AppState: ObservableObject {
1717 @Published var hasRowSelection : Bool = false // True when rows are selected in data grid
1818 @Published var hasTableSelection : Bool = false // True when tables are selected in sidebar
1919 @Published var isHistoryPanelVisible : Bool = false // Global history panel visibility
20+ @Published var isSheetPresented : Bool = false // True when any modal sheet is open (blocks ESC key handling)
21+ }
22+
23+ // MARK: - Pasteboard Commands with FocusedValue Support
24+
25+ /// Custom Commands struct to properly access FocusedValue for disabling ESC when sheet is open
26+ struct PasteboardCommands : Commands {
27+ @ObservedObject var appState : AppState
28+ @FocusedValue ( \. isDatabaseSwitcherOpen) var isDatabaseSwitcherOpen : Bool ?
29+
30+ var body : some Commands {
31+ CommandGroup ( replacing: . pasteboard) {
32+ Button ( " Cut " ) {
33+ NSApp . sendAction ( #selector( NSText . cut ( _: ) ) , to: nil , from: nil )
34+ }
35+ . keyboardShortcut ( " x " , modifiers: . command)
36+
37+ Button ( " Copy " ) {
38+ // Check if user is editing text in a cell (firstResponder is NSTextView field editor)
39+ if let firstResponder = NSApp . keyWindow? . firstResponder,
40+ firstResponder is NSTextView {
41+ // User is editing text - let standard copy handle selected text
42+ NSApp . sendAction ( #selector( NSText . copy ( _: ) ) , to: nil , from: nil )
43+ } else if appState. hasRowSelection {
44+ // Copy entire rows when rows are selected
45+ NotificationCenter . default. post ( name: . copySelectedRows, object: nil )
46+ } else if appState. hasTableSelection {
47+ // Copy table names when tables are selected
48+ NotificationCenter . default. post ( name: . copyTableNames, object: nil )
49+ } else {
50+ // Fallback to standard copy
51+ NSApp . sendAction ( #selector( NSText . copy ( _: ) ) , to: nil , from: nil )
52+ }
53+ }
54+ . keyboardShortcut ( " c " , modifiers: . command)
55+
56+ Button ( " Paste " ) {
57+ NSApp . sendAction ( #selector( NSText . paste ( _: ) ) , to: nil , from: nil )
58+ }
59+ . keyboardShortcut ( " v " , modifiers: . command)
60+
61+ Button ( " Delete " ) {
62+ // Check if first responder is the history panel's table view
63+ // History panel uses responder chain for delete actions
64+ // Data grid uses notifications for batched undo support
65+ if let firstResponder = NSApp . keyWindow? . firstResponder {
66+ // Check class name to identify HistoryTableView
67+ let className = String ( describing: type ( of: firstResponder) )
68+ if className. contains ( " HistoryTableView " ) {
69+ // Let history panel handle via responder chain
70+ NSApp . sendAction ( #selector( NSText . delete ( _: ) ) , to: nil , from: nil )
71+ return
72+ }
73+ }
74+
75+ // For data grid and other views, use notification for batched undo
76+ NotificationCenter . default. post ( name: . deleteSelectedRows, object: nil )
77+ }
78+ . keyboardShortcut ( . delete, modifiers: . command)
79+ . disabled ( !appState. isCurrentTabEditable && !appState. hasTableSelection)
80+
81+ Divider ( )
82+
83+ Button ( " Select All " ) {
84+ NSApp . sendAction ( #selector( NSText . selectAll ( _: ) ) , to: nil , from: nil )
85+ }
86+ . keyboardShortcut ( " a " , modifiers: . command)
87+
88+ Button ( " Clear Selection " ) {
89+ NotificationCenter . default. post ( name: . clearSelection, object: nil )
90+ }
91+ . keyboardShortcut ( . escape, modifiers: [ ] )
92+ . disabled ( isDatabaseSwitcherOpen == true )
93+ }
94+ }
2095}
2196
2297// MARK: - App
@@ -69,6 +144,12 @@ struct OpenTableApp: App {
69144 . keyboardShortcut ( " t " , modifiers: . command)
70145 . disabled ( !appState. isConnected)
71146
147+ Button ( " Open Database... " ) {
148+ NotificationCenter . default. post ( name: . openDatabaseSwitcher, object: nil )
149+ }
150+ . keyboardShortcut ( " k " , modifiers: . command)
151+ . disabled ( !appState. isConnected)
152+
72153 Divider ( )
73154
74155 Button ( " Save Changes " ) {
@@ -129,70 +210,9 @@ struct OpenTableApp: App {
129210 . keyboardShortcut ( " z " , modifiers: [ . command, . shift] )
130211 }
131212
132- // Edit menu - replace pasteboard to add our Delete with shortcut
133- CommandGroup ( replacing: . pasteboard) {
134- Button ( " Cut " ) {
135- NSApp . sendAction ( #selector( NSText . cut ( _: ) ) , to: nil , from: nil )
136- }
137- . keyboardShortcut ( " x " , modifiers: . command)
138-
139- Button ( " Copy " ) {
140- // Check if user is editing text in a cell (firstResponder is NSTextView field editor)
141- if let firstResponder = NSApp . keyWindow? . firstResponder,
142- firstResponder is NSTextView {
143- // User is editing text - let standard copy handle selected text
144- NSApp . sendAction ( #selector( NSText . copy ( _: ) ) , to: nil , from: nil )
145- } else if appState. hasRowSelection {
146- // Copy entire rows when rows are selected
147- NotificationCenter . default. post ( name: . copySelectedRows, object: nil )
148- } else if appState. hasTableSelection {
149- // Copy table names when tables are selected
150- NotificationCenter . default. post ( name: . copyTableNames, object: nil )
151- } else {
152- // Fallback to standard copy
153- NSApp . sendAction ( #selector( NSText . copy ( _: ) ) , to: nil , from: nil )
154- }
155- }
156- . keyboardShortcut ( " c " , modifiers: . command)
157-
158- Button ( " Paste " ) {
159- NSApp . sendAction ( #selector( NSText . paste ( _: ) ) , to: nil , from: nil )
160- }
161- . keyboardShortcut ( " v " , modifiers: . command)
162-
163- Button ( " Delete " ) {
164- // Check if first responder is the history panel's table view
165- // History panel uses responder chain for delete actions
166- // Data grid uses notifications for batched undo support
167- if let firstResponder = NSApp . keyWindow? . firstResponder {
168- // Check class name to identify HistoryTableView
169- let className = String ( describing: type ( of: firstResponder) )
170- if className. contains ( " HistoryTableView " ) {
171- // Let history panel handle via responder chain
172- NSApp . sendAction ( #selector( NSText . delete ( _: ) ) , to: nil , from: nil )
173- return
174- }
175- }
176-
177- // For data grid and other views, use notification for batched undo
178- NotificationCenter . default. post ( name: . deleteSelectedRows, object: nil )
179- }
180- . keyboardShortcut ( . delete, modifiers: . command)
181- . disabled ( !appState. isCurrentTabEditable && !appState. hasTableSelection)
182-
183- Divider ( )
184-
185- Button ( " Select All " ) {
186- NSApp . sendAction ( #selector( NSText . selectAll ( _: ) ) , to: nil , from: nil )
187- }
188- . keyboardShortcut ( " a " , modifiers: . command)
189-
190- Button ( " Clear Selection " ) {
191- NotificationCenter . default. post ( name: . clearSelection, object: nil )
192- }
193- . keyboardShortcut ( . escape, modifiers: [ ] )
194- }
195-
213+ // Edit menu - pasteboard commands with FocusedValue support
214+ PasteboardCommands ( appState: appState)
215+
196216 // Edit menu - row operations (after pasteboard)
197217 CommandGroup ( after: . pasteboard) {
198218 Divider ( )
@@ -286,7 +306,10 @@ extension Notification.Name {
286306
287307 // History panel notifications
288308 static let toggleHistoryPanel = Notification . Name ( " toggleHistoryPanel " )
289-
309+
310+ // Database switcher notifications
311+ static let openDatabaseSwitcher = Notification . Name ( " openDatabaseSwitcher " )
312+
290313 // Window lifecycle notifications
291314 static let mainWindowWillClose = Notification . Name ( " mainWindowWillClose " )
292315}
0 commit comments