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
44 changes: 44 additions & 0 deletions apple/TablePro/Core/Menu/EditMenuBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ enum EditMenuBuilder {

private static func copySpecialSubmenu(keyboard: KeyboardSettings) -> NSMenuItem {
MenuItemFactory.submenu(String(localized: "Copy Special"), items: [
MenuItemFactory.item(
String(localized: "Copy Cell Value"),
action: #selector(GridCopyCommandResponding.copyCellValue(_:)),
shortcut: .copyCellValue,
keyboard: keyboard
),
MenuItemFactory.separator,
MenuItemFactory.item(
String(localized: "Copy Rows"),
action: #selector(MainSplitViewController.copySelectedRows(_:)),
Expand All @@ -140,6 +147,43 @@ enum EditMenuBuilder {
action: #selector(MainSplitViewController.copyRowsAsJson(_:)),
shortcut: .copyAsJson,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Copy as CSV"),
action: #selector(GridCopyCommandResponding.copyRowsAsCsv(_:)),
shortcut: .copyAsCsv,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Copy as CSV with Headers"),
action: #selector(GridCopyCommandResponding.copyRowsAsCsvWithHeaders(_:)),
shortcut: .copyAsCsvWithHeaders,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Copy as Markdown"),
action: #selector(GridCopyCommandResponding.copyRowsAsMarkdown(_:)),
shortcut: .copyAsMarkdown,
keyboard: keyboard
),
MenuItemFactory.separator,
MenuItemFactory.item(
String(localized: "Copy as IN Clause"),
action: #selector(GridCopyCommandResponding.copyRowsAsInClause(_:)),
shortcut: .copyAsInClause,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Copy as INSERT Statements"),
action: #selector(GridCopyCommandResponding.copyRowsAsInsertStatements(_:)),
shortcut: .copyAsInsert,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Copy as UPDATE Statements"),
action: #selector(GridCopyCommandResponding.copyRowsAsUpdateStatements(_:)),
shortcut: .copyAsUpdate,
keyboard: keyboard
)
])
}
Expand Down
18 changes: 18 additions & 0 deletions apple/TablePro/Core/Menu/GridCopyCommandResponding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// GridCopyCommandResponding.swift
// TablePro
//

import AppKit

// The focused grid answers for its own selection, so these work in any window that shows one; elsewhere they dim.
@MainActor
@objc protocol GridCopyCommandResponding {
func copyCellValue(_ sender: Any?)
func copyRowsAsCsv(_ sender: Any?)
func copyRowsAsCsvWithHeaders(_ sender: Any?)
func copyRowsAsMarkdown(_ sender: Any?)
func copyRowsAsInClause(_ sender: Any?)
func copyRowsAsInsertStatements(_ sender: Any?)
func copyRowsAsUpdateStatements(_ sender: Any?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ protocol ClipboardProvider {
func writeCsv(_ csv: String)
func writeImage(_ image: NSImage)
func writeRows(tsv: String, html: String?, gridRows: GridRowsClipboardPayload)
func writeText(_ text: String, gridRows: GridRowsClipboardPayload)
func writeCsv(_ csv: String, gridRows: GridRowsClipboardPayload)
func writeCells(_ cells: GridCellsClipboardPayload)
var hasText: Bool { get }
var hasGridRows: Bool { get }
Expand Down Expand Up @@ -145,8 +147,22 @@ struct NSPasteboardClipboardProvider: ClipboardProvider {
if let html {
pb.setString(html, forType: .html)
}
setGridRows(gridRows)
}

func writeText(_ text: String, gridRows: GridRowsClipboardPayload) {
writeText(text)
setGridRows(gridRows)
}

func writeCsv(_ csv: String, gridRows: GridRowsClipboardPayload) {
writeCsv(csv)
setGridRows(gridRows)
}

private func setGridRows(_ gridRows: GridRowsClipboardPayload) {
if let data = try? JSONEncoder().encode(gridRows) {
pb.setData(data, forType: Self.gridRowsType)
NSPasteboard.general.setData(data, forType: Self.gridRowsType)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension MainSplitViewController {
@objc func copy(_ sender: Any?) {
guard let actions = commandActions else { return }
if actions.hasRowSelection {
actions.copySelectedRows()
actions.copySelectedRowsInCopyFormat()
return
}
actions.copyTableNames()
Expand Down
32 changes: 30 additions & 2 deletions apple/TablePro/Models/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ enum DefaultSortBehavior: String, Codable, CaseIterable, Identifiable, Equatable
}
}

// IN Clause, INSERT and UPDATE are left out: they need a column or a single table, so no grid can always produce them.
enum RowCopyFormat: String, Codable, CaseIterable, Identifiable {
case rows
case rowsWithHeaders
case json
case csv
case csvWithHeaders
case markdown

var id: String { rawValue }

var displayName: String {
switch self {
case .rows: return "TSV"
case .rowsWithHeaders: return String(localized: "TSV with Headers")
case .json: return "JSON"
case .csv: return "CSV"
case .csvWithHeaders: return String(localized: "CSV with Headers")
case .markdown: return "Markdown"
}
}
}

/// Data grid settings
struct DataGridSettings: Codable, Equatable {
var rowHeight: DataGridRowHeight
Expand All @@ -101,6 +124,7 @@ struct DataGridSettings: Codable, Equatable {
var truncateQueryResults: Bool
var defaultSortBehavior: DefaultSortBehavior
var defaultSortDirection: SortDirection
var rowCopyFormat: RowCopyFormat

static let `default` = DataGridSettings(
rowHeight: .normal,
Expand All @@ -115,7 +139,8 @@ struct DataGridSettings: Codable, Equatable {
queryResultRowCap: 10_000,
truncateQueryResults: true,
defaultSortBehavior: .none,
defaultSortDirection: .ascending
defaultSortDirection: .ascending,
rowCopyFormat: .rows
)

init(
Expand All @@ -131,7 +156,8 @@ struct DataGridSettings: Codable, Equatable {
queryResultRowCap: Int = 10_000,
truncateQueryResults: Bool = true,
defaultSortBehavior: DefaultSortBehavior = .none,
defaultSortDirection: SortDirection = .ascending
defaultSortDirection: SortDirection = .ascending,
rowCopyFormat: RowCopyFormat = .rows
) {
self.rowHeight = rowHeight
self.dateFormat = dateFormat
Expand All @@ -146,6 +172,7 @@ struct DataGridSettings: Codable, Equatable {
self.truncateQueryResults = truncateQueryResults
self.defaultSortBehavior = defaultSortBehavior
self.defaultSortDirection = defaultSortDirection
self.rowCopyFormat = rowCopyFormat
}

init(from decoder: Decoder) throws {
Expand All @@ -169,6 +196,7 @@ struct DataGridSettings: Codable, Equatable {
truncateQueryResults = container.decodeSetting(.truncateQueryResults, default: fallback.truncateQueryResults)
defaultSortBehavior = container.decodeSetting(.defaultSortBehavior, default: fallback.defaultSortBehavior)
defaultSortDirection = container.decodeSetting(.defaultSortDirection, default: fallback.defaultSortDirection)
rowCopyFormat = container.decodeSetting(.rowCopyFormat, default: fallback.rowCopyFormat)
}

// MARK: - Validated Properties
Expand Down
4 changes: 4 additions & 0 deletions apple/TablePro/Models/UI/DataGridConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ struct DataGridConfiguration: Equatable {
/// question is what the grid holds, not which tab it sits in.
var appliesRowSortPreferences: Bool = false

// Records: a table, a query, a file or a listing. Off on the Structure grids, whose Copy writes column and
// index definitions a paste reads back, and which have no INSERT or UPDATE to offer.
var appliesRowCopyFormat: Bool = false

/// Whether a heading click or the heading menu orders the rows. A Cassandra table cannot be ordered by an
/// arbitrary column, so its table grid offers no sort rather than one the server refuses.
var supportsColumnSort: Bool = true
Expand Down
19 changes: 18 additions & 1 deletion apple/TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,16 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case redo
case cut
case copy
case copyCellValue
case copyRowsExplicit
case copyWithHeaders
case copyAsJson
case copyAsCsv
case copyAsCsvWithHeaders
case copyAsMarkdown
case copyAsInClause
case copyAsInsert
case copyAsUpdate
case paste
case delete
case selectAll
Expand Down Expand Up @@ -185,7 +192,8 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
.previewSQL, .find, .findAndReplace, .findNext, .findPrevious, .useSelectionForFind,
.aiReviewQuery, .aiExplainQuery, .aiOptimizeQuery:
return .editor
case .undo, .redo, .cut, .copy, .copyRowsExplicit, .copyWithHeaders, .copyAsJson,
case .undo, .redo, .cut, .copy, .copyCellValue, .copyRowsExplicit, .copyWithHeaders, .copyAsJson,
.copyAsCsv, .copyAsCsvWithHeaders, .copyAsMarkdown, .copyAsInClause, .copyAsInsert, .copyAsUpdate,
.paste, .delete, .selectAll, .clearSelection, .addRow, .duplicateRow,
.setValueEmpty, .setValueNull, .setValueDefault, .generateUUIDv4, .generateUUIDv7,
.restorePreviousValues,
Expand Down Expand Up @@ -224,6 +232,8 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
.setValueEmpty, .setValueNull, .setValueDefault, .generateUUIDv4, .generateUUIDv7,
.delete, .truncateTable, .previewFKReference, .saveAsFavorite,
.copyRowsExplicit, .copyWithHeaders, .copyAsJson, .toggleFilters, .jumpToColumn,
.copyCellValue, .copyAsCsv, .copyAsCsvWithHeaders, .copyAsMarkdown, .copyAsInClause,
.copyAsInsert, .copyAsUpdate,
/// Named rather than left to the `default:` below. It reverses a row the grid is
/// showing, so it belongs in the grid's context the way Add Row and Delete do, and
/// inheriting `.global` would let the recorder call a grid combo free for it.
Expand Down Expand Up @@ -302,6 +312,13 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .copyRowsExplicit: return String(localized: "Copy Rows")
case .copyWithHeaders: return String(localized: "Copy with Headers")
case .copyAsJson: return String(localized: "Copy as JSON")
case .copyCellValue: return String(localized: "Copy Cell Value")
case .copyAsCsv: return String(localized: "Copy as CSV")
case .copyAsCsvWithHeaders: return String(localized: "Copy as CSV with Headers")
case .copyAsMarkdown: return String(localized: "Copy as Markdown")
case .copyAsInClause: return String(localized: "Copy as IN Clause")
case .copyAsInsert: return String(localized: "Copy as INSERT Statements")
case .copyAsUpdate: return String(localized: "Copy as UPDATE Statements")
case .paste: return String(localized: "Paste")
case .delete: return String(localized: "Delete")
case .selectAll: return String(localized: "Select All")
Expand Down
33 changes: 33 additions & 0 deletions apple/TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -49258,6 +49258,12 @@
}
}
}
},
"Copy as CSV" : {

},
"Copy as CSV with Headers" : {

},
"Copy as Hex" : {
"localizations" : {
Expand Down Expand Up @@ -49298,6 +49304,12 @@
}
}
}
},
"Copy as IN Clause" : {

},
"Copy as INSERT Statements" : {

},
"Copy as JSON" : {
"localizations" : {
Expand Down Expand Up @@ -49338,6 +49350,12 @@
}
}
}
},
"Copy as Markdown" : {

},
"Copy as UPDATE Statements" : {

},
"Copy Average" : {

Expand All @@ -49351,6 +49369,9 @@
}
}
}
},
"Copy Cell Value" : {

},
"Copy Column Name" : {
"localizations" : {
Expand Down Expand Up @@ -50462,6 +50483,9 @@
}
}
}
},
"Copy rows as:" : {

},
"Copy rows into %@ on the server" : {
"localizations" : {
Expand Down Expand Up @@ -50795,6 +50819,9 @@
}
}
}
},
"Copy uses this format for selected rows. A single cell still copies its value, and Edit > Copy Special offers every format." : {

},
"Copy Value" : {
"localizations" : {
Expand Down Expand Up @@ -50928,6 +50955,9 @@
}
}
}
},
"Copying" : {

},
"Correct the Key Passphrase on the connection's Network tab." : {
"extractionState" : "manual",
Expand Down Expand Up @@ -199954,6 +199984,9 @@
}
}
}
},
"TSV with Headers" : {

},
"TTL has to be a whole number of seconds above 0, or -1 or NULL for no expiry." : {
"extractionState" : "manual",
Expand Down
3 changes: 2 additions & 1 deletion apple/TablePro/Views/Agent/AgentResultRowsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ internal struct AgentResultRowsView: View {
databaseType: connection?.type,
showRowNumbers: true,
supportsColumnCommands: false,
supportsValueFilter: false
supportsValueFilter: false,
appliesRowCopyFormat: true
),
delegate: gridDelegate,
selectedRowIndices: $selectedRows,
Expand Down
1 change: 1 addition & 0 deletions apple/TablePro/Views/DataFiles/DataFileContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ struct DataFileContentView: View {
config.showRowNumbers = true
config.hiddenColumns = controller.columnLayout.hiddenColumns
config.supportsValueFilter = false
config.appliesRowCopyFormat = true
config.filterMenuColumnTypes = controller.columnNames.ids.map {
DataFileColumnTypes.filterType(for: controller.kind(of: $0))
}
Expand Down
Loading
Loading