advanced async copy of files

This commit is contained in:
Simon Lübeß
2025-06-26 10:16:39 +02:00
parent bfd1754d14
commit 108e24eb38
11 changed files with 1202 additions and 28 deletions
+38 -23
View File
@@ -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<BackgroundTask> CopyExternFileToNodeBackground(TreeNode<AssetNode> 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<BackgroundTask> CopyExternFilesToNodeBackground(TreeNode<AssetNode> targetDirectory, List<String> 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<void, CopyError> CopyExternFileToNode(TreeNode<AssetNode> 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}");
}
}
}
@@ -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();
@@ -608,12 +608,14 @@ namespace GlitchyEditor.EditWindows
List<String> 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
{
+12 -1
View File
@@ -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()
+2
View File
@@ -1243,6 +1243,8 @@ namespace GlitchyEditor
}
#endif
EditorApp.Instance.BackgroundTaskManager.ImGuiRender();
_editor.SceneViewportWindow.RenderTarget = _editorViewportTarget;
_editor.GameViewportWindow.RenderTarget = _gameViewportTarget;
@@ -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);
}
}
@@ -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<BackgroundTask> TryPopFront()
{
if (_sentinel._next == _sentinel)
return .Err;
BackgroundTask first = _sentinel._next;
Remove(first, false);
return first;
}
public Result<BackgroundTask> 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!");
}
}
@@ -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<BackgroundTask> 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<void> 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;
}
}
}
}
@@ -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<String> _sourcePath ~ {ClearAndDeleteItems!(_); delete:append _;};
private String _targetDirectoryPath ~ delete:append _;
private int _totalEntriesToCopy;
private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_);
private append Queue<String> _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<String> sourcePaths, StringView targetDirectoryPath)
{
List<String> sourcePathList = append List<String>(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<void, CopyError> 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<void> 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();
}
}
}
@@ -0,0 +1,77 @@
using System;
using System.Collections;
namespace GlitchyEngine.Collections;
/*class IntrusiveLinkedList<T> : ICollection<T> 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<T> span)
{
}
public bool Remove(T item)
{
return default;
}
}
class ListLink<T> where T : ILinkedListElement
{
public IntrusiveLinkedList<T> List { get; internal set; }
public ListLink<T> Previous { get; internal set; }
public ListLink<T> 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
{
}*/
@@ -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}");
}
}
}