diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 9f499a0..9a8bfc3 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -18,19 +18,22 @@ namespace GlitchyEditor.EditWindows { private String _name ~ delete _; private String _defaultFileName ~ delete _; + private String _fileExtension ~ delete _; private CreateAssetFunc _doCreateAsset ~ delete _; public delegate void CreateAssetFunc(StringView outputPath); public StringView Name => _name; public StringView DefaultFileName => _defaultFileName; + public StringView FileExtension => _fileExtension; public CreateAssetFunc CreateAsset => _doCreateAsset; - public this(StringView name, StringView defaultFileName, CreateAssetFunc createAsset) + public this(StringView name, StringView defaultFileName, StringView fileExtension, CreateAssetFunc createAsset) { _name = new String(name); _defaultFileName = new String(defaultFileName); + _fileExtension = new String(fileExtension); _doCreateAsset = createAsset; } } @@ -59,6 +62,10 @@ namespace GlitchyEditor.EditWindows public StringView SelectedFile => _selectedFile; + char8[256] _newFileName = .(); + bool _showNewFile = false; + AssetCreator _newFileCreator = null; + public this(EditorContentManager contentManager) { _manager = contentManager; @@ -106,6 +113,7 @@ namespace GlitchyEditor.EditWindows bool alt_pressed = ImGui.GetIO().KeyAlt; + // Table with two columns, one for Directory-Sidebar and one for File-Browser if (ImGui.BeginTable("table", 2, .BordersInnerV | .Resizable | .Reorderable | .NoPadOuterX)) { if (alt_pressed) @@ -178,6 +186,13 @@ namespace GlitchyEditor.EditWindows ImGui.Separator(); ShowCreateContextMenu(_creatorTree); + + ImGui.Separator(); + + if (ImGui.MenuItem("Open C# Project...")) + { + // TODO + } } /// Shows the menu for the given asset creator tree. @@ -187,7 +202,7 @@ namespace GlitchyEditor.EditWindows { if (ImGui.MenuItem(subtree->Name.ToScopeCStr!())) { - CreateAsset(subtree->Creator); + ShowNewFile(subtree->Creator); } } else @@ -204,8 +219,24 @@ namespace GlitchyEditor.EditWindows } } + /// Shows a dummy file in the asset browser where the user can enter a file name and create an asset. + private void ShowNewFile(AssetCreator creator) + { + _newFileCreator = creator; + creator.DefaultFileName.CopyTo(_newFileName); + _showNewFile = true; + } + + /// Hides the dummy file. + private void HideNewFile() + { + _newFileCreator = null; + _showNewFile = false; + _newFileName = .(); + } + /// Creates a new asset with the given creator - private void CreateAsset(AssetCreator creator) + private void CreateAsset() { if (!Directory.Exists(_currentDirectory)) { @@ -214,23 +245,29 @@ namespace GlitchyEditor.EditWindows } String currentFile = scope String(); - Path.Combine(currentFile, _currentDirectory, creator.DefaultFileName); - - String fileExtension = scope String(); - Path.GetExtension(currentFile, fileExtension); + Path.Combine(currentFile, _currentDirectory, StringView(&_newFileName)); - StringView fileWithoutExtension = currentFile[0...^(fileExtension.Length + 1)]; + // Add file extension, if necessary + if (!currentFile.EndsWith(_newFileCreator.FileExtension)) + { + currentFile.Append(_newFileCreator.FileExtension); + } + StringView fileWithoutExtension = currentFile[0...^(_newFileCreator.FileExtension.Length + 1)]; + + // If the file already exists find a unused number to attach to the end. int i = 0; while (File.Exists(currentFile)) { i++; currentFile.Set(fileWithoutExtension); currentFile.AppendF($"({i})"); - currentFile.Append(fileExtension); + currentFile.Append(_newFileCreator.FileExtension); } - creator.CreateAsset(currentFile); + _newFileCreator.CreateAsset(currentFile); + + HideNewFile(); } /// Renders a sidebar that shows a tree of all directories in the asset folder. @@ -367,6 +404,15 @@ namespace GlitchyEditor.EditWindows ImGui.PopID(); } + + if (_showNewFile) + { + ImGui.PushID("New File"); + + ShowNewFile(); + + ImGui.PopID(); + } } float _zoom = 1.0f; @@ -600,6 +646,59 @@ namespace GlitchyEditor.EditWindows ImGui.AttachTooltip(entry->Name); } + /// Renders the button for the given directory item. + private void ShowNewFile() + { + ImGui.PushStyleVar(.WindowPadding, .(0, 0)); + ImGui.PushStyleVar(.FramePadding, .(0, 0)); + ImGui.PushStyleVar(.ItemSpacing, .(0, 0)); + + defer { ImGui.PopStyleVar(3); } + + float2 DirectoryItemSize = IconSize + (float2)ImGui.GetStyle().WindowPadding * 2 + float2(0, ImGui.GetFontSize()); + + ImGui.BeginChild("item", (.)DirectoryItemSize, false, .NoScrollbar); + + ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0)); + + // TODO: preview images + SubTexture2D image = s_FileTexture; + + ImGui.ImageButton("FileImage", image, (.)IconSize); + + ImGui.PopStyleColor(); + + ImGui.PushItemWidth((.)IconSize.X); + + ImGui.InputText("##newFileNameBox", &_newFileName, _newFileName.Count - 1, .AutoSelectAll); + + ImGui.PopItemWidth(); + + ImGui.SetKeyboardFocusHere(-1); + + void CreateFile() + { + CreateAsset(); + } + + if (ImGui.IsKeyDown(.Escape)) + { + // Abort + HideNewFile(); + } + else if (ImGui.IsItemDeactivatedAfterEdit()) + { + CreateFile(); + } + else if (ImGui.IsItemDeactivated()) + { + // Abort + HideNewFile(); + } + + ImGui.EndChild(); + } + private void DeleteItemPopup(TreeNode fileOrFolder) { // Always center this window when appearing diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index bde8a8a..3ef2266 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -144,7 +144,7 @@ namespace GlitchyEditor private void RegisterAssetCreators() { - Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Folder", "New Folder", new (path) => + Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Folder", "New Folder", "", new (path) => { if (Directory.CreateDirectory(path) case .Err(let error)) { @@ -152,7 +152,7 @@ namespace GlitchyEditor } })); - Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Scene", "New Scene.scene", new (path) => + Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Scene", "New Scene", ".scene", new (path) => { using (Scene newScene = CreateNewScene()) { @@ -160,13 +160,104 @@ namespace GlitchyEditor } })); - Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Material", "New Material.mat", new (path) => + Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Material", "New Material", ".mat", new (path) => { using (Material newMaterial = new Material()) { _contentManager.SaveAssetToFile(newMaterial, path); } })); + + Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Script", "New Entity", ".cs", new (path) => + { + String templatePath = scope .(); + Directory.GetCurrentDirectory(templatePath); + Path.Combine(templatePath, "Resources/Templates/ScriptTemplate.cs"); + + String scriptContent = scope .(); + + if (File.ReadAllText(templatePath, scriptContent, true) case .Err(let error)) + { + Log.EngineLogger.Error($"Failed to read template file from \"{templatePath}\". Error: {error}"); + return; + } + + scriptContent.Replace("[ProjectName]", CurrentProject.Name); + + String fileName = scope .(); + Path.GetFileName(path, fileName); + + Log.EngineLogger.AssertDebug(fileName.EndsWith(".cs")); + + if (fileName.EndsWith(".cs")) + fileName.RemoveFromEnd(3); + + String pascalFileName = scope .(fileName.Length); + ToPascalCase(fileName, pascalFileName); + String cSharpClassName = scope .(pascalFileName.Length); + ToCSharpName(pascalFileName, cSharpClassName); + + scriptContent.Replace("[EntityName]", cSharpClassName); + + if (File.WriteAllText(path, scriptContent, false) case .Err) + { + Log.EngineLogger.Error($"Failed to save script file to \"{path}\"."); + return; + } + })); + } + + private void ToCSharpName(String input, String output) + { + for (char32 c in input.DecodedChars) + { + if (c.IsLetterOrDigit || c == '_' || c == '@') + { + output.Append(c); + } + } + + let decode = System.Text.UTF8.Decode(output.Ptr, output.Length); + + // Make sure the name starts with a letter or _ or @ + if (!(decode.c.IsLetter || decode.c == '_' || decode.c == '@')) + { + output.Insert(0, '@'); + } + } + + private void ToPascalCase(String input, String output) + { + bool newWord = true; + + for (char32 c in input.DecodedChars) + { + if (c.IsLetter) + { + if (newWord) + { + output.Append(c.ToUpper); + newWord = false; + } + else + { + output.Append(c); + } + } + else + { + newWord = true; + + if (c.IsWhiteSpace || c == '_') + { + continue; + } + else + { + output.Append(c); + } + } + } } private void InitGraphics()