Skip to content

Commit 731cb9c

Browse files
committed
Add batch operations support to TUI editor
Implement multi-selection and bulk operations in the Terminal UI editor, allowing users to select multiple keys and perform operations on them simultaneously. Features: - Multi-selection with keyboard controls (Space, Ctrl+A, Esc) - Range selection with Shift+Up/Down - Visual selection indicator (► marker before key name) - Selection count display in status bar - Selection persistence across table rebuilds and filters - Bulk translate: translate multiple selected keys at once - Bulk delete: delete multiple selected keys with single confirmation Implementation: - Add ResourceEditorWindow.BatchOperations.cs (311 lines) - ToggleCurrentRowSelection(): Space key toggles individual rows - SelectAll(): Ctrl+A selects all visible keys - ClearSelection(): Esc clears all selections - ExtendSelectionUp/Down(): Shift+arrows for range selection - SelectRange(): Helper for range-based selection - BulkTranslate(): Translate all selected keys - BulkDelete(): Delete all selected keys with backup - IsRowSelected(), IsEntrySelected(): Selection state helpers - RebuildSelectionIndices(): Maintain selection across rebuilds - Multi-selection state tracking: - _selectedRowIndices: HashSet<int> for current row indices - _selectedEntries: Dictionary<string, EntryReference> for persistence - _selectionAnchor: int for Shift+selection range tracking - UI updates: - Add ► marker to selected rows in BuildDataTable() - Update status bar to show selection count - Add keyboard shortcuts (Space, Ctrl+A, Esc, Shift+Up/Down) - Add menu items: Select All, Clear Selection, Bulk Translate, Bulk Delete - Selection persistence: - Selections persist across search, filter, and table rebuilds - Uses DisplayKey mapping to maintain selection state - RebuildSelectionIndices() re-maps entries after table changes Documentation: - Update docs/TUI.md with batch operations section - Keyboard shortcuts table - Selection workflow documentation - Bulk operation examples - Two new workflow examples (bulk translate, cleanup unused keys) - Update ROADMAP.md to mark batch operations as completed Testing: - Build successful: 0 errors, 0 warnings - All 488 tests passing (100% pass rate) - Tested multi-selection, bulk translate, bulk delete workflows
1 parent abfc43c commit 731cb9c

8 files changed

Lines changed: 472 additions & 9 deletions

ROADMAP.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -520,12 +520,12 @@
520520
- [x] Operation descriptions in menu (e.g., "Undo: Edit 'HelloWorld' in en")
521521
- [x] Max history size: 50 operations (configurable)
522522

523-
- [ ] Batch Operations - **DEFERRED** (complex feature for future release)
524-
- [ ] Multi-select rows (Shift+Up/Down, Ctrl+Click)
525-
- [ ] Bulk translate selected keys
526-
- [ ] Bulk delete selected keys
527-
- [ ] Visual indication of selected rows
528-
- [ ] "Select All" (Ctrl+A) and "Deselect All"
523+
- [x] Batch Operations - **COMPLETED**
524+
- [x] Multi-select rows (Space, Shift+Up/Down for range selection)
525+
- [x] Bulk translate selected keys
526+
- [x] Bulk delete selected keys
527+
- [x] Visual indication of selected rows (► marker)
528+
- [x] "Select All" (Ctrl+A) and "Clear Selection" (Esc)
529529

530530
- [ ] Export Filtered View - **DEFERRED** (CLI export command already exists)
531531
- [ ] Add "Export Current View" option (Ctrl+E)
@@ -1013,10 +1013,10 @@ None
10131013
- ✅ Undo/Redo system (UI/OperationHistory.cs with Ctrl+Z/Ctrl+Y)
10141014
- ✅ OperationHistoryTests.cs (15 comprehensive unit tests)
10151015
- ✅ All 488 tests passing (100% pass rate)
1016-
- ✅ Comprehensive docs/TUI.md (500+ lines with all features, shortcuts, examples)
1016+
- ✅ Comprehensive docs/TUI.md (600+ lines with all features, shortcuts, examples)
10171017
- ✅ Updated README.md and ROADMAP.md
10181018
- ✅ Code refactoring - COMPLETED (ResourceEditorWindow split into 7 partial classes)
1019-
- ⏭️ Batch operations - DEFERRED (complex feature for future)
1019+
- Batch operations - COMPLETED (multi-select with Space/Ctrl+A, bulk translate/delete)
10201020
- ⏭️ Export filtered view - DEFERRED (CLI export exists)
10211021

10221022
**Next Milestone:** Phase 5 - Simple CLI Chaining
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
// Copyright (c) 2025 Nikolaos Protopapas
2+
// Licensed under the MIT License
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in all
12+
// copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
// SOFTWARE.
21+
22+
using System.Data;
23+
using System.Timers;
24+
using LocalizationManager.Core;
25+
using LocalizationManager.Core.Backup;
26+
using LocalizationManager.Core.Configuration;
27+
using LocalizationManager.Core.Models;
28+
using LocalizationManager.Core.Scanning;
29+
using LocalizationManager.Core.Scanning.Models;
30+
using LocalizationManager.Core.Translation;
31+
using LocalizationManager.UI.Filters;
32+
using Terminal.Gui;
33+
34+
namespace LocalizationManager.UI;
35+
36+
/// <summary>
37+
/// Batch Operations
38+
/// </summary>
39+
public partial class ResourceEditorWindow : Window
40+
{
41+
/// <summary>
42+
/// Toggles selection for the current row
43+
/// </summary>
44+
private void ToggleCurrentRowSelection()
45+
{
46+
if (_tableView == null || _tableView.SelectedRow < 0)
47+
return;
48+
49+
var rowIndex = _tableView.SelectedRow;
50+
var entryRef = GetEntryReferenceFromSelectedRow(rowIndex);
51+
if (entryRef == null)
52+
return;
53+
54+
var displayKey = entryRef.DisplayKey;
55+
56+
if (_selectedEntries.ContainsKey(displayKey))
57+
{
58+
_selectedEntries.Remove(displayKey);
59+
_selectedRowIndices.Remove(rowIndex);
60+
}
61+
else
62+
{
63+
_selectedEntries[displayKey] = entryRef;
64+
_selectedRowIndices.Add(rowIndex);
65+
_selectionAnchor = rowIndex;
66+
}
67+
68+
UpdateStatus();
69+
RebuildTable(); // Refresh to show selection markers
70+
}
71+
72+
/// <summary>
73+
/// Selects all visible rows
74+
/// </summary>
75+
private void SelectAll()
76+
{
77+
if (_tableView == null)
78+
return;
79+
80+
_selectedRowIndices.Clear();
81+
_selectedEntries.Clear();
82+
83+
// Select all rows in the current table view
84+
for (int i = 0; i < _tableView.Table.Rows.Count; i++)
85+
{
86+
var entryRef = GetEntryReferenceFromSelectedRow(i);
87+
if (entryRef != null)
88+
{
89+
_selectedRowIndices.Add(i);
90+
_selectedEntries[entryRef.DisplayKey] = entryRef;
91+
}
92+
}
93+
94+
if (_selectedRowIndices.Any())
95+
{
96+
_selectionAnchor = 0;
97+
}
98+
99+
UpdateStatus();
100+
RebuildTable();
101+
}
102+
103+
/// <summary>
104+
/// Clears all selections
105+
/// </summary>
106+
private void ClearSelection()
107+
{
108+
_selectedRowIndices.Clear();
109+
_selectedEntries.Clear();
110+
_selectionAnchor = -1;
111+
UpdateStatus();
112+
RebuildTable();
113+
}
114+
115+
/// <summary>
116+
/// Extends selection upward from current position
117+
/// </summary>
118+
private void ExtendSelectionUp()
119+
{
120+
if (_tableView == null || _tableView.SelectedRow < 0)
121+
return;
122+
123+
var currentRow = _tableView.SelectedRow;
124+
125+
// Set anchor if not set
126+
if (_selectionAnchor < 0)
127+
{
128+
_selectionAnchor = currentRow;
129+
}
130+
131+
// Move selection up one row
132+
if (currentRow > 0)
133+
{
134+
var newRow = currentRow - 1;
135+
_tableView.SelectedRow = newRow;
136+
137+
// Select range from anchor to new position
138+
SelectRange(_selectionAnchor, newRow);
139+
}
140+
}
141+
142+
/// <summary>
143+
/// Extends selection downward from current position
144+
/// </summary>
145+
private void ExtendSelectionDown()
146+
{
147+
if (_tableView == null || _tableView.SelectedRow < 0)
148+
return;
149+
150+
var currentRow = _tableView.SelectedRow;
151+
152+
// Set anchor if not set
153+
if (_selectionAnchor < 0)
154+
{
155+
_selectionAnchor = currentRow;
156+
}
157+
158+
// Move selection down one row
159+
if (currentRow < _tableView.Table.Rows.Count - 1)
160+
{
161+
var newRow = currentRow + 1;
162+
_tableView.SelectedRow = newRow;
163+
164+
// Select range from anchor to new position
165+
SelectRange(_selectionAnchor, newRow);
166+
}
167+
}
168+
169+
/// <summary>
170+
/// Selects a range of rows between start and end (inclusive)
171+
/// </summary>
172+
private void SelectRange(int start, int end)
173+
{
174+
_selectedRowIndices.Clear();
175+
_selectedEntries.Clear();
176+
177+
var minRow = Math.Min(start, end);
178+
var maxRow = Math.Max(start, end);
179+
180+
for (int i = minRow; i <= maxRow; i++)
181+
{
182+
var entryRef = GetEntryReferenceFromSelectedRow(i);
183+
if (entryRef != null)
184+
{
185+
_selectedRowIndices.Add(i);
186+
_selectedEntries[entryRef.DisplayKey] = entryRef;
187+
}
188+
}
189+
190+
UpdateStatus();
191+
RebuildTable();
192+
}
193+
194+
/// <summary>
195+
/// Gets the list of selected EntryReferences
196+
/// </summary>
197+
private List<EntryReference> GetSelectedEntries()
198+
{
199+
return _selectedEntries.Values.ToList();
200+
}
201+
202+
/// <summary>
203+
/// Bulk translate selected keys
204+
/// </summary>
205+
private void BulkTranslate()
206+
{
207+
var selectedEntries = GetSelectedEntries();
208+
209+
if (!selectedEntries.Any())
210+
{
211+
MessageBox.ErrorQuery("No Selection", "No keys selected. Select keys using Space or Ctrl+A.", "OK");
212+
return;
213+
}
214+
215+
var keysToTranslate = selectedEntries.Select(e => e.DisplayKey).ToList();
216+
ShowTranslateDialog(keysToTranslate);
217+
}
218+
219+
/// <summary>
220+
/// Bulk delete selected keys
221+
/// </summary>
222+
private void BulkDelete()
223+
{
224+
var selectedEntries = GetSelectedEntries();
225+
226+
if (!selectedEntries.Any())
227+
{
228+
MessageBox.ErrorQuery("No Selection", "No keys selected. Select keys using Space or Ctrl+A.", "OK");
229+
return;
230+
}
231+
232+
var keyCount = selectedEntries.Count;
233+
var uniqueKeys = selectedEntries.Select(e => e.Key).Distinct().Count();
234+
235+
var result = MessageBox.Query(
236+
"Confirm Bulk Delete",
237+
$"Delete {keyCount} selected entries ({uniqueKeys} unique keys)?",
238+
"Delete", "Cancel"
239+
);
240+
241+
if (result == 0) // Delete
242+
{
243+
try
244+
{
245+
// Create backup before bulk delete
246+
var backupManager = new BackupVersionManager(10);
247+
var basePath = Path.GetDirectoryName(_resourceFiles.First().Language.FilePath) ?? Environment.CurrentDirectory;
248+
foreach (var rf in _resourceFiles)
249+
{
250+
backupManager.CreateBackupAsync(rf.Language.FilePath, "tui-bulk-delete", basePath)
251+
.GetAwaiter().GetResult();
252+
}
253+
254+
// Delete each selected entry
255+
foreach (var entryRef in selectedEntries)
256+
{
257+
DeleteSpecificOccurrence(entryRef.Key, entryRef.OccurrenceNumber);
258+
}
259+
260+
_hasUnsavedChanges = true;
261+
ClearSelection();
262+
BuildEntryReferences();
263+
RebuildTable();
264+
UpdateStatus();
265+
266+
MessageBox.Query("Success", $"Deleted {keyCount} entries.", "OK");
267+
}
268+
catch (Exception ex)
269+
{
270+
MessageBox.ErrorQuery("Error", $"Failed to delete entries: {ex.Message}", "OK");
271+
}
272+
}
273+
}
274+
275+
/// <summary>
276+
/// Checks if a row index is selected
277+
/// </summary>
278+
private bool IsRowSelected(int rowIndex)
279+
{
280+
return _selectedRowIndices.Contains(rowIndex);
281+
}
282+
283+
/// <summary>
284+
/// Rebuilds the row index mapping after table rebuild
285+
/// Maps _selectedEntries back to current row indices
286+
/// </summary>
287+
private void RebuildSelectionIndices()
288+
{
289+
_selectedRowIndices.Clear();
290+
291+
if (_tableView == null || !_selectedEntries.Any())
292+
return;
293+
294+
// Find current row indices for each selected entry
295+
for (int i = 0; i < _tableView.Table.Rows.Count; i++)
296+
{
297+
var entryRef = GetEntryReferenceFromSelectedRow(i);
298+
if (entryRef != null && _selectedEntries.ContainsKey(entryRef.DisplayKey))
299+
{
300+
_selectedRowIndices.Add(i);
301+
}
302+
}
303+
}
304+
305+
/// <summary>
306+
/// Checks if an EntryReference is selected
307+
/// </summary>
308+
private bool IsEntrySelected(EntryReference entryRef)
309+
{
310+
return _selectedEntries.ContainsKey(entryRef.DisplayKey);
311+
}
312+
}

UI/ResourceEditorWindow/ResourceEditorWindow.Data.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,23 @@ private DataTable BuildDataTable()
8989
row[$"_Comment_{rf.Language.Code}"] = entry?.Comment ?? "";
9090
}
9191

92-
// Add status indicator to display key based on row status
92+
// Build display key with selection marker and status indicator
9393
var displayKey = entryRef.DisplayKey;
94+
95+
// Add selection marker if this entry is selected
96+
if (IsEntrySelected(entryRef))
97+
{
98+
displayKey = $"► {displayKey}";
99+
}
100+
101+
// Add status indicator based on row status
94102
var status = DetermineRowStatus(row);
95103
var statusIcon = GetStatusIcon(status);
96104
if (!string.IsNullOrEmpty(statusIcon))
97105
{
98106
displayKey = $"{statusIcon} {displayKey}";
99107
}
108+
100109
row["Key"] = displayKey;
101110

102111
dt.Rows.Add(row);
@@ -369,6 +378,9 @@ private void RebuildTable()
369378
}
370379

371380
FilterKeys();
381+
382+
// Rebuild selection indices to map selected entries to current row indices
383+
RebuildSelectionIndices();
372384
}
373385

374386
// Case-Insensitive Duplicate Detection

0 commit comments

Comments
 (0)