diff --git a/CHANGELOG.md b/CHANGELOG.md index 194ececb3..b032124f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Delete key now respects cell-range selection in the data grid, removing all rows covered by the selection instead of ignoring it. +- Right-clicking inside a multi-row or cell-range selection no longer collapses the selection before the context menu appears. - Oracle connections no longer crash the app during connect. A short or unexpected handshake packet from the server (such as session-setup metadata or an error) now surfaces the error or continues instead of trapping. (#1683) - MongoDB filters on `_id` and other ObjectId fields now match. A 24-character hex value is matched as an ObjectId as well as a string, so filtering by `_id` returns the row instead of nothing. (#1682) - The sidebar and inspector keep their width per connection, the sidebar keeps its collapsed state, and the inspector keeps its selected tab, when you quit and reopen the app. diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 0b39899f7..365d0c7a9 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -222,6 +222,10 @@ final class KeyHandlingTableView: NSTableView { @objc func delete(_ sender: Any?) { guard coordinator?.isEditable == true else { return } + if let controller = gridSelection, !controller.isEmpty { + coordinator?.delegate?.dataGridDeleteRows(Set(controller.selection.affectedRows)) + return + } guard !selectedRowIndexes.isEmpty else { return } coordinator?.delegate?.dataGridDeleteRows(Set(selectedRowIndexes)) } @@ -279,7 +283,8 @@ final class KeyHandlingTableView: NSTableView { override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { switch item.action { case #selector(delete(_:)), #selector(deleteBackward(_:)): - return coordinator?.isEditable == true && !selectedRowIndexes.isEmpty + let hasGridSelection = gridSelection?.isEmpty == false + return coordinator?.isEditable == true && (hasGridSelection || !selectedRowIndexes.isEmpty) case #selector(copy(_:)): let hasGridSelection = gridSelection?.isEmpty == false return hasGridSelection || !selectedRowIndexes.isEmpty @@ -407,7 +412,7 @@ final class KeyHandlingTableView: NSTableView { private func deleteSelectedRowsIfPossible() { guard coordinator?.isEditable == true else { return } - guard !selectedRowIndexes.isEmpty else { return } + guard gridSelection?.isEmpty == false || !selectedRowIndexes.isEmpty else { return } delete(nil) } @@ -521,6 +526,31 @@ final class KeyHandlingTableView: NSTableView { scrollColumnToVisible(prevColumn) } + override func rightMouseDown(with event: NSEvent) { + let point = convert(event.locationInWindow, from: nil) + let clickedRow = row(at: point) + if clickedRow >= 0, clickIsInsideSelection(row: clickedRow, point: point) { + window?.makeFirstResponder(self) + if let menu = menu(for: event) { + NSMenu.popUpContextMenu(menu, with: event, for: self) + } + return + } + super.rightMouseDown(with: event) + } + + private func clickIsInsideSelection(row clickedRow: Int, point: NSPoint) -> Bool { + if selectedRowIndexes.contains(clickedRow) { return true } + guard let controller = gridSelection, !controller.isEmpty else { return false } + let clickedColumn = column(at: point) + guard clickedColumn >= 0, + let schema = coordinator?.identitySchema, + let dataColumn = DataGridView.dataColumnIndex(for: clickedColumn, in: self, schema: schema) else { + return false + } + return controller.selection.contains(row: clickedRow, column: dataColumn) + } + override func menu(for event: NSEvent) -> NSMenu? { let point = convert(event.locationInWindow, from: nil) let clickedRow = row(at: point) diff --git a/TableProTests/Views/Results/CellSelectionTests.swift b/TableProTests/Views/Results/CellSelectionTests.swift index 37a30ad80..cf83d74b1 100644 --- a/TableProTests/Views/Results/CellSelectionTests.swift +++ b/TableProTests/Views/Results/CellSelectionTests.swift @@ -63,6 +63,16 @@ struct GridSelectionTests { #expect(selection.boundingRectangle == rect) } + @Test("a cell-range spanning rows reports every covered row") + func cellRangeAffectsEveryCoveredRow() { + let selection = GridSelection.single( + GridRect(rows: 2...5, columns: 1...3), + anchor: GridCoord(row: 2, column: 1), + active: GridCoord(row: 5, column: 3) + ) + #expect(selection.affectedRows == IndexSet(integersIn: 2...5)) + } + @Test("multiple rectangles report union of affected rows and columns") func multipleRectanglesUnion() { let selection = GridSelection(