FileDialog.cs 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013
  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. using Terminal.Gui.Resources;
  17. namespace Terminal.Gui {
  18. internal class DirListView : View {
  19. int top, selected;
  20. DirectoryInfo dirInfo;
  21. FileSystemWatcher watcher;
  22. List<(string, bool, bool)> infos;
  23. internal bool canChooseFiles = true;
  24. internal bool canChooseDirectories = false;
  25. internal bool allowsMultipleSelection = false;
  26. FileDialog host;
  27. public DirListView (FileDialog host)
  28. {
  29. infos = new List<(string, bool, bool)> ();
  30. CanFocus = true;
  31. this.host = host;
  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) || ft == ".*")
  41. return true;
  42. return false;
  43. }
  44. internal bool Reload (ustring value = null)
  45. {
  46. bool valid = false;
  47. try {
  48. dirInfo = new DirectoryInfo (value == null ? directory.ToString () : value.ToString ());
  49. watcher = new FileSystemWatcher (dirInfo.FullName);
  50. watcher.NotifyFilter = NotifyFilters.Attributes
  51. | NotifyFilters.CreationTime
  52. | NotifyFilters.DirectoryName
  53. | NotifyFilters.FileName
  54. | NotifyFilters.LastAccess
  55. | NotifyFilters.LastWrite
  56. | NotifyFilters.Security
  57. | NotifyFilters.Size;
  58. watcher.Changed += Watcher_Changed;
  59. watcher.Created += Watcher_Changed;
  60. watcher.Deleted += Watcher_Changed;
  61. watcher.Renamed += Watcher_Changed;
  62. watcher.Error += Watcher_Error;
  63. watcher.EnableRaisingEvents = true;
  64. infos = (from x in dirInfo.GetFileSystemInfos ()
  65. where IsAllowed (x) && (!canChooseFiles ? x.Attributes.HasFlag (FileAttributes.Directory) : true)
  66. orderby (!x.Attributes.HasFlag (FileAttributes.Directory)) + x.Name
  67. select (x.Name, x.Attributes.HasFlag (FileAttributes.Directory), false)).ToList ();
  68. infos.Insert (0, ("..", true, false));
  69. top = 0;
  70. selected = 0;
  71. valid = true;
  72. } catch (Exception ex) {
  73. switch (ex) {
  74. case DirectoryNotFoundException _:
  75. case ArgumentException _:
  76. dirInfo = null;
  77. watcher = null;
  78. infos.Clear ();
  79. valid = true;
  80. break;
  81. default:
  82. valid = false;
  83. break;
  84. }
  85. } finally {
  86. if (valid) {
  87. SetNeedsDisplay ();
  88. }
  89. }
  90. return valid;
  91. }
  92. private bool _disposedValue;
  93. protected override void Dispose (bool disposing)
  94. {
  95. if (!_disposedValue) {
  96. if (disposing) {
  97. watcher?.Dispose ();
  98. }
  99. _disposedValue = true;
  100. }
  101. // Call base class implementation.
  102. base.Dispose (disposing);
  103. }
  104. void Watcher_Error (object sender, ErrorEventArgs e)
  105. {
  106. Application.MainLoop.Invoke (() => Reload ());
  107. }
  108. void Watcher_Changed (object sender, FileSystemEventArgs e)
  109. {
  110. Application.MainLoop.Invoke (() => Reload ());
  111. }
  112. ustring directory;
  113. public ustring Directory {
  114. get => directory;
  115. set {
  116. if (directory == value) {
  117. return;
  118. }
  119. if (Reload (value)) {
  120. directory = value;
  121. }
  122. }
  123. }
  124. public override void PositionCursor ()
  125. {
  126. Move (0, selected - top);
  127. }
  128. int lastSelected;
  129. bool shiftOnWheel;
  130. public override bool MouseEvent (MouseEvent me)
  131. {
  132. if ((me.Flags & (MouseFlags.Button1Clicked | MouseFlags.Button1DoubleClicked |
  133. MouseFlags.WheeledUp | MouseFlags.WheeledDown)) == 0)
  134. return false;
  135. if (!HasFocus)
  136. SetFocus ();
  137. if (infos == null)
  138. return false;
  139. if (me.Y + top >= infos.Count)
  140. return true;
  141. int lastSelectedCopy = shiftOnWheel ? lastSelected : selected;
  142. switch (me.Flags) {
  143. case MouseFlags.Button1Clicked:
  144. SetSelected (me);
  145. OnSelectionChanged ();
  146. SetNeedsDisplay ();
  147. break;
  148. case MouseFlags.Button1DoubleClicked:
  149. UnMarkAll ();
  150. SetSelected (me);
  151. if (ExecuteSelection ()) {
  152. host.canceled = false;
  153. Application.RequestStop ();
  154. }
  155. return true;
  156. case MouseFlags.Button1Clicked | MouseFlags.ButtonShift:
  157. SetSelected (me);
  158. if (shiftOnWheel)
  159. lastSelected = lastSelectedCopy;
  160. shiftOnWheel = false;
  161. PerformMultipleSelection (lastSelected);
  162. return true;
  163. case MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl:
  164. SetSelected (me);
  165. PerformMultipleSelection ();
  166. return true;
  167. case MouseFlags.WheeledUp:
  168. SetSelected (me);
  169. selected = lastSelected;
  170. MoveUp ();
  171. return true;
  172. case MouseFlags.WheeledDown:
  173. SetSelected (me);
  174. selected = lastSelected;
  175. MoveDown ();
  176. return true;
  177. case MouseFlags.WheeledUp | MouseFlags.ButtonShift:
  178. SetSelected (me);
  179. selected = lastSelected;
  180. lastSelected = lastSelectedCopy;
  181. shiftOnWheel = true;
  182. MoveUp ();
  183. return true;
  184. case MouseFlags.WheeledDown | MouseFlags.ButtonShift:
  185. SetSelected (me);
  186. selected = lastSelected;
  187. lastSelected = lastSelectedCopy;
  188. shiftOnWheel = true;
  189. MoveDown ();
  190. return true;
  191. }
  192. return true;
  193. }
  194. void UnMarkAll ()
  195. {
  196. for (int i = 0; i < infos.Count; i++) {
  197. if (infos [i].Item3) {
  198. infos [i] = (infos [i].Item1, infos [i].Item2, false);
  199. }
  200. }
  201. }
  202. void SetSelected (MouseEvent me)
  203. {
  204. lastSelected = selected;
  205. selected = top + me.Y;
  206. }
  207. void DrawString (int line, string str)
  208. {
  209. var f = Frame;
  210. var width = f.Width;
  211. var ustr = ustring.Make (str);
  212. Move (allowsMultipleSelection ? 3 : 2, line);
  213. int byteLen = ustr.Length;
  214. int used = allowsMultipleSelection ? 2 : 1;
  215. for (int i = 0; i < byteLen;) {
  216. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  217. var count = Rune.ColumnWidth (rune);
  218. if (used + count >= width)
  219. break;
  220. Driver.AddRune (rune);
  221. used += count;
  222. i += size;
  223. }
  224. for (; used < width - 1; used++) {
  225. Driver.AddRune (' ');
  226. }
  227. }
  228. public override void Redraw (Rect bounds)
  229. {
  230. var current = ColorScheme.Focus;
  231. Driver.SetAttribute (current);
  232. Move (0, 0);
  233. var f = Frame;
  234. var item = top;
  235. bool focused = HasFocus;
  236. var width = bounds.Width;
  237. for (int row = 0; row < f.Height; row++, item++) {
  238. bool isSelected = item == selected;
  239. Move (0, row);
  240. var newcolor = focused ? (isSelected ? ColorScheme.HotNormal : ColorScheme.Focus)
  241. : Enabled ? ColorScheme.Focus : ColorScheme.Disabled;
  242. if (newcolor != current) {
  243. Driver.SetAttribute (newcolor);
  244. current = newcolor;
  245. }
  246. if (item >= infos.Count) {
  247. for (int c = 0; c < f.Width; c++)
  248. Driver.AddRune (' ');
  249. continue;
  250. }
  251. var fi = infos [item];
  252. Driver.AddRune (isSelected ? '>' : ' ');
  253. if (allowsMultipleSelection)
  254. Driver.AddRune (fi.Item3 ? '*' : ' ');
  255. if (fi.Item2)
  256. Driver.AddRune ('/');
  257. else
  258. Driver.AddRune (' ');
  259. DrawString (row, fi.Item1);
  260. }
  261. }
  262. public Action<(string, bool)> SelectedChanged { get; set; }
  263. public Action<ustring> DirectoryChanged { get; set; }
  264. public Action<ustring> FileChanged { get; set; }
  265. string splitString = ",";
  266. void OnSelectionChanged ()
  267. {
  268. if (allowsMultipleSelection) {
  269. if (FilePaths.Count > 0) {
  270. FileChanged?.Invoke (string.Join (splitString, GetFilesName (FilePaths)));
  271. } else {
  272. FileChanged?.Invoke (infos [selected].Item2 && !canChooseDirectories ? "" : Path.GetFileName (infos [selected].Item1));
  273. }
  274. } else {
  275. var sel = infos [selected];
  276. SelectedChanged?.Invoke ((sel.Item1, sel.Item2));
  277. }
  278. }
  279. List<string> GetFilesName (IReadOnlyList<string> files)
  280. {
  281. List<string> filesName = new List<string> ();
  282. foreach (var file in files) {
  283. filesName.Add (Path.GetFileName (file));
  284. }
  285. return filesName;
  286. }
  287. public bool GetValidFilesName (string files, out string result)
  288. {
  289. result = string.Empty;
  290. if (infos?.Count == 0) {
  291. return false;
  292. }
  293. var valid = true;
  294. IReadOnlyList<string> filesList = new List<string> (files.Split (splitString.ToArray (), StringSplitOptions.None));
  295. var filesName = new List<string> ();
  296. UnMarkAll ();
  297. foreach (var file in filesList) {
  298. if (!allowsMultipleSelection && filesName.Count > 0) {
  299. break;
  300. }
  301. var idx = infos.IndexOf (x => x.Item1.IndexOf (file, StringComparison.OrdinalIgnoreCase) >= 0);
  302. if (idx > -1 && string.Equals (infos [idx].Item1, file, StringComparison.OrdinalIgnoreCase)) {
  303. if (canChooseDirectories && !canChooseFiles && !infos [idx].Item2) {
  304. valid = false;
  305. }
  306. if (allowsMultipleSelection && !infos [idx].Item3) {
  307. infos [idx] = (infos [idx].Item1, infos [idx].Item2, true);
  308. }
  309. if (!allowsMultipleSelection) {
  310. selected = idx;
  311. }
  312. filesName.Add (Path.GetFileName (infos [idx].Item1));
  313. } else if (idx > -1) {
  314. valid = false;
  315. filesName.Add (Path.GetFileName (file));
  316. }
  317. }
  318. result = string.Join (splitString, filesName);
  319. if (string.IsNullOrEmpty (result)) {
  320. valid = false;
  321. }
  322. return valid;
  323. }
  324. public override bool ProcessKey (KeyEvent keyEvent)
  325. {
  326. switch (keyEvent.Key) {
  327. case Key.CursorUp:
  328. case Key.P | Key.CtrlMask:
  329. MoveUp ();
  330. return true;
  331. case Key.CursorDown:
  332. case Key.N | Key.CtrlMask:
  333. MoveDown ();
  334. return true;
  335. case Key.V | Key.CtrlMask:
  336. case Key.PageDown:
  337. var n = (selected + Frame.Height);
  338. if (n > infos.Count)
  339. n = infos.Count - 1;
  340. if (n != selected) {
  341. selected = n;
  342. if (infos.Count >= Frame.Height)
  343. top = selected;
  344. else
  345. top = 0;
  346. OnSelectionChanged ();
  347. SetNeedsDisplay ();
  348. }
  349. return true;
  350. case Key.Enter:
  351. UnMarkAll ();
  352. if (ExecuteSelection ())
  353. return false;
  354. else
  355. return true;
  356. case Key.PageUp:
  357. n = (selected - Frame.Height);
  358. if (n < 0)
  359. n = 0;
  360. if (n != selected) {
  361. selected = n;
  362. top = selected;
  363. OnSelectionChanged ();
  364. SetNeedsDisplay ();
  365. }
  366. return true;
  367. case Key.Space:
  368. case Key.T | Key.CtrlMask:
  369. PerformMultipleSelection ();
  370. return true;
  371. case Key.Home:
  372. MoveFirst ();
  373. return true;
  374. case Key.End:
  375. MoveLast ();
  376. return true;
  377. }
  378. return base.ProcessKey (keyEvent);
  379. }
  380. void MoveLast ()
  381. {
  382. selected = infos.Count - 1;
  383. top = infos.Count () - 1;
  384. OnSelectionChanged ();
  385. SetNeedsDisplay ();
  386. }
  387. void MoveFirst ()
  388. {
  389. selected = 0;
  390. top = 0;
  391. OnSelectionChanged ();
  392. SetNeedsDisplay ();
  393. }
  394. void MoveDown ()
  395. {
  396. if (selected + 1 < infos.Count) {
  397. selected++;
  398. if (selected >= top + Frame.Height)
  399. top++;
  400. OnSelectionChanged ();
  401. SetNeedsDisplay ();
  402. }
  403. }
  404. void MoveUp ()
  405. {
  406. if (selected > 0) {
  407. selected--;
  408. if (selected < top)
  409. top = selected;
  410. OnSelectionChanged ();
  411. SetNeedsDisplay ();
  412. }
  413. }
  414. internal bool ExecuteSelection (bool navigateFolder = true)
  415. {
  416. if (infos.Count == 0) {
  417. return false;
  418. }
  419. var isDir = infos [selected].Item2;
  420. if (isDir) {
  421. Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1));
  422. DirectoryChanged?.Invoke (Directory);
  423. if (canChooseDirectories && !navigateFolder) {
  424. return true;
  425. }
  426. } else {
  427. OnSelectionChanged ();
  428. if (canChooseFiles) {
  429. // Ensures that at least one file is selected.
  430. if (FilePaths.Count == 0)
  431. PerformMultipleSelection ();
  432. // Let the OK handler take it over
  433. return true;
  434. }
  435. // No files allowed, do not let the default handler take it.
  436. }
  437. return false;
  438. }
  439. void PerformMultipleSelection (int? firstSelected = null)
  440. {
  441. if (allowsMultipleSelection) {
  442. int first = Math.Min (firstSelected ?? selected, selected);
  443. int last = Math.Max (selected, firstSelected ?? selected);
  444. for (int i = first; i <= last; i++) {
  445. if ((canChooseFiles && infos [i].Item2 == false) ||
  446. (canChooseDirectories && infos [i].Item2 &&
  447. infos [i].Item1 != "..")) {
  448. infos [i] = (infos [i].Item1, infos [i].Item2, !infos [i].Item3);
  449. }
  450. }
  451. OnSelectionChanged ();
  452. SetNeedsDisplay ();
  453. }
  454. }
  455. string [] allowedFileTypes;
  456. public string [] AllowedFileTypes {
  457. get => allowedFileTypes;
  458. set {
  459. allowedFileTypes = value;
  460. Reload ();
  461. }
  462. }
  463. public string MakePath (string relativePath)
  464. {
  465. var dir = Directory.ToString ();
  466. return string.IsNullOrEmpty (dir) ? "" : Path.GetFullPath (Path.Combine (dir, relativePath));
  467. }
  468. public IReadOnlyList<string> FilePaths {
  469. get {
  470. if (allowsMultipleSelection) {
  471. var res = new List<string> ();
  472. foreach (var item in infos) {
  473. if (item.Item3)
  474. res.Add (MakePath (item.Item1));
  475. }
  476. if (res.Count == 0 && infos.Count > 0 && infos [selected].Item1 != "..") {
  477. res.Add (MakePath (infos [selected].Item1));
  478. }
  479. return res;
  480. } else {
  481. if (infos.Count == 0) {
  482. return null;
  483. }
  484. if (infos [selected].Item2) {
  485. if (canChooseDirectories) {
  486. var sel = infos [selected].Item1;
  487. return sel == ".." ? new List<string> () : new List<string> () { MakePath (infos [selected].Item1) };
  488. }
  489. return Array.Empty<string> ();
  490. } else {
  491. if (canChooseFiles) {
  492. return new List<string> () { MakePath (infos [selected].Item1) };
  493. }
  494. return Array.Empty<string> ();
  495. }
  496. }
  497. }
  498. }
  499. ///<inheritdoc/>
  500. public override bool OnEnter (View view)
  501. {
  502. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  503. return base.OnEnter (view);
  504. }
  505. }
  506. /// <summary>
  507. /// Base class for the <see cref="OpenDialog"/> and the <see cref="SaveDialog"/>
  508. /// </summary>
  509. public class FileDialog : Dialog {
  510. Button prompt, cancel;
  511. Label nameFieldLabel, message, nameDirLabel;
  512. TextField dirEntry, nameEntry;
  513. internal DirListView dirListView;
  514. ComboBox cmbAllowedTypes;
  515. /// <summary>
  516. /// Initializes a new <see cref="FileDialog"/>.
  517. /// </summary>
  518. public FileDialog () : this (title: string.Empty, prompt: string.Empty,
  519. nameFieldLabel: string.Empty, message: string.Empty)
  520. { }
  521. /// <summary>
  522. /// Initializes a new instance of <see cref="FileDialog"/>
  523. /// </summary>
  524. /// <param name="title">The title.</param>
  525. /// <param name="prompt">The prompt.</param>
  526. /// <param name="nameFieldLabel">The name of the file field label..</param>
  527. /// <param name="message">The message.</param>
  528. /// <param name="allowedTypes">The allowed types.</param>
  529. public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message, List<string> allowedTypes = null)
  530. : this (title, prompt, ustring.Empty, nameFieldLabel, message, allowedTypes) { }
  531. /// <summary>
  532. /// Initializes a new instance of <see cref="FileDialog"/>
  533. /// </summary>
  534. /// <param name="title">The title.</param>
  535. /// <param name="prompt">The prompt.</param>
  536. /// <param name="message">The message.</param>
  537. /// <param name="allowedTypes">The allowed types.</param>
  538. public FileDialog (ustring title, ustring prompt, ustring message, List<string> allowedTypes)
  539. : this (title, prompt, ustring.Empty, message, allowedTypes) { }
  540. /// <summary>
  541. /// Initializes a new instance of <see cref="FileDialog"/>
  542. /// </summary>
  543. /// <param name="title">The title.</param>
  544. /// <param name="prompt">The prompt.</param>
  545. /// <param name="nameDirLabel">The name of the directory field label.</param>
  546. /// <param name="nameFieldLabel">The name of the file field label..</param>
  547. /// <param name="message">The message.</param>
  548. /// <param name="allowedTypes">The allowed types.</param>
  549. public FileDialog (ustring title, ustring prompt, ustring nameDirLabel, ustring nameFieldLabel, ustring message,
  550. List<string> allowedTypes = null) : base (title)//, Driver.Cols - 20, Driver.Rows - 5, null)
  551. {
  552. this.message = new Label (message) {
  553. X = 1,
  554. Y = 0,
  555. };
  556. Add (this.message);
  557. var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
  558. this.nameDirLabel = new Label (nameDirLabel.IsEmpty ? $"{Strings.fdDirectory}: " : $"{nameDirLabel}: ") {
  559. X = 1,
  560. Y = 1 + msgLines,
  561. AutoSize = true
  562. };
  563. dirEntry = new TextField ("") {
  564. X = Pos.Right (this.nameDirLabel),
  565. Y = 1 + msgLines,
  566. Width = Dim.Fill () - 1,
  567. };
  568. dirEntry.TextChanged += (e) => {
  569. DirectoryPath = dirEntry.Text;
  570. nameEntry.Text = ustring.Empty;
  571. };
  572. Add (this.nameDirLabel, dirEntry);
  573. this.nameFieldLabel = new Label (nameFieldLabel.IsEmpty ? $"{Strings.fdFile}: " : $"{nameFieldLabel}: ") {
  574. X = 1,
  575. Y = 3 + msgLines,
  576. AutoSize = true
  577. };
  578. nameEntry = new TextField ("") {
  579. X = Pos.Left (dirEntry),
  580. Y = 3 + msgLines,
  581. Width = Dim.Percent (70, true)
  582. };
  583. Add (this.nameFieldLabel, nameEntry);
  584. cmbAllowedTypes = new ComboBox () {
  585. X = Pos.Right (nameEntry) + 2,
  586. Y = Pos.Top (nameEntry),
  587. Width = Dim.Fill (1),
  588. Height = allowedTypes != null ? allowedTypes.Count + 1 : 1,
  589. Text = allowedTypes?.Count > 0 ? allowedTypes [0] : string.Empty,
  590. ReadOnly = true
  591. };
  592. cmbAllowedTypes.SetSource (allowedTypes ?? new List<string> ());
  593. cmbAllowedTypes.OpenSelectedItem += (e) => AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
  594. Add (cmbAllowedTypes);
  595. dirListView = new DirListView (this) {
  596. X = 1,
  597. Y = 3 + msgLines + 2,
  598. Width = Dim.Fill () - 1,
  599. Height = Dim.Fill () - 2,
  600. };
  601. DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
  602. Add (dirListView);
  603. AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
  604. dirListView.DirectoryChanged = (dir) => { nameEntry.Text = ustring.Empty; dirEntry.Text = dir; };
  605. dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
  606. dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
  607. this.cancel = new Button ("Cancel");
  608. this.cancel.Clicked += () => {
  609. Cancel ();
  610. };
  611. AddButton (cancel);
  612. this.prompt = new Button (prompt.IsEmpty ? "Ok" : prompt) {
  613. IsDefault = true,
  614. Enabled = nameEntry.Text.IsEmpty ? false : true
  615. };
  616. this.prompt.Clicked += () => {
  617. if (this is OpenDialog) {
  618. if (!dirListView.GetValidFilesName (nameEntry.Text.ToString (), out string res)) {
  619. nameEntry.Text = res;
  620. dirListView.SetNeedsDisplay ();
  621. return;
  622. }
  623. if (!dirListView.canChooseDirectories && !dirListView.ExecuteSelection (false)) {
  624. return;
  625. }
  626. } else if (this is SaveDialog) {
  627. var name = nameEntry.Text.ToString ();
  628. if (FilePath.IsEmpty || name.Split (',').Length > 1) {
  629. return;
  630. }
  631. var ext = name.EndsWith (cmbAllowedTypes.Text.ToString ())
  632. ? "" : cmbAllowedTypes.Text.ToString ();
  633. FilePath = Path.Combine (FilePath.ToString (), $"{name}{ext}");
  634. }
  635. canceled = false;
  636. Application.RequestStop ();
  637. };
  638. AddButton (this.prompt);
  639. nameEntry.TextChanged += (e) => {
  640. if (nameEntry.Text.IsEmpty) {
  641. this.prompt.Enabled = false;
  642. } else {
  643. this.prompt.Enabled = true;
  644. }
  645. };
  646. Width = Dim.Percent (80);
  647. Height = Dim.Percent (80);
  648. // On success, we will set this to false.
  649. canceled = true;
  650. KeyPress += (e) => {
  651. if (e.KeyEvent.Key == Key.Esc) {
  652. Cancel ();
  653. e.Handled = true;
  654. }
  655. };
  656. void Cancel ()
  657. {
  658. canceled = true;
  659. Application.RequestStop ();
  660. }
  661. }
  662. internal bool canceled;
  663. ///<inheritdoc/>
  664. public override void WillPresent ()
  665. {
  666. base.WillPresent ();
  667. dirListView.SetFocus ();
  668. }
  669. //protected override void Dispose (bool disposing)
  670. //{
  671. // message?.Dispose ();
  672. // base.Dispose (disposing);
  673. //}
  674. /// <summary>
  675. /// Gets or sets the prompt label for the <see cref="Button"/> displayed to the user
  676. /// </summary>
  677. /// <value>The prompt.</value>
  678. public ustring Prompt {
  679. get => prompt.Text;
  680. set {
  681. prompt.Text = value;
  682. }
  683. }
  684. /// <summary>
  685. /// Gets or sets the name of the directory field label.
  686. /// </summary>
  687. /// <value>The name of the directory field label.</value>
  688. public ustring NameDirLabel {
  689. get => nameDirLabel.Text;
  690. set {
  691. nameDirLabel.Text = $"{value}: ";
  692. }
  693. }
  694. /// <summary>
  695. /// Gets or sets the name field label.
  696. /// </summary>
  697. /// <value>The name field label.</value>
  698. public ustring NameFieldLabel {
  699. get => nameFieldLabel.Text;
  700. set {
  701. nameFieldLabel.Text = $"{value}: ";
  702. }
  703. }
  704. /// <summary>
  705. /// Gets or sets the message displayed to the user, defaults to nothing
  706. /// </summary>
  707. /// <value>The message.</value>
  708. public ustring Message {
  709. get => message.Text;
  710. set {
  711. message.Text = value;
  712. }
  713. }
  714. /// <summary>
  715. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> can create directories.
  716. /// </summary>
  717. /// <value><c>true</c> if can create directories; otherwise, <c>false</c>.</value>
  718. public bool CanCreateDirectories { get; set; }
  719. /// <summary>
  720. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> is extension hidden.
  721. /// </summary>
  722. /// <value><c>true</c> if is extension hidden; otherwise, <c>false</c>.</value>
  723. public bool IsExtensionHidden { get; set; }
  724. /// <summary>
  725. /// Gets or sets the directory path for this panel
  726. /// </summary>
  727. /// <value>The directory path.</value>
  728. public ustring DirectoryPath {
  729. get => dirEntry.Text;
  730. set {
  731. dirEntry.Text = value;
  732. dirListView.Directory = value;
  733. }
  734. }
  735. /// <summary>
  736. /// The array of filename extensions allowed, or null if all file extensions are allowed.
  737. /// </summary>
  738. /// <value>The allowed file types.</value>
  739. public string [] AllowedFileTypes {
  740. get => dirListView.AllowedFileTypes;
  741. set => dirListView.AllowedFileTypes = value;
  742. }
  743. /// <summary>
  744. /// Gets or sets a value indicating whether this <see cref="FileDialog"/> allows the file to be saved with a different extension
  745. /// </summary>
  746. /// <value><c>true</c> if allows other file types; otherwise, <c>false</c>.</value>
  747. public bool AllowsOtherFileTypes { get; set; }
  748. /// <summary>
  749. /// The File path that is currently shown on the panel
  750. /// </summary>
  751. /// <value>The absolute file path for the file path entered.</value>
  752. public ustring FilePath {
  753. get => dirListView.MakePath (nameEntry.Text.ToString ());
  754. set {
  755. nameEntry.Text = Path.GetFileName (value.ToString ());
  756. }
  757. }
  758. /// <summary>
  759. /// Check if the dialog was or not canceled.
  760. /// </summary>
  761. public bool Canceled { get => canceled; }
  762. }
  763. /// <summary>
  764. /// The <see cref="SaveDialog"/> provides an interactive dialog box for users to pick a file to
  765. /// save.
  766. /// </summary>
  767. /// <remarks>
  768. /// <para>
  769. /// To use, create an instance of <see cref="SaveDialog"/>, and pass it to
  770. /// <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
  771. /// and when this returns, the <see cref="FileName"/>property will contain the selected file name or
  772. /// null if the user canceled.
  773. /// </para>
  774. /// </remarks>
  775. public class SaveDialog : FileDialog {
  776. /// <summary>
  777. /// Initializes a new <see cref="SaveDialog"/>.
  778. /// </summary>
  779. public SaveDialog () : this (title: string.Empty, message: string.Empty) { }
  780. /// <summary>
  781. /// Initializes a new <see cref="SaveDialog"/>.
  782. /// </summary>
  783. /// <param name="title">The title.</param>
  784. /// <param name="message">The message.</param>
  785. /// <param name="allowedTypes">The allowed types.</param>
  786. public SaveDialog (ustring title, ustring message, List<string> allowedTypes = null)
  787. : base (title, prompt: Strings.fdSave, nameFieldLabel: $"{Strings.fdSaveAs}:", message: message, allowedTypes) { }
  788. /// <summary>
  789. /// Gets the name of the file the user selected for saving, or null
  790. /// if the user canceled the <see cref="SaveDialog"/>.
  791. /// </summary>
  792. /// <value>The name of the file.</value>
  793. public ustring FileName {
  794. get {
  795. if (canceled)
  796. return null;
  797. return Path.GetFileName (FilePath.ToString ());
  798. }
  799. }
  800. }
  801. /// <summary>
  802. /// The <see cref="OpenDialog"/>provides an interactive dialog box for users to select files or directories.
  803. /// </summary>
  804. /// <remarks>
  805. /// <para>
  806. /// The open dialog can be used to select files for opening, it can be configured to allow
  807. /// multiple items to be selected (based on the AllowsMultipleSelection) variable and
  808. /// you can control whether this should allow files or directories to be selected.
  809. /// </para>
  810. /// <para>
  811. /// To use, create an instance of <see cref="OpenDialog"/>, and pass it to
  812. /// <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
  813. /// and when this returns, the list of files will be available on the <see cref="FilePaths"/> property.
  814. /// </para>
  815. /// <para>
  816. /// To select more than one file, users can use the spacebar, or control-t.
  817. /// </para>
  818. /// </remarks>
  819. public class OpenDialog : FileDialog {
  820. OpenMode openMode;
  821. /// <summary>
  822. /// Determine which <see cref="System.IO"/> type to open.
  823. /// </summary>
  824. public enum OpenMode {
  825. /// <summary>
  826. /// Opens only file or files.
  827. /// </summary>
  828. File,
  829. /// <summary>
  830. /// Opens only directory or directories.
  831. /// </summary>
  832. Directory,
  833. /// <summary>
  834. /// Opens files and directories.
  835. /// </summary>
  836. Mixed
  837. }
  838. /// <summary>
  839. /// Initializes a new <see cref="OpenDialog"/>.
  840. /// </summary>
  841. public OpenDialog () : this (title: string.Empty, message: string.Empty) { }
  842. /// <summary>
  843. /// Initializes a new <see cref="OpenDialog"/>.
  844. /// </summary>
  845. /// <param name="title">The title.</param>
  846. /// <param name="message">The message.</param>
  847. /// <param name="allowedTypes">The allowed types.</param>
  848. /// <param name="openMode">The open mode.</param>
  849. public OpenDialog (ustring title, ustring message, List<string> allowedTypes = null, OpenMode openMode = OpenMode.File) : base (title,
  850. prompt: openMode == OpenMode.File ? Strings.fdOpen : openMode == OpenMode.Directory ? Strings.fdSelectFolder : Strings.fdSelectMixed,
  851. nameFieldLabel: Strings.fdOpen, message: message, allowedTypes)
  852. {
  853. this.openMode = openMode;
  854. switch (openMode) {
  855. case OpenMode.File:
  856. CanChooseFiles = true;
  857. CanChooseDirectories = false;
  858. break;
  859. case OpenMode.Directory:
  860. CanChooseFiles = false;
  861. CanChooseDirectories = true;
  862. break;
  863. case OpenMode.Mixed:
  864. CanChooseFiles = true;
  865. CanChooseDirectories = true;
  866. AllowsMultipleSelection = true;
  867. break;
  868. }
  869. }
  870. /// <summary>
  871. /// Gets or sets a value indicating whether this <see cref="Terminal.Gui.OpenDialog"/> can choose files.
  872. /// </summary>
  873. /// <value><c>true</c> if can choose files; otherwise, <c>false</c>. Defaults to <c>true</c></value>
  874. public bool CanChooseFiles {
  875. get => dirListView.canChooseFiles;
  876. set {
  877. dirListView.canChooseFiles = value;
  878. dirListView.Reload ();
  879. }
  880. }
  881. /// <summary>
  882. /// Gets or sets a value indicating whether this <see cref="OpenDialog"/> can choose directories.
  883. /// </summary>
  884. /// <value><c>true</c> if can choose directories; otherwise, <c>false</c> defaults to <c>false</c>.</value>
  885. public bool CanChooseDirectories {
  886. get => dirListView.canChooseDirectories;
  887. set {
  888. dirListView.canChooseDirectories = value;
  889. dirListView.Reload ();
  890. }
  891. }
  892. /// <summary>
  893. /// Gets or sets a value indicating whether this <see cref="OpenDialog"/> allows multiple selection.
  894. /// </summary>
  895. /// <value><c>true</c> if allows multiple selection; otherwise, <c>false</c>, defaults to false.</value>
  896. public bool AllowsMultipleSelection {
  897. get => dirListView.allowsMultipleSelection;
  898. set {
  899. if (!value && openMode == OpenMode.Mixed) {
  900. return;
  901. }
  902. dirListView.allowsMultipleSelection = value;
  903. dirListView.Reload ();
  904. }
  905. }
  906. /// <summary>
  907. /// Returns the selected files, or an empty list if nothing has been selected
  908. /// </summary>
  909. /// <value>The file paths.</value>
  910. public IReadOnlyList<string> FilePaths {
  911. get => dirListView.FilePaths;
  912. }
  913. }
  914. }