Make drag-and-drop follow Windows Explorer and refresh the tree after folder operations.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -188,7 +188,11 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
public Task UpAsync() => ActivePane.UpAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public Task RefreshAsync() => ActivePane.RefreshAsync();
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
await Tree.RefreshAfterChangesAsync(AffectedDirectories(ActivePane.CurrentPath)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task GoAsync()
|
||||
@@ -275,10 +279,20 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task DeleteAsync()
|
||||
public Task DeleteAsync() => DeleteSelectedAsync(permanent: false);
|
||||
|
||||
public Task DeleteSelectedAsync(bool permanent)
|
||||
{
|
||||
var paths = SelectedPaths();
|
||||
return paths.Count == 0 ? Task.CompletedTask : _ops.DeleteAsync(paths);
|
||||
var paths = SelectedPaths()
|
||||
.Where(p => !LocationRoots.IsVirtual(p) && !PathRules.IsDriveRoot(p))
|
||||
.ToList();
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Footer = permanent ? "Deleting permanently…" : "Moving to Recycle Bin…";
|
||||
return _ops.DeleteAsync(paths, permanent);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -303,7 +317,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
|
||||
_ops.NewFolder(ActivePane.CurrentPath);
|
||||
EnqueueReconcile(ActivePane.CurrentPath);
|
||||
_ = ActivePane.RefreshAsync();
|
||||
_ = RefreshFolderViewsAsync(ActivePane.CurrentPath);
|
||||
}
|
||||
|
||||
public void RenameSelected(string newName)
|
||||
@@ -316,7 +330,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
|
||||
_ops.Rename(item.FullPath, newName);
|
||||
EnqueueReconcile(ActivePane.CurrentPath);
|
||||
_ = ActivePane.RefreshAsync();
|
||||
_ = RefreshFolderViewsAsync(ActivePane.CurrentPath);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -619,10 +633,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
{
|
||||
foreach (var pane in new[] { tab.Left, tab.Right })
|
||||
{
|
||||
if (LocationRoots.IsVirtual(pane.CurrentPath))
|
||||
{
|
||||
await pane.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
await pane.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,17 +649,29 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DropAsync(IReadOnlyList<string> files, string targetDirectory, bool move)
|
||||
public async Task DropAsync(IReadOnlyList<string> files, string targetDirectory, DropAction action)
|
||||
{
|
||||
if (files.Count == 0)
|
||||
if (files.Count == 0 || action is DropAction.None || DragDropPolicy.IsInvalidTarget(files, targetDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (move)
|
||||
Footer = action switch
|
||||
{
|
||||
DropAction.Move => "Moving…",
|
||||
DropAction.Link => "Creating shortcuts…",
|
||||
_ => "Copying…"
|
||||
};
|
||||
|
||||
if (action == DropAction.Move)
|
||||
{
|
||||
await _ops.MoveAsync(files, targetDirectory).ConfigureAwait(true);
|
||||
}
|
||||
else if (action == DropAction.Link)
|
||||
{
|
||||
await _ops.CreateShortcutsAsync(files, targetDirectory).ConfigureAwait(true);
|
||||
await RefreshFolderViewsAsync(targetDirectory).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _ops.CopyAsync(files, targetDirectory).ConfigureAwait(true);
|
||||
@@ -697,8 +720,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
|
||||
foreach (var part in path.Split('|', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var dir = Directory.Exists(part) ? part : PathRules.Parent(part);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
foreach (var dir in AffectedDirectories(part))
|
||||
{
|
||||
dirs.Add(dir);
|
||||
}
|
||||
@@ -706,7 +728,11 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
Add(job.SourcePath);
|
||||
Add(job.DestinationPath);
|
||||
if (job.Op != TransferOp.Delete)
|
||||
{
|
||||
Add(job.DestinationPath);
|
||||
}
|
||||
|
||||
foreach (var extra in job.AdditionalSources)
|
||||
{
|
||||
Add(extra);
|
||||
@@ -717,12 +743,54 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
EnqueueReconcile(dir);
|
||||
}
|
||||
|
||||
if (job.Status == TransferStatus.Failed)
|
||||
{
|
||||
Footer = job.Error ?? "Delete failed.";
|
||||
}
|
||||
else if (job.Op == TransferOp.Delete)
|
||||
{
|
||||
Footer = string.Equals(job.DestinationPath, "permanent", StringComparison.Ordinal)
|
||||
? "Deleted permanently."
|
||||
: "Moved to Recycle Bin.";
|
||||
}
|
||||
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
if (ActiveTab.IsSplit)
|
||||
{
|
||||
var other = ActivePane == ActiveTab.Left ? ActiveTab.Right : ActiveTab.Left;
|
||||
await other.RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
await Tree.RefreshAfterChangesAsync(dirs).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task RefreshFolderViewsAsync(string directory)
|
||||
{
|
||||
await ActivePane.RefreshAsync().ConfigureAwait(true);
|
||||
await Tree.RefreshAfterChangesAsync(AffectedDirectories(directory)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> AffectedDirectories(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || LocationRoots.IsVirtual(path))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var folder = Directory.Exists(path) ? path : PathRules.Parent(path);
|
||||
if (string.IsNullOrWhiteSpace(folder) || LocationRoots.IsVirtual(folder))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return folder;
|
||||
var parent = PathRules.Parent(folder);
|
||||
if (!string.IsNullOrWhiteSpace(parent)
|
||||
&& !LocationRoots.IsVirtual(parent)
|
||||
&& !NavigationTreeViewModel.PathsEqual(parent, folder))
|
||||
{
|
||||
yield return parent;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueReconcile(string path)
|
||||
|
||||
@@ -75,12 +75,17 @@ public sealed class NavigationTreeViewModel
|
||||
Roots.Add(thisPc);
|
||||
|
||||
var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(true);
|
||||
foreach (var source in sources.Where(s => !s.Kind.IsNetwork()))
|
||||
foreach (var source in sources.Where(s => !s.Kind.IsNetwork())
|
||||
.OrderBy(s => PathRules.DriveLetterSortKey(s.LastRootPath))
|
||||
.ThenBy(s => s.DisplayName, StringComparer.CurrentCultureIgnoreCase))
|
||||
{
|
||||
thisPc.Children.Add(CreateSourceNode(source, _sources.CanForget(source)));
|
||||
}
|
||||
|
||||
var network = sources.Where(s => s.Kind.IsNetwork()).ToList();
|
||||
var network = sources.Where(s => s.Kind.IsNetwork())
|
||||
.OrderBy(s => PathRules.DriveLetterSortKey(s.LastRootPath))
|
||||
.ThenBy(s => s.DisplayName, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
if (prefs.GroupNetworkPlaces && network.Count > 0)
|
||||
{
|
||||
var group = new NavNodeViewModel
|
||||
@@ -174,7 +179,7 @@ public sealed class NavigationTreeViewModel
|
||||
{
|
||||
var child = new NavNodeViewModel
|
||||
{
|
||||
Label = dir.Name,
|
||||
Label = dir.DisplayName ?? dir.Name,
|
||||
Path = dir.FullPath
|
||||
};
|
||||
AddPlaceholder(child);
|
||||
@@ -184,6 +189,78 @@ public sealed class NavigationTreeViewModel
|
||||
node.ChildrenLoaded = true;
|
||||
}
|
||||
|
||||
public async Task RefreshAfterChangesAsync(IEnumerable<string> directories)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var raw in directories)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw) || LocationRoots.IsVirtual(raw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dir = PathRules.EnsureDirectoryTrailingSlashIfRoot(PathRules.FromExtended(raw).TrimEnd('\\'));
|
||||
if (!seen.Add(dir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var node = FindByPath(Roots, dir);
|
||||
if (node is null || !node.ChildrenLoaded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await SyncChildrenAsync(node).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncChildrenAsync(NavNodeViewModel node)
|
||||
{
|
||||
if (node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var listing = await _browse.ListAsync(node.Path).ConfigureAwait(true);
|
||||
var dirs = listing.Items
|
||||
.Where(i => i.IsDirectory)
|
||||
.OrderBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase)
|
||||
.Take(200)
|
||||
.ToList();
|
||||
|
||||
var previous = node.Children.Where(c => !c.IsPlaceholder).ToList();
|
||||
var next = new List<NavNodeViewModel>(dirs.Count);
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
var match = previous.FirstOrDefault(c => PathsEqual(c.Path, dir.FullPath));
|
||||
if (match is null)
|
||||
{
|
||||
match = new NavNodeViewModel
|
||||
{
|
||||
Label = dir.DisplayName ?? dir.Name,
|
||||
Path = dir.FullPath
|
||||
};
|
||||
AddPlaceholder(match);
|
||||
}
|
||||
else
|
||||
{
|
||||
match.Label = dir.DisplayName ?? dir.Name;
|
||||
match.Path = dir.FullPath;
|
||||
}
|
||||
|
||||
next.Add(match);
|
||||
}
|
||||
|
||||
node.Children.Clear();
|
||||
foreach (var child in next)
|
||||
{
|
||||
node.Children.Add(child);
|
||||
}
|
||||
|
||||
node.ChildrenLoaded = true;
|
||||
}
|
||||
|
||||
public async Task RevealPathAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || Roots.Count == 0)
|
||||
|
||||
Reference in New Issue
Block a user