FileDialog.cs 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657
  1. using System.IO.Abstractions;
  2. using System.Text.RegularExpressions;
  3. namespace Terminal.Gui.Views;
  4. /// <summary>
  5. /// The base-class for <see cref="OpenDialog"/> and <see cref="SaveDialog"/>
  6. /// </summary>
  7. public class FileDialog : Dialog, IDesignable
  8. {
  9. private const int ALIGNMENT_GROUP_COMPLETE = 55;
  10. /// <summary>Gets the Path separators for the operating system</summary>
  11. // ReSharper disable once InconsistentNaming
  12. internal static char [] Separators =
  13. [
  14. System.IO.Path.AltDirectorySeparatorChar,
  15. System.IO.Path.DirectorySeparatorChar
  16. ];
  17. /// <summary>
  18. /// Characters to prevent entry into <see cref="_tbPath"/>. Note that this is not using
  19. /// <see cref="System.IO.Path.GetInvalidFileNameChars"/> because we do want to allow directory separators, arrow keys
  20. /// etc.
  21. /// </summary>
  22. private static readonly char [] _badChars = ['"', '<', '>', '|', '*', '?'];
  23. /// <summary>Locking object for ensuring only a single <see cref="SearchState"/> executes at once.</summary>
  24. internal object _onlyOneSearchLock = new ();
  25. private readonly Button _btnBack;
  26. private readonly Button _btnCancel;
  27. private readonly Button _btnForward;
  28. private readonly Button _btnOk;
  29. private readonly Button _btnUp;
  30. private readonly Button _btnTreeToggle;
  31. private readonly IFileSystem? _fileSystem;
  32. private readonly FileDialogHistory _history;
  33. private readonly SpinnerView _spinnerView;
  34. private readonly View _tableViewContainer;
  35. private readonly TableView _tableView;
  36. private readonly TextField _tbFind;
  37. private readonly TextField _tbPath;
  38. private readonly TreeView<IFileSystemInfo> _treeView;
  39. #if MENU_V1
  40. private MenuBarItem? _allowedTypeMenu;
  41. private MenuBar? _allowedTypeMenuBar;
  42. private MenuItem []? _allowedTypeMenuItems;
  43. #endif
  44. private int _currentSortColumn;
  45. private bool _currentSortIsAsc = true;
  46. private bool _disposed;
  47. private string? _feedback;
  48. private bool _loaded;
  49. private bool _pushingState;
  50. private Dictionary<IDirectoryInfo, string> _treeRoots = new ();
  51. /// <summary>Initializes a new instance of the <see cref="FileDialog"/> class.</summary>
  52. public FileDialog () : this (new FileSystem ()) { }
  53. /// <summary>Initializes a new instance of the <see cref="FileDialog"/> class with a custom <see cref="IFileSystem"/>.</summary>
  54. /// <remarks>This overload is mainly useful for testing.</remarks>
  55. internal FileDialog (IFileSystem? fileSystem)
  56. {
  57. Height = Dim.Percent (80);
  58. Width = Dim.Percent (80);
  59. // Assume canceled
  60. Canceled = true;
  61. _fileSystem = fileSystem;
  62. Style = new (fileSystem);
  63. _btnOk = new ()
  64. {
  65. X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_COMPLETE),
  66. Y = Pos.AnchorEnd (),
  67. IsDefault = true, Text = Style.OkButtonText
  68. };
  69. _btnOk.Accepting += (s, e) =>
  70. {
  71. if (e.Handled)
  72. {
  73. return;
  74. }
  75. Accept (true);
  76. e.Handled = true;
  77. };
  78. _btnCancel = new ()
  79. {
  80. X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_COMPLETE),
  81. Y = Pos.AnchorEnd (),
  82. Text = Strings.btnCancel
  83. };
  84. _btnCancel.Accepting += (s, e) =>
  85. {
  86. if (e.Handled)
  87. {
  88. return;
  89. }
  90. e.Handled = true;
  91. if (Modal)
  92. {
  93. (s as View)?.App?.RequestStop ();
  94. }
  95. };
  96. // Tree toggle button - shares alignment group with OK/Cancel
  97. _btnTreeToggle = new ()
  98. {
  99. X = 0,//Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_COMPLETE),
  100. Y = Pos.AnchorEnd (),
  101. NoPadding = true
  102. };
  103. _btnTreeToggle.Accepting += (s, e) =>
  104. {
  105. e.Handled = true;
  106. ToggleTreeVisibility ();
  107. };
  108. _btnUp = new () { X = 0, Y = 1, NoPadding = true };
  109. _btnUp.Text = GetUpButtonText ();
  110. _btnUp.Accepting += (s, e) =>
  111. {
  112. _history?.Up ();
  113. e.Handled = true;
  114. };
  115. _btnBack = new () { X = Pos.Right (_btnUp) + 1, Y = 1, NoPadding = true };
  116. _btnBack.Text = GetBackButtonText ();
  117. _btnBack.Accepting += (s, e) =>
  118. {
  119. _history?.Back ();
  120. e.Handled = true;
  121. };
  122. _btnForward = new () { X = Pos.Right (_btnBack) + 1, Y = 1, NoPadding = true };
  123. _btnForward.Text = GetForwardButtonText ();
  124. _btnForward.Accepting += (s, e) =>
  125. {
  126. _history?.Forward ();
  127. e.Handled = true;
  128. };
  129. _tbPath = new () { Width = Dim.Fill () };
  130. _tbPath.KeyDown += (s, k) =>
  131. {
  132. ClearFeedback ();
  133. AcceptIf (k, KeyCode.Enter);
  134. SuppressIfBadChar (k);
  135. };
  136. _tbPath.Autocomplete = new AppendAutocomplete (_tbPath);
  137. _tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator ();
  138. // Create tree view container (left pane)
  139. _treeView = new ()
  140. {
  141. X = 0,
  142. Y = Pos.Bottom (_btnBack),
  143. Width = Dim.Fill (Dim.Func (_ => IsInitialized ? _tableViewContainer!.Frame.Width - 30 : 30)),
  144. Height = Dim.Fill (Dim.Func (_ => IsInitialized ? _btnOk.Frame.Height : 1)),
  145. Visible = false
  146. };
  147. // Create table view container (right pane)
  148. _tableViewContainer = new ()
  149. {
  150. X = 0,
  151. Y = Pos.Bottom (_btnBack),
  152. Width = Dim.Fill (),
  153. Height = Dim.Fill (Dim.Func (_ => IsInitialized ? _btnOk.Frame.Height : 1)),
  154. Arrangement = ViewArrangement.LeftResizable,
  155. BorderStyle = LineStyle.Dashed,
  156. SuperViewRendersLineCanvas = true,
  157. CanFocus = true,
  158. Id = "_tableViewContainer"
  159. };
  160. _tableView = new ()
  161. {
  162. Width = Dim.Fill (),
  163. Height = Dim.Fill (1),
  164. FullRowSelect = true,
  165. Id = "_tableView"
  166. };
  167. _tableView.CollectionNavigator = new FileDialogCollectionNavigator (this, _tableView);
  168. _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select);
  169. _tableView.MouseClick += OnTableViewMouseClick;
  170. Style.TableStyle = _tableView.Style;
  171. ColumnStyle nameStyle = Style.TableStyle.GetOrCreateColumnStyle (0);
  172. nameStyle.MinWidth = 10;
  173. nameStyle.ColorGetter = ColorGetter;
  174. ColumnStyle sizeStyle = Style.TableStyle.GetOrCreateColumnStyle (1);
  175. sizeStyle.MinWidth = 10;
  176. sizeStyle.ColorGetter = ColorGetter;
  177. ColumnStyle dateModifiedStyle = Style.TableStyle.GetOrCreateColumnStyle (2);
  178. dateModifiedStyle.MinWidth = 30;
  179. dateModifiedStyle.ColorGetter = ColorGetter;
  180. ColumnStyle typeStyle = Style.TableStyle.GetOrCreateColumnStyle (3);
  181. typeStyle.MinWidth = 6;
  182. typeStyle.ColorGetter = ColorGetter;
  183. var fileDialogTreeBuilder = new FileSystemTreeBuilder ();
  184. _treeView.TreeBuilder = fileDialogTreeBuilder;
  185. _treeView.AspectGetter = AspectGetter;
  186. Style.TreeStyle = _treeView.Style;
  187. _treeView.SelectionChanged += TreeView_SelectionChanged;
  188. _tableViewContainer.Add (_tableView);
  189. _tableView.Style.ShowHorizontalHeaderOverline = true;
  190. _tableView.Style.ShowVerticalCellLines = true;
  191. _tableView.Style.ShowVerticalHeaderLines = true;
  192. _tableView.Style.AlwaysShowHeaders = true;
  193. _tableView.Style.ShowHorizontalHeaderUnderline = true;
  194. _tableView.Style.ShowHorizontalScrollIndicators = true;
  195. _history = new (this);
  196. _tbPath.TextChanged += (s, e) => PathChanged ();
  197. _tableView.CellActivated += CellActivate;
  198. _tableView.KeyDown += (s, k) => k.Handled = TableView_KeyUp (k);
  199. _tableView.SelectedCellChanged += TableView_SelectedCellChanged;
  200. _tableView.KeyBindings.ReplaceCommands (Key.Home, Command.Start);
  201. _tableView.KeyBindings.ReplaceCommands (Key.End, Command.End);
  202. _tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.StartExtend);
  203. _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.EndExtend);
  204. _tbFind = new ()
  205. {
  206. X = 0,
  207. Width = Dim.Fill (),
  208. Y = Pos.AnchorEnd (),
  209. Id = "_tbFind",
  210. };
  211. _spinnerView = new ()
  212. {
  213. // The spinner view is positioned over the last column of _tbFind
  214. X = Pos.Right (_tbFind) - 1,
  215. Y = Pos.Top (_tbFind),
  216. Visible = false
  217. };
  218. _tbFind.TextChanged += (s, o) => RestartSearch ();
  219. _tbFind.KeyDown += (s, o) =>
  220. {
  221. if (o.KeyCode == KeyCode.Enter)
  222. {
  223. RestartSearch ();
  224. o.Handled = true;
  225. }
  226. if (o.KeyCode == KeyCode.Esc)
  227. {
  228. if (CancelSearch ())
  229. {
  230. o.Handled = true;
  231. }
  232. }
  233. };
  234. AllowsMultipleSelection = false;
  235. UpdateNavigationVisibility ();
  236. base.Add (_tbPath);
  237. base.Add (_btnUp);
  238. base.Add (_btnBack);
  239. base.Add (_btnForward);
  240. base.Add (_treeView);
  241. base.Add (_tableViewContainer);
  242. _tableViewContainer.Add (_tbFind);
  243. _tableViewContainer.Add (_spinnerView);
  244. // Add the toggle along with OK/Cancel so they align as a group
  245. base.Add (_btnTreeToggle);
  246. base.Add (_btnOk);
  247. base.Add (_btnCancel);
  248. // Default: Tree hidden and splitter hidden
  249. SetTreeVisible (false);
  250. }
  251. /// <summary>
  252. /// Gets or Sets a collection of file types that the user can/must select. Only applies when
  253. /// <see cref="OpenMode"/> is <see cref="OpenMode.File"/> or <see cref="OpenMode.Mixed"/>.
  254. /// </summary>
  255. /// <remarks>
  256. /// <see cref="AllowedTypeAny"/> adds the option to select any type (*.*). If this collection is empty then any
  257. /// type is supported and no Types drop-down is shown.
  258. /// </remarks>
  259. public List<IAllowedType> AllowedTypes { get; set; } = [];
  260. /// <summary>
  261. /// Gets or Sets a value indicating whether to allow selecting multiple existing files/directories. Defaults to
  262. /// false.
  263. /// </summary>
  264. public bool AllowsMultipleSelection
  265. {
  266. get => _tableView.MultiSelect;
  267. set => _tableView.MultiSelect = value;
  268. }
  269. /// <summary>The UI selected <see cref="IAllowedType"/> from combo box. May be null.</summary>
  270. public IAllowedType? CurrentFilter { get; private set; }
  271. /// <summary>
  272. /// Gets or sets behavior of the <see cref="FileDialog"/> when the user attempts to delete a selected file(s). Set
  273. /// to null to prevent deleting.
  274. /// </summary>
  275. /// <remarks>
  276. /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a <see cref="MessageBox"/>
  277. /// </remarks>
  278. public IFileOperations FileOperationsHandler { get; set; } = new DefaultFileOperations ();
  279. /// <summary>The maximum number of results that will be collected when searching before stopping.</summary>
  280. /// <remarks>This prevents performance issues e.g. when searching root of file system for a common letter (e.g. 'e').</remarks>
  281. [ConfigurationProperty (Scope = typeof (SettingsScope))]
  282. public static int MaxSearchResults { get; set; } = 10000;
  283. /// <summary>
  284. /// Gets all files/directories selected or an empty collection <see cref="AllowsMultipleSelection"/> is
  285. /// <see langword="false"/> or <see cref="CancelSearch"/>.
  286. /// </summary>
  287. /// <remarks>If selecting only a single file/directory then you should use <see cref="Path"/> instead.</remarks>
  288. public IReadOnlyList<string> MultiSelected { get; private set; }
  289. = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
  290. /// <summary>
  291. /// True if the file/folder must exist already to be selected. This prevents user from entering the name of
  292. /// something that doesn't exist. Defaults to false.
  293. /// </summary>
  294. public bool MustExist { get; set; }
  295. /// <summary>
  296. /// Gets or Sets which <see cref="System.IO.FileSystemInfo"/> type can be selected. Defaults to
  297. /// <see cref="OpenMode.Mixed"/> (i.e. <see cref="DirectoryInfo"/> or <see cref="FileInfo"/>).
  298. /// </summary>
  299. public virtual OpenMode OpenMode { get; set; } = OpenMode.Mixed;
  300. /// <summary>
  301. /// Gets or Sets the selected path in the dialog. This is the result that should be used if
  302. /// <see cref="AllowsMultipleSelection"/> is off and <see cref="CancelSearch"/> is true.
  303. /// </summary>
  304. public string Path
  305. {
  306. get => _tbPath.Text;
  307. set
  308. {
  309. _tbPath.Text = value;
  310. _tbPath.MoveEnd ();
  311. }
  312. }
  313. /// <summary>
  314. /// Defines how the dialog matches files/folders when using the search box. Provide a custom implementation if you
  315. /// want to tailor how matching is performed.
  316. /// </summary>
  317. public ISearchMatcher SearchMatcher { get; set; } = new DefaultSearchMatcher ();
  318. /// <summary>
  319. /// Gets settings for controlling how visual elements behave. Style changes should be made before the
  320. /// <see cref="Dialog"/> is loaded and shown to the user for the first time.
  321. /// </summary>
  322. public FileDialogStyle Style { get; }
  323. /// <summary>Gets the currently open directory and known children presented in the dialog.</summary>
  324. internal FileDialogState? State { get; private set; }
  325. /// <summary>
  326. /// Event fired when user attempts to confirm a selection (or multi selection). Allows you to cancel the selection
  327. /// or undertake alternative behavior e.g. open a dialog "File already exists, Overwrite? yes/no".
  328. /// </summary>
  329. public event EventHandler<FilesSelectedEventArgs>? FilesSelected;
  330. /// <summary>
  331. /// Returns true if there are no <see cref="AllowedTypes"/> or one of them agrees that <paramref name="file"/>
  332. /// <see cref="IAllowedType.IsAllowed(string)"/>.
  333. /// </summary>
  334. /// <param name="file"></param>
  335. /// <returns></returns>
  336. public bool IsCompatibleWithAllowedExtensions (IFileInfo file)
  337. {
  338. // no restrictions
  339. if (!AllowedTypes.Any ())
  340. {
  341. return true;
  342. }
  343. return MatchesAllowedTypes (file);
  344. }
  345. /// <inheritdoc/>
  346. protected override bool OnDrawingContent ()
  347. {
  348. if (!string.IsNullOrWhiteSpace (_feedback))
  349. {
  350. int feedbackWidth = _feedback.EnumerateRunes ().Sum (c => c.GetColumns ());
  351. int feedbackPadLeft = (Viewport.Width - feedbackWidth) / 2 - 1;
  352. feedbackPadLeft = Math.Min (Viewport.Width, feedbackPadLeft);
  353. feedbackPadLeft = Math.Max (0, feedbackPadLeft);
  354. int feedbackPadRight = Viewport.Width - (feedbackPadLeft + feedbackWidth + 2);
  355. feedbackPadRight = Math.Min (Viewport.Width, feedbackPadRight);
  356. feedbackPadRight = Math.Max (0, feedbackPadRight);
  357. Move (0, Viewport.Height / 2);
  358. SetAttribute (new (Color.Red, GetAttributeForRole (VisualRole.Normal).Background));
  359. AddStr (new (' ', feedbackPadLeft));
  360. AddStr (_feedback);
  361. AddStr (new (' ', feedbackPadRight));
  362. }
  363. return true;
  364. }
  365. /// <inheritdoc/>
  366. public override void OnLoaded ()
  367. {
  368. base.OnLoaded ();
  369. if (_loaded)
  370. {
  371. return;
  372. }
  373. Arrangement |= ViewArrangement.Resizable;
  374. _loaded = true;
  375. // May have been updated after instance was constructed
  376. _btnOk.Text = Style.OkButtonText;
  377. _btnCancel.Text = Style.CancelButtonText;
  378. _btnUp.Text = GetUpButtonText ();
  379. _btnBack.Text = GetBackButtonText ();
  380. _btnForward.Text = GetForwardButtonText ();
  381. _tbPath.Title = Style.PathCaption;
  382. _tbFind.Title = Style.SearchCaption;
  383. _tbPath.Autocomplete.Scheme = new (_tbPath.GetScheme ())
  384. {
  385. Normal = new (Color.Black, _tbPath.GetAttributeForRole (VisualRole.Normal).Background)
  386. };
  387. _treeRoots = Style.TreeRootGetter ();
  388. Style.IconProvider.IsOpenGetter = _treeView.IsExpanded;
  389. _treeView.AddObjects (_treeRoots.Keys);
  390. // if filtering on file type is configured then create the ComboBox and establish
  391. // initial filtering by extension(s)
  392. if (AllowedTypes.Any ())
  393. {
  394. CurrentFilter = AllowedTypes [0];
  395. // Fiddle factor
  396. int width = AllowedTypes.Max (a => a.ToString ()!.Length) + 6;
  397. #if MENU_V1
  398. _allowedTypeMenu = new (
  399. "<placeholder>",
  400. _allowedTypeMenuItems = AllowedTypes.Select (
  401. (a, i) => new MenuItem (
  402. a.ToString (),
  403. null,
  404. () => { AllowedTypeMenuClicked (i); })
  405. )
  406. .ToArray ()
  407. );
  408. _allowedTypeMenuBar = new ()
  409. {
  410. Width = width,
  411. Y = 1,
  412. X = Pos.AnchorEnd (width),
  413. // TODO: Does not work, if this worked then we could tab to it instead
  414. // of having to hit F9
  415. CanFocus = true,
  416. TabStop = TabBehavior.TabStop,
  417. Menus = [_allowedTypeMenu]
  418. };
  419. AllowedTypeMenuClicked (0);
  420. // TODO: Using v1's menu bar here is a hack. Need to upgrade this.
  421. _allowedTypeMenuBar.DrawingContent += (s, e) =>
  422. {
  423. _allowedTypeMenuBar.Move (e.NewViewport.Width - 1, 0);
  424. AddRune (Glyphs.DownArrow);
  425. };
  426. Add (_allowedTypeMenuBar);
  427. #endif
  428. }
  429. // if no path has been provided
  430. if (_tbPath.Text.Length <= 0)
  431. {
  432. Path = _fileSystem!.Directory.GetCurrentDirectory ();
  433. }
  434. // to streamline user experience and allow direct typing of paths
  435. // with zero navigation we start with focus in the text box and any
  436. // default/current path fully selected and ready to be overwritten
  437. _tbPath.SetFocus ();
  438. _tbPath.SelectAll ();
  439. if (string.IsNullOrEmpty (Title))
  440. {
  441. Title = GetDefaultTitle ();
  442. }
  443. if (Style.FlipOkCancelButtonLayoutOrder)
  444. {
  445. _btnCancel.X = Pos.Func (CalculateOkButtonPosX);
  446. _btnOk.X = Pos.Right (_btnCancel) + 1;
  447. MoveSubViewTowardsStart (_btnCancel);
  448. }
  449. // Ensure toggle button text matches current state after sizing
  450. SetTreeVisible (false);
  451. SetNeedsDraw ();
  452. SetNeedsLayout ();
  453. }
  454. /// <inheritdoc/>
  455. protected override void Dispose (bool disposing)
  456. {
  457. _disposed = true;
  458. base.Dispose (disposing);
  459. CancelSearch ();
  460. }
  461. /// <summary>
  462. /// Gets a default dialog title, when <see cref="View.Title"/> is not set or empty, result of the function will be
  463. /// shown.
  464. /// </summary>
  465. protected virtual string GetDefaultTitle ()
  466. {
  467. List<string> titleParts = [Strings.fdOpen];
  468. if (MustExist)
  469. {
  470. titleParts.Add (Strings.fdExisting);
  471. }
  472. switch (OpenMode)
  473. {
  474. case OpenMode.File:
  475. titleParts.Add (Strings.fdFile);
  476. break;
  477. case OpenMode.Directory:
  478. titleParts.Add (Strings.fdDirectory);
  479. break;
  480. }
  481. return string.Join (' ', titleParts);
  482. }
  483. internal void ApplySort ()
  484. {
  485. FileSystemInfoStats [] stats = State?.Children ?? [];
  486. // This portion is never reordered (always .. at top then folders)
  487. IOrderedEnumerable<FileSystemInfoStats> forcedOrder = stats
  488. .OrderByDescending (f => f.IsParent)
  489. .ThenBy (f => f.IsDir ? -1 : 100);
  490. // This portion is flexible based on the column clicked (e.g. alphabetical)
  491. IOrderedEnumerable<FileSystemInfoStats> ordered =
  492. _currentSortIsAsc
  493. ? forcedOrder.ThenBy (
  494. f =>
  495. FileDialogTableSource.GetRawColumnValue (_currentSortColumn, f)
  496. )
  497. : forcedOrder.ThenByDescending (
  498. f =>
  499. FileDialogTableSource.GetRawColumnValue (_currentSortColumn, f)
  500. );
  501. if (State is { })
  502. {
  503. State.Children = ordered.ToArray ();
  504. }
  505. _tableView.Update ();
  506. }
  507. /// <summary>Changes the dialog such that <paramref name="d"/> is being explored.</summary>
  508. /// <param name="d"></param>
  509. /// <param name="addCurrentStateToHistory"></param>
  510. /// <param name="setPathText"></param>
  511. /// <param name="clearForward"></param>
  512. /// <param name="pathText">Optional alternate string to set path to.</param>
  513. internal void PushState (
  514. IDirectoryInfo d,
  515. bool addCurrentStateToHistory,
  516. bool setPathText = true,
  517. bool clearForward = true,
  518. string? pathText = null
  519. )
  520. {
  521. // no change of state
  522. if (d == State?.Directory)
  523. {
  524. return;
  525. }
  526. if (d.FullName == State?.Directory.FullName)
  527. {
  528. return;
  529. }
  530. PushState (
  531. new FileDialogState (d, this),
  532. addCurrentStateToHistory,
  533. setPathText,
  534. clearForward,
  535. pathText
  536. );
  537. }
  538. /// <summary>Select <paramref name="toRestore"/> in the table view (if present)</summary>
  539. /// <param name="toRestore"></param>
  540. internal void RestoreSelection (IFileSystemInfo toRestore)
  541. {
  542. _tableView.SelectedRow = State!.Children.IndexOf (r => r.FileSystemInfo == toRestore);
  543. _tableView.EnsureSelectedCellIsVisible ();
  544. }
  545. internal void SortColumn (int col, bool isAsc)
  546. {
  547. // set a sort order
  548. _currentSortColumn = col;
  549. _currentSortIsAsc = isAsc;
  550. ApplySort ();
  551. }
  552. private void Accept (IEnumerable<FileSystemInfoStats> toMultiAccept)
  553. {
  554. if (!AllowsMultipleSelection)
  555. {
  556. return;
  557. }
  558. // Don't include ".." (IsParent) in multi-selections
  559. MultiSelected = toMultiAccept
  560. .Where (s => !s.IsParent)
  561. .Select (s => s.FileSystemInfo!.FullName)
  562. .ToList ()
  563. .AsReadOnly ();
  564. Path = MultiSelected.Count == 1 ? MultiSelected [0] : string.Empty;
  565. FinishAccept ();
  566. }
  567. private void Accept (IFileInfo f)
  568. {
  569. if (!IsCompatibleWithOpenMode (f.FullName, out string reason))
  570. {
  571. _feedback = reason;
  572. SetNeedsDraw ();
  573. return;
  574. }
  575. Path = f.FullName;
  576. if (AllowsMultipleSelection)
  577. {
  578. MultiSelected = new List<string> { f.FullName }.AsReadOnly ();
  579. }
  580. FinishAccept ();
  581. }
  582. private void Accept (bool allowMulti)
  583. {
  584. if (allowMulti && TryAcceptMulti ())
  585. {
  586. return;
  587. }
  588. if (!IsCompatibleWithOpenMode (_tbPath.Text, out string reason))
  589. {
  590. _feedback = reason;
  591. SetNeedsDraw ();
  592. return;
  593. }
  594. FinishAccept ();
  595. }
  596. private void AcceptIf (Key key, KeyCode isKey)
  597. {
  598. if (!key.Handled && key.KeyCode == isKey)
  599. {
  600. key.Handled = true;
  601. // User hit Enter in text box so probably wants the
  602. // contents of the text box as their selection not
  603. // whatever lingering selection is in TableView
  604. Accept (false);
  605. }
  606. }
  607. #if MENU_V1
  608. private void AllowedTypeMenuClicked (int idx)
  609. {
  610. IAllowedType allow = AllowedTypes [idx];
  611. for (var i = 0; i < AllowedTypes.Count; i++)
  612. {
  613. _allowedTypeMenuItems! [i].Checked = i == idx;
  614. }
  615. _allowedTypeMenu!.Title = allow.ToString ()!;
  616. CurrentFilter = allow;
  617. _tbPath.ClearAllSelection ();
  618. _tbPath.Autocomplete.ClearSuggestions ();
  619. State?.RefreshChildren ();
  620. WriteStateToTableView ();
  621. }
  622. #endif
  623. private string AspectGetter (object o)
  624. {
  625. var fsi = (IFileSystemInfo)o;
  626. if (o is IDirectoryInfo dir && _treeRoots.ContainsKey (dir))
  627. {
  628. // Directory has a special name e.g. 'Pictures'
  629. return _treeRoots [dir];
  630. }
  631. return (Style.IconProvider.GetIconWithOptionalSpace (fsi) + fsi.Name).Trim ();
  632. }
  633. private int CalculateOkButtonPosX (View? _)
  634. {
  635. if (!IsInitialized || !_btnOk.IsInitialized || !_btnCancel.IsInitialized)
  636. {
  637. return 0;
  638. }
  639. return Viewport.Width
  640. - _btnOk.Viewport.Width
  641. - _btnCancel.Viewport.Width
  642. - 1
  643. // TODO: Fiddle factor, seems the Viewport are wrong for someone
  644. - 2;
  645. }
  646. private bool CancelSearch ()
  647. {
  648. if (State is SearchState search)
  649. {
  650. return search.Cancel ();
  651. }
  652. return false;
  653. }
  654. private void CellActivate (object? sender, CellActivatedEventArgs obj)
  655. {
  656. if (TryAcceptMulti ())
  657. {
  658. return;
  659. }
  660. FileSystemInfoStats stats = RowToStats (obj.Row);
  661. if (stats.FileSystemInfo is IDirectoryInfo d)
  662. {
  663. PushState (d, true);
  664. //if (d == State?.Directory || d.FullName == State?.Directory.FullName)
  665. //{
  666. // FinishAccept ();
  667. //}
  668. return;
  669. }
  670. if (stats.FileSystemInfo is IFileInfo f)
  671. {
  672. Accept (f);
  673. }
  674. }
  675. private void ClearFeedback () { _feedback = null; }
  676. private Scheme ColorGetter (CellColorGetterArgs args)
  677. {
  678. FileSystemInfoStats stats = RowToStats (args.RowIndex);
  679. if (!Style.UseColors)
  680. {
  681. return _tableView.GetScheme ();
  682. }
  683. Color color = Style.ColorProvider.GetColor (stats.FileSystemInfo!) ?? new Color (Color.White);
  684. var black = new Color (Color.Black);
  685. // TODO: Add some kind of cache for this
  686. return new ()
  687. {
  688. Normal = new (color, black),
  689. HotNormal = new (color, black),
  690. Focus = new (black, color),
  691. HotFocus = new (black, color)
  692. };
  693. }
  694. private void Delete ()
  695. {
  696. IFileSystemInfo [] toDelete = GetFocusedFiles ()!;
  697. if (FileOperationsHandler.Delete (App, toDelete))
  698. {
  699. RefreshState ();
  700. }
  701. }
  702. private void FinishAccept ()
  703. {
  704. var e = new FilesSelectedEventArgs (this);
  705. FilesSelected?.Invoke (this, e);
  706. if (e.Cancel)
  707. {
  708. return;
  709. }
  710. // if user uses Path selection mode (e.g. Enter in text box)
  711. // then also copy to MultiSelected
  712. if (AllowsMultipleSelection && !MultiSelected.Any ())
  713. {
  714. MultiSelected = string.IsNullOrWhiteSpace (Path)
  715. ? Enumerable.Empty<string> ().ToList ().AsReadOnly ()
  716. : new List<string> { Path }.AsReadOnly ();
  717. }
  718. Canceled = false;
  719. if (Modal)
  720. {
  721. App?.RequestStop ();
  722. }
  723. }
  724. private string GetBackButtonText () { return Glyphs.LeftArrow + "-"; }
  725. private IFileSystemInfo? []? GetFocusedFiles ()
  726. {
  727. if (!_tableView.HasFocus || !_tableView.CanFocus)
  728. {
  729. return null;
  730. }
  731. _tableView.EnsureValidSelection ();
  732. if (_tableView.SelectedRow < 0)
  733. {
  734. return null;
  735. }
  736. return _tableView.GetAllSelectedCells ()
  737. .Select (c => c.Y)
  738. .Distinct ()
  739. .Select (RowToStats)
  740. .Where (s => !s.IsParent)
  741. .Select (d => d.FileSystemInfo)
  742. .ToArray ();
  743. }
  744. private string GetForwardButtonText () { return "-" + Glyphs.RightArrow; }
  745. private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
  746. {
  747. // work out new sort order
  748. if (_currentSortColumn == clickedCol && _currentSortIsAsc)
  749. {
  750. isAsc = false;
  751. return string.Format (Strings.fdCtxSortDesc, _tableView.Table.ColumnNames [clickedCol]);
  752. }
  753. isAsc = true;
  754. return string.Format (Strings.fdCtxSortAsc, _tableView.Table.ColumnNames [clickedCol]);
  755. }
  756. private string GetUpButtonText () { return Style.UseUnicodeCharacters ? "◭" : "▲"; }
  757. private void HideColumn (int clickedCol)
  758. {
  759. ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (clickedCol);
  760. style.Visible = false;
  761. _tableView.Update ();
  762. }
  763. private bool IsCompatibleWithAllowedExtensions (string path)
  764. {
  765. // no restrictions
  766. if (!AllowedTypes.Any ())
  767. {
  768. return true;
  769. }
  770. return AllowedTypes.Any (t => t.IsAllowed (path));
  771. }
  772. private bool IsCompatibleWithOpenMode (string s, out string reason)
  773. {
  774. reason = string.Empty;
  775. if (string.IsNullOrWhiteSpace (s))
  776. {
  777. return false;
  778. }
  779. if (!IsCompatibleWithAllowedExtensions (s))
  780. {
  781. reason = Style.WrongFileTypeFeedback;
  782. return false;
  783. }
  784. switch (OpenMode)
  785. {
  786. case OpenMode.Directory:
  787. if (MustExist && !Directory.Exists (s))
  788. {
  789. reason = Style.DirectoryMustExistFeedback;
  790. return false;
  791. }
  792. if (File.Exists (s))
  793. {
  794. reason = Style.FileAlreadyExistsFeedback;
  795. return false;
  796. }
  797. return true;
  798. case OpenMode.File:
  799. if (MustExist && !File.Exists (s))
  800. {
  801. reason = Style.FileMustExistFeedback;
  802. return false;
  803. }
  804. if (Directory.Exists (s))
  805. {
  806. reason = Style.DirectoryAlreadyExistsFeedback;
  807. return false;
  808. }
  809. return true;
  810. case OpenMode.Mixed:
  811. if (MustExist && !File.Exists (s) && !Directory.Exists (s))
  812. {
  813. reason = Style.FileOrDirectoryMustExistFeedback;
  814. return false;
  815. }
  816. return true;
  817. default: throw new ArgumentOutOfRangeException (nameof (OpenMode));
  818. }
  819. }
  820. /// <summary>Returns true if any <see cref="AllowedTypes"/> matches <paramref name="file"/>.</summary>
  821. /// <param name="file"></param>
  822. /// <returns></returns>
  823. private bool MatchesAllowedTypes (IFileInfo file) { return AllowedTypes.Any (t => t.IsAllowed (file.FullName)); }
  824. /// <summary>
  825. /// If <see cref="TableView.MultiSelect"/> is this returns a union of all <see cref="FileSystemInfoStats"/> in the
  826. /// selection.
  827. /// </summary>
  828. /// <returns></returns>
  829. private IEnumerable<FileSystemInfoStats> MultiRowToStats ()
  830. {
  831. HashSet<FileSystemInfoStats> toReturn = new ();
  832. if (AllowsMultipleSelection && _tableView.MultiSelectedRegions.Any ())
  833. {
  834. foreach (Point p in _tableView.GetAllSelectedCells ())
  835. {
  836. FileSystemInfoStats add = State?.Children [p.Y]!;
  837. toReturn.Add (add);
  838. }
  839. }
  840. return toReturn;
  841. }
  842. private void New ()
  843. {
  844. {
  845. IFileSystemInfo created = FileOperationsHandler.New (App, _fileSystem!, State!.Directory);
  846. if (created is { })
  847. {
  848. RefreshState ();
  849. RestoreSelection (created);
  850. }
  851. }
  852. }
  853. private void OnTableViewMouseClick (object? sender, MouseEventArgs e)
  854. {
  855. Point? clickedCell = _tableView.ScreenToCell (e.Position.X, e.Position.Y, out int? clickedCol);
  856. if (clickedCol is { })
  857. {
  858. if (e.Flags.HasFlag (MouseFlags.Button1Clicked))
  859. {
  860. // left click in a header
  861. SortColumn (clickedCol.Value);
  862. }
  863. else if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
  864. {
  865. // right click in a header
  866. ShowHeaderContextMenu (clickedCol.Value, e);
  867. }
  868. }
  869. else
  870. {
  871. if (clickedCell is { } && e.Flags.HasFlag (MouseFlags.Button3Clicked))
  872. {
  873. // right click in rest of table
  874. ShowCellContextMenu (clickedCell, e);
  875. }
  876. }
  877. }
  878. private void PathChanged ()
  879. {
  880. // avoid re-entry
  881. if (_pushingState)
  882. {
  883. return;
  884. }
  885. string path = _tbPath.Text;
  886. if (string.IsNullOrWhiteSpace (path))
  887. {
  888. return;
  889. }
  890. IDirectoryInfo dir = StringToDirectoryInfo (path);
  891. if (dir.Exists)
  892. {
  893. PushState (dir, true, false);
  894. }
  895. else if (dir.Parent?.Exists ?? false)
  896. {
  897. PushState (dir.Parent, true, false);
  898. }
  899. _tbPath.Autocomplete.GenerateSuggestions (
  900. new AutocompleteFilepathContext (_tbPath.Text, _tbPath.CursorPosition, State)
  901. );
  902. }
  903. private void PushState (
  904. FileDialogState newState,
  905. bool addCurrentStateToHistory,
  906. bool setPathText = true,
  907. bool clearForward = true,
  908. string? pathText = null
  909. )
  910. {
  911. if (State is SearchState search)
  912. {
  913. search.Cancel ();
  914. }
  915. try
  916. {
  917. _pushingState = true;
  918. // push the old state to history
  919. if (addCurrentStateToHistory)
  920. {
  921. _history.Push (State, clearForward);
  922. }
  923. _tbPath.Autocomplete.ClearSuggestions ();
  924. if (pathText is { })
  925. {
  926. Path = pathText;
  927. }
  928. else if (setPathText)
  929. {
  930. SetPathToSelectedObject (newState.Directory);
  931. }
  932. State = newState;
  933. _tbPath.Autocomplete.GenerateSuggestions (
  934. new AutocompleteFilepathContext (_tbPath.Text, _tbPath.CursorPosition, State)
  935. );
  936. WriteStateToTableView ();
  937. if (clearForward)
  938. {
  939. _history.ClearForward ();
  940. }
  941. _tableView.RowOffset = 0;
  942. _tableView.SelectedRow = 0;
  943. SetNeedsDraw ();
  944. UpdateNavigationVisibility ();
  945. }
  946. finally
  947. {
  948. _pushingState = false;
  949. }
  950. ClearFeedback ();
  951. }
  952. private void RefreshState ()
  953. {
  954. State!.RefreshChildren ();
  955. PushState (State, false, false, false);
  956. }
  957. private void Rename (IApplication? app)
  958. {
  959. IFileSystemInfo [] toRename = GetFocusedFiles ()!;
  960. if (toRename?.Length == 1)
  961. {
  962. IFileSystemInfo newNamed = FileOperationsHandler.Rename (app, _fileSystem!, toRename.Single ());
  963. if (newNamed is { })
  964. {
  965. RefreshState ();
  966. RestoreSelection (newNamed);
  967. }
  968. }
  969. }
  970. private void RestartSearch ()
  971. {
  972. if (_disposed || State?.Directory is null)
  973. {
  974. return;
  975. }
  976. if (State is SearchState oldSearch)
  977. {
  978. oldSearch.Cancel ();
  979. }
  980. // user is clearing search terms
  981. if (_tbFind.Text is null || _tbFind.Text.Length == 0)
  982. {
  983. // Wait for search cancellation (if any) to finish
  984. // then push the current dir state
  985. lock (_onlyOneSearchLock)
  986. {
  987. PushState (new FileDialogState (State.Directory, this), false);
  988. }
  989. return;
  990. }
  991. PushState (new SearchState (State?.Directory!, this, _tbFind.Text), true);
  992. }
  993. private FileSystemInfoStats RowToStats (int rowIndex) { return State?.Children [rowIndex]!; }
  994. private void ShowCellContextMenu (Point? clickedCell, MouseEventArgs e)
  995. {
  996. if (clickedCell is null)
  997. {
  998. return;
  999. }
  1000. PopoverMenu? contextMenu = new (
  1001. [
  1002. new (Strings.fdCtxNew, string.Empty, New),
  1003. new (Strings.fdCtxRename, string.Empty, () => Rename (App)),
  1004. new (Strings.fdCtxDelete, string.Empty, Delete)
  1005. ]);
  1006. _tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
  1007. // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
  1008. // and the context menu is disposed when it is closed.
  1009. App!.Popover?.Register (contextMenu);
  1010. contextMenu?.MakeVisible (e.ScreenPosition);
  1011. }
  1012. private void ShowHeaderContextMenu (int clickedCol, MouseEventArgs e)
  1013. {
  1014. string sort = GetProposedNewSortOrder (clickedCol, out bool isAsc);
  1015. PopoverMenu? contextMenu = new (
  1016. [
  1017. new (
  1018. string.Format (
  1019. Strings.fdCtxHide,
  1020. StripArrows (_tableView.Table.ColumnNames [clickedCol])
  1021. ),
  1022. string.Empty,
  1023. () => HideColumn (clickedCol)
  1024. ),
  1025. new (
  1026. StripArrows (sort),
  1027. string.Empty,
  1028. () => SortColumn (clickedCol, isAsc))
  1029. ]
  1030. );
  1031. // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
  1032. // and the context menu is disposed when it is closed.
  1033. App!.Popover?.Register (contextMenu);
  1034. contextMenu?.MakeVisible (e.ScreenPosition);
  1035. }
  1036. private void SortColumn (int clickedCol)
  1037. {
  1038. GetProposedNewSortOrder (clickedCol, out bool isAsc);
  1039. SortColumn (clickedCol, isAsc);
  1040. _tableView.Table =
  1041. new FileDialogTableSource (this, State, Style, _currentSortColumn, _currentSortIsAsc);
  1042. }
  1043. private IDirectoryInfo StringToDirectoryInfo (string path)
  1044. {
  1045. // if you pass new DirectoryInfo("C:") you get a weird object
  1046. // where the FullName is in fact the current working directory.
  1047. // really not what most users would expect
  1048. if (Regex.IsMatch (path, "^\\w:$"))
  1049. {
  1050. return _fileSystem!.DirectoryInfo.New (path + _fileSystem.Path.DirectorySeparatorChar);
  1051. }
  1052. return _fileSystem!.DirectoryInfo.New (path);
  1053. }
  1054. private static string StripArrows (string columnName) { return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty); }
  1055. private void SuppressIfBadChar (Key k)
  1056. {
  1057. // don't let user type bad letters
  1058. var ch = (char)k;
  1059. if (_badChars.Contains (ch))
  1060. {
  1061. k.Handled = true;
  1062. }
  1063. }
  1064. private bool TableView_KeyUp (Key keyEvent)
  1065. {
  1066. if (keyEvent.KeyCode == KeyCode.Backspace)
  1067. {
  1068. return _history.Back ();
  1069. }
  1070. if (keyEvent.KeyCode == (KeyCode.ShiftMask | KeyCode.Backspace))
  1071. {
  1072. return _history.Forward ();
  1073. }
  1074. if (keyEvent.KeyCode == KeyCode.Delete)
  1075. {
  1076. Delete ();
  1077. return true;
  1078. }
  1079. if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.R))
  1080. {
  1081. Rename (App);
  1082. return true;
  1083. }
  1084. if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.N))
  1085. {
  1086. New ();
  1087. return true;
  1088. }
  1089. return false;
  1090. }
  1091. private void TableView_SelectedCellChanged (object? sender, SelectedCellChangedEventArgs obj)
  1092. {
  1093. if (!_tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows == 0)
  1094. {
  1095. return;
  1096. }
  1097. if (_tableView.MultiSelect && _tableView.MultiSelectedRegions.Any ())
  1098. {
  1099. return;
  1100. }
  1101. FileSystemInfoStats? stats = RowToStats (obj.NewRow);
  1102. IFileSystemInfo? dest;
  1103. if (stats.IsParent)
  1104. {
  1105. dest = State!.Directory;
  1106. }
  1107. else
  1108. {
  1109. dest = stats.FileSystemInfo;
  1110. }
  1111. try
  1112. {
  1113. _pushingState = true;
  1114. SetPathToSelectedObject (dest);
  1115. State!.Selected = stats;
  1116. _tbPath.Autocomplete.ClearSuggestions ();
  1117. }
  1118. finally
  1119. {
  1120. _pushingState = false;
  1121. }
  1122. }
  1123. private void TreeView_SelectionChanged (object? sender, SelectionChangedEventArgs<IFileSystemInfo> e)
  1124. {
  1125. SetPathToSelectedObject (e.NewValue);
  1126. }
  1127. private void SetPathToSelectedObject (IFileSystemInfo? selected)
  1128. {
  1129. if (selected is null)
  1130. {
  1131. return;
  1132. }
  1133. if (selected is IDirectoryInfo && Style.PreserveFilenameOnDirectoryChanges)
  1134. {
  1135. if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem!.Directory.Exists (Path))
  1136. {
  1137. var currentFile = _fileSystem.Path.GetFileName (Path);
  1138. if (!string.IsNullOrWhiteSpace (currentFile))
  1139. {
  1140. Path = _fileSystem.Path.Combine (selected.FullName, currentFile);
  1141. return;
  1142. }
  1143. }
  1144. }
  1145. Path = selected.FullName;
  1146. }
  1147. private bool TryAcceptMulti ()
  1148. {
  1149. IEnumerable<FileSystemInfoStats> multi = MultiRowToStats ();
  1150. string? reason = null;
  1151. IEnumerable<FileSystemInfoStats> fileSystemInfoStatsEnumerable = multi as FileSystemInfoStats [] ?? multi.ToArray ();
  1152. if (!fileSystemInfoStatsEnumerable.Any ())
  1153. {
  1154. return false;
  1155. }
  1156. if (fileSystemInfoStatsEnumerable.All (
  1157. m => m.FileSystemInfo is { } && IsCompatibleWithOpenMode (
  1158. m.FileSystemInfo.FullName,
  1159. out reason
  1160. )
  1161. ))
  1162. {
  1163. Accept (fileSystemInfoStatsEnumerable);
  1164. return true;
  1165. }
  1166. if (reason is { })
  1167. {
  1168. _feedback = reason;
  1169. SetNeedsDraw ();
  1170. }
  1171. return false;
  1172. }
  1173. private void UpdateNavigationVisibility ()
  1174. {
  1175. _btnBack.Visible = _history.CanBack ();
  1176. _btnForward.Visible = _history.CanForward ();
  1177. _btnUp.Visible = _history.CanUp ();
  1178. }
  1179. private void WriteStateToTableView ()
  1180. {
  1181. _tableView.Table =
  1182. new FileDialogTableSource (this, State, Style, _currentSortColumn, _currentSortIsAsc);
  1183. ApplySort ();
  1184. _tableView.Update ();
  1185. }
  1186. // --- Tree visibility management ---
  1187. private void ToggleTreeVisibility ()
  1188. {
  1189. SetTreeVisible (!_treeView.Visible);
  1190. }
  1191. private void SetTreeVisible (bool visible)
  1192. {
  1193. _treeView.Enabled = visible;
  1194. _treeView.Visible = visible;
  1195. if (visible)
  1196. {
  1197. // When visible, the table view's left edge is a splitter next to the tree
  1198. _treeView.Width = Dim.Fill (Dim.Func (_ => IsInitialized ? _tableViewContainer!.Frame.Width - 30 : 30));
  1199. _tableViewContainer.X = 30;
  1200. _tableViewContainer.Arrangement = ViewArrangement.LeftResizable;
  1201. _tableViewContainer.Border!.Thickness = new (1, 0, 0, 0);
  1202. }
  1203. else
  1204. {
  1205. // When hidden, table occupies full width and splitter is hidden/disabled
  1206. _treeView.Width = 0;
  1207. _tableViewContainer.X = 0;
  1208. _tableViewContainer.Width = Dim.Fill ();
  1209. _tableViewContainer.Arrangement = ViewArrangement.Fixed;
  1210. _tableViewContainer.Border!.Thickness = new (0, 0, 0, 0);
  1211. }
  1212. _btnTreeToggle.Text = GetTreeToggleText (visible);
  1213. SetNeedsLayout ();
  1214. SetNeedsDraw ();
  1215. }
  1216. private string GetTreeToggleText (bool visible)
  1217. {
  1218. return visible
  1219. ? $"{Glyphs.LeftArrow}{Strings.fdTree}"
  1220. : $"{Glyphs.RightArrow}{Strings.fdTree}";
  1221. }
  1222. /// <summary>State representing a recursive search from <see cref="FileDialogState.Directory"/> downwards.</summary>
  1223. internal class SearchState : FileDialogState
  1224. {
  1225. // TODO: Add thread safe child adding
  1226. private readonly List<FileSystemInfoStats> _found = [];
  1227. private readonly object _oLockFound = new ();
  1228. private readonly CancellationTokenSource _token = new ();
  1229. private bool _cancel;
  1230. private bool _finished;
  1231. public SearchState (IDirectoryInfo dir, FileDialog parent, string searchTerms) : base (dir, parent)
  1232. {
  1233. parent.SearchMatcher.Initialize (searchTerms);
  1234. Children = [];
  1235. BeginSearch ();
  1236. }
  1237. /// <summary>
  1238. /// Cancels the current search (if any). Returns true if a search was running and cancellation was successfully
  1239. /// set.
  1240. /// </summary>
  1241. /// <returns></returns>
  1242. internal bool Cancel ()
  1243. {
  1244. bool alreadyCancelled = _token.IsCancellationRequested || _cancel;
  1245. _cancel = true;
  1246. _token.Cancel ();
  1247. return !alreadyCancelled;
  1248. }
  1249. internal override void RefreshChildren () { }
  1250. private void BeginSearch ()
  1251. {
  1252. Task.Run (
  1253. () =>
  1254. {
  1255. RecursiveFind (Directory);
  1256. _finished = true;
  1257. }
  1258. );
  1259. Task.Run (UpdateChildren);
  1260. }
  1261. private void RecursiveFind (IDirectoryInfo directory)
  1262. {
  1263. foreach (FileSystemInfoStats f in GetChildren (directory))
  1264. {
  1265. if (_cancel)
  1266. {
  1267. return;
  1268. }
  1269. if (f.IsParent)
  1270. {
  1271. continue;
  1272. }
  1273. lock (_oLockFound)
  1274. {
  1275. if (_found.Count >= MaxSearchResults)
  1276. {
  1277. _finished = true;
  1278. return;
  1279. }
  1280. }
  1281. if (Parent.SearchMatcher.IsMatch (f.FileSystemInfo!))
  1282. {
  1283. lock (_oLockFound)
  1284. {
  1285. _found.Add (f);
  1286. }
  1287. }
  1288. if (f.FileSystemInfo is IDirectoryInfo sub)
  1289. {
  1290. RecursiveFind (sub);
  1291. }
  1292. }
  1293. }
  1294. private void UpdateChildren ()
  1295. {
  1296. lock (Parent._onlyOneSearchLock)
  1297. {
  1298. while (!_cancel && !_finished)
  1299. {
  1300. try
  1301. {
  1302. Task.Delay (250).Wait (_token.Token);
  1303. }
  1304. catch (OperationCanceledException)
  1305. {
  1306. _cancel = true;
  1307. }
  1308. if (_cancel || _finished)
  1309. {
  1310. break;
  1311. }
  1312. UpdateChildrenToFound ();
  1313. }
  1314. if (_finished && !_cancel)
  1315. {
  1316. UpdateChildrenToFound ();
  1317. }
  1318. Application.Invoke ((_) => { Parent._spinnerView.Visible = false; });
  1319. }
  1320. }
  1321. private void UpdateChildrenToFound ()
  1322. {
  1323. lock (_oLockFound)
  1324. {
  1325. Children = _found.ToArray ();
  1326. }
  1327. Application.Invoke (
  1328. (_) =>
  1329. {
  1330. Parent._tbPath.Autocomplete.GenerateSuggestions (
  1331. new AutocompleteFilepathContext (
  1332. Parent._tbPath.Text,
  1333. Parent._tbPath.CursorPosition,
  1334. this
  1335. )
  1336. );
  1337. Parent.WriteStateToTableView ();
  1338. Parent._spinnerView.Visible = true;
  1339. Parent._spinnerView.SetNeedsDraw ();
  1340. }
  1341. );
  1342. }
  1343. }
  1344. bool IDesignable.EnableForDesign ()
  1345. {
  1346. Modal = false;
  1347. OnLoaded ();
  1348. return true;
  1349. }
  1350. }