FileDialog.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. //
  2. // FileDialog.cs: File system dialogs for open and save
  3. //
  4. // TODO:
  5. // * Add directory selector
  6. // * Implement subclasses
  7. // * Figure out why message text does not show
  8. // * Remove the extra space when message does not show
  9. // * Use a line separator to show the file listing, so we can use same colors as the rest
  10. // * DirListView: Add mouse support
  11. using System;
  12. using System.Collections.Generic;
  13. using NStack;
  14. using System.IO;
  15. using System.Linq;
  16. namespace Terminal.Gui {
  17. internal class DirListView : View {
  18. int top, selected;
  19. DirectoryInfo dirInfo;
  20. List<(string, bool, bool)> infos;
  21. internal bool canChooseFiles = true;
  22. internal bool canChooseDirectories = false;
  23. internal bool allowsMultipleSelection = false;
  24. FileDialog host;
  25. public DirListView (FileDialog host)
  26. {
  27. infos = new List<(string, bool, bool)> ();
  28. CanFocus = true;
  29. this.host = host;
  30. }
  31. bool IsAllowed (FileSystemInfo fsi)
  32. {
  33. if (fsi.Attributes.HasFlag (FileAttributes.Directory))
  34. return true;
  35. if (allowedFileTypes == null)
  36. return true;
  37. foreach (var ft in allowedFileTypes)
  38. if (fsi.Name.EndsWith (ft))
  39. return true;
  40. return false;
  41. }
  42. internal void Reload ()
  43. {
  44. dirInfo = new DirectoryInfo (directory.ToString ());
  45. infos = (from x in dirInfo.GetFileSystemInfos ()
  46. where IsAllowed (x)
  47. orderby (!x.Attributes.HasFlag (FileAttributes.Directory)) + x.Name
  48. select (x.Name, x.Attributes.HasFlag (FileAttributes.Directory), false)).ToList ();
  49. infos.Insert (0, ("..", true, false));
  50. top = 0;
  51. selected = 0;
  52. SetNeedsDisplay ();
  53. }
  54. ustring directory;
  55. public ustring Directory {
  56. get => directory;
  57. set {
  58. if (directory == value)
  59. return;
  60. directory = value;
  61. Reload ();
  62. }
  63. }
  64. public override void PositionCursor ()
  65. {
  66. Move (0, selected - top);
  67. }
  68. int lastSelected;
  69. bool shiftOnWheel;
  70. public override bool MouseEvent (MouseEvent me)
  71. {
  72. if ((me.Flags & (MouseFlags.Button1Clicked | MouseFlags.Button1DoubleClicked |
  73. MouseFlags.WheeledUp | MouseFlags.WheeledDown)) == 0)
  74. return false;
  75. if (!HasFocus)
  76. SuperView.SetFocus (this);
  77. if (infos == null)
  78. return false;
  79. if (me.Y + top >= infos.Count)
  80. return true;
  81. int lastSelectedCopy = shiftOnWheel ? lastSelected : selected;
  82. switch (me.Flags) {
  83. case MouseFlags.Button1Clicked:
  84. SetSelected (me);
  85. SelectionChanged ();
  86. SetNeedsDisplay ();
  87. break;
  88. case MouseFlags.Button1DoubleClicked:
  89. SetSelected (me);
  90. if (ExecuteSelection ()) {
  91. host.canceled = false;
  92. Application.RequestStop ();
  93. }
  94. return true;
  95. case MouseFlags.Button1Clicked | MouseFlags.ButtonShift:
  96. SetSelected (me);
  97. if (shiftOnWheel)
  98. lastSelected = lastSelectedCopy;
  99. shiftOnWheel = false;
  100. PerformMultipleSelection (lastSelected);
  101. return true;
  102. case MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl:
  103. SetSelected (me);
  104. PerformMultipleSelection ();
  105. return true;
  106. case MouseFlags.WheeledUp:
  107. SetSelected (me);
  108. selected = lastSelected;
  109. MoveUp ();
  110. return true;
  111. case MouseFlags.WheeledDown:
  112. SetSelected (me);
  113. selected = lastSelected;
  114. MoveDown ();
  115. return true;
  116. case MouseFlags.WheeledUp | MouseFlags.ButtonShift:
  117. SetSelected (me);
  118. selected = lastSelected;
  119. lastSelected = lastSelectedCopy;
  120. shiftOnWheel = true;
  121. MoveUp ();
  122. return true;
  123. case MouseFlags.WheeledDown | MouseFlags.ButtonShift:
  124. SetSelected (me);
  125. selected = lastSelected;
  126. lastSelected = lastSelectedCopy;
  127. shiftOnWheel = true;
  128. MoveDown ();
  129. return true;
  130. }
  131. return true;
  132. }
  133. void SetSelected (MouseEvent me)
  134. {
  135. lastSelected = selected;
  136. selected = top + me.Y;
  137. }
  138. void DrawString (int line, string str)
  139. {
  140. var f = Frame;
  141. var width = f.Width;
  142. var ustr = ustring.Make (str);
  143. Move (allowsMultipleSelection ? 3 : 2, line);
  144. int byteLen = ustr.Length;
  145. int used = allowsMultipleSelection ? 2 : 1;
  146. for (int i = 0; i < byteLen;) {
  147. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  148. var count = Rune.ColumnWidth (rune);
  149. if (used + count >= width)
  150. break;
  151. Driver.AddRune (rune);
  152. used += count;
  153. i += size;
  154. }
  155. for (; used < width - 1; used++) {
  156. Driver.AddRune (' ');
  157. }
  158. }
  159. public override void Redraw (Rect bounds)
  160. {
  161. var current = ColorScheme.Focus;
  162. Driver.SetAttribute (current);
  163. Move (0, 0);
  164. var f = Frame;
  165. var item = top;
  166. bool focused = HasFocus;
  167. var width = bounds.Width;
  168. for (int row = 0; row < f.Height; row++, item++) {
  169. bool isSelected = item == selected;
  170. Move (0, row);
  171. var newcolor = focused ? (isSelected ? ColorScheme.HotNormal : ColorScheme.Focus) : ColorScheme.Focus;
  172. if (newcolor != current) {
  173. Driver.SetAttribute (newcolor);
  174. current = newcolor;
  175. }
  176. if (item >= infos.Count) {
  177. for (int c = 0; c < f.Width; c++)
  178. Driver.AddRune (' ');
  179. continue;
  180. }
  181. var fi = infos [item];
  182. Driver.AddRune (isSelected ? '>' : ' ');
  183. if (allowsMultipleSelection)
  184. Driver.AddRune (fi.Item3 ? '*' : ' ');
  185. if (fi.Item2)
  186. Driver.AddRune ('/');
  187. else
  188. Driver.AddRune (' ');
  189. DrawString (row, fi.Item1);
  190. }
  191. }
  192. public Action<(string, bool)> SelectedChanged { get; set; }
  193. public Action<ustring> DirectoryChanged { get; set; }
  194. public Action<ustring> FileChanged { get; set; }
  195. void SelectionChanged ()
  196. {
  197. if (FilePaths.Count > 0)
  198. FileChanged?.Invoke (string.Join (", ", GetFilesName (FilePaths)));
  199. else
  200. FileChanged?.Invoke (infos [selected].Item2 ? "" : Path.GetFileName (infos [selected].Item1));
  201. if (SelectedChanged != null) {
  202. var sel = infos [selected];
  203. SelectedChanged ((sel.Item1, sel.Item2));
  204. }
  205. }
  206. List<string> GetFilesName (IReadOnlyList<string> files)
  207. {
  208. List<string> filesName = new List<string> ();
  209. foreach (var file in files) {
  210. filesName.Add (Path.GetFileName (file));
  211. }
  212. return filesName;
  213. }
  214. public override bool ProcessKey (KeyEvent keyEvent)
  215. {
  216. switch (keyEvent.Key) {
  217. case Key.CursorUp:
  218. case Key.ControlP:
  219. MoveUp ();
  220. return true;
  221. case Key.CursorDown:
  222. case Key.ControlN:
  223. MoveDown ();
  224. return true;
  225. case Key.ControlV:
  226. case Key.PageDown:
  227. var n = (selected + Frame.Height);
  228. if (n > infos.Count)
  229. n = infos.Count - 1;
  230. if (n != selected) {
  231. selected = n;
  232. if (infos.Count >= Frame.Height)
  233. top = selected;
  234. else
  235. top = 0;
  236. SelectionChanged ();
  237. SetNeedsDisplay ();
  238. }
  239. return true;
  240. case Key.Enter:
  241. if (ExecuteSelection ())
  242. return false;
  243. else
  244. return true;
  245. case Key.PageUp:
  246. n = (selected - Frame.Height);
  247. if (n < 0)
  248. n = 0;
  249. if (n != selected) {
  250. selected = n;
  251. top = selected;
  252. SelectionChanged ();
  253. SetNeedsDisplay ();
  254. }
  255. return true;
  256. case Key.Space:
  257. case Key.ControlT:
  258. PerformMultipleSelection ();
  259. return true;
  260. case Key.Home:
  261. MoveFirst ();
  262. return true;
  263. case Key.End:
  264. MoveLast ();
  265. return true;
  266. }
  267. return base.ProcessKey (keyEvent);
  268. }
  269. void MoveLast ()
  270. {
  271. selected = infos.Count - 1;
  272. top = infos.Count () - 1;
  273. SelectionChanged ();
  274. SetNeedsDisplay ();
  275. }
  276. void MoveFirst ()
  277. {
  278. selected = 0;
  279. top = 0;
  280. SelectionChanged ();
  281. SetNeedsDisplay ();
  282. }
  283. void MoveDown ()
  284. {
  285. if (selected + 1 < infos.Count) {
  286. selected++;
  287. if (selected >= top + Frame.Height)
  288. top++;
  289. SelectionChanged ();
  290. SetNeedsDisplay ();
  291. }
  292. }
  293. void MoveUp ()
  294. {
  295. if (selected > 0) {
  296. selected--;
  297. if (selected < top)
  298. top = selected;
  299. SelectionChanged ();
  300. SetNeedsDisplay ();
  301. }
  302. }
  303. internal bool ExecuteSelection ()
  304. {
  305. var isDir = infos [selected].Item2;
  306. if (isDir) {
  307. Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1));
  308. DirectoryChanged?.Invoke (Directory);
  309. } else {
  310. FileChanged?.Invoke (infos [selected].Item1);
  311. if (canChooseFiles) {
  312. // Ensures that at least one file is selected.
  313. if (FilePaths.Count == 0)
  314. PerformMultipleSelection ();
  315. // Let the OK handler take it over
  316. return true;
  317. }
  318. // No files allowed, do not let the default handler take it.
  319. }
  320. return false;
  321. }
  322. void PerformMultipleSelection (int? firstSelected = null)
  323. {
  324. if (allowsMultipleSelection) {
  325. int first = Math.Min (firstSelected ?? selected, selected);
  326. int last = Math.Max (selected, firstSelected ?? selected);
  327. for (int i = first; i <= last; i++) {
  328. if ((canChooseFiles && infos [i].Item2 == false) ||
  329. (canChooseDirectories && infos [i].Item2 &&
  330. infos [i].Item1 != "..")) {
  331. infos [i] = (infos [i].Item1, infos [i].Item2, !infos [i].Item3);
  332. }
  333. }
  334. SelectionChanged ();
  335. SetNeedsDisplay ();
  336. }
  337. }
  338. string [] allowedFileTypes;
  339. public string [] AllowedFileTypes {
  340. get => allowedFileTypes;
  341. set {
  342. allowedFileTypes = value;
  343. Reload ();
  344. }
  345. }
  346. public string MakePath (string relativePath)
  347. {
  348. return Path.GetFullPath (Path.Combine (Directory.ToString (), relativePath));
  349. }
  350. public IReadOnlyList<string> FilePaths {
  351. get {
  352. if (allowsMultipleSelection) {
  353. var res = new List<string> ();
  354. foreach (var item in infos)
  355. if (item.Item3)
  356. res.Add (MakePath (item.Item1));
  357. return res;
  358. } else {
  359. if (infos [selected].Item2) {
  360. if (canChooseDirectories)
  361. return new List<string> () { MakePath (infos [selected].Item1) };
  362. return Array.Empty<string> ();
  363. } else {
  364. if (canChooseFiles)
  365. return new List<string> () { MakePath (infos [selected].Item1) };
  366. return Array.Empty<string> ();
  367. }
  368. }
  369. }
  370. }
  371. }
  372. /// <summary>
  373. /// Base class for the <see cref="OpenDialog"/> and the <see cref="SaveDialog"/>
  374. /// </summary>
  375. public class FileDialog : Dialog {
  376. Button prompt, cancel;
  377. Label nameFieldLabel, message, dirLabel;
  378. TextField dirEntry, nameEntry;
  379. internal DirListView dirListView;
  380. /// <summary>
  381. /// Initializes a new <see cref="FileDialog"/>.
  382. /// </summary>
  383. public FileDialog () : this (title: string.Empty, prompt: string.Empty, nameFieldLabel: string.Empty, message: string.Empty) { }
  384. /// <summary>
  385. /// Initializes a new instance of <see cref="FileDialog"/>
  386. /// </summary>
  387. /// <param name="title">The title.</param>
  388. /// <param name="prompt">The prompt.</param>
  389. /// <param name="nameFieldLabel">The name field label.</param>
  390. /// <param name="message">The message.</param>
  391. public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title)//, Driver.Cols - 20, Driver.Rows - 5, null)
  392. {
  393. this.message = new Label (Rect.Empty, "MESSAGE" + message);
  394. var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
  395. dirLabel = new Label ("Directory: ") {
  396. X = 1,
  397. Y = 1 + msgLines
  398. };
  399. dirEntry = new TextField ("") {
  400. X = Pos.Right (dirLabel),
  401. Y = 1 + msgLines,
  402. Width = Dim.Fill () - 1
  403. };
  404. Add (dirLabel, dirEntry);
  405. this.nameFieldLabel = new Label ("Open: ") {
  406. X = 6,
  407. Y = 3 + msgLines,
  408. };
  409. nameEntry = new TextField ("") {
  410. X = Pos.Left (dirEntry),
  411. Y = 3 + msgLines,
  412. Width = Dim.Fill () - 1
  413. };
  414. Add (this.nameFieldLabel, nameEntry);
  415. dirListView = new DirListView (this) {
  416. X = 1,
  417. Y = 3 + msgLines + 2,
  418. Width = Dim.Fill () - 1,
  419. Height = Dim.Fill () - 2,
  420. };
  421. DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
  422. Add (dirListView);
  423. dirListView.DirectoryChanged = (dir) => dirEntry.Text = dir;
  424. dirListView.FileChanged = (file) => nameEntry.Text = file;
  425. this.cancel = new Button ("Cancel");
  426. this.cancel.Clicked += () => {
  427. canceled = true;
  428. Application.RequestStop ();
  429. };
  430. AddButton (cancel);
  431. this.prompt = new Button (prompt) {
  432. IsDefault = true,
  433. };
  434. this.prompt.Clicked += () => {
  435. dirListView.ExecuteSelection ();
  436. canceled = false;
  437. Application.RequestStop ();
  438. };
  439. AddButton (this.prompt);
  440. Width = Dim.Percent (80);
  441. Height = Dim.Percent (80);
  442. // On success, we will set this to false.
  443. canceled = true;
  444. }
  445. internal bool canceled;
  446. ///<inheritdoc/>
  447. public override void WillPresent ()
  448. {
  449. base.WillPresent ();
  450. //SetFocus (nameEntry);
  451. }
  452. /// <summary>
  453. /// Gets or sets the prompt label for the <see cref="Button"/> displayed to the user
  454. /// </summary>
  455. /// <value>The prompt.</value>
  456. public ustring Prompt {
  457. get => prompt.Text;
  458. set {
  459. prompt.Text = value;
  460. }
  461. }
  462. /// <summary>
  463. /// Gets or sets the name field label.
  464. /// </summary>
  465. /// <value>The name field label.</value>
  466. public ustring NameFieldLabel {
  467. get => nameFieldLabel.Text;
  468. set {
  469. nameFieldLabel.Text = value;
  470. }
  471. }
  472. /// <summary>
  473. /// Gets or sets the message displayed to the user, defaults to nothing
  474. /// </summary>
  475. /// <value>The message.</value>
  476. public ustring Message {
  477. get => message.Text;
  478. set {
  479. message.Text = value;
  480. }
  481. }
  482. /// <summary>
  483. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> can create directories.
  484. /// </summary>
  485. /// <value><c>true</c> if can create directories; otherwise, <c>false</c>.</value>
  486. public bool CanCreateDirectories { get; set; }
  487. /// <summary>
  488. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> is extension hidden.
  489. /// </summary>
  490. /// <value><c>true</c> if is extension hidden; otherwise, <c>false</c>.</value>
  491. public bool IsExtensionHidden { get; set; }
  492. /// <summary>
  493. /// Gets or sets the directory path for this panel
  494. /// </summary>
  495. /// <value>The directory path.</value>
  496. public ustring DirectoryPath {
  497. get => dirEntry.Text;
  498. set {
  499. dirEntry.Text = value;
  500. dirListView.Directory = value;
  501. }
  502. }
  503. /// <summary>
  504. /// The array of filename extensions allowed, or null if all file extensions are allowed.
  505. /// </summary>
  506. /// <value>The allowed file types.</value>
  507. public string [] AllowedFileTypes {
  508. get => dirListView.AllowedFileTypes;
  509. set => dirListView.AllowedFileTypes = value;
  510. }
  511. /// <summary>
  512. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> allows the file to be saved with a different extension
  513. /// </summary>
  514. /// <value><c>true</c> if allows other file types; otherwise, <c>false</c>.</value>
  515. public bool AllowsOtherFileTypes { get; set; }
  516. /// <summary>
  517. /// The File path that is currently shown on the panel
  518. /// </summary>
  519. /// <value>The absolute file path for the file path entered.</value>
  520. public ustring FilePath {
  521. get => dirListView.MakePath (nameEntry.Text.ToString ());
  522. set {
  523. nameEntry.Text = Path.GetFileName (value.ToString ());
  524. }
  525. }
  526. /// <summary>
  527. /// Check if the dialog was or not canceled.
  528. /// </summary>
  529. public bool Canceled { get => canceled; }
  530. }
  531. /// <summary>
  532. /// The <see cref="SaveDialog"/> provides an interactive dialog box for users to pick a file to
  533. /// save.
  534. /// </summary>
  535. /// <remarks>
  536. /// <para>
  537. /// To use, create an instance of <see cref="SaveDialog"/>, and pass it to
  538. /// <see cref="Application.Run()"/>. This will run the dialog modally,
  539. /// and when this returns, the <see cref="FileName"/>property will contain the selected file name or
  540. /// null if the user canceled.
  541. /// </para>
  542. /// </remarks>
  543. public class SaveDialog : FileDialog {
  544. /// <summary>
  545. /// Initializes a new <see cref="SaveDialog"/>.
  546. /// </summary>
  547. public SaveDialog () : this (title: string.Empty, message: string.Empty) { }
  548. /// <summary>
  549. /// Initializes a new <see cref="SaveDialog"/>.
  550. /// </summary>
  551. /// <param name="title">The title.</param>
  552. /// <param name="message">The message.</param>
  553. public SaveDialog (ustring title, ustring message) : base (title, prompt: "Save", nameFieldLabel: "Save as:", message: message) { }
  554. /// <summary>
  555. /// Gets the name of the file the user selected for saving, or null
  556. /// if the user canceled the <see cref="SaveDialog"/>.
  557. /// </summary>
  558. /// <value>The name of the file.</value>
  559. public ustring FileName {
  560. get {
  561. if (canceled)
  562. return null;
  563. return Path.GetFileName (FilePath.ToString ());
  564. }
  565. }
  566. }
  567. /// <summary>
  568. /// The <see cref="OpenDialog"/>provides an interactive dialog box for users to select files or directories.
  569. /// </summary>
  570. /// <remarks>
  571. /// <para>
  572. /// The open dialog can be used to select files for opening, it can be configured to allow
  573. /// multiple items to be selected (based on the AllowsMultipleSelection) variable and
  574. /// you can control whether this should allow files or directories to be selected.
  575. /// </para>
  576. /// <para>
  577. /// To use, create an instance of <see cref="OpenDialog"/>, and pass it to
  578. /// <see cref="Application.Run()"/>. This will run the dialog modally,
  579. /// and when this returns, the list of filds will be available on the <see cref="FilePaths"/> property.
  580. /// </para>
  581. /// <para>
  582. /// To select more than one file, users can use the spacebar, or control-t.
  583. /// </para>
  584. /// </remarks>
  585. public class OpenDialog : FileDialog {
  586. /// <summary>
  587. /// Initializes a new <see cref="OpenDialog"/>.
  588. /// </summary>
  589. public OpenDialog () : this (title: string.Empty, message: string.Empty) { }
  590. /// <summary>
  591. /// Initializes a new <see cref="OpenDialog"/>.
  592. /// </summary>
  593. /// <param name="title"></param>
  594. /// <param name="message"></param>
  595. public OpenDialog (ustring title, ustring message) : base (title, prompt: "Open", nameFieldLabel: "Open", message: message)
  596. {
  597. }
  598. /// <summary>
  599. /// Gets or sets a value indicating whether this <see cref="Terminal.Gui.OpenDialog"/> can choose files.
  600. /// </summary>
  601. /// <value><c>true</c> if can choose files; otherwise, <c>false</c>. Defaults to <c>true</c></value>
  602. public bool CanChooseFiles {
  603. get => dirListView.canChooseFiles;
  604. set {
  605. dirListView.canChooseFiles = value;
  606. dirListView.Reload ();
  607. }
  608. }
  609. /// <summary>
  610. /// Gets or sets a value indicating whether this <see cref="OpenDialog"/> can choose directories.
  611. /// </summary>
  612. /// <value><c>true</c> if can choose directories; otherwise, <c>false</c> defaults to <c>false</c>.</value>
  613. public bool CanChooseDirectories {
  614. get => dirListView.canChooseDirectories;
  615. set {
  616. dirListView.canChooseDirectories = value;
  617. dirListView.Reload ();
  618. }
  619. }
  620. /// <summary>
  621. /// Gets or sets a value indicating whether this <see cref="OpenDialog"/> allows multiple selection.
  622. /// </summary>
  623. /// <value><c>true</c> if allows multiple selection; otherwise, <c>false</c>, defaults to false.</value>
  624. public bool AllowsMultipleSelection {
  625. get => dirListView.allowsMultipleSelection;
  626. set {
  627. dirListView.allowsMultipleSelection = value;
  628. dirListView.Reload ();
  629. }
  630. }
  631. /// <summary>
  632. /// Returns the selected files, or an empty list if nothing has been selected
  633. /// </summary>
  634. /// <value>The file paths.</value>
  635. public IReadOnlyList<string> FilePaths {
  636. get => dirListView.FilePaths;
  637. }
  638. }
  639. }