From 9d975798579426f31f341a26b028370f793a4292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Mon, 2 Jan 2023 20:00:48 +0100 Subject: [PATCH] More content browser stuff - Seperate AssetHierarchy from EditorContentManager - Make files selectable and context menu --- .../src/EditWindows/ContentBrowserWindow.bf | 168 +++++++++++++--- GlitchyEditor/src/EditorContentManager.bf | 186 +++++++++++------- GlitchyEngine/src/Core/FilePath.bf | 112 +++++++++++ GlitchyEngine/src/Extension/System/IO/Path.bf | 39 ++++ 4 files changed, 398 insertions(+), 107 deletions(-) create mode 100644 GlitchyEngine/src/Core/FilePath.bf create mode 100644 GlitchyEngine/src/Extension/System/IO/Path.bf diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 8a13af0..320e541 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -5,6 +5,7 @@ using GlitchyEngine.Collections; using System.Collections; using GlitchyEngine.Renderer; using GlitchyEngine.Math; +using GlitchyEngine; namespace GlitchyEditor.EditWindows { @@ -15,8 +16,9 @@ namespace GlitchyEditor.EditWindows // TODO: Get from project const String ContentDirectory = "./content"; - //private String _currentDirectory ~ delete _; - private TreeNode _currentDirectoryNode; + private append String _currentDirectory = .(); + + private append String _selectedFile = .(); public static SubTexture2D s_FolderTexture; public static SubTexture2D s_FileTexture; @@ -31,8 +33,12 @@ namespace GlitchyEditor.EditWindows protected override void InternalShow() { _manager.Update(); - - _currentDirectoryNode ??= _manager._assetHierarchy; + + // Make sure we are in an existing directory. + if (!_manager.AssetHierarchy.FileExists(_currentDirectory)) + { + _currentDirectory.Set(_manager.ContentDirectory); + } if(!ImGui.Begin("Content Browser", &_open, .None)) { @@ -40,6 +46,13 @@ namespace GlitchyEditor.EditWindows return; } + // Context menu when clicking on the background. + if (ImGui.BeginPopupContextWindow()) + { + ShowCurrentFolderContextMenu(); + ImGui.EndPopup(); + } + ImGui.Columns(2); DrawDirectorySideBar(); @@ -53,14 +66,26 @@ namespace GlitchyEditor.EditWindows ImGui.End(); } + /// Renders the context menu that is shown when the user right clicks on the background of the file browser. + private void ShowCurrentFolderContextMenu() + { + if (ImGui.MenuItem("Open in file browser...")) + { + Path.OpenFolder(_currentDirectory); + } + } + + /// Renders a sidebar that shows a tree of all directories in the asset folder. private void DrawDirectorySideBar() { - for(var child in _manager._assetHierarchy.Children) + for(var child in _manager.AssetHierarchy.[Friend]_assetHierarchy.Children) { ImGuiPrintEntityTree(child); } } + /// Renders an ImGui tree of all directories in the given tree. + /// @param tree The file hierarchy of which to render all directories. private void ImGuiPrintEntityTree(TreeNode tree) { if (!tree->IsDirectory) @@ -73,7 +98,7 @@ namespace GlitchyEditor.EditWindows if(tree.Children.Count == 0) flags |= .Leaf; - if (tree == _currentDirectoryNode) + if (tree->Path == _currentDirectory) { flags |= .Selected; } @@ -82,11 +107,7 @@ namespace GlitchyEditor.EditWindows if (ImGui.IsItemClicked(.Left)) { - /*if (_currentDirectory != null) - delete _currentDirectory; - - _currentDirectory = new String(tree.Value.Path);*/ - _currentDirectoryNode = tree; + _currentDirectory.Set(tree->Path); } if(isOpen) @@ -104,44 +125,70 @@ namespace GlitchyEditor.EditWindows const Vector2 padding = .(24, 24); + /// Renders the contents of _currentDirectory. private void DrawCurrentDirectory() { - if (_currentDirectoryNode == null) + if (_currentDirectory.IsEmpty) return; ImGui.Style* style = ImGui.GetStyle(); - // TODO: crashes when _currentDirectoryNode was deleted, obviously - float window_visible_x2 = ImGui.GetWindowPos().x + ImGui.GetWindowContentRegionMax().x; - for (var entry in _currentDirectoryNode.Children) + + // Get the node of the current directory. + var currentDirectoryNode = _manager.AssetHierarchy.GetNodeFromPath(_currentDirectory); + + if (currentDirectoryNode case .Err) + { + Log.EngineLogger.Error($"No node exists for {_currentDirectory}."); + ImGui.TextUnformatted("Failed to display contents of directory."); + return; + } + + for (var entry in currentDirectoryNode->Children) { ImGui.PushID(entry->Name); DrawDirectoryItem(entry); - - float last_button_x2 = ImGui.GetItemRectMax().x; - float next_button_x2 = last_button_x2 + style.ItemSpacing.x + DirectoryItemSize.X; // Expected position if next button was on same line - if (entry != _currentDirectoryNode.Children.Back && next_button_x2 < window_visible_x2) + + // X-Coordinate of the right side of the current entry. + float currentButtonRight = ImGui.GetItemRectMax().x; + // Expected right-Coordinate if next entry was on the same line. + float expectedButtonRight = currentButtonRight + style.ItemSpacing.x + DirectoryItemSize.X; + + // If we aren't the last entry and the next button won't fit on the same line we start a new line. + if (entry != currentDirectoryNode->Children.Back && expectedButtonRight < window_visible_x2) ImGui.SameLine(); ImGui.PopID(); } } + /// Renders the button for the given directory item. private void DrawDirectoryItem(TreeNode entry) { ImGui.BeginChild("item", (.)DirectoryItemSize); + if (entry->Path == _selectedFile) + { + var color = ImGui.GetStyleColorVec4(.ButtonHovered); + ImGui.PushStyleColor(.Button, *color); + } + else + { + ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0)); + } + + // TODO: preview images SubTexture2D image = entry->IsDirectory ? s_FolderTexture : s_FileTexture; - ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0)); - ImGui.ImageButton(image, (.)(DirectoryItemSize - padding)); + ImGui.PopStyleColor(); + if (ImGui.BeginDragDropSource()) { - String fullpath = scope $"{_currentDirectoryNode->Path}{Path.DirectorySeparatorChar}{entry->Name}"; + String fullpath = scope String(entry->Path); // TODO: this is dirty if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase)) @@ -152,15 +199,27 @@ namespace GlitchyEditor.EditWindows ImGui.EndDragDropSource(); } + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) + { + if (_selectedFile != entry->Path) + { + _selectedFile.Set(entry->Path); + } + } + if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(.Left)) { EntryDoubleClicked(entry); } - ImGui.PopStyleColor(); - ImGui.TextUnformatted(entry->Name); + if (ImGui.BeginPopupContextWindow()) + { + ShowItemContextMenu(entry); + ImGui.EndPopup(); + } + if (entry->SubAssets?.Count > 0) { // Button for revealing sub assets (e.g. Meshes in 3D-Model) @@ -178,15 +237,9 @@ namespace GlitchyEditor.EditWindows if (ImGui.BeginDragDropSource()) { - String fullpath = Path.InternalCombine(.. scope String(), _currentDirectoryNode->Path, entry->Name); + String fullpath = scope String(entry->Path); fullpath.AppendF($"#{subAsset.Name}"); - //scope $"{_currentDirectoryNode->Path}{Path.DirectorySeparatorChar}{entry->Name}"; - //String fullpath = scope $"{subAsset.Asset.Path}#{subAsset.Name}"; - // TODO: this is dirty - //if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase)) - // fullpath.Remove(0, ContentDirectory.Length); - ImGui.SetDragDropPayload("CONTENT_BROWSER_ITEM", fullpath.CStr(), (.)fullpath.Length, .Once); ImGui.EndDragDropSource(); @@ -195,15 +248,66 @@ namespace GlitchyEditor.EditWindows ImGui.EndPopup(); } + + DeleteItemPopup(entry); ImGui.EndChild(); } + private void DeleteItemPopup(TreeNode fileOrFolder) + { + // Always center this window when appearing + ImGui.Vec2 center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(center, .Appearing, ImGui.Vec2(0.5f, 0.5f)); + + // TODO: fix delete popup + + if (ImGui.BeginPopupModal("Delete?", null, .AlwaysAutoResize)) + { + ImGui.Text($""" + Delete "{fileOrFolder->Name}"? + + + """); + + ImGui.Separator(); + + if (ImGui.Button("Yes", ImGui.Vec2(120, 0))) + { + ImGui.CloseCurrentPopup(); + } + + ImGui.SetItemDefaultFocus(); + ImGui.SameLine(); + + if (ImGui.Button("Cancel", ImGui.Vec2(120, 0))) + { + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + } + + /// Shows the context menu for the given file/folder. + private void ShowItemContextMenu(TreeNode fileOrFolder) + { + if (ImGui.MenuItem("Show in file browser...")) + { + Path.OpenFolderAndSelectItem(fileOrFolder->Path); + } + + if (ImGui.MenuItem("Delete")) + { + ImGui.OpenPopup("Delete?"); + } + } + private void EntryDoubleClicked(TreeNode entry) { if (entry->IsDirectory) { - _currentDirectoryNode = entry; + _currentDirectory.Set(entry->Path); } } } diff --git a/GlitchyEditor/src/EditorContentManager.bf b/GlitchyEditor/src/EditorContentManager.bf index fdec24b..eff9497 100644 --- a/GlitchyEditor/src/EditorContentManager.bf +++ b/GlitchyEditor/src/EditorContentManager.bf @@ -99,93 +99,51 @@ public class Asset public Texture2D PreviewImage ~ _?.ReleaseRef(); } -class EditorContentManager : IContentManager +class AssetHierarchy { - // TODO: Get from workspace - //const String ContentDirectory = "./content"; - FileSystemWatcher fsw ~ { _.StopRaisingEvents(); delete _; }; bool _fileSystemDirty = false; - + internal TreeNode _assetHierarchy = null ~ DeleteTreeAndChildren!(_); - + private append Dictionary> _pathToAssetNode = .(); + private append String _contentDirectory = .(); - public StringView ContentDirectory => _contentDirectory; - - private append List _identifiers = .() ~ _.ClearAndDeleteItems(); + private EditorContentManager _contentManager; - private append Dictionary _loadedAssets = .(); // Check if all resources are unloaded - - public this() + public StringView ContentDirectory { + get => _contentDirectory; + private set + { + _contentDirectory.Clear(); + _contentDirectory.Append(value); + } + } + + public this(EditorContentManager contentManager) + { + _contentManager = contentManager; } public void SetContentDirectory(StringView contentDirectory) { - _contentDirectory.Clear(); - _contentDirectory.Append(contentDirectory); + ContentDirectory = contentDirectory; + _fileSystemDirty = true; SetupFileSystemWatcher(); } - - private TreeNode FileFileNode(StringView filePath) - { - //String relativeFilePath = scope String(filePath.Length); - - //Path.GetRelativePath(filePath, ContentDirectory, relativeFilePath); - - // Having a Path -> AssetNode dictionary would make this a lot easier! - - TreeNode walker = _assetHierarchy; - - Log.EngineLogger.AssertDebug(filePath.StartsWith(walker->Path), "filePath not in walker :("); - - Walking: while (true) - { - for (TreeNode child in walker.Children) - { - if (filePath.StartsWith(child->Path)) - { - walker = child; - - if (walker->Path == filePath) - return walker; - - continue Walking; - } - } - - // If we make it here, no child matched - Runtime.FatalError("Failed to find treeNode for given path"); - //Log.EngineLogger.Assert(false, "Failed to find ") - } - - /*// Check whether the new file is a directory of a file. - if (Directory.Exists(filePath)) - { - - } - else if (File.Exists(filePath)) - { - - } - else - { - Log.EngineLogger.Assert(false, scope $"The given path \"{filePath}\" doesn't exist."); - }*/ - } - + /// Initializes the FSW for the current ContentDirectory and registers the events. private void SetupFileSystemWatcher() { delete fsw; - fsw = new FileSystemWatcher(ContentDirectory); + fsw = new FileSystemWatcher(_contentDirectory); fsw.IncludeSubdirectories = true; fsw.OnChanged.Add(new (filename) => { @@ -208,15 +166,40 @@ class EditorContentManager : IContentManager _fileSystemDirty = true; }); - fsw.OnRenamed.Add(new (newName, oldName) => { + fsw.OnRenamed.Add(new (oldName, newName) => { Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")"); - + _fileSystemDirty = true; + /*String contentFilePath = scope String(); + + Path.InternalCombine(contentFilePath, ContentDirectory, oldName); + + //_fileSystemDirty = true; + TreeNode fileNode = GetNodeFromPath(contentFilePath); + fileNode->*/ }); fsw.StartRaisingEvents(); } + /// Gets the tree node for the given filePath or null, if the file/directory doesn't exist. + /// @param filePath the path for which to return the tree node. + /// @remarks Do not hold a reference to the TreeNode because it can become invalid when the file hierarchy changes. + public Result> GetNodeFromPath(StringView filePath) + { + if (_pathToAssetNode.TryGetValue(filePath, let treeNode)) + { + return treeNode; + } + + return .Err; + } + + public bool FileExists(StringView filePath) + { + return _pathToAssetNode.ContainsKey(filePath); + } + public void Update() { // TODO: do we really need to do this in the update loop? @@ -225,14 +208,7 @@ class EditorContentManager : IContentManager UpdateFiles(); } } - - public IAssetLoader GetDefaultAssetLoader(StringView fileExtension) - { - if (_defaultAssetLoaders.TryGetValue(fileExtension, let value)) - return value; - - return null; - } + /// Rebuilds the asset file hierarchy. private void UpdateFiles() @@ -248,11 +224,13 @@ class EditorContentManager : IContentManager _assetHierarchy->Name = new String("Content"); Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\""); + + _pathToAssetNode.Add(ContentDirectory, _assetHierarchy); } void HandleFile(AssetNode node) { - node.AssetFile = new AssetFile(this, node.Path, node.IsDirectory); + node.AssetFile = new AssetFile(_contentManager, node.Path, node.IsDirectory); } /// Determines the files that belong to the given directory and adds them to the tree. @@ -288,7 +266,8 @@ class EditorContentManager : IContentManager assetNode.IsDirectory = false; treeNode = directory.AddChild(assetNode); - + _pathToAssetNode.Add(assetNode.Path, treeNode); + //GrabSubAssets(node); HandleFile(treeNode.Value); @@ -299,6 +278,17 @@ class EditorContentManager : IContentManager void RemoveOrphanedEntries(TreeNode node) { + /// Removes the node and its children from _pathToAssetNode + void RemoveSubtree(TreeNode tree) + { + _pathToAssetNode.Remove(tree->Path); + + for (var child in tree.Children) + { + RemoveSubtree(child); + } + } + for (TreeNode child in node.Children) { if (!Directory.Exists(child->Path) && !File.Exists(child->Path)) @@ -307,6 +297,8 @@ class EditorContentManager : IContentManager @child.Remove(); + RemoveSubtree(child); + DeleteTreeAndChildren!(child); } } @@ -327,7 +319,9 @@ class EditorContentManager : IContentManager assetNode.Name = new String(); assetNode.IsDirectory = true; Path.GetFileName(assetNode.Path, assetNode.Name); + treeNode = parentNode.AddChild(assetNode); + _pathToAssetNode.Add(assetNode.Path, treeNode); Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\""); } @@ -364,7 +358,49 @@ class EditorContentManager : IContentManager _fileSystemDirty = false; } +} +class EditorContentManager : IContentManager +{ + // TODO: Get from workspace + //const String ContentDirectory = "./content"; + + private append String _contentDirectory = .(); + + public StringView ContentDirectory => _contentDirectory; + + private append List _identifiers = .() ~ _.ClearAndDeleteItems(); + + private append Dictionary _loadedAssets = .(); // Check if all resources are unloaded + + private append AssetHierarchy _assetHierarchy = .(this); + + public AssetHierarchy AssetHierarchy => _assetHierarchy; + + public this() + { + } + + public void SetContentDirectory(StringView contentDirectory) + { + _contentDirectory.Clear(); + _contentDirectory.Append(contentDirectory); + + _assetHierarchy.SetContentDirectory(contentDirectory); + } + + public void Update() + { + _assetHierarchy.Update(); + } + + public IAssetLoader GetDefaultAssetLoader(StringView fileExtension) + { + if (_defaultAssetLoaders.TryGetValue(fileExtension, let value)) + return value; + + return null; + } private append List _supportedExtensions = .() ~ ClearAndDeleteItems!(_); private append List _assetLoaders = .() ~ ClearAndDeleteItems!(_); private append Dictionary _defaultAssetLoaders = .(); diff --git a/GlitchyEngine/src/Core/FilePath.bf b/GlitchyEngine/src/Core/FilePath.bf new file mode 100644 index 0000000..580f239 --- /dev/null +++ b/GlitchyEngine/src/Core/FilePath.bf @@ -0,0 +1,112 @@ +using System; +using System.IO; + +namespace GlitchyEngine.Core; + +// TODO: make usable +class FilePath : IHashable +{ + append String _path = .(); + + public bool IsRooted => Path.IsPathRooted(_path); + + public this() + { + + } + + public this(StringView path) + { + Set(path); + } + + public static implicit operator StringView(FilePath filePath) => filePath._path; + + /// @param fixDirectorySeperators If true all alternative directory seperators will be replaced by the primary seperator. + /// @param resolveRelativeDirectories If true relative directories ('.' and '..') will be removed from the path. + public enum CanonicalizationFlags + { + FixDirectorySeperators = 1, + ResolveRelativeDirectories = _ << 1, + MakeFullPath = _ << 1 + } + + public void Set(StringView path, CanonicalizationFlags canonicalizationFlags = .FixDirectorySeperators | .ResolveRelativeDirectories) + { + _path.Append(path); + Canonicalize(canonicalizationFlags); + } + + /// Converts the path to a canonicalized path. + public void Canonicalize(CanonicalizationFlags canonicalizationFlags = .FixDirectorySeperators | .ResolveRelativeDirectories) + { + if (canonicalizationFlags.HasFlag(.FixDirectorySeperators)) + { + FixDirectorySeperators(); + } + + if (canonicalizationFlags.HasFlag(.ResolveRelativeDirectories)) + { + ResolveRelativeDirectories(); + } + + if (canonicalizationFlags.HasFlag(.MakeFullPath)) + { + MakeFullPath(); + } + } + + public void MakeFullPath() + { + if (IsRooted) + return; + + String buffer = scope String(Path.[Friend]MaxPath); + + Path.GetFullPath(_path, buffer); + + _path..Clear().Append(buffer); + } + + public void FixDirectorySeperators() + { + _path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + public void ResolveRelativeDirectories() + { + // find . + // find entire entry name + // remove entry name (if its only .) + + /*for (char32 c in _path.DecodedChars) + { + if (c == '.') + { + } + } + + for (StringView component in _path.Split(Path.DirectorySeparatorChar)) + { + if (component == ".") + { + // . can be removed without replacement + } + else if (component == "..") + { + // .. can only be removed when not at the start of after another .. + + // e.g. "../foo" and "../../foo" can't be changed + // but "foo/.." can become "foo" + // "foo/../.." can become ".." + } + }*/ + } + + public void Append(StringView newPath) + { + + } + + public int GetHashCode() => _path.GetHashCode(); +} \ No newline at end of file diff --git a/GlitchyEngine/src/Extension/System/IO/Path.bf b/GlitchyEngine/src/Extension/System/IO/Path.bf new file mode 100644 index 0000000..01c7e4d --- /dev/null +++ b/GlitchyEngine/src/Extension/System/IO/Path.bf @@ -0,0 +1,39 @@ +using System.Diagnostics; +namespace System.IO; + +extension Path +{ + /// Opens the file browser and selects the specified file. + /// @param path The path of the file to select. + public static void OpenFolderAndSelectItem(String path) + { + String fullPath = scope String(256); + Path.GetFullPath(path, fullPath); + +#if BF_PLATFORM_WINDOWS + ProcessStartInfo processInfo = scope .(); + processInfo.SetFileNameAndArguments(scope $"explorer /select,\"{fullPath}\""); + + scope SpawnedProcess().Start(processInfo); +#else + Runtime.NotImplemented(); +#endif + } + + /// Opens the file browser in the given directory. + /// @param directory The directory to show in the file browser. + public static void OpenFolder(String directory) + { + String fullPath = scope String(256); + Path.GetFullPath(directory, fullPath); + +#if BF_PLATFORM_WINDOWS + ProcessStartInfo processInfo = scope .(); + processInfo.SetFileNameAndArguments(scope $"explorer \"{fullPath}\""); + + scope SpawnedProcess().Start(processInfo); +#else + Runtime.NotImplemented(); +#endif + } +} \ No newline at end of file