CsvEditor.cs 22 KB

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