diff --git a/GlitchyEditor/src/Assets/AssetHierarchy.bf b/GlitchyEditor/src/Assets/AssetHierarchy.bf index 5d13be3..5d93e50 100644 --- a/GlitchyEditor/src/Assets/AssetHierarchy.bf +++ b/GlitchyEditor/src/Assets/AssetHierarchy.bf @@ -6,6 +6,7 @@ using System.Collections; using System.IO; using System.Linq; using GlitchyEngine.Content; +using GlitchyEditor.Multithreading; namespace GlitchyEditor.Assets; @@ -662,6 +663,41 @@ class AssetHierarchy Combine } + /*public Result CopyExternFileToNodeBackground(TreeNode targetDirectory, String sourcePath, OverwriteMode overwrite) + { + if (!targetDirectory->IsDirectory) + return .Err; + + FileInfo sourceInfo = scope FileInfo(sourcePath); + + if (!sourceInfo.Exists) + return .Err; + + CopyBackgroundTask copyTask = new CopyBackgroundTask(sourcePath, targetDirectory->Path); + + EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(copyTask); + + return copyTask; + }*/ + + public Result CopyExternFilesToNodeBackground(TreeNode targetDirectory, List sourcePaths, OverwriteMode overwrite) + { + if (!targetDirectory->IsDirectory) + return .Err; + + /*FileInfo sourceInfo = scope FileInfo(sourcePath); + + if (!sourceInfo.Exists) + return .Err;*/ + + CopyBackgroundTask copyTask = new CopyBackgroundTask(sourcePaths, targetDirectory->Path); + copyTask.DeleteWhenStopped = true; + + EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(copyTask); + + return copyTask; + } + public Result CopyExternFileToNode(TreeNode targetDirectory, String sourcePath, OverwriteMode overwrite) { if (!targetDirectory->IsDirectory) @@ -691,7 +727,7 @@ class AssetHierarchy case .KeepBoth: String fileExtension = scope .(); Path.GetExtension(targetPath, fileExtension); - FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); + Path.FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); case .Overwrite: forceOverwrite = true; default: @@ -709,7 +745,7 @@ class AssetHierarchy case .KeepBoth: String fileExtension = scope .(); Path.GetExtension(targetPath, fileExtension); - FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); + Path.FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); case .Overwrite: forceOverwrite = true; default: @@ -722,25 +758,4 @@ class AssetHierarchy return .Ok; } - - public void FindFreePath(StringView directory, StringView wantedName, StringView fileExtension, String outFreePath) - { - int fileNumber = 0; - - String currentFileName = scope $"{wantedName}{fileExtension}"; - while (true) - { - Path.Combine(outFreePath..Clear(), directory, currentFileName); - - FileInfo targetInfo = scope FileInfo(outFreePath); - - if (!targetInfo.Exists) - { - break; - } - - fileNumber++; - currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}"); - } - } } diff --git a/GlitchyEditor/src/Assets/Editors/MaterialEditor.bf b/GlitchyEditor/src/Assets/Editors/MaterialEditor.bf index 9da249c..e4d0aa8 100644 --- a/GlitchyEditor/src/Assets/Editors/MaterialEditor.bf +++ b/GlitchyEditor/src/Assets/Editors/MaterialEditor.bf @@ -52,6 +52,8 @@ class MaterialEditor if (DrawLockButton(bufferVariable.Flags.HasFlag(.Locked)) && !readOnly) { bufferVariable.[Friend]_flags ^= .Locked; + // Also mark as dirty so we update our children! + bufferVariable.[Friend]_flags |= .Dirty; } ImGui.SameLine(); @@ -165,6 +167,8 @@ class MaterialEditor if (DrawLockButton(texture.Flags.HasFlag(.Locked)) && !readOnly) { texture.Flags ^= .Locked; + // Also mark as dirty so we update our children! + texture.Flags |= .Dirty; } ImGui.SameLine(); diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index ea0e3f0..288316b 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -608,12 +608,14 @@ namespace GlitchyEditor.EditWindows List droppedFiles = (.)Internal.UnsafeCastToObject(*(void**)peekPayload.Data); - for (String path in droppedFiles) + _manager.AssetHierarchy.CopyExternFilesToNodeBackground(dropTarget, droppedFiles, .None); + + /*for (String path in droppedFiles) { Log.EngineLogger.Info($"Dropping file {path}"); - _manager.AssetHierarchy.CopyExternFileToNode(dropTarget, path, .None); - } + //_manager.AssetHierarchy.CopyExternFileToNodeBackground(dropTarget, path, .None); + }*/ } else { diff --git a/GlitchyEditor/src/EditorApp.bf b/GlitchyEditor/src/EditorApp.bf index f6dc2e2..d9015f9 100644 --- a/GlitchyEditor/src/EditorApp.bf +++ b/GlitchyEditor/src/EditorApp.bf @@ -7,6 +7,8 @@ using GlitchyEditor.Assets.Processors; using GlitchyEditor.Assets.Exporters; using DirectX.Common; using GlitchyEditor.Platform.Windows; +using GlitchyEditor.Multithreading; +using System.Threading; namespace GlitchyEditor { @@ -14,22 +16,31 @@ namespace GlitchyEditor { EditorContentManager _contentManager; + public new static EditorApp Instance => Application.Instance as EditorApp; + + private append BackgroundTaskManager _backgroundTaskManager = .(); + + public BackgroundTaskManager BackgroundTaskManager => _backgroundTaskManager; + public this(String[] args) { Log.ClientLogger = new EditorLogger(); Log.EngineLogger = new EditorLogger() { IsEngineLogger = true }; - + + _backgroundTaskManager.Init(); + // TODO: Windows only HResult result = OleInitialize(null); Log.EngineLogger.Assert(result case .S_OK); PushLayer(new EditorLayer(args, _contentManager)); - } public ~this() { OleUninitialize(); + + _backgroundTaskManager.Deinit(); } protected override IContentManager InitContentManager() diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index 9ecefca..d992846 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -1243,6 +1243,8 @@ namespace GlitchyEditor } #endif + EditorApp.Instance.BackgroundTaskManager.ImGuiRender(); + _editor.SceneViewportWindow.RenderTarget = _editorViewportTarget; _editor.GameViewportWindow.RenderTarget = _gameViewportTarget; diff --git a/GlitchyEditor/src/Multithreading/BackgroundTask.bf b/GlitchyEditor/src/Multithreading/BackgroundTask.bf new file mode 100644 index 0000000..f3b75e9 --- /dev/null +++ b/GlitchyEditor/src/Multithreading/BackgroundTask.bf @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Threading; +using System.Collections; +using System.Diagnostics; +using ImGui; + +namespace GlitchyEditor.Multithreading; + +using internal GlitchyEditor.Multithreading; + +abstract class BackgroundTask +{ + internal BackgroundTaskList _list; + internal BackgroundTask _previous; + internal BackgroundTask _next; + + internal BackgroundTaskManager _taskManager; + + public enum RunState + { + Ready, + Blocked, + Paused, + Running, + Aborted, + Finished + } + + public enum RunResult + { + /// The task gives up it's runtime, but has to run again. + Continue, + /// The task gives up it's runtime, because it cannot continue without user intervention through the UI. + Pause, + /// The task finished and wont run again. + Finished, + /// The operation was aborted, the task will not run again. + Abort + } + + public RunState State { get; internal set; } = .Ready; + + public bool Ready => State == .Ready; + public bool Paused => State == .Paused; + public bool Blocked => State == .Blocked; + public bool Running => State == .Running; + public bool Aborted => State == .Aborted; + public bool Finished => State == .Finished; + + public bool Ended => Finished || Aborted; + + public bool DeleteWhenStopped { get; set; } + + public abstract RunResult Run(); + + public abstract void OnRenderPopup(); + + public void Continue() + { + _taskManager.ContinueTask(this); + } + + public void Pause() + { + if (State == .Ready || State == .Running) + { + State = .Paused; + } + } + + public void Abort() + { + _taskManager.AbortTask(this); + } +} diff --git a/GlitchyEditor/src/Multithreading/BackgroundTaskList.bf b/GlitchyEditor/src/Multithreading/BackgroundTaskList.bf new file mode 100644 index 0000000..c535e09 --- /dev/null +++ b/GlitchyEditor/src/Multithreading/BackgroundTaskList.bf @@ -0,0 +1,422 @@ +using System; +using System.Diagnostics; + +namespace GlitchyEditor.Multithreading; + +using internal GlitchyEditor.Multithreading; + +class BackgroundTaskList +{ + BackgroundTask _sentinel = new BackgroundTask() + { + public override BackgroundTask.RunResult Run() => .Finished; + public override void OnRenderPopup() { } + _next = _, + _previous = _, + _list = this + } ~ delete _; + + private int _count; + + public BackgroundTask First => _sentinel._next == _sentinel ? null : _sentinel._next; + public BackgroundTask Last => _sentinel._previous == _sentinel ? null : _sentinel._previous; + public BackgroundTask End => _sentinel; + + public int Count => _count; + + private void InsertBefore(BackgroundTask item, BackgroundTask beforeNode) + { + item._next = beforeNode; + item._previous = beforeNode._previous; + item._next._previous = item; + item._previous._next = item; + + item._list = this; + + _count++; + + Validate(); + } + + public void AddFirst(BackgroundTask item) + { + if (item._list != null) + item._list.Remove(item, false); + + InsertBefore(item, _sentinel._next); + } + + public void AddLast(BackgroundTask item) + { + if (item._list != null) + item._list.Remove(item, false); + + InsertBefore(item, _sentinel); + } + + public void Clear() + { + Clear(false); + } + + public void Clear(bool deleteElements) + { + BackgroundTask task = _sentinel._next; + + while (task != _sentinel) + { + BackgroundTask nextTask = task._next; + + UnlinkElement(task); + if (deleteElements) + delete task; + + task = nextTask; + } + + _sentinel._next = _sentinel._previous = _sentinel; + + _count = 0; + + Validate(); + } + + private void UnlinkElement(BackgroundTask element) + { + element._next = null; + element._previous = null; + element._list = null; + } + + public bool Contains(BackgroundTask item) + { + return item._list == this; + } + + public bool Remove(BackgroundTask item) + { + return Remove(item, false); + } + + public bool Remove(BackgroundTask item, bool deleteElement) + { + if (item._list != this) + return false; + + item._previous._next = item._next; + item._next._previous = item._previous; + + UnlinkElement(item); + + if (deleteElement) + delete item; + + _count--; + + Validate(); + + return true; + } + + public Result TryPopFront() + { + if (_sentinel._next == _sentinel) + return .Err; + + BackgroundTask first = _sentinel._next; + + Remove(first, false); + + return first; + } + + public Result TryPopBack() + { + if (_sentinel._previous == _sentinel) + return .Err; + + BackgroundTask first = _sentinel._previous; + + Remove(first, false); + + return first; + } + + internal void Validate() + { + BackgroundTask task = _sentinel._next; + + int visitedNodes = 0; + + while (task != _sentinel) + { + visitedNodes++; + + Debug.Assert(visitedNodes <= Count); + Debug.Assert(task._list == this); + + BackgroundTask nextTask = task._next; + + Debug.Assert(nextTask._previous == task); + task = nextTask; + } + + Debug.Assert(visitedNodes == Count); + } +} + +class BackgroundTaskListTests +{ + // Stub implementation + class BackgroundTask : GlitchyEditor.Multithreading.BackgroundTask + { + public override BackgroundTask.RunResult Run() + { + return default; + } + + public override void OnRenderPopup() + { + + } + } + + static void TestBasicOperations() + { + Debug.WriteLine("πŸ§ͺ Testing Basic Operations..."); + + var list1 = scope BackgroundTaskList(); + + var task1 = scope BackgroundTask(); + var task2 = scope BackgroundTask(); + + // Test AddFirst + list1.AddFirst(task1); + Debug.Assert(list1.Count == 1); + Debug.Assert(task1._list == list1); + Debug.Assert(list1.Contains(task1)); + list1.Validate(); + + // Test AddLast + list1.AddLast(task2); + Debug.Assert(list1.Count == 2); + Debug.Assert(task2._list == list1); + Debug.Assert(list1.Contains(task2)); + list1.Validate(); + + // Test Remove + list1.Remove(task1); + Debug.Assert(list1.Count == 1); + Debug.Assert(task1._list == null); + Debug.Assert(!list1.Contains(task1)); + Debug.Assert(list1.Contains(task2)); + list1.Validate(); + + Debug.WriteLine("βœ… Basic Operations passed!"); + } + + static void TestAutoRemoveFromOldList() + { + Debug.WriteLine("πŸ§ͺ Testing Auto-Remove from Old List..."); + + var list1 = scope BackgroundTaskList(); + var list2 = scope BackgroundTaskList(); + + var task = scope BackgroundTask(); + + // Add to list1 + list1.AddFirst(task); + Debug.Assert(list1.Count == 1); + Debug.Assert(list2.Count == 0); + Debug.Assert(task._list == list1); + list1.Validate(); + list2.Validate(); + + // Add to list2 - should remove from list1 + list2.AddLast(task); + Debug.Assert(list1.Count == 0); + Debug.Assert(list2.Count == 1); + Debug.Assert(task._list == list2); + Debug.Assert(!list1.Contains(task)); + Debug.Assert(list2.Contains(task)); + list1.Validate(); + list2.Validate(); + + // add to list1 again, should be removed from list2 + list1.AddFirst(task); + Debug.Assert(list1.Count == 1); + Debug.Assert(list2.Count == 0); + Debug.Assert(task._list == list1); + list1.Validate(); + list2.Validate(); + + Debug.WriteLine("βœ… Auto-Remove passed!"); + } + + static void TestTryPopFront() + { + Debug.WriteLine("πŸ§ͺ Testing TryPopFront..."); + + var list = scope BackgroundTaskList(); + + // Test empty list + let emptyResult = list.TryPopFront(); + Debug.Assert(emptyResult == .Err); + list.Validate(); + + var task1 = scope BackgroundTask(); + var task2 = scope BackgroundTask(); + var task3 = scope BackgroundTask(); + + // Add three elements + list.AddLast(task1); + list.Validate(); + list.AddLast(task2); + list.Validate(); + list.AddLast(task3); + list.Validate(); + Debug.Assert(list.Count == 3); + + // Pop first element + let pop1 = list.TryPopFront(); + Debug.Assert(pop1 == .Ok(task1)); + Debug.Assert(list.Count == 2); + Debug.Assert(task1._list == null); + list.Validate(); + + // Pop second element + let pop2 = list.TryPopFront(); + Debug.Assert(pop2 == .Ok(task2)); + Debug.Assert(list.Count == 1); + Debug.Assert(task2._list == null); + list.Validate(); + + // Pop third element + let pop3 = list.TryPopFront(); + Debug.Assert(pop3 == .Ok(task3)); + Debug.Assert(list.Count == 0); + Debug.Assert(task3._list == null); + list.Validate(); + + // Pop on now empty list + let pop4 = list.TryPopFront(); + Debug.Assert(pop4 == .Err); + list.Validate(); + + Debug.WriteLine("βœ… TryPopFront passed!"); + } + + /*static void TestIterator() + { + Debug.WriteLine("πŸ§ͺ Testing Iterator..."); + + var list = scope BackgroundTaskList(); + var tasks = scope BackgroundTask*[5]; + + for (int i = 0; i < tasks.Count; i++) + { + tasks[i] = new BackgroundTask(); + list.AddLast(tasks[i]); + } + defer { for (var t in tasks) delete t; } + + // Teste Iterator + int count = 0; + for (var task in list) + { + Debug.Assert(task == tasks[count]); + count++; + } + Debug.Assert(count == 5); + + Debug.WriteLine("βœ… Iterator passed!"); + }*/ + + static void TestClear() + { + Debug.WriteLine("πŸ§ͺ Testing Clear..."); + + var list = scope BackgroundTaskList(); + + var task1 = scope BackgroundTask(); + var task2 = scope BackgroundTask(); + var task3 = scope BackgroundTask(); + + list.AddLast(task1); + list.Validate(); + list.AddLast(task2); + list.Validate(); + list.AddLast(task3); + list.Validate(); + Debug.Assert(list.Count == 3); + + // Clear sollte task1 und task3 lΓΆschen, aber nicht task2 + list.Clear(); + Debug.Assert(list.Count == 0); + list.Validate(); + + Debug.WriteLine("βœ… Clear passed!"); + } + + static void TestComplexScenario() + { + // Simulate processing queues. + Debug.WriteLine("πŸ§ͺ Testing Complex Scenario..."); + + var pending = scope BackgroundTaskList(); + var processing = scope BackgroundTaskList(); + var completed = scope BackgroundTaskList(); + + // Add to pending + var tasks = scope BackgroundTask[10]; + for (int i = 0; i < tasks.Count; i++) + { + tasks[i] = scope:: BackgroundTask(); + pending.AddLast(tasks[i]); + pending.Validate(); + } + + // Move from pending to processing + while (true) + { + let result = pending.TryPopFront(); + if (result == .Err) break; + + let task = result.Value; + processing.AddLast(task); + processing.Validate(); + } + + Debug.Assert(pending.Count == 0); + Debug.Assert(processing.Count == 10); + + // Move to completed + while (true) + { + let result = processing.TryPopFront(); + if (result == .Err) break; + + completed.AddLast(result.Value); + completed.Validate(); + } + + Debug.Assert(processing.Count == 0); + Debug.Assert(completed.Count == 10); + + Debug.WriteLine("βœ… Complex Scenario passed!"); + } + + public static void RunTests() + { + Debug.WriteLine("πŸš€ Starting BackgroundTaskList Tests...\n"); + + TestBasicOperations(); + TestAutoRemoveFromOldList(); + TestTryPopFront(); + //TestIterator(); + TestClear(); + TestComplexScenario(); + + Debug.WriteLine("\nπŸŽ‰ All tests passed!"); + } +} \ No newline at end of file diff --git a/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf b/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf new file mode 100644 index 0000000..b4702ff --- /dev/null +++ b/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf @@ -0,0 +1,216 @@ +using System; +using System.Threading; +using System.Collections; +using GlitchyEngine; +using GlitchyEngine.Collections; + +namespace GlitchyEditor.Multithreading; + +using internal GlitchyEditor.Multithreading; + +// This would go nicely with some light weight threading! +class BackgroundTaskManager +{ + private Thread _workerThread; + + private bool _run; + + private BackgroundTask _runningTask; + private append BackgroundTaskList _readyQueue = .(); + private append BackgroundTaskList _waitingTasks = .(); + private append Monitor _queueLock = .(); + + public void Init() + { + _run = true; + _workerThread = new Thread(new => WorkerLoop); + _workerThread.Start(); + } + + public void Deinit() + { + _run = false; + + // TODO: Delete jobs if necessary + // TODO: Danger if thread is deadlocked. + _workerThread.Join(); + } + + private void WorkerLoop() + { + while (_run) + { + Result nextRunningTask = .Err; + + using (_queueLock.Enter()) + { + nextRunningTask = _readyQueue.TryPopFront(); + } + + if (nextRunningTask case .Ok(let task)) + { + _runningTask = task; + _runningTask.State = .Running; + BackgroundTask.RunResult runResult = task.Run(); + _runningTask = null; + + switch (runResult) + { + case .Continue: + task.State = .Ready; + using (_queueLock.Enter()) + { + _readyQueue.AddLast(task); + } + case .Pause: + task.State = .Paused; + using (_queueLock.Enter()) + { + _waitingTasks.AddLast(task); + } + case .Finished: + task.State = .Finished; + + if (task.DeleteWhenStopped) + { + delete task; + } + case .Abort: + task.State = .Aborted; + + if (task.DeleteWhenStopped) + { + delete task; + } + } + } + else + { + Thread.Sleep(10); + } + } + } + + public Result StartBackgroundTask(BackgroundTask task) + { + if (task._taskManager != null) + { + Log.EngineLogger.Error("Task is already managed by a taskmanager."); + return .Err; + } + + if (task.State == .Running || task.State == .Aborted) + { + Log.EngineLogger.Error("Task State illegal. (Did it already run?)"); + return .Err; + } + + task._taskManager = this; + + using (_queueLock.Enter()) + { + if (task.State == .Ready) + _readyQueue.AddLast(task); + else + _waitingTasks.AddLast(task); + } + + return .Ok; + } + + internal void ContinueTask(BackgroundTask task) + { + Log.EngineLogger.AssertDebug(task._taskManager == this); + Log.EngineLogger.AssertDebug(!task.Ended); + + Application.Instance.InvokeOnMainThread(new () => + { + if (task.State == .Paused) + { + task.State = .Ready; + + using (_queueLock.Enter()) + { + Log.EngineLogger.AssertDebug(!_readyQueue.Contains(task)); + // We assume this was a user interaction so this task will get some priority. + _readyQueue.AddFirst(task); + } + } + return true; + }); + } + + internal void Pause(BackgroundTask task) + { + Log.EngineLogger.AssertDebug(task._taskManager == this); + Log.EngineLogger.AssertDebug(!task.Ended); + + Application.Instance.InvokeOnMainThread(new () => + { + if (task.State == .Ready) + { + task.State = .Ready; + + using (_queueLock.Enter()) + { + Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task)); + _waitingTasks.AddLast(task); + } + } + return true; + }); + } + + internal void AbortTask(BackgroundTask task) + { + Log.EngineLogger.AssertDebug(task._taskManager == this); + Log.EngineLogger.AssertDebug(!task.Ended); + + Application.Instance.InvokeOnMainThread(new () => + { + if (task.State != .Aborted) + { + task.State = .Aborted; + + using (_queueLock.Enter()) + { + _waitingTasks.Remove(task); + } + + if (task.DeleteWhenStopped) + { + delete task; + } + } + return true; + }); + } + + public void ImGuiRender() + { + using (_queueLock.Enter()) + { + _runningTask?.OnRenderPopup(); + + BackgroundTask task = _readyQueue.First; + + while (task != null && task != _readyQueue.End) + { + Log.EngineLogger.Assert(task._list == _readyQueue); + + task.OnRenderPopup(); + task = task._next; + } + + BackgroundTask wtask = _waitingTasks.First; + + while (wtask != null && wtask != _waitingTasks.End) + { + Log.EngineLogger.Assert(wtask._list == _waitingTasks); + + wtask.OnRenderPopup(); + wtask = wtask._next; + } + } + } +} \ No newline at end of file diff --git a/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf new file mode 100644 index 0000000..6d21a6f --- /dev/null +++ b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf @@ -0,0 +1,320 @@ +using System; +using System.IO; +using System.Collections; +using System.Diagnostics; +using ImGui; + +namespace GlitchyEditor.Multithreading; + +class CopyBackgroundTask : BackgroundTask +{ + private List _sourcePath ~ {ClearAndDeleteItems!(_); delete:append _;}; + private String _targetDirectoryPath ~ delete:append _; + + private int _totalEntriesToCopy; + private append Queue _pathsToCopy = .() ~ ClearAndDeleteItems!(_); + private append Queue _scanQueue = .() ~ ClearAndDeleteItems!(_); + + private enum OverwriteMode + { + None, + OverwriteFile = 1, + KeepBoth = 2, + CombineDirectories = 4, + Skip = 8 + } + + private OverwriteMode _nextFileMode; + private OverwriteMode _allFilesMode; + + private OverwriteMode CurrentFileMode => _nextFileMode | _allFilesMode; + + private class CopyInfo + { + public String SourcePath ~ delete:append _; + public String TargetPath ~ delete:append _; + public OverwriteMode OverwriteMode; + + [AllowAppend] + public this(StringView sourcePath, StringView targetPath, OverwriteMode overwriteMode) + { + String src = append String(sourcePath); + String tgt = append String(targetPath); + + SourcePath = src; + TargetPath = tgt; + + OverwriteMode = overwriteMode; + } + } + + public bool ScanningFiles => !_scanQueue.IsEmpty; + + [AllowAppend] + public this(List sourcePaths, StringView targetDirectoryPath) + { + List sourcePathList = append List(sourcePaths.Count); + String targetDirectoryPathStr = append String(targetDirectoryPath); + + _sourcePath = sourcePathList; + _targetDirectoryPath = targetDirectoryPathStr; + + for (String path in sourcePaths) + { + _sourcePath.Add(new String(path)); + _scanQueue.Add(new String(path)); + } + + _totalEntriesToCopy = sourcePaths.Count; + } + + public enum CopyError : IDisposable + { + case None; + case TargetDirectoryIsNotDirectory; + case SourceDoesntExist(String SourcePath); + case TargetFileExists(String FileName); + case TargetDirectoryExists(String DirectoryName); + + public void Dispose() + { + switch (this) + { + case .None: + case .TargetDirectoryIsNotDirectory: + case .SourceDoesntExist(let SourcePath): + delete SourcePath; + case .TargetFileExists(let FileName): + delete FileName; + case .TargetDirectoryExists(let DirectoryName): + delete DirectoryName; + } + } + } + + private Result CollectFilesToCopy() + { + void RemoveFront() + { + String path = _scanQueue.PopFront(); + delete path; + } + + while (!_scanQueue.IsEmpty && Running) + { + //Thread.Sleep(1000); + + String currentPath = _scanQueue.Peek(); + + //FileInfo sourceInfo = scope FileInfo(currentPath); + + String fileName = scope .(); + Path.GetFileName(currentPath, fileName); + + String targetPath = scope .(); + Path.Combine(targetPath, _targetDirectoryPath, fileName); + + //FileInfo targetInfo = scope FileInfo(targetPath); + + if (Directory.Exists(currentPath)) + { + /*for (FileFindEntry e in Directory.Enumerate(currentPath)) + { + String entryPath = new String(); + e.GetFilePath(entryPath); + _scanQueue.Add(entryPath); + }*/ + } + else if (File.Exists(currentPath)) + { + if (File.Exists(targetPath)) + { + switch (CurrentFileMode) + { + case .KeepBoth, .OverwriteFile, .Skip: + default: + return .Err(.TargetFileExists(new String(fileName))); + } + } + + _pathsToCopy.Add(new CopyInfo(currentPath, targetPath, CurrentFileMode)); + RemoveFront(); + _nextFileMode = .None; + } + } + + _totalEntriesToCopy = _pathsToCopy.Count; + + return .Ok; + } + + private CopyError _currentError = .None ~ _.Dispose(); + + public override RunResult Run() + { + _currentError.Dispose(); + _currentError = .None; + if (CollectFilesToCopy() case .Err(out _currentError)) + { + return .Pause; + } + + while (!_pathsToCopy.IsEmpty && Running) + { + //Thread.Sleep(1000); + CopyInfo currentPath = _pathsToCopy.PopFront(); + + defer + { + delete currentPath; + } + + CopyPath(currentPath); + } + + return .Finished; + } + + private Result CopyPath(CopyInfo sourcePath) + { + if (Directory.Exists(sourcePath.SourcePath)) + { + } + else if (File.Exists(sourcePath.SourcePath)) + { + bool forceOverwrite = false; + if (File.Exists(sourcePath.TargetPath)) + { + switch (sourcePath.OverwriteMode) + { + case .KeepBoth: + String fileExtension = scope .(); + Path.GetExtension(sourcePath.TargetPath, fileExtension); + + String fileName = scope .(); + Path.GetFileName(sourcePath.SourcePath, fileName); + + Path.FindFreePath(_targetDirectoryPath, fileName.Substring(0..<^fileExtension.Length), fileExtension, sourcePath.TargetPath..Clear()); + case .OverwriteFile: + forceOverwrite = true; + case .Skip: + default: + return .Err;//(.TargetFileExists); + } + } + + File.Copy(sourcePath.SourcePath, sourcePath.TargetPath, forceOverwrite); + } + else + { + // TODO error + return .Err; + } + + return .Ok; + } + + public override void OnRenderPopup() + { + if (ImGui.Begin("Copying files...")) + { + if (ScanningFiles) + { + ImGui.ProgressBar(-1.0f * (float)ImGui.GetTime(), .(-1, 0), scope $"Found {_pathsToCopy.Count} files..."); + } + else + { + int copiedFiles = _totalEntriesToCopy - _pathsToCopy.Count; + + ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files."); + } + + switch (_currentError) + { + case .None: + // Do nothing + case .TargetDirectoryIsNotDirectory: + // This shouldn't be possible + Debug.Break(); + case .SourceDoesntExist(let SourcePath): + // This isn't good! + case .TargetFileExists(let fileName): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists."); + ImGui.PopStyleColor(); + + if (ImGui.Button("Overwrite")) + { + _nextFileMode = .OverwriteFile; + Continue(); + } + ImGui.AttachTooltip("Overwrites the existing file. Will ask again if another conflict occurs."); + + if (ImGui.Button("Overwrite All")) + { + _allFilesMode = .OverwriteFile; + Continue(); + } + ImGui.AttachTooltip("Overwrites all existing files."); + + if (ImGui.Button("Keep both")) + { + _nextFileMode = .KeepBoth; + Continue(); + } + ImGui.AttachTooltip("Keeps both the existing file as well as the new one, differentiates by adding a number to the name. Will ask again if another conflict occurs."); + + if (ImGui.Button("Keep all")) + { + _allFilesMode = .KeepBoth; + Continue(); + } + ImGui.AttachTooltip("Keeps both the existing files as well as the new ones, differentiates by adding a number to the name."); + + if (ImGui.Button("Cancel")) + { + Abort(); + } + case .TargetDirectoryExists(let directoryName): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"A file with the name \"{directoryName}\" already exists."); + ImGui.PopStyleColor(); + + if (ImGui.Button("Combine")) + { + _nextFileMode = .CombineDirectories; + Continue(); + } + ImGui.AttachTooltip("Merges the copied directory into the target directory. Will ask again if another conflict occurs."); + + if (ImGui.Button("Combine All")) + { + _allFilesMode = .CombineDirectories; + Continue(); + } + ImGui.AttachTooltip("Merges all copied directories into the target directories."); + + if (ImGui.Button("Keep both")) + { + _nextFileMode = .KeepBoth; + Continue(); + } + ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict. Will ask again if another conflict occurs."); + + if (ImGui.Button("Keep all")) + { + _allFilesMode = .KeepBoth; + Continue(); + } + ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict."); + + if (ImGui.Button("Cancel")) + { + Abort(); + } + } + + ImGui.End(); + } + } +} diff --git a/GlitchyEngine/src/Collections/IntrusiveLinkedList.bf b/GlitchyEngine/src/Collections/IntrusiveLinkedList.bf new file mode 100644 index 0000000..08fe364 --- /dev/null +++ b/GlitchyEngine/src/Collections/IntrusiveLinkedList.bf @@ -0,0 +1,77 @@ +using System; +using System.Collections; + +namespace GlitchyEngine.Collections; + +/*class IntrusiveLinkedList : ICollection where T : ILinkedListElement, var +{ + private T _head; + private T _tail; + + private bool _ownsElements; + + public void Add(T element) + { + element.List = this; + if (_head == null) + { + _head = _tail = element; + } + else + { + //_tail.ListLink.Previous = this; + } + } + + public void Clear() + { + + } + + public bool Contains(T item) + { + return default; + } + + public void CopyTo(Span span) + { + + } + + public bool Remove(T item) + { + return default; + } +} + +class ListLink where T : ILinkedListElement +{ + public IntrusiveLinkedList List { get; internal set; } + public ListLink Previous { get; internal set; } + public ListLink Next { get; internal set; } +} + +interface ILinkedListElement +{ +} + +[AttributeUsage(.Class | .Struct)] +struct IntrusiveLinkedListAttribute : Attribute, IComptimeTypeApply +{ + [Comptime] + public void ApplyToType(Type type) + { + Compiler.EmitAddInterface(type, typeof(ILinkedListElement)); + + Compiler.EmitTypeBody(type, new $""" + //ListLink<{type}> _listLink; + public using ListLink<{type}> _listLink; + public ListLink<{type}> ListLink => _listLink; + """); + } +} + +[IntrusiveLinkedList] +class LLTest +{ +}*/ \ No newline at end of file diff --git a/GlitchyEngine/src/Extension/System/IO/Path.bf b/GlitchyEngine/src/Extension/System/IO/Path.bf index 966130b..8551a4c 100644 --- a/GlitchyEngine/src/Extension/System/IO/Path.bf +++ b/GlitchyEngine/src/Extension/System/IO/Path.bf @@ -34,6 +34,7 @@ extension Path path.Remove(0, 1); } + public static void Combine(String target, params StringView[] components) { for (var component in components) @@ -46,4 +47,32 @@ extension Path target.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); } + + /** + * If a file with the given path ([targetDirectory]/[wantedName][fileExtension]) already exists it will add a number at the end of the file name. + * @param targetDirectory The directory in which the file will be put. + * @param wantedFileName The wanted name of the file. If it already exists a number will be put behind it. + * @param fileExtension The file extension. This should contain the dot. + * @param outFreePath The string that will contain the resulting filepath. Note: This will be cleared before writing the file name. + */ + public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath) + { + int fileNumber = 0; + + String currentFileName = scope $"{wantedName}{fileExtension}"; + while (true) + { + Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName); + + FileInfo targetInfo = scope FileInfo(outFreePath); + + if (!targetInfo.Exists) + { + break; + } + + fileNumber++; + currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}"); + } + } } \ No newline at end of file