diff --git a/GlitchyEditor/src/Assets/AssetHierarchy.bf b/GlitchyEditor/src/Assets/AssetHierarchy.bf index 6a73cd2..3b4c67d 100644 --- a/GlitchyEditor/src/Assets/AssetHierarchy.bf +++ b/GlitchyEditor/src/Assets/AssetHierarchy.bf @@ -66,6 +66,30 @@ class AssetHierarchy Log.EngineLogger.Trace($"Created root node"); } + public void DeletePathsBackground(Span paths) + { + DeleteBackgroundTask deleteTask = new .(paths); + deleteTask.DeleteWhenEnded = true; + + for (StringView path in paths) + { + Result> nodeResult = GetNodeFromPath(path); + + if (nodeResult case .Ok(TreeNode assetNode)) + { + StringView assetDescriptorPath = assetNode->AssetFile?.AssetConfigPath ?? ""; + + // If it exists, also try to delete the .ass file + if (File.Exists(assetDescriptorPath)) + { + deleteTask.AddPath(assetDescriptorPath); + } + } + } + + EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(deleteTask); + } + /// Deletes the file that belongs to the given assetNode public void DeleteFile(AssetNode assetNode) { diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index ae97738..5eb3192 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -61,7 +61,9 @@ namespace GlitchyEditor.EditWindows private append String _currentDirectory = .(); - private append String _selectedFile = .(); + //private append String _selectedFile = .(); + + private append List _selectedFiles = .() ~ ClearAndDeleteItems!(_); private append String _assetToRename = .(); private char8[128] _renameFileNameBuffer; @@ -73,7 +75,8 @@ namespace GlitchyEditor.EditWindows public EditorContentManager _manager; - public StringView SelectedFile => _selectedFile; + // TODO: Update + public StringView SelectedFile => _selectedFiles.First(); char8[256] _newFileName = .(); bool _showNewFile = false; @@ -125,6 +128,11 @@ namespace GlitchyEditor.EditWindows parent.AddChild(("", null)); } + private SelectionMode _selectionMode; + private bool _addToSelection; + + bool _wantsDelete = false; + protected override void InternalShow() { _manager.Update(); @@ -135,10 +143,51 @@ namespace GlitchyEditor.EditWindows _currentDirectory.Set(_manager.AssetDirectory); } - if(!ImGui.Begin(s_WindowTitle, &_open, .None)) + // Update selection modifies { - ImGui.End(); - return; + if(!ImGui.Begin(s_WindowTitle, &_open, .None)) + { + ImGui.End(); + return; + } + + _addToSelection = Input.IsKeyPressed(.Control); + + if (Input.IsKeyPressed(.Shift)) + { + _selectionMode = .Range; + } + else + { + _selectionMode = .SingleFile; + } + } + + // Remove paths that no longer exist. + for (String path in _selectedFiles) + { + Result> node = _manager.AssetHierarchy.GetNodeFromPath(path); + if (node case .Err) + { + @path.Remove(); + delete path; + } + } + + // Deletion + { + if (Input.IsKeyPressing(.Delete) && _selectedFiles.Count > 0) + { + _wantsDelete = true; + } + + if (_wantsDelete) + { + ImGui.OpenPopup("Delete?"); + _wantsDelete = false; + } + + DrawDeleteItemPopup(); } ImGui.PushStyleVar(.CellPadding, .(0, 0)); @@ -198,36 +247,52 @@ namespace GlitchyEditor.EditWindows ImGui.End(); } - /*private void ExternalFileDropTarget() + private bool IsFileSelected(StringView fullFilePath) { - if (ImGui.BeginDragDropTarget()) + return _selectedFiles.ContainsAlt(fullFilePath); + } + + enum SelectionMode + { + SingleFile, + Range + } + + private void SelectFile(StringView fullFilePath, bool clearOldSelection, SelectionMode mode) + { + if (clearOldSelection) { - ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery); - - if (peekPayload != null) - { - if (peekPayload.IsDelivery()) - { - Log.EngineLogger.Info("Buup!"); - } - else - { - EditorLayer.SetDropEffect(.Copy); - } - } - - ImGui.EndDragDropTarget(); + _selectedFiles.ClearAndDeleteItems(); } - }*/ - private void SelectFile(StringView fullFileName) - { - if (_selectedFile == fullFileName) - return; + if (mode == .SingleFile) + { + if (IsFileSelected(fullFilePath)) + { + String oldPath = _selectedFiles.GetAndRemoveAlt(fullFilePath); + delete oldPath; + } + else + { + _selectedFiles.Add(new String(fullFilePath)); + } + } + else if (mode == .Range) + { + if (IsFileSelected(fullFilePath)) + { + String oldPath = _selectedFiles.GetAndRemoveAlt(fullFilePath); + delete oldPath; + } + else + { + _selectedFiles.Add(new String(fullFilePath)); + } + } + - _selectedFile.Set(fullFileName); - - OnFileSelected(this, _selectedFile); + // TODO: Update event to also provide all selected paths? + OnFileSelected(this, fullFilePath); } private void DrawSearchBar() @@ -431,6 +496,18 @@ namespace GlitchyEditor.EditWindows /// Renders the contents of _currentDirectory. Returns the node of the current directory, or null if the browser isn't in a directory. private void DrawCurrentDirectory(TreeNode currentDirectoryNode) { + var currentDirectoryNode; + if (Input.IsKeyPressed(.Alt) && Input.IsKeyPressing(.Up)) + { + if (currentDirectoryNode.Parent != _manager.AssetHierarchy.RootNode) + { + _currentDirectory.Set(currentDirectoryNode.Parent->Path); + + _selectedFiles.ClearAndDeleteItems(); + currentDirectoryNode = currentDirectoryNode.Parent; + } + } + List> directoryEntries = null; ImGui.Style* style = ImGui.GetStyle(); @@ -619,7 +696,7 @@ namespace GlitchyEditor.EditWindows ImGui.BeginChild("item", (.)DirectoryItemSize, .None, .NoScrollbar); - if (entry->Path == _selectedFile) + if (IsFileSelected(entry->Path)) { var color = ImGui.GetStyleColorVec4(.ButtonHovered); ImGui.PushStyleColor(.Button, *color); @@ -658,11 +735,9 @@ namespace GlitchyEditor.EditWindows if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) { - if (_selectedFile != entry->Path) - { - SelectFile(entry->Path); - _assetToRename.Clear(); - } + SelectFile(entry->Path, !_addToSelection, _selectionMode); + // TODO: WHY? + _assetToRename.Clear(); } if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(.Left)) @@ -706,17 +781,12 @@ namespace GlitchyEditor.EditWindows ImGui.TextUnformatted(itemType == .ParentDirectory ? ".." : entry->Name); } - bool wantsDelete = false; - if (ImGui.BeginPopupContextWindow()) { - ShowItemContextMenu(entry, itemType, ref wantsDelete); + ShowItemContextMenu(entry, itemType, ref _wantsDelete); ImGui.EndPopup(); } - if (wantsDelete) - ImGui.OpenPopup("Delete?"); - // TODO: Sub assets are probably borked now... I don't know if they ever worked, didn't test them if (entry->SubAssets?.Count > 0) { @@ -746,9 +816,7 @@ namespace GlitchyEditor.EditWindows ImGui.EndPopup(); } - - DeleteItemPopup(entry); - + ImGui.EndChild(); ImGui.AttachTooltip(entry->Name); @@ -807,8 +875,11 @@ namespace GlitchyEditor.EditWindows ImGui.EndChild(); } - private void DeleteItemPopup(TreeNode fileOrFolder) + private void DrawDeleteItemPopup() { + if (_selectedFiles.IsEmpty) + return; + // Always center this window when appearing ImGui.Vec2 center = ImGui.GetMainViewport().GetCenter(); ImGui.SetNextWindowPos(center, .Appearing, ImGui.Vec2(0.5f, 0.5f)); @@ -817,17 +888,42 @@ namespace GlitchyEditor.EditWindows if (ImGui.BeginPopupModal("Delete?", null, .AlwaysAutoResize)) { - ImGui.Text($""" - Delete "{fileOrFolder->Name}"? + if (_selectedFiles.Count == 1) + { + Result> nodeResult = _manager.AssetHierarchy.GetNodeFromPath(_selectedFiles[0]); + + if (nodeResult case .Ok(TreeNode node)) + { + ImGui.Text( + $""" + Do you really want to delete "{node->Name}"? - """); + """); + } + } + else + { + ImGui.Text( + $""" + Do you really want to delete {_selectedFiles.Count} directories/assets? + + + """); + } + ImGui.Separator(); if (ImGui.Button("Yes", ImGui.Vec2(120, 0))) { - _manager.AssetHierarchy.DeleteFile(fileOrFolder.Value); + List paths = scope .(); + for (String path in _selectedFiles) + { + paths.Add(path); + } + + _manager.AssetHierarchy.DeletePathsBackground(paths); ImGui.CloseCurrentPopup(); } diff --git a/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf index 661e739..5ff0d68 100644 --- a/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf +++ b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf @@ -238,13 +238,14 @@ class CopyBackgroundTask : BackgroundTask while (!_pathsToCopy.IsEmpty && Running) { - CopyInfo currentPath = _pathsToCopy.PopFront(); + CopyInfo currentPath = _pathsToCopy.Peek(); if (CopyPath(currentPath) case .Err(out _currentError) || Paused) { return .Pause; } + _pathsToCopy.PopFront(); delete currentPath; } diff --git a/GlitchyEditor/src/Multithreading/DeleteBackgroundTask.bf b/GlitchyEditor/src/Multithreading/DeleteBackgroundTask.bf new file mode 100644 index 0000000..c571e3f --- /dev/null +++ b/GlitchyEditor/src/Multithreading/DeleteBackgroundTask.bf @@ -0,0 +1,285 @@ +using System; +using System.IO; +using System.Collections; +using System.Diagnostics; +using ImGui; +using System.Threading; + +namespace GlitchyEditor.Multithreading; + +class DeleteBackgroundTask : BackgroundTask +{ + private List _sourcePath ~ {ClearAndDeleteItems!(_); delete:append _;}; + + private int _totalEntriesToDelete; + private append Queue _pathsToDelete = .() ~ ClearAndDeleteItems!(_); + private append Queue _scanQueue = .() ~ ClearAndDeleteItems!(_); + + enum ConflictResolutionMode + { + None, + IgnoreUnexpectedErrors = 1, + SkipNotFound = 2 + } + + private ConflictResolutionMode _nextFileMode; + private ConflictResolutionMode _allFilesMode; + + private ConflictResolutionMode CurrentFileMode => _nextFileMode | _allFilesMode; + + public bool ScanningFiles => !_scanQueue.IsEmpty; + + [AllowAppend] + public this(Span sourcePaths) + { + List sourcePathList = append List(sourcePaths.Length); + + _sourcePath = sourcePathList; + + for (StringView path in sourcePaths) + { + AddPath(path); + } + } + + public void AddPath(StringView pathToDelete) + { + _sourcePath.Add(new String(pathToDelete)); + _scanQueue.Add(new String(pathToDelete)); + _totalEntriesToDelete++; + } + + public enum DeleteError : IDisposable + { + case None; + case EntryNotFound(String SourcePath); + case UnexpectedError(String Message); + + public void Dispose() + { + switch (this) + { + case .None: + case .EntryNotFound(let SourcePath): + delete SourcePath; + case .UnexpectedError(let Message): + delete Message; + } + } + } + + private Result CollectFiles() + { + String currentPath = null; + defer + { + if (currentPath != null && @return case .Err) + { + _scanQueue.AddFront(currentPath); + } + } + + void AddToDeleteStack(String path) + { + _pathsToDelete.Add(path); + } + + while (!_scanQueue.IsEmpty && Running) + { + Thread.Sleep(500); + + // Pop from back, so that we delete children before parent directories + currentPath = _scanQueue.PopBack(); + + let fileName = scope String(); + Path.GetFileName(currentPath, fileName); + + if (Directory.Exists(currentPath)) + { + // Current node is ready, add it to delete-queue. + AddToDeleteStack(currentPath); + + // Add nested files and directories to the scan-queue. + for (FileFindEntry e in Directory.Enumerate(currentPath)) + { + let entryPath = new String(); + e.GetFilePath(entryPath); + + _scanQueue.Add(entryPath); + } + } + else if (File.Exists(currentPath)) + { + AddToDeleteStack(currentPath); + } + } + + _totalEntriesToDelete = _pathsToDelete.Count; + + return .Ok; + } + + private DeleteError _currentError = .None ~ _.Dispose(); + + public override RunResult Run() + { + _currentError.Dispose(); + _currentError = .None; + if (CollectFiles() case .Err(out _currentError) || Paused) + { + return .Pause; + } + + while (!_pathsToDelete.IsEmpty && Running) + { + Thread.Sleep(500); + + String currentPath = _pathsToDelete.Back; + + if (DeletePath(currentPath) case .Err(out _currentError) || Paused) + { + return .Pause; + } + + _pathsToDelete.PopBack(); + delete currentPath; + } + + return .Finished; + } + + private Result DeletePath(StringView deletePath) + { + if (Directory.Exists(deletePath)) + { + if (Directory.Delete(deletePath) case .Err(let err) && + !CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"Failed to delete directory {deletePath}: {err}")); + } + } + else if (File.Exists(deletePath)) + { + if (File.Delete(deletePath) case .Err(let err) && + !CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"Failed to delete file {deletePath}: {err}")); + } + } + else + { + if (!CurrentFileMode.HasFlag(.SkipNotFound)) + return .Err(.EntryNotFound(new String(deletePath))); + } + + return .Ok; + } + + public override void OnRenderPopup() + { + String title = scope .("Deleting files..."); + + if (ImGui.Begin(title, null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize)) + { + if (ScanningFiles) + { + ImGui.ProgressBar(-1.0f * (float)ImGui.GetTime(), .(-1, 0), scope $"Found {_pathsToDelete.Count} files..."); + } + else + { + int deletedEntries = _totalEntriesToDelete - _pathsToDelete.Count; + + ImGui.ProgressBar(deletedEntries / _totalEntriesToDelete, .(-1, 0), scope $"Deleted {deletedEntries} / {_totalEntriesToDelete} files."); + } + + + switch (_currentError) + { + case .EntryNotFound(let SourcePath): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"\"{SourcePath}\" doesn't exist."); + ImGui.PopStyleColor(); + case .UnexpectedError(let Message): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"Unexpected error: {Message}"); + ImGui.PopStyleColor(); + default: + // Do nothing + } + + if (ImGui.BeginTable("buttonTable", 2)) + { + switch (_currentError) + { + case .None: + // Do nothing + case .EntryNotFound(let SourcePath): + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + if (ImGui.Button("Skip")) + { + _nextFileMode |= .SkipNotFound; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Skip all")) + { + _allFilesMode |= .SkipNotFound; + Continue(); + } + case .UnexpectedError(let Message): + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + if (ImGui.Button("Retry")) + { + Continue(); + } + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + if (ImGui.Button("Ignore")) + { + _nextFileMode |= .IgnoreUnexpectedErrors; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Ignore all unexpected")) + { + _allFilesMode |= .IgnoreUnexpectedErrors; + Continue(); + } + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + + if (_currentError case .None) + { + if (Paused) + { + if (ImGui.Button("Continue")) + { + Continue(); + } + } + else + { + if (ImGui.Button("Pause")) + { + Pause(); + } + } + + ImGui.SameLine(); + } + + if (ImGui.Button("Cancel")) + { + Abort(); + } + ImGui.EndTable(); + } + + ImGui.End(); + } + } +} diff --git a/GlitchyEditor/src/Platform/Windows/DropTarget.bf b/GlitchyEditor/src/Platform/Windows/DropTarget.bf index b24b9dc..0011b99 100644 --- a/GlitchyEditor/src/Platform/Windows/DropTarget.bf +++ b/GlitchyEditor/src/Platform/Windows/DropTarget.bf @@ -301,37 +301,6 @@ abstract class IDropTargetImplBase : IUnknownImplBase