More content browser stuff

- Seperate AssetHierarchy from EditorContentManager
- Make files selectable and context menu
This commit is contained in:
Simon Lübeß
2023-01-02 20:00:48 +01:00
parent fdf680508d
commit 9d97579857
4 changed files with 398 additions and 107 deletions
@@ -5,6 +5,7 @@ using GlitchyEngine.Collections;
using System.Collections; using System.Collections;
using GlitchyEngine.Renderer; using GlitchyEngine.Renderer;
using GlitchyEngine.Math; using GlitchyEngine.Math;
using GlitchyEngine;
namespace GlitchyEditor.EditWindows namespace GlitchyEditor.EditWindows
{ {
@@ -15,8 +16,9 @@ namespace GlitchyEditor.EditWindows
// TODO: Get from project // TODO: Get from project
const String ContentDirectory = "./content"; const String ContentDirectory = "./content";
//private String _currentDirectory ~ delete _; private append String _currentDirectory = .();
private TreeNode<AssetNode> _currentDirectoryNode;
private append String _selectedFile = .();
public static SubTexture2D s_FolderTexture; public static SubTexture2D s_FolderTexture;
public static SubTexture2D s_FileTexture; public static SubTexture2D s_FileTexture;
@@ -31,8 +33,12 @@ namespace GlitchyEditor.EditWindows
protected override void InternalShow() protected override void InternalShow()
{ {
_manager.Update(); _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)) if(!ImGui.Begin("Content Browser", &_open, .None))
{ {
@@ -40,6 +46,13 @@ namespace GlitchyEditor.EditWindows
return; return;
} }
// Context menu when clicking on the background.
if (ImGui.BeginPopupContextWindow())
{
ShowCurrentFolderContextMenu();
ImGui.EndPopup();
}
ImGui.Columns(2); ImGui.Columns(2);
DrawDirectorySideBar(); DrawDirectorySideBar();
@@ -53,14 +66,26 @@ namespace GlitchyEditor.EditWindows
ImGui.End(); 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() private void DrawDirectorySideBar()
{ {
for(var child in _manager._assetHierarchy.Children) for(var child in _manager.AssetHierarchy.[Friend]_assetHierarchy.Children)
{ {
ImGuiPrintEntityTree(child); 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<AssetNode> tree) private void ImGuiPrintEntityTree(TreeNode<AssetNode> tree)
{ {
if (!tree->IsDirectory) if (!tree->IsDirectory)
@@ -73,7 +98,7 @@ namespace GlitchyEditor.EditWindows
if(tree.Children.Count == 0) if(tree.Children.Count == 0)
flags |= .Leaf; flags |= .Leaf;
if (tree == _currentDirectoryNode) if (tree->Path == _currentDirectory)
{ {
flags |= .Selected; flags |= .Selected;
} }
@@ -82,11 +107,7 @@ namespace GlitchyEditor.EditWindows
if (ImGui.IsItemClicked(.Left)) if (ImGui.IsItemClicked(.Left))
{ {
/*if (_currentDirectory != null) _currentDirectory.Set(tree->Path);
delete _currentDirectory;
_currentDirectory = new String(tree.Value.Path);*/
_currentDirectoryNode = tree;
} }
if(isOpen) if(isOpen)
@@ -104,44 +125,70 @@ namespace GlitchyEditor.EditWindows
const Vector2 padding = .(24, 24); const Vector2 padding = .(24, 24);
/// Renders the contents of _currentDirectory.
private void DrawCurrentDirectory() private void DrawCurrentDirectory()
{ {
if (_currentDirectoryNode == null) if (_currentDirectory.IsEmpty)
return; return;
ImGui.Style* style = ImGui.GetStyle(); ImGui.Style* style = ImGui.GetStyle();
// TODO: crashes when _currentDirectoryNode was deleted, obviously
float window_visible_x2 = ImGui.GetWindowPos().x + ImGui.GetWindowContentRegionMax().x; 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); ImGui.PushID(entry->Name);
DrawDirectoryItem(entry); DrawDirectoryItem(entry);
float last_button_x2 = ImGui.GetItemRectMax().x; // X-Coordinate of the right side of the current entry.
float next_button_x2 = last_button_x2 + style.ItemSpacing.x + DirectoryItemSize.X; // Expected position if next button was on same line float currentButtonRight = ImGui.GetItemRectMax().x;
if (entry != _currentDirectoryNode.Children.Back && next_button_x2 < window_visible_x2) // 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.SameLine();
ImGui.PopID(); ImGui.PopID();
} }
} }
/// Renders the button for the given directory item.
private void DrawDirectoryItem(TreeNode<AssetNode> entry) private void DrawDirectoryItem(TreeNode<AssetNode> entry)
{ {
ImGui.BeginChild("item", (.)DirectoryItemSize); 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; SubTexture2D image = entry->IsDirectory ? s_FolderTexture : s_FileTexture;
ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0));
ImGui.ImageButton(image, (.)(DirectoryItemSize - padding)); ImGui.ImageButton(image, (.)(DirectoryItemSize - padding));
ImGui.PopStyleColor();
if (ImGui.BeginDragDropSource()) if (ImGui.BeginDragDropSource())
{ {
String fullpath = scope $"{_currentDirectoryNode->Path}{Path.DirectorySeparatorChar}{entry->Name}"; String fullpath = scope String(entry->Path);
// TODO: this is dirty // TODO: this is dirty
if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase)) if (fullpath.StartsWith(ContentDirectory, .OrdinalIgnoreCase))
@@ -152,15 +199,27 @@ namespace GlitchyEditor.EditWindows
ImGui.EndDragDropSource(); ImGui.EndDragDropSource();
} }
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left))
{
if (_selectedFile != entry->Path)
{
_selectedFile.Set(entry->Path);
}
}
if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(.Left)) if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(.Left))
{ {
EntryDoubleClicked(entry); EntryDoubleClicked(entry);
} }
ImGui.PopStyleColor();
ImGui.TextUnformatted(entry->Name); ImGui.TextUnformatted(entry->Name);
if (ImGui.BeginPopupContextWindow())
{
ShowItemContextMenu(entry);
ImGui.EndPopup();
}
if (entry->SubAssets?.Count > 0) if (entry->SubAssets?.Count > 0)
{ {
// Button for revealing sub assets (e.g. Meshes in 3D-Model) // Button for revealing sub assets (e.g. Meshes in 3D-Model)
@@ -178,15 +237,9 @@ namespace GlitchyEditor.EditWindows
if (ImGui.BeginDragDropSource()) if (ImGui.BeginDragDropSource())
{ {
String fullpath = Path.InternalCombine(.. scope String(), _currentDirectoryNode->Path, entry->Name); String fullpath = scope String(entry->Path);
fullpath.AppendF($"#{subAsset.Name}"); 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.SetDragDropPayload("CONTENT_BROWSER_ITEM", fullpath.CStr(), (.)fullpath.Length, .Once);
ImGui.EndDragDropSource(); ImGui.EndDragDropSource();
@@ -195,15 +248,66 @@ namespace GlitchyEditor.EditWindows
ImGui.EndPopup(); ImGui.EndPopup();
} }
DeleteItemPopup(entry);
ImGui.EndChild(); ImGui.EndChild();
} }
private void DeleteItemPopup(TreeNode<AssetNode> 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<AssetNode> fileOrFolder)
{
if (ImGui.MenuItem("Show in file browser..."))
{
Path.OpenFolderAndSelectItem(fileOrFolder->Path);
}
if (ImGui.MenuItem("Delete"))
{
ImGui.OpenPopup("Delete?");
}
}
private void EntryDoubleClicked(TreeNode<AssetNode> entry) private void EntryDoubleClicked(TreeNode<AssetNode> entry)
{ {
if (entry->IsDirectory) if (entry->IsDirectory)
{ {
_currentDirectoryNode = entry; _currentDirectory.Set(entry->Path);
} }
} }
} }
+111 -75
View File
@@ -99,93 +99,51 @@ public class Asset
public Texture2D PreviewImage ~ _?.ReleaseRef(); public Texture2D PreviewImage ~ _?.ReleaseRef();
} }
class EditorContentManager : IContentManager class AssetHierarchy
{ {
// TODO: Get from workspace
//const String ContentDirectory = "./content";
FileSystemWatcher fsw ~ { FileSystemWatcher fsw ~ {
_.StopRaisingEvents(); _.StopRaisingEvents();
delete _; delete _;
}; };
bool _fileSystemDirty = false; bool _fileSystemDirty = false;
internal TreeNode<AssetNode> _assetHierarchy = null ~ DeleteTreeAndChildren!(_); internal TreeNode<AssetNode> _assetHierarchy = null ~ DeleteTreeAndChildren!(_);
private append Dictionary<StringView, TreeNode<AssetNode>> _pathToAssetNode = .();
private append String _contentDirectory = .(); private append String _contentDirectory = .();
public StringView ContentDirectory => _contentDirectory; private EditorContentManager _contentManager;
private append List<String> _identifiers = .() ~ _.ClearAndDeleteItems();
private append Dictionary<StringView, IRefCounted> _loadedAssets = .(); // Check if all resources are unloaded public StringView ContentDirectory
public this()
{ {
get => _contentDirectory;
private set
{
_contentDirectory.Clear();
_contentDirectory.Append(value);
}
}
public this(EditorContentManager contentManager)
{
_contentManager = contentManager;
} }
public void SetContentDirectory(StringView contentDirectory) public void SetContentDirectory(StringView contentDirectory)
{ {
_contentDirectory.Clear(); ContentDirectory = contentDirectory;
_contentDirectory.Append(contentDirectory);
_fileSystemDirty = true; _fileSystemDirty = true;
SetupFileSystemWatcher(); SetupFileSystemWatcher();
} }
private TreeNode<AssetNode> 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<AssetNode> walker = _assetHierarchy;
Log.EngineLogger.AssertDebug(filePath.StartsWith(walker->Path), "filePath not in walker :(");
Walking: while (true)
{
for (TreeNode<AssetNode> 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. /// Initializes the FSW for the current ContentDirectory and registers the events.
private void SetupFileSystemWatcher() private void SetupFileSystemWatcher()
{ {
delete fsw; delete fsw;
fsw = new FileSystemWatcher(ContentDirectory); fsw = new FileSystemWatcher(_contentDirectory);
fsw.IncludeSubdirectories = true; fsw.IncludeSubdirectories = true;
fsw.OnChanged.Add(new (filename) => { fsw.OnChanged.Add(new (filename) => {
@@ -208,15 +166,40 @@ class EditorContentManager : IContentManager
_fileSystemDirty = true; _fileSystemDirty = true;
}); });
fsw.OnRenamed.Add(new (newName, oldName) => { fsw.OnRenamed.Add(new (oldName, newName) => {
Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")"); Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")");
_fileSystemDirty = true; _fileSystemDirty = true;
/*String contentFilePath = scope String();
Path.InternalCombine(contentFilePath, ContentDirectory, oldName);
//_fileSystemDirty = true;
TreeNode<AssetNode> fileNode = GetNodeFromPath(contentFilePath);
fileNode->*/
}); });
fsw.StartRaisingEvents(); 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<TreeNode<AssetNode>> 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() public void Update()
{ {
// TODO: do we really need to do this in the update loop? // TODO: do we really need to do this in the update loop?
@@ -225,14 +208,7 @@ class EditorContentManager : IContentManager
UpdateFiles(); UpdateFiles();
} }
} }
public IAssetLoader GetDefaultAssetLoader(StringView fileExtension)
{
if (_defaultAssetLoaders.TryGetValue(fileExtension, let value))
return value;
return null;
}
/// Rebuilds the asset file hierarchy. /// Rebuilds the asset file hierarchy.
private void UpdateFiles() private void UpdateFiles()
@@ -248,11 +224,13 @@ class EditorContentManager : IContentManager
_assetHierarchy->Name = new String("Content"); _assetHierarchy->Name = new String("Content");
Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\""); Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\"");
_pathToAssetNode.Add(ContentDirectory, _assetHierarchy);
} }
void HandleFile(AssetNode node) 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. /// 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; assetNode.IsDirectory = false;
treeNode = directory.AddChild(assetNode); treeNode = directory.AddChild(assetNode);
_pathToAssetNode.Add(assetNode.Path, treeNode);
//GrabSubAssets(node); //GrabSubAssets(node);
HandleFile(treeNode.Value); HandleFile(treeNode.Value);
@@ -299,6 +278,17 @@ class EditorContentManager : IContentManager
void RemoveOrphanedEntries(TreeNode<AssetNode> node) void RemoveOrphanedEntries(TreeNode<AssetNode> node)
{ {
/// Removes the node and its children from _pathToAssetNode
void RemoveSubtree(TreeNode<AssetNode> tree)
{
_pathToAssetNode.Remove(tree->Path);
for (var child in tree.Children)
{
RemoveSubtree(child);
}
}
for (TreeNode<AssetNode> child in node.Children) for (TreeNode<AssetNode> child in node.Children)
{ {
if (!Directory.Exists(child->Path) && !File.Exists(child->Path)) if (!Directory.Exists(child->Path) && !File.Exists(child->Path))
@@ -307,6 +297,8 @@ class EditorContentManager : IContentManager
@child.Remove(); @child.Remove();
RemoveSubtree(child);
DeleteTreeAndChildren!(child); DeleteTreeAndChildren!(child);
} }
} }
@@ -327,7 +319,9 @@ class EditorContentManager : IContentManager
assetNode.Name = new String(); assetNode.Name = new String();
assetNode.IsDirectory = true; assetNode.IsDirectory = true;
Path.GetFileName(assetNode.Path, assetNode.Name); Path.GetFileName(assetNode.Path, assetNode.Name);
treeNode = parentNode.AddChild(assetNode); treeNode = parentNode.AddChild(assetNode);
_pathToAssetNode.Add(assetNode.Path, treeNode);
Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\""); Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\"");
} }
@@ -364,7 +358,49 @@ class EditorContentManager : IContentManager
_fileSystemDirty = false; _fileSystemDirty = false;
} }
}
class EditorContentManager : IContentManager
{
// TODO: Get from workspace
//const String ContentDirectory = "./content";
private append String _contentDirectory = .();
public StringView ContentDirectory => _contentDirectory;
private append List<String> _identifiers = .() ~ _.ClearAndDeleteItems();
private append Dictionary<StringView, IRefCounted> _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<String> _supportedExtensions = .() ~ ClearAndDeleteItems!(_); private append List<String> _supportedExtensions = .() ~ ClearAndDeleteItems!(_);
private append List<IAssetLoader> _assetLoaders = .() ~ ClearAndDeleteItems!(_); private append List<IAssetLoader> _assetLoaders = .() ~ ClearAndDeleteItems!(_);
private append Dictionary<StringView, IAssetLoader> _defaultAssetLoaders = .(); private append Dictionary<StringView, IAssetLoader> _defaultAssetLoaders = .();
+112
View File
@@ -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();
}
@@ -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
}
}