瀏覽代碼

Fixes #2726 - Refactor filedialog classes to be more easily reused (#2727)

* Refactor FileDialogTreeBuilder to be more generally useful outside of dialog context

* Fix comparer

* Change TreeViewFileSystem scenario to use the core builder

* Refactor icon provision for reusability

* Add IsOpenGetter implementations

* Xmldoc and tests

* xmldoc and trim icon when blank (files and no nerd)

* unit test fixes

* FixFix unit tests when running on linux

* Add option to pick which icon set to use for TreeViewFileSystem

* Add spaces when using nerd to avoid icon overaps

* Refactor the addition of space for nerd icons to reduce code duplication
Thomas Nind 2 年之前
父節點
當前提交
a8d1a79615

+ 0 - 35
Terminal.Gui/FileServices/FileDialogIconGetterArgs.cs

@@ -1,35 +0,0 @@
-using System.IO.Abstractions;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Arguments for the <see cref="FileDialogStyle.IconGetter"/> delegate
-	/// </summary>
-	public class FileDialogIconGetterArgs {
-
-		/// <summary>
-		/// Creates a new instance of the class
-		/// </summary>
-		public FileDialogIconGetterArgs (FileDialog fileDialog, IFileSystemInfo file, FileDialogIconGetterContext context)
-		{
-			FileDialog = fileDialog;
-			File = file;
-			Context = context;
-		}
-
-		/// <summary>
-		/// Gets the dialog that requires the icon.
-		/// </summary>
-		public FileDialog FileDialog { get; }
-
-		/// <summary>
-		/// Gets the file/folder for which the icon is required.
-		/// </summary>
-		public IFileSystemInfo File { get; }
-
-		/// <summary>
-		/// Gets the context in which the icon will be used in.
-		/// </summary>
-		public FileDialogIconGetterContext Context { get; }
-	}
-}

+ 0 - 18
Terminal.Gui/FileServices/FileDialogIconGetterContext.cs

@@ -1,18 +0,0 @@
-namespace Terminal.Gui {
-	/// <summary>
-	/// Describes the context in which icons are being sought
-	/// during <see cref="FileDialogIconGetterArgs"/>.
-	/// </summary>
-	public enum FileDialogIconGetterContext {
-
-		/// <summary>
-		/// Icon will be used in the tree view
-		/// </summary>
-		Tree,
-
-		/// <summary>
-		/// Icon will be used in the main table area of the dialog
-		/// </summary>
-		Table
-	}
-}

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

@@ -1,52 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
-
-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, IDirectoryInfo 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 IDirectoryInfo Path { get; }
-
-		/// <summary>
-		/// Returns a string representation of this instance (<see cref="DisplayName"/>).
-		/// </summary>
-		/// <returns></returns>
-		public override string ToString ()
-		{
-			return this.DisplayName;
-		}
-	}
-}

+ 23 - 48
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -38,6 +38,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public bool UseColors { get; set; } = DefaultUseColors;
 		public bool UseColors { get; set; } = DefaultUseColors;
 
 
+		/// <summary>
+		/// Gets or sets the class responsible for determining which symbol
+		/// to use to represent files and directories.
+		/// </summary>
+		public FileSystemIconProvider IconProvider { get; set;} = new FileSystemIconProvider();
+
 		/// <summary>
 		/// <summary>
 		/// Gets or sets the culture to use (e.g. for number formatting).
 		/// Gets or sets the culture to use (e.g. for number formatting).
 		/// Defaults to <see cref="CultureInfo.CurrentUICulture"/>.
 		/// Defaults to <see cref="CultureInfo.CurrentUICulture"/>.
@@ -156,7 +162,7 @@ namespace Terminal.Gui {
 		/// <see cref="Environment.SpecialFolder"/>.
 		/// <see cref="Environment.SpecialFolder"/>.
 		/// </summary>
 		/// </summary>
 		/// <remarks>Must be configured before showing the dialog.</remarks>
 		/// <remarks>Must be configured before showing the dialog.</remarks>
-		public FileDialogTreeRootGetter TreeRootGetter { get; set; }
+		public Func<Dictionary<IDirectoryInfo, string>> TreeRootGetter { get; set; }
 
 
 		/// <summary>
 		/// <summary>
 		/// Gets or sets whether to use advanced unicode characters which might not be installed
 		/// Gets or sets whether to use advanced unicode characters which might not be installed
@@ -164,12 +170,6 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public bool UseUnicodeCharacters { get; set; } = DefaultUseUnicodeCharacters;
 		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<FileDialogIconGetterArgs, string> IconGetter { get; set; }
-
 		/// <summary>
 		/// <summary>
 		/// Gets or sets the format to use for date/times in the Modified column.
 		/// Gets or sets the format to use for date/times in the Modified column.
 		/// Defaults to <see cref="DateTimeFormatInfo.SortableDateTimePattern"/> 
 		/// Defaults to <see cref="DateTimeFormatInfo.SortableDateTimePattern"/> 
@@ -183,14 +183,8 @@ namespace Terminal.Gui {
 		public FileDialogStyle (IFileSystem fileSystem)
 		public FileDialogStyle (IFileSystem fileSystem)
 		{
 		{
 			_fileSystem = fileSystem;
 			_fileSystem = fileSystem;
-			IconGetter = DefaultIconGetter;
 			TreeRootGetter = DefaultTreeRootGetter;
 			TreeRootGetter = DefaultTreeRootGetter;
 
 
-			if(NerdFonts.Enable)
-			{
-				UseNerdForIcons();
-			}
-
 			DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
 			DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
 
 
 			ColorSchemeDirectory = new ColorScheme {
 			ColorSchemeDirectory = new ColorScheme {
@@ -221,37 +215,18 @@ namespace Terminal.Gui {
 			};
 			};
 		}
 		}
 
 
-		/// <summary>
-		/// Changes <see cref="IconGetter"/> to serve diverse icon set using
-		/// the Nerd fonts. This option requires users to have specific font(s)
-		/// installed.
-		/// </summary>
-		public void UseNerdForIcons ()
-		{
-			var nerd = new NerdFonts();
-			IconGetter = nerd.GetNerdIcon;
-		}
 
 
-		private string DefaultIconGetter (FileDialogIconGetterArgs args)
+		private Dictionary<IDirectoryInfo,string> DefaultTreeRootGetter ()
 		{
 		{
-			var file = args.File;
-
-			if (file is IDirectoryInfo) {
-				return UseUnicodeCharacters ? ConfigurationManager.Glyphs.Folder + " " : Path.DirectorySeparatorChar.ToString();
-			}
-
-			return UseUnicodeCharacters ?  ConfigurationManager.Glyphs.File + " " : "";
-
-		}
-
-		private IEnumerable<FileDialogRootTreeNode> DefaultTreeRootGetter ()
-		{
-			var roots = new List<FileDialogRootTreeNode> ();
+			var roots = new Dictionary<IDirectoryInfo, string> ();
 			try {
 			try {
 				foreach (var d in Environment.GetLogicalDrives ()) {
 				foreach (var d in Environment.GetLogicalDrives ()) {
 
 
-					
-					roots.Add (new FileDialogRootTreeNode (d, _fileSystem.DirectoryInfo.New(d)));
+					var dir = _fileSystem.DirectoryInfo.New (d);
+
+					if (!roots.ContainsKey(dir)) {
+						roots.Add (dir, d);
+					}
 				}
 				}
 
 
 			} catch (Exception) {
 			} catch (Exception) {
@@ -262,15 +237,15 @@ namespace Terminal.Gui {
 				foreach (var special in Enum.GetValues (typeof (Environment.SpecialFolder)).Cast<SpecialFolder> ()) {
 				foreach (var special in Enum.GetValues (typeof (Environment.SpecialFolder)).Cast<SpecialFolder> ()) {
 					try {
 					try {
 						var path = Environment.GetFolderPath (special);
 						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 (),
-							_fileSystem.DirectoryInfo.New(Environment.GetFolderPath (special))
-							));
+
+						if(string.IsNullOrWhiteSpace (path)) {
+							continue;
+						}
+
+						var dir = _fileSystem.DirectoryInfo.New (path);
+
+						if (!roots.ContainsKey (dir) && dir.Exists) {
+							roots.Add (dir, special.ToString());
 						}
 						}
 					} catch (Exception) {
 					} catch (Exception) {
 						// Special file exists but contents are unreadable (permissions?)
 						// Special file exists but contents are unreadable (permissions?)

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

@@ -1,67 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
-using System.Linq;
-
-namespace Terminal.Gui {
-
-	class FileDialogTreeBuilder : ITreeBuilder<object> {
-		readonly FileDialog _dlg;
-
-		public FileDialogTreeBuilder(FileDialog dlg)
-		{
-			_dlg = dlg;
-		}
-
-		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 IDirectoryInfo NodeToDirectory (object toExpand)
-		{
-			return toExpand is FileDialogRootTreeNode f ? f.Path : (IDirectoryInfo)toExpand;
-		}
-
-		internal string AspectGetter(object o)
-		{
-			string icon;
-			string name;
-
-			if(o is FileDialogRootTreeNode r)
-			{
-				icon = _dlg.Style.IconGetter.Invoke(
-					new FileDialogIconGetterArgs(_dlg, r.Path, FileDialogIconGetterContext.Tree));
-				name = r.DisplayName;
-			}
-			else
-			{
-				var dir  = (IDirectoryInfo)o;
-				icon = _dlg.Style.IconGetter.Invoke(
-					new FileDialogIconGetterArgs(_dlg, dir, FileDialogIconGetterContext.Tree));
-				name = dir.Name;
-			}
-
-			return icon + name;
-		}
-
-		private IEnumerable<IDirectoryInfo> TryGetDirectories (IDirectoryInfo directoryInfo)
-		{
-			try {
-				return directoryInfo.EnumerateDirectories ();
-			} catch (Exception) {
-
-				return Enumerable.Empty<IDirectoryInfo> ();
-			}
-		}
-
-	}
-}

+ 80 - 0
Terminal.Gui/FileServices/FileSystemTreeBuilder.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// TreeView builder for creating file system based trees.
+	/// </summary>
+	public class FileSystemTreeBuilder : ITreeBuilder<IFileSystemInfo>, IComparer<IFileSystemInfo> {
+
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="FileSystemTreeBuilder"/> class.
+		/// </summary>
+		public FileSystemTreeBuilder ()
+		{
+			Sorter = this;
+		}
+
+		/// <inheritdoc/>
+		public bool SupportsCanExpand => true;
+
+		/// <summary>
+		/// Gets or sets a flag indicating whether to show files as leaf elements
+		/// in the tree. Defaults to true.
+		/// </summary>
+		public bool IncludeFiles { get; } = true;
+
+		/// <summary>
+		/// Gets or sets the order of directory children.  Defaults to <see langword="this"/>.
+		/// </summary>
+		public IComparer<IFileSystemInfo> Sorter { get; set; }
+
+		/// <inheritdoc/>
+		public bool CanExpand (IFileSystemInfo toExpand)
+		{
+			return this.TryGetChildren (toExpand).Any ();
+		}
+
+		/// <inheritdoc/>
+		public IEnumerable<IFileSystemInfo> GetChildren (IFileSystemInfo forObject)
+		{
+			return this.TryGetChildren (forObject).OrderBy(k=>k,Sorter);
+		}
+
+		private IEnumerable<IFileSystemInfo> TryGetChildren (IFileSystemInfo entry)
+		{
+			if (entry is IFileInfo) {
+				return Enumerable.Empty<IFileSystemInfo> ();
+			}
+
+			var dir = (IDirectoryInfo)entry;
+
+			try {
+				return dir.GetFileSystemInfos ().Where (e => IncludeFiles || e is IDirectoryInfo);
+
+			} catch (Exception) {
+
+				return Enumerable.Empty<IFileSystemInfo> ();
+			}
+		}
+
+		/// <inheritdoc/>
+		public int Compare (IFileSystemInfo x, IFileSystemInfo y)
+		{
+			if (x is IDirectoryInfo && y is not IDirectoryInfo) {
+				return -1;
+			}
+
+			if (x is not IDirectoryInfo && y is IDirectoryInfo) {
+				return 1;
+			}
+
+			return x.Name.CompareTo (y.Name);
+		}
+	}
+}

+ 24 - 7
Terminal.Gui/Views/FileDialog.cs

@@ -87,7 +87,7 @@ namespace Terminal.Gui {
 		private FileDialogHistory history;
 		private FileDialogHistory history;
 
 
 		private TableView tableView;
 		private TableView tableView;
-		private TreeView<object> treeView;
+		private TreeView<IFileSystemInfo> treeView;
 		private TileView splitContainer;
 		private TileView splitContainer;
 		private Button btnOk;
 		private Button btnOk;
 		private Button btnCancel;
 		private Button btnCancel;
@@ -105,6 +105,7 @@ namespace Terminal.Gui {
 		private int currentSortColumn;
 		private int currentSortColumn;
 
 
 		private bool currentSortIsAsc = true;
 		private bool currentSortIsAsc = true;
+		private Dictionary<IDirectoryInfo,string> _treeRoots = new Dictionary<IDirectoryInfo, string>();
 
 
 		/// <summary>
 		/// <summary>
 		/// Event fired when user attempts to confirm a selection (or multi selection).
 		/// Event fired when user attempts to confirm a selection (or multi selection).
@@ -255,14 +256,14 @@ namespace Terminal.Gui {
 				}
 				}
 			};
 			};
 
 
-			this.treeView = new TreeView<object> () {
+			this.treeView = new TreeView<IFileSystemInfo> () {
 				Width = Dim.Fill (),
 				Width = Dim.Fill (),
 				Height = Dim.Fill (),
 				Height = Dim.Fill (),
 			};
 			};
 
 
-			var fileDialogTreeBuilder = new FileDialogTreeBuilder (this);
+			var fileDialogTreeBuilder = new FileSystemTreeBuilder ();
 			this.treeView.TreeBuilder = fileDialogTreeBuilder;
 			this.treeView.TreeBuilder = fileDialogTreeBuilder;
-			this.treeView.AspectGetter = fileDialogTreeBuilder.AspectGetter;
+			this.treeView.AspectGetter = this.AspectGetter;
 			this.Style.TreeStyle = treeView.Style;
 			this.Style.TreeStyle = treeView.Style;
 
 
 			this.treeView.SelectionChanged += this.TreeView_SelectionChanged;
 			this.treeView.SelectionChanged += this.TreeView_SelectionChanged;
@@ -372,6 +373,19 @@ namespace Terminal.Gui {
 			this.Add (this.splitContainer);
 			this.Add (this.splitContainer);
 		}
 		}
 
 
+		private string AspectGetter (object o)
+		{
+			var fsi = (IFileSystemInfo)o;
+
+			if(o is IDirectoryInfo dir && _treeRoots.ContainsKey(dir)) {
+
+				// Directory has a special name e.g. 'Pictures'
+				return _treeRoots [dir];
+			}
+
+			return (Style.IconProvider.GetIconWithOptionalSpace(fsi) + fsi.Name).Trim();
+		}
+
 		private void OnTableViewMouseClick (object sender, MouseEventEventArgs e)
 		private void OnTableViewMouseClick (object sender, MouseEventEventArgs e)
 		{
 		{
 			var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
 			var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
@@ -637,7 +651,10 @@ namespace Terminal.Gui {
 
 
 			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
 			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
 
 
-			treeView.AddObjects (Style.TreeRootGetter ());
+			_treeRoots = Style.TreeRootGetter ();
+			Style.IconProvider.IsOpenGetter = treeView.IsExpanded;
+
+			treeView.AddObjects (_treeRoots.Keys);
 
 
 			// if filtering on file type is configured then create the ComboBox and establish
 			// if filtering on file type is configured then create the ComboBox and establish
 			// initial filtering by extension(s)
 			// initial filtering by extension(s)
@@ -860,13 +877,13 @@ namespace Terminal.Gui {
 			return false;
 			return false;
 		}
 		}
 
 
-		private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs<object> e)
+		private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs<IFileSystemInfo> e)
 		{
 		{
 			if (e.NewValue == null) {
 			if (e.NewValue == null) {
 				return;
 				return;
 			}
 			}
 
 
-			this.tbPath.Text = FileDialogTreeBuilder.NodeToDirectory (e.NewValue).FullName;
+			this.tbPath.Text = e.NewValue.FullName;
 		}
 		}
 
 
 		private void UpdateNavigationVisibility ()
 		private void UpdateNavigationVisibility ()

+ 7 - 3
Terminal.Gui/Views/FileDialogTableSource.cs

@@ -24,9 +24,13 @@ namespace Terminal.Gui {
 		{
 		{
 			switch (col) {
 			switch (col) {
 			case 0:
 			case 0:
-				var icon = stats.IsParent ? null : style.IconGetter?.Invoke (
-					new FileDialogIconGetterArgs(dlg,stats.FileSystemInfo, FileDialogIconGetterContext.Table));
-				return icon + (stats?.Name ?? string.Empty);
+				// do not use icon for ".."
+				if(stats?.IsParent ?? false) {
+					return stats.Name;
+				}
+
+				var icon = dlg.Style.IconProvider.GetIconWithOptionalSpace(stats.FileSystemInfo);
+				return (icon + (stats?.Name ?? string.Empty)).Trim();
 			case 1:
 			case 1:
 				return stats?.HumanReadableLength ?? string.Empty;
 				return stats?.HumanReadableLength ?? string.Empty;
 			case 2:
 			case 2:

+ 86 - 0
Terminal.Gui/Views/FileSystemIconProvider.cs

@@ -0,0 +1,86 @@
+using System;
+using System.IO;
+using System.IO.Abstractions;
+using System.Text;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Determines which symbol to use to represent files and directories.
+	/// </summary>
+	public class FileSystemIconProvider {
+		/// <summary>
+		/// <para>
+		/// Gets or sets a flag indicating whether to use Nerd Font icons.
+		/// Defaults to <see cref="NerdFonts.Enable"/> which can be configured
+		/// by end users from their <c>./.tui/config.json</c> 
+		/// via <see cref="ConfigurationManager"/>.
+		/// </para>
+		/// <remarks>Enabling <see cref="UseNerdIcons"/> implicitly
+		/// disables <see cref="UseUnicodeCharacters"/>.</remarks>
+		/// </summary>
+		public bool UseNerdIcons {
+			get => _useNerdIcons; set {
+				_useNerdIcons = value;
+				if (value) {
+					UseUnicodeCharacters = false;
+				}
+			}
+		}
+		/// <summary>
+		/// Gets or sets a flag indicating whether to use common unicode
+		/// characters for file/directory icons.
+		/// </summary>
+		public bool UseUnicodeCharacters {
+			get => _useUnicodeCharacters;
+			set { 
+				_useUnicodeCharacters = value;
+				if (value) {
+					UseNerdIcons = false;
+				}
+			}
+		}
+
+		private NerdFonts _nerd = new NerdFonts ();
+		private bool _useNerdIcons = NerdFonts.Enable;
+		private bool _useUnicodeCharacters;
+
+		/// <summary>
+		/// Gets or sets the delegate to be used to determine opened state of directories
+		/// when resolving <see cref="GetIcon(IFileSystemInfo)"/>.  Defaults to always false.
+		/// </summary>
+		public Func<IDirectoryInfo, bool> IsOpenGetter { get; set; } = (d) => false;
+
+		/// <summary>
+		/// Returns the character to use to represent <paramref name="fileSystemInfo"/> or an empty
+		/// space if no icon should be used.
+		/// </summary>
+		/// <param name="fileSystemInfo">The file or directory requiring an icon.</param>
+		/// <returns></returns>
+		public Rune GetIcon (IFileSystemInfo fileSystemInfo)
+		{
+			if (UseNerdIcons) {
+				return new Rune (
+					_nerd.GetNerdIcon (
+						fileSystemInfo,
+						fileSystemInfo is IDirectoryInfo dir ? IsOpenGetter (dir) : false
+					));
+			}
+
+			if (fileSystemInfo is IDirectoryInfo) {
+				return UseUnicodeCharacters ? ConfigurationManager.Glyphs.Folder : new Rune (Path.DirectorySeparatorChar);
+			}
+
+			return UseUnicodeCharacters ? ConfigurationManager.Glyphs.File : new Rune (' ');
+		}
+		
+		/// <summary>
+		/// Returns <see cref="GetIcon(IFileSystemInfo)"/> with an extra
+		/// space on the end if icon is likely to overlap adjacent cells.
+		/// </summary>
+		public string GetIconWithOptionalSpace(IFileSystemInfo fileSystemInfo)
+		{
+			var space = UseNerdIcons ? " " : "";
+			return GetIcon(fileSystemInfo) + space;
+		}
+	}
+}

+ 2 - 16
Terminal.Gui/Views/NerdFonts.cs

@@ -16,16 +16,8 @@ namespace Terminal.Gui {
 		[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
 		[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
 		public static bool Enable { get; set; } = false;
 		public static bool Enable { get; set; } = false;
 
 
-		public string GetNerdIcon(FileDialogIconGetterArgs args)
+		public char GetNerdIcon(IFileSystemInfo file, bool isOpen)
 		{
 		{
-			return GetNerdIconChar(args) + " ";
-		}
-
-		private char GetNerdIconChar(FileDialogIconGetterArgs args)
-		{
-			var file = args.File;
-			var path = args.FileDialog.Path;
-
 			if(FilenameToIcon.ContainsKey(file.Name))
 			if(FilenameToIcon.ContainsKey(file.Name))
 			{
 			{
 				return Glyphs[FilenameToIcon[file.Name]];
 				return Glyphs[FilenameToIcon[file.Name]];
@@ -38,13 +30,7 @@ namespace Terminal.Gui {
 
 
 			if(file is IDirectoryInfo d)
 			if(file is IDirectoryInfo d)
 			{
 			{
-				if(!string.IsNullOrWhiteSpace(path) &&
-				   path.Contains(d.FullName) &&
-				   args.Context == FileDialogIconGetterContext.Tree)
-				{
-					return _nf_cod_folder_opened;
-				}
-				return _nf_cod_folder;
+				return isOpen ? _nf_cod_folder_opened :_nf_cod_folder;
 			}
 			}
 				
 				
 			return _nf_cod_file;
 			return _nf_cod_file;

+ 3 - 7
UICatalog/Scenarios/FileDialogExamples.cs

@@ -148,11 +148,8 @@ namespace UICatalog.Scenarios {
 				fd.FilesSelected += ConfirmOverwrite;
 				fd.FilesSelected += ConfirmOverwrite;
 			}
 			}
 
 
-			if (rgIcons.SelectedItem == 1) {
-				fd.Style.UseUnicodeCharacters = true;
-			} else if (rgIcons.SelectedItem == 2) {
-				fd.Style.UseNerdForIcons ();
-			}
+			fd.Style.IconProvider.UseUnicodeCharacters = rgIcons.SelectedItem == 1;
+			fd.Style.IconProvider.UseNerdIcons = rgIcons.SelectedItem == 2;
 
 
 			if (cbCaseSensitive.Checked ?? false) {
 			if (cbCaseSensitive.Checked ?? false) {
 
 
@@ -168,8 +165,7 @@ namespace UICatalog.Scenarios {
 
 
 			if (cbDrivesOnlyInTree.Checked ?? false) {
 			if (cbDrivesOnlyInTree.Checked ?? false) {
 				fd.Style.TreeRootGetter = () => {
 				fd.Style.TreeRootGetter = () => {
-					return System.Environment.GetLogicalDrives ()
-					.Select (d => new FileDialogRootTreeNode (d, dirInfoFactory.New (d)));
+					return System.Environment.GetLogicalDrives ().ToDictionary(dirInfoFactory.New,k=>k);
 				};
 				};
 			}
 			}
 
 

+ 67 - 59
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.IO.Abstractions;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
 using Terminal.Gui;
 using Terminal.Gui;
@@ -13,7 +14,7 @@ namespace UICatalog.Scenarios {
 		/// <summary>
 		/// <summary>
 		/// A tree view where nodes are files and folders
 		/// A tree view where nodes are files and folders
 		/// </summary>
 		/// </summary>
-		TreeView<FileSystemInfo> treeViewFiles;
+		TreeView<IFileSystemInfo> treeViewFiles;
 
 
 		MenuItem miShowLines;
 		MenuItem miShowLines;
 		private MenuItem _miPlusMinus;
 		private MenuItem _miPlusMinus;
@@ -21,7 +22,11 @@ namespace UICatalog.Scenarios {
 		private MenuItem _miNoSymbols;
 		private MenuItem _miNoSymbols;
 		private MenuItem _miColoredSymbols;
 		private MenuItem _miColoredSymbols;
 		private MenuItem _miInvertSymbols;
 		private MenuItem _miInvertSymbols;
-		private MenuItem _miUnicodeSymbols;
+
+		private MenuItem _miBasicIcons;
+		private MenuItem _miUnicodeIcons;
+		private MenuItem _miNerdIcons;
+
 		private MenuItem _miFullPaths;
 		private MenuItem _miFullPaths;
 		private MenuItem _miLeaveLastRow;
 		private MenuItem _miLeaveLastRow;
 		private MenuItem _miHighlightModelTextOnly;
 		private MenuItem _miHighlightModelTextOnly;
@@ -30,6 +35,7 @@ namespace UICatalog.Scenarios {
 		private MenuItem _miMultiSelect;
 		private MenuItem _miMultiSelect;
 
 
 		private DetailsFrame _detailsFrame;
 		private DetailsFrame _detailsFrame;
+		private FileSystemIconProvider _iconProvider = new ();
 
 
 		public override void Setup ()
 		public override void Setup ()
 		{
 		{
@@ -54,11 +60,14 @@ namespace UICatalog.Scenarios {
 					_miPlusMinus = new MenuItem ("_Plus Minus Symbols", "+ -", () => SetExpandableSymbols((Rune)'+',(Rune)'-')){Checked = true, CheckType = MenuItemCheckStyle.Radio},
 					_miPlusMinus = new MenuItem ("_Plus Minus Symbols", "+ -", () => SetExpandableSymbols((Rune)'+',(Rune)'-')){Checked = true, CheckType = MenuItemCheckStyle.Radio},
 					_miArrowSymbols = new MenuItem ("_Arrow Symbols", "> v", () => SetExpandableSymbols((Rune)'>',(Rune)'v')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
 					_miArrowSymbols = new MenuItem ("_Arrow Symbols", "> v", () => SetExpandableSymbols((Rune)'>',(Rune)'v')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
 					_miNoSymbols = new MenuItem ("_No Symbols", "", () => SetExpandableSymbols(default,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio},
 					_miNoSymbols = new MenuItem ("_No Symbols", "", () => SetExpandableSymbols(default,null)){Checked = false, CheckType = MenuItemCheckStyle.Radio},
-					_miUnicodeSymbols = new MenuItem ("_Unicode", "ஹ ﷽", () => SetExpandableSymbols((Rune)'ஹ',(Rune)'﷽')){Checked = false, CheckType = MenuItemCheckStyle.Radio},
 					null /*separator*/,
 					null /*separator*/,
 					_miColoredSymbols = new MenuItem ("_Colored Symbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					_miColoredSymbols = new MenuItem ("_Colored Symbols", "", () => ShowColoredExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					_miInvertSymbols = new MenuItem ("_Invert Symbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					_miInvertSymbols = new MenuItem ("_Invert Symbols", "", () => InvertExpandableSymbols()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					null /*separator*/,
 					null /*separator*/,
+					_miBasicIcons = new MenuItem ("_Basic Icons",null, SetNoIcons){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					_miUnicodeIcons = new MenuItem ("_Unicode Icons", null, SetUnicodeIcons){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					_miNerdIcons = new MenuItem ("_Nerd Icons", null, SetNerdIcons){Checked = false, CheckType = MenuItemCheckStyle.Radio},
+					null /*separator*/,
 					_miLeaveLastRow = new MenuItem ("_Leave Last Row", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked},
 					_miLeaveLastRow = new MenuItem ("_Leave Last Row", "", () => SetLeaveLastRow()){Checked = true, CheckType = MenuItemCheckStyle.Checked},
 					_miHighlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => SetCheckHighlightModelTextOnly()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					_miHighlightModelTextOnly = new MenuItem ("_Highlight Model Text Only", "", () => SetCheckHighlightModelTextOnly()){Checked = false, CheckType = MenuItemCheckStyle.Checked},
 					null /*separator*/,
 					null /*separator*/,
@@ -69,14 +78,14 @@ namespace UICatalog.Scenarios {
 			});
 			});
 			Application.Top.Add (menu);
 			Application.Top.Add (menu);
 
 
-			treeViewFiles = new TreeView<FileSystemInfo> () {
+			treeViewFiles = new TreeView<IFileSystemInfo> () {
 				X = 0,
 				X = 0,
 				Y = 0,
 				Y = 0,
 				Width = Dim.Percent (50),
 				Width = Dim.Percent (50),
 				Height = Dim.Fill (),
 				Height = Dim.Fill (),
 			};
 			};
 
 
-			_detailsFrame = new DetailsFrame () {
+			_detailsFrame = new DetailsFrame (_iconProvider) {
 				X = Pos.Right (treeViewFiles),
 				X = Pos.Right (treeViewFiles),
 				Y = 0,
 				Y = 0,
 				Width = Dim.Fill (),
 				Width = Dim.Fill (),
@@ -97,10 +106,36 @@ namespace UICatalog.Scenarios {
 			SetupScrollBar ();
 			SetupScrollBar ();
 
 
 			treeViewFiles.SetFocus ();
 			treeViewFiles.SetFocus ();
+			
+			UpdateIconCheckedness ();
+		}
 
 
+		private void SetNoIcons ()
+		{
+			_iconProvider.UseUnicodeCharacters = false;
+			_iconProvider.UseNerdIcons = false;
+			UpdateIconCheckedness ();
 		}
 		}
 
 
-		private void TreeViewFiles_SelectionChanged (object sender, SelectionChangedEventArgs<FileSystemInfo> e)
+		private void SetUnicodeIcons ()
+		{
+			_iconProvider.UseUnicodeCharacters = true;
+			UpdateIconCheckedness ();
+		}
+		private void SetNerdIcons ()
+		{
+			_iconProvider.UseNerdIcons = true;
+			UpdateIconCheckedness ();
+		}
+		private void UpdateIconCheckedness ()
+		{
+			_miBasicIcons.Checked = !_iconProvider.UseNerdIcons && !_iconProvider.UseUnicodeCharacters;
+			_miUnicodeIcons.Checked = _iconProvider.UseUnicodeCharacters;
+			_miNerdIcons.Checked = _iconProvider.UseNerdIcons;
+			treeViewFiles.SetNeedsDisplay ();
+		}
+
+		private void TreeViewFiles_SelectionChanged (object sender, SelectionChangedEventArgs<IFileSystemInfo> e)
 		{
 		{
 			ShowPropertiesOf (e.NewValue);
 			ShowPropertiesOf (e.NewValue);
 		}
 		}
@@ -146,7 +181,7 @@ namespace UICatalog.Scenarios {
 			}
 			}
 		}
 		}
 
 
-		private void ShowContextMenu (Point screenPoint, FileSystemInfo forObject)
+		private void ShowContextMenu (Point screenPoint, IFileSystemInfo forObject)
 		{
 		{
 			var menu = new ContextMenu ();
 			var menu = new ContextMenu ();
 			menu.Position = screenPoint;
 			menu.Position = screenPoint;
@@ -157,21 +192,24 @@ namespace UICatalog.Scenarios {
 		}
 		}
 
 
 		class DetailsFrame : FrameView {
 		class DetailsFrame : FrameView {
-			private FileSystemInfo fileInfo;
+			private IFileSystemInfo fileInfo;
+			private FileSystemIconProvider _iconProvider;
 
 
-			public DetailsFrame ()
+			public DetailsFrame (FileSystemIconProvider  iconProvider)
 			{
 			{
 				Title = "Details";
 				Title = "Details";
 				Visible = true;
 				Visible = true;
 				CanFocus = true;
 				CanFocus = true;
+				_iconProvider = iconProvider;
 			}
 			}
 
 
-			public FileSystemInfo FileInfo {
+			public IFileSystemInfo FileInfo {
 				get => fileInfo; set {
 				get => fileInfo; set {
 					fileInfo = value;
 					fileInfo = value;
 					System.Text.StringBuilder sb = null;
 					System.Text.StringBuilder sb = null;
-					if (fileInfo is FileInfo f) {
-						Title = $"File: {f.Name}";
+
+					if (fileInfo is IFileInfo f) {
+						Title = $"{_iconProvider.GetIconWithOptionalSpace(f)}{f.Name}".Trim();
 						sb = new System.Text.StringBuilder ();
 						sb = new System.Text.StringBuilder ();
 						sb.AppendLine ($"Path:\n {f.FullName}\n");
 						sb.AppendLine ($"Path:\n {f.FullName}\n");
 						sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n");
 						sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n");
@@ -179,8 +217,8 @@ namespace UICatalog.Scenarios {
 						sb.AppendLine ($"Created:\n {f.CreationTime}");
 						sb.AppendLine ($"Created:\n {f.CreationTime}");
 					}
 					}
 
 
-					if (fileInfo is DirectoryInfo dir) {
-						Title = $"Directory: {dir.Name}";
+					if (fileInfo is IDirectoryInfo dir) {
+						Title = $"{_iconProvider.GetIconWithOptionalSpace(dir)}{dir.Name}".Trim();
 						sb = new System.Text.StringBuilder ();
 						sb = new System.Text.StringBuilder ();
 						sb.AppendLine ($"Path:\n {dir?.FullName}\n");
 						sb.AppendLine ($"Path:\n {dir?.FullName}\n");
 						sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n");
 						sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n");
@@ -191,7 +229,7 @@ namespace UICatalog.Scenarios {
 			}
 			}
 		}
 		}
 
 
-		private void ShowPropertiesOf (FileSystemInfo fileSystemInfo)
+		private void ShowPropertiesOf (IFileSystemInfo fileSystemInfo)
 		{
 		{
 			_detailsFrame.FileInfo = fileSystemInfo;
 			_detailsFrame.FileInfo = fileSystemInfo;
 		}
 		}
@@ -230,20 +268,21 @@ namespace UICatalog.Scenarios {
 
 
 		private void SetupFileTree ()
 		private void SetupFileTree ()
 		{
 		{
-
-			// setup delegates
-			treeViewFiles.TreeBuilder = new DelegateTreeBuilder<FileSystemInfo> (
-
-				// Determines how to compute children of any given branch
-				GetChildren,
-				// As a shortcut to enumerating half the file system, tell tree that all directories are expandable (even if they turn out to be empty later on)				
-				(o) => o is DirectoryInfo
-			);
+			// setup how to build tree
+			var fs =  new FileSystem();
+			var rootDirs = DriveInfo.GetDrives ().Select (d=>fs.DirectoryInfo.New(d.RootDirectory.FullName));
+			treeViewFiles.TreeBuilder = new FileSystemTreeBuilder ();
+			treeViewFiles.AddObjects (rootDirs);
 
 
 			// Determines how to represent objects as strings on the screen
 			// Determines how to represent objects as strings on the screen
-			treeViewFiles.AspectGetter = FileSystemAspectGetter;
+			treeViewFiles.AspectGetter = AspectGetter;
+			
+			_iconProvider.IsOpenGetter = treeViewFiles.IsExpanded;
+		}
 
 
-			treeViewFiles.AddObjects (DriveInfo.GetDrives ().Select (d => d.RootDirectory));
+		private string AspectGetter (IFileSystemInfo f)
+		{
+				return (_iconProvider.GetIconWithOptionalSpace(f) + f.Name).Trim();
 		}
 		}
 
 
 		private void ShowLines ()
 		private void ShowLines ()
@@ -259,7 +298,6 @@ namespace UICatalog.Scenarios {
 			_miPlusMinus.Checked = expand.Value == '+';
 			_miPlusMinus.Checked = expand.Value == '+';
 			_miArrowSymbols.Checked = expand.Value == '>';
 			_miArrowSymbols.Checked = expand.Value == '>';
 			_miNoSymbols.Checked = expand.Value == default;
 			_miNoSymbols.Checked = expand.Value == default;
-			_miUnicodeSymbols.Checked = expand.Value == 'ஹ';
 
 
 			treeViewFiles.Style.ExpandableSymbol = expand;
 			treeViewFiles.Style.ExpandableSymbol = expand;
 			treeViewFiles.Style.CollapseableSymbol = collapse;
 			treeViewFiles.Style.CollapseableSymbol = collapse;
@@ -319,8 +357,8 @@ namespace UICatalog.Scenarios {
 
 
 			if (_miCustomColors.Checked == true) {
 			if (_miCustomColors.Checked == true) {
 				treeViewFiles.ColorGetter = (m) => {
 				treeViewFiles.ColorGetter = (m) => {
-					if (m is DirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
-					if (m is FileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
+					if (m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
+					if (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) return hidden;
 					return null;
 					return null;
 				};
 				};
 			} else {
 			} else {
@@ -336,36 +374,6 @@ namespace UICatalog.Scenarios {
 			treeViewFiles.SetNeedsDisplay ();
 			treeViewFiles.SetNeedsDisplay ();
 		}
 		}
 
 
-		private IEnumerable<FileSystemInfo> GetChildren (FileSystemInfo model)
-		{
-			// If it is a directory it's children are all contained files and dirs
-			if (model is DirectoryInfo d) {
-				try {
-					return d.GetFileSystemInfos ()
-						//show directories first
-						.OrderBy (a => a is DirectoryInfo ? 0 : 1)
-						.ThenBy (b => b.Name);
-				} catch (SystemException) {
-
-					// Access violation or other error getting the file list for directory
-					return Enumerable.Empty<FileSystemInfo> ();
-				}
-			}
-
-			return Enumerable.Empty<FileSystemInfo> (); ;
-		}
-		private string FileSystemAspectGetter (FileSystemInfo model)
-		{
-			if (model is DirectoryInfo d) {
-				return d.Name;
-			}
-			if (model is FileInfo f) {
-				return f.Name;
-			}
-
-			return model.ToString ();
-		}
-
 		private void Quit ()
 		private void Quit ()
 		{
 		{
 			Application.RequestStop ();
 			Application.RequestStop ();

+ 80 - 0
UnitTests/FileServices/FileSystemIconProviderTests.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.FileServicesTests {
+	public class FileSystemIconProviderTests {
+		[Fact]
+		public void FlagsShouldBeMutuallyExclusive()
+		{
+			var p = new FileSystemIconProvider {
+				UseUnicodeCharacters = false,
+				UseNerdIcons = false
+			};
+
+			Assert.False (p.UseUnicodeCharacters);
+			Assert.False (p.UseNerdIcons);
+
+			p.UseUnicodeCharacters = true;
+
+			Assert.True (p.UseUnicodeCharacters);
+			Assert.False (p.UseNerdIcons);
+
+			// Cannot use both nerd and unicode so unicode should have switched off
+			p.UseNerdIcons = true;
+
+			Assert.True (p.UseNerdIcons);
+			Assert.False (p.UseUnicodeCharacters);
+
+			// Cannot use both unicode and nerd so now nerd should have switched off
+			p.UseUnicodeCharacters = true;
+
+			Assert.True (p.UseUnicodeCharacters);
+			Assert.False (p.UseNerdIcons);
+		}
+
+		[Fact]
+		public void TestBasicIcons ()
+		{
+			var p = new FileSystemIconProvider ();
+			var fs = GetMockFileSystem ();
+			
+			Assert.Equal(IsWindows() ? new Rune('\\') : new Rune('/'), p.GetIcon(fs.DirectoryInfo.New(@"c:\")));
+
+			Assert.Equal (new Rune (' '), p.GetIcon (
+				fs.FileInfo.New (GetFileSystemRoot() + @"myfile.txt"))
+				);
+		}
+		private bool IsWindows ()
+		{
+			return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform (System.Runtime.InteropServices.OSPlatform.Windows);
+		}
+
+		private IFileSystem GetMockFileSystem()
+		{
+			string root = GetFileSystemRoot();
+
+			var fileSystem = new MockFileSystem (new Dictionary<string, MockFileData> (), root);
+			
+			fileSystem.AddFile (root+@"myfile.txt", new MockFileData ("Testing is meh."));
+			fileSystem.AddFile (root+@"demo/jQuery.js", new MockFileData ("some js"));
+			fileSystem.AddFile (root+@"demo/mybinary.exe", new MockFileData ("some js"));
+			fileSystem.AddFile (root+@"demo/image.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }));
+
+			var m = (MockDirectoryInfo)fileSystem.DirectoryInfo.New (root + @"demo/subfolder");
+			m.Create ();
+
+			return fileSystem;
+		}
+
+		private string GetFileSystemRoot ()
+		{
+			return IsWindows () ? @"c:\" : "/";
+		}
+	}
+}