CsvEditor.cs 24 KB

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