|
1 | | -// |
2 | | -// ResponderChainActions.swift |
3 | | -// TablePro |
4 | | -// |
5 | | -// Documentation protocol listing all responder chain actions used in TablePro. |
6 | | -// This is a reference guide, not implemented by any class directly. |
7 | | -// |
8 | | -// ## Architecture Pattern |
9 | | -// |
10 | | -// TablePro uses three mechanisms for keyboard shortcuts and commands: |
11 | | -// |
12 | | -// 1. **Responder Chain** (Apple Standard): |
13 | | -// - Standard edit actions: copy, paste, undo, delete, cancelOperation (ESC) |
14 | | -// - Context-aware: First responder handles action appropriately |
15 | | -// - Commands send via `NSApp.sendAction(#selector(...), to: nil, from: nil)` |
16 | | -// |
17 | | -// 2. **@FocusedValue** (Menu/Toolbar → single handler): |
18 | | -// - Most menu commands call `MainContentCommandActions` directly |
19 | | -// - Toolbar buttons also use `@FocusedValue` for direct calls |
20 | | -// - Clean method calls, no global event bus |
21 | | -// - Commands are automatically nil (disabled) when no connection is active |
22 | | -// |
23 | | -// 3. **AppCommands** (Multi-listener broadcasts only): |
24 | | -// - `refreshData` (Sidebar + Coordinator + StructureView) |
25 | | -// - Non-menu commands from AppKit views (DataGrid, SidebarView context menus) |
26 | | -// - Typed Combine publishers for broadcasts where multiple views respond |
27 | | -// |
28 | | -// ## Example Flow |
29 | | -// |
30 | | -// ``` |
31 | | -// User presses: Cmd+Delete |
32 | | -// ↓ |
33 | | -// SwiftUI Command: .keyboardShortcut(.delete, modifiers: .command) |
34 | | -// ↓ |
35 | | -// TableProApp: NSApp.sendAction(#selector(delete(_:)), to: nil, from: nil) |
36 | | -// ↓ |
37 | | -// Responder Chain: First Responder (KeyHandlingTableView) |
38 | | -// ↓ |
39 | | -// KeyHandlingTableView: @objc func delete(_ sender: Any?) { ... } |
40 | | -// ``` |
41 | | -// |
42 | | -// ## Reference Files |
43 | | -// - `TableProApp.swift` - SwiftUI Commands that define shortcuts |
44 | | -// - `KeyHandlingTableView.swift` - Data grid keyboard handling |
45 | | -// - `HistoryPanelView.swift` - SwiftUI history panel (uses onDeleteCommand) |
46 | | -// - `EditorTextView.swift` - SQL editor keyboard handling |
47 | | -// |
48 | | - |
49 | 1 | import AppKit |
50 | 2 |
|
51 | | -/// Documentation protocol listing all responder chain actions in TablePro. |
52 | | -/// |
53 | | -/// **IMPORTANT**: This protocol is for documentation only. Do NOT implement it |
54 | | -/// on any classes. Instead, add individual `@objc` methods as needed. |
55 | | -/// |
56 | | -/// Responders should implement: |
57 | | -/// 1. The `@objc` action method (e.g., `@objc func delete(_ sender: Any?)`) |
58 | | -/// 2. Validation via `NSUserInterfaceValidations` or `NSMenuItemValidation` |
59 | | -/// |
60 | 3 | @objc protocol TableProResponderActions { |
61 | | - // MARK: - Standard Edit Menu Actions |
62 | | - |
63 | | - /// Delete the selected items |
64 | | - /// - Standard AppKit selector for Delete/Backspace key |
65 | | - /// - Triggered by: Delete key, Cmd+Delete, or Edit > Delete menu |
66 | | - @objc optional func delete(_ sender: Any?) |
67 | | - |
68 | | - /// Copy selected content to clipboard |
69 | | - /// - Standard AppKit selector for Cmd+C |
70 | | - @objc optional func copy(_ sender: Any?) |
71 | | - |
72 | | - /// Paste clipboard content |
73 | | - /// - Standard AppKit selector for Cmd+V |
74 | | - @objc optional func paste(_ sender: Any?) |
75 | | - |
76 | | - /// Cut selected content to clipboard |
77 | | - /// - Standard AppKit selector for Cmd+X |
78 | | - @objc optional func cut(_ sender: Any?) |
79 | | - |
80 | | - /// Select all items |
81 | | - /// - Standard AppKit selector for Cmd+A |
82 | | - @objc optional func selectAll(_ sender: Any?) |
83 | | - |
84 | | - /// Undo last action |
85 | | - /// - Standard AppKit selector for Cmd+Z |
86 | 4 | @objc optional func undo(_ sender: Any?) |
87 | | - |
88 | | - /// Redo last undone action |
89 | | - /// - Standard AppKit selector for Cmd+Shift+Z |
90 | 5 | @objc optional func redo(_ sender: Any?) |
91 | | - |
92 | | - // MARK: - Standard Navigation Actions |
93 | | - |
94 | | - /// Move selection up |
95 | | - /// - Standard AppKit selector for Up Arrow |
96 | | - @objc optional func moveUp(_ sender: Any?) |
97 | | - |
98 | | - /// Move selection down |
99 | | - /// - Standard AppKit selector for Down Arrow |
100 | | - @objc optional func moveDown(_ sender: Any?) |
101 | | - |
102 | | - /// Move selection left |
103 | | - /// - Standard AppKit selector for Left Arrow |
104 | | - @objc optional func moveLeft(_ sender: Any?) |
105 | | - |
106 | | - /// Move selection right |
107 | | - /// - Standard AppKit selector for Right Arrow |
108 | | - @objc optional func moveRight(_ sender: Any?) |
109 | | - |
110 | | - /// Insert newline (Enter/Return key) |
111 | | - /// - Standard AppKit selector for Return key |
112 | | - @objc optional func insertNewline(_ sender: Any?) |
113 | | - |
114 | | - /// Cancel current operation (ESC key) |
115 | | - /// - Standard AppKit selector for Escape key |
116 | | - /// - Automatically called by `.onExitCommand` in SwiftUI |
117 | | - @objc optional func cancelOperation(_ sender: Any?) |
118 | | - |
119 | | - // MARK: - App-Specific Database Actions |
120 | | - |
121 | | - /// Add a new row to the current table |
122 | | - /// - Custom action for Cmd+N in data grid |
123 | | - @objc optional func addRow(_ sender: Any?) |
124 | | - |
125 | | - /// Duplicate the selected row |
126 | | - /// - Custom action for Cmd+D |
127 | | - @objc optional func duplicateRow(_ sender: Any?) |
128 | | - |
129 | | - /// Save pending changes to database |
130 | | - /// - Custom action for Cmd+S |
131 | | - @objc optional func saveChanges(_ sender: Any?) |
132 | | - |
133 | | - /// Refresh data from database |
134 | | - /// - Custom action for Cmd+R |
135 | | - @objc optional func refreshData(_ sender: Any?) |
136 | | - |
137 | | - /// Execute SQL query |
138 | | - /// - Custom action for Cmd+Enter in editor |
139 | | - @objc optional func executeQuery(_ sender: Any?) |
140 | | - |
141 | | - /// Clear current selection |
142 | | - /// - Custom action for Cmd+Esc |
143 | | - @objc optional func clearSelection(_ sender: Any?) |
144 | | - |
145 | | - // MARK: - View Actions |
146 | | - |
147 | | - /// Toggle table browser visibility |
148 | | - /// - Custom action for Cmd+B |
149 | | - @objc optional func toggleTableBrowser(_ sender: Any?) |
150 | | - |
151 | | - /// Toggle inspector panel |
152 | | - /// - Custom action for Cmd+I |
153 | | - @objc optional func toggleInspector(_ sender: Any?) |
154 | | - |
155 | | - /// Toggle filters panel |
156 | | - /// - Custom action for Cmd+F |
157 | | - @objc optional func toggleFilters(_ sender: Any?) |
158 | | - |
159 | | - /// Toggle query history panel |
160 | | - /// - Custom action for Cmd+H |
161 | | - @objc optional func toggleHistory(_ sender: Any?) |
| 6 | + @objc optional func copyRowsAsTSV(_ sender: Any?) |
162 | 7 | } |
163 | | - |
164 | | -// MARK: - Implementation Guide |
165 | | - |
166 | | -/* |
167 | | - |
168 | | - ## How to Implement Responder Chain Actions |
169 | | - |
170 | | - ### Step 1: Add @objc Method to Your Responder |
171 | | - |
172 | | - ```swift |
173 | | - final class MyTableView: NSTableView { |
174 | | - override var acceptsFirstResponder: Bool { true } |
175 | | - |
176 | | - @objc func delete(_ sender: Any?) { |
177 | | - // Your delete logic here |
178 | | - logger.debug("Deleting selected rows") |
179 | | - } |
180 | | - } |
181 | | - ``` |
182 | | - |
183 | | - ### Step 2: Add Validation (Optional but Recommended) |
184 | | - |
185 | | - ```swift |
186 | | - extension MyTableView: NSUserInterfaceValidations { |
187 | | - func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { |
188 | | - switch item.action { |
189 | | - case #selector(delete(_:)): |
190 | | - // Enable Delete only when rows are selected |
191 | | - return !selectedRowIndexes.isEmpty |
192 | | - default: |
193 | | - return false |
194 | | - } |
195 | | - } |
196 | | - } |
197 | | - ``` |
198 | | - |
199 | | - ### Step 3: Register Command in TableProApp.swift |
200 | | - |
201 | | - ```swift |
202 | | - .commands { |
203 | | - CommandGroup(after: .newItem) { |
204 | | - Button("Delete Row") { |
205 | | - NSApp.sendAction(#selector(TableProResponderActions.delete(_:)), |
206 | | - to: nil, from: nil) |
207 | | - } |
208 | | - .keyboardShortcut(.delete, modifiers: .command) |
209 | | - } |
210 | | - } |
211 | | - ``` |
212 | | - |
213 | | - ### Step 4: Use interpretKeyEvents for Bare Keys (Optional) |
214 | | - |
215 | | - For non-modifier keys (arrows, Return, ESC), use `interpretKeyEvents`: |
216 | | - |
217 | | - ```swift |
218 | | - override func keyDown(with event: NSEvent) { |
219 | | - interpretKeyEvents([event]) |
220 | | - } |
221 | | - |
222 | | - @objc override func moveUp(_ sender: Any?) { |
223 | | - // Custom up arrow handling |
224 | | - } |
225 | | - ``` |
226 | | - |
227 | | - ## Benefits of Responder Chain |
228 | | - |
229 | | - ✅ **Automatic validation** - Menu items enable/disable based on context |
230 | | - ✅ **No manual routing** - macOS finds the right handler automatically |
231 | | - ✅ **Standard behavior** - Users expect Cmd+C/V/Z to work everywhere |
232 | | - ✅ **VoiceOver support** - Accessibility built-in |
233 | | - ✅ **Easy to extend** - Just add @objc methods, no global event bus |
234 | | - |
235 | | - ## Anti-Patterns to Avoid |
236 | | - |
237 | | - ❌ **NotificationCenter for commands** - Bypasses validation, hard to debug |
238 | | - ❌ **Magic keyCode numbers** - Use KeyCode enum instead |
239 | | - ❌ **performKeyEquivalent for bare keys** - Only for Cmd+ shortcuts |
240 | | - ❌ **Custom ESC systems** - Use cancelOperation(_:) selector |
241 | | - ❌ **Manual keyDown switches** - Use interpretKeyEvents + selectors |
242 | | - |
243 | | - */ |
0 commit comments