Skip to content

Commit 81cf9eb

Browse files
authored
refactor(coordinator): delete dead AppCommands subjects, PasteboardActionRouter, and unused selectors (#1340)
1 parent 3a27325 commit 81cf9eb

7 files changed

Lines changed: 12 additions & 478 deletions

File tree

TablePro/Core/Events/AppCommands.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ import Foundation
1010
final class AppCommands {
1111
static let shared = AppCommands()
1212

13-
// MARK: - Row Commands
14-
15-
let deleteSelectedRows = PassthroughSubject<Void, Never>()
16-
let addNewRow = PassthroughSubject<Void, Never>()
17-
let duplicateRow = PassthroughSubject<Void, Never>()
18-
let copySelectedRows = PassthroughSubject<Void, Never>()
19-
let pasteRows = PassthroughSubject<Void, Never>()
20-
2113
// MARK: - Refresh
2214

2315
let refreshData = PassthroughSubject<UUID?, Never>()

TablePro/Core/KeyboardHandling/PasteboardActionRouter.swift

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 1 addition & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,243 +1,7 @@
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-
491
import AppKit
502

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-
///
603
@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
864
@objc optional func undo(_ sender: Any?)
87-
88-
/// Redo last undone action
89-
/// - Standard AppKit selector for Cmd+Shift+Z
905
@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?)
1627
}
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-
*/

TablePro/TableProApp.swift

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
// Created by Ngo Quoc Dat on 16/12/25.
66
//
77

8-
import CodeEditTextView
98
import Combine
109
import Observation
1110
import os
@@ -37,26 +36,19 @@ struct PasteboardCommands: Commands {
3736
.optionalKeyboardShortcut(shortcut(for: .cut))
3837

3938
Button("Copy") {
40-
let action = PasteboardActionRouter.resolveCopyAction(
41-
firstResponder: NSApp.keyWindow?.firstResponder,
42-
hasRowSelection: actions?.hasRowSelection ?? false,
43-
hasTableSelection: actions?.hasTableSelection ?? false
44-
)
45-
switch action {
46-
case .textCopy:
47-
NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil)
48-
case .copyRows:
49-
if !NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil) {
50-
actions?.copySelectedRows()
51-
}
52-
case .copyTableNames:
39+
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil) {
40+
return
41+
}
42+
if actions?.hasRowSelection == true {
43+
actions?.copySelectedRows()
44+
} else if actions?.hasTableSelection == true {
5345
actions?.copyTableNames()
5446
}
5547
}
5648
.optionalKeyboardShortcut(shortcut(for: .copy))
5749

5850
Button("Copy Rows") {
59-
if !NSApp.sendAction(#selector(KeyHandlingTableView.copyRowsAsTSV(_:)), to: nil, from: nil) {
51+
if !NSApp.sendAction(#selector(TableProResponderActions.copyRowsAsTSV(_:)), to: nil, from: nil) {
6052
actions?.copySelectedRows()
6153
}
6254
}
@@ -76,14 +68,10 @@ struct PasteboardCommands: Commands {
7668
.disabled(!(actions?.hasRowSelection ?? false))
7769

7870
Button("Paste") {
79-
let action = PasteboardActionRouter.resolvePasteAction(
80-
firstResponder: NSApp.keyWindow?.firstResponder,
81-
isCurrentTabEditable: actions?.isCurrentTabEditable ?? false
82-
)
83-
switch action {
84-
case .textPaste:
85-
NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil)
86-
case .pasteRows:
71+
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil) {
72+
return
73+
}
74+
if actions?.isCurrentTabEditable == true {
8775
actions?.pasteRows()
8876
}
8977
}

TablePro/Views/Main/Child/DataTabGridDelegate.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@ final class DataTabGridDelegate: DataGridViewDelegate {
6969
AppCommands.shared.exportQueryResults.send(())
7070
}
7171

72-
func dataGridUndo() {}
73-
74-
func dataGridRedo() {}
75-
7672
func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo) {
7773
coordinator?.navigateToFKReference(value: value, fkInfo: fkInfo)
7874
}

0 commit comments

Comments
 (0)