Fixed copying with multiple conflict handling flags,

made canceling and pausing of tasks more robust
This commit is contained in:
Simon Lübeß
2025-06-26 17:51:32 +02:00
parent 05671649b4
commit 95ccb597d6
6 changed files with 409 additions and 191 deletions
@@ -50,7 +50,7 @@ abstract class BackgroundTask
public bool Ended => Finished || Aborted; public bool Ended => Finished || Aborted;
public bool DeleteWhenStopped { get; set; } public bool DeleteWhenEnded { get; set; }
public abstract RunResult Run(); public abstract RunResult Run();
@@ -63,10 +63,7 @@ abstract class BackgroundTask
public void Pause() public void Pause()
{ {
if (State == .Ready || State == .Running) _taskManager.Pause(this);
{
State = .Paused;
}
} }
public void Abort() public void Abort()
@@ -20,6 +20,8 @@ class BackgroundTaskManager
private append BackgroundTaskList _waitingTasks = .(); private append BackgroundTaskList _waitingTasks = .();
private append Monitor _queueLock = .(); private append Monitor _queueLock = .();
private append Dictionary<BackgroundTask, BackgroundTask.RunState> _deferredStateChanges = .();
public void Init() public void Init()
{ {
_run = true; _run = true;
@@ -54,6 +56,25 @@ class BackgroundTaskManager
BackgroundTask.RunResult runResult = task.Run(); BackgroundTask.RunResult runResult = task.Run();
_runningTask = null; _runningTask = null;
if (_deferredStateChanges.TryGetValue(task, let desiredState))
{
// If the task already finished or aborted by itself, we don't care about the desired state, because it no longer applies.
if (runResult != .Finished && runResult != .Abort)
{
switch (desiredState)
{
case .Paused:
runResult = .Pause;
case .Aborted:
runResult = .Abort;
default:
Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}.");
}
}
_deferredStateChanges.Remove(task);
}
switch (runResult) switch (runResult)
{ {
case .Continue: case .Continue:
@@ -71,14 +92,14 @@ class BackgroundTaskManager
case .Finished: case .Finished:
task.State = .Finished; task.State = .Finished;
if (task.DeleteWhenStopped) if (task.DeleteWhenEnded)
{ {
delete task; delete task;
} }
case .Abort: case .Abort:
task.State = .Aborted; task.State = .Aborted;
if (task.DeleteWhenStopped) if (task.DeleteWhenEnded)
{ {
delete task; delete task;
} }
@@ -123,67 +144,32 @@ class BackgroundTaskManager
Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(task._taskManager == this);
Log.EngineLogger.AssertDebug(!task.Ended); Log.EngineLogger.AssertDebug(!task.Ended);
Application.Instance.InvokeOnMainThread(new () =>
{
if (task.State == .Paused) if (task.State == .Paused)
{ {
task.State = .Ready; _deferredStateChanges.Add(task, .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) internal void Pause(BackgroundTask task)
{ {
Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(task._taskManager == this);
Log.EngineLogger.AssertDebug(!task.Ended); Log.EngineLogger.AssertDebug(!task.Ended);
Application.Instance.InvokeOnMainThread(new () => if (task.State == .Ready || task.State == .Running)
{ {
if (task.State == .Ready) _deferredStateChanges.Add(task, .Paused);
{
task.State = .Ready;
using (_queueLock.Enter())
{
Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task));
_waitingTasks.AddLast(task);
} }
} }
return true;
});
}
internal void AbortTask(BackgroundTask task) internal void AbortTask(BackgroundTask task)
{ {
Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(task._taskManager == this);
Log.EngineLogger.AssertDebug(!task.Ended); Log.EngineLogger.AssertDebug(!task.Ended);
Application.Instance.InvokeOnMainThread(new () =>
{
if (task.State != .Aborted) if (task.State != .Aborted)
{ {
task.State = .Aborted; _deferredStateChanges.Add(task, .Aborted);
using (_queueLock.Enter())
{
_waitingTasks.Remove(task);
} }
if (task.DeleteWhenStopped)
{
delete task;
}
}
return true;
});
} }
public void ImGuiRender() public void ImGuiRender()
@@ -211,6 +197,59 @@ class BackgroundTaskManager
wtask.OnRenderPopup(); wtask.OnRenderPopup();
wtask = wtask._next; wtask = wtask._next;
} }
for (var (task, desiredState) in _deferredStateChanges)
{
bool deleteEntry = true;
switch (desiredState)
{
case .Ready:
task.State = .Ready;
// We assume this was a user interaction so this task will get priority.
_readyQueue.AddFirst(task);
case .Paused:
if (task.Ready)
{
Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task));
_waitingTasks.AddLast(task);
}
else if (task.Running)
{
// The task is running, we have to hope that it cooperates and handle the rest in the worker thread.
deleteEntry = false;
}
task.State = .Paused;
case .Aborted:
if (task.Ready || task.Paused)
{
_waitingTasks.Remove(task);
task.State = .Aborted;
if (task.DeleteWhenEnded)
{
delete task;
task = null;
}
}
else if (task.Running)
{
task.State = .Aborted;
// The task is running, we have to hope that it cooperates and handle the rest in the worker thread.
deleteEntry = false;
}
default:
Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}.");
}
if (deleteEntry)
{
@task.Remove();
}
}
} }
} }
} }
@@ -3,6 +3,7 @@ using System.IO;
using System.Collections; using System.Collections;
using System.Diagnostics; using System.Diagnostics;
using ImGui; using ImGui;
using System.Threading;
namespace GlitchyEditor.Multithreading; namespace GlitchyEditor.Multithreading;
@@ -14,20 +15,25 @@ class CopyBackgroundTask : BackgroundTask
private int _totalEntriesToCopy; private int _totalEntriesToCopy;
private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_); private append Queue<CopyInfo> _pathsToCopy = .() ~ ClearAndDeleteItems!(_);
private append Queue<CopyInfo> _scanQueue = .() ~ ClearAndDeleteItems!(_); private append Queue<CopyInfo> _scanQueue = .() ~ ClearAndDeleteItems!(_);
/// Contains the file paths, that will definitely exist after copying. This is used for file renaming during scanning.
private append HashSet<StringView> _targetPaths = .();
private enum OverwriteMode private enum ConflictResolutionMode
{ {
None, None,
OverwriteFile = 1, OverwriteFile = 1,
KeepBoth = 2, KeepBoth = 2,
CombineDirectories = 4, CombineDirectories = 4,
Skip = 8 SkipFiles = 8,
SkipDirectories = 16,
SkipNotFound = 32,
IgnoreUnexpectedErrors = 64
} }
private OverwriteMode _nextFileMode; private ConflictResolutionMode _nextFileMode;
private OverwriteMode _allFilesMode; private ConflictResolutionMode _allFilesMode;
private OverwriteMode CurrentFileMode => _nextFileMode | _allFilesMode; private ConflictResolutionMode CurrentFileMode => _nextFileMode | _allFilesMode;
private class CopyInfo private class CopyInfo
{ {
@@ -35,14 +41,14 @@ class CopyBackgroundTask : BackgroundTask
// If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory. // If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory.
public append String SubPath = .(); public append String SubPath = .();
public append String TargetPath = .(); public append String TargetPath = .();
public OverwriteMode OverwriteMode; public ConflictResolutionMode ConflictResolutionMode;
[AllowAppend] [AllowAppend]
public this(StringView sourcePath, StringView targetPath, OverwriteMode overwriteMode) public this(StringView sourcePath, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
{ {
SourcePath.Set(sourcePath); SourcePath.Set(sourcePath);
TargetPath.Set(targetPath); TargetPath.Set(targetPath);
OverwriteMode = overwriteMode; ConflictResolutionMode = conflictResolutionMode;
} }
} }
@@ -70,9 +76,10 @@ class CopyBackgroundTask : BackgroundTask
{ {
case None; case None;
case TargetDirectoryIsNotDirectory; case TargetDirectoryIsNotDirectory;
case SourceDoesntExist(String SourcePath);
case TargetFileExists(String FileName); case TargetFileExists(String FileName);
case TargetDirectoryExists(String DirectoryName); case TargetDirectoryExists(String DirectoryName);
case EntryNotFound(String SourcePath);
case UnexpectedError(String Message);
public void Dispose() public void Dispose()
{ {
@@ -80,12 +87,14 @@ class CopyBackgroundTask : BackgroundTask
{ {
case .None: case .None:
case .TargetDirectoryIsNotDirectory: case .TargetDirectoryIsNotDirectory:
case .SourceDoesntExist(let SourcePath):
delete SourcePath;
case .TargetFileExists(let FileName): case .TargetFileExists(let FileName):
delete FileName; delete FileName;
case .TargetDirectoryExists(let DirectoryName): case .TargetDirectoryExists(let DirectoryName):
delete DirectoryName; delete DirectoryName;
case .EntryNotFound(let SourcePath):
delete SourcePath;
case .UnexpectedError(let Message):
delete Message;
} }
} }
} }
@@ -101,10 +110,16 @@ class CopyBackgroundTask : BackgroundTask
} }
} }
void AddToCopyQueue(CopyInfo entry, StringView targetPath, ConflictResolutionMode conflictResolutionMode)
{
entry.TargetPath.Set(targetPath);
entry.ConflictResolutionMode = conflictResolutionMode;
_pathsToCopy.Add(entry);
_targetPaths.Add(entry.TargetPath);
}
while (!_scanQueue.IsEmpty && Running) while (!_scanQueue.IsEmpty && Running)
{ {
//Thread.Sleep(1000);
currentEntry = _scanQueue.PopFront(); currentEntry = _scanQueue.PopFront();
let sourcePath = (StringView)currentEntry.SourcePath; let sourcePath = (StringView)currentEntry.SourcePath;
@@ -117,49 +132,90 @@ class CopyBackgroundTask : BackgroundTask
if (Directory.Exists(sourcePath)) if (Directory.Exists(sourcePath))
{ {
if (Directory.Exists(targetPath)) bool skip = false;
if (Directory.Exists(targetPath) || _targetPaths.Contains(targetPath))
{ {
switch (CurrentFileMode) if (CurrentFileMode.HasFlag(.KeepBoth))
{
Path.FindFreePath(_targetDirectoryPath, fileName, "", targetPath, fileName, _targetPaths);
}
else if (CurrentFileMode.HasFlag(.SkipDirectories))
{
skip = true;
}
else if (Enum.HasAnyFlag(CurrentFileMode, .CombineDirectories))
{
// Intentionally do nothing.
}
else
{ {
case .KeepBoth, .CombineDirectories, .Skip:
default:
return .Err(.TargetDirectoryExists(new String(fileName))); return .Err(.TargetDirectoryExists(new String(fileName)));
} }
} }
if (!CurrentFileMode.HasFlag(.Skip)) if (skip)
{ {
_pathsToCopy.Add(currentEntry); delete currentEntry;
currentEntry.OverwriteMode = CurrentFileMode; }
else
{
// Current node is ready, add it to copy-queue.
AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
// Add nested files and directories to the scan-queue.
for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath)) for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath))
{ {
let entryPath = scope String(); let entryPath = scope String();
e.GetFilePath(entryPath); e.GetFilePath(entryPath);
let newEntry = new CopyInfo(entryPath, "", .None); let newEntry = new CopyInfo(entryPath, targetPath, .None);
_scanQueue.Add(newEntry); _scanQueue.Add(newEntry);
Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName); Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName);
} }
} }
_nextFileMode = .None; _nextFileMode = .None;
} }
else if (File.Exists(sourcePath)) else if (File.Exists(sourcePath))
{ {
if (File.Exists(targetPath)) bool skip = false;
if (File.Exists(targetPath) || _targetPaths.Contains(targetPath))
{ {
switch (CurrentFileMode) if (CurrentFileMode.HasFlag(.KeepBoth))
{
let fileExtension = scope String();
Path.GetExtension(targetPath, fileExtension);
StringView fileNameWithoutExtension = fileName.Substring(0..<^fileExtension.Length);
Path.FindFreePath(_targetDirectoryPath, fileNameWithoutExtension, fileExtension, targetPath, fileName, _targetPaths);
}
else if (CurrentFileMode.HasFlag(.SkipFiles))
{
skip = true;
}
else if (Enum.HasAnyFlag(CurrentFileMode, .OverwriteFile))
{
// Intentionally do nothing.
}
else
{ {
case .KeepBoth, .OverwriteFile, .Skip:
default:
return .Err(.TargetFileExists(new String(fileName))); return .Err(.TargetFileExists(new String(fileName)));
} }
} }
if (!CurrentFileMode.HasFlag(.Skip)) if (skip)
{ {
_pathsToCopy.Add(currentEntry); delete currentEntry;
currentEntry.OverwriteMode = CurrentFileMode;
} }
else
{
AddToCopyQueue(currentEntry, targetPath, CurrentFileMode);
}
_nextFileMode = .None; _nextFileMode = .None;
} }
} }
@@ -175,78 +231,76 @@ class CopyBackgroundTask : BackgroundTask
{ {
_currentError.Dispose(); _currentError.Dispose();
_currentError = .None; _currentError = .None;
if (CollectFilesToCopy() case .Err(out _currentError)) if (CollectFilesToCopy() case .Err(out _currentError) || Paused)
{ {
return .Pause; return .Pause;
} }
while (!_pathsToCopy.IsEmpty && Running) while (!_pathsToCopy.IsEmpty && Running)
{ {
//Thread.Sleep(1000);
CopyInfo currentPath = _pathsToCopy.PopFront(); CopyInfo currentPath = _pathsToCopy.PopFront();
defer if (CopyPath(currentPath) case .Err(out _currentError) || Paused)
{ {
delete currentPath; return .Pause;
} }
CopyPath(currentPath); delete currentPath;
} }
return .Finished; return .Finished;
} }
private Result<void> CopyPath(CopyInfo copyInfo) private Result<void, CopyError> CopyPath(CopyInfo copyInfo)
{ {
if (Directory.Exists(copyInfo.SourcePath)) if (Directory.Exists(copyInfo.SourcePath))
{ {
//bool forceOverwrite = false;
if (Directory.Exists(copyInfo.TargetPath)) if (Directory.Exists(copyInfo.TargetPath))
{ {
switch (CurrentFileMode) if (copyInfo.ConflictResolutionMode.HasFlag(.CombineDirectories))
{ {
case .KeepBoth: // We don't have to do anything, because we will be using the target directory.
String fileName = scope .();
Path.GetFileName(copyInfo.SourcePath, fileName);
Path.FindFreePath(_targetDirectoryPath, fileName, "", copyInfo.TargetPath..Clear());
case .CombineDirectories:
return .Ok; return .Ok;
default: }
return .Err;//(.TargetDirectoryExists); else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
{
return .Err(.UnexpectedError(new $"The target directory already exists, but it wasn't properly handled during the scanning phase."));
} }
} }
Directory.CreateDirectory(copyInfo.TargetPath); if (Directory.CreateDirectory(copyInfo.TargetPath) case .Err(let err) &&
!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
{
return .Err(.UnexpectedError(new $"Failed to create Directory {copyInfo.TargetPath}: {err}"));
}
} }
else if (File.Exists(copyInfo.SourcePath)) else if (File.Exists(copyInfo.SourcePath))
{ {
bool forceOverwrite = false; bool forceOverwrite = false;
if (File.Exists(copyInfo.TargetPath)) if (File.Exists(copyInfo.TargetPath))
{ {
switch (copyInfo.OverwriteMode) if (copyInfo.ConflictResolutionMode.HasFlag(.OverwriteFile))
{ {
case .KeepBoth:
String fileExtension = scope .();
Path.GetExtension(copyInfo.TargetPath, fileExtension);
String fileName = scope .();
Path.GetFileName(copyInfo.SourcePath, fileName);
Path.FindFreePath(_targetDirectoryPath, fileName.Substring(0..<^fileExtension.Length), fileExtension, copyInfo.TargetPath..Clear());
case .OverwriteFile:
forceOverwrite = true; forceOverwrite = true;
default: }
return .Err;//(.TargetFileExists); else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
{
return .Err(.UnexpectedError(new $"The target file already exists, but it wasn't properly handled during the scanning phase."));
} }
} }
File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite); if (File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite) case .Err(let err) &&
!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors))
{
return .Err(.UnexpectedError(new $"Failed to copy File {copyInfo.TargetPath}: {err}"));
}
} }
else else
{ {
// TODO error if (copyInfo.ConflictResolutionMode.HasFlag(.SkipNotFound))
return .Err; return .Ok;
else
return .Err(.EntryNotFound(new String(copyInfo.SourcePath)));
} }
return .Ok; return .Ok;
@@ -254,7 +308,9 @@ class CopyBackgroundTask : BackgroundTask
public override void OnRenderPopup() public override void OnRenderPopup()
{ {
if (ImGui.Begin("Copying files...")) String title = scope .("Copying files...");
if (ImGui.Begin(title, null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize))
{ {
if (ScanningFiles) if (ScanningFiles)
{ {
@@ -267,6 +323,31 @@ class CopyBackgroundTask : BackgroundTask
ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files."); ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files.");
} }
switch (_currentError)
{
case .TargetFileExists(let fileName):
ImGui.PushStyleColor(.Text, 0xFF0000FF);
ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists.");
ImGui.PopStyleColor();
case .TargetDirectoryExists(let directoryName):
ImGui.PushStyleColor(.Text, 0xFF0000FF);
ImGui.TextUnformatted(scope $"A directory with the name \"{directoryName}\" already exists.");
ImGui.PopStyleColor();
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) switch (_currentError)
{ {
case .None: case .None:
@@ -274,82 +355,156 @@ class CopyBackgroundTask : BackgroundTask
case .TargetDirectoryIsNotDirectory: case .TargetDirectoryIsNotDirectory:
// This shouldn't be possible // This shouldn't be possible
Debug.Break(); Debug.Break();
case .SourceDoesntExist(let SourcePath):
// This isn't good!
case .TargetFileExists(let fileName): case .TargetFileExists(let fileName):
ImGui.PushStyleColor(.Text, 0xFF0000FF); ImGui.TableNextRow();
ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists."); ImGui.TableSetColumnIndex(0);
ImGui.PopStyleColor();
if (ImGui.Button("Overwrite")) if (ImGui.Button("Overwrite the target file."))
{ {
_nextFileMode = .OverwriteFile; _nextFileMode = .OverwriteFile;
Continue(); Continue();
} }
ImGui.AttachTooltip("Overwrites the existing file. Will ask again if another conflict occurs."); ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Overwrite all conflicting target files."))
if (ImGui.Button("Overwrite All"))
{ {
_allFilesMode = .OverwriteFile; _allFilesMode |= .OverwriteFile;
Continue(); Continue();
} }
ImGui.AttachTooltip("Overwrites all existing files.");
if (ImGui.Button("Keep both")) ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
if (ImGui.Button("Rename copied file."))
{ {
_nextFileMode = .KeepBoth; _nextFileMode = .KeepBoth;
Continue(); 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."); ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Rename all conflicting copied files."))
if (ImGui.Button("Keep all"))
{ {
_allFilesMode = .KeepBoth; _allFilesMode |= .KeepBoth;
Continue(); 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")) ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
if (ImGui.Button("Skip this file."))
{ {
Abort(); _nextFileMode = .SkipFiles;
Continue();
} }
ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Skip all conflicting files."))
{
_allFilesMode |= .SkipFiles;
Continue();
}
case .TargetDirectoryExists(let directoryName): case .TargetDirectoryExists(let directoryName):
ImGui.PushStyleColor(.Text, 0xFF0000FF); ImGui.TableNextRow();
ImGui.TextUnformatted(scope $"A file with the name \"{directoryName}\" already exists."); ImGui.TableSetColumnIndex(0);
ImGui.PopStyleColor();
if (ImGui.Button("Combine")) if (ImGui.Button("Combine with target."))
{ {
_nextFileMode = .CombineDirectories; _nextFileMode |= .CombineDirectories;
Continue(); Continue();
} }
ImGui.AttachTooltip("Merges the copied directory into the target directory. Will ask again if another conflict occurs."); ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Combine all conflicting directories."))
if (ImGui.Button("Combine All"))
{ {
_allFilesMode = .CombineDirectories; _allFilesMode |= .CombineDirectories;
Continue(); Continue();
} }
ImGui.AttachTooltip("Merges all copied directories into the target directories.");
if (ImGui.Button("Keep both")) ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
if (ImGui.Button("Rename copied directory."))
{ {
_nextFileMode = .KeepBoth; _nextFileMode |= .KeepBoth;
Continue(); Continue();
} }
ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict. Will ask again if another conflict occurs."); ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Rename all conflicting directories."))
if (ImGui.Button("Keep all"))
{ {
_allFilesMode = .KeepBoth; _allFilesMode |= .KeepBoth;
Continue(); Continue();
} }
ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict.");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
if (ImGui.Button("Skip this directory."))
{
_nextFileMode |= .SkipDirectories;
Continue();
}
ImGui.TableSetColumnIndex(1);
if (ImGui.Button("Skip all conflicting directories."))
{
_allFilesMode |= .SkipFiles;
Continue();
}
case .EntryNotFound(let SourcePath):
if (ImGui.Button("Skip"))
{
_nextFileMode |= .SkipNotFound;
Continue();
}
ImGui.SameLine();
if (ImGui.Button("Skip all"))
{
_allFilesMode |= .SkipNotFound;
Continue();
}
case .UnexpectedError(let Message):
if (ImGui.Button("Retry"))
{
Continue();
}
ImGui.SameLine();
if (ImGui.Button("Ignore"))
{
_nextFileMode |= .IgnoreUnexpectedErrors;
Continue();
}
ImGui.SameLine();
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")) if (ImGui.Button("Cancel"))
{ {
Abort(); Abort();
} }
ImGui.EndTable();
} }
ImGui.End(); ImGui.End();
@@ -0,0 +1,22 @@
namespace System.Collections;
extension HashSet<T> : ICollection<T> where T : IHashable
{
public void ICollection<T>.Add(T item)
{
Add(item);
}
public void CopyTo(Span<T> span)
{
int i = 0;
for (T item in this)
{
span[i] = item;
i++;
if (i >= span.Length)
break;
}
}
}
@@ -19,4 +19,9 @@ extension Enum
else else
ClearFlag<T>(ref value, flag); ClearFlag<T>(ref value, flag);
} }
public static bool HasAnyFlag<T>(T value, T flags) where T : enum
{
return (value.Underlying & flags.Underlying) != default;
}
} }
+10 -10
View File
@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Collections;
namespace System.IO; namespace System.IO;
@@ -34,8 +35,8 @@ extension Path
path.Remove(0, 1); path.Remove(0, 1);
} }
// Compared to the original Combine, this one makes sure we don't add multiple seperators. Also makes sure, the path contains only the main Separator char.
public static void Combine(String target, params StringView[] components) new public static void Combine(String target, params StringView[] components)
{ {
for (var component in components) for (var component in components)
{ {
@@ -54,25 +55,24 @@ extension Path
* @param wantedFileName The wanted name of the file. If it already exists a number will be put behind it. * @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 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. * @param outFreePath The string that will contain the resulting filepath. Note: This will be cleared before writing the file name.
* @param outFreeFilename Optional, if set to a string, the final file name that is free will be stored in this varialbe.
* @param blockedPaths Optional, can be used to provide a collection of paths that will be considered as existing.
*/ */
public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath) public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath, String outFreeFilename = null, ICollection<StringView> blockedPaths = null)
{ {
int fileNumber = 0; int fileNumber = 0;
String currentFileName = scope $"{wantedName}{fileExtension}"; String currentFileName = outFreeFilename ?? scope .();
currentFileName.SetF($"{wantedName}{fileExtension}");
while (true) while (true)
{ {
Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName); Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName);
FileInfo targetInfo = scope FileInfo(outFreePath); if (!File.Exists(outFreePath) && !Directory.Exists(outFreePath) && !(blockedPaths?.Contains(outFreePath) ?? false))
if (!targetInfo.Exists)
{
break; break;
}
fileNumber++; fileNumber++;
currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}"); currentFileName.SetF($"{wantedName} ({fileNumber}){fileExtension}");
} }
} }
} }