Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Rectangular cell selection in the data grid. Click and drag to select a range, Shift+click to extend, Cmd+click to add cells, Cmd+click a column header to select the column, Shift+Arrow to extend by one cell, Cmd+A to select the whole grid, Cmd+C to copy as TSV. (#1446)
- BigQuery datasets show as expandable nodes in the sidebar, instead of one at a time behind a picker.
- OpenCode Zen as an AI provider, with free models when no key is set. (#1400)
- Oracle Database 11g (11.1 and 11.2) now connects. (#1425)
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Models/UI/ColumnIdentitySchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ struct ColumnIdentitySchema: Equatable {
slotByColumnName[name]
}

var totalDataColumns: Int { columnNames.count }

static func slotIdentifier(_ slot: Int) -> NSUserInterfaceItemIdentifier {
NSUserInterfaceItemIdentifier("\(dataColumnPrefix)\(slot)")
}
Expand Down
13 changes: 13 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,16 @@
"%1$@, %2$@" : {
"shouldTranslate" : false
},
"%d cells selected, rows %d to %d, columns %d to %d" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$d cells selected, rows %2$d to %3$d, columns %4$d to %5$d"
}
}
}
},
"%d columns" : {

},
Expand Down Expand Up @@ -8712,6 +8722,9 @@
}
}
}
},
"Cell selection cleared" : {

},
"Cell Value" : {
"localizations" : {
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Views/Results/CellOverlayBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,30 @@ class CellOverlayBase: NSObject {
self.columnIndex = columnIndex
tableView.addSubview(container)
self.container = container
underlyingCell(in: tableView, row: row, column: column)?.applyOverlayActive(true)
selectionOverlay(in: tableView)?.needsDisplay = true
installDismissObservers()
}

private func underlyingCell(in tableView: NSTableView, row: Int, column: Int) -> DataGridCellView? {
tableView.view(atColumn: column, row: row, makeIfNecessary: false) as? DataGridCellView
}

private func selectionOverlay(in tableView: NSTableView) -> GridSelectionOverlay? {
(tableView as? KeyHandlingTableView)?.selectionOverlay
}

func handleDismiss(reason: CellOverlayDismissReason) {
removeOverlay()
}

func removeOverlay() {
guard let activeContainer = container else { return }
removeDismissObservers()
if let hostTableView {
underlyingCell(in: hostTableView, row: row, column: column)?.applyOverlayActive(false)
selectionOverlay(in: hostTableView)?.needsDisplay = true
}
activeContainer.removeFromSuperview()
container = nil
if let hostTableView {
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Results/CellOverlayEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class CellOverlayEditor: CellOverlayBase, NSTextViewDelegate {
textView.font = ThemeEngine.shared.dataGridFonts.regular
textView.textColor = .labelColor
textView.backgroundColor = .textBackgroundColor
textView.focusRingType = .none
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
textView.textContainer?.widthTracksTextView = true
Expand Down
24 changes: 19 additions & 5 deletions TablePro/Views/Results/Cells/DataGridCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class DataGridCellView: NSView {
private var visualState: RowVisualState = .empty
private var isFocusedCell: Bool = false
private var onEmphasizedSelection: Bool = false
private var hasOverlay: Bool = false

private var cachedLine: CTLine?

Expand Down Expand Up @@ -83,6 +84,12 @@ final class DataGridCellView: NSView {
cellRow = state.row
cellColumnIndex = state.columnIndex

if hasOverlay {
hasOverlay = false
updateFocusPresentation()
needsRedraw = true
}

let nextDisplayText: String
let nextFont: NSFont
let nextColor: NSColor
Expand Down Expand Up @@ -150,7 +157,6 @@ final class DataGridCellView: NSView {
updateFocusPresentation()
needsRedraw = true
}

setAccessibilityRowIndexRange(NSRange(location: state.row, length: 1))
setAccessibilityColumnIndexRange(NSRange(location: state.columnIndex, length: 1))

Expand All @@ -176,18 +182,26 @@ final class DataGridCellView: NSView {
updateFocusPresentation()
}

func applyOverlayActive(_ value: Bool) {
guard hasOverlay != value else { return }
hasOverlay = value
updateFocusPresentation()
needsDisplay = true
}

private func updateFocusPresentation() {
focusRingType = (isFocusedCell && !onEmphasizedSelection) ? .exterior : .none
let shouldShowRing = isFocusedCell && !onEmphasizedSelection && !hasOverlay
focusRingType = shouldShowRing ? .exterior : .none
noteFocusRingMaskChanged()
needsDisplay = true
}

override var focusRingMaskBounds: NSRect {
onEmphasizedSelection ? .zero : bounds
(onEmphasizedSelection || hasOverlay) ? .zero : bounds
}

override func drawFocusRingMask() {
guard !onEmphasizedSelection else { return }
guard !onEmphasizedSelection, !hasOverlay else { return }
NSBezierPath(rect: bounds).fill()
}

Expand All @@ -211,7 +225,7 @@ final class DataGridCellView: NSView {
drawAccessory(in: accessoryRect)
NSGraphicsContext.current?.restoreGraphicsState()

if isFocusedCell && onEmphasizedSelection {
if isFocusedCell && onEmphasizedSelection && !hasOverlay {
drawFocusBorder()
}
}
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
let cellRegistry: DataGridCellRegistry
let columnPool = DataGridColumnPool()
let tableRowsController = TableRowsController()
let selectionController = GridSelectionController()
var overlayEditor: CellOverlayEditor?
var overlayViewer: CellOverlayViewer?

Expand Down Expand Up @@ -203,6 +204,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
prewarmResumeTask?.cancel()
prewarmResumeTask = nil
detachScrollObservers()
selectionController.clear()
overlayEditor?.dismiss(commit: false)
overlayViewer?.dismiss()
settingsCancellable?.cancel()
Expand Down Expand Up @@ -264,6 +266,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
guard let tableView else { return }
invalidateAllDisplayCaches()
updateCache()
selectionController.clear()
tableView.reloadData()
startBackgroundPrewarm()
}
Expand Down
31 changes: 28 additions & 3 deletions TablePro/Views/Results/DataGridRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,34 @@ class DataGridRowView: NSTableRowView {

override func drawBackground(in dirtyRect: NSRect) {
super.drawBackground(in: dirtyRect)
guard let rowTint, !isSelected else { return }
rowTint.setFill()
bounds.fill()
if let rowTint, !isSelected {
rowTint.setFill()
bounds.fill()
}
drawCellSelectionFill(in: dirtyRect)
}

private func drawCellSelectionFill(in dirtyRect: NSRect) {
guard let selection = coordinator?.selectionController.selection,
!selection.isEmpty,
let tableView = coordinator?.tableView else { return }
let columns = selection.columns(in: rowIndex)
guard !columns.isEmpty else { return }

let fillColor: NSColor = isSelected
? NSColor.unemphasizedSelectedContentBackgroundColor
: NSColor.selectedContentBackgroundColor.withAlphaComponent(0.28)
fillColor.setFill()

let schema = coordinator?.identitySchema
for dataColumn in columns {
guard let schema,
let tableColumnIndex = DataGridView.tableColumnIndex(for: dataColumn, in: tableView, schema: schema) else { continue }
let columnRect = tableView.rect(ofColumn: tableColumnIndex)
let localRect = NSRect(x: columnRect.minX, y: 0, width: columnRect.width, height: bounds.height)
guard localRect.intersects(dirtyRect) else { continue }
localRect.fill()
}
}

private func colorsEqual(_ lhs: NSColor?, _ rhs: NSColor?) -> Bool {
Expand Down
51 changes: 49 additions & 2 deletions TablePro/Views/Results/DataGridView+RowActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,12 @@ extension TableViewCoordinator {

var lines: [String] = []
lines.reserveCapacity(rowCount)

for rowIndex in 0..<rowCount {
guard let row = displayRow(at: rowIndex), row.values.indices.contains(columnIndex) else { continue }
let text = RowValueCopyFormatter.copyText(cell: row.values[columnIndex], columnType: columnType) ?? "NULL"
lines.append(text)
}

guard !lines.isEmpty else { return }
ClipboardService.shared.writeText(lines.joined(separator: "\n"))
}

Expand Down Expand Up @@ -317,4 +316,52 @@ extension TableViewCoordinator {
delegate.dataGridMoveRow(from: fromRow, to: row)
return true
}

func selectColumn(_ dataColumnIndex: Int) {
let totalRows = sortedIDs?.count ?? tableRowsProvider().rows.count
selectionController.selectEntireColumn(dataColumnIndex, totalRows: totalRows)
if let keyTableView = tableView as? KeyHandlingTableView {
keyTableView.deselectAll(nil)
}
}

func copyGridSelection(_ selection: GridSelection) {
guard let rect = selection.boundingRectangle else { return }
let tableRows = tableRowsProvider()
let columnTypes = tableRows.columnTypes
let rowCount = sortedIDs?.count ?? tableRows.rows.count
let columnCount = tableRows.columns.count

let rowRange = rect.rows.lowerBound...min(rect.rows.upperBound, max(0, rowCount - 1))
let columnRange = rect.columns.lowerBound...min(rect.columns.upperBound, max(0, columnCount - 1))
guard rowRange.lowerBound <= rowRange.upperBound,
columnRange.lowerBound <= columnRange.upperBound else { return }

var lines: [String] = []
lines.reserveCapacity(rowRange.count)
for rowIndex in rowRange {
guard let row = displayRow(at: rowIndex) else {
lines.append(String(repeating: "\t", count: columnRange.count - 1))
continue
}
var fields: [String] = []
fields.reserveCapacity(columnRange.count)
for columnIndex in columnRange {
guard selection.contains(row: rowIndex, column: columnIndex) else {
fields.append("")
continue
}
guard row.values.indices.contains(columnIndex) else {
fields.append("")
continue
}
let columnType = columnTypes.indices.contains(columnIndex) ? columnTypes[columnIndex] : nil
let text = RowValueCopyFormatter.copyText(cell: row.values[columnIndex], columnType: columnType) ?? "NULL"
fields.append(text)
}
lines.append(fields.joined(separator: "\t"))
}

ClipboardService.shared.writeText(lines.joined(separator: "\n"))
}
}
13 changes: 13 additions & 0 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ struct DataGridView: NSViewRepresentable {

scrollView.documentView = tableView
context.coordinator.tableView = tableView
installSelectionOverlay(tableView: tableView, coordinator: context.coordinator)
context.coordinator.attachScrollObservers(scrollView: scrollView)
context.coordinator.tableRowsController.attach(tableView)
context.coordinator.tableRowsProvider = tableRowsProvider
Expand Down Expand Up @@ -280,6 +281,7 @@ struct DataGridView: NSViewRepresentable {
}

if needsFullReload {
coordinator.selectionController.clear()
tableView.reloadData()
coordinator.startBackgroundPrewarm()
}
Expand Down Expand Up @@ -384,6 +386,17 @@ struct DataGridView: NSViewRepresentable {
column.width = columnWidth
}

private func installSelectionOverlay(tableView: KeyHandlingTableView, coordinator: TableViewCoordinator) {
let overlay = GridSelectionOverlay(frame: tableView.bounds)
overlay.tableView = tableView
overlay.coordinator = coordinator
tableView.addSubview(overlay)
coordinator.selectionController.tableView = tableView
coordinator.selectionController.overlay = overlay
coordinator.selectionController.coordinator = coordinator
tableView.selectionOverlay = overlay
}

static let firstDataTableColumnIndex: Int = 1

static func isDataTableColumn(_ tableColumnIndex: Int) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ extension TableViewCoordinator {

guard let keyTableView = tableView as? KeyHandlingTableView else { return }

if !isSyncingSelection, !newSelection.isEmpty, !selectionController.isEmpty {
selectionController.clear()
}

let newFocus = resolvedFocus(
previous: previousSelection,
current: newSelection,
Expand Down
Loading
Loading