CsvEditor.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. using System;
  2. using System.Data;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text.RegularExpressions;
  7. using CsvHelper;
  8. using Terminal.Gui;
  9. namespace UICatalog.Scenarios;
  10. [ScenarioMetadata ("Csv Editor", "Open and edit simple CSV files using the TableView class.")]
  11. [ScenarioCategory ("TableView")]
  12. [ScenarioCategory ("TextView")]
  13. [ScenarioCategory ("Controls")]
  14. [ScenarioCategory ("Dialogs")]
  15. [ScenarioCategory ("Text and Formatting")]
  16. [ScenarioCategory ("Dialogs")]
  17. [ScenarioCategory ("Top Level Windows")]
  18. [ScenarioCategory ("Files and IO")]
  19. public class CsvEditor : Scenario
  20. {
  21. private string _currentFile;
  22. private DataTable _currentTable;
  23. private MenuItem _miCentered;
  24. private MenuItem _miLeft;
  25. private MenuItem _miRight;
  26. private TextField _selectedCellTextField;
  27. private TableView _tableView;
  28. public override void Main ()
  29. {
  30. // Init
  31. Application.Init ();
  32. // Setup - Create a top-level application window and configure it.
  33. Toplevel appWindow = new ()
  34. {
  35. Title = $"{GetName ()}"
  36. };
  37. //appWindow.Height = Dim.Fill (1); // status bar
  38. _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (2) };
  39. var fileMenu = new MenuBarItem (
  40. "_File",
  41. new MenuItem []
  42. {
  43. new ("_Open CSV", "", () => Open ()),
  44. new ("_Save", "", () => Save ()),
  45. new ("_Quit", "Quits The App", () => Quit ())
  46. }
  47. );
  48. //fileMenu.Help = "Help";
  49. var menu = new MenuBar
  50. {
  51. Menus =
  52. [
  53. fileMenu,
  54. new (
  55. "_Edit",
  56. new MenuItem []
  57. {
  58. new ("_New Column", "", () => AddColumn ()),
  59. new ("_New Row", "", () => AddRow ()),
  60. new (
  61. "_Rename Column",
  62. "",
  63. () => RenameColumn ()
  64. ),
  65. new ("_Delete Column", "", () => DeleteColum ()),
  66. new ("_Move Column", "", () => MoveColumn ()),
  67. new ("_Move Row", "", () => MoveRow ()),
  68. new ("_Sort Asc", "", () => Sort (true)),
  69. new ("_Sort Desc", "", () => Sort (false))
  70. }
  71. ),
  72. new (
  73. "_View",
  74. new []
  75. {
  76. _miLeft = new (
  77. "_Align Left",
  78. "",
  79. () => Align (Alignment.Start)
  80. ),
  81. _miRight = new (
  82. "_Align Right",
  83. "",
  84. () => Align (Alignment.End)
  85. ),
  86. _miCentered = new (
  87. "_Align Centered",
  88. "",
  89. () => Align (Alignment.Center)
  90. ),
  91. // Format requires hard typed data table, when we read a CSV everything is untyped (string) so this only works for new columns in this demo
  92. _miCentered = new (
  93. "_Set Format Pattern",
  94. "",
  95. () => SetFormat ()
  96. )
  97. }
  98. )
  99. ]
  100. };
  101. appWindow.Add (menu);
  102. _selectedCellTextField = new ()
  103. {
  104. Text = "0,0",
  105. Width = 10,
  106. Height = 1
  107. };
  108. _selectedCellTextField.TextChanged += SelectedCellLabel_TextChanged;
  109. var statusBar = new StatusBar (
  110. [
  111. new (Application.QuitKey, "Quit", Quit, "Quit!"),
  112. new (Key.O.WithCtrl, "Open", Open, "Open a file."),
  113. new (Key.S.WithCtrl, "Save", Save, "Save current."),
  114. new ()
  115. {
  116. HelpText = "Cell:",
  117. CommandView = _selectedCellTextField,
  118. AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast,
  119. Enabled = false
  120. }
  121. ])
  122. {
  123. AlignmentModes = AlignmentModes.IgnoreFirstOrLast
  124. };
  125. appWindow.Add (statusBar);
  126. appWindow.Add (_tableView);
  127. _tableView.SelectedCellChanged += OnSelectedCellChanged;
  128. _tableView.CellActivated += EditCurrentCell;
  129. _tableView.KeyDown += TableViewKeyPress;
  130. SetupScrollBar ();
  131. // Run - Start the application.
  132. Application.Run (appWindow);
  133. appWindow.Dispose ();
  134. // Shutdown - Calling Application.Shutdown is required.
  135. Application.Shutdown ();
  136. }
  137. private void AddColumn ()
  138. {
  139. if (NoTableLoaded ())
  140. {
  141. return;
  142. }
  143. if (GetText ("Enter column name", "Name:", "", out string colName))
  144. {
  145. var col = new DataColumn (colName);
  146. int newColIdx = Math.Min (
  147. Math.Max (0, _tableView.SelectedColumn + 1),
  148. _tableView.Table.Columns
  149. );
  150. int result = MessageBox.Query (
  151. "Column Type",
  152. "Pick a data type for the column",
  153. "Date",
  154. "Integer",
  155. "Double",
  156. "Text",
  157. "Cancel"
  158. );
  159. if (result <= -1 || result >= 4)
  160. {
  161. return;
  162. }
  163. switch (result)
  164. {
  165. case 0:
  166. col.DataType = typeof (DateTime);
  167. break;
  168. case 1:
  169. col.DataType = typeof (int);
  170. break;
  171. case 2:
  172. col.DataType = typeof (double);
  173. break;
  174. case 3:
  175. col.DataType = typeof (string);
  176. break;
  177. }
  178. _currentTable.Columns.Add (col);
  179. col.SetOrdinal (newColIdx);
  180. _tableView.Update ();
  181. }
  182. }
  183. private void AddRow ()
  184. {
  185. if (NoTableLoaded ())
  186. {
  187. return;
  188. }
  189. DataRow newRow = _currentTable.NewRow ();
  190. int newRowIdx = Math.Min (Math.Max (0, _tableView.SelectedRow + 1), _tableView.Table.Rows);
  191. _currentTable.Rows.InsertAt (newRow, newRowIdx);
  192. _tableView.Update ();
  193. }
  194. private void Align (Alignment newAlignment)
  195. {
  196. if (NoTableLoaded ())
  197. {
  198. return;
  199. }
  200. ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (_tableView.SelectedColumn);
  201. style.Alignment = newAlignment;
  202. _miLeft.Checked = style.Alignment == Alignment.Start;
  203. _miRight.Checked = style.Alignment == Alignment.End;
  204. _miCentered.Checked = style.Alignment == Alignment.Center;
  205. _tableView.Update ();
  206. }
  207. private void ClearColumnStyles ()
  208. {
  209. _tableView.Style.ColumnStyles.Clear ();
  210. _tableView.Update ();
  211. }
  212. private void CloseExample () { _tableView.Table = null; }
  213. private void DeleteColum ()
  214. {
  215. if (NoTableLoaded ())
  216. {
  217. return;
  218. }
  219. if (_tableView.SelectedColumn == -1)
  220. {
  221. MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
  222. return;
  223. }
  224. try
  225. {
  226. _currentTable.Columns.RemoveAt (_tableView.SelectedColumn);
  227. _tableView.Update ();
  228. }
  229. catch (Exception ex)
  230. {
  231. MessageBox.ErrorQuery ("Could not remove column", ex.Message, "Ok");
  232. }
  233. }
  234. private void EditCurrentCell (object sender, CellActivatedEventArgs e)
  235. {
  236. if (e.Table == null)
  237. {
  238. return;
  239. }
  240. var oldValue = _currentTable.Rows [e.Row] [e.Col].ToString ();
  241. if (GetText ("Enter new value", _currentTable.Columns [e.Col].ColumnName, oldValue, out string newText))
  242. {
  243. try
  244. {
  245. _currentTable.Rows [e.Row] [e.Col] =
  246. string.IsNullOrWhiteSpace (newText) ? DBNull.Value : newText;
  247. }
  248. catch (Exception ex)
  249. {
  250. MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
  251. }
  252. _tableView.Update ();
  253. }
  254. }
  255. private bool GetText (string title, string label, string initialText, out string enteredText)
  256. {
  257. var okPressed = false;
  258. var ok = new Button { Text = "Ok", IsDefault = true };
  259. ok.Accept += (s, e) =>
  260. {
  261. okPressed = true;
  262. Application.RequestStop ();
  263. };
  264. var cancel = new Button { Text = "Cancel" };
  265. cancel.Accept += (s, e) => { Application.RequestStop (); };
  266. var d = new Dialog { Title = title, Buttons = [ok, cancel] };
  267. var lbl = new Label { X = 0, Y = 1, Text = label };
  268. var tf = new TextField { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () };
  269. d.Add (lbl, tf);
  270. tf.SetFocus ();
  271. Application.Run (d);
  272. d.Dispose ();
  273. enteredText = okPressed ? tf.Text : null;
  274. return okPressed;
  275. }
  276. private void MoveColumn ()
  277. {
  278. if (NoTableLoaded ())
  279. {
  280. return;
  281. }
  282. if (_tableView.SelectedColumn == -1)
  283. {
  284. MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
  285. return;
  286. }
  287. try
  288. {
  289. DataColumn currentCol = _currentTable.Columns [_tableView.SelectedColumn];
  290. if (GetText ("Move Column", "New Index:", currentCol.Ordinal.ToString (), out string newOrdinal))
  291. {
  292. int newIdx = Math.Min (
  293. Math.Max (0, int.Parse (newOrdinal)),
  294. _tableView.Table.Columns - 1
  295. );
  296. currentCol.SetOrdinal (newIdx);
  297. _tableView.SetSelection (newIdx, _tableView.SelectedRow, false);
  298. _tableView.EnsureSelectedCellIsVisible ();
  299. _tableView.SetNeedsDisplay ();
  300. }
  301. }
  302. catch (Exception ex)
  303. {
  304. MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok");
  305. }
  306. }
  307. private void MoveRow ()
  308. {
  309. if (NoTableLoaded ())
  310. {
  311. return;
  312. }
  313. if (_tableView.SelectedRow == -1)
  314. {
  315. MessageBox.ErrorQuery ("No Rows", "No row selected", "Ok");
  316. return;
  317. }
  318. try
  319. {
  320. int oldIdx = _tableView.SelectedRow;
  321. DataRow currentRow = _currentTable.Rows [oldIdx];
  322. if (GetText ("Move Row", "New Row:", oldIdx.ToString (), out string newOrdinal))
  323. {
  324. int newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), _tableView.Table.Rows - 1);
  325. if (newIdx == oldIdx)
  326. {
  327. return;
  328. }
  329. object [] arrayItems = currentRow.ItemArray;
  330. _currentTable.Rows.Remove (currentRow);
  331. // Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance
  332. DataRow newRow = _currentTable.NewRow ();
  333. newRow.ItemArray = arrayItems;
  334. _currentTable.Rows.InsertAt (newRow, newIdx);
  335. _tableView.SetSelection (_tableView.SelectedColumn, newIdx, false);
  336. _tableView.EnsureSelectedCellIsVisible ();
  337. _tableView.SetNeedsDisplay ();
  338. }
  339. }
  340. catch (Exception ex)
  341. {
  342. MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok");
  343. }
  344. }
  345. private bool NoTableLoaded ()
  346. {
  347. if (_tableView.Table == null)
  348. {
  349. MessageBox.ErrorQuery ("No Table Loaded", "No table has currently be opened", "Ok");
  350. return true;
  351. }
  352. return false;
  353. }
  354. private void OnSelectedCellChanged (object sender, SelectedCellChangedEventArgs e)
  355. {
  356. // only update the text box if the user is not manually editing it
  357. if (!_selectedCellTextField.HasFocus)
  358. {
  359. _selectedCellTextField.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}";
  360. }
  361. if (_tableView.Table == null || _tableView.SelectedColumn == -1)
  362. {
  363. return;
  364. }
  365. ColumnStyle style = _tableView.Style.GetColumnStyleIfAny (_tableView.SelectedColumn);
  366. _miLeft.Checked = style?.Alignment == Alignment.Start;
  367. _miRight.Checked = style?.Alignment == Alignment.End;
  368. _miCentered.Checked = style?.Alignment == Alignment.Center;
  369. }
  370. private void Open ()
  371. {
  372. var ofd = new FileDialog
  373. {
  374. AllowedTypes = new () { new AllowedType ("Comma Separated Values", ".csv") }
  375. };
  376. ofd.Style.OkButtonText = "Open";
  377. Application.Run (ofd);
  378. if (!ofd.Canceled && !string.IsNullOrWhiteSpace (ofd.Path))
  379. {
  380. Open (ofd.Path);
  381. }
  382. ofd.Dispose ();
  383. }
  384. private void Open (string filename)
  385. {
  386. var lineNumber = 0;
  387. _currentFile = null;
  388. try
  389. {
  390. using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture);
  391. var dt = new DataTable ();
  392. reader.Read ();
  393. if (reader.ReadHeader ())
  394. {
  395. foreach (string h in reader.HeaderRecord)
  396. {
  397. dt.Columns.Add (h);
  398. }
  399. }
  400. while (reader.Read ())
  401. {
  402. lineNumber++;
  403. DataRow newRow = dt.Rows.Add ();
  404. for (var i = 0; i < dt.Columns.Count; i++)
  405. {
  406. newRow [i] = reader [i];
  407. }
  408. }
  409. SetTable (dt);
  410. // Only set the current filename if we successfully loaded the entire file
  411. _currentFile = filename;
  412. _selectedCellTextField.SuperView.Enabled = true;
  413. Application.Top.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}";
  414. }
  415. catch (Exception ex)
  416. {
  417. MessageBox.ErrorQuery (
  418. "Open Failed",
  419. $"Error on line {lineNumber}{Environment.NewLine}{ex.Message}",
  420. "Ok"
  421. );
  422. }
  423. }
  424. private void Quit () { Application.RequestStop (); }
  425. private void RenameColumn ()
  426. {
  427. if (NoTableLoaded ())
  428. {
  429. return;
  430. }
  431. DataColumn currentCol = _currentTable.Columns [_tableView.SelectedColumn];
  432. if (GetText ("Rename Column", "Name:", currentCol.ColumnName, out string newName))
  433. {
  434. currentCol.ColumnName = newName;
  435. _tableView.Update ();
  436. }
  437. }
  438. private void Save ()
  439. {
  440. if (_tableView.Table == null || string.IsNullOrWhiteSpace (_currentFile))
  441. {
  442. MessageBox.ErrorQuery ("No file loaded", "No file is currently loaded", "Ok");
  443. return;
  444. }
  445. using var writer = new CsvWriter (
  446. new StreamWriter (File.OpenWrite (_currentFile)),
  447. CultureInfo.InvariantCulture
  448. );
  449. foreach (string col in _currentTable.Columns.Cast<DataColumn> ().Select (c => c.ColumnName))
  450. {
  451. writer.WriteField (col);
  452. }
  453. writer.NextRecord ();
  454. foreach (DataRow row in _currentTable.Rows)
  455. {
  456. foreach (object item in row.ItemArray)
  457. {
  458. writer.WriteField (item);
  459. }
  460. writer.NextRecord ();
  461. }
  462. }
  463. private void SelectedCellLabel_TextChanged (object sender, EventArgs e)
  464. {
  465. // if user is in the text control and editing the selected cell
  466. if (!_selectedCellTextField.HasFocus)
  467. {
  468. return;
  469. }
  470. // change selected cell to the one the user has typed into the box
  471. Match match = Regex.Match (_selectedCellTextField.Text, "^(\\d+),(\\d+)$");
  472. if (match.Success)
  473. {
  474. _tableView.SelectedColumn = int.Parse (match.Groups [1].Value);
  475. _tableView.SelectedRow = int.Parse (match.Groups [2].Value);
  476. }
  477. }
  478. private void SetFormat ()
  479. {
  480. if (NoTableLoaded ())
  481. {
  482. return;
  483. }
  484. DataColumn col = _currentTable.Columns [_tableView.SelectedColumn];
  485. if (col.DataType == typeof (string))
  486. {
  487. MessageBox.ErrorQuery (
  488. "Cannot Format Column",
  489. "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type",
  490. "Ok"
  491. );
  492. return;
  493. }
  494. ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
  495. if (GetText ("Format", "Pattern:", style.Format ?? "", out string newPattern))
  496. {
  497. style.Format = newPattern;
  498. _tableView.Update ();
  499. }
  500. }
  501. private void SetTable (DataTable dataTable) { _tableView.Table = new DataTableSource (_currentTable = dataTable); }
  502. private void SetupScrollBar ()
  503. {
  504. var scrollBar = new ScrollBarView (_tableView, true);
  505. scrollBar.ChangedPosition += (s, e) =>
  506. {
  507. _tableView.RowOffset = scrollBar.Position;
  508. if (_tableView.RowOffset != scrollBar.Position)
  509. {
  510. scrollBar.Position = _tableView.RowOffset;
  511. }
  512. _tableView.SetNeedsDisplay ();
  513. };
  514. /*
  515. scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
  516. tableView.LeftItem = scrollBar.OtherScrollBarView.Position;
  517. if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) {
  518. scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
  519. }
  520. tableView.SetNeedsDisplay ();
  521. };*/
  522. _tableView.DrawContent += (s, e) =>
  523. {
  524. scrollBar.Size = _tableView.Table?.Rows ?? 0;
  525. scrollBar.Position = _tableView.RowOffset;
  526. //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1;
  527. //scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
  528. scrollBar.Refresh ();
  529. };
  530. }
  531. private void Sort (bool asc)
  532. {
  533. if (NoTableLoaded ())
  534. {
  535. return;
  536. }
  537. if (_tableView.SelectedColumn == -1)
  538. {
  539. MessageBox.ErrorQuery ("No Column", "No column selected", "Ok");
  540. return;
  541. }
  542. string colName = _tableView.Table.ColumnNames [_tableView.SelectedColumn];
  543. _currentTable.DefaultView.Sort = colName + (asc ? " asc" : " desc");
  544. SetTable (_currentTable.DefaultView.ToTable ());
  545. }
  546. private void TableViewKeyPress (object sender, Key e)
  547. {
  548. if (e.KeyCode == KeyCode.Delete)
  549. {
  550. if (_tableView.FullRowSelect)
  551. {
  552. // Delete button deletes all rows when in full row mode
  553. foreach (int toRemove in _tableView.GetAllSelectedCells ()
  554. .Select (p => p.Y)
  555. .Distinct ()
  556. .OrderByDescending (i => i))
  557. {
  558. _currentTable.Rows.RemoveAt (toRemove);
  559. }
  560. }
  561. else
  562. {
  563. // otherwise set all selected cells to null
  564. foreach (Point pt in _tableView.GetAllSelectedCells ())
  565. {
  566. _currentTable.Rows [pt.Y] [pt.X] = DBNull.Value;
  567. }
  568. }
  569. _tableView.Update ();
  570. e.Handled = true;
  571. }
  572. }
  573. }