Files
Terminal.Gui/Examples/UICatalog/Scenarios/CsvEditor.cs
Tig 0f72cf8a74 Fixes #4425 - ApplicationImpl internal (#4426)
* Pulled from v2_release

* Refactor migration guide for Terminal.Gui v2

Restructured and expanded the migration guide to provide a comprehensive resource for transitioning from Terminal.Gui v1 to v2. Key updates include:

- Added a Table of Contents for easier navigation.
- Summarized major architectural changes in v2, including the instance-based application model, IRunnable architecture, and 24-bit TrueColor support.
- Updated examples to reflect new patterns, such as initializers replacing constructors and explicit disposal using `IDisposable`.
- Documented changes to the layout system, including the removal of `Absolute`/`Computed` styles and the introduction of `Viewport`.
- Standardized event patterns to use `object sender, EventArgs args`.
- Detailed updates to the Keyboard, Mouse, and Navigation APIs, including configurable key bindings and viewport-relative mouse coordinates.
- Replaced legacy components like `ScrollView` and `ContextMenu` with built-in scrolling and `PopoverMenu`.
- Clarified disposal rules and introduced best practices for resource management.
- Provided a complete migration example and a summary of breaking changes.

This update aims to simplify the migration process by addressing breaking changes, introducing new features, and aligning with modern .NET conventions.

* Refactor to use Application.Instance for lifecycle management

Replaced all occurrences of `ApplicationImpl.Instance` with the new `Application.Instance` property across the codebase to align with the updated application lifecycle model.

Encapsulated the `ApplicationImpl` class by making it `internal`, ensuring it is no longer directly accessible outside its assembly. Introduced the `[Obsolete]` `Application.Instance` property as a backward-compatible singleton for the legacy static `Application` model, while encouraging the use of `Application.Create()` for new code.

Updated `MessageBox` methods to use `Application.Instance` for consistent modal dialog management. Improved documentation to reflect these changes and emphasize the transition to the instance-based application model.

Performed code cleanup in multiple classes to ensure consistency and maintainability. These changes maintain backward compatibility while preparing the codebase for the eventual removal of the legacy `ApplicationImpl` class.

* Fix doc bug

* - Removed obsolete `.cd` class diagram files.
- Introduced `IRunnable` interface for decoupling component execution.
- Added fluent API for running dialogs and retrieving results.
- Enhanced `View` with `App` and `Driver` properties for better decoupling.
- Improved testability with support for mock and real applications.
- Implemented `IDisposable` for proper resource cleanup.
- Replaced `RunnableSessionStack` with `SessionStack` for session management.
- Updated driver architecture to align with the new model.
- Scoped `IKeyboard` to application contexts for modularity.
- Updated documentation with migration strategies and best practices.

These changes modernize the library, improve maintainability, and align with current development practices.
2025-12-01 14:40:31 -07:00

759 lines
24 KiB
C#

#nullable enable
using System.Data;
using System.Globalization;
using System.Text.RegularExpressions;
using CsvHelper;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("Csv Editor", "Open and edit simple CSV files using the TableView class.")]
[ScenarioCategory ("TableView")]
[ScenarioCategory ("TextView")]
[ScenarioCategory ("Controls")]
[ScenarioCategory ("Dialogs")]
[ScenarioCategory ("Text and Formatting")]
[ScenarioCategory ("Arrangement")]
[ScenarioCategory ("Files and IO")]
public class CsvEditor : Scenario
{
private string? _currentFile;
private DataTable? _currentTable;
private CheckBox? _miCenteredCheckBox;
private CheckBox? _miLeftCheckBox;
private CheckBox? _miRightCheckBox;
private TextField? _selectedCellTextField;
private TableView? _tableView;
public override void Main ()
{
Application.Init ();
Window appWindow = new ()
{
Title = GetName ()
};
// MenuBar
MenuBar menu = new ();
_tableView = new ()
{
X = 0,
Y = Pos.Bottom (menu),
Width = Dim.Fill (),
Height = Dim.Fill (1)
};
_selectedCellTextField = new ()
{
Text = "0,0",
Width = 10,
Height = 1
};
_selectedCellTextField.TextChanged += SelectedCellLabel_TextChanged;
// StatusBar
StatusBar statusBar = new (
[
new (Application.QuitKey, "Quit", Quit, "Quit!"),
new (Key.O.WithCtrl, "Open", Open, "Open a file."),
new (Key.S.WithCtrl, "Save", Save, "Save current."),
new ()
{
HelpText = "Cell:",
CommandView = _selectedCellTextField,
AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast,
Enabled = false
}
]
)
{
AlignmentModes = AlignmentModes.IgnoreFirstOrLast
};
// Setup menu checkboxes for alignment
_miLeftCheckBox = new ()
{
Title = "_Align Left"
};
_miLeftCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.Start);
_miRightCheckBox = new ()
{
Title = "_Align Right"
};
_miRightCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.End);
_miCenteredCheckBox = new ()
{
Title = "_Align Centered"
};
_miCenteredCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.Center);
MenuBarItem fileMenu = new (
"_File",
[
new MenuItem
{
Title = "_Open CSV",
Action = Open
},
new MenuItem
{
Title = "_Save",
Action = Save
},
new MenuItem
{
Title = "_Quit",
HelpText = "Quits The App",
Action = Quit
}
]
);
menu.Add (fileMenu);
menu.Add (
new MenuBarItem (
"_Edit",
[
new MenuItem
{
Title = "_New Column",
Action = AddColumn
},
new MenuItem
{
Title = "_New Row",
Action = AddRow
},
new MenuItem
{
Title = "_Rename Column",
Action = RenameColumn
},
new MenuItem
{
Title = "_Delete Column",
Action = DeleteColum
},
new MenuItem
{
Title = "_Move Column",
Action = MoveColumn
},
new MenuItem
{
Title = "_Move Row",
Action = MoveRow
},
new MenuItem
{
Title = "_Sort Asc",
Action = () => Sort (true)
},
new MenuItem
{
Title = "_Sort Desc",
Action = () => Sort (false)
}
]
)
);
menu.Add (
new MenuBarItem (
"_View",
[
new MenuItem
{
CommandView = _miLeftCheckBox
},
new MenuItem
{
CommandView = _miRightCheckBox
},
new MenuItem
{
CommandView = _miCenteredCheckBox
},
new MenuItem
{
Title = "_Set Format Pattern",
Action = SetFormat
}
]
)
);
appWindow.Add (menu, _tableView, statusBar);
_tableView.SelectedCellChanged += OnSelectedCellChanged;
_tableView.CellActivated += EditCurrentCell;
_tableView.KeyDown += TableViewKeyPress;
Application.Run (appWindow);
appWindow.Dispose ();
Application.Shutdown ();
}
private void AddColumn ()
{
if (NoTableLoaded () || _tableView is null || _currentTable is null)
{
return;
}
if (GetText ("Enter column name", "Name:", "", out string colName))
{
DataColumn col = new (colName);
int newColIdx = Math.Min (
Math.Max (0, _tableView.SelectedColumn + 1),
_tableView.Table.Columns
);
int? result = MessageBox.Query (Application.Instance,
"Column Type",
"Pick a data type for the column",
"Date",
"Integer",
"Double",
"Text",
"Cancel"
);
if (result is null || result >= 4)
{
return;
}
switch (result)
{
case 0:
col.DataType = typeof (DateTime);
break;
case 1:
col.DataType = typeof (int);
break;
case 2:
col.DataType = typeof (double);
break;
case 3:
col.DataType = typeof (string);
break;
}
_currentTable.Columns.Add (col);
col.SetOrdinal (newColIdx);
_tableView.Update ();
}
}
private void AddRow ()
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
DataRow newRow = _currentTable.NewRow ();
int newRowIdx = Math.Min (Math.Max (0, _tableView.SelectedRow + 1), _tableView.Table.Rows);
_currentTable.Rows.InsertAt (newRow, newRowIdx);
_tableView.Update ();
}
private void Align (Alignment newAlignment)
{
if (NoTableLoaded () || _tableView is null)
{
return;
}
ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (_tableView.SelectedColumn);
style.Alignment = newAlignment;
if (_miLeftCheckBox is { })
{
_miLeftCheckBox.CheckedState = style.Alignment == Alignment.Start ? CheckState.Checked : CheckState.UnChecked;
}
if (_miRightCheckBox is { })
{
_miRightCheckBox.CheckedState = style.Alignment == Alignment.End ? CheckState.Checked : CheckState.UnChecked;
}
if (_miCenteredCheckBox is { })
{
_miCenteredCheckBox.CheckedState = style.Alignment == Alignment.Center ? CheckState.Checked : CheckState.UnChecked;
}
_tableView.Update ();
}
private void DeleteColum ()
{
if (NoTableLoaded () || _tableView is null || _currentTable is null)
{
return;
}
if (_tableView.SelectedColumn == -1)
{
MessageBox.ErrorQuery (Application.Instance, "No Column", "No column selected", "Ok");
return;
}
try
{
_currentTable.Columns.RemoveAt (_tableView.SelectedColumn);
_tableView.Update ();
}
catch (Exception ex)
{
MessageBox.ErrorQuery (Application.Instance, "Could not remove column", ex.Message, "Ok");
}
}
private void EditCurrentCell (object? sender, CellActivatedEventArgs e)
{
if (e.Table is null || _currentTable is null || _tableView is null)
{
return;
}
var oldValue = _currentTable.Rows [e.Row] [e.Col].ToString ();
if (GetText ("Enter new value", _currentTable.Columns [e.Col].ColumnName, oldValue ?? "", out string newText))
{
try
{
_currentTable.Rows [e.Row] [e.Col] =
string.IsNullOrWhiteSpace (newText) ? DBNull.Value : newText;
}
catch (Exception ex)
{
MessageBox.ErrorQuery (Application.Instance, 60, 20, "Failed to set text", ex.Message, "Ok");
}
_tableView.Update ();
}
}
private bool GetText (string title, string label, string initialText, out string enteredText)
{
var okPressed = false;
Button ok = new () { Text = "Ok", IsDefault = true };
ok.Accepting += (s, e) =>
{
okPressed = true;
Application.RequestStop ();
};
Button cancel = new () { Text = "Cancel" };
cancel.Accepting += (s, e) => { Application.RequestStop (); };
Dialog d = new () { Title = title, Buttons = [ok, cancel] };
Label lbl = new () { X = 0, Y = 1, Text = label };
TextField tf = new () { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () };
d.Add (lbl, tf);
tf.SetFocus ();
Application.Run (d);
d.Dispose ();
enteredText = okPressed ? tf.Text : string.Empty;
return okPressed;
}
private void MoveColumn ()
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
if (_tableView.SelectedColumn == -1)
{
MessageBox.ErrorQuery (Application.Instance, "No Column", "No column selected", "Ok");
return;
}
try
{
DataColumn currentCol = _currentTable.Columns [_tableView.SelectedColumn];
if (GetText ("Move Column", "New Index:", currentCol.Ordinal.ToString (), out string newOrdinal))
{
int newIdx = Math.Min (
Math.Max (0, int.Parse (newOrdinal)),
_tableView.Table.Columns - 1
);
currentCol.SetOrdinal (newIdx);
_tableView.SetSelection (newIdx, _tableView.SelectedRow, false);
_tableView.EnsureSelectedCellIsVisible ();
_tableView.SetNeedsDraw ();
}
}
catch (Exception ex)
{
MessageBox.ErrorQuery (Application.Instance, "Error moving column", ex.Message, "Ok");
}
}
private void MoveRow ()
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
if (_tableView.SelectedRow == -1)
{
MessageBox.ErrorQuery (Application.Instance, "No Rows", "No row selected", "Ok");
return;
}
try
{
int oldIdx = _tableView.SelectedRow;
DataRow currentRow = _currentTable.Rows [oldIdx];
if (GetText ("Move Row", "New Row:", oldIdx.ToString (), out string newOrdinal))
{
int newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), _tableView.Table.Rows - 1);
if (newIdx == oldIdx)
{
return;
}
object? [] arrayItems = currentRow.ItemArray;
_currentTable.Rows.Remove (currentRow);
// Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance
DataRow newRow = _currentTable.NewRow ();
newRow.ItemArray = arrayItems;
_currentTable.Rows.InsertAt (newRow, newIdx);
_tableView.SetSelection (_tableView.SelectedColumn, newIdx, false);
_tableView.EnsureSelectedCellIsVisible ();
_tableView.SetNeedsDraw ();
}
}
catch (Exception ex)
{
MessageBox.ErrorQuery (Application.Instance, "Error moving column", ex.Message, "Ok");
}
}
private bool NoTableLoaded ()
{
if (_tableView?.Table is null)
{
MessageBox.ErrorQuery (Application.Instance, "No Table Loaded", "No table has currently be opened", "Ok");
return true;
}
return false;
}
private void OnSelectedCellChanged (object? sender, SelectedCellChangedEventArgs e)
{
if (_selectedCellTextField is null || _tableView is null)
{
return;
}
// only update the text box if the user is not manually editing it
if (!_selectedCellTextField.HasFocus)
{
_selectedCellTextField.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}";
}
if (_tableView.Table is null || _tableView.SelectedColumn == -1)
{
return;
}
ColumnStyle? style = _tableView.Style.GetColumnStyleIfAny (_tableView.SelectedColumn);
if (_miLeftCheckBox is { })
{
_miLeftCheckBox.CheckedState = style?.Alignment == Alignment.Start ? CheckState.Checked : CheckState.UnChecked;
}
if (_miRightCheckBox is { })
{
_miRightCheckBox.CheckedState = style?.Alignment == Alignment.End ? CheckState.Checked : CheckState.UnChecked;
}
if (_miCenteredCheckBox is { })
{
_miCenteredCheckBox.CheckedState = style?.Alignment == Alignment.Center ? CheckState.Checked : CheckState.UnChecked;
}
}
private void Open ()
{
FileDialog ofd = new ()
{
AllowedTypes = [new AllowedType ("Comma Separated Values", ".csv")]
};
ofd.Style.OkButtonText = "Open";
Application.Run (ofd);
if (!ofd.Canceled && !string.IsNullOrWhiteSpace (ofd.Path))
{
Open (ofd.Path);
}
ofd.Dispose ();
}
private void Open (string filename)
{
var lineNumber = 0;
_currentFile = null;
try
{
using CsvReader reader = new (File.OpenText (filename), CultureInfo.InvariantCulture);
DataTable dt = new ();
reader.Read ();
if (reader.ReadHeader () && reader.HeaderRecord is { })
{
foreach (string h in reader.HeaderRecord)
{
dt.Columns.Add (h);
}
}
while (reader.Read ())
{
lineNumber++;
DataRow newRow = dt.Rows.Add ();
for (var i = 0; i < dt.Columns.Count; i++)
{
newRow [i] = reader [i];
}
}
SetTable (dt);
// Only set the current filename if we successfully loaded the entire file
_currentFile = filename;
if (_selectedCellTextField?.SuperView is { })
{
_selectedCellTextField.SuperView.Enabled = true;
}
if (Application.TopRunnableView is { })
{
Application.TopRunnableView.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}";
}
}
catch (Exception ex)
{
MessageBox.ErrorQuery (Application.Instance,
"Open Failed",
$"Error on line {lineNumber}{Environment.NewLine}{ex.Message}",
"Ok"
);
}
}
private void Quit () { Application.RequestStop (); }
private void RenameColumn ()
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
DataColumn currentCol = _currentTable.Columns [_tableView.SelectedColumn];
if (GetText ("Rename Column", "Name:", currentCol.ColumnName, out string newName))
{
currentCol.ColumnName = newName;
_tableView.Update ();
}
}
private void Save ()
{
if (_tableView?.Table is null || string.IsNullOrWhiteSpace (_currentFile) || _currentTable is null)
{
MessageBox.ErrorQuery (Application.Instance, "No file loaded", "No file is currently loaded", "Ok");
return;
}
using CsvWriter writer = new (
new StreamWriter (File.OpenWrite (_currentFile)),
CultureInfo.InvariantCulture
);
foreach (string col in _currentTable.Columns.Cast<DataColumn> ().Select (c => c.ColumnName))
{
writer.WriteField (col);
}
writer.NextRecord ();
foreach (DataRow row in _currentTable.Rows)
{
foreach (object? item in row.ItemArray)
{
writer.WriteField (item);
}
writer.NextRecord ();
}
}
private void SelectedCellLabel_TextChanged (object? sender, EventArgs e)
{
if (_selectedCellTextField is null || _tableView is null)
{
return;
}
// if user is in the text control and editing the selected cell
if (!_selectedCellTextField.HasFocus)
{
return;
}
// change selected cell to the one the user has typed into the box
Match match = Regex.Match (_selectedCellTextField.Text, "^(\\d+),(\\d+)$");
if (match.Success)
{
_tableView.SelectedColumn = int.Parse (match.Groups [2].Value);
_tableView.SelectedRow = int.Parse (match.Groups [1].Value);
}
}
private void SetFormat ()
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
DataColumn col = _currentTable.Columns [_tableView.SelectedColumn];
if (col.DataType == typeof (string))
{
MessageBox.ErrorQuery (Application.Instance,
"Cannot Format Column",
"String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type",
"Ok"
);
return;
}
ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
if (GetText ("Format", "Pattern:", style.Format ?? "", out string newPattern))
{
style.Format = newPattern;
_tableView.Update ();
}
}
private void SetTable (DataTable dataTable)
{
if (_tableView is null)
{
return;
}
_tableView.Table = new DataTableSource (_currentTable = dataTable);
}
private void Sort (bool asc)
{
if (NoTableLoaded () || _currentTable is null || _tableView is null)
{
return;
}
if (_tableView.SelectedColumn == -1)
{
MessageBox.ErrorQuery (Application.Instance, "No Column", "No column selected", "Ok");
return;
}
string colName = _tableView.Table.ColumnNames [_tableView.SelectedColumn];
_currentTable.DefaultView.Sort = colName + (asc ? " asc" : " desc");
SetTable (_currentTable.DefaultView.ToTable ());
}
private void TableViewKeyPress (object? sender, Key e)
{
if (_currentTable is null || _tableView is null)
{
return;
}
if (e.KeyCode == Key.Delete)
{
if (_tableView.FullRowSelect)
{
// Delete button deletes all rows when in full row mode
foreach (int toRemove in _tableView.GetAllSelectedCells ()
.Select (p => p.Y)
.Distinct ()
.OrderByDescending (i => i))
{
_currentTable.Rows.RemoveAt (toRemove);
}
}
else
{
// otherwise set all selected cells to null
foreach (Point pt in _tableView.GetAllSelectedCells ())
{
_currentTable.Rows [pt.Y] [pt.X] = DBNull.Value;
}
}
_tableView.Update ();
e.Handled = true;
}
}
}