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) {