FileDialog.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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. public DirListView ()
  25. {
  26. infos = new List<(string,bool,bool)> ();
  27. CanFocus = true;
  28. }
  29. bool IsAllowed (FileSystemInfo fsi)
  30. {
  31. if (fsi.Attributes.HasFlag (FileAttributes.Directory))
  32. return true;
  33. if (allowedFileTypes == null)
  34. return true;
  35. foreach (var ft in allowedFileTypes)
  36. if (fsi.Name.EndsWith (ft))
  37. return true;
  38. return false;
  39. }
  40. internal void Reload ()
  41. {
  42. dirInfo = new DirectoryInfo (directory.ToString ());
  43. infos = (from x in dirInfo.GetFileSystemInfos ()
  44. where IsAllowed (x)
  45. orderby (!x.Attributes.HasFlag (FileAttributes.Directory)) + x.Name
  46. select (x.Name, x.Attributes.HasFlag (FileAttributes.Directory), false)).ToList ();
  47. infos.Insert (0, ("..", true, false));
  48. top = 0;
  49. selected = 0;
  50. SetNeedsDisplay ();
  51. }
  52. ustring directory;
  53. public ustring Directory {
  54. get => directory;
  55. set {
  56. if (directory == value)
  57. return;
  58. directory = value;
  59. Reload ();
  60. }
  61. }
  62. public override void PositionCursor ()
  63. {
  64. Move (0, selected - top);
  65. }
  66. void DrawString (int line, string str)
  67. {
  68. var f = Frame;
  69. var width = f.Width;
  70. var ustr = ustring.Make (str);
  71. Move (allowsMultipleSelection ? 3 : 2, line);
  72. int byteLen = ustr.Length;
  73. int used = 0;
  74. for (int i = 0; i < byteLen;) {
  75. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  76. var count = Rune.ColumnWidth (rune);
  77. if (used + count >= width)
  78. break;
  79. Driver.AddRune (rune);
  80. used += count;
  81. i += size;
  82. }
  83. for (; used < width; used++) {
  84. Driver.AddRune (' ');
  85. }
  86. }
  87. public override void Redraw (Rect region)
  88. {
  89. var current = ColorScheme.Focus;
  90. Driver.SetAttribute (current);
  91. Move (0, 0);
  92. var f = Frame;
  93. var item = top;
  94. bool focused = HasFocus;
  95. var width = region.Width;
  96. for (int row = 0; row < f.Height; row++, item++) {
  97. bool isSelected = item == selected;
  98. Move (0, row);
  99. var newcolor = focused ? (isSelected ? ColorScheme.HotNormal : ColorScheme.Focus) : ColorScheme.Focus;
  100. if (newcolor != current) {
  101. Driver.SetAttribute (newcolor);
  102. current = newcolor;
  103. }
  104. if (item >= infos.Count) {
  105. for (int c = 0; c < f.Width; c++)
  106. Driver.AddRune (' ');
  107. continue;
  108. }
  109. var fi = infos [item];
  110. Driver.AddRune (isSelected ? '>' : ' ');
  111. if (allowsMultipleSelection)
  112. Driver.AddRune (fi.Item3 ? '*' : ' ');
  113. if (fi.Item2)
  114. Driver.AddRune ('/');
  115. else
  116. Driver.AddRune (' ');
  117. DrawString (row, fi.Item1);
  118. }
  119. }
  120. public Action<(string,bool)> SelectedChanged;
  121. public Action<ustring> DirectoryChanged;
  122. public Action<ustring> FileChanged;
  123. void SelectionChanged ()
  124. {
  125. if (SelectedChanged != null) {
  126. var sel = infos [selected];
  127. SelectedChanged ((sel.Item1, sel.Item2));
  128. }
  129. }
  130. public override bool ProcessKey (KeyEvent keyEvent)
  131. {
  132. switch (keyEvent.Key) {
  133. case Key.CursorUp:
  134. case Key.ControlP:
  135. if (selected > 0) {
  136. selected--;
  137. if (selected < top)
  138. top = selected;
  139. SelectionChanged ();
  140. SetNeedsDisplay ();
  141. }
  142. return true;
  143. case Key.CursorDown:
  144. case Key.ControlN:
  145. if (selected + 1 < infos.Count) {
  146. selected++;
  147. if (selected >= top + Frame.Height)
  148. top++;
  149. SelectionChanged ();
  150. SetNeedsDisplay ();
  151. }
  152. return true;
  153. case Key.ControlV:
  154. case Key.PageDown:
  155. var n = (selected + Frame.Height);
  156. if (n > infos.Count)
  157. n = infos.Count - 1;
  158. if (n != selected) {
  159. selected = n;
  160. if (infos.Count >= Frame.Height)
  161. top = selected;
  162. else
  163. top = 0;
  164. SelectionChanged ();
  165. SetNeedsDisplay ();
  166. }
  167. return true;
  168. case Key.Enter:
  169. var isDir = infos [selected].Item2;
  170. if (isDir) {
  171. Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1));
  172. if (DirectoryChanged != null)
  173. DirectoryChanged (Directory);
  174. } else {
  175. if (FileChanged != null)
  176. FileChanged (infos [selected].Item1);
  177. if (canChooseFiles) {
  178. // Let the OK handler take it over
  179. return false;
  180. }
  181. // No files allowed, do not let the default handler take it.
  182. }
  183. return true;
  184. case Key.PageUp:
  185. n = (selected - Frame.Height);
  186. if (n < 0)
  187. n = 0;
  188. if (n != selected) {
  189. selected = n;
  190. top = selected;
  191. SelectionChanged ();
  192. SetNeedsDisplay ();
  193. }
  194. return true;
  195. case Key.Space:
  196. case Key.ControlT:
  197. if (allowsMultipleSelection) {
  198. if ((canChooseFiles && infos [selected].Item2 == false) ||
  199. (canChooseDirectories && infos [selected].Item2 &&
  200. infos [selected].Item1 != "..")){
  201. infos [selected] = (infos [selected].Item1, infos [selected].Item2, !infos [selected].Item3);
  202. SelectionChanged ();
  203. SetNeedsDisplay ();
  204. }
  205. }
  206. return true;
  207. }
  208. return base.ProcessKey (keyEvent);
  209. }
  210. string [] allowedFileTypes;
  211. public string [] AllowedFileTypes {
  212. get => allowedFileTypes;
  213. set {
  214. allowedFileTypes = value;
  215. Reload ();
  216. }
  217. }
  218. public string MakePath (string relativePath)
  219. {
  220. return Path.GetFullPath (Path.Combine (Directory.ToString (), relativePath));
  221. }
  222. public IReadOnlyList<string> FilePaths {
  223. get {
  224. if (allowsMultipleSelection) {
  225. var res = new List<string> ();
  226. foreach (var item in infos)
  227. if (item.Item3)
  228. res.Add (MakePath (item.Item1));
  229. return res;
  230. } else {
  231. if (infos [selected].Item2) {
  232. if (canChooseDirectories)
  233. return new List<string> () { MakePath (infos [selected].Item1) };
  234. return Array.Empty<string> ();
  235. } else {
  236. if (canChooseFiles)
  237. return new List<string> () { MakePath (infos [selected].Item1) };
  238. return Array.Empty<string> ();
  239. }
  240. }
  241. }
  242. }
  243. }
  244. /// <summary>
  245. /// Base class for the OpenDialog and the SaveDialog
  246. /// </summary>
  247. public class FileDialog : Dialog {
  248. Button prompt, cancel;
  249. Label nameFieldLabel, message, dirLabel;
  250. TextField dirEntry, nameEntry;
  251. internal DirListView dirListView;
  252. public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title, Driver.Cols - 20, Driver.Rows - 5, null)
  253. {
  254. this.message = new Label (Rect.Empty, "MESSAGE" + message);
  255. var msgLines = Label.MeasureLines (message, Driver.Cols - 20);
  256. dirLabel = new Label ("Directory: ") {
  257. X = 1,
  258. Y = 1 + msgLines
  259. };
  260. dirEntry = new TextField ("") {
  261. X = Pos.Right (dirLabel),
  262. Y = 1 + msgLines,
  263. Width = Dim.Fill () - 1
  264. };
  265. Add (dirLabel, dirEntry);
  266. this.nameFieldLabel = new Label ("Open: ") {
  267. X = 6,
  268. Y = 3 + msgLines,
  269. };
  270. nameEntry = new TextField ("") {
  271. X = Pos.Left (dirEntry),
  272. Y = 3 + msgLines,
  273. Width = Dim.Fill () - 1
  274. };
  275. Add (this.nameFieldLabel, nameEntry);
  276. dirListView = new DirListView () {
  277. X = 1,
  278. Y = 3 + msgLines + 2,
  279. Width = Dim.Fill () - 2,
  280. Height = Dim.Fill () - 2,
  281. };
  282. DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
  283. Add (dirListView);
  284. dirListView.DirectoryChanged = (dir) => dirEntry.Text = dir;
  285. dirListView.FileChanged = (file) => {
  286. nameEntry.Text = file;
  287. };
  288. this.cancel = new Button ("Cancel");
  289. this.cancel.Clicked += () => {
  290. canceled = true;
  291. Application.RequestStop ();
  292. };
  293. AddButton (cancel);
  294. this.prompt = new Button (prompt) {
  295. IsDefault = true,
  296. };
  297. this.prompt.Clicked += () => {
  298. canceled = false;
  299. Application.RequestStop ();
  300. };
  301. AddButton (this.prompt);
  302. // On success, we will set this to false.
  303. canceled = true;
  304. }
  305. internal bool canceled;
  306. public override void WillPresent ()
  307. {
  308. base.WillPresent ();
  309. //SetFocus (nameEntry);
  310. }
  311. /// <summary>
  312. /// Gets or sets the prompt label for the button displayed to the user
  313. /// </summary>
  314. /// <value>The prompt.</value>
  315. public ustring Prompt {
  316. get => prompt.Text;
  317. set {
  318. prompt.Text = value;
  319. }
  320. }
  321. /// <summary>
  322. /// Gets or sets the name field label.
  323. /// </summary>
  324. /// <value>The name field label.</value>
  325. public ustring NameFieldLabel {
  326. get => nameFieldLabel.Text;
  327. set {
  328. nameFieldLabel.Text = value;
  329. }
  330. }
  331. /// <summary>
  332. /// Gets or sets the message displayed to the user, defaults to nothing
  333. /// </summary>
  334. /// <value>The message.</value>
  335. public ustring Message {
  336. get => message.Text;
  337. set {
  338. message.Text = value;
  339. }
  340. }
  341. /// <summary>
  342. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.FileDialog"/> can create directories.
  343. /// </summary>
  344. /// <value><c>true</c> if can create directories; otherwise, <c>false</c>.</value>
  345. public bool CanCreateDirectories { get; set; }
  346. /// <summary>
  347. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.FileDialog"/> is extension hidden.
  348. /// </summary>
  349. /// <value><c>true</c> if is extension hidden; otherwise, <c>false</c>.</value>
  350. public bool IsExtensionHidden { get; set; }
  351. /// <summary>
  352. /// Gets or sets the directory path for this panel
  353. /// </summary>
  354. /// <value>The directory path.</value>
  355. public ustring DirectoryPath {
  356. get => dirEntry.Text;
  357. set {
  358. dirEntry.Text = value;
  359. dirListView.Directory = value;
  360. }
  361. }
  362. /// <summary>
  363. /// The array of filename extensions allowed, or null if all file extensions are allowed.
  364. /// </summary>
  365. /// <value>The allowed file types.</value>
  366. public string [] AllowedFileTypes {
  367. get => dirListView.AllowedFileTypes;
  368. set => dirListView.AllowedFileTypes = value;
  369. }
  370. /// <summary>
  371. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.FileDialog"/> allows the file to be saved with a different extension
  372. /// </summary>
  373. /// <value><c>true</c> if allows other file types; otherwise, <c>false</c>.</value>
  374. public bool AllowsOtherFileTypes { get; set; }
  375. /// <summary>
  376. /// The File path that is currently shown on the panel
  377. /// </summary>
  378. /// <value>The absolute file path for the file path entered.</value>
  379. public ustring FilePath {
  380. get => nameEntry.Text;
  381. set {
  382. nameEntry.Text = value;
  383. }
  384. }
  385. }
  386. /// <summary>
  387. /// The save dialog provides an interactive dialog box for users to pick a file to
  388. /// save.
  389. /// </summary>
  390. /// <remarks>
  391. /// <para>
  392. /// To use it, create an instance of the SaveDialog, and then
  393. /// call Application.Run on the resulting instance. This will run the dialog modally,
  394. /// and when this returns, the FileName property will contain the selected value or
  395. /// null if the user canceled.
  396. /// </para>
  397. /// </remarks>
  398. public class SaveDialog : FileDialog {
  399. public SaveDialog (ustring title, ustring message) : base (title, prompt: "Save", nameFieldLabel: "Save as:", message: message)
  400. {
  401. }
  402. /// <summary>
  403. /// Gets the name of the file the user selected for saving, or null
  404. /// if the user canceled the dialog box.
  405. /// </summary>
  406. /// <value>The name of the file.</value>
  407. public ustring FileName {
  408. get {
  409. if (canceled)
  410. return null;
  411. return FilePath;
  412. }
  413. }
  414. }
  415. /// <summary>
  416. /// The Open Dialog provides an interactive dialog box for users to select files or directories.
  417. /// </summary>
  418. /// <remarks>
  419. /// <para>
  420. /// The open dialog can be used to select files for opening, it can be configured to allow
  421. /// multiple items to be selected (based on the AllowsMultipleSelection) variable and
  422. /// you can control whether this should allow files or directories to be selected.
  423. /// </para>
  424. /// <para>
  425. /// To use it, create an instance of the OpenDialog, configure its properties, and then
  426. /// call Application.Run on the resulting instance. This will run the dialog modally,
  427. /// and when this returns, the list of filds will be available on the FilePaths property.
  428. /// </para>
  429. /// <para>
  430. /// To select more than one file, users can use the spacebar, or control-t.
  431. /// </para>
  432. /// </remarks>
  433. public class OpenDialog : FileDialog {
  434. public OpenDialog (ustring title, ustring message) : base (title, prompt: "Open", nameFieldLabel: "Open", message: message)
  435. {
  436. }
  437. /// <summary>
  438. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.OpenDialog"/> can choose files.
  439. /// </summary>
  440. /// <value><c>true</c> if can choose files; otherwise, <c>false</c>. Defaults to <c>true</c></value>
  441. public bool CanChooseFiles {
  442. get => dirListView.canChooseFiles;
  443. set {
  444. dirListView.canChooseFiles = value;
  445. dirListView.Reload ();
  446. }
  447. }
  448. /// <summary>
  449. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.OpenDialog"/> can choose directories.
  450. /// </summary>
  451. /// <value><c>true</c> if can choose directories; otherwise, <c>false</c> defaults to <c>false</c>.</value>
  452. public bool CanChooseDirectories {
  453. get => dirListView.canChooseDirectories;
  454. set {
  455. dirListView.canChooseDirectories = value;
  456. dirListView.Reload ();
  457. }
  458. }
  459. /// <summary>
  460. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.OpenDialog"/> allows multiple selection.
  461. /// </summary>
  462. /// <value><c>true</c> if allows multiple selection; otherwise, <c>false</c>, defaults to false.</value>
  463. public bool AllowsMultipleSelection {
  464. get => dirListView.allowsMultipleSelection;
  465. set {
  466. dirListView.allowsMultipleSelection = value;
  467. dirListView.Reload ();
  468. }
  469. }
  470. /// <summary>
  471. /// Returns the selected files, or an empty list if nothing has been selected
  472. /// </summary>
  473. /// <value>The file paths.</value>
  474. public IReadOnlyList<string> FilePaths {
  475. get => dirListView.FilePaths;
  476. }
  477. }
  478. }