Skip to content

Commit 90b98a6

Browse files
committed
feat: Improve history list keyboard navigation and bookmark form UX
- Add keyboard shortcuts for history/bookmark list: * Return: Open selected query in new tab * Cmd+C: Copy query to clipboard * Cmd+E: Edit bookmark (bookmarks mode only) * Escape: Clear search/selection * Delete: Delete selected entry - Implement smart selection after deletion: * Automatically select next item after delete * Select previous if last item deleted * Update preview panel immediately - Add keyboard shortcut hints to context menu items - Improve bookmark form (AppKit): * Better spacing (24px margins, 16px row spacing) * Larger dialog (520x360) * Improved placeholder text * Larger fonts (13pt for readability) * Better text insets (8px) - Fix responder chain for standard Edit menu commands Note: Reverted from SwiftUI form due to beep sound issue with keyboard shortcuts in hybrid AppKit/SwiftUI architecture
1 parent 5f44724 commit 90b98a6

8 files changed

Lines changed: 607 additions & 102 deletions

File tree

OpenTable/ContentView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct ContentView: View {
2323

2424
@Environment(\.openWindow) private var openWindow
2525
@Environment(\.dismissWindow) private var dismissWindow
26+
@EnvironmentObject private var appState: AppState
2627

2728
private let storage = ConnectionStorage.shared
2829

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// NSViewController+SwiftUI.swift
3+
// OpenTable
4+
//
5+
// Helper extension to present SwiftUI views from AppKit
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
extension NSViewController {
12+
/// Present a SwiftUI view as a sheet with proper keyboard handling
13+
func presentAsSheet<Content: View>(_ swiftUIView: Content, onSave: (() -> Void)? = nil, onCancel: (() -> Void)? = nil) {
14+
let hostingController = KeyboardHandlingHostingController(rootView: swiftUIView)
15+
hostingController.onSave = onSave
16+
hostingController.onCancel = onCancel ?? { [weak hostingController] in
17+
hostingController?.dismiss(nil)
18+
}
19+
presentAsSheet(hostingController)
20+
}
21+
}
22+
23+
/// Custom NSHostingController that properly handles keyboard events
24+
private class KeyboardHandlingHostingController<Content: View>: NSHostingController<Content> {
25+
26+
var onSave: (() -> Void)?
27+
var onCancel: (() -> Void)?
28+
29+
override func performKeyEquivalent(with event: NSEvent) -> Bool {
30+
// Check for Command modifier
31+
let commandPressed = event.modifierFlags.contains(.command)
32+
33+
// Handle Cmd+Return (Save)
34+
if commandPressed && (event.keyCode == 36 || event.keyCode == 76) {
35+
onSave?()
36+
return true
37+
}
38+
39+
// Let super handle other events
40+
return super.performKeyEquivalent(with: event)
41+
}
42+
43+
override func cancelOperation(_ sender: Any?) {
44+
// Handle Escape key
45+
onCancel?()
46+
}
47+
48+
override func keyDown(with event: NSEvent) {
49+
// Check for Escape key without modifiers
50+
if event.keyCode == 53 && event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty {
51+
onCancel?()
52+
return
53+
}
54+
55+
// Pass other keys to super
56+
super.keyDown(with: event)
57+
}
58+
}

OpenTable/OpenTableApp.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class AppState: ObservableObject {
1616
@Published var isCurrentTabEditable: Bool = false // True when current tab is an editable table
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
19+
@Published var isHistoryPanelVisible: Bool = false // Global history panel visibility
1920
}
2021

2122
// MARK: - App
@@ -136,6 +137,20 @@ struct OpenTableApp: App {
136137
.keyboardShortcut("v", modifiers: .command)
137138

138139
Button("Delete") {
140+
// Check if first responder is the history panel's table view
141+
// History panel uses responder chain for delete actions
142+
// Data grid uses notifications for batched undo support
143+
if let firstResponder = NSApp.keyWindow?.firstResponder {
144+
// Check class name to identify HistoryTableView
145+
let className = String(describing: type(of: firstResponder))
146+
if className.contains("HistoryTableView") {
147+
// Let history panel handle via responder chain
148+
NSApp.sendAction(#selector(NSText.delete(_:)), to: nil, from: nil)
149+
return
150+
}
151+
}
152+
153+
// For data grid and other views, use notification for batched undo
139154
NotificationCenter.default.post(name: .deleteSelectedRows, object: nil)
140155
}
141156
.keyboardShortcut(.delete, modifiers: .command)

OpenTable/Views/Editor/BookmarkEditorController.swift

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ final class BookmarkEditorController: NSViewController {
4343
// MARK: - Lifecycle
4444

4545
override func loadView() {
46-
// Create main container
47-
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 450, height: 380))
46+
// Create main container with better spacing
47+
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 520, height: 360))
4848
contentView.translatesAutoresizingMaskIntoConstraints = false
4949

5050
// Build form using NSGridView (native macOS form layout)
@@ -57,12 +57,12 @@ final class BookmarkEditorController: NSViewController {
5757
contentView.addSubview(buttonStack)
5858

5959
NSLayoutConstraint.activate([
60-
gridView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
61-
gridView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
62-
gridView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
60+
gridView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24),
61+
gridView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
62+
gridView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
6363

64-
buttonStack.topAnchor.constraint(equalTo: gridView.bottomAnchor, constant: 20),
65-
buttonStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
64+
buttonStack.topAnchor.constraint(equalTo: gridView.bottomAnchor, constant: 24),
65+
buttonStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
6666
buttonStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
6767
])
6868

@@ -73,7 +73,7 @@ final class BookmarkEditorController: NSViewController {
7373
super.viewDidLoad()
7474
title = isEditing ? "Edit Bookmark" : "New Bookmark"
7575
populateFields()
76-
preferredContentSize = NSSize(width: 450, height: 400)
76+
preferredContentSize = NSSize(width: 520, height: 380)
7777

7878
// Add accessibility identifiers
7979
nameField.setAccessibilityIdentifier("bookmarkNameField")
@@ -91,23 +91,29 @@ final class BookmarkEditorController: NSViewController {
9191
// Name row
9292
let nameLabel = NSTextField(labelWithString: "Name:")
9393
nameLabel.alignment = .right
94+
nameLabel.font = .systemFont(ofSize: 13)
9495
nameField = NSTextField()
9596
nameField.placeholderString = "Bookmark name"
97+
nameField.font = .systemFont(ofSize: 13)
9698

9799
// Query row
98100
let queryLabel = NSTextField(labelWithString: "Query:")
99101
queryLabel.alignment = .right
102+
queryLabel.font = .systemFont(ofSize: 13)
100103
let queryScrollView = buildQueryScrollView()
101104

102105
// Tags row
103106
let tagsLabel = NSTextField(labelWithString: "Tags:")
104107
tagsLabel.alignment = .right
108+
tagsLabel.font = .systemFont(ofSize: 13)
105109
tagsField = NSTextField()
106-
tagsField.placeholderString = "Optional, comma-separated"
110+
tagsField.placeholderString = "e.g., reports, analytics, daily"
111+
tagsField.font = .systemFont(ofSize: 13)
107112

108113
// Notes row
109114
let notesLabel = NSTextField(labelWithString: "Notes:")
110115
notesLabel.alignment = .right
116+
notesLabel.font = .systemFont(ofSize: 13)
111117
let notesScrollView = buildNotesScrollView()
112118

113119
// Create grid
@@ -119,24 +125,24 @@ final class BookmarkEditorController: NSViewController {
119125
])
120126

121127
gridView.translatesAutoresizingMaskIntoConstraints = false
122-
gridView.columnSpacing = 10
123-
gridView.rowSpacing = 14
128+
gridView.columnSpacing = 12
129+
gridView.rowSpacing = 16
124130

125-
// Configure column widths (slightly wider for label column)
126-
gridView.column(at: 0).width = 65
131+
// Configure column widths - slightly wider label column
132+
gridView.column(at: 0).width = 70
127133
gridView.column(at: 0).xPlacement = .trailing
128134

129-
// Configure row alignments
135+
// Configure row alignments with better padding
130136
gridView.row(at: 0).topPadding = 0
131-
gridView.row(at: 1).topPadding = 6
132-
gridView.row(at: 2).topPadding = 6
133-
gridView.row(at: 3).topPadding = 6
137+
gridView.row(at: 1).topPadding = 8
138+
gridView.row(at: 2).topPadding = 8
139+
gridView.row(at: 3).topPadding = 8
134140

135141
// Align labels to top for multi-line fields
136142
gridView.cell(atColumnIndex: 0, rowIndex: 1).yPlacement = .top
137143
gridView.cell(atColumnIndex: 0, rowIndex: 3).yPlacement = .top
138144

139-
// Set heights for scroll views with better proportions
145+
// Set heights for scroll views
140146
queryScrollView.heightAnchor.constraint(equalToConstant: 90).isActive = true
141147
notesScrollView.heightAnchor.constraint(equalToConstant: 70).isActive = true
142148

@@ -156,7 +162,7 @@ final class BookmarkEditorController: NSViewController {
156162
queryTextView.isSelectable = true
157163
queryTextView.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
158164
queryTextView.string = query
159-
queryTextView.textContainerInset = NSSize(width: 6, height: 6)
165+
queryTextView.textContainerInset = NSSize(width: 8, height: 8)
160166
queryTextView.isVerticallyResizable = true
161167
queryTextView.isHorizontallyResizable = false
162168
queryTextView.autoresizingMask = [.width]
@@ -180,7 +186,7 @@ final class BookmarkEditorController: NSViewController {
180186
notesTextView = NSTextView()
181187
notesTextView.isRichText = false
182188
notesTextView.font = .systemFont(ofSize: 13)
183-
notesTextView.textContainerInset = NSSize(width: 6, height: 6)
189+
notesTextView.textContainerInset = NSSize(width: 8, height: 8)
184190
notesTextView.isVerticallyResizable = true
185191
notesTextView.isHorizontallyResizable = false
186192
notesTextView.autoresizingMask = [.width]
@@ -194,11 +200,11 @@ final class BookmarkEditorController: NSViewController {
194200
private func buildButtonStack() -> NSStackView {
195201
let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancelAction))
196202
cancelButton.bezelStyle = .rounded
197-
cancelButton.keyEquivalent = "\u{1b}"
203+
cancelButton.keyEquivalent = "\u{1b}" // Escape
198204

199205
let saveButton = NSButton(title: isEditing ? "Save" : "Save Bookmark", target: self, action: #selector(saveAction))
200206
saveButton.bezelStyle = .rounded
201-
saveButton.keyEquivalent = "\r"
207+
saveButton.keyEquivalent = "\r" // Return
202208

203209
let stackView = NSStackView(views: [cancelButton, saveButton])
204210
stackView.orientation = .horizontal

0 commit comments

Comments
 (0)