diff --git a/Source/AGS.API/UI/Controls/Tree/ITreeNodeView.cs b/Source/AGS.API/UI/Controls/Tree/ITreeNodeView.cs
index 09b88feae..a21d3e8c0 100644
--- a/Source/AGS.API/UI/Controls/Tree/ITreeNodeView.cs
+++ b/Source/AGS.API/UI/Controls/Tree/ITreeNodeView.cs
@@ -37,5 +37,11 @@ public interface ITreeNodeView
///
/// The horizontal panel.
IPanel HorizontalPanel { get; }
+
+ ///
+ /// An event that can be triggered to notify the tree view that this node needs to be re-rendered on screen.
+ ///
+ /// The on refresh display needed event.
+ IBlockingEvent OnRefreshDisplayNeeded { get; }
}
}
diff --git a/Source/Editor/AGS.Editor/AGSEditor.cs b/Source/Editor/AGS.Editor/AGSEditor.cs
index 2bc3b1cb7..72d00598f 100644
--- a/Source/Editor/AGS.Editor/AGSEditor.cs
+++ b/Source/Editor/AGS.Editor/AGSEditor.cs
@@ -94,8 +94,9 @@ public static void SetupResolver()
Resolver.Override(resolver => resolver.Builder.RegisterType().SingleInstance());
Resolver.Override(resolver => resolver.Builder.RegisterType().SingleInstance());
Resolver.Override(resolver => resolver.Builder.RegisterAssemblyTypes(typeof(GameLoader).Assembly).
- Except().Except().Except().AsImplementedInterfaces().ExternallyOwned());
+ Except().Except().AsImplementedInterfaces().ExternallyOwned());
Resolver.Override(resolver => { var editor = new AGSEditor(resolver); resolver.Builder.RegisterInstance(editor); });
+ Resolver.Override(resolver => resolver.RegisterType());
}
public static IEditorPlatform Platform { get; set; }
diff --git a/Source/Editor/AGS.Editor/Components/FileSelector/FolderNodeViewProvider.cs b/Source/Editor/AGS.Editor/Components/FileSelector/FolderNodeViewProvider.cs
new file mode 100644
index 000000000..eb2b10807
--- /dev/null
+++ b/Source/Editor/AGS.Editor/Components/FileSelector/FolderNodeViewProvider.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Linq;
+using System.Threading;
+using AGS.API;
+using AGS.Engine;
+
+namespace AGS.Editor
+{
+ public class FolderNodeViewProvider: ITreeNodeViewProvider
+ {
+ private readonly ITreeNodeViewProvider _inner;
+ private readonly IGameFactory _factory;
+ private const string FOLDER_HOVERED = "FolderHovered";
+
+ private static int _nextNodeId;
+
+ public FolderNodeViewProvider(ITreeNodeViewProvider inner, IGameFactory factory)
+ {
+ _inner = inner;
+ _factory = factory;
+ }
+
+ public void BeforeDisplayingNode(ITreeStringNode item, ITreeNodeView nodeView, bool isCollapsed, bool isHovered, bool isSelected)
+ {
+ isHovered |= item.Properties.Bools.GetValue(FOLDER_HOVERED);
+ _inner.BeforeDisplayingNode(item, nodeView, isCollapsed, isHovered, isSelected);
+ var folderIcon = (ILabel) nodeView.ExpandButton.TreeNode.Children.First(c => c.ID.StartsWith("FolderIcon", StringComparison.InvariantCulture));
+ folderIcon.Text = isSelected ? FontIcons.FolderOpen : FontIcons.Folder;
+ folderIcon.TextConfig.Brush = isHovered ? GameViewColors.HoveredTextBrush : GameViewColors.TextBrush;
+ }
+
+ public ITreeNodeView CreateNode(ITreeStringNode node, IRenderLayer layer, IObject parent)
+ {
+ var view = _inner.CreateNode(node, layer, parent);
+ int nodeId = Interlocked.Increment(ref _nextNodeId);
+ var folderIcon = _factory.UI.GetLabel($"FolderIcon_{node.Text}_{nodeId}", "", 25f, 25f, 15f, 0f, view.ExpandButton, AGSTextConfig.Clone(FontIcons.IconConfig));
+ folderIcon.Text = FontIcons.Folder;
+ folderIcon.IsPixelPerfect = false;
+ folderIcon.Enabled = true;
+ folderIcon.MouseEnter.Subscribe(() =>
+ {
+ node.Properties.Bools.SetValue(FOLDER_HOVERED, true);
+ view.OnRefreshDisplayNeeded.Invoke();
+ });
+ folderIcon.MouseLeave.Subscribe(() =>
+ {
+ node.Properties.Bools.SetValue(FOLDER_HOVERED, false);
+ view.OnRefreshDisplayNeeded.Invoke();
+ });
+ folderIcon.MouseClicked.Subscribe(async (args) => await view.TreeItem.MouseClicked.InvokeAsync(args));
+ return view;
+ }
+ }
+}
diff --git a/Source/Editor/AGS.Editor/Components/FileSelector/FolderTree.cs b/Source/Editor/AGS.Editor/Components/FileSelector/FolderTree.cs
new file mode 100644
index 000000000..b58cfab46
--- /dev/null
+++ b/Source/Editor/AGS.Editor/Components/FileSelector/FolderTree.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using AGS.API;
+using AGS.Engine;
+
+namespace AGS.Editor
+{
+ [RequiredComponent(typeof(ITreeViewComponent))]
+ public class FolderTree: AGSComponent
+ {
+ private ITreeViewComponent _treeView;
+ private string _defaultFolder;
+ private readonly IDevice _device;
+ private Folder _root;
+ private readonly IGameFactory _factory;
+
+ public FolderTree(IDevice device, IGameFactory factory)
+ {
+ _device = device;
+ _factory = factory;
+ }
+
+ public string DefaultFolder
+ {
+ get => _defaultFolder;
+ set
+ {
+ _defaultFolder = value;
+ buildTreeModel();
+ }
+ }
+
+ public override void Init()
+ {
+ base.Init();
+ Entity.Bind(c => { _treeView = c; configureTreeUI(); refreshTreeUI(); }, _ => _treeView = null);
+ }
+
+ private async void buildTreeModel()
+ {
+ _root = await buildTreeModel(DefaultFolder);
+ refreshTreeUI();
+ }
+
+ private async Task buildTreeModel(string path)
+ {
+ var dirs = (await Task.Run(() => _device.FileSystem.GetDirectories(path))).ToArray();
+ List folders = new List(dirs.Length);
+ foreach (var dir in dirs)
+ {
+ folders.Add(await buildTreeModel(dir));
+ }
+ return new Folder(Path.GetFileName(path), path, folders.ToArray());
+ }
+
+ private void configureTreeUI()
+ {
+ var tree = _treeView;
+ if (tree == null) return;
+ tree.NodeViewProvider = new FolderNodeViewProvider(tree.NodeViewProvider, _factory);
+ }
+
+ private void refreshTreeUI()
+ {
+ var root = _root;
+ var tree = _treeView;
+ if (root == null || tree == null) return;
+
+ tree.Tree = _root.ToNode(GameViewColors.ButtonTextConfig.Font);
+ tree.Expand(tree.Tree);
+ }
+
+ private class Folder
+ {
+ public Folder(string name, string fullPath, Folder[] folders)
+ {
+ Name = name;
+ FullPath = fullPath;
+ Folders = folders;
+ }
+
+ public string FullPath { get; }
+ public string Name { get; }
+ public Folder[] Folders { get; }
+
+ public ITreeStringNode ToNode(IFont font)
+ {
+ AGSTreeStringNode node = new AGSTreeStringNode(Name, font);
+ if (Folders.Length > 0)
+ {
+ node.TreeNode.AddChildren(Folders.Select(f => f.ToNode(font)).ToList());
+ }
+ return node;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Editor/AGS.Editor/Skins/FontIcons.cs b/Source/Editor/AGS.Editor/Skins/FontIcons.cs
index 1f741e78d..a1e06d42e 100644
--- a/Source/Editor/AGS.Editor/Skins/FontIcons.cs
+++ b/Source/Editor/AGS.Editor/Skins/FontIcons.cs
@@ -60,5 +60,8 @@ public static void Init(IFontFactory fontLoader)
public const string RadioUnchecked = "\uf111";
public const string RadioChecked = "\uf192";
+
+ public const string Folder = "\uf07b";
+ public const string FolderOpen = "\uf07c";
}
-}
+}
\ No newline at end of file
diff --git a/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeNodeView.cs b/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeNodeView.cs
index e7c456e32..94dba6435 100644
--- a/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeNodeView.cs
+++ b/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeNodeView.cs
@@ -11,16 +11,19 @@ public AGSTreeNodeView(IUIControl treeItem, IButton expandButton, IPanel parentP
ParentPanel = parentPanel;
VerticalPanel = verticalPanel;
HorizontalPanel = horizontalPanel;
+ OnRefreshDisplayNeeded = new AGSEvent();
}
- public IUIControl TreeItem { get; private set; }
+ public IUIControl TreeItem { get; }
- public IButton ExpandButton { get; private set; }
+ public IButton ExpandButton { get; }
- public IPanel ParentPanel { get; private set; }
+ public IPanel ParentPanel { get; }
- public IPanel VerticalPanel { get; private set; }
+ public IPanel VerticalPanel { get; }
- public IPanel HorizontalPanel { get; private set; }
+ public IPanel HorizontalPanel { get; }
+
+ public IBlockingEvent OnRefreshDisplayNeeded { get; }
}
}
diff --git a/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeViewComponent.cs b/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeViewComponent.cs
index 3dad7d1c1..ac8b00e9d 100644
--- a/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeViewComponent.cs
+++ b/Source/Engine/AGS.Engine/UI/Controls/Tree/AGSTreeViewComponent.cs
@@ -408,6 +408,7 @@ public void Dispose()
view.TreeItem.MouseEnter.Unsubscribe(onMouseEnterNode);
view.TreeItem.MouseLeave.Unsubscribe(onMouseLeaveNode);
view.TreeItem.MouseClicked.Unsubscribe(onItemSelected);
+ view.OnRefreshDisplayNeeded.Unsubscribe(RefreshDisplay);
var expandButton = view.ExpandButton;
if (expandButton != null)
{
@@ -576,6 +577,7 @@ private void initView()
view.TreeItem.MouseEnter.Subscribe(onMouseEnterNode);
view.TreeItem.MouseLeave.Subscribe(onMouseLeaveNode);
view.TreeItem.MouseClicked.Subscribe(onItemSelected);
+ view.OnRefreshDisplayNeeded.Subscribe(RefreshDisplay);
var expandButton = view.ExpandButton;
if (expandButton != null)
{