FileDialog.cs 12 KB

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