From 3f37596613d395dac5838a66230c9fd06265d0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Wed, 10 Jun 2026 22:26:09 +0700 Subject: [PATCH 1/2] fix(datagrid): Delete key and right-click respect multi-row/cell-range selection D12A: delete(_:) checks gridSelection.affectedRows first, mirroring copy(). D12B: rightMouseDown override skips super when clicking inside existing selection, calls NSMenu.popUpContextMenu directly to preserve the selection set. --- CHANGELOG.md | 2 ++ .../Views/Results/KeyHandlingTableView.swift | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32625cb83..02791051a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,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 a row inside a multi-row selection no longer collapses the selection before the context menu appears. - iCloud Sync between the iPhone and Mac apps: the iOS app now uses the Production CloudKit environment, so a development build no longer syncs into a separate database the Mac never reads. - Exports no longer fail mid-table on servers that enforce a statement time limit; the export session disables the limit and restores it afterwards, the same way mysqldump does. (#1633) diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 0b39899f7..ed6b6d3ec 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -222,6 +222,12 @@ final class KeyHandlingTableView: NSTableView { @objc func delete(_ sender: Any?) { guard coordinator?.isEditable == true else { return } + if let controller = gridSelection, !controller.isEmpty { + let rows = controller.selection.affectedRows + guard !rows.isEmpty else { return } + coordinator?.delegate?.dataGridDeleteRows(Set(rows)) + return + } guard !selectedRowIndexes.isEmpty else { return } coordinator?.delegate?.dataGridDeleteRows(Set(selectedRowIndexes)) } @@ -279,7 +285,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 +414,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 +528,19 @@ 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, selectedRowIndexes.contains(clickedRow) { + window?.makeFirstResponder(self) + if let menu = menu(for: event) { + NSMenu.popUpContextMenu(menu, with: event, for: self) + } + return + } + super.rightMouseDown(with: event) + } + override func menu(for event: NSEvent) -> NSMenu? { let point = convert(event.locationInWindow, from: nil) let clickedRow = row(at: point) From 6e51390ec46dd47a5c1b9580cd50bbefb96f010c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 15 Jun 2026 23:40:47 +0700 Subject: [PATCH 2/2] refactor(datagrid): handle cell-range selection in right-click and simplify delete --- CHANGELOG.md | 2 +- .../Views/Results/KeyHandlingTableView.swift | 18 ++++++++++++++---- .../Views/Results/CellSelectionTests.swift | 10 ++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc2c4469..b032124f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ 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 a row inside a multi-row selection no longer collapses the selection before the context menu appears. +- 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 ed6b6d3ec..365d0c7a9 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -223,9 +223,7 @@ final class KeyHandlingTableView: NSTableView { @objc func delete(_ sender: Any?) { guard coordinator?.isEditable == true else { return } if let controller = gridSelection, !controller.isEmpty { - let rows = controller.selection.affectedRows - guard !rows.isEmpty else { return } - coordinator?.delegate?.dataGridDeleteRows(Set(rows)) + coordinator?.delegate?.dataGridDeleteRows(Set(controller.selection.affectedRows)) return } guard !selectedRowIndexes.isEmpty else { return } @@ -531,7 +529,7 @@ final class KeyHandlingTableView: NSTableView { override func rightMouseDown(with event: NSEvent) { let point = convert(event.locationInWindow, from: nil) let clickedRow = row(at: point) - if clickedRow >= 0, selectedRowIndexes.contains(clickedRow) { + if clickedRow >= 0, clickIsInsideSelection(row: clickedRow, point: point) { window?.makeFirstResponder(self) if let menu = menu(for: event) { NSMenu.popUpContextMenu(menu, with: event, for: self) @@ -541,6 +539,18 @@ final class KeyHandlingTableView: NSTableView { 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(