Selaa lähdekoodia

Fixes #2150. Revamping FileDialog (#2259)

* Investigating new file dialog

* Column sorting

* Add navigationStack

* Append autocomplete half working

* Change autocomplete append to use draw effect instead of selection effect

* WIP on FileDialog2

* Refactor sort order and add more 'ls' colors

* Refactor history to its own class

* FileDialog2 navigation fixes/improvements

* Added centered Title

* Add tree view and split container

* Add FileDialogState

* Add AllowsMultipleSelection

* Add result fields and Scenario

* Added FileDialo2 test file

* Fix FileDialog2 Redraw padding to respect max/min view bounds

* Fix unit tests and warnings

* Started on better keyboard navigation in FileDialog2

* Update to match new SplitContainer API

* Quality of Life improvements

* Fix recommending parent folder

* Remove border from SplitContainer in FileDialog2 and fixed tests

* Bugfixes and improvements to FileDialog2

* usability improvements to FileDialog2

* Add multi select and OpenMode

* Enforce OpenMode when making a multi selection

* Prevent typing illegal characters

* Added AllowedTypes to FileDialog2

* Added combo box filter AllowedType

* Improve code readability by reordering members

* Do not update FileDialog2 text box when selecting ".." in TableView

* Fix history navigation in FileDialog2

* Restore selection after navigating backwards in history

* Add FileDialog2 tests

* Make FileDialog2 Title user configurable

* Fix DirectTyping_Allowed unit test when running on linux

* Change Home/End to go to first/last cell in table in FileDialog2

* Remove overloaded Title property

* Switch to `ustring.IsNullOrEmpty`

* Update to latest TileView API

* Add TableView navigation by letter using CollectionNavigator

* Fix recreating search navigator too often

* Add tests for proper disposing

* Make Ctrl+F10 toggle split line focusability

* Fix layout bug in first tile when orientation is horizontal

* Switch to GenerateImage

* Fix not calling base constructor

* Revert "Merges latest LineCanvas into TileView"

* Fix keyboard tab navigation problems

* Workaround for changing CanFocus throwing Exceptions sometimes

* Update to latest splitcontainer API

* Adjust suggestions to be gray and properly update on keystrokes

* Move ok and cancel to bottom

* Add MustExist and fix multi selection of 1 result

* bugfixes and quality of life

* Navigating to .. clears path up to current dir

* Better arrow key navigation

* Make title pretty and informative

* Fix test

* Fix test

* Trim default Titles to be more compact and readable

* Fix bad merge changes

* Remove EscSeqReq files that are not in v2... came from develop?!

* Fix nullable and enable toggle select on table view

* Fix multi select return value

* Add icon and monochrome support

* Add search elements

* Add search for current directory

* WIP: Async search

* Thread safety and disposal

* Improve UX

* Fix for rapid search results

* Fix warnings and whitespace

* Don't add more than 10000 search results

* Add support for adjusting search matching

* Added ISearchMatcher example to FileDialog2Example

* Remove double spaces after periods

* Make MaxSearchResults a config setting

* Localization for FileDialog2

* Fix build error

* Support for custom open button Text

* Improve file dialog scenario UX

* Change feedback to a drawing effect in center of screen

* Explore MenuBar instead of ComboBox for AllowedTypes

* Fix prompt and move file open into try/catch for errors reading files

* Open menu when tabbed to

* FileDialog2 improvements
- Expose table/tree style properties
- Rename Monochrome to UseColors and default to false
- IconGetter no longer forces space
- On Windows in Scenario just use a backslash for folder icon (i.e. not unicode)
-

* Add style settings in scenario and make autocomplete case insensitive on Windows

* Move ok button text to Style

* xmldoc

* Remove old FileDialog and re-wire OpenDialog and SaveDialog to use FileDialog2 base

* localization

* Move open/save dialog to their own files

* Rename FileDialog2 to FileDialog

* Fix repetition in string

* Add IAllowedType

* Get rid of AllowedTypesIsStrict

User now adds the `IAllowedType` implementation `AllowedTypeAny`

* Add max length to AllowedType ToString

* Pressing Enter in find restarts search instead of confirm selection

* Add TreeRootGetter for customizing the quick access items in FileDialog

* Add FilesSelected event
Allows user to do things like confirm dialogs on selecting existing file(s)

* Update to new sender, event args signature

* Fix naming on MouseEventArgs

* Fix mouse events naming

* Revert "Fix naming on MouseEventArgs"

This reverts commit 2f557f52d9581e2ca20cc6c022cf1de3c0c326dc.

* Add deletion support

* Move delete keybinding to tableview

* Scaffold for rename and new operations

* Prevent delete dialog popping up again on cancel

* Add rename and new folder implementations

* Add rename,delete,new to context menu

* On rename or new, reselect the file in its new location in tree

* Support searching on multiple terms

* Localization support for new/rename/delete

* Refactor internal classes and add class diagram

* Move some visual properties to FileDialogStyle

* Ensure MultiSelected is never null and always contains Path if relevant

* Fix spacing/indentation

* WIP: Add new namespace Terminal.Gui.FileServices

* Improve appearance of back/forward/up

* Move SpinnerLabel to Views

* Add SpinnerView

* Code formatting

* Add AutoSpin test

* Avoid ever removing spinner timeout twice

* Make SpinnerView show/hide instead of stopping

* WIP: Refactor to use 3 sub PRs
- SpinnerView
- Suggest Append Autocomplete
- Caption TextField

* Add FilepathSuggestionGenerator

* WIP: FileDialog autocomplete append mostly working again

* Improve file autocomplete

* Move IconGetter to Style and provide default implementation

- Depends on `UseUnicodeCharacters`
- Also updated up/back/collapse/expand tree to use spicier icons

* Fix failing unit test

* Improved colors and layout

* Adjust use of unicode

* Fix UseUnicodeCharacters

* Update table style to include scroll indicators and lines

* Fix cycle suggestion with down cursor

* Adjust sort indicators

* Add default sort order on isDir then name

* Always use left/right unicode arrows

* Fix autocomplete suggesting in empty textbox

* Press escape to cancel ongoing search (when search box is focused)

* When entering a TreeView if there is no selection then select first object

* Move CursorIsAtEnd to TextField

* Improve layout

* Change UseColors to be a cell color fill

* Fxied tests for new apis

* Remove manual title drawing code

* Fix MoveEnd name conflicting with base class

* Fix merge conflicts

* Switched to IFileSystem for unit testing

* Add unit test

* Revert "Fix MoveEnd name conflicting with base class"

This reverts commit a5f9c070223815ac2aac0aa1d60a37bb5d61ff8b.

* Fix MoveEnd name collision with 'new' keyword

* Fixed tree not toggling

* DateTime fixes and mocking

* Fix TestDirectoryContents_Windows

* Expose UseColors and UseUnicodeCharacters as config settings

* Fix linter settings having removed curly brackets

* Fix namespace on test

* Move tests to file services folder

* Remove the FileServices namespace

* Updated class diagram

* Clear title from tests for futureproofing

* Localization support for FileDialog title

* Remove trailing whitespace in "open existing"

* Fix listing suggestions immediately after folder path entered

---------

Co-authored-by: Tig <[email protected]>
Thomas Nind 2 vuotta sitten
vanhempi
commit
c1a578891b
36 muutettua tiedostoa jossa 3803 lisäystä ja 886 poistoa
  1. 108 0
      Terminal.Gui/FileServices/AllowedType.cs
  2. 139 0
      Terminal.Gui/FileServices/DefaultFileOperations.cs
  3. 29 0
      Terminal.Gui/FileServices/DefaultSearchMatcher.cs
  4. 108 0
      Terminal.Gui/FileServices/FileDialogHistory.cs
  5. 48 0
      Terminal.Gui/FileServices/FileDialogRootTreeNode.cs
  6. 77 0
      Terminal.Gui/FileServices/FileDialogState.cs
  7. 263 0
      Terminal.Gui/FileServices/FileDialogStyle.cs
  8. 38 0
      Terminal.Gui/FileServices/FileDialogTreeBuilder.cs
  9. 134 0
      Terminal.Gui/FileServices/FileSystemInfoStats.cs
  10. 30 0
      Terminal.Gui/FileServices/FilesSelectedEventArgs.cs
  11. 47 0
      Terminal.Gui/FileServices/IFileOperations.cs
  12. 23 0
      Terminal.Gui/FileServices/ISearchMatcher.cs
  13. 216 0
      Terminal.Gui/Resources/Strings.Designer.cs
  14. 76 0
      Terminal.Gui/Resources/Strings.resx
  15. 1 0
      Terminal.Gui/Terminal.Gui.csproj
  16. 3 12
      Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs
  17. 91 0
      Terminal.Gui/Views/AutocompleteFilepathContext.cs
  18. 19 1
      Terminal.Gui/Views/Button.cs
  19. 1 1
      Terminal.Gui/Views/DateField.cs
  20. 162 0
      Terminal.Gui/Views/FileDialog.cd
  21. 1430 849
      Terminal.Gui/Views/FileDialog.cs
  22. 90 0
      Terminal.Gui/Views/OpenDialog.cs
  23. 67 0
      Terminal.Gui/Views/SaveDialog.cs
  24. 1 1
      Terminal.Gui/Views/TableView/TableView.cs
  25. 25 1
      Terminal.Gui/Views/TextField.cs
  26. 1 1
      Terminal.Gui/Views/TimeField.cs
  27. 6 2
      Terminal.Gui/Views/TreeView/TreeView.cs
  28. 3 0
      UICatalog/Resources/config.json
  29. 8 5
      UICatalog/Scenarios/CsvEditor.cs
  30. 15 8
      UICatalog/Scenarios/Editor.cs
  31. 214 0
      UICatalog/Scenarios/FileDialogExamples.cs
  32. 1 1
      UICatalog/Scenarios/HexEditor.cs
  33. 3 3
      UICatalog/Scenarios/Notepad.cs
  34. 317 0
      UnitTests/FileServices/FileDialogTests.cs
  35. 8 1
      UnitTests/TestHelpers.cs
  36. 1 0
      UnitTests/UnitTests.csproj

+ 108 - 0
Terminal.Gui/FileServices/AllowedType.cs

@@ -0,0 +1,108 @@
+using System;
+using System.CodeDom;
+using System.Data;
+using System.IO;
+using System.Linq;
+using Terminal.Gui.Resources;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Interface for <see cref="FileDialog"/> restrictions on which file type(s) the
+	/// user is allowed to select/enter.
+	/// </summary>
+	public interface IAllowedType
+	{
+		/// <summary>
+		/// Returns true if the file at <paramref name="path"/> is compatible with this
+		/// allow option.  Note that the file may not exist (e.g. in the case of saving).
+		/// </summary>
+		/// <param name="path"></param>
+		/// <returns></returns>
+		bool IsAllowed (string path);
+	}
+
+
+	/// <summary>
+	/// <see cref="IAllowedType"/> that allows selection of any types (*.*).
+	/// </summary>
+	public class AllowedTypeAny : IAllowedType {
+
+		/// <inheritdoc/>
+		public bool IsAllowed (string path)
+		{
+			return true;
+		}
+
+		/// <inheritdoc/>
+		public override string ToString ()
+		{
+			return Strings.fdAnyFiles + "(*.*)";
+		}
+	}
+
+	/// <summary>
+	/// Describes a requirement on what <see cref="FileInfo"/> can be selected.
+	/// This can be combined with other <see cref="IAllowedType"/> in a <see cref="FileDialog"/>
+	/// to for example show only .csv files but let user change to open any if they want.
+	/// </summary>
+	public class AllowedType : IAllowedType {
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="AllowedType"/> class.
+		/// </summary>
+		/// <param name="description">The human readable text to display.</param>
+		/// <param name="extensions">Extension(s) to match e.g. .csv.</param>
+		public AllowedType (string description, params string [] extensions)
+		{
+			if (extensions.Length == 0) {
+				throw new ArgumentException ("You must supply at least one extension");
+			}
+
+			this.Description = description;
+			this.Extensions = extensions;
+		}
+
+		/// <summary>
+		/// Gets or Sets the human readable description for the file type
+		/// e.g. "Comma Separated Values".
+		/// </summary>
+		public string Description { get; set; }
+
+		/// <summary>
+		/// Gets or Sets the permitted file extension(s) (e.g. ".csv").
+		/// </summary>
+		public string [] Extensions { get; set; }
+
+
+		/// <summary>
+		/// Returns <see cref="Description"/> plus all <see cref="Extensions"/> separated by semicolons.
+		/// </summary>
+		public override string ToString ()
+		{
+			const int maxLength = 30;
+
+			var desc = $"{this.Description} ({string.Join (";", this.Extensions.Select (e => '*' + e).ToArray ())})";
+
+			if(desc.Length > maxLength) {
+				return desc.Substring (0, maxLength-2) + "…";
+			}
+			return desc;
+		}
+
+		/// <inheritdoc/>
+		public bool IsAllowed(string path)
+		{
+			var extension = Path.GetExtension (path);
+
+			// There is a requirement to have a particular extension and we have none
+			if (string.IsNullOrEmpty (extension)) {
+				return false;
+			}
+
+
+			return this.Extensions.Any (e => e.Equals (extension));
+		}
+	}
+	
+}

+ 139 - 0
Terminal.Gui/FileServices/DefaultFileOperations.cs

@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using Terminal.Gui.Resources;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Default file operation handlers using modal dialogs.
+	/// </summary>
+	public class DefaultFileOperations : IFileOperations {
+
+		/// <inheritdoc/>
+		public bool Delete (IEnumerable<IFileSystemInfo> toDelete)
+		{
+			// Default implementation does not allow deleting multiple files
+			if (toDelete.Count () != 1) {
+				return false;
+			}
+			var d = toDelete.Single ();
+			var adjective = d.Name;
+
+			int result = MessageBox.Query (
+				string.Format (Strings.fdDeleteTitle, adjective),
+				string.Format (Strings.fdDeleteBody, adjective),
+				Strings.fdYes, Strings.fdNo);
+
+			try {
+				if (result == 0) {
+					if (d is IFileInfo) {
+						d.Delete ();
+					} else {
+						((IDirectoryInfo)d).Delete (true);
+					}
+
+					return true;
+				}
+			} catch (Exception ex) {
+				MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, "Ok");
+			}
+
+			return false;
+		}
+
+		private bool Prompt (string title, string defaultText, out string result)
+		{
+
+			bool confirm = false;
+			var btnOk = new Button ("Ok") {
+				IsDefault = true,
+			};
+			btnOk.Clicked += (s, e) => {
+				confirm = true;
+				Application.RequestStop ();
+			};
+			var btnCancel = new Button ("Cancel");
+			btnCancel.Clicked += (s, e) => {
+				confirm = false;
+				Application.RequestStop ();
+			};
+
+			var lbl = new Label (Strings.fdRenamePrompt);
+			var tf = new TextField (defaultText) {
+				X = Pos.Right (lbl),
+				Width = Dim.Fill (),
+			};
+			tf.SelectAll ();
+
+			var dlg = new Dialog (title) {
+				Width = Dim.Percent (50),
+				Height = 4
+			};
+			dlg.Add (lbl);
+			dlg.Add (tf);
+
+			// Add buttons last so tab order is friendly
+			// and TextField gets focus
+			dlg.AddButton (btnOk);
+			dlg.AddButton (btnCancel);
+
+			Application.Run (dlg);
+
+			result = tf.Text?.ToString ();
+
+			return confirm;
+		}
+
+		/// <inheritdoc/>
+		public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename)
+		{
+			// Don't allow renaming C: or D: or / (on linux) etc
+			if (toRename is IDirectoryInfo dir && dir.Parent == null) {
+				return null;
+			}
+
+			if (Prompt (Strings.fdRenameTitle, toRename.Name, out var newName)) {
+				if (!string.IsNullOrWhiteSpace (newName)) {
+					try {
+						if (toRename is IFileInfo f) {
+
+							var newLocation = fileSystem.FileInfo.New (Path.Combine (f.Directory.FullName, newName));
+							f.MoveTo (newLocation.FullName);
+							return newLocation;
+
+						} else {
+							var d = (IDirectoryInfo)toRename;
+
+							var newLocation = fileSystem.DirectoryInfo.New (Path.Combine (d.Parent.FullName, newName));
+							d.MoveTo (newLocation.FullName);
+							return newLocation;
+						}
+					} catch (Exception ex) {
+						MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok");
+					}
+				}
+			}
+
+			return null;
+		}
+
+		/// <inheritdoc/>
+		public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory)
+		{
+			if (Prompt (Strings.fdNewTitle, "", out var named)) {
+				if (!string.IsNullOrWhiteSpace (named)) {
+					try {
+						var newDir = fileSystem.DirectoryInfo.New (Path.Combine (inDirectory.FullName, named));
+						newDir.Create ();
+						return newDir;
+					} catch (Exception ex) {
+						MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok");
+					}
+				}
+			}
+			return null;
+		}
+	}
+}

+ 29 - 0
Terminal.Gui/FileServices/DefaultSearchMatcher.cs

@@ -0,0 +1,29 @@
+using System;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace Terminal.Gui {
+	class DefaultSearchMatcher : ISearchMatcher {
+		string [] terms;
+
+		public void Initialize (string terms)
+		{
+			this.terms = terms.Split (new string [] { " " }, StringSplitOptions.RemoveEmptyEntries);
+		}
+
+		public bool IsMatch (IFileSystemInfo f)
+		{
+			//Contains overload with StringComparison is not available in (net472) or (netstandard2.0)
+			//return f.Name.Contains (terms, StringComparison.OrdinalIgnoreCase);
+
+			return
+				// At least one term must match the file name only e.g. "my" in "myfile.csv"
+				terms.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0)
+				&&
+				// All terms must exist in full path e.g. "dos my" can match "c:\documents\myfile.csv"
+				terms.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0);
+		}
+	}
+
+}

+ 108 - 0
Terminal.Gui/FileServices/FileDialogHistory.cs

@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+
+namespace Terminal.Gui {
+
+	internal class FileDialogHistory {
+		private Stack<FileDialogState> back = new Stack<FileDialogState> ();
+		private Stack<FileDialogState> forward = new Stack<FileDialogState> ();
+		private FileDialog dlg;
+
+		public FileDialogHistory (FileDialog dlg)
+		{
+			this.dlg = dlg;
+		}
+
+		public bool Back ()
+		{
+
+			IDirectoryInfo goTo = null;
+			FileSystemInfoStats restoreSelection = null;
+
+			if (this.CanBack ()) {
+
+				var backTo = this.back.Pop ();
+				goTo = backTo.Directory;
+				restoreSelection = backTo.Selected;
+			} else if (this.CanUp ()) {
+				goTo = this.dlg.State?.Directory.Parent;
+			}
+
+			// nowhere to go
+			if (goTo == null) {
+				return false;
+			}
+
+			this.forward.Push (this.dlg.State);
+			this.dlg.PushState (goTo, false, true, false);
+
+			if (restoreSelection != null) {
+				this.dlg.RestoreSelection (restoreSelection.FileSystemInfo);
+			}
+
+			return true;
+		}
+
+		internal bool CanBack ()
+		{
+			return this.back.Count > 0;
+		}
+
+		internal bool Forward ()
+		{
+			if (this.forward.Count > 0) {
+
+				this.dlg.PushState (this.forward.Pop ().Directory, true, true, false);
+				return true;
+			}
+
+			return false;
+		}
+
+		internal bool Up ()
+		{
+			var parent = this.dlg.State?.Directory.Parent;
+			if (parent != null) {
+
+				this.back.Push (new FileDialogState (parent, this.dlg));
+				this.dlg.PushState (parent, false);
+				return true;
+			}
+
+			return false;
+		}
+
+		internal bool CanUp ()
+		{
+			return this.dlg.State?.Directory.Parent != null;
+		}
+
+
+		internal void Push (FileDialogState state, bool clearForward)
+		{
+			if (state == null) {
+				return;
+			}
+
+			// if changing to a new directory push onto the Back history
+			if (this.back.Count == 0 || this.back.Peek ().Directory.FullName != state.Directory.FullName) {
+
+				this.back.Push (state);
+				if (clearForward) {
+					this.ClearForward ();
+				}
+			}
+		}
+
+		internal bool CanForward ()
+		{
+			return this.forward.Count > 0;
+		}
+
+		internal void ClearForward ()
+		{
+			this.forward.Clear ();
+		}
+	}
+}

+ 48 - 0
Terminal.Gui/FileServices/FileDialogRootTreeNode.cs

@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Delegate for providing an implementation that returns all <see cref="FileDialogRootTreeNode"/>
+	/// that should be shown in a <see cref="FileDialog"/> (in the collapse-able tree area of the dialog).
+	/// </summary>
+	/// <returns></returns>
+	public delegate IEnumerable<FileDialogRootTreeNode> FileDialogTreeRootGetter ();
+
+	/// <summary>
+	/// Describes a top level directory that should be offered to the user in the
+	/// tree view section of a <see cref="FileDialog"/>.  For example "Desktop",
+	/// "Downloads", "Documents" etc.
+	/// </summary>
+	public class FileDialogRootTreeNode {
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="FileDialogRootTreeNode"/> class
+		/// </summary>
+		/// <param name="displayName"></param>
+		/// <param name="path"></param>
+		public FileDialogRootTreeNode (string displayName, DirectoryInfo path)
+		{
+			this.DisplayName = displayName;
+			this.Path = path;
+		}
+
+		/// <summary>
+		/// Gets the text that should be displayed in the tree for this item.
+		/// </summary>
+		public string DisplayName { get; }
+
+		/// <summary>
+		/// Gets the path that should be shown/explored when selecting this node
+		/// of the tree.
+		/// </summary>
+		public DirectoryInfo Path { get; }
+
+		/// <inheritdoc/>
+		public override string ToString ()
+		{
+			return this.DisplayName;
+		}
+	}
+}

+ 77 - 0
Terminal.Gui/FileServices/FileDialogState.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	internal class FileDialogState {
+
+		public FileSystemInfoStats Selected { get; set; }
+		protected readonly FileDialog Parent;
+		public FileDialogState (IDirectoryInfo dir, FileDialog parent)
+		{
+			this.Directory = dir;
+			Parent = parent;
+
+			this.RefreshChildren ();
+		}
+
+		public IDirectoryInfo Directory { get; }
+
+		public FileSystemInfoStats [] Children { get; protected set; }
+
+		internal virtual void RefreshChildren ()
+		{
+			var dir = this.Directory;
+			Children = GetChildren (dir).ToArray ();
+		}
+
+		protected virtual IEnumerable<FileSystemInfoStats> GetChildren (IDirectoryInfo dir)
+		{
+			try {
+
+				List<FileSystemInfoStats> children;
+
+				// if directories only
+				if (Parent.OpenMode == OpenMode.Directory) {
+					children = dir.GetDirectories ().Select (e => new FileSystemInfoStats (e)).ToList ();
+				} else {
+					children = dir.GetFileSystemInfos ().Select (e => new FileSystemInfoStats (e)).ToList ();
+				}
+
+				// if only allowing specific file types
+				if (Parent.AllowedTypes.Any () && Parent.OpenMode == OpenMode.File) {
+
+					children = children.Where (
+						c => c.IsDir () ||
+						(c.FileSystemInfo is IFileInfo f && Parent.IsCompatibleWithAllowedExtensions (f)))
+						.ToList ();
+				}
+
+				// if theres a UI filter in place too
+				if (Parent.CurrentFilter != null) {
+					children = children.Where (MatchesApiFilter).ToList ();
+				}
+
+
+				// allow navigating up as '..'
+				if (dir.Parent != null) {
+					children.Add (new FileSystemInfoStats (dir.Parent) { IsParent = true });
+				}
+
+				return children;
+			} catch (Exception) {
+				// Access permissions Exceptions, Dir not exists etc
+				return Enumerable.Empty<FileSystemInfoStats> ();
+			}
+		}
+
+		protected bool MatchesApiFilter (FileSystemInfoStats arg)
+		{
+			return arg.IsDir () ||
+			(arg.FileSystemInfo is IFileInfo f && Parent.CurrentFilter.IsAllowed (f.FullName));
+		}
+	}
+}

+ 263 - 0
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -0,0 +1,263 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Globalization;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using Terminal.Gui.Resources;
+using static System.Environment;
+using static Terminal.Gui.ConfigurationManager;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Stores style settings for <see cref="FileDialog"/>.
+	/// </summary>
+	public class FileDialogStyle {
+
+		/// <summary>
+		/// Gets or sets the default value to use for <see cref="UseColors"/>.
+		/// This can be populated from .tui config files via <see cref="ConfigurationManager"/>
+		/// </summary>
+		[SerializableConfigurationProperty(Scope = typeof (SettingsScope))]
+		public static bool DefaultUseColors { get; set; }
+
+
+		/// <summary>
+		/// Gets or sets the default value to use for <see cref="UseUnicodeCharacters"/>.
+		/// This can be populated from .tui config files via <see cref="ConfigurationManager"/>
+		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+		public static bool DefaultUseUnicodeCharacters { get; set; }
+
+		/// <summary>
+		/// Gets or Sets a value indicating whether different colors
+		/// should be used for different file types/directories.  Defaults
+		/// to false.
+		/// </summary>
+		public bool UseColors { get; set; }
+
+		/// <summary>
+		/// Sets a <see cref="ColorScheme"/> to use for directories rows of
+		/// the <see cref="TableView"/>.
+		/// </summary>
+		public ColorScheme ColorSchemeDirectory { get; set; }
+
+		/// <summary>
+		/// Sets a <see cref="ColorScheme"/> to use for file rows with an image extension
+		/// of the <see cref="TableView"/>. Defaults to White text on Black background.
+		/// </summary>
+		public ColorScheme ColorSchemeImage { get; set; }
+
+		/// <summary>
+		/// Sets a <see cref="ColorScheme"/> to use for file rows with an executable extension
+		/// or that match <see cref="FileDialog.AllowedTypes"/> in the <see cref="TableView"/>.
+		/// </summary>
+		public ColorScheme ColorSchemeExeOrRecommended { get; set; }
+
+		/// <summary>
+		/// Colors to use when <see cref="UseColors"/> is true but file does not match any other
+		/// classification (<see cref="ColorSchemeDirectory"/>, <see cref="ColorSchemeImage"/> etc).
+		/// </summary>
+		public ColorScheme ColorSchemeOther { get; set; }
+
+		/// <summary>
+		/// Gets or sets the header text displayed in the Filename column of the files table.
+		/// </summary>
+		public string FilenameColumnName { get; set; } = Strings.fdFilename;
+
+		/// <summary>
+		/// Gets or sets the header text displayed in the Size column of the files table.
+		/// </summary>
+		public string SizeColumnName { get; set; } = Strings.fdSize;
+
+		/// <summary>
+		/// Gets or sets the header text displayed in the Modified column of the files table.
+		/// </summary>
+		public string ModifiedColumnName { get; set; } = Strings.fdModified;
+
+		/// <summary>
+		/// Gets or sets the header text displayed in the Type column of the files table.
+		/// </summary>
+		public string TypeColumnName { get; set; } = Strings.fdType;
+
+		/// <summary>
+		/// Gets or sets the text displayed in the 'Search' text box when user has not supplied any input yet.
+		/// </summary>
+		public string SearchCaption { get; internal set; } = Strings.fdSearchCaption;
+
+		/// <summary>
+		/// Gets or sets the text displayed in the 'Path' text box when user has not supplied any input yet.
+		/// </summary>
+		public string PathCaption { get; internal set; } = Strings.fdPathCaption;
+
+		/// <summary>
+		/// Gets or sets the text on the 'Ok' button.  Typically you may want to change this to
+		/// "Open" or "Save" etc.
+		/// </summary>
+		public string OkButtonText { get; set; } = "Ok";
+
+		/// <summary>
+		/// Gets or sets error message when user attempts to select a file type that is not one of <see cref="FileDialog.AllowedTypes"/>
+		/// </summary>
+		public string WrongFileTypeFeedback { get; internal set; } = Strings.fdWrongFileTypeFeedback;
+
+		/// <summary>
+		/// Gets or sets error message when user selects a directory that does not exist and
+		/// <see cref="OpenMode"/> is <see cref="OpenMode.Directory"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+		/// </summary>
+		public string DirectoryMustExistFeedback { get; internal set; } = Strings.fdDirectoryMustExistFeedback;
+
+		/// <summary>
+		/// Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.Directory"/>
+		/// and user enters the name of an existing file (File system cannot have a folder with the same name as a file).
+		/// </summary>
+		public string FileAlreadyExistsFeedback { get; internal set; } = Strings.fdFileAlreadyExistsFeedback;
+
+		/// <summary>
+		/// Gets or sets error message when user selects a file that does not exist and
+		/// <see cref="OpenMode"/> is <see cref="OpenMode.File"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+		/// </summary>
+		public string FileMustExistFeedback { get; internal set; } = Strings.fdFileMustExistFeedback;
+
+		/// <summary>
+		/// Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.File"/>
+		/// and user enters the name of an existing directory (File system cannot have a folder with the same name as a file).
+		/// </summary>
+		public string DirectoryAlreadyExistsFeedback { get; internal set; } = Strings.fdDirectoryAlreadyExistsFeedback;
+
+		/// <summary>
+		/// Gets or sets error message when user selects a file/dir that does not exist and
+		/// <see cref="OpenMode"/> is <see cref="OpenMode.Mixed"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+		/// </summary>
+		public string FileOrDirectoryMustExistFeedback { get; internal set; } = Strings.fdFileOrDirectoryMustExistFeedback;
+
+		/// <summary>
+		/// Gets the style settings for the table of files (in currently selected directory).
+		/// </summary>
+		public TableView.TableStyle TableStyle { get; internal set; }
+
+		/// <summary>
+		/// Gets the style settings for the collapse-able directory/places tree
+		/// </summary>
+		public TreeStyle TreeStyle { get; internal set; }
+
+		/// <summary>
+		/// Gets or Sets the method for getting the root tree objects that are displayed in
+		/// the collapse-able tree in the <see cref="FileDialog"/>.  Defaults to all accessible
+		/// <see cref="System.Environment.GetLogicalDrives"/> and unique
+		/// <see cref="Environment.SpecialFolder"/>.
+		/// </summary>
+		/// <remarks>Must be configured before showing the dialog.</remarks>
+		public FileDialogTreeRootGetter TreeRootGetter { get; set; } = DefaultTreeRootGetter;
+
+		/// <summary>
+		/// Gets or sets whether to use advanced unicode characters which might not be installed
+		/// on all users computers.
+		/// </summary>
+		public bool UseUnicodeCharacters { get; set; } = DefaultUseUnicodeCharacters;
+
+
+		/// <summary>
+		/// User defined delegate for picking which character(s)/unicode
+		/// symbol(s) to use as an 'icon' for files/folders. 
+		/// </summary>
+		public Func<IFileSystemInfo, string> IconGetter { get; set; }
+
+
+		/// <summary>
+		/// Gets or sets the format to use for date/times in the Modified column.
+		/// Defaults to <see cref="DateTimeFormatInfo.SortableDateTimePattern"/> 
+		/// of the <see cref="CultureInfo.CurrentCulture"/>
+		/// </summary>
+		public string DateFormat { get; set; }
+
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="FileDialogStyle"/> class.
+		/// </summary>
+		public FileDialogStyle ()
+		{
+			IconGetter = DefaultIconGetter;
+			DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
+
+			ColorSchemeDirectory = new ColorScheme {
+				Normal = Application.Driver.MakeAttribute (Color.Blue, Color.Black),
+				HotNormal = Application.Driver.MakeAttribute (Color.Blue, Color.Black),
+				Focus = Application.Driver.MakeAttribute (Color.Black, Color.Blue),
+				HotFocus = Application.Driver.MakeAttribute (Color.Black, Color.Blue),
+
+			};
+
+			ColorSchemeImage = new ColorScheme {
+				Normal = Application.Driver.MakeAttribute (Color.Magenta, Color.Black),
+				HotNormal = Application.Driver.MakeAttribute (Color.Magenta, Color.Black),
+				Focus = Application.Driver.MakeAttribute (Color.Black, Color.Magenta),
+				HotFocus = Application.Driver.MakeAttribute (Color.Black, Color.Magenta),
+			};
+			ColorSchemeExeOrRecommended = new ColorScheme {
+				Normal = Application.Driver.MakeAttribute (Color.Green, Color.Black),
+				HotNormal = Application.Driver.MakeAttribute (Color.Green, Color.Black),
+				Focus = Application.Driver.MakeAttribute (Color.Black, Color.Green),
+				HotFocus = Application.Driver.MakeAttribute (Color.Black, Color.Green),
+			};
+			ColorSchemeOther = new ColorScheme {
+				Normal = Application.Driver.MakeAttribute (Color.White, Color.Black),
+				HotNormal = Application.Driver.MakeAttribute (Color.White, Color.Black),
+				Focus = Application.Driver.MakeAttribute (Color.Black, Color.White),
+				HotFocus = Application.Driver.MakeAttribute (Color.Black, Color.White),
+			};
+
+		}
+
+		private string DefaultIconGetter (IFileSystemInfo arg)
+		{
+			if (arg is IDirectoryInfo) {
+				return UseUnicodeCharacters ? "\ua909 " : "\\";
+			}
+
+			return UseUnicodeCharacters ? "\u2630 " : "";
+
+		}
+
+		private static IEnumerable<FileDialogRootTreeNode> DefaultTreeRootGetter ()
+		{
+			var roots = new List<FileDialogRootTreeNode> ();
+			try {
+				foreach (var d in Environment.GetLogicalDrives ()) {
+					roots.Add (new FileDialogRootTreeNode (d, new DirectoryInfo (d)));
+				}
+
+			} catch (Exception) {
+				// Cannot get the system disks thats fine
+			}
+
+
+			try {
+				foreach (var special in Enum.GetValues (typeof (Environment.SpecialFolder)).Cast<SpecialFolder> ()) {
+					try {
+						var path = Environment.GetFolderPath (special);
+						if (
+							!string.IsNullOrWhiteSpace (path)
+							&& Directory.Exists (path)
+							&& !roots.Any (r => string.Equals (r.Path.FullName, path))) {
+
+							roots.Add (new FileDialogRootTreeNode (
+							special.ToString (),
+							new DirectoryInfo (Environment.GetFolderPath (special))));
+						}
+					} catch (Exception) {
+						// Special file exists but contents are unreadable (permissions?)
+						// skip it anyway
+					}
+				}
+			} catch (Exception) {
+				// Cannot get the special files for this OS oh well
+			}
+
+			return roots;
+		}
+	}
+
+}

+ 38 - 0
Terminal.Gui/FileServices/FileDialogTreeBuilder.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+
+namespace Terminal.Gui {
+
+	class FileDialogTreeBuilder : ITreeBuilder<object> {
+		public bool SupportsCanExpand => true;
+
+		public bool CanExpand (object toExpand)
+		{
+			return this.TryGetDirectories (NodeToDirectory (toExpand)).Any ();
+		}
+
+		public IEnumerable<object> GetChildren (object forObject)
+		{
+			return this.TryGetDirectories (NodeToDirectory (forObject));
+		}
+
+		internal static DirectoryInfo NodeToDirectory (object toExpand)
+		{
+			return toExpand is FileDialogRootTreeNode f ? f.Path : (DirectoryInfo)toExpand;
+		}
+
+		private IEnumerable<DirectoryInfo> TryGetDirectories (DirectoryInfo directoryInfo)
+		{
+			try {
+				return directoryInfo.EnumerateDirectories ();
+			} catch (Exception) {
+
+				return Enumerable.Empty<DirectoryInfo> ();
+			}
+		}
+
+	}
+}

+ 134 - 0
Terminal.Gui/FileServices/FileSystemInfoStats.cs

@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Wrapper for <see cref="FileSystemInfo"/> that contains additional information
+	/// (e.g. <see cref="IsParent"/>) and helper methods.
+	/// </summary>
+	internal class FileSystemInfoStats {
+
+
+		/* ---- Colors used by the ls command line tool ----
+		 *
+		* Blue: Directory
+		* Green: Executable or recognized data file
+		* Cyan (Sky Blue): Symbolic link file
+		* Yellow with black background: Device
+		* Magenta (Pink): Graphic image file
+		* Red: Archive file
+		* Red with black background: Broken link
+		*/
+
+		private const long ByteConversion = 1024;
+
+		private static readonly string [] SizeSuffixes = { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
+		private static readonly List<string> ImageExtensions = new List<string> { ".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG" };
+		private static readonly List<string> ExecutableExtensions = new List<string> { ".EXE", ".BAT" };
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="FileSystemInfoStats"/> class.
+		/// </summary>
+		/// <param name="fsi">The directory of path to wrap.</param>
+		public FileSystemInfoStats (IFileSystemInfo fsi)
+		{
+			this.FileSystemInfo = fsi;
+			this.LastWriteTime = fsi.LastWriteTime;
+
+			if (fsi is IFileInfo fi) {
+				this.MachineReadableLength = fi.Length;
+				this.HumanReadableLength = GetHumanReadableFileSize (this.MachineReadableLength);				
+				this.Type = fi.Extension;
+			} else {
+				this.HumanReadableLength = string.Empty;
+				this.Type = "dir";
+			}
+		}
+
+		/// <summary>
+		/// Gets the wrapped <see cref="FileSystemInfo"/> (directory or file).
+		/// </summary>
+		public IFileSystemInfo FileSystemInfo { get; }
+		public string HumanReadableLength { get; }
+		public long MachineReadableLength { get; }
+		public DateTime? LastWriteTime { get; }
+		public string Type { get; }
+
+		/// <summary>
+		/// Gets or Sets a value indicating whether this instance represents
+		/// the parent of the current state (i.e. "..").
+		/// </summary>
+		public bool IsParent { get; internal set; }
+		public string Name => this.IsParent ? ".." : this.FileSystemInfo.Name;
+
+		public bool IsDir ()
+		{
+			return this.Type == "dir";
+		}
+
+		public bool IsImage ()
+		{
+			return this.FileSystemInfo is FileSystemInfo f &&
+				ImageExtensions.Contains (
+					f.Extension,
+					StringComparer.InvariantCultureIgnoreCase);
+		}
+
+		public bool IsExecutable ()
+		{
+			// TODO: handle linux executable status
+			return this.FileSystemInfo is FileSystemInfo f &&
+				ExecutableExtensions.Contains (
+					f.Extension,
+					StringComparer.InvariantCultureIgnoreCase);
+		}
+
+		internal object GetOrderByValue (FileDialog dlg, string columnName)
+		{
+			if (dlg.Style.FilenameColumnName == columnName)
+				return this.FileSystemInfo.Name;
+
+			if (dlg.Style.SizeColumnName == columnName)
+				return this.MachineReadableLength;
+
+			if (dlg.Style.ModifiedColumnName == columnName)
+				return this.LastWriteTime;
+
+			if (dlg.Style.TypeColumnName == columnName)
+				return this.Type;
+
+			throw new ArgumentOutOfRangeException ("Unknown column " + nameof (columnName));
+		}
+
+		internal object GetOrderByDefault ()
+		{
+			if (this.IsDir ()) {
+				return -1;
+			}
+
+			return 100;
+		}
+
+		private static string GetHumanReadableFileSize (long value)
+		{
+
+			if (value < 0) {
+				return "-" + GetHumanReadableFileSize (-value);
+			}
+
+			if (value == 0) {
+				return "0.0 bytes";
+			}
+
+			int mag = (int)Math.Log (value, ByteConversion);
+			double adjustedSize = value / Math.Pow (1000, mag);
+
+
+			return string.Format ("{0:n2} {1}", adjustedSize, SizeSuffixes [mag]);
+		}
+	}
+}

+ 30 - 0
Terminal.Gui/FileServices/FilesSelectedEventArgs.cs

@@ -0,0 +1,30 @@
+using System;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Event args for the <see cref="FileDialog.FilesSelected"/> event
+	/// </summary>
+	public class FilesSelectedEventArgs : EventArgs {
+		/// <summary>
+		/// Set to true if you want to prevent the selection
+		/// going ahead (this will leave the <see cref="FileDialog"/>
+		/// still showing).
+		/// </summary>
+		public bool Cancel { get; set; }
+
+		/// <summary>
+		/// The dialog where the choice is being made.  Use <see cref="FileDialog.Path"/>
+		/// and/or <see cref="FileDialog.MultiSelected"/> to evaluate the users choice.
+		/// </summary>
+		public FileDialog Dialog { get; }
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="FilesSelectedEventArgs"/>
+		/// </summary>
+		/// <param name="dialog"></param>
+		public FilesSelectedEventArgs (FileDialog dialog)
+		{
+			Dialog = dialog;
+		}
+	}
+}

+ 47 - 0
Terminal.Gui/FileServices/IFileOperations.cs

@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Interface for defining how to handle file/directory 
+	/// deletion, rename and newing attempts in <see cref="FileDialog"/>.
+	/// </summary>
+	public interface IFileOperations {
+		/// <summary>
+		/// Specifies how to handle file/directory deletion attempts
+		/// in <see cref="FileDialog"/>.
+		/// </summary>
+		/// <param name="toDelete"></param>
+		/// <returns><see langword="true"/> if operation was completed or 
+		/// <see langword="false"/> if cancelled</returns>
+		/// <remarks>Ensure you use a try/catch block with appropriate
+		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
+		bool Delete (IEnumerable<IFileSystemInfo> toDelete);
+
+
+		/// <summary>
+		/// Specifies how to handle file/directory rename attempts
+		/// in <see cref="FileDialog"/>.
+		/// </summary>
+		/// <param name="fileSystem"></param>
+		/// <param name="toRename"></param>
+		/// <returns>The new name for the file or null if cancelled</returns>
+		/// <remarks>Ensure you use a try/catch block with appropriate
+		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
+		IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename);
+
+
+		/// <summary>
+		/// Specifies how to handle 'new directory' operation
+		/// in <see cref="FileDialog"/>.
+		/// </summary>
+		/// <param name="fileSystem"></param>
+		/// <param name="inDirectory">The parent directory in which the new
+		/// directory should be created</param>
+		/// <returns>The newly created directory or null if cancelled.</returns>
+		/// <remarks>Ensure you use a try/catch block with appropriate
+		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
+		IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory);
+	}
+}

+ 23 - 0
Terminal.Gui/FileServices/ISearchMatcher.cs

@@ -0,0 +1,23 @@
+using System.IO;
+using System.IO.Abstractions;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Defines whether a given file/directory matches a set of
+	/// search terms.
+	/// </summary>
+	public interface ISearchMatcher {
+		/// <summary>
+		/// Called once for each new search. Defines the string
+		/// the user has provided as search terms.
+		/// </summary>
+		void Initialize (string terms);
+
+		/// <summary>
+		/// Return true if <paramref name="f"/> is a match to the
+		/// last provided search terms
+		/// </summary>
+		bool IsMatch (IFileSystemInfo f);
+	}
+}

+ 216 - 0
Terminal.Gui/Resources/Strings.Designer.cs

@@ -123,6 +123,42 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Any Files.
+        /// </summary>
+        internal static string fdAnyFiles {
+            get {
+                return ResourceManager.GetString("fdAnyFiles", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Are you sure you want to delete &apos;{0}&apos;? This operation is permanent.
+        /// </summary>
+        internal static string fdDeleteBody {
+            get {
+                return ResourceManager.GetString("fdDeleteBody", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Delete Failed.
+        /// </summary>
+        internal static string fdDeleteFailedTitle {
+            get {
+                return ResourceManager.GetString("fdDeleteFailedTitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Delete {0}.
+        /// </summary>
+        internal static string fdDeleteTitle {
+            get {
+                return ResourceManager.GetString("fdDeleteTitle", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Directory.
         /// </summary>
@@ -132,6 +168,33 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Directory already exists with that name.
+        /// </summary>
+        internal static string fdDirectoryAlreadyExistsFeedback {
+            get {
+                return ResourceManager.GetString("fdDirectoryAlreadyExistsFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Must select an existing directory.
+        /// </summary>
+        internal static string fdDirectoryMustExistFeedback {
+            get {
+                return ResourceManager.GetString("fdDirectoryMustExistFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Existing.
+        /// </summary>
+        internal static string fdExisting {
+            get {
+                return ResourceManager.GetString("fdExisting", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to File.
         /// </summary>
@@ -141,6 +204,78 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to File already exists with that name.
+        /// </summary>
+        internal static string fdFileAlreadyExistsFeedback {
+            get {
+                return ResourceManager.GetString("fdFileAlreadyExistsFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Must select an existing file.
+        /// </summary>
+        internal static string fdFileMustExistFeedback {
+            get {
+                return ResourceManager.GetString("fdFileMustExistFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Filename.
+        /// </summary>
+        internal static string fdFilename {
+            get {
+                return ResourceManager.GetString("fdFilename", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Must select an existing file or directory.
+        /// </summary>
+        internal static string fdFileOrDirectoryMustExistFeedback {
+            get {
+                return ResourceManager.GetString("fdFileOrDirectoryMustExistFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Modified.
+        /// </summary>
+        internal static string fdModified {
+            get {
+                return ResourceManager.GetString("fdModified", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to New Failed.
+        /// </summary>
+        internal static string fdNewFailed {
+            get {
+                return ResourceManager.GetString("fdNewFailed", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to New Folder.
+        /// </summary>
+        internal static string fdNewTitle {
+            get {
+                return ResourceManager.GetString("fdNewTitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to No.
+        /// </summary>
+        internal static string fdNo {
+            get {
+                return ResourceManager.GetString("fdNo", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Open.
         /// </summary>
@@ -150,6 +285,42 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Enter Path.
+        /// </summary>
+        internal static string fdPathCaption {
+            get {
+                return ResourceManager.GetString("fdPathCaption", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Rename Failed.
+        /// </summary>
+        internal static string fdRenameFailedTitle {
+            get {
+                return ResourceManager.GetString("fdRenameFailedTitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Name:.
+        /// </summary>
+        internal static string fdRenamePrompt {
+            get {
+                return ResourceManager.GetString("fdRenamePrompt", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Rename.
+        /// </summary>
+        internal static string fdRenameTitle {
+            get {
+                return ResourceManager.GetString("fdRenameTitle", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Save.
         /// </summary>
@@ -168,6 +339,15 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Enter Search.
+        /// </summary>
+        internal static string fdSearchCaption {
+            get {
+                return ResourceManager.GetString("fdSearchCaption", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Select folder.
         /// </summary>
@@ -186,6 +366,42 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Size.
+        /// </summary>
+        internal static string fdSize {
+            get {
+                return ResourceManager.GetString("fdSize", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Type.
+        /// </summary>
+        internal static string fdType {
+            get {
+                return ResourceManager.GetString("fdType", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Wrong file type.
+        /// </summary>
+        internal static string fdWrongFileTypeFeedback {
+            get {
+                return ResourceManager.GetString("fdWrongFileTypeFeedback", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Yes.
+        /// </summary>
+        internal static string fdYes {
+            get {
+                return ResourceManager.GetString("fdYes", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to _Back.
         /// </summary>

+ 76 - 0
Terminal.Gui/Resources/Strings.resx

@@ -168,4 +168,80 @@
   <data name="wzNext" xml:space="preserve">
     <value>_Next...</value>
   </data>
+  <data name="fdDirectoryAlreadyExistsFeedback" xml:space="preserve">
+    <value>Directory already exists with that name</value>
+    <comment>When trying to save a file with a name already taken by a directory</comment>
+  </data>
+  <data name="fdDirectoryMustExistFeedback" xml:space="preserve">
+    <value>Must select an existing directory</value>
+  </data>
+  <data name="fdFileAlreadyExistsFeedback" xml:space="preserve">
+    <value>File already exists with that name</value>
+  </data>
+  <data name="fdFileMustExistFeedback" xml:space="preserve">
+    <value>Must select an existing file</value>
+    <comment>When trying to save a directory with a name already used by a file</comment>
+  </data>
+  <data name="fdFilename" xml:space="preserve">
+    <value>Filename</value>
+  </data>
+  <data name="fdFileOrDirectoryMustExistFeedback" xml:space="preserve">
+    <value>Must select an existing file or directory</value>
+  </data>
+  <data name="fdModified" xml:space="preserve">
+    <value>Modified</value>
+  </data>
+  <data name="fdPathCaption" xml:space="preserve">
+    <value>Enter Path</value>
+  </data>
+  <data name="fdSearchCaption" xml:space="preserve">
+    <value>Enter Search</value>
+  </data>
+  <data name="fdSize" xml:space="preserve">
+    <value>Size</value>
+  </data>
+  <data name="fdType" xml:space="preserve">
+    <value>Type</value>
+  </data>
+  <data name="fdWrongFileTypeFeedback" xml:space="preserve">
+    <value>Wrong file type</value>
+    <comment>When trying to open/save a file that does not match the provided filter (e.g. csv)</comment>
+  </data>
+  <data name="fdAnyFiles" xml:space="preserve">
+    <value>Any Files</value>
+    <comment>Describes an AllowedType that matches anything</comment>
+  </data>
+  <data name="fdDeleteBody" xml:space="preserve">
+    <value>Are you sure you want to delete '{0}'? This operation is permanent</value>
+  </data>
+  <data name="fdDeleteFailedTitle" xml:space="preserve">
+    <value>Delete Failed</value>
+  </data>
+  <data name="fdDeleteTitle" xml:space="preserve">
+    <value>Delete {0}</value>
+  </data>
+  <data name="fdNewFailed" xml:space="preserve">
+    <value>New Failed</value>
+  </data>
+  <data name="fdNewTitle" xml:space="preserve">
+    <value>New Folder</value>
+  </data>
+  <data name="fdNo" xml:space="preserve">
+    <value>No</value>
+  </data>
+  <data name="fdRenameFailedTitle" xml:space="preserve">
+    <value>Rename Failed</value>
+  </data>
+  <data name="fdRenamePrompt" xml:space="preserve">
+    <value>Name:</value>
+  </data>
+  <data name="fdRenameTitle" xml:space="preserve">
+    <value>Rename</value>
+  </data>
+  <data name="fdYes" xml:space="preserve">
+    <value>Yes</value>
+  </data>
+  <data name="fdExisting" xml:space="preserve">
+    <value>Existing</value>
+  </data>
 </root>

+ 1 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -27,6 +27,7 @@
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
     <PackageReference Include="NStack.Core" Version="1.0.7" />
+    <PackageReference Include="System.IO.Abstractions" Version="19.2.4" />
     <PackageReference Include="System.Text.Json" Version="7.0.1" />
     <PackageReference Include="System.Management" Version="7.0.0" />
     <InternalsVisibleTo Include="UnitTests" />

+ 3 - 12
Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs

@@ -138,7 +138,7 @@ namespace Terminal.Gui {
 				newText += insert.Replacement;
 				textField.Text = newText;
 
-				this.MoveCursorToEnd ();
+				this.textField.MoveEnd();
 
 				this.ClearSuggestions ();
 				return true;
@@ -147,11 +147,6 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		internal void MoveCursorToEnd ()
-		{
-			textField.ClearAllSelection ();
-			textField.CursorPosition = textField.Text.Length;
-		}
 
 		internal void SetTextTo (FileSystemInfo fileSystemInfo)
 		{
@@ -160,13 +155,9 @@ namespace Terminal.Gui {
 				newText += System.IO.Path.DirectorySeparatorChar;
 			}
 			textField.Text = newText;
-			this.MoveCursorToEnd ();
+			textField.MoveEnd ();
 		}
 
-		internal bool CursorIsAtEnd ()
-		{
-			return textField.CursorPosition == textField.Text.Length;
-		}
 
 		/// <summary>
 		/// Returns true if there is a suggestion that can be made and the control
@@ -176,7 +167,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		private bool MakingSuggestion ()
 		{
-			return Suggestions.Any () && this.SelectedIdx != -1 && textField.HasFocus && this.CursorIsAtEnd ();
+			return Suggestions.Any () && this.SelectedIdx != -1 && textField.HasFocus && textField.CursorIsAtEnd ();
 		}
 
 		private bool CycleSuggestion (int direction)

+ 91 - 0
Terminal.Gui/Views/AutocompleteFilepathContext.cs

@@ -0,0 +1,91 @@
+using NStack;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui {
+	internal class AutocompleteFilepathContext : AutocompleteContext {
+		public FileDialogState State { get; set; }
+
+		public AutocompleteFilepathContext (ustring currentLine, int cursorPosition, FileDialogState state)
+			: base (currentLine.ToRuneList (), cursorPosition)
+		{
+			this.State = state;
+		}
+	}
+
+
+	internal class FilepathSuggestionGenerator : ISuggestionGenerator {
+
+		FileDialogState state;
+		public IEnumerable<Suggestion> GenerateSuggestions (AutocompleteContext context)
+		{
+			if (context is AutocompleteFilepathContext fileState) {
+				this.state = fileState.State;
+			}
+
+			if (state == null) {
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			var path = ustring.Make (context.CurrentLine).ToString ();
+			var last = path.LastIndexOfAny (FileDialog.Separators);
+			
+			if(string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) {
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			var term = path.Substring (last + 1);
+			
+			// If path is /tmp/ then don't just list everything in it
+			if(string.IsNullOrWhiteSpace(term))
+			{
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			if (term.Equals (state?.Directory?.Name)) {
+				// Clear suggestions
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			bool isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows);
+
+			var suggestions = state.Children.Where(d=> !d.IsParent).Select (
+				e => e.FileSystemInfo is IDirectoryInfo d
+					? d.Name + System.IO.Path.DirectorySeparatorChar
+					: e.FileSystemInfo.Name)
+				.ToArray ();
+
+			var validSuggestions = suggestions
+				.Where (s => s.StartsWith (term, isWindows ?
+					StringComparison.InvariantCultureIgnoreCase :
+					StringComparison.InvariantCulture))
+				.OrderBy (m => m.Length)
+				.ToArray ();
+
+
+			// nothing to suggest
+			if (validSuggestions.Length == 0 || validSuggestions [0].Length == term.Length) {
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			return validSuggestions.Select (
+				f => new Suggestion (term.Length, f, f)).ToList ();
+		}
+
+		public bool IsWordChar (Rune rune)
+		{
+			if (rune == '\n') {
+				return false;
+			}
+
+
+			return true;
+		}
+
+
+	}
+}

+ 19 - 1
Terminal.Gui/Views/Button.cs

@@ -163,13 +163,31 @@ namespace Terminal.Gui {
 			}
 		}
 
+		public bool NoDecorations {get;set;}
+		public bool NoPadding {get;set;}
+
 		/// <inheritdoc/>
 		protected override void UpdateTextFormatterText ()
 		{
+			if(NoDecorations)
+			{
+				TextFormatter.Text = Text;
+			}
+			else
 			if (IsDefault)
 				TextFormatter.Text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + Text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket);
 			else
-				TextFormatter.Text = ustring.Make (_leftBracket) + " " + Text + " " + ustring.Make (_rightBracket);
+			{
+				if(NoPadding)
+				{
+					TextFormatter.Text = ustring.Make (_leftBracket) + Text + ustring.Make (_rightBracket);
+				}
+				else
+				{
+					TextFormatter.Text = ustring.Make (_leftBracket) + " " + Text + " " + ustring.Make (_rightBracket);
+				}
+			}
+				
 		}
 
 		///<inheritdoc/>

+ 1 - 1
Terminal.Gui/Views/DateField.cs

@@ -354,7 +354,7 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		bool MoveEnd ()
+		new bool MoveEnd ()
 		{
 			CursorPosition = fieldLen;
 			return true;

+ 162 - 0
Terminal.Gui/Views/FileDialog.cd

@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+  <Class Name="Terminal.Gui.FileDialog">
+    <Position X="0.5" Y="0.5" Width="2.75" />
+    <Compartments>
+      <Compartment Name="Fields" Collapsed="true" />
+      <Compartment Name="Methods" Collapsed="true" />
+      <Compartment Name="Nested Types" Collapsed="false" />
+    </Compartments>
+    <NestedTypes>
+      <Class Name="Terminal.Gui.FileDialog.FileDialogSorter" Collapsed="true">
+        <TypeIdentifier>
+          <NewMemberFileName>Windows\FileDialog.cs</NewMemberFileName>
+        </TypeIdentifier>
+      </Class>
+      <Class Name="Terminal.Gui.FileDialog.SearchState" Collapsed="true">
+        <TypeIdentifier>
+          <NewMemberFileName>Windows\FileDialog.cs</NewMemberFileName>
+        </TypeIdentifier>
+      </Class>
+    </NestedTypes>
+    <TypeIdentifier>
+      <HashCode>goYYDAEnEDIZgHMByFAikQDFSIUQpUDABoZIFRSQwgQ=</HashCode>
+      <FileName>Views\FileDialog.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="history" />
+      <Property Name="Style" />
+      <Property Name="OpenMode" />
+    </ShowAsAssociation>
+    <ShowAsCollectionAssociation>
+      <Property Name="AllowedTypes" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.AllowedTypeAny" BaseTypeListCollapsed="true">
+    <Position X="11.75" Y="5.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA=</HashCode>
+      <FileName>FileServices\AllowedType.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AllowedType" BaseTypeListCollapsed="true">
+    <Position X="13.25" Y="5.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAgAAAEAAAAAAAAAAAAAAAAAAgAAAAABAA=</HashCode>
+      <FileName>FileServices\AllowedType.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.DefaultFileOperations" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="7" Y="6" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAACAAAAAAACAAAAEAAAAAAAAAAAAAgAA=</HashCode>
+      <FileName>FileServices\DefaultFileOperations.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.DefaultSearchMatcher" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="9.25" Y="6" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAACAAAAAAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\DefaultSearchMatcher.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogHistory">
+    <Position X="9.25" Y="0.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AQABAgEAAAAAAAAAIACAAAAAAAAAAQAAAAAAAAAADAI=</HashCode>
+      <FileName>FileServices\FileDialogHistory.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsCollectionAssociation>
+      <Field Name="back" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogRootTreeNode" Collapsed="true">
+    <Position X="2.5" Y="8.25" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAEAAAAAAAAAIEAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\FileDialogRootTreeNode.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogState">
+    <Position X="11.5" Y="0.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AABAABAAAAAAAAAAAAAEQAAAAAAAQAAAAgAAAAAAAAI=</HashCode>
+      <FileName>FileServices\FileDialogState.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsCollectionAssociation>
+      <Property Name="Children" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogStyle">
+    <Position X="3.5" Y="0.5" Width="2.75" />
+    <TypeIdentifier>
+      <HashCode>GgBIAAFEAAAAuAAAAAAEEACABAACKRkAAAEYACCAAAA=</HashCode>
+      <FileName>FileServices\FileDialogStyle.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogTreeBuilder" BaseTypeListCollapsed="true">
+    <Position X="0.5" Y="6.75" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>EAAAAAAAAAAAAAAAQAAAAAAAAAAAQAAAAAAAAAQACAA=</HashCode>
+      <FileName>FileServices\FileDialogTreeBuilder.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.FilesSelectedEventArgs">
+    <Position X="6.5" Y="0.5" Width="2" />
+    <Compartments>
+      <Compartment Name="Methods" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\FilesSelectedEventArgs.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.FileSystemInfoStats">
+    <Position X="14" Y="0.5" Width="2.5" />
+    <TypeIdentifier>
+      <HashCode>ABAIQAIIIAAAAAACQAAAAIQAAAQAAIAAAQABAAAYAAI=</HashCode>
+      <FileName>FileServices\FileSystemInfoStats.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Interface Name="Terminal.Gui.IAllowedType" Collapsed="true">
+    <Position X="12" Y="4.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA=</HashCode>
+      <FileName>FileServices\AllowedType.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IFileOperations">
+    <Position X="7" Y="4.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAACAAAAAAAAAAAAEAAAAAAAAAAAAAgAA=</HashCode>
+      <FileName>FileServices\IFileOperations.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.ISearchMatcher">
+    <Position X="9.25" Y="4.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\ISearchMatcher.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Enum Name="Terminal.Gui.OpenMode">
+    <Position X="7.5" Y="2.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAABAAAAAAACAAAAAAAAAAAAAEAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>Views\OpenDialog.cs</FileName>
+    </TypeIdentifier>
+  </Enum>
+  <Delegate Name="Terminal.Gui.FileDialogTreeRootGetter" Collapsed="true">
+    <Position X="2.5" Y="7.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA=</HashCode>
+      <FileName>FileServices\FileDialogRootTreeNode.cs</FileName>
+    </TypeIdentifier>
+  </Delegate>
+  <Font Name="Segoe UI" Size="9" />
+</ClassDiagram>

+ 1430 - 849
Terminal.Gui/Views/FileDialog.cs

@@ -1,1053 +1,1634 @@
-// 
-// FileDialog.cs: File system dialogs for open and save
-//
-// TODO:
-//   * Add directory selector
-//   * Implement subclasses
-//   * Figure out why message text does not show
-//   * Remove the extra space when message does not show
-//   * Use a line separator to show the file listing, so we can use same colors as the rest
-//   * DirListView: Add mouse support
-
 using System;
 using System.Collections.Generic;
-using NStack;
+using System.Data;
 using System.IO;
+using System.IO.Abstractions;
 using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using NStack;
 using Terminal.Gui.Resources;
+using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui {
-	internal class DirListView : View {
-		int top, selected;
-		DirectoryInfo dirInfo;
-		FileSystemWatcher watcher;
-		List<(string, bool, bool)> infos;
-		internal bool canChooseFiles = true;
-		internal bool canChooseDirectories = false;
-		internal bool allowsMultipleSelection = false;
-		FileDialog host;
-
-		public DirListView (FileDialog host)
+
+	/// <summary>
+	/// Modal dialog for selecting files/directories. Has auto-complete and expandable
+	/// navigation pane (Recent, Root drives etc).
+	/// </summary>
+	public partial class FileDialog : Dialog {
+
+		/// <summary>
+		/// Gets settings for controlling how visual elements behave.  Style changes should
+		/// be made before the <see cref="Dialog"/> is loaded and shown to the user for the
+		/// first time.
+		/// </summary>
+		public FileDialogStyle Style { get; } = new FileDialogStyle ();
+
+		/// <summary>
+		/// The maximum number of results that will be collected
+		/// when searching before stopping.
+		/// </summary>
+		/// <remarks>
+		/// This prevents performance issues e.g. when searching
+		/// root of file system for a common letter (e.g. 'e').
+		/// </remarks>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+		public static int MaxSearchResults { get; set; } = 10000;
+
+		/// <summary>
+		/// True if the file/folder must exist already to be selected.
+		/// This prevents user from entering the name of something that
+		/// doesn't exist. Defaults to false.
+		/// </summary>
+		public bool MustExist { get; set; }
+
+		/// <summary>
+		/// Gets the Path separators for the operating system
+		/// </summary>
+		internal static char [] Separators = new []
 		{
-			infos = new List<(string, bool, bool)> ();
-			CanFocus = true;
-			this.host = host;
-		}
+			System.IO.Path.AltDirectorySeparatorChar,
+			System.IO.Path.DirectorySeparatorChar,
+		};
 
-		bool IsAllowed (FileSystemInfo fsi)
+		/// <summary>
+		/// Characters to prevent entry into <see cref="tbPath"/>. Note that this is not using
+		/// <see cref="System.IO.Path.GetInvalidFileNameChars"/> because we do want to allow directory
+		/// separators, arrow keys etc.
+		/// </summary>
+		private static char [] badChars = new []
 		{
-			if (fsi.Attributes.HasFlag (FileAttributes.Directory))
-				return true;
-			if (allowedFileTypes == null)
-				return true;
-			foreach (var ft in allowedFileTypes)
-				if (fsi.Name.EndsWith (ft, StringComparison.InvariantCultureIgnoreCase) || ft == ".*")
-					return true;
-			return false;
+			'"','<','>','|','*','?',
+		};
+
+
+		/// <summary>
+		/// The UI selected <see cref="IAllowedType"/> from combo box. May be null.
+		/// </summary>
+		public IAllowedType CurrentFilter { get; private set; }
+
+		private bool pushingState = false;
+		private bool loaded = false;
+
+		/// <summary>
+		/// Gets the currently open directory and known children presented in the dialog.
+		/// </summary>
+		internal FileDialogState State { get; private set; }
+
+		/// <summary>
+		/// Locking object for ensuring only a single <see cref="SearchState"/> executes at once.
+		/// </summary>
+		internal object onlyOneSearchLock = new object ();
+
+		private bool disposed = false;
+		private IFileSystem fileSystem;
+		private TextField tbPath;
+
+		private FileDialogSorter sorter;
+		private FileDialogHistory history;
+
+		private DataTable dtFiles;
+		private TableView tableView;
+		private TreeView<object> treeView;
+		private TileView splitContainer;
+		private Button btnOk;
+		private Button btnCancel;
+		private Button btnToggleSplitterCollapse;
+		private Button btnForward;
+		private Button btnBack;
+		private Button btnUp;
+		private string feedback;
+
+		private CollectionNavigator collectionNavigator = new CollectionNavigator ();
+
+		private TextField tbFind;
+		private SpinnerView spinnerView;
+		private MenuBar allowedTypeMenuBar;
+		private MenuBarItem allowedTypeMenu;
+		private MenuItem [] allowedTypeMenuItems;
+		private DataColumn filenameColumn;
+
+		/// <summary>
+		/// Event fired when user attempts to confirm a selection (or multi selection).
+		/// Allows you to cancel the selection or undertake alternative behavior e.g.
+		/// open a dialog "File already exists, Overwrite? yes/no".
+		/// </summary>
+		public event EventHandler<FilesSelectedEventArgs> FilesSelected;
+
+		/// <summary>
+		/// Gets or sets behavior of the <see cref="FileDialog"/> when the user attempts
+		/// to delete a selected file(s).  Set to null to prevent deleting.
+		/// </summary>
+		/// <remarks>Ensure you use a try/catch block with appropriate
+		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
+		public IFileOperations FileOperationsHandler { get; set; } = new DefaultFileOperations ();
+
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="FileDialog"/> class.
+		/// </summary>
+		public FileDialog () : this(new FileSystem())
+		{
+
 		}
 
-		internal bool Reload (ustring value = null)
+		/// <summary>
+		/// Initializes a new instance of the <see cref="FileDialog"/> class with
+		/// a custom <see cref="IFileSystem"/>.
+		/// </summary>
+		/// <remarks>This overload is mainly useful for testing.</remarks>
+		public FileDialog (IFileSystem fileSystem)
 		{
-			bool valid = false;
-			try {
-				dirInfo = new DirectoryInfo (value == null ? directory.ToString () : value.ToString ());
-
-				// Dispose of the old watcher
-				watcher?.Dispose ();
-
-				watcher = new FileSystemWatcher (dirInfo.FullName);
-				watcher.NotifyFilter = NotifyFilters.Attributes
-				 | NotifyFilters.CreationTime
-				 | NotifyFilters.DirectoryName
-				 | NotifyFilters.FileName
-				 | NotifyFilters.LastAccess
-				 | NotifyFilters.LastWrite
-				 | NotifyFilters.Security
-				 | NotifyFilters.Size;
-				watcher.Changed += Watcher_Changed;
-				watcher.Created += Watcher_Changed;
-				watcher.Deleted += Watcher_Changed;
-				watcher.Renamed += Watcher_Changed;
-				watcher.Error += Watcher_Error;
-				watcher.EnableRaisingEvents = true;
-				infos = (from x in dirInfo.GetFileSystemInfos ()
-					 where IsAllowed (x) && (!canChooseFiles ? x.Attributes.HasFlag (FileAttributes.Directory) : true)
-					 orderby (!x.Attributes.HasFlag (FileAttributes.Directory)) + x.Name
-					 select (x.Name, x.Attributes.HasFlag (FileAttributes.Directory), false)).ToList ();
-				infos.Insert (0, ("..", true, false));
-				top = 0;
-				selected = 0;
-				valid = true;
-			} catch (Exception ex) {
-				switch (ex) {
-				case DirectoryNotFoundException _:
-				case ArgumentException _:
-					dirInfo = null;
-					watcher?.Dispose ();
-					watcher = null;
-					infos.Clear ();
-					valid = true;
-					break;
-				default:
-					valid = false;
-					break;
+			this.fileSystem = fileSystem;
+			this.btnOk = new Button (Style.OkButtonText) {
+				Y = Pos.AnchorEnd (1),
+				X = Pos.Function (() =>
+					this.Bounds.Width
+					- btnOk.Bounds.Width
+					// TODO: Fiddle factor, seems the Bounds are wrong for someone
+					- 2)
+			};
+			this.btnOk.Clicked += (s, e) => this.Accept ();
+			this.btnOk.KeyPress += (s, k) => {
+				this.NavigateIf (k, Key.CursorLeft, this.btnCancel);
+				this.NavigateIf (k, Key.CursorUp, this.tableView);
+			};
+
+			this.btnCancel = new Button ("Cancel") {
+				Y = Pos.AnchorEnd (1),
+				X = Pos.Function (() =>
+					this.Bounds.Width
+					- btnOk.Bounds.Width
+					- btnCancel.Bounds.Width
+					- 1
+					// TODO: Fiddle factor, seems the Bounds are wrong for someone
+					- 2
+					)
+			};
+			this.btnCancel.KeyPress += (s, k) => {
+				this.NavigateIf (k, Key.CursorLeft, this.btnToggleSplitterCollapse);
+				this.NavigateIf (k, Key.CursorUp, this.tableView);
+				this.NavigateIf (k, Key.CursorRight, this.btnOk);
+			};
+			this.btnCancel.Clicked += (s, e) => {
+				Application.RequestStop ();
+			};
+
+			this.btnUp = new Button () { X = 0, Y = 1, NoPadding = true };
+			btnUp.Text = GetUpButtonText ();
+			this.btnUp.Clicked += (s, e) => this.history.Up ();
+
+			this.btnBack = new Button () { X = Pos.Right (btnUp) + 1, Y = 1, NoPadding = true };
+			btnBack.Text = GetBackButtonText ();
+			this.btnBack.Clicked += (s, e) => this.history.Back ();
+
+			this.btnForward = new Button () { X = Pos.Right (btnBack) + 1, Y = 1, NoPadding = true };
+			btnForward.Text = GetForwardButtonText();
+			this.btnForward.Clicked += (s, e) => this.history.Forward ();
+
+			this.tbPath = new TextField {
+				Width = Dim.Fill (0),
+				Caption = Style.PathCaption,
+				CaptionColor = Color.Black
+			};
+			this.tbPath.KeyPress += (s, k) => {
+
+				ClearFeedback ();
+
+				this.AcceptIf (k, Key.Enter);
+
+				this.SuppressIfBadChar (k);
+			};
+
+			tbPath.Autocomplete = new AppendAutocomplete (tbPath);
+			tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator ();
+
+			this.splitContainer = new TileView () {
+				X = 0,
+				Y = 2,
+				Width = Dim.Fill (0),
+				Height = Dim.Fill (1),
+			};
+			this.splitContainer.SetSplitterPos (0, 30);
+//			this.splitContainer.Border.BorderStyle = BorderStyle.None;
+			this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false;
+
+			this.tableView = new TableView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				FullRowSelect = true,
+			};
+
+			this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
+			Style.TableStyle = tableView.Style;
+
+			this.tableView.KeyPress += (s, k) => {
+				if (this.tableView.SelectedRow <= 0) {
+					this.NavigateIf (k, Key.CursorUp, this.tbPath);
 				}
-			} finally {
-				if (valid) {
-					SetNeedsDisplay ();
+				if (this.tableView.SelectedRow == this.tableView.Table.Rows.Count-1) {
+					this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse);
 				}
-			}
-			return valid;
-		}
 
-		private bool _disposedValue;
-		protected override void Dispose (bool disposing)
-		{
-			if (!_disposedValue) {
-				if (disposing) {
-					if (watcher != null) {
-						watcher.Changed -= Watcher_Changed;
-						watcher.Created -= Watcher_Changed;
-						watcher.Deleted -= Watcher_Changed;
-						watcher.Renamed -= Watcher_Changed;
-						watcher.Error -= Watcher_Error;
-					}
-					watcher?.Dispose ();
-					watcher = null;
+				if (splitContainer.Tiles.First ().ContentView.Visible && tableView.SelectedColumn == 0) {
+					this.NavigateIf (k, Key.CursorLeft, this.treeView);
 				}
 
-				_disposedValue = true;
-			}
+				if (k.Handled) {
+					return;
+				}
 
-			// Call base class implementation.
-			base.Dispose (disposing);
-		}
+				if (this.tableView.HasFocus &&
+				!k.KeyEvent.Key.HasFlag (Key.CtrlMask) &&
+				!k.KeyEvent.Key.HasFlag (Key.AltMask) &&
+					char.IsLetterOrDigit ((char)k.KeyEvent.KeyValue)) {
+					CycleToNextTableEntryBeginningWith (k);
+				}
 
-		void Watcher_Error (object sender, ErrorEventArgs e)
-		{
-			if (Application.MainLoop == null)
-				return;
 
-			Application.MainLoop.Invoke (() => Reload ());
-		}
+			};
 
-		void Watcher_Changed (object sender, FileSystemEventArgs e)
-		{
-			if (Application.MainLoop == null)
-				return;
+			this.treeView = new TreeView<object> () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+			};
 
-			Application.MainLoop.Invoke (() => Reload ());
-		}
+			this.treeView.TreeBuilder = new FileDialogTreeBuilder ();
+			this.treeView.AspectGetter = (m) => m is IDirectoryInfo d ? d.Name : m.ToString ();
+			this.Style.TreeStyle = treeView.Style;
 
-		ustring directory;
-		public ustring Directory {
-			get => directory;
-			set {
-				if (directory == value) {
-					return;
+			this.treeView.SelectionChanged += this.TreeView_SelectionChanged;
+
+			this.splitContainer.Tiles.ElementAt (0).ContentView.Add (this.treeView);
+			this.splitContainer.Tiles.ElementAt (1).ContentView.Add (this.tableView);
+
+			this.btnToggleSplitterCollapse = new Button (GetToggleSplitterText (false)) {
+				Y = Pos.AnchorEnd (1),
+			};
+			this.btnToggleSplitterCollapse.Clicked += (s, e) => {
+				var tile = this.splitContainer.Tiles.ElementAt (0);
+
+				var newState = !tile.ContentView.Visible;
+				tile.ContentView.Visible = newState;
+				this.btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState);
+				this.LayoutSubviews();
+			};
+
+
+			tbFind = new TextField {
+				X = Pos.Right (this.btnToggleSplitterCollapse) + 1,
+				Caption = Style.SearchCaption,
+				CaptionColor = Color.Black,
+				Width = 30,
+				Y = Pos.AnchorEnd (1),
+			};
+			spinnerView = new SpinnerView () {
+				X = Pos.Right (tbFind) + 1,
+				Y = Pos.AnchorEnd (1),
+				Visible = false,
+			};
+
+			tbFind.TextChanged += (s, o) => RestartSearch ();
+			tbFind.KeyPress += (s, o) => {
+				if (o.KeyEvent.Key == Key.Enter) {
+					RestartSearch ();
+					o.Handled = true;
+				}
+
+				if(o.KeyEvent.Key == Key.Esc) {
+					if(CancelSearch()) {
+						o.Handled = true;
+					}
 				}
-				if (Reload (value)) {
-					directory = value;
+				if(tbFind.CursorIsAtEnd()) {
+					NavigateIf (o, Key.CursorRight, btnCancel);
 				}
-			}
-		}
+				if (tbFind.CursorIsAtStart ()) {
+					NavigateIf (o, Key.CursorLeft, btnToggleSplitterCollapse);
+				}
+			};
 
-		public override void PositionCursor ()
-		{
-			Move (0, selected - top);
-		}
+			this.tableView.Style.ShowHorizontalHeaderOverline = true;
+			this.tableView.Style.ShowVerticalCellLines = true;
+			this.tableView.Style.ShowVerticalHeaderLines = true;
+			this.tableView.Style.AlwaysShowHeaders = true;
+			this.tableView.Style.ShowHorizontalHeaderUnderline = true;
+			this.tableView.Style.ShowHorizontalScrollIndicators = true;
 
-		int lastSelected;
-		bool shiftOnWheel;
-		public override bool MouseEvent (MouseEvent me)
-		{
-			if ((me.Flags & (MouseFlags.Button1Clicked | MouseFlags.Button1DoubleClicked |
-				MouseFlags.WheeledUp | MouseFlags.WheeledDown)) == 0)
-				return false;
 
-			if (!HasFocus)
-				SetFocus ();
+			this.SetupTableColumns ();
 
-			if (infos == null)
-				return false;
+			this.sorter = new FileDialogSorter (this, this.tableView);
+			this.history = new FileDialogHistory (this);
 
-			if (me.Y + top >= infos.Count)
-				return true;
+			this.tableView.Table = this.dtFiles;
 
-			int lastSelectedCopy = shiftOnWheel ? lastSelected : selected;
+			this.tbPath.TextChanged += (s, e) => this.PathChanged ();
 
-			switch (me.Flags) {
-			case MouseFlags.Button1Clicked:
-				SetSelected (me);
-				OnSelectionChanged ();
-				SetNeedsDisplay ();
-				break;
-			case MouseFlags.Button1DoubleClicked:
-				UnMarkAll ();
-				SetSelected (me);
-				if (ExecuteSelection ()) {
-					host.canceled = false;
-					Application.RequestStop ();
+			this.tableView.CellActivated += this.CellActivate;
+			this.tableView.KeyUp += (s, k) => k.Handled = this.TableView_KeyUp (k.KeyEvent);
+			this.tableView.SelectedCellChanged += this.TableView_SelectedCellChanged;
+
+			this.tableView.AddKeyBinding (Key.Home, Command.TopHome);
+			this.tableView.AddKeyBinding (Key.End, Command.BottomEnd);
+			this.tableView.AddKeyBinding (Key.Home | Key.ShiftMask, Command.TopHomeExtend);
+			this.tableView.AddKeyBinding (Key.End | Key.ShiftMask, Command.BottomEndExtend);
+
+			this.treeView.KeyDown += (s, k) => {
+
+
+				var selected = treeView.SelectedObject;
+				if (selected != null) {
+					if (!treeView.CanExpand (selected) || treeView.IsExpanded (selected)) {
+						this.NavigateIf (k, Key.CursorRight, this.tableView);
+					} else
+					if (treeView.GetObjectRow (selected) == 0) {
+						this.NavigateIf (k, Key.CursorUp, this.tbPath);
+					}
 				}
-				return true;
-			case MouseFlags.Button1Clicked | MouseFlags.ButtonShift:
-				SetSelected (me);
-				if (shiftOnWheel)
-					lastSelected = lastSelectedCopy;
-				shiftOnWheel = false;
-				PerformMultipleSelection (lastSelected);
-				return true;
-			case MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl:
-				SetSelected (me);
-				PerformMultipleSelection ();
-				return true;
-			case MouseFlags.WheeledUp:
-				SetSelected (me);
-				selected = lastSelected;
-				MoveUp ();
-				return true;
-			case MouseFlags.WheeledDown:
-				SetSelected (me);
-				selected = lastSelected;
-				MoveDown ();
-				return true;
-			case MouseFlags.WheeledUp | MouseFlags.ButtonShift:
-				SetSelected (me);
-				selected = lastSelected;
-				lastSelected = lastSelectedCopy;
-				shiftOnWheel = true;
-				MoveUp ();
-				return true;
-			case MouseFlags.WheeledDown | MouseFlags.ButtonShift:
-				SetSelected (me);
-				selected = lastSelected;
-				lastSelected = lastSelectedCopy;
-				shiftOnWheel = true;
-				MoveDown ();
-				return true;
-			}
 
-			return true;
+				if (k.Handled) {
+					return;
+				}
+
+				k.Handled = this.TreeView_KeyDown (k.KeyEvent);
+
+			};
+
+			this.AllowsMultipleSelection = false;
+
+			this.UpdateNavigationVisibility ();
+
+			// Determines tab order
+			this.Add (this.btnToggleSplitterCollapse);
+			this.Add (this.tbFind);
+			this.Add (this.spinnerView);
+			this.Add (this.btnOk);
+			this.Add (this.btnCancel);
+			this.Add (this.btnUp);
+			this.Add (this.btnBack);
+			this.Add (this.btnForward);
+			this.Add (this.tbPath);
+			this.Add (this.splitContainer);
+
+			// Default sort order is by name
+			sorter.SortColumn(this.filenameColumn,true);
 		}
 
-		void UnMarkAll ()
+		private string GetForwardButtonText ()
 		{
-			for (int i = 0; i < infos.Count; i++) {
-				if (infos [i].Item3) {
-					infos [i] = (infos [i].Item1, infos [i].Item2, false);
-				}
-			}
+			return "-" + Driver.RightArrow;
 		}
 
-		void SetSelected (MouseEvent me)
+		private string GetBackButtonText ()
 		{
-			lastSelected = selected;
-			selected = top + me.Y;
+			return Driver.LeftArrow + "-";
 		}
 
-		void DrawString (int line, string str)
+		private string GetUpButtonText ()
 		{
-			var f = Frame;
-			var width = f.Width;
-			var ustr = ustring.Make (str);
-
-			Move (allowsMultipleSelection ? 3 : 2, line);
-			int byteLen = ustr.Length;
-			int used = allowsMultipleSelection ? 2 : 1;
-			for (int i = 0; i < byteLen;) {
-				(var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
-				var count = Rune.ColumnWidth (rune);
-				if (used + count >= width)
-					break;
-				Driver.AddRune (rune);
-				used += count;
-				i += size;
-			}
-			for (; used < width - 1; used++) {
-				Driver.AddRune (' ');
-			}
+			return Style.UseUnicodeCharacters ? "◭" : "▲";
 		}
 
-		public override void Redraw (Rect bounds)
+		private string GetToggleSplitterText (bool isExpanded)
 		{
-			var current = ColorScheme.Focus;
-			Driver.SetAttribute (current);
-			Move (0, 0);
-			var f = Frame;
-			var item = top;
-			bool focused = HasFocus;
-			var width = bounds.Width;
-
-			for (int row = 0; row < f.Height; row++, item++) {
-				bool isSelected = item == selected;
-				Move (0, row);
-				var newcolor = focused ? (isSelected ? ColorScheme.HotNormal : ColorScheme.Focus)
-					: Enabled ? ColorScheme.Focus : ColorScheme.Disabled;
-				if (newcolor != current) {
-					Driver.SetAttribute (newcolor);
-					current = newcolor;
-				}
-				if (item >= infos.Count) {
-					for (int c = 0; c < f.Width; c++)
-						Driver.AddRune (' ');
-					continue;
-				}
-				var fi = infos [item];
-
-				Driver.AddRune (isSelected ? '>' : ' ');
+			return isExpanded ? 
+				new string ((char)Driver.LeftArrow, 2) :
+				new string ((char)Driver.RightArrow, 2);
+		}
 
-				if (allowsMultipleSelection)
-					Driver.AddRune (fi.Item3 ? '*' : ' ');
+		private void Delete ()
+		{
+			var toDelete = GetFocusedFiles ();
 
-				if (fi.Item2)
-					Driver.AddRune ('/');
-				else
-					Driver.AddRune (' ');
-				DrawString (row, fi.Item1);
+			if (toDelete != null && FileOperationsHandler.Delete (toDelete)) {
+				RefreshState ();
 			}
 		}
 
-		public Action<(string, bool)> SelectedChanged { get; set; }
-		public Action<ustring> DirectoryChanged { get; set; }
-		public Action<ustring> FileChanged { get; set; }
+		private void Rename ()
+		{
+			var toRename = GetFocusedFiles ();
 
-		string splitString = ",";
+			if (toRename?.Length == 1) {
+				var newNamed = FileOperationsHandler.Rename (this.fileSystem, toRename.Single ());
 
-		void OnSelectionChanged ()
-		{
-			if (allowsMultipleSelection) {
-				if (FilePaths.Count > 0) {
-					FileChanged?.Invoke (string.Join (splitString, GetFilesName (FilePaths)));
-				} else {
-					FileChanged?.Invoke (infos [selected].Item2 && !canChooseDirectories ? "" : Path.GetFileName (infos [selected].Item1));
+				if (newNamed != null) {
+					RefreshState ();
+					RestoreSelection (newNamed);
 				}
-			} else {
-				var sel = infos [selected];
-				SelectedChanged?.Invoke ((sel.Item1, sel.Item2));
 			}
 		}
-
-		List<string> GetFilesName (IReadOnlyList<string> files)
+		private void New ()
 		{
-			List<string> filesName = new List<string> ();
-
-			foreach (var file in files) {
-				filesName.Add (Path.GetFileName (file));
+			if (State != null) {
+				var created = FileOperationsHandler.New (this.fileSystem, State.Directory);
+				if (created != null) {
+					RefreshState ();
+					RestoreSelection (created);
+				}
 			}
-
-			return filesName;
 		}
-
-		public bool GetValidFilesName (string files, out string result)
+		private IFileSystemInfo [] GetFocusedFiles ()
 		{
-			result = string.Empty;
-			if (infos?.Count == 0) {
-				return false;
+
+			if (!tableView.HasFocus || !tableView.CanFocus || FileOperationsHandler == null) {
+				return null;
 			}
 
-			var valid = true;
-			IReadOnlyList<string> filesList = new List<string> (files.Split (splitString.ToArray (), StringSplitOptions.None));
-			var filesName = new List<string> ();
-			UnMarkAll ();
+			tableView.EnsureValidSelection ();
 
-			foreach (var file in filesList) {
-				if (!allowsMultipleSelection && filesName.Count > 0) {
-					break;
-				}
-				var idx = infos.IndexOf (x => x.Item1.IndexOf (file, StringComparison.OrdinalIgnoreCase) >= 0);
-				if (idx > -1 && string.Equals (infos [idx].Item1, file, StringComparison.OrdinalIgnoreCase)) {
-					if (canChooseDirectories && !canChooseFiles && !infos [idx].Item2) {
-						valid = false;
-					}
-					if (allowsMultipleSelection && !infos [idx].Item3) {
-						infos [idx] = (infos [idx].Item1, infos [idx].Item2, true);
-					}
-					if (!allowsMultipleSelection) {
-						selected = idx;
-					}
-					filesName.Add (Path.GetFileName (infos [idx].Item1));
-				} else if (idx > -1) {
-					valid = false;
-					filesName.Add (Path.GetFileName (file));
-				}
-			}
-			result = string.Join (splitString, filesName);
-			if (string.IsNullOrEmpty (result)) {
-				valid = false;
+			if (tableView.SelectedRow < 0) {
+				return null;
 			}
-			return valid;
+
+			return tableView.GetAllSelectedCells ()
+				.Select (c => c.Y)
+				.Distinct ()
+				.Select (RowToStats)
+				.Where (s => !s.IsParent)
+				.Select (d => d.FileSystemInfo)
+				.ToArray ();
 		}
 
-		public override bool ProcessKey (KeyEvent keyEvent)
-		{
-			switch (keyEvent.Key) {
-			case Key.CursorUp:
-			case Key.P | Key.CtrlMask:
-				MoveUp ();
-				return true;
 
-			case Key.CursorDown:
-			case Key.N | Key.CtrlMask:
-				MoveDown ();
+
+		/// <inheritdoc/>
+		public override bool ProcessHotKey (KeyEvent keyEvent)
+		{
+			if (this.NavigateIf (keyEvent, Key.CtrlMask | Key.F, this.tbFind)) {
 				return true;
+			}
 
-			case Key.V | Key.CtrlMask:
-			case Key.PageDown:
-				var n = (selected + Frame.Height);
-				if (n > infos.Count)
-					n = infos.Count - 1;
-				if (n != selected) {
-					selected = n;
-					if (infos.Count >= Frame.Height)
-						top = selected;
-					else
-						top = 0;
-					OnSelectionChanged ();
+			ClearFeedback ();
 
-					SetNeedsDisplay ();
-				}
-				return true;
+			if (allowedTypeMenuBar != null &&
+				keyEvent.Key == Key.Tab &&
+				allowedTypeMenuBar.IsMenuOpen) {
+				allowedTypeMenuBar.CloseMenu (false, false, false);
+			}
 
-			case Key.Enter:
-				UnMarkAll ();
-				if (ExecuteSelection ())
-					return false;
-				else
-					return true;
-
-			case Key.PageUp:
-				n = (selected - Frame.Height);
-				if (n < 0)
-					n = 0;
-				if (n != selected) {
-					selected = n;
-					top = selected;
-					OnSelectionChanged ();
-					SetNeedsDisplay ();
-				}
-				return true;
+			return base.ProcessHotKey (keyEvent);
+		}
+		private void RestartSearch ()
+		{
+			if (disposed || State?.Directory == null) {
+				return;
+			}
 
-			case Key.Space:
-			case Key.T | Key.CtrlMask:
-				PerformMultipleSelection ();
-				return true;
+			if (State is SearchState oldSearch) {
+				oldSearch.Cancel ();
+			}
 
-			case Key.Home:
-				MoveFirst ();
-				return true;
+			// user is clearing search terms
+			if (tbFind.Text == null || tbFind.Text.Length == 0) {
 
-			case Key.End:
-				MoveLast ();
-				return true;
+				// Wait for search cancellation (if any) to finish
+				// then push the current dir state
+				lock (onlyOneSearchLock) {
+					PushState (new FileDialogState (State.Directory, this), false);
+				}
+				return;
 			}
-			return base.ProcessKey (keyEvent);
-		}
 
-		void MoveLast ()
-		{
-			selected = infos.Count - 1;
-			top = infos.Count () - 1;
-			OnSelectionChanged ();
-			SetNeedsDisplay ();
+			PushState (new SearchState (State?.Directory, this, tbFind.Text.ToString ()), true);
 		}
 
-		void MoveFirst ()
+		/// <inheritdoc/>
+		protected override void Dispose (bool disposing)
 		{
-			selected = 0;
-			top = 0;
-			OnSelectionChanged ();
-			SetNeedsDisplay ();
+			disposed = true;
+			base.Dispose (disposing);
+
+			CancelSearch ();
 		}
 
-		void MoveDown ()
+		private bool CancelSearch ()
 		{
-			if (selected + 1 < infos.Count) {
-				selected++;
-				if (selected >= top + Frame.Height)
-					top++;
-				OnSelectionChanged ();
-				SetNeedsDisplay ();
+			if (State is SearchState search) {
+				return search.Cancel ();
 			}
+
+			return false;
 		}
 
-		void MoveUp ()
+		private void ClearFeedback ()
 		{
-			if (selected > 0) {
-				selected--;
-				if (selected < top)
-					top = selected;
-				OnSelectionChanged ();
-				SetNeedsDisplay ();
-			}
+			feedback = null;
 		}
 
-		internal bool ExecuteSelection (bool navigateFolder = true)
+		private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent)
 		{
-			if (infos.Count == 0) {
-				return false;
+			if (tableView.Table.Rows.Count == 0) {
+				return;
 			}
-			var isDir = infos [selected].Item2;
 
-			if (isDir) {
-				Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1));
-				DirectoryChanged?.Invoke (Directory);
-				if (canChooseDirectories && !navigateFolder) {
-					return true;
-				}
-			} else {
-				OnSelectionChanged ();
-				if (canChooseFiles) {
-					// Ensures that at least one file is selected.
-					if (FilePaths.Count == 0)
-						PerformMultipleSelection ();
-					// Let the OK handler take it over
-					return true;
-				}
-				// No files allowed, do not let the default handler take it.
-			}
-			return false;
-		}
+			var row = tableView.SelectedRow;
 
-		void PerformMultipleSelection (int? firstSelected = null)
-		{
-			if (allowsMultipleSelection) {
-				int first = Math.Min (firstSelected ?? selected, selected);
-				int last = Math.Max (selected, firstSelected ?? selected);
-				for (int i = first; i <= last; i++) {
-					if ((canChooseFiles && infos [i].Item2 == false) ||
-					    (canChooseDirectories && infos [i].Item2 &&
-					     infos [i].Item1 != "..")) {
-						infos [i] = (infos [i].Item1, infos [i].Item2, !infos [i].Item3);
-					}
-				}
-				OnSelectionChanged ();
-				SetNeedsDisplay ();
+			// There is a multi select going on and not just for the current row
+			if (tableView.GetAllSelectedCells ().Any (c => c.Y != row)) {
+				return;
 			}
-		}
 
-		string [] allowedFileTypes;
-		public string [] AllowedFileTypes {
-			get => allowedFileTypes;
-			set {
-				allowedFileTypes = value;
-				Reload ();
+			int match = collectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyEvent.KeyValue);
+
+			if (match != -1) {
+				tableView.SelectedRow = match;
+				tableView.EnsureValidSelection ();
+				tableView.EnsureSelectedCellIsVisible ();
+				keyEvent.Handled = true;
 			}
 		}
 
-		public string MakePath (string relativePath)
+		private void UpdateCollectionNavigator ()
 		{
-			var dir = Directory.ToString ();
-			return string.IsNullOrEmpty (dir) ? "" : Path.GetFullPath (Path.Combine (dir, relativePath));
-		}
 
-		public IReadOnlyList<string> FilePaths {
-			get {
-				if (allowsMultipleSelection) {
-					var res = new List<string> ();
-					foreach (var item in infos) {
-						if (item.Item3)
-							res.Add (MakePath (item.Item1));
-					}
-					if (res.Count == 0 && infos.Count > 0 && infos [selected].Item1 != "..") {
-						res.Add (MakePath (infos [selected].Item1));
-					}
-					return res;
-				} else {
-					if (infos.Count == 0) {
-						return null;
-					}
-					if (infos [selected].Item2) {
-						if (canChooseDirectories) {
-							var sel = infos [selected].Item1;
-							return sel == ".." ? new List<string> () : new List<string> () { MakePath (infos [selected].Item1) };
-						}
-						return Array.Empty<string> ();
-					} else {
-						if (canChooseFiles) {
-							return new List<string> () { MakePath (infos [selected].Item1) };
-						}
-						return Array.Empty<string> ();
-					}
-				}
-			}
+			var collection = tableView
+				.Table
+				.Rows
+				.Cast<DataRow> ()
+				.Select ((o, idx) => RowToStats (idx))
+				.Select (s => s.FileSystemInfo.Name)
+				.ToArray ();
+
+			collectionNavigator = new CollectionNavigator (collection);
 		}
 
-		///<inheritdoc/>
-		public override bool OnEnter (View view)
-		{
-			Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
+		/// <summary>
+		/// Gets or Sets which <see cref="System.IO.FileSystemInfo"/> type can be selected.
+		/// Defaults to <see cref="OpenMode.Mixed"/> (i.e. <see cref="DirectoryInfo"/> or
+		/// <see cref="FileInfo"/>).
+		/// </summary>
+		public OpenMode OpenMode { get; set; } = OpenMode.Mixed;
 
-			return base.OnEnter (view);
+		/// <summary>
+		/// Gets or Sets the selected path in the dialog. This is the result that should
+		/// be used if <see cref="AllowsMultipleSelection"/> is off and <see cref="Canceled"/>
+		/// is true.
+		/// </summary>
+		public string Path {
+			get => this.tbPath.Text.ToString ();
+			set {
+				this.tbPath.Text = value;
+				this.tbPath.MoveEnd ();
+			}
 		}
-	}
 
-	/// <summary>
-	/// Base class for the <see cref="OpenDialog"/> and the <see cref="SaveDialog"/>
-	/// </summary>
-	public class FileDialog : Dialog {
-		Button prompt, cancel;
-		Label nameFieldLabel, message, nameDirLabel;
-		TextField dirEntry, nameEntry;
-		internal DirListView dirListView;
-		ComboBox cmbAllowedTypes;
+		/// <summary>
+		/// Defines how the dialog matches files/folders when using the search
+		/// box. Provide a custom implementation if you want to tailor how matching
+		/// is performed.
+		/// </summary>
+		public ISearchMatcher SearchMatcher { get; set; } = new DefaultSearchMatcher ();
 
 		/// <summary>
-		/// Initializes a new <see cref="FileDialog"/>.
+		/// Gets or Sets a value indicating whether to allow selecting 
+		/// multiple existing files/directories. Defaults to false.
 		/// </summary>
-		public FileDialog () : this (title: string.Empty, prompt: string.Empty,
-			nameFieldLabel: string.Empty, message: string.Empty)
-		{ }
+		public bool AllowsMultipleSelection {
+			get => this.tableView.MultiSelect;
+			set => this.tableView.MultiSelect = value;
+		}
+
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="FileDialog"/>
+		/// Gets or Sets a collection of file types that the user can/must select. Only applies
+		/// when <see cref="OpenMode"/> is <see cref="OpenMode.File"/> or <see cref="OpenMode.Mixed"/>.
 		/// </summary>
-		/// <param name="title">The title.</param>
-		/// <param name="prompt">The prompt.</param>
-		/// <param name="nameFieldLabel">The name of the file field label..</param>
-		/// <param name="message">The message.</param>
-		/// <param name="allowedTypes">The allowed types.</param>
-		public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message, List<string> allowedTypes = null)
-			: this (title, prompt, ustring.Empty, nameFieldLabel, message, allowedTypes) { }
+		/// <remarks><see cref="AllowedTypeAny"/> adds the option to select any type (*.*). If this
+		/// collection is empty then any type is supported and no Types drop-down is shown.</remarks> 
+		public List<IAllowedType> AllowedTypes { get; set; } = new List<IAllowedType> ();
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="FileDialog"/>
+		/// Gets a value indicating whether the <see cref="FileDialog"/> was closed
+		/// without confirming a selection.
 		/// </summary>
-		/// <param name="title">The title.</param>
-		/// <param name="prompt">The prompt.</param>
-		/// <param name="message">The message.</param>
-		/// <param name="allowedTypes">The allowed types.</param>
-		public FileDialog (ustring title, ustring prompt, ustring message, List<string> allowedTypes)
-			: this (title, prompt, ustring.Empty, message, allowedTypes) { }
+		public bool Canceled { get; private set; } = true;
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="FileDialog"/>
+		/// Gets all files/directories selected or an empty collection
+		/// <see cref="AllowsMultipleSelection"/> is <see langword="false"/> or <see cref="Canceled"/>.
 		/// </summary>
-		/// <param name="title">The title.</param>
-		/// <param name="prompt">The prompt.</param>
-		/// <param name="nameDirLabel">The name of the directory field label.</param>
-		/// <param name="nameFieldLabel">The name of the file field label..</param>
-		/// <param name="message">The message.</param>
-		/// <param name="allowedTypes">The allowed types.</param>
-		public FileDialog (ustring title, ustring prompt, ustring nameDirLabel, ustring nameFieldLabel, ustring message,
-			List<string> allowedTypes = null) : base (title)//, Driver.Cols - 20, Driver.Rows - 5, null)
+		/// <remarks>If selecting only a single file/directory then you should use <see cref="Path"/> instead.</remarks>
+		public IReadOnlyList<string> MultiSelected { get; private set; }
+			= Enumerable.Empty<string> ().ToList ().AsReadOnly ();
+
+
+		/// <inheritdoc/>
+		public override void Redraw (Rect bounds)
 		{
-			this.message = new Label (message) {
-				X = 1,
-				Y = 0,
-			};
-			Add (this.message);
-			var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
+			base.Redraw (bounds);
 
-			this.nameDirLabel = new Label (nameDirLabel.IsEmpty ? $"{Strings.fdDirectory}: " : $"{nameDirLabel}: ") {
-				X = 1,
-				Y = 1 + msgLines,
-				AutoSize = true
-			};
+			if (!string.IsNullOrWhiteSpace (feedback)) {
+				var feedbackWidth = feedback.Sum (c => Rune.ColumnWidth (c));
+				var feedbackPadLeft = ((bounds.Width - feedbackWidth) / 2) - 1;
 
-			dirEntry = new TextField ("") {
-				X = Pos.Right (this.nameDirLabel),
-				Y = 1 + msgLines,
-				Width = Dim.Fill () - 1,
-			};
-			dirEntry.TextChanged += (s, e) => {
-				DirectoryPath = dirEntry.Text;
-				nameEntry.Text = ustring.Empty;
-			};
-			Add (this.nameDirLabel, dirEntry);
+				feedbackPadLeft = Math.Min (bounds.Width, feedbackPadLeft);
+				feedbackPadLeft = Math.Max (0, feedbackPadLeft);
 
-			this.nameFieldLabel = new Label (nameFieldLabel.IsEmpty ? $"{Strings.fdFile}: " : $"{nameFieldLabel}: ") {
-				X = 1,
-				Y = 3 + msgLines,
-				AutoSize = true
-			};
-			nameEntry = new TextField ("") {
-				X = Pos.Left (dirEntry),
-				Y = 3 + msgLines,
-				Width = Dim.Percent (70, true)
-			};
-			Add (this.nameFieldLabel, nameEntry);
-
-			cmbAllowedTypes = new ComboBox () {
-				X = Pos.Right (nameEntry) + 2,
-				Y = Pos.Top (nameEntry),
-				Width = Dim.Fill (1),
-				Height = SetComboBoxHeight (allowedTypes),
-				Text = allowedTypes?.Count > 0 ? allowedTypes [0] : string.Empty,
-				SelectedItem = allowedTypes?.Count > 0 ? 0 : -1,
-				ReadOnly = true,
-				HideDropdownListOnClick = true
-			};
-			cmbAllowedTypes.SetSource (allowedTypes ?? new List<string> ());
-			cmbAllowedTypes.OpenSelectedItem += (s, e) => {
-				dirListView.AllowedFileTypes = cmbAllowedTypes.Text.ToString ().Split (';');
-				dirListView.Reload ();
-			};
-			Add (cmbAllowedTypes);
+				var feedbackPadRight = bounds.Width - (feedbackPadLeft + feedbackWidth + 2);
+				feedbackPadRight = Math.Min (bounds.Width, feedbackPadRight);
+				feedbackPadRight = Math.Max (0, feedbackPadRight);
 
-			dirListView = new DirListView (this) {
-				X = 1,
-				Y = 3 + msgLines + 2,
-				Width = Dim.Fill () - 1,
-				Height = Dim.Fill () - 2,
-			};
-			DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory);
-			Add (dirListView);
-
-			AllowedFileTypes = allowedTypes?.Count > 0 ? allowedTypes?.ToArray () : null;
-			dirListView.DirectoryChanged = (dir) => { nameEntry.Text = ustring.Empty; dirEntry.Text = dir; };
-			dirListView.FileChanged = (file) => nameEntry.Text = file == ".." ? "" : file;
-			dirListView.SelectedChanged = (file) => nameEntry.Text = file.Item1 == ".." ? "" : file.Item1;
-			this.cancel = new Button ("Cancel");
-			this.cancel.Clicked += (s,e) => {
-				Cancel ();
-			};
-			AddButton (cancel);
+				Move (0, Bounds.Height / 2);
 
-			this.prompt = new Button (prompt.IsEmpty ? "Ok" : prompt) {
-				IsDefault = true,
-				Enabled = nameEntry.Text.IsEmpty ? false : true
-			};
-			this.prompt.Clicked += (s,e) => {
-				if (this is OpenDialog) {
-					if (!dirListView.GetValidFilesName (nameEntry.Text.ToString (), out string res)) {
-						nameEntry.Text = res;
-						dirListView.SetNeedsDisplay ();
-						return;
-					}
-					if (!dirListView.canChooseDirectories && !dirListView.ExecuteSelection (false)) {
-						return;
-					}
-				} else if (this is SaveDialog) {
-					var name = nameEntry.Text.ToString ();
-					if (FilePath.IsEmpty || name.Split (',').Length > 1) {
-						return;
-					}
-					var ext = name.EndsWith (cmbAllowedTypes.Text.ToString ())
-						? "" : cmbAllowedTypes.Text.ToString ();
-					FilePath = Path.Combine (FilePath.ToString (), $"{name}{ext}");
-				}
-				canceled = false;
-				Application.RequestStop ();
-			};
-			AddButton (this.prompt);
+				Driver.SetAttribute (new Attribute (Color.Red, this.ColorScheme.Normal.Background));
+				Driver.AddStr (new string (' ', feedbackPadLeft));
+				Driver.AddStr (feedback);
+				Driver.AddStr (new string (' ', feedbackPadRight));
+			}
+		}
 
-			nameEntry.TextChanged += (s,e) => {
-				if (nameEntry.Text.IsEmpty) {
-					this.prompt.Enabled = false;
-				} else {
-					this.prompt.Enabled = true;
-				}
-			};
+		/// <inheritdoc/>
+		public override void OnLoaded ()
+		{
+			base.OnLoaded ();
+			if (loaded) {
+				return;
+			}
+			loaded = true;
+
+			// May have been updated after instance was constructed
+			this.btnOk.Text = Style.OkButtonText;
+			this.btnUp.Text = this.GetUpButtonText();
+			this.btnBack.Text = this.GetBackButtonText();
+			this.btnForward.Text = this.GetForwardButtonText();
+			this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText(false);
+			
+			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
+
+			treeView.AddObjects (Style.TreeRootGetter ());
+
+			// if filtering on file type is configured then create the ComboBox and establish
+			// initial filtering by extension(s)
+			if (this.AllowedTypes.Any ()) {
+
+				this.CurrentFilter = this.AllowedTypes [0];
+
+				// Fiddle factor
+				var width = this.AllowedTypes.Max (a => a.ToString ().Length) + 6;
+
+				allowedTypeMenu = new MenuBarItem ("<placeholder>",
+					allowedTypeMenuItems = AllowedTypes.Select (
+						(a, i) => new MenuItem (a.ToString (), null, () => {
+							AllowedTypeMenuClicked (i);
+						}))
+					.ToArray ());
+
+				allowedTypeMenuBar = new MenuBar (new [] { allowedTypeMenu }) {
+					Width = width,
+					Y = 1,
+					X = Pos.AnchorEnd (width),
+
+					// TODO: Does not work, if this worked then we could tab to it instead
+					// of having to hit F9
+					CanFocus = true,
+					TabStop = true
+				};
+				AllowedTypeMenuClicked (0);
+
+				allowedTypeMenuBar.Enter += (s, e) => {
+					allowedTypeMenuBar.OpenMenu (0);
+				};
+
+				allowedTypeMenuBar.DrawContentComplete += (s, e) => {
+					
+					allowedTypeMenuBar.Move (e.Rect.Width - 1, 0);
+					Driver.AddRune (Driver.DownArrow);
+
+				};
+
+				this.Add (allowedTypeMenuBar);
+			}
 
-			Width = Dim.Percent (80);
-			Height = Dim.Percent (80);
+			// if no path has been provided
+			if (this.tbPath.Text.Length <= 0) {
+				this.tbPath.Text = Environment.CurrentDirectory;
+			}
 
-			// On success, we will set this to false.
-			canceled = true;
+			// to streamline user experience and allow direct typing of paths
+			// with zero navigation we start with focus in the text box and any
+			// default/current path fully selected and ready to be overwritten
+			this.tbPath.FocusFirst ();
+			this.tbPath.SelectAll ();
 
-			KeyPress += (s, e) => {
-				if (e.KeyEvent.Key == Key.Esc) {
-					Cancel ();
-					e.Handled = true;
+			if (ustring.IsNullOrEmpty (Title)) {
+				switch (OpenMode) {
+				case OpenMode.File:
+					this.Title = $"{Strings.fdOpen} {(MustExist ? Strings.fdExisting + " " : "")}{Strings.fdFile}";
+					break;
+				case OpenMode.Directory:
+					this.Title = $"{Strings.fdOpen} {(MustExist ? Strings.fdExisting + " " : "")}{Strings.fdDirectory}";
+					break;
+				case OpenMode.Mixed:
+					this.Title = $"{Strings.fdOpen} {(MustExist ? Strings.fdExisting : "")}";
+					break;
 				}
-			};
-			void Cancel ()
-			{
-				canceled = true;
-				Application.RequestStop ();
 			}
+			this.LayoutSubviews ();
 		}
 
-		private static int SetComboBoxHeight (List<string> allowedTypes)
+		private void AllowedTypeMenuClicked (int idx)
 		{
-			return allowedTypes != null ? Math.Min (allowedTypes.Count + 1, 8) : 8;
+
+			var allow = AllowedTypes [idx];
+			for (int i = 0; i < AllowedTypes.Count; i++) {
+				allowedTypeMenuItems [i].Checked = i == idx;
+			}
+			allowedTypeMenu.Title = allow.ToString ();
+
+			this.CurrentFilter = allow;
+
+			this.tbPath.ClearAllSelection ();
+			this.tbPath.Autocomplete.ClearSuggestions ();
+
+			if (this.State != null) {
+				this.State.RefreshChildren ();
+				this.WriteStateToTableView ();
+			}
 		}
 
-		internal bool canceled;
+		private void SuppressIfBadChar (KeyEventEventArgs k)
+		{
+			// don't let user type bad letters
+			var ch = (char)k.KeyEvent.KeyValue;
 
-		///<inheritdoc/>
-		public override void WillPresent ()
+			if (badChars.Contains (ch)) {
+				k.Handled = true;
+			}
+		}
+
+		private bool TreeView_KeyDown (KeyEvent keyEvent)
 		{
-			base.WillPresent ();
-			dirListView.SetFocus ();
+			if (this.treeView.HasFocus && Separators.Contains ((char)keyEvent.KeyValue)) {
+				this.tbPath.FocusFirst ();
+
+				// let that keystroke go through on the tbPath instead
+				return true;
+			}
+
+			return false;
 		}
 
-		//protected override void Dispose (bool disposing)
-		//{
-		//	message?.Dispose ();
-		//	base.Dispose (disposing);
-		//}
+		private void AcceptIf (KeyEventEventArgs keyEvent, Key isKey)
+		{
+			if (!keyEvent.Handled && keyEvent.KeyEvent.Key == isKey) {
+				keyEvent.Handled = true;
+				this.Accept ();
+			}
+		}
 
-		/// <summary>
-		/// Gets or sets the prompt label for the <see cref="Button"/> displayed to the user
-		/// </summary>
-		/// <value>The prompt.</value>
-		public ustring Prompt {
-			get => prompt.Text;
-			set {
-				prompt.Text = value;
+		private void Accept (IEnumerable<FileSystemInfoStats> toMultiAccept)
+		{
+			if (!this.AllowsMultipleSelection) {
+				return;
 			}
+
+			this.MultiSelected = toMultiAccept.Select (s => s.FileSystemInfo.FullName).ToList ().AsReadOnly ();
+			this.tbPath.Text = this.MultiSelected.Count == 1 ? this.MultiSelected [0] : string.Empty;
+
+			FinishAccept ();
 		}
 
-		/// <summary>
-		/// Gets or sets the name of the directory field label.
-		/// </summary>
-		/// <value>The name of the directory field label.</value>
-		public ustring NameDirLabel {
-			get => nameDirLabel.Text;
-			set {
-				nameDirLabel.Text = $"{value}: ";
+
+		private void Accept (IFileInfo f)
+		{
+			if (!this.IsCompatibleWithOpenMode (f.FullName, out var reason)) {
+				feedback = reason;
+				SetNeedsDisplay ();
+				return;
 			}
+
+			this.tbPath.Text = f.FullName;
+
+			if (AllowsMultipleSelection) {
+				this.MultiSelected = new List<string> { f.FullName }.AsReadOnly ();
+			}
+
+			FinishAccept ();
 		}
 
-		/// <summary>
-		/// Gets or sets the name field label.
-		/// </summary>
-		/// <value>The name field label.</value>
-		public ustring NameFieldLabel {
-			get => nameFieldLabel.Text;
-			set {
-				nameFieldLabel.Text = $"{value}: ";
+		private void Accept ()
+		{
+			if (!this.IsCompatibleWithOpenMode (this.tbPath.Text.ToString (), out string reason)) {
+				if (reason != null) {
+					feedback = reason;
+					SetNeedsDisplay ();
+				}
+				return;
 			}
+
+			FinishAccept ();
 		}
 
-		/// <summary>
-		/// Gets or sets the message displayed to the user, defaults to nothing
-		/// </summary>
-		/// <value>The message.</value>
-		public ustring Message {
-			get => message.Text;
-			set {
-				message.Text = value;
+		private void FinishAccept ()
+		{
+			var e = new FilesSelectedEventArgs (this);
+
+			this.FilesSelected?.Invoke (this, e);
+
+			if (e.Cancel) {
+				return;
+			}
+
+			// if user uses Path selection mode (e.g. Enter in text box)
+			// then also copy to MultiSelected
+			if (AllowsMultipleSelection && (!MultiSelected.Any ())) {
+
+				MultiSelected = string.IsNullOrWhiteSpace (Path) ?
+						Enumerable.Empty<string> ().ToList ().AsReadOnly () :
+						new List<string> () { Path }.AsReadOnly ();
 			}
+
+			this.Canceled = false;
+			Application.RequestStop ();
 		}
 
-		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="FileDialog"/> can create directories.
-		/// </summary>
-		/// <value><c>true</c> if can create directories; otherwise, <c>false</c>.</value>
-		public bool CanCreateDirectories { get; set; }
+		private void NavigateIf (KeyEventEventArgs keyEvent, Key isKey, View to)
+		{
+			if (!keyEvent.Handled) {
 
-		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="FileDialog"/> is extension hidden.
-		/// </summary>
-		/// <value><c>true</c> if is extension hidden; otherwise, <c>false</c>.</value>
-		public bool IsExtensionHidden { get; set; }
+				if (NavigateIf (keyEvent.KeyEvent, isKey, to)) {
+					keyEvent.Handled = true;
+				}
+			}
+		}
 
-		/// <summary>
-		/// Gets or sets the directory path for this panel
-		/// </summary>
-		/// <value>The directory path.</value>
-		public ustring DirectoryPath {
-			get => dirEntry.Text;
-			set {
-				dirEntry.Text = value;
-				dirListView.Directory = value;
+		private bool NavigateIf (KeyEvent keyEvent, Key isKey, View to)
+		{
+			if (keyEvent.Key == isKey) {
+
+				to.FocusFirst ();
+				if (to == tbPath) {
+					tbPath.MoveEnd ();
+				}
+				return true;
 			}
+
+			return false;
 		}
 
-		private string [] allowedFileTypes;
+		private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs<object> e)
+		{
+			if (e.NewValue == null) {
+				return;
+			}
+
+			this.tbPath.Text = FileDialogTreeBuilder.NodeToDirectory (e.NewValue).FullName;
+		}
 
-		/// <summary>
-		/// The array of filename extensions allowed, or null if all file extensions are allowed.
-		/// </summary>
-		/// <value>The allowed file types.</value>
-		public string [] AllowedFileTypes {
-			get => allowedFileTypes;
-			set {
-				allowedFileTypes = value;
-				var selected = cmbAllowedTypes.SelectedItem;
-				cmbAllowedTypes.SetSource (value);
-				cmbAllowedTypes.SelectedItem = selected > -1 ? selected : 0;
-				SetComboBoxHeight (value?.ToList ());
-				dirListView.AllowedFileTypes = value != null
-					? value [cmbAllowedTypes.SelectedItem].Split (';')
-					: null;
+		private void UpdateNavigationVisibility ()
+		{
+			this.btnBack.Visible = this.history.CanBack ();
+			this.btnForward.Visible = this.history.CanForward ();
+			this.btnUp.Visible = this.history.CanUp ();
+		}
+
+		private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEventArgs obj)
+		{
+			if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows.Count == 0) {
+				return;
+			}
+
+			if (this.tableView.MultiSelect && this.tableView.MultiSelectedRegions.Any ()) {
+				return;
+			}
+
+			var stats = this.RowToStats (obj.NewRow);
+
+			if (stats == null) {
+				return;
+			}
+			IFileSystemInfo dest;
+
+			if (stats.IsParent) {
+				dest = State.Directory;
+			} else {
+				dest = stats.FileSystemInfo;
+			}
+
+			try {
+				this.pushingState = true;
+
+				this.tbPath.Text = dest.FullName;
+				this.State.Selected = stats;
+				this.tbPath.Autocomplete.ClearSuggestions ();
+
+			} finally {
+
+				this.pushingState = false;
 			}
 		}
 
-		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="FileDialog"/> allows the file to be saved with a different extension
-		/// </summary>
-		/// <value><c>true</c> if allows other file types; otherwise, <c>false</c>.</value>
-		public bool AllowsOtherFileTypes { get; set; }
 
-		/// <summary>
-		/// The File path that is currently shown on the panel
-		/// </summary>
-		/// <value>The absolute file path for the file path entered.</value>
-		public ustring FilePath {
-			get => dirListView.MakePath (nameEntry.Text.ToString ());
-			set {
-				nameEntry.Text = Path.GetFileName (value.ToString ());
+		private bool TableView_KeyUp (KeyEvent keyEvent)
+		{
+			if (keyEvent.Key == Key.Backspace) {
+				return this.history.Back ();
 			}
+			if (keyEvent.Key == (Key.ShiftMask | Key.Backspace)) {
+				return this.history.Forward ();
+			}
+
+			if (keyEvent.Key == Key.DeleteChar) {
+
+				Delete ();
+				return true;
+			}
+
+			if (keyEvent.Key == (Key.CtrlMask | Key.R)) {
+
+				Rename ();
+				return true;
+			}
+
+			if (keyEvent.Key == (Key.CtrlMask | Key.N)) {
+				New ();
+				return true;
+			}
+
+			return false;
 		}
 
-		/// <summary>
-		/// Check if the dialog was or not canceled.
-		/// </summary>
-		public bool Canceled { get => canceled; }
-	}
 
-	/// <summary>
-	///  The <see cref="SaveDialog"/> provides an interactive dialog box for users to pick a file to 
-	///  save.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	///   To use, create an instance of <see cref="SaveDialog"/>, and pass it to
-	///   <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
-	///   and when this returns, the <see cref="FileName"/>property will contain the selected file name or 
-	///   null if the user canceled. 
-	/// </para>
-	/// </remarks>
-	public class SaveDialog : FileDialog {
-		/// <summary>
-		/// Initializes a new <see cref="SaveDialog"/>.
-		/// </summary>
-		public SaveDialog () : this (title: string.Empty, message: string.Empty) { }
+		private void SetupTableColumns ()
+		{
+			this.dtFiles = new DataTable ();
 
-		/// <summary>
-		/// Initializes a new <see cref="SaveDialog"/>.
-		/// </summary>
-		/// <param name="title">The title.</param>
-		/// <param name="message">The message.</param>
-		/// <param name="allowedTypes">The allowed types.</param>
-		public SaveDialog (ustring title, ustring message, List<string> allowedTypes = null)
-			: base (title, prompt: Strings.fdSave, nameFieldLabel: $"{Strings.fdSaveAs}:", message: message, allowedTypes) { }
+			var nameStyle = this.tableView.Style.GetOrCreateColumnStyle (
+				filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int))
+				);
+			nameStyle.RepresentationGetter = (i) => {
 
-		/// <summary>
-		/// Gets the name of the file the user selected for saving, or null
-		/// if the user canceled the <see cref="SaveDialog"/>.
-		/// </summary>
-		/// <value>The name of the file.</value>
-		public ustring FileName {
-			get {
-				if (canceled)
-					return null;
-				return Path.GetFileName (FilePath.ToString ());
+				var stats = this.State?.Children [(int)i];
+
+				if (stats == null) {
+					return string.Empty;
+				}
+
+				var icon = stats.IsParent ? null : Style.IconGetter?.Invoke (stats.FileSystemInfo);
+
+				if (icon != null) {
+					return icon + stats.Name;
+				}
+				return stats.Name;
+			};
+
+			nameStyle.MinWidth = 50;
+
+			var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)));
+			sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty;
+			nameStyle.MinWidth = 10;
+
+			var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)));
+			dateModifiedStyle.RepresentationGetter = (i) => 
+			{
+				var s = this.State?.Children [(int)i];
+				if(s == null || s.IsParent || s.LastWriteTime == null)
+				{
+					return string.Empty;
+				}
+				return s.LastWriteTime.Value.ToString (Style.DateFormat);
+			};
+
+			dateModifiedStyle.MinWidth = 30;
+
+			var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)));
+			typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty;
+			typeStyle.MinWidth = 6;
+
+			foreach(var colStyle in Style.TableStyle.ColumnStyles) {
+				colStyle.Value.ColorGetter = this.ColorGetter;
 			}
+			
 		}
-	}
 
-	/// <summary>
-	/// The <see cref="OpenDialog"/>provides an interactive dialog box for users to select files or directories.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	///   The open dialog can be used to select files for opening, it can be configured to allow
-	///   multiple items to be selected (based on the AllowsMultipleSelection) variable and
-	///   you can control whether this should allow files or directories to be selected.
-	/// </para>
-	/// <para>
-	///   To use, create an instance of <see cref="OpenDialog"/>, and pass it to
-	///   <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
-	///   and when this returns, the list of files will be available on the <see cref="FilePaths"/> property.
-	/// </para>
-	/// <para>
-	/// To select more than one file, users can use the spacebar, or control-t.
-	/// </para>
-	/// </remarks>
-	public class OpenDialog : FileDialog {
-		OpenMode openMode;
+		private void CellActivate (object sender, CellActivatedEventArgs obj)
+		{
+			var multi = this.MultiRowToStats ();
+			string reason = null;
+			if (multi.Any ()) {
+				if (multi.All (m => this.IsCompatibleWithOpenMode (m.FileSystemInfo.FullName, out reason))) {
+					this.Accept (multi);
+					return;
+				} else {
+					if (reason != null) {
+						feedback = reason;
+						SetNeedsDisplay ();
+					}
 
-		/// <summary>
-		/// Determine which <see cref="System.IO"/> type to open.
-		/// </summary>
-		public enum OpenMode {
-			/// <summary>
-			/// Opens only file or files.
-			/// </summary>
-			File,
-			/// <summary>
-			/// Opens only directory or directories.
-			/// </summary>
-			Directory,
-			/// <summary>
-			/// Opens files and directories.
-			/// </summary>
-			Mixed
+					return;
+				}
+			}
+
+
+			var stats = this.RowToStats (obj.Row);
+
+
+			if (stats.FileSystemInfo is IDirectoryInfo d) {
+				this.PushState (d, true);
+				return;
+			}
+
+			if (stats.FileSystemInfo is IFileInfo f) {
+				this.Accept (f);
+			}
 		}
 
 		/// <summary>
-		/// Initializes a new <see cref="OpenDialog"/>.
+		/// Returns true if there are no <see cref="AllowedTypes"/> or one of them agrees
+		/// that <paramref name="file"/> <see cref="IAllowedType.IsAllowed(string)"/>.
 		/// </summary>
-		public OpenDialog () : this (title: string.Empty, message: string.Empty) { }
+		/// <param name="file"></param>
+		/// <returns></returns>
+		public bool IsCompatibleWithAllowedExtensions (IFileInfo file)
+		{
+			// no restrictions
+			if (!this.AllowedTypes.Any ()) {
+				return true;
+			}
+			return this.MatchesAllowedTypes (file);
+		}
+
+		private bool IsCompatibleWithAllowedExtensions (string path)
+		{
+			// no restrictions
+			if (!this.AllowedTypes.Any ()) {
+				return true;
+			}
+
+			return this.AllowedTypes.Any (t => t.IsAllowed (path));
+		}
 
 		/// <summary>
-		/// Initializes a new <see cref="OpenDialog"/>.
+		/// Returns true if any <see cref="AllowedTypes"/> matches <paramref name="file"/>.
 		/// </summary>
-		/// <param name="title">The title.</param>
-		/// <param name="message">The message.</param>
-		/// <param name="allowedTypes">The allowed types.</param>
-		/// <param name="openMode">The open mode.</param>
-		public OpenDialog (ustring title, ustring message, List<string> allowedTypes = null, OpenMode openMode = OpenMode.File) : base (title,
-			prompt: openMode == OpenMode.File ? Strings.fdOpen : openMode == OpenMode.Directory ? Strings.fdSelectFolder : Strings.fdSelectMixed,
-			nameFieldLabel: Strings.fdOpen, message: message, allowedTypes)
+		/// <param name="file"></param>
+		/// <returns></returns>
+		private bool MatchesAllowedTypes (IFileInfo file)
 		{
-			this.openMode = openMode;
-			switch (openMode) {
-			case OpenMode.File:
-				CanChooseFiles = true;
-				CanChooseDirectories = false;
-				break;
+			return this.AllowedTypes.Any (t => t.IsAllowed (file.FullName));
+		}
+		private bool IsCompatibleWithOpenMode (string s, out string reason)
+		{
+			reason = null;
+			if (string.IsNullOrWhiteSpace (s)) {
+				return false;
+			}
+
+			if (!this.IsCompatibleWithAllowedExtensions (s)) {
+				reason = Style.WrongFileTypeFeedback;
+				return false;
+			}
+
+			switch (this.OpenMode) {
 			case OpenMode.Directory:
-				CanChooseFiles = false;
-				CanChooseDirectories = true;
-				break;
+				if (MustExist && !Directory.Exists (s)) {
+					reason = Style.DirectoryMustExistFeedback;
+					return false;
+				}
+
+				if (File.Exists (s)) {
+					reason = Style.FileAlreadyExistsFeedback;
+					return false;
+				}
+				return true;
+			case OpenMode.File:
+
+				if (MustExist && !File.Exists (s)) {
+					reason = Style.FileMustExistFeedback;
+					return false;
+				}
+				if (Directory.Exists (s)) {
+					reason = Style.DirectoryAlreadyExistsFeedback;
+					return false;
+				}
+				return true;
 			case OpenMode.Mixed:
-				CanChooseFiles = true;
-				CanChooseDirectories = true;
-				AllowsMultipleSelection = true;
-				break;
+				if (MustExist && !File.Exists (s) && !Directory.Exists (s)) {
+					reason = Style.FileOrDirectoryMustExistFeedback;
+					return false;
+				}
+				return true;
+			default: throw new ArgumentOutOfRangeException (nameof (this.OpenMode));
 			}
 		}
 
 		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="Terminal.Gui.OpenDialog"/> can choose files.
+		/// Changes the dialog such that <paramref name="d"/> is being explored.
 		/// </summary>
-		/// <value><c>true</c> if can choose files; otherwise, <c>false</c>.  Defaults to <c>true</c></value>
-		public bool CanChooseFiles {
-			get => dirListView.canChooseFiles;
-			set {
-				dirListView.canChooseFiles = value;
-				dirListView.Reload ();
+		/// <param name="d"></param>
+		/// <param name="addCurrentStateToHistory"></param>
+		/// <param name="setPathText"></param>
+		/// <param name="clearForward"></param>
+		internal void PushState (IDirectoryInfo d, bool addCurrentStateToHistory, bool setPathText = true, bool clearForward = true)
+		{
+			// no change of state
+			if (d == this.State?.Directory) {
+				return;
+			}
+			if (d.FullName == this.State?.Directory.FullName) {
+				return;
+			}
+
+			PushState (new FileDialogState (d, this), addCurrentStateToHistory, setPathText, clearForward);
+		}
+
+		private void RefreshState ()
+		{
+			State.RefreshChildren ();
+			PushState (State, false, false, false);
+		}
+
+		private void PushState (FileDialogState newState, bool addCurrentStateToHistory, bool setPathText = true, bool clearForward = true)
+		{
+			if (State is SearchState search) {
+				search.Cancel ();
+			}
+
+			try {
+				this.pushingState = true;
+
+				// push the old state to history
+				if (addCurrentStateToHistory) {
+					this.history.Push (this.State, clearForward);
+				}
+
+				this.tbPath.Autocomplete.ClearSuggestions ();
+
+				if (setPathText) {
+					this.tbPath.Text = newState.Directory.FullName;
+					this.tbPath.MoveEnd ();
+				}
+
+				this.State = newState;
+				this.tbPath.Autocomplete.GenerateSuggestions (
+					new AutocompleteFilepathContext (tbPath.Text, tbPath.CursorPosition, this.State));
+
+				this.WriteStateToTableView ();
+
+				if (clearForward) {
+					this.history.ClearForward ();
+				}
+
+				this.tableView.RowOffset = 0;
+				this.tableView.SelectedRow = 0;
+
+				this.SetNeedsDisplay ();
+				this.UpdateNavigationVisibility ();
+
+			} finally {
+
+				this.pushingState = false;
+			}
+			ClearFeedback ();
+		}
+
+		private void WriteStateToTableView ()
+		{
+			if (this.State == null) {
+				return;
+			}
+
+			this.dtFiles.Rows.Clear ();
+
+			for (int i = 0; i < this.State.Children.Length; i++) {
+				this.BuildRow (i);
+			}
+
+			this.sorter.ApplySort ();
+			this.tableView.Update ();
+			UpdateCollectionNavigator ();
+		}
+
+		private void BuildRow (int idx)
+		{
+			this.tableView.Table.Rows.Add (idx, idx, idx, idx);
+		}
+
+		private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
+		{
+			var stats = this.RowToStats (args.RowIndex);
+
+			if (!Style.UseColors) {
+				return tableView.ColorScheme;
+			}
+
+			if (stats.IsDir ()) {
+				return Style.ColorSchemeDirectory;
+			}
+			if (stats.IsImage ()) {
+				return Style.ColorSchemeImage;
+			}
+			if (stats.IsExecutable ()) {
+				return Style.ColorSchemeExeOrRecommended;
 			}
+			if (stats.FileSystemInfo is IFileInfo f && this.MatchesAllowedTypes (f)) {
+				return Style.ColorSchemeExeOrRecommended;
+			}
+
+			return Style.ColorSchemeOther;
 		}
 
 		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="OpenDialog"/> can choose directories.
+		/// If <see cref="TableView.MultiSelect"/> is on and multiple rows are selected
+		/// this returns a union of all <see cref="FileSystemInfoStats"/> in the selection.
 		/// </summary>
-		/// <value><c>true</c> if can choose directories; otherwise, <c>false</c> defaults to <c>false</c>.</value>
-		public bool CanChooseDirectories {
-			get => dirListView.canChooseDirectories;
-			set {
-				dirListView.canChooseDirectories = value;
-				dirListView.Reload ();
+		/// <remarks>Returns an empty collection if there are not at least 2 rows in the selection</remarks>
+		/// <returns></returns>
+		private IEnumerable<FileSystemInfoStats> MultiRowToStats ()
+		{
+			var toReturn = new HashSet<FileSystemInfoStats> ();
+
+			if (this.AllowsMultipleSelection && this.tableView.MultiSelectedRegions.Any ()) {
+
+				foreach (var p in this.tableView.GetAllSelectedCells ()) {
+
+					var add = this.State?.Children [(int)this.tableView.Table.Rows [p.Y] [0]];
+					if (add != null) {
+						toReturn.Add (add);
+					}
+				}
+			}
+
+			return toReturn.Count > 1 ? toReturn : Enumerable.Empty<FileSystemInfoStats> ();
+		}
+		private FileSystemInfoStats RowToStats (int rowIndex)
+		{
+			return this.State?.Children [(int)this.tableView.Table.Rows [rowIndex] [0]];
+		}
+		private int? StatsToRow (IFileSystemInfo fileSystemInfo)
+		{
+			// find array index of the current state for the stats
+			var idx = State?.Children.IndexOf ((f) => f.FileSystemInfo.FullName == fileSystemInfo.FullName);
+
+			if (idx != -1 && idx != null) {
+
+				// find the row number in our DataTable where the cell
+				// contains idx
+				var match = tableView.Table.Rows
+					.Cast<DataRow> ()
+					.Select ((r, rIdx) => new { row = r, rowIdx = rIdx })
+					.Where (t => (int)t.row [0] == idx)
+					.ToArray ();
+
+				if (match.Length == 1) {
+					return match [0].rowIdx;
+				}
 			}
+
+			return null;
+		}
+
+
+		private void PathChanged ()
+		{
+			// avoid re-entry
+			if (this.pushingState) {
+				return;
+			}
+
+			var path = this.tbPath.Text?.ToString ();
+
+			if (string.IsNullOrWhiteSpace (path)) {
+				return;
+			}
+
+			var dir = this.StringToDirectoryInfo (path);
+
+			if (dir.Exists) {
+				this.PushState (dir, true, false);
+			} else
+			if (dir.Parent?.Exists ?? false) {
+				this.PushState (dir.Parent, true, false);
+			}
+
+			tbPath.Autocomplete.GenerateSuggestions (new AutocompleteFilepathContext (tbPath.Text, tbPath.CursorPosition, State));
+		}
+
+		private IDirectoryInfo StringToDirectoryInfo (string path)
+		{
+			// if you pass new DirectoryInfo("C:") you get a weird object
+			// where the FullName is in fact the current working directory.
+			// really not what most users would expect
+			if (Regex.IsMatch (path, "^\\w:$")) {
+				return fileSystem.DirectoryInfo.New(path + System.IO.Path.DirectorySeparatorChar);
+			}
+
+			return fileSystem.DirectoryInfo.New(path);
 		}
 
 		/// <summary>
-		/// Gets or sets a value indicating whether this <see cref="OpenDialog"/> allows multiple selection.
+		/// Select <paramref name="toRestore"/> in the table view (if present)
 		/// </summary>
-		/// <value><c>true</c> if allows multiple selection; otherwise, <c>false</c>, defaults to false.</value>
-		public bool AllowsMultipleSelection {
-			get => dirListView.allowsMultipleSelection;
-			set {
-				if (!value && openMode == OpenMode.Mixed) {
+		/// <param name="toRestore"></param>
+		internal void RestoreSelection (IFileSystemInfo toRestore)
+		{
+			var toReselect = StatsToRow (toRestore);
+
+			if (toReselect.HasValue) {
+				tableView.SelectedRow = toReselect.Value;
+				tableView.EnsureSelectedCellIsVisible ();
+			}
+		}
+		private class FileDialogSorter {
+			private readonly FileDialog dlg;
+			private TableView tableView;
+
+			private DataColumn currentSort = null;
+			private bool currentSortIsAsc = true;
+
+			public FileDialogSorter (FileDialog dlg, TableView tableView)
+			{
+				this.dlg = dlg;
+				this.tableView = tableView;
+
+				// if user clicks the mouse in TableView
+				this.tableView.MouseClick += (s, e) => {
+
+					var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
+
+					if (clickedCol != null) {
+						if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+
+							// left click in a header
+							this.SortColumn (clickedCol);
+						} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
+
+							// right click in a header
+							this.ShowHeaderContextMenu (clickedCol, e);
+						}
+					} else {
+						if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
+
+							// right click in rest of table
+							this.ShowCellContextMenu (clickedCell, e);
+						}
+					}
+
+				};
+
+			}
+
+			internal void ApplySort ()
+			{
+				var col = this.currentSort;
+
+				// TODO: Consider preserving selection
+				this.tableView.Table.Rows.Clear ();
+
+				var colName = col == null ? null : StripArrows (col.ColumnName);
+
+				var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0];
+
+				// Do we sort on a column or just use the default sort order?
+				Func<FileSystemInfoStats, object> sortAlgorithm;
+
+				if (colName == null) {
+					sortAlgorithm = (v) => v.GetOrderByDefault ();
+					this.currentSortIsAsc = true;
+				} else {
+					sortAlgorithm = (v) => v.GetOrderByValue (dlg, colName);
+				}
+
+				// This portion is never reordered (aways .. at top then folders)
+				var forcedOrder = stats.Select ((v, i) => new { v, i })
+						.OrderByDescending (f => f.v.IsParent)
+						.ThenBy (f => f.v.IsDir() ? -1:100);
+
+				// This portion is flexible based on the column clicked (e.g. alphabetical)
+				var ordered = 
+					this.currentSortIsAsc ?
+					    forcedOrder.ThenBy (f => sortAlgorithm (f.v)):
+						forcedOrder.ThenByDescending (f => sortAlgorithm (f.v));
+
+				foreach (var o in ordered) {
+					this.dlg.BuildRow (o.i);
+				}
+
+				foreach (DataColumn c in this.tableView.Table.Columns) {
+
+					// remove any lingering sort indicator
+					c.ColumnName = StripArrows (c.ColumnName);
+
+					// add a new one if this the one that is being sorted
+					if (c == col) {
+						c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)";
+					}
+				}
+
+				this.tableView.Update ();
+				dlg.UpdateCollectionNavigator ();
+			}
+
+			private static string StripArrows (string columnName)
+			{
+				return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
+			}
+
+			private void SortColumn (DataColumn clickedCol)
+			{
+				this.GetProposedNewSortOrder (clickedCol, out var isAsc);
+				this.SortColumn (clickedCol, isAsc);
+			}
+
+			internal void SortColumn (DataColumn col, bool isAsc)
+			{
+				// set a sort order
+				this.currentSort = col;
+				this.currentSortIsAsc = isAsc;
+
+				this.ApplySort ();
+			}
+
+			private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
+			{
+				// work out new sort order
+				if (this.currentSort == clickedCol && this.currentSortIsAsc) {
+					isAsc = false;
+					return $"{clickedCol.ColumnName} DESC";
+				} else {
+					isAsc = true;
+					return $"{clickedCol.ColumnName} ASC";
+				}
+			}
+
+			private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e)
+			{
+				var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
+
+				var contextMenu = new ContextMenu (
+					e.MouseEvent.X + 1,
+					e.MouseEvent.Y + 1,
+					new MenuBarItem (new MenuItem []
+					{
+						new MenuItem($"Hide {StripArrows(clickedCol.ColumnName)}", string.Empty, () => this.HideColumn(clickedCol)),
+						new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
+					})
+				);
+
+				contextMenu.Show ();
+			}
+
+			private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
+			{
+				if (clickedCell == null) {
 					return;
 				}
-				dirListView.allowsMultipleSelection = value;
-				dirListView.Reload ();
+
+				var contextMenu = new ContextMenu (
+					e.MouseEvent.X + 1,
+					e.MouseEvent.Y + 1,
+					new MenuBarItem (new MenuItem []
+					{
+						new MenuItem($"New", string.Empty, () => dlg.New()),
+						new MenuItem($"Rename",string.Empty, ()=>  dlg.Rename()),
+						new MenuItem($"Delete",string.Empty, ()=>  dlg.Delete()),
+					})
+				);
+
+				dlg.tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
+
+				contextMenu.Show ();
 			}
-		}
 
+			private void HideColumn (DataColumn clickedCol)
+			{
+				var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
+				style.Visible = false;
+				this.tableView.Update ();
+			}
+		}
 		/// <summary>
-		/// Returns the selected files, or an empty list if nothing has been selected
+		/// State representing a recursive search from <see cref="FileDialogState.Directory"/>
+		/// downwards.
 		/// </summary>
-		/// <value>The file paths.</value>
-		public IReadOnlyList<string> FilePaths {
-			get => dirListView.FilePaths;
+		internal class SearchState : FileDialogState {
+
+			bool cancel = false;
+			bool finished = false;
+
+			// TODO: Add thread safe child adding
+			List<FileSystemInfoStats> found = new List<FileSystemInfoStats> ();
+			object oLockFound = new object ();
+			CancellationTokenSource token = new CancellationTokenSource ();
+
+			public SearchState (IDirectoryInfo dir, FileDialog parent, string searchTerms) : base (dir, parent)
+			{
+				parent.SearchMatcher.Initialize (searchTerms);
+				Children = new FileSystemInfoStats [0];
+				BeginSearch ();
+			}
+
+			private void BeginSearch ()
+			{
+				Task.Run (() => {
+					RecursiveFind (Directory);
+					finished = true;
+				});
+
+				Task.Run (() => {
+					UpdateChildren ();
+				});
+			}
+
+			private void UpdateChildren ()
+			{
+				lock (Parent.onlyOneSearchLock) {
+					while (!cancel && !finished) {
+
+						try {
+							Task.Delay (250).Wait (token.Token);
+						} catch (OperationCanceledException) {
+							cancel = true;
+						}
+
+
+						if (cancel || finished) {
+							break;
+						}
+
+						UpdateChildrenToFound ();
+					}
+
+					if (finished && !cancel) {
+						UpdateChildrenToFound ();
+					}
+
+					Application.MainLoop.Invoke (() => {
+						Parent.spinnerView.Visible = false;
+					});
+				}
+			}
+
+			private void UpdateChildrenToFound ()
+			{
+				lock (oLockFound) {
+					Children = found.ToArray ();
+				}
+
+				Application.MainLoop.Invoke (() => {
+					Parent.tbPath.Autocomplete.GenerateSuggestions (
+						new AutocompleteFilepathContext (Parent.tbPath.Text, Parent.tbPath.CursorPosition, this)
+						);
+					Parent.WriteStateToTableView ();
+
+					Parent.spinnerView.Visible = true;
+					Parent.spinnerView.SetNeedsDisplay ();
+				});
+			}
+
+			private void RecursiveFind (IDirectoryInfo directory)
+			{
+				foreach (var f in GetChildren (directory)) {
+
+					if (cancel) {
+						return;
+					}
+
+					if (f.IsParent) {
+						continue;
+					}
+
+					lock (oLockFound) {
+						if (found.Count >= FileDialog.MaxSearchResults) {
+							finished = true;
+							return;
+						}
+					}
+
+					if (Parent.SearchMatcher.IsMatch (f.FileSystemInfo)) {
+						lock (oLockFound) {
+							found.Add (f);
+						}
+					}
+
+					if (f.FileSystemInfo is IDirectoryInfo sub) {
+						RecursiveFind (sub);
+					}
+				}
+			}
+
+			internal override void RefreshChildren ()
+			{
+			}
+
+			/// <summary>
+			/// Cancels the current search (if any).  Returns true if a search
+			/// was running and cancellation was successfully set.
+			/// </summary>
+			/// <returns></returns>
+			internal bool Cancel ()
+			{
+				var alreadyCancelled = token.IsCancellationRequested || cancel; 
+
+				cancel = true;
+				token.Cancel ();
+
+				return !alreadyCancelled;
+			}
 		}
 	}
-}
+}

+ 90 - 0
Terminal.Gui/Views/OpenDialog.cs

@@ -0,0 +1,90 @@
+// 
+// FileDialog.cs: File system dialogs for open and save
+//
+// TODO:
+//   * Add directory selector
+//   * Implement subclasses
+//   * Figure out why message text does not show
+//   * Remove the extra space when message does not show
+//   * Use a line separator to show the file listing, so we can use same colors as the rest
+//   * DirListView: Add mouse support
+
+using System;
+using System.Collections.Generic;
+using NStack;
+using System.IO;
+using System.Linq;
+using Terminal.Gui.Resources;
+using System.Collections.ObjectModel;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Determine which <see cref="System.IO"/> type to open.
+	/// </summary>
+	public enum OpenMode {
+		/// <summary>
+		/// Opens only file or files.
+		/// </summary>
+		File,
+		/// <summary>
+		/// Opens only directory or directories.
+		/// </summary>
+		Directory,
+		/// <summary>
+		/// Opens files and directories.
+		/// </summary>
+		Mixed
+	}
+
+	/// <summary>
+	/// The <see cref="OpenDialog"/>provides an interactive dialog box for users to select files or directories.
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	///   The open dialog can be used to select files for opening, it can be configured to allow
+	///   multiple items to be selected (based on the AllowsMultipleSelection) variable and
+	///   you can control whether this should allow files or directories to be selected.
+	/// </para>
+	/// <para>
+	///   To use, create an instance of <see cref="OpenDialog"/>, and pass it to
+	///   <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
+	///   and when this returns, the list of files will be available on the <see cref="FilePaths"/> property.
+	/// </para>
+	/// <para>
+	/// To select more than one file, users can use the spacebar, or control-t.
+	/// </para>
+	/// </remarks>
+	public class OpenDialog : FileDialog {		
+
+		/// <summary>
+		/// Initializes a new <see cref="OpenDialog"/>.
+		/// </summary>
+		public OpenDialog () : this (title: string.Empty) { }
+
+		/// <summary>
+		/// Initializes a new <see cref="OpenDialog"/>.
+		/// </summary>
+		/// <param name="title">The title.</param>
+		/// <param name="allowedTypes">The allowed types.</param>
+		/// <param name="openMode">The open mode.</param>
+		public OpenDialog (ustring title, List<IAllowedType> allowedTypes = null, OpenMode openMode = OpenMode.File)
+		{
+			this.OpenMode = openMode;
+			Title = title;
+			Style.OkButtonText = openMode == OpenMode.File ? Strings.fdOpen : openMode == OpenMode.Directory ? Strings.fdSelectFolder : Strings.fdSelectMixed;
+			
+			if (allowedTypes != null) {
+				AllowedTypes = allowedTypes;
+			}
+		}
+		/// <summary>
+		/// Returns the selected files, or an empty list if nothing has been selected
+		/// </summary>
+		/// <value>The file paths.</value>
+		public IReadOnlyList<string> FilePaths {
+			get => Canceled ? Enumerable.Empty<string> ().ToList().AsReadOnly()
+				: AllowsMultipleSelection ? base.MultiSelected : new ReadOnlyCollection<string>(new [] { Path });
+		}
+	}
+}

+ 67 - 0
Terminal.Gui/Views/SaveDialog.cs

@@ -0,0 +1,67 @@
+// 
+// FileDialog.cs: File system dialogs for open and save
+//
+// TODO:
+//   * Add directory selector
+//   * Implement subclasses
+//   * Figure out why message text does not show
+//   * Remove the extra space when message does not show
+//   * Use a line separator to show the file listing, so we can use same colors as the rest
+//   * DirListView: Add mouse support
+
+using System;
+using System.Collections.Generic;
+using NStack;
+using Terminal.Gui.Resources;
+
+namespace Terminal.Gui {
+	/// <summary>
+	///  The <see cref="SaveDialog"/> provides an interactive dialog box for users to pick a file to 
+	///  save.
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	///   To use, create an instance of <see cref="SaveDialog"/>, and pass it to
+	///   <see cref="Application.Run(Func{Exception, bool})"/>. This will run the dialog modally,
+	///   and when this returns, the <see cref="FileName"/>property will contain the selected file name or 
+	///   null if the user canceled. 
+	/// </para>
+	/// </remarks>
+	public class SaveDialog : FileDialog {
+		/// <summary>
+		/// Initializes a new <see cref="SaveDialog"/>.
+		/// </summary>
+		public SaveDialog () : this (title: string.Empty) { }
+
+		/// <summary>
+		/// Initializes a new <see cref="SaveDialog"/>.
+		/// </summary>
+		/// <param name="title">The title.</param>
+		/// <param name="allowedTypes">The allowed types.</param>
+		public SaveDialog (ustring title, List<IAllowedType> allowedTypes = null)
+		{
+			//: base (title, prompt: Strings.fdSave, nameFieldLabel: $"{Strings.fdSaveAs}:", message: message, allowedTypes) { }
+			Title = title;
+			Style.OkButtonText = Strings.fdSave;
+
+			if(allowedTypes != null) {
+				AllowedTypes = allowedTypes;
+			}
+		}
+
+
+		/// <summary>
+		/// Gets the name of the file the user selected for saving, or null
+		/// if the user canceled the <see cref="SaveDialog"/>.
+		/// </summary>
+		/// <value>The name of the file.</value>
+		public ustring FileName {
+			get {
+				if (Canceled)
+					return null;
+
+				return Path;
+			}
+		}
+	}
+}

+ 1 - 1
Terminal.Gui/Views/TableView/TableView.cs

@@ -1,4 +1,4 @@
-using NStack;
+using NStack;
 using System;
 using System.Collections.Generic;
 using System.Data;

+ 25 - 1
Terminal.Gui/Views/TextField.cs

@@ -516,6 +516,7 @@ namespace Terminal.Gui {
 
 			Driver.AddStr (render);
 		}
+    
 		private void GenerateSuggestions ()
 		{
 			var currentLine = Text.ToRuneList ();
@@ -786,7 +787,10 @@ namespace Terminal.Gui {
 			Adjust ();
 		}
 
-		void MoveEnd ()
+		/// <summary>
+		/// Moves cursor to the end of the typed text.
+		/// </summary>
+		public void MoveEnd ()
 		{
 			ClearAllSelection ();
 			point = text.Count;
@@ -1355,6 +1359,26 @@ namespace Terminal.Gui {
 		{
 			historyText.Clear (Text);
 		}
+
+		/// <summary>
+		/// Returns <see langword="true"/> if the current cursor position is
+		/// at the end of the <see cref="Text"/>. This includes when it is empty.
+		/// </summary>
+		/// <returns></returns>
+		internal bool CursorIsAtEnd ()
+		{
+			return CursorPosition == Text.Length;
+		}
+
+		/// <summary>
+		/// Returns <see langword="true"/> if the current cursor position is
+		/// at the start of the <see cref="TextField"/>.
+		/// </summary>
+		/// <returns></returns>
+		internal bool CursorIsAtStart()
+		{
+			return CursorPosition <= 0;
+		}
 	}
 	/// <summary>
 	/// Renders an overlay on another view at a given point that allows selecting

+ 1 - 1
Terminal.Gui/Views/TimeField.cs

@@ -272,7 +272,7 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		bool MoveEnd ()
+		new bool MoveEnd ()
 		{
 			CursorPosition = fieldLen;
 			return true;

+ 6 - 2
Terminal.Gui/Views/TreeView/TreeView.cs

@@ -306,6 +306,10 @@ namespace Terminal.Gui {
 		{
 			Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
 
+			if (SelectedObject == null && Objects.Any ()) {
+				SelectedObject = Objects.First ();
+			}
+
 			return base.OnEnter (view);
 		}
 
@@ -545,7 +549,7 @@ namespace Terminal.Gui {
 			}
 
 			cachedLineMap = new ReadOnlyCollection<Branch<T>> (toReturn);
-			
+
 			// Update the collection used for search-typing
 			KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
 			return cachedLineMap;
@@ -712,7 +716,7 @@ namespace Terminal.Gui {
 		/// <param name="e"></param>
 		protected virtual void OnObjectActivated (ObjectActivatedEventArgs<T> e)
 		{
-			ObjectActivated?.Invoke (this,e);
+			ObjectActivated?.Invoke (this, e);
 		}
 
 		/// <summary>

+ 3 - 0
UICatalog/Resources/config.json

@@ -3,6 +3,9 @@
   "Application.QuitKey": {
     "Key": "Esc"
   },
+  "FileDialog.MaxSearchResults": 10000,
+  "FileDialogStyle.DefaultUseColors": false,
+  "FileDialogStyle.DefaultUseUnicodeCharacters": false,
   "AppSettings": {
     "UICatalog.StatusBar": true,
     "ConfigurationEditor.EditorColorScheme": {

+ 8 - 5
UICatalog/Scenarios/CsvEditor.cs

@@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
 using NStack;
 using Terminal.Gui;
 using CsvHelper;
+using System.Collections.Generic;
 
 namespace UICatalog.Scenarios {
 
@@ -390,14 +391,15 @@ namespace UICatalog.Scenarios {
 
 		private void Open ()
 		{
-			var ofd = new FileDialog ("Select File", "Open", "File", "Select a CSV file to open (does not support newlines, escaping etc)") {
-				AllowedFileTypes = new string [] { ".csv" }
+			var ofd = new FileDialog () {
+				AllowedTypes = new List<IAllowedType> { new AllowedType("Comma Separated Values", ".csv") }
 			};
+			ofd.Style.OkButtonText = "Open";
 
 			Application.Run (ofd);
 
-			if (!ofd.Canceled && !string.IsNullOrWhiteSpace (ofd.FilePath?.ToString ())) {
-				Open (ofd.FilePath.ToString ());
+			if (!ofd.Canceled && !string.IsNullOrWhiteSpace (ofd.Path?.ToString ())) {
+				Open (ofd.Path.ToString ());
 			}
 		}
 
@@ -407,9 +409,10 @@ namespace UICatalog.Scenarios {
 			int lineNumber = 0;
 			currentFile = null;
 
-			using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture);
 
 			try {
+				using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture);
+
 				var dt = new DataTable ();
 
 				reader.Read ();

+ 15 - 8
UICatalog/Scenarios/Editor.cs

@@ -367,8 +367,11 @@ namespace UICatalog.Scenarios {
 			if (!CanCloseFile ()) {
 				return;
 			}
-			var aTypes = new List<string> () { ".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".json", ".*" };
-			var d = new OpenDialog ("Open", "Choose the path where to open the file.", aTypes) { AllowsMultipleSelection = false };
+			var aTypes = new List<IAllowedType> () {
+				new AllowedType("Text",".txt;.bin;.xml;.json", ".txt", ".bin", ".xml", ".json"),
+				new AllowedTypeAny()
+			};
+			var d = new OpenDialog ("Open", aTypes) { AllowsMultipleSelection = false };
 			Application.Run (d);
 
 			if (!d.Canceled && d.FilePaths.Count > 0) {
@@ -390,22 +393,26 @@ namespace UICatalog.Scenarios {
 
 		private bool SaveAs ()
 		{
-			var aTypes = new List<string> () { ".txt", ".bin", ".xml", ".*" };
-			var sd = new SaveDialog ("Save file", "Choose the path where to save the file.", aTypes);
-			sd.FilePath = System.IO.Path.Combine (sd.FilePath.ToString (), Win.Title.ToString ());
+			var aTypes = new List<IAllowedType> () {
+				new AllowedType("Text Files", ".txt", ".bin", ".xml"),
+				new AllowedTypeAny()
+			};
+			var sd = new SaveDialog ("Save file", aTypes);
+
+			sd.Path = System.IO.Path.Combine (sd.FileName.ToString (), Win.Title.ToString ());
 			Application.Run (sd);
 
 			if (!sd.Canceled) {
-				if (System.IO.File.Exists (sd.FilePath.ToString ())) {
+				if (System.IO.File.Exists (sd.Path.ToString ())) {
 					if (MessageBox.Query ("Save File",
 						"File already exists. Overwrite any way?", "No", "Ok") == 1) {
-						return SaveFile (sd.FileName.ToString (), sd.FilePath.ToString ());
+						return SaveFile (sd.FileName.ToString (), sd.Path.ToString ());
 					} else {
 						_saved = false;
 						return _saved;
 					}
 				} else {
-					return SaveFile (sd.FileName.ToString (), sd.FilePath.ToString ());
+					return SaveFile (sd.FileName.ToString (), sd.Path.ToString ());
 				}
 			} else {
 				_saved = false;

+ 214 - 0
UICatalog/Scenarios/FileDialogExamples.cs

@@ -0,0 +1,214 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Terminal.Gui;
+using static Terminal.Gui.OpenDialog;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "FileDialog", Description: "Demonstrates how to the FileDialog class")]
+	[ScenarioCategory ("Dialogs")]
+	[ScenarioCategory ("Files and IO")]
+	public class FileDialogExamples : Scenario {
+		private CheckBox cbMustExist;
+		private CheckBox cbUnicode;
+		private CheckBox cbUseColors;
+		private CheckBox cbCaseSensitive;
+		private CheckBox cbAllowMultipleSelection;
+		private CheckBox cbShowTreeBranchLines;
+		private CheckBox cbAlwaysTableShowHeaders;
+		private CheckBox cbDrivesOnlyInTree;
+
+		private RadioGroup rgCaption;
+		private RadioGroup rgOpenMode;
+		private RadioGroup rgAllowedTypes;
+
+		public override void Setup ()
+		{
+			var y = 0;
+			var x = 1;
+
+			cbMustExist = new CheckBox ("Must Exist") { Checked = true, Y = y++, X = x };
+			Win.Add (cbMustExist);
+
+
+			cbUnicode = new CheckBox ("UseUnicode") { Checked = FileDialogStyle.DefaultUseUnicodeCharacters, Y = y++, X = x };
+			Win.Add (cbUnicode);
+
+			cbUseColors = new CheckBox ("Use Colors") { Checked = FileDialogStyle.DefaultUseColors, Y = y++, X = x };
+			Win.Add (cbUseColors);
+
+			cbCaseSensitive = new CheckBox ("Case Sensitive Search") { Checked = false, Y = y++, X = x };
+			Win.Add (cbCaseSensitive);
+
+			cbAllowMultipleSelection = new CheckBox ("Multiple") { Checked = false, Y = y++, X = x };
+			Win.Add (cbAllowMultipleSelection);
+
+			cbShowTreeBranchLines = new CheckBox ("Tree Branch Lines") { Checked = true, Y = y++, X = x };
+			Win.Add (cbShowTreeBranchLines);
+
+			cbAlwaysTableShowHeaders = new CheckBox ("Always Show Headers") { Checked = true, Y = y++, X = x };
+			Win.Add (cbAlwaysTableShowHeaders);
+
+			cbDrivesOnlyInTree = new CheckBox ("Only Show Drives") { Checked = false, Y = y++, X = x };
+			Win.Add (cbDrivesOnlyInTree);
+
+			y = 0;
+			x = 24;
+
+			Win.Add (new LineView (Orientation.Vertical) {
+				X = x++,
+				Y = 1,
+				Height = 4
+			});
+			Win.Add (new Label ("Caption") { X = x++, Y = y++ });
+
+			rgCaption = new RadioGroup { X = x, Y = y };
+			rgCaption.RadioLabels = new NStack.ustring [] { "Ok", "Open", "Save" };
+			Win.Add (rgCaption);
+
+			y = 0;
+			x = 37;
+
+			Win.Add (new LineView (Orientation.Vertical) {
+				X = x++,
+				Y = 1,
+				Height = 4
+			});
+			Win.Add (new Label ("OpenMode") { X = x++, Y = y++ });
+
+			rgOpenMode = new RadioGroup { X = x, Y = y };
+			rgOpenMode.RadioLabels = new NStack.ustring [] { "File", "Directory", "Mixed" };
+			Win.Add (rgOpenMode);
+
+			y = 5;
+			x = 24;
+
+			Win.Add (new LineView (Orientation.Vertical) {
+				X = x++,
+				Y = y + 1,
+				Height = 4
+			});
+			Win.Add (new Label ("Allowed") { X = x++, Y = y++ });
+
+			rgAllowedTypes = new RadioGroup { X = x, Y = y };
+			rgAllowedTypes.RadioLabels = new NStack.ustring [] { "Any", "Csv (Recommended)", "Csv (Strict)" };
+			Win.Add (rgAllowedTypes);
+
+			var btn = new Button ($"Run Dialog") {
+				X = 1,
+				Y = 9
+			};
+
+			SetupHandler (btn);
+			Win.Add (btn);
+		}
+
+		private void SetupHandler (Button btn)
+		{
+			btn.Clicked += (s,e) => {
+				try
+				{
+					CreateDialog();
+				}	
+				catch(Exception ex)
+				{
+					MessageBox.ErrorQuery("Error",ex.ToString(),"Ok");
+
+				}
+			};
+		}
+
+		private void CreateDialog ()
+		{
+			
+				var fd = new FileDialog () {
+					OpenMode = Enum.Parse<OpenMode> (
+						rgOpenMode.RadioLabels [rgOpenMode.SelectedItem].ToString ()),
+					MustExist = cbMustExist.Checked ?? false,
+					AllowsMultipleSelection = cbAllowMultipleSelection.Checked ?? false,
+				};
+
+				fd.Style.OkButtonText = rgCaption.RadioLabels [rgCaption.SelectedItem].ToString ();
+
+				// If Save style dialog then give them an overwrite prompt
+				if(rgCaption.SelectedItem == 2) {
+					fd.FilesSelected += ConfirmOverwrite;
+				}
+
+				fd.Style.UseUnicodeCharacters = cbUnicode.Checked ?? false;
+
+				if (cbCaseSensitive.Checked ?? false) {
+
+					fd.SearchMatcher = new CaseSensitiveSearchMatcher ();
+				}
+
+				fd.Style.UseColors = cbUseColors.Checked ?? false;
+
+				fd.Style.TreeStyle.ShowBranchLines = cbShowTreeBranchLines.Checked ?? false;
+				fd.Style.TableStyle.AlwaysShowHeaders = cbAlwaysTableShowHeaders.Checked ?? false;
+
+				if (cbDrivesOnlyInTree.Checked ?? false) {
+					fd.Style.TreeRootGetter = () => {
+						return System.Environment.GetLogicalDrives ()
+						.Select (d => new FileDialogRootTreeNode (d, new DirectoryInfo (d)));
+					};
+				}
+
+				if (rgAllowedTypes.SelectedItem > 0) {
+					fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv"));
+
+					if (rgAllowedTypes.SelectedItem == 1) {
+						fd.AllowedTypes.Insert (1, new AllowedTypeAny ());
+					}
+
+				}
+
+				Application.Run (fd);
+
+				if (fd.Canceled) {
+					MessageBox.Query (
+						"Canceled",
+						"You canceled navigation and did not pick anything",
+					"Ok");
+				} else if (cbAllowMultipleSelection.Checked ?? false) {
+					MessageBox.Query (
+						"Chosen!",
+						"You chose:" + Environment.NewLine +
+						string.Join (Environment.NewLine, fd.MultiSelected.Select (m => m)),
+						"Ok");
+				} else {
+					MessageBox.Query (
+						"Chosen!",
+						"You chose:" + Environment.NewLine + fd.Path,
+						"Ok");
+				}
+		}
+
+		private void ConfirmOverwrite (object sender, FilesSelectedEventArgs e)
+		{
+			if (!string.IsNullOrWhiteSpace (e.Dialog.Path)) {
+				if(File.Exists(e.Dialog.Path)) {
+					int result = MessageBox.Query ("Overwrite?", "File already exists", "Yes", "No");
+					e.Cancel = result == 1;
+				}
+			}
+		}
+
+		private class CaseSensitiveSearchMatcher : ISearchMatcher {
+			private string terms;
+
+			public void Initialize (string terms)
+			{
+				this.terms = terms;
+			}
+
+			public bool IsMatch (IFileSystemInfo f)
+			{
+				return f.Name.Contains (terms, StringComparison.CurrentCulture);
+			}
+		}
+	}
+}

+ 1 - 1
UICatalog/Scenarios/HexEditor.cs

@@ -124,7 +124,7 @@ namespace UICatalog.Scenarios {
 
 		private void Open ()
 		{
-			var d = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = false };
+			var d = new OpenDialog ("Open") { AllowsMultipleSelection = false };
 			Application.Run (d);
 
 			if (!d.Canceled) {

+ 3 - 3
UICatalog/Scenarios/Notepad.cs

@@ -253,7 +253,7 @@ namespace UICatalog.Scenarios {
 
 		private void Open ()
 		{
-			var open = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = true };
+			var open = new OpenDialog ("Open") { AllowsMultipleSelection = true };
 
 			Application.Run (open);
 
@@ -312,7 +312,7 @@ namespace UICatalog.Scenarios {
 			var fd = new SaveDialog ();
 			Application.Run (fd);
 
-			if (string.IsNullOrWhiteSpace (fd.FilePath?.ToString ())) {
+			if (string.IsNullOrWhiteSpace (fd.Path?.ToString ())) {
 				return false;
 			}
 			
@@ -320,7 +320,7 @@ namespace UICatalog.Scenarios {
 				return false;
 			}
 
-			tab.File = new FileInfo (fd.FilePath.ToString ());
+			tab.File = new FileInfo (fd.Path.ToString ());
 			tab.Text = fd.FileName.ToString ();
 			tab.Save ();
 

+ 317 - 0
UnitTests/FileServices/FileDialogTests.cs

@@ -0,0 +1,317 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions.TestingHelpers;
+using System.Linq;
+using Terminal.Gui;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.FileServicesTests {
+	public class FileDialogTests {
+
+		readonly ITestOutputHelper output;
+
+		public FileDialogTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void OnLoad_TextBoxIsFocused ()
+		{
+			var dlg = GetInitializedFileDialog ();
+
+			var tf = dlg.Subviews.FirstOrDefault (t => t.HasFocus);
+			Assert.NotNull (tf);
+			Assert.IsType<TextField> (tf);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void DirectTyping_Allowed ()
+		{
+			var dlg = GetInitializedFileDialog ();
+			var tf = dlg.Subviews.OfType<TextField> ().First (t => t.HasFocus);
+			tf.ClearAllSelection ();
+			tf.CursorPosition = tf.Text.Length;
+			Assert.True (tf.HasFocus);
+
+			SendSlash ();
+
+			Assert.Equal (
+				new DirectoryInfo (Environment.CurrentDirectory + Path.DirectorySeparatorChar).FullName,
+				new DirectoryInfo (dlg.Path + Path.DirectorySeparatorChar).FullName
+				);
+
+			// continue typing the rest of the path
+			Send ("bob");
+			Send ('.', ConsoleKey.OemPeriod, false);
+			Send ("csv");
+
+			Assert.True (dlg.Canceled);
+
+			Send ('\n', ConsoleKey.Enter, false);
+			Assert.False (dlg.Canceled);
+			Assert.Equal ("bob.csv", Path.GetFileName (dlg.Path));
+		}
+
+		private void SendSlash ()
+		{
+			if (Path.DirectorySeparatorChar == '/') {
+				Send ('/', ConsoleKey.Separator, false);
+			} else {
+				Send ('\\', ConsoleKey.Separator, false);
+			}
+		}
+
+		[Fact, AutoInitShutdown]
+		public void DirectTyping_AutoComplete ()
+		{
+			var dlg = GetInitializedFileDialog ();
+			var openIn = Path.Combine (Environment.CurrentDirectory, "zz");
+
+			Directory.CreateDirectory (openIn);
+
+			var expectedDest = Path.Combine (openIn, "xx");
+			Directory.CreateDirectory (expectedDest);
+
+			dlg.Path = openIn + Path.DirectorySeparatorChar;
+
+			Send ("x");
+
+			// nothing selected yet
+			Assert.True (dlg.Canceled);
+			Assert.Equal ("x", Path.GetFileName (dlg.Path));
+
+			// complete auto typing
+			Send ('\t', ConsoleKey.Tab, false);
+
+			// but do not close dialog
+			Assert.True (dlg.Canceled);
+			Assert.EndsWith ("xx" + Path.DirectorySeparatorChar, dlg.Path);
+
+			// press enter again to confirm the dialog
+			Send ('\n', ConsoleKey.Enter, false);
+			Assert.False (dlg.Canceled);
+			Assert.EndsWith ("xx" + Path.DirectorySeparatorChar, dlg.Path);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void DoNotConfirmSelectionWhenFindFocused ()
+		{
+			var dlg = GetInitializedFileDialog ();
+			var openIn = Path.Combine (Environment.CurrentDirectory, "zz");
+			Directory.CreateDirectory (openIn);
+			dlg.Path = openIn + Path.DirectorySeparatorChar;
+
+			Send ('f', ConsoleKey.F, false, false, true);
+
+			Assert.IsType<TextField> (dlg.MostFocused);
+			var tf = (TextField)dlg.MostFocused;
+			Assert.Equal ("Enter Search", tf.Caption);
+
+			// Dialog has not yet been confirmed with a choice
+			Assert.True (dlg.Canceled);
+
+			//pressing enter while search focused should not confirm path
+			Send ('\n', ConsoleKey.Enter, false);
+
+			Assert.True (dlg.Canceled);
+
+			// tabbing out of search 
+			Send ('\t', ConsoleKey.Tab, false);
+
+			//should allow enter to confirm path
+			Send ('\n', ConsoleKey.Enter, false);
+
+			// Dialog has not yet been confirmed with a choice
+			Assert.False (dlg.Canceled);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestDirectoryContents_Linux ()
+		{
+			if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform (System.Runtime.InteropServices.OSPlatform.Windows)) {
+				// Cannot run test except on linux :( 
+				// See: https://github.com/TestableIO/System.IO.Abstractions/issues/800
+				return;
+			}
+
+			// Arrange
+			var fileSystem = new MockFileSystem (new Dictionary<string, MockFileData> (), "/");
+			fileSystem.MockTime (() => new DateTime (2010, 01, 01, 11, 12, 43));
+
+			fileSystem.AddFile (@"/myfile.txt", new MockFileData ("Testing is meh.") { LastWriteTime = new DateTime (2001, 01, 01, 11, 12, 11) });
+			fileSystem.AddFile (@"/demo/jQuery.js", new MockFileData ("some js") { LastWriteTime = new DateTime (2001, 01, 01, 11, 44, 42) });
+			fileSystem.AddFile (@"/demo/image.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }) { LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10) });
+
+			var m = (MockDirectoryInfo)fileSystem.DirectoryInfo.New (@"/demo/subfolder");
+			m.Create ();
+			m.LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10);
+
+			fileSystem.AddFile (@"/demo/subfolder/image2.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }) { LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10) });
+
+			var fd = new FileDialog (fileSystem) {
+				Height = 15
+			};
+			fd.Path = @"/demo/";
+			Begin (fd);
+			fd.Title = string.Empty;
+
+			fd.Redraw (fd.Bounds);
+
+			fd.Style.DateFormat = "yyyy-MM-dd hh:mm:ss";
+
+			string expected =
+			@"
+ ┌──────────────────────────────────────────────────────────────────┐
+ │/demo/                                                            │
+ │[▲]                                                               │
+ │┌────────────┬──────────┬──────────────────────────────┬─────────┐│
+ ││Filename (▲)│Size      │Modified                      │Type     ││
+ │├────────────┼──────────┼──────────────────────────────┼─────────┤│
+ ││..          │          │                              │dir      ││
+ ││\subfolder  │          │2002-01-01T22:42:10           │dir      ││
+ ││image.gif   │4.00 bytes│2002-01-01T22:42:10           │.gif     ││
+ ││jQuery.js   │7.00 bytes│2001-01-01T11:44:42           │.js      ││
+ │                                                                  │
+ │                                                                  │
+ │                                                                  │
+ │[ ►► ] Enter Search                            [ Cancel ] [ Ok ]  │
+ └──────────────────────────────────────────────────────────────────┘
+";
+			TestHelpers.AssertDriverContentsAre (expected, output, true);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestDirectoryContents_Windows ()
+		{
+			if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform (System.Runtime.InteropServices.OSPlatform.Windows)) {
+				// Can only run this test on windows :( 
+				// See: https://github.com/TestableIO/System.IO.Abstractions/issues/800
+				return;
+			}
+			// Arrange
+			var fileSystem = new MockFileSystem (new Dictionary<string, MockFileData> (), @"c:\");
+			fileSystem.MockTime (() => new DateTime (2010, 01, 01, 11, 12, 43));
+
+			fileSystem.AddFile (@"c:\myfile.txt", new MockFileData ("Testing is meh.") { LastWriteTime = new DateTime (2001, 01, 01, 11, 12, 11) });
+			fileSystem.AddFile (@"c:\demo\jQuery.js", new MockFileData ("some js") { LastWriteTime = new DateTime (2001, 01, 01, 11, 44, 42) });
+			fileSystem.AddFile (@"c:\demo\image.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }) { LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10) });
+
+			var m = (MockDirectoryInfo)fileSystem.DirectoryInfo.New (@"c:\demo\subfolder");
+			m.Create ();
+			m.LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10);
+
+			fileSystem.AddFile (@"c:\demo\subfolder\image2.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }) { LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10) });
+
+			var fd = new FileDialog (fileSystem) {
+				Height = 15
+			};
+			fd.Path = @"c:\demo\";
+			Begin (fd);
+			fd.Title = string.Empty;
+
+			fd.Redraw (fd.Bounds);
+
+			fd.Style.DateFormat = "yyyy-MM-dd hh:mm:ss";
+
+			string expected =
+			@"
+┌──────────────────────────────────────────────────────────────────┐
+│c:\demo\                                                          │
+│[▲]                                                               │
+│┌────────────┬──────────┬──────────────────────────────┬─────────┐│
+││Filename (▲)│Size      │Modified                      │Type     ││
+│├────────────┼──────────┼──────────────────────────────┼─────────┤│
+││..          │          │                              │dir      ││
+││\subfolder  │          │2002-01-01T22:42:10           │dir      ││
+││image.gif   │4.00 bytes│2002-01-01T22:42:10           │.gif     ││
+││jQuery.js   │7.00 bytes│2001-01-01T11:44:42           │.js      ││
+│                                                                  │
+│                                                                  │
+│                                                                  │
+│[ ►► ] Enter Search                            [ Cancel ] [ Ok ]  │
+└──────────────────────────────────────────────────────────────────┘
+";
+			TestHelpers.AssertDriverContentsAre (expected, output, true);
+		}
+
+		[Theory, AutoInitShutdown]
+		[InlineData (true)]
+		[InlineData (false)]
+		public void CancelSelection (bool cancel)
+		{
+			var dlg = GetInitializedFileDialog ();
+			var openIn = Path.Combine (Environment.CurrentDirectory, "zz");
+			Directory.CreateDirectory (openIn);
+			dlg.Path = openIn + Path.DirectorySeparatorChar;
+
+			dlg.FilesSelected += (s, e) => e.Cancel = cancel;
+
+			//pressing enter will complete the current selection
+			// unless the event cancels the confirm
+			Send ('\n', ConsoleKey.Enter, false);
+
+			Assert.Equal (cancel, dlg.Canceled);
+		}
+
+		private void Send (char ch, ConsoleKey ck, bool shift = false, bool alt = false, bool control = false)
+		{
+			Application.Driver.SendKeys (ch, ck, shift, alt, control);
+		}
+		private void Send (string chars)
+		{
+			foreach (var ch in chars) {
+				Application.Driver.SendKeys (ch, ConsoleKey.NoName, false, false, false);
+			}
+
+		}
+		/*
+				[Fact, AutoInitShutdown]
+				public void Autocomplete_NoSuggestion_WhenTextMatchesExactly ()
+				{
+					var tb = new TextFieldWithAppendAutocomplete ();
+					ForceFocus (tb);
+
+					tb.Text = "/bob/fish";
+					tb.CursorPosition = tb.Text.Length;
+					tb.GenerateSuggestions (null, "fish", "fishes");
+
+					// should not report success for autocompletion because we already have that exact
+					// string
+					Assert.False (tb.AcceptSelectionIfAny ());
+				}
+
+
+				[Fact, AutoInitShutdown]
+				public void Autocomplete_AcceptSuggstion ()
+				{
+					var tb = new TextFieldWithAppendAutocomplete ();
+					ForceFocus (tb);
+
+					tb.Text = @"/bob/fi";
+					tb.CursorPosition = tb.Text.Length;
+					tb.GenerateSuggestions (null, "fish", "fishes");
+
+					Assert.True (tb.AcceptSelectionIfAny ());
+					Assert.Equal (@"/bob/fish", tb.Text);
+				}*/
+
+
+		private FileDialog GetInitializedFileDialog ()
+		{
+			var dlg = new FileDialog ();
+			Begin (dlg);
+
+			return dlg;
+		}
+		private void Begin (FileDialog dlg)
+		{
+			dlg.BeginInit ();
+			dlg.EndInit ();
+			Application.Begin (dlg);
+		}
+	}
+}

+ 8 - 1
UnitTests/TestHelpers.cs

@@ -84,7 +84,7 @@ public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
 
 class TestHelpers {
 #pragma warning disable xUnit1013 // Public method should be marked as test
-	public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
+	public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output, bool ignoreLeadingWhitespace = false)
 	{
 #pragma warning restore xUnit1013 // Public method should be marked as test
 
@@ -114,11 +114,18 @@ class TestHelpers {
 
 			// ignore trailing whitespace on each line
 			var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
+			var leadingWhitespace = new Regex(@"^\s+",RegexOptions.Multiline);
 
 			// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
 			expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
 			actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
 
+			if(ignoreLeadingWhitespace)
+			{
+				expectedLook = leadingWhitespace.Replace (expectedLook, "").Trim ();
+				actualLook = leadingWhitespace.Replace (actualLook, "").Trim ();
+			}
+
 			// standardize line endings for the comparison
 			expectedLook = expectedLook.Replace ("\r\n", "\n");
 			actualLook = actualLook.Replace ("\r\n", "\n");

+ 1 - 0
UnitTests/UnitTests.csproj

@@ -24,6 +24,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
     <PackageReference Include="ReportGenerator" Version="5.1.18" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
+    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="19.2.4" />
     <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
       <PrivateAssets>all</PrivateAssets>