-
-
Notifications
You must be signed in to change notification settings - Fork 337
feat(inspector): insert rows and confirm data-bearing deletes in the CSV editor #1939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // | ||
| // InspectorDeleteConfirmation.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import AppKit | ||
|
|
||
| @MainActor | ||
| enum InspectorDeleteConfirmation { | ||
| static func rowsContainData(_ rowsCells: [[String]]) -> Bool { | ||
| rowsCells.contains { cells in cells.contains { !$0.isEmpty } } | ||
| } | ||
|
|
||
| static func rowDeleteTitle(count: Int) -> String { | ||
| count == 1 | ||
| ? String(localized: "Delete this row?") | ||
| : String(format: String(localized: "Delete %lld rows?"), count) | ||
| } | ||
|
|
||
| static func confirmDeleteRowsIfNeeded( | ||
| rowsCells: [[String]], | ||
| window: NSWindow?, | ||
| proceed: @escaping @MainActor () -> Void | ||
| ) { | ||
| guard rowsContainData(rowsCells) else { | ||
| proceed() | ||
| return | ||
| } | ||
| present(messageText: rowDeleteTitle(count: rowsCells.count), window: window, proceed: proceed) | ||
| } | ||
|
|
||
| private static func present( | ||
| messageText: String, | ||
| window: NSWindow?, | ||
| proceed: @escaping @MainActor () -> Void | ||
| ) { | ||
| guard let window else { | ||
| proceed() | ||
| return | ||
| } | ||
| let alert = NSAlert() | ||
| alert.messageText = messageText | ||
| alert.alertStyle = .warning | ||
| let deleteButton = alert.addButton(withTitle: String(localized: "Delete")) | ||
| deleteButton.hasDestructiveAction = true | ||
| alert.addButton(withTitle: String(localized: "Cancel")) | ||
| alert.beginSheetModal(for: window) { response in | ||
| guard response == .alertFirstButtonReturn else { return } | ||
| proceed() | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // | ||
| // InspectorRowInsertion.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum InspectorRowInsertion { | ||
| static func storeIndex(anchorDisplayRow: Int?, below: Bool, displayToStore: [Int], rowCount: Int) -> Int { | ||
| guard let displayRow = anchorDisplayRow, | ||
| displayRow >= 0, displayRow < displayToStore.count else { | ||
| return below ? rowCount : 0 | ||
| } | ||
| let storeRow = displayToStore[displayRow] | ||
| return below ? storeRow + 1 : storeRow | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // | ||
| // InspectorRowMenuBuilder.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import AppKit | ||
|
|
||
| @MainActor | ||
| enum InspectorRowMenuBuilder { | ||
| static func structureItems(forRow displayRow: Int) -> [NSMenuItem] { | ||
| [ | ||
| actionItem( | ||
| title: String(localized: "Insert Row Above"), | ||
| action: #selector(InspectorViewController.inspectorInsertRowAbove(_:)), | ||
| row: displayRow | ||
| ), | ||
| actionItem( | ||
| title: String(localized: "Insert Row Below"), | ||
| action: #selector(InspectorViewController.inspectorInsertRowBelow(_:)), | ||
| row: displayRow | ||
| ), | ||
| ] | ||
| } | ||
|
|
||
| private static func actionItem(title: String, action: Selector, row: Int) -> NSMenuItem { | ||
| let item = NSMenuItem(title: title, action: action, keyEquivalent: "") | ||
| item.tag = row | ||
| item.target = nil | ||
| return item | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,14 +143,14 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation | |
|
|
||
| let undoManager = nsDocument?.undoManager | ||
| undoManager?.beginUndoGrouping() | ||
| undoManager?.setActionName(String(localized: "Paste")) | ||
| for row in rows { | ||
| let newRowIndex = inspectorDocument.rowCount | ||
| inspectorDocument.appendRow() | ||
| for (column, value) in row.enumerated() { | ||
| inspectorDocument.setCell(row: newRowIndex, column: column, to: value) | ||
| } | ||
| } | ||
| undoManager?.setActionName(String(localized: "Paste")) | ||
| undoManager?.endUndoGrouping() | ||
| } | ||
|
|
||
|
|
@@ -162,8 +162,19 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation | |
| return displayToStore[index] | ||
| } | ||
| guard !storeIndices.isEmpty else { return } | ||
| pendingPostRefresh = .selectClamped(displayRow: sortedDisplay.first ?? 0) | ||
| inspectorDocument.removeRows(at: IndexSet(storeIndices)) | ||
| let columnCount = inspectorDocument.columnNames.count | ||
| let rowsCells = storeIndices.map { storeRow in | ||
| (0..<columnCount).map { inspectorDocument.value(row: storeRow, column: $0) } | ||
| } | ||
| let firstDisplayRow = sortedDisplay.first ?? 0 | ||
| InspectorDeleteConfirmation.confirmDeleteRowsIfNeeded( | ||
| rowsCells: rowsCells, | ||
| window: view.window | ||
| ) { [weak self] in | ||
| guard let self else { return } | ||
| self.pendingPostRefresh = .selectClamped(displayRow: firstDisplayRow) | ||
| self.inspectorDocument?.removeRows(at: IndexSet(storeIndices)) | ||
| } | ||
| } | ||
|
|
||
| fileprivate func handleSortChanged(_ newState: SortState) { | ||
|
|
@@ -206,6 +217,37 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation | |
| handleDeleteRows(state.selectedRowIndices) | ||
| } | ||
|
|
||
| @objc func inspectorInsertRowAbove(_ sender: Any?) { | ||
| performInsertRow(anchoredBy: sender, below: false) | ||
| } | ||
|
|
||
| @objc func inspectorInsertRowBelow(_ sender: Any?) { | ||
| performInsertRow(anchoredBy: sender, below: true) | ||
| } | ||
|
|
||
| private func performInsertRow(anchoredBy sender: Any?, below: Bool) { | ||
| guard let inspectorDocument else { return } | ||
| let storeIndex = insertStoreIndex(anchoredBy: sender, below: below) | ||
| pendingPostRefresh = .focusStoreIndex(storeIndex) | ||
| inspectorDocument.insertRow(at: storeIndex) | ||
| } | ||
|
|
||
| private func insertStoreIndex(anchoredBy sender: Any?, below: Bool) -> Int { | ||
| let anchorDisplayRow: Int? = if let item = sender as? NSMenuItem { | ||
| item.tag | ||
| } else if below { | ||
| state.selectedRowIndices.max() | ||
| } else { | ||
| state.selectedRowIndices.min() | ||
| } | ||
| return InspectorRowInsertion.storeIndex( | ||
| anchorDisplayRow: anchorDisplayRow, | ||
| below: below, | ||
| displayToStore: displayToStore, | ||
| rowCount: inspectorDocument?.rowCount ?? 0 | ||
| ) | ||
| } | ||
|
|
||
| @objc func inspectorAddColumn(_ sender: Any?) { | ||
| promptForColumnName(title: String(localized: "Add Column"), initial: "") { [weak self] name in | ||
| guard let self, let name, !name.isEmpty else { return } | ||
|
|
@@ -278,6 +320,11 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation | |
| ) | ||
| } | ||
|
|
||
| fileprivate func rowStructureMenuItems(forRow displayRow: Int) -> [NSMenuItem] { | ||
| guard inspectorDocument != nil else { return [] } | ||
| return InspectorRowMenuBuilder.structureItems(forRow: displayRow) | ||
| } | ||
|
|
||
| private func renameLayoutKey(from oldName: String, to newName: String) { | ||
| if state.columnLayout.columnOrder != nil { | ||
| state.columnLayout.columnOrder = state.columnLayout.columnOrder?.map { $0 == oldName ? newName : $0 } | ||
|
|
@@ -350,7 +397,8 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation | |
| case #selector(redo(_:)): | ||
| return nsDocument?.undoManager?.canRedo ?? false | ||
| case #selector(saveDocument(_:)), #selector(saveDocumentAs(_:)), | ||
| #selector(toggleInspectorFilter(_:)), #selector(inspectorAddRow(_:)): | ||
| #selector(toggleInspectorFilter(_:)), #selector(inspectorAddRow(_:)), | ||
| #selector(inspectorInsertRowAbove(_:)), #selector(inspectorInsertRowBelow(_:)): | ||
| return nsDocument != nil | ||
|
Comment on lines
399
to
402
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Edit-menu insert commands validate whenever an inspector document exists, even with no selected row. In that state Useful? React with 👍 / 👎. |
||
| case #selector(inspectorDeleteSelectedRows(_:)): | ||
| return !state.selectedRowIndices.isEmpty | ||
|
|
@@ -701,6 +749,10 @@ private final class InspectorGridDelegate: DataGridViewDelegate { | |
| owner?.columnStructureMenuItems(forColumn: dataColumnIndex) ?? [] | ||
| } | ||
|
|
||
| func dataGridRowStructureMenuItems(forRow displayRow: Int) -> [NSMenuItem] { | ||
| owner?.rowStructureMenuItems(forRow: displayRow) ?? [] | ||
| } | ||
|
|
||
| func dataGridUndo() { | ||
| owner?.handleUndo() | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../Plugins/CSVInspectorPlugin/CSVDocument.swift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial undo label is now restored after the paste loop, but undoing a multi-row paste executes each
appendRowundo handler, which calls this method and changes the redo group's action name toAdd Row. Consequently, afterUndo Paste, the Edit menu presents the next action as “Redo Add Row” rather than “Redo Paste”; the enclosing paste group needs to retain or restore its name while its inverse actions are registered.Useful? React with 👍 / 👎.