Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Right-click a column header to copy all its values from the loaded rows (#1325)
- Copy as submenu on the row context menu now offers CSV, CSV with Headers, Markdown table, and IN Clause for SQL `WHERE id IN (...)` lookups (#1325)

### Changed

- `Cmd+C` now copies the focused cell value when one row is selected and a cell has focus; with multiple rows selected, or when no cell is focused, it still copies row(s) as TSV. `Cmd+Shift+C` now always copies row(s) as TSV. "Copy with Headers" stays in the Edit menu and row context menu without a default shortcut (#1332)

### Fixed

- DuckDB Spatial `GEOMETRY` columns render as WKT, not NULL (#1324)
Expand Down
9 changes: 5 additions & 4 deletions TablePro/Core/KeyboardHandling/PasteboardActionRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ enum PasteboardActionRouter {
if let responder = firstResponder,
responder is NSTextView || responder is TextView {
return .textCopy
} else if hasRowSelection {
}
if hasRowSelection {
return .copyRows
} else if hasTableSelection {
}
if hasTableSelection {
return .copyTableNames
} else {
return .textCopy
}
return .textCopy
}

static func resolvePasteAction(
Expand Down
6 changes: 4 additions & 2 deletions TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case redo
case cut
case copy
case copyRowsExplicit
case copyWithHeaders
case copyAsJson
case paste
Expand Down Expand Up @@ -103,7 +104,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
.executeQuery, .explainQuery, .formatQuery, .export, .importData, .quickSwitcher,
.previousPage, .nextPage, .saveAsFavorite, .openTerminal:
return .file
case .undo, .redo, .cut, .copy, .copyWithHeaders, .copyAsJson, .paste,
case .undo, .redo, .cut, .copy, .copyRowsExplicit, .copyWithHeaders, .copyAsJson, .paste,
.delete, .selectAll, .clearSelection, .addRow,
.duplicateRow, .truncateTable, .previewFKReference:
return .edit
Expand Down Expand Up @@ -142,6 +143,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .redo: return String(localized: "Redo")
case .cut: return String(localized: "Cut")
case .copy: return String(localized: "Copy")
case .copyRowsExplicit: return String(localized: "Copy Rows")
case .copyWithHeaders: return String(localized: "Copy with Headers")
case .copyAsJson: return String(localized: "Copy as JSON")
case .paste: return String(localized: "Paste")
Expand Down Expand Up @@ -482,7 +484,7 @@ struct KeyboardSettings: Codable, Equatable {
.redo: KeyCombo(key: "z", command: true, shift: true),
.cut: KeyCombo(key: "x", command: true),
.copy: KeyCombo(key: "c", command: true),
.copyWithHeaders: KeyCombo(key: "c", command: true, shift: true),
.copyRowsExplicit: KeyCombo(key: "c", command: true, shift: true),
.copyAsJson: KeyCombo(key: "j", command: true, option: true),
.paste: KeyCombo(key: "v", command: true),
.delete: KeyCombo(key: "delete", command: true, isSpecialKey: true),
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -12511,6 +12511,9 @@
}
}
}
},
"Copy Rows" : {

},
"Copy SQL" : {
"extractionState" : "stale",
Expand Down
21 changes: 14 additions & 7 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ import TableProPluginKit
/// Custom Commands struct for pasteboard operations
struct PasteboardCommands: Commands {
var settingsManager: AppSettingsManager
@FocusedValue(\.commandActions) var actions: MainContentCommandActions?
@FocusedValue(\.commandActions) var focusedActions: MainContentCommandActions?
@Bindable var commandRegistry: CommandActionsRegistry

private var actions: MainContentCommandActions? {
focusedActions ?? commandRegistry.current
}

/// Build a SwiftUI KeyboardShortcut from keyboard settings
private func shortcut(for action: ShortcutAction) -> KeyboardShortcut? {
settingsManager.keyboard.keyboardShortcut(for: action)
}
Expand All @@ -39,16 +43,20 @@ struct PasteboardCommands: Commands {
hasTableSelection: actions?.hasTableSelection ?? false
)
switch action {
case .textCopy:
case .textCopy, .copyRows:
NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil)
case .copyRows:
actions?.copySelectedRows()
case .copyTableNames:
actions?.copyTableNames()
}
}
.optionalKeyboardShortcut(shortcut(for: .copy))

Button("Copy Rows") {
NSApp.sendAction(#selector(KeyHandlingTableView.copyRowsAsTSV(_:)), to: nil, from: nil)
}
.optionalKeyboardShortcut(shortcut(for: .copyRowsExplicit))
.disabled(!(actions?.hasRowSelection ?? false))

Button("Copy with Headers") {
actions?.copySelectedRowsWithHeaders()
}
Expand Down Expand Up @@ -458,8 +466,7 @@ struct AppMenuCommands: Commands {
.optionalKeyboardShortcut(shortcut(for: .redo))
}

// Edit menu - pasteboard commands with FocusedValue support
PasteboardCommands(settingsManager: settingsManager)
PasteboardCommands(settingsManager: settingsManager, commandRegistry: commandRegistry)

// Edit menu - Find + row operations (after pasteboard)
CommandGroup(after: .pasteboard) {
Expand Down
7 changes: 5 additions & 2 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,11 @@ struct MainEditorContentView: View {
case .structure:
if let tableName = tab.tableContext.tableName {
TableStructureView(
tableName: tableName, connection: connection,
toolbarState: coordinator.toolbarState, coordinator: coordinator
tableName: tableName,
connection: connection,
toolbarState: coordinator.toolbarState,
coordinator: coordinator,
selectionState: selectionState
)
.id(tableName)
.frame(maxHeight: .infinity)
Expand Down
4 changes: 1 addition & 3 deletions TablePro/Views/Main/MainContentCommandActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,7 @@ final class MainContentCommandActions {
observeKeyWindowOnly(AppCommands.shared.exportQueryResults) { [weak self] _ in self?.exportQueryResults() }

observeKeyWindowOnly(AppCommands.shared.copySelectedRows) { [weak self] _ in
guard let self else { return }
let indices = self.selectionState.indices
self.coordinator?.copySelectedRowsToClipboard(indices: indices)
self?.copySelectedRows()
}

observeKeyWindowOnly(AppCommands.shared.pasteRows) { [weak self] _ in
Expand Down
21 changes: 20 additions & 1 deletion TablePro/Views/Results/KeyHandlingTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,28 @@ final class KeyHandlingTableView: NSTableView {
}

@objc func copy(_ sender: Any?) {
if let cell = focusedDataCell() {
coordinator?.copyCellValue(at: cell.row, columnIndex: cell.columnIndex)
} else {
coordinator?.delegate?.dataGridCopyRows(Set(selectedRowIndexes))
}
}

@objc func copyRowsAsTSV(_ sender: Any?) {
coordinator?.delegate?.dataGridCopyRows(Set(selectedRowIndexes))
}

private func focusedDataCell() -> (row: Int, columnIndex: Int)? {
guard selectedRowIndexes.count == 1,
focusedRow >= 0,
DataGridView.isDataTableColumn(focusedColumn),
let schema = coordinator?.identitySchema,
let dataColumn = DataGridView.dataColumnIndex(for: focusedColumn, in: self, schema: schema) else {
return nil
}
return (focusedRow, dataColumn)
}

@objc func paste(_ sender: Any?) {
guard coordinator?.isEditable == true else { return }
if focusedRow >= 0,
Expand All @@ -137,7 +156,7 @@ final class KeyHandlingTableView: NSTableView {
switch item.action {
case #selector(delete(_:)), #selector(deleteBackward(_:)):
return coordinator?.isEditable == true && !selectedRowIndexes.isEmpty
case #selector(copy(_:)):
case #selector(copy(_:)), #selector(copyRowsAsTSV(_:)):
return !selectedRowIndexes.isEmpty
case #selector(paste(_:)):
return coordinator?.isEditable == true && coordinator?.delegate != nil
Expand Down
15 changes: 13 additions & 2 deletions TablePro/Views/Structure/TableStructureView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct TableStructureView: View {
let connection: DatabaseConnection
let toolbarState: ConnectionToolbarState
let coordinator: MainContentCoordinator?
let selectionState: GridSelectionState

@State var selectedTab: StructureTab = .columns
@State var columns: [ColumnInfo] = []
Expand Down Expand Up @@ -53,11 +54,18 @@ struct TableStructureView: View {
@State var actionHandler = StructureViewActionHandler()
@State var gridDelegate: StructureGridDelegate

init(tableName: String, connection: DatabaseConnection, toolbarState: ConnectionToolbarState, coordinator: MainContentCoordinator?) {
init(
tableName: String,
connection: DatabaseConnection,
toolbarState: ConnectionToolbarState,
coordinator: MainContentCoordinator?,
selectionState: GridSelectionState
) {
self.tableName = tableName
self.connection = connection
self.toolbarState = toolbarState
self.coordinator = coordinator
self.selectionState = selectionState

let manager = StructureChangeManager()
_structureChangeManager = State(wrappedValue: manager)
Expand All @@ -78,6 +86,7 @@ struct TableStructureView: View {
contentArea
}
.task(loadInitialData)
.onChange(of: selectedRows) { _, newRows in selectionState.indices = newRows }
.onChange(of: selectedTab) { _, newValue in onSelectedTabChanged(newValue) }
.onChange(of: columns) { onColumnsChanged() }
.onChange(of: indexes) { onIndexesChanged() }
Expand Down Expand Up @@ -114,6 +123,7 @@ struct TableStructureView: View {
.onDisappear {
coordinator?.toolbarState.hasStructureChanges = false
coordinator?.structureActions = nil
selectionState.indices = []
}
.onChange(of: structureChangeManager.hasChanges) { _, newValue in
coordinator?.toolbarState.hasStructureChanges = newValue
Expand Down Expand Up @@ -360,7 +370,8 @@ struct TableStructureView: View {
type: .mysql
),
toolbarState: ConnectionToolbarState(),
coordinator: nil
coordinator: nil,
selectionState: GridSelectionState()
)
.frame(width: 800, height: 600)
}
2 changes: 1 addition & 1 deletion docs/features/data-grid.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ Use arrow keys to move between cells. Press `Enter` to edit, `Escape` to cancel.

Click a cell to select it. Drag or Shift+click to select a range. Click row numbers for entire rows; Cmd+click for non-contiguous rows.

Select rows and press `Cmd+C` for TSV, `Cmd+Shift+C` for TSV with headers, or `Cmd+Option+J` for JSON.
`Cmd+C` copies the focused cell value when a single row is selected and a cell has focus; otherwise it copies the selected row(s) as TSV. `Cmd+Shift+C` always copies the selected row(s) as TSV. `Cmd+Option+J` copies as JSON.

Right-click a row and choose **Copy as** for additional formats:

Expand Down
6 changes: 3 additions & 3 deletions docs/features/keyboard-shortcuts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut

| Action | Shortcut |
|--------|----------|
| Copy selection | `Cmd+C` |
| Copy with Headers | `Cmd+Shift+C` |
| Copy (cell value if a cell is focused on a single row, otherwise row(s) TSV) | `Cmd+C` |
| Copy Rows as TSV | `Cmd+Shift+C` |
| Copy as JSON | `Cmd+Option+J` |
| Copy as TSV | Available from context menu |
| Copy with Headers | Available from Edit menu and context menu |

## CSV Inspector

Expand Down
Loading