Skip to content

Commit 06c60c1

Browse files
committed
fix(inspector): operate CSV column verbs on fully selected columns, make header Cmd-click additive
1 parent 86df3ef commit 06c60c1

8 files changed

Lines changed: 122 additions & 30 deletions

File tree

TablePro/TableProApp.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,17 +620,17 @@ struct AppMenuCommands: Commands {
620620
}
621621
.disabled(!keyWindowIsInspector)
622622

623-
Button(String(localized: "Insert Column Left")) {
623+
Button("Insert Column Left") {
624624
NSApp.sendAction(#selector(InspectorViewController.inspectorInsertColumnLeft(_:)), to: nil, from: nil)
625625
}
626626
.disabled(!keyWindowIsInspector)
627627

628-
Button(String(localized: "Insert Column Right")) {
628+
Button("Insert Column Right") {
629629
NSApp.sendAction(#selector(InspectorViewController.inspectorInsertColumnRight(_:)), to: nil, from: nil)
630630
}
631631
.disabled(!keyWindowIsInspector)
632632

633-
Button(String(localized: "Delete Column")) {
633+
Button("Delete Column") {
634634
NSApp.sendAction(#selector(InspectorViewController.inspectorDeleteColumn(_:)), to: nil, from: nil)
635635
}
636636
.disabled(!keyWindowIsInspector)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// InspectorColumnTargets.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
enum InspectorColumnTargets {
9+
static func deleteMenuSelection(clicked: Int, fullySelected: IndexSet) -> [Int] {
10+
guard fullySelected.contains(clicked), fullySelected.count > 1 else { return [clicked] }
11+
return fullySelected.sorted()
12+
}
13+
14+
static func deleteTargets(explicit: [Int]?, fullySelected: IndexSet, columnCount: Int) -> [Int] {
15+
let candidates = explicit ?? Array(fullySelected)
16+
return candidates.filter { $0 >= 0 && $0 < columnCount }.sorted()
17+
}
18+
19+
static func insertAnchor(clicked: Int?, fullySelected: IndexSet, columnCount: Int, toRight: Bool) -> Int? {
20+
guard columnCount > 0 else { return nil }
21+
if let clicked, clicked >= 0, clicked < columnCount {
22+
return clicked
23+
}
24+
if let bound = toRight ? fullySelected.max() : fullySelected.min(), bound >= 0, bound < columnCount {
25+
return bound
26+
}
27+
return toRight ? columnCount - 1 : 0
28+
}
29+
}

TablePro/Views/Inspector/InspectorViewController.swift

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -292,17 +292,14 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
292292
}
293293

294294
private func columnInsertAnchor(from sender: Any?, toRight: Bool) -> Int? {
295-
guard let inspector = inspectorDocument, !inspector.columnNames.isEmpty else { return nil }
296-
let count = inspector.columnNames.count
297-
if let menuItem = sender as? NSMenuItem, menuItem.tag >= 0, menuItem.tag < count {
298-
return menuItem.tag
299-
}
300-
if let affected = gridDelegate.coordinator?.selectionController.selection.affectedColumns,
301-
let bound = toRight ? affected.max() : affected.min(),
302-
bound >= 0, bound < count {
303-
return bound
304-
}
305-
return toRight ? count - 1 : 0
295+
guard let inspector = inspectorDocument else { return nil }
296+
let clicked = (sender as? NSMenuItem).map(\.tag)
297+
return InspectorColumnTargets.insertAnchor(
298+
clicked: clicked,
299+
fullySelected: selectedFullColumns(),
300+
columnCount: inspector.columnNames.count,
301+
toRight: toRight
302+
)
306303
}
307304

308305
@objc func inspectorDeleteColumn(_ sender: Any?) {
@@ -312,14 +309,15 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
312309

313310
private func columnDeleteTargets(from sender: Any?) -> [Int] {
314311
guard let inspector = inspectorDocument else { return [] }
315-
let count = inspector.columnNames.count
316-
if let menuItem = sender as? NSMenuItem, let represented = menuItem.representedObject as? [Int] {
317-
return represented.filter { $0 >= 0 && $0 < count }.sorted()
318-
}
319-
if let affected = gridDelegate.coordinator?.selectionController.selection.affectedColumns {
320-
return affected.filter { $0 >= 0 && $0 < count }.sorted()
321-
}
322-
return []
312+
return InspectorColumnTargets.deleteTargets(
313+
explicit: (sender as? NSMenuItem)?.representedObject as? [Int],
314+
fullySelected: selectedFullColumns(),
315+
columnCount: inspector.columnNames.count
316+
)
317+
}
318+
319+
private func selectedFullColumns() -> IndexSet {
320+
gridDelegate.coordinator?.selectionController.selectedFullColumns() ?? IndexSet()
323321
}
324322

325323
private func performDeleteColumns(_ columns: [Int]) {
@@ -377,11 +375,7 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
377375
}
378376

379377
private func columnDeleteSelection(clicked: Int) -> [Int] {
380-
guard let affected = gridDelegate.coordinator?.selectionController.selection.affectedColumns,
381-
affected.contains(clicked), affected.count > 1 else {
382-
return [clicked]
383-
}
384-
return affected.sorted()
378+
InspectorColumnTargets.deleteMenuSelection(clicked: clicked, fullySelected: selectedFullColumns())
385379
}
386380

387381
fileprivate func rowStructureMenuItems(forRow displayRow: Int) -> [NSMenuItem] {
@@ -468,7 +462,7 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
468462
case #selector(inspectorDeleteColumn(_:)):
469463
guard nsDocument != nil else { return false }
470464
if let menuItem = item as? NSMenuItem, menuItem.representedObject is [Int] { return true }
471-
return !(gridDelegate.coordinator?.selectionController.selection.affectedColumns.isEmpty ?? true)
465+
return !selectedFullColumns().isEmpty
472466
case #selector(inspectorDeleteSelectedRows(_:)):
473467
return !state.selectedRowIndices.isEmpty
474468
default:

TablePro/Views/Results/DataGridView+RowActions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,14 @@ extension TableViewCoordinator {
340340
}
341341
}
342342

343+
func extendColumnSelection(_ dataColumnIndex: Int) {
344+
let totalRows = displayIDs?.count ?? tableRowsProvider().rows.count
345+
selectionController.addEntireColumn(dataColumnIndex, totalRows: totalRows)
346+
if let keyTableView = tableView as? KeyHandlingTableView {
347+
keyTableView.deselectAll(nil)
348+
}
349+
}
350+
343351
func copyGridSelection(_ selection: GridSelection) {
344352
guard let rect = selection.boundingRectangle else { return }
345353
let tableRows = tableRowsProvider()

TablePro/Views/Results/Selection/GridSelectionController.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ final class GridSelectionController {
152152
update(.single(rect, anchor: anchor, active: anchor))
153153
}
154154

155+
func addEntireColumn(_ column: Int, totalRows: Int) {
156+
guard column >= 0, totalRows > 0 else { return }
157+
let rect = GridRect(rows: 0...(totalRows - 1), columns: column...column)
158+
let anchor = GridCoord(row: 0, column: column)
159+
let addition = GridSelection.single(rect, anchor: anchor, active: anchor)
160+
update(selection.isEmpty ? addition : selection.union(addition))
161+
}
162+
163+
func selectedFullColumns() -> IndexSet {
164+
fullySelectedColumns(in: selection)
165+
}
166+
155167
func selectEntireRow(_ row: Int, totalColumns: Int) {
156168
guard row >= 0, totalColumns > 0 else { return }
157169
let rect = GridRect(rows: row...row, columns: 0...(totalColumns - 1))

TablePro/Views/Results/SortableHeaderView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ final class SortableHeaderView: NSTableHeaderView {
303303
}
304304

305305
if modifierFlags.contains(.command) && !modifierFlags.contains(.shift) {
306-
coordinator.selectColumn(dataIndex)
306+
coordinator.extendColumnSelection(dataIndex)
307307
return
308308
}
309309

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// InspectorColumnTargetsTests.swift
3+
// TableProTests
4+
//
5+
6+
import Foundation
7+
@testable import TablePro
8+
import Testing
9+
10+
@Suite("InspectorColumnTargets")
11+
struct InspectorColumnTargetsTests {
12+
@Test("Delete menu targets the whole selection only when the clicked column is inside it")
13+
func deleteMenuSelection() {
14+
#expect(InspectorColumnTargets.deleteMenuSelection(
15+
clicked: 1, fullySelected: IndexSet([1, 2, 3])) == [1, 2, 3])
16+
#expect(InspectorColumnTargets.deleteMenuSelection(
17+
clicked: 5, fullySelected: IndexSet([1, 2, 3])) == [5])
18+
#expect(InspectorColumnTargets.deleteMenuSelection(
19+
clicked: 2, fullySelected: IndexSet([2])) == [2])
20+
#expect(InspectorColumnTargets.deleteMenuSelection(
21+
clicked: 0, fullySelected: IndexSet()) == [0])
22+
}
23+
24+
@Test("Delete targets prefer the explicit list and fall back to the selection, filtered to range")
25+
func deleteTargets() {
26+
#expect(InspectorColumnTargets.deleteTargets(
27+
explicit: [3, 1], fullySelected: IndexSet([9]), columnCount: 5) == [1, 3])
28+
#expect(InspectorColumnTargets.deleteTargets(
29+
explicit: nil, fullySelected: IndexSet([0, 2]), columnCount: 5) == [0, 2])
30+
#expect(InspectorColumnTargets.deleteTargets(
31+
explicit: [1, 99], fullySelected: IndexSet(), columnCount: 3) == [1])
32+
}
33+
34+
@Test("Insert anchor prefers the clicked column, then the selection bound, then the edge")
35+
func insertAnchor() {
36+
#expect(InspectorColumnTargets.insertAnchor(
37+
clicked: 2, fullySelected: IndexSet([0, 4]), columnCount: 5, toRight: true) == 2)
38+
#expect(InspectorColumnTargets.insertAnchor(
39+
clicked: nil, fullySelected: IndexSet([1, 3]), columnCount: 5, toRight: false) == 1)
40+
#expect(InspectorColumnTargets.insertAnchor(
41+
clicked: nil, fullySelected: IndexSet([1, 3]), columnCount: 5, toRight: true) == 3)
42+
#expect(InspectorColumnTargets.insertAnchor(
43+
clicked: nil, fullySelected: IndexSet(), columnCount: 5, toRight: false) == 0)
44+
#expect(InspectorColumnTargets.insertAnchor(
45+
clicked: nil, fullySelected: IndexSet(), columnCount: 5, toRight: true) == 4)
46+
#expect(InspectorColumnTargets.insertAnchor(
47+
clicked: 3, fullySelected: IndexSet(), columnCount: 0, toRight: false) == nil)
48+
}
49+
}

docs/features/csv-inspector.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Right-click a column header, or open the toolbar `Columns` menu, to reach the sa
5353
- **Change Type ▸** overrides the inferred type as Text, Integer, Real, Boolean, or Date. **Reset to Inferred** drops the override.
5454
- **Delete Column** removes the column. It sits last in the menu and is undoable with Cmd+Z. Deleting a column that holds data asks you to confirm first.
5555

56-
Select several columns first (click a header, then Cmd-click others) and the menu reads **Delete Columns**, removing all of them in one undoable step. Insert Column Left / Right, and Delete Column, also appear in the Edit menu, where they act on the selected column.
56+
Cmd-click several column headers to select whole columns, and the menu reads **Delete Columns**, removing all of them in one undoable step. Insert Column Left / Right, and Delete Column, also appear in the Edit menu, where they act on the selected columns.
5757

5858
In the toolbar `Columns` menu, **Add Column…** at the top appends a new column at the end, and every column is listed below it with its current type and the same submenu.
5959

0 commit comments

Comments
 (0)