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:
2026-08-23 16:33:49 +02:00
parent 09a8cfafa3
commit d79605cde9
15 changed files with 879 additions and 58 deletions

View File

@@ -14,6 +14,11 @@ public partial class MainWindow : Window
{
private Point _dragStart;
private bool _dragPending;
private MouseButton _dragButton;
private FolderItemViewModel? _dragItem;
private bool _suppressItemContextMenu;
private bool _incomingRightDrag;
private bool _sourceRightDrag;
public MainWindow()
{
@@ -132,6 +137,13 @@ public partial class MainWindow : Window
private void OnTreeContextOpening(object sender, ContextMenuEventArgs e)
{
if (_suppressItemContextMenu)
{
e.Handled = true;
_suppressItemContextMenu = false;
return;
}
var node = FindTreeNode(e.OriginalSource as DependencyObject);
if (node is null || !node.CanRemove)
{
@@ -378,34 +390,85 @@ public partial class MainWindow : Window
private void OnListMouseDown(object sender, MouseButtonEventArgs e)
{
_suppressItemContextMenu = false;
_dragStart = e.GetPosition(null);
_dragPending = true;
_dragButton = e.ChangedButton;
_dragItem = HitTestFolderItem(sender as DependencyObject, e.GetPosition((IInputElement)sender));
}
private void OnListContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (_suppressItemContextMenu)
{
e.Handled = true;
_suppressItemContextMenu = false;
}
}
private void OnListDragOver(object sender, DragEventArgs e)
{
e.Effects = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 ? DragDropEffects.Move : DragDropEffects.Copy;
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
{
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
ActivatePaneFromList(list);
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
var dest = ResolveDropDirectory(list, e);
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
{
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
if (_sourceRightDrag || _incomingRightDrag)
{
e.Effects = DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link;
}
else
{
e.Effects = ToEffects(ResolveDropAction(e, files, dest));
}
e.Handled = true;
}
private async void OnListDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop) || sender is not ListView list)
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
{
return;
}
ActivatePaneFromList(list);
var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
var move = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 || e.AllowedEffects == DragDropEffects.Move;
await Vm.DropAsync(files, Vm.ActivePane.CurrentPath, move).ConfigureAwait(true);
await Vm.RefreshAsync().ConfigureAwait(true);
var dest = ResolveDropDirectory(list, e);
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
{
return;
}
var right = IsRightDrop(e);
_incomingRightDrag = false;
_sourceRightDrag = false;
if (right)
{
ShowDropMenu(list, e.GetPosition(list), files, dest);
return;
}
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (!_dragPending || e.LeftButton != MouseButtonState.Pressed)
var held = _dragButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed
|| _dragButton == MouseButton.Right && e.RightButton == MouseButtonState.Pressed;
if (!_dragPending || !held)
{
return;
}
@@ -418,14 +481,219 @@ public partial class MainWindow : Window
}
_dragPending = false;
var paths = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToArray();
if (paths.Length == 0)
var selected = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
IReadOnlyList<string> paths;
if (_dragItem is not null && (selected.Count == 0
|| selected.TrueForAll(p => !p.Equals(_dragItem.FullPath, StringComparison.OrdinalIgnoreCase))))
{
paths = [_dragItem.FullPath];
}
else
{
paths = selected;
}
if (paths.Count == 0)
{
return;
}
var data = new DataObject(DataFormats.FileDrop, paths);
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move);
_sourceRightDrag = _dragButton == MouseButton.Right;
_suppressItemContextMenu = _sourceRightDrag;
var data = new DataObject(DataFormats.FileDrop, paths.ToArray());
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
_sourceRightDrag = false;
_incomingRightDrag = false;
}
protected override void OnQueryContinueDrag(QueryContinueDragEventArgs e)
{
if (e.EscapePressed)
{
e.Action = DragAction.Cancel;
e.Handled = true;
return;
}
var buttonHeld = (e.KeyStates & DragDropKeyStates.LeftMouseButton) != 0
|| (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
e.Action = buttonHeld ? DragAction.Continue : DragAction.Drop;
e.Handled = true;
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseUp(e);
if (e.ChangedButton == _dragButton)
{
_dragPending = false;
}
}
private static bool TryGetDropFiles(DragEventArgs e, out string[] files)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] dropped && dropped.Length > 0)
{
files = dropped;
return true;
}
files = [];
return false;
}
private string? ResolveDropDirectory(ListView list, DragEventArgs e)
{
var item = HitTestFolderItem(list, e.GetPosition(list));
if (item is { IsDirectory: true })
{
return item.FullPath;
}
var tab = Vm.ActiveTab;
var path = list.ItemsSource == tab.Right.Items ? tab.Right.CurrentPath : tab.Left.CurrentPath;
return LocationRoots.IsVirtual(path) ? null : path;
}
private static FolderItemViewModel? HitTestFolderItem(DependencyObject? origin, Point point)
{
if (origin is not Visual visual)
{
return null;
}
var hit = VisualTreeHelper.HitTest(visual, point)?.VisualHit as DependencyObject;
while (hit is not null)
{
if (hit is ListViewItem { DataContext: FolderItemViewModel item })
{
return item;
}
hit = VisualTreeHelper.GetParent(hit);
}
return null;
}
private static DragDropEffects ToEffects(DropAction action)
=> action switch
{
DropAction.Copy => DragDropEffects.Copy,
DropAction.Move => DragDropEffects.Move,
DropAction.Link => DragDropEffects.Link,
_ => DragDropEffects.None
};
private DropAction ResolveDropAction(DragEventArgs e, IReadOnlyList<string> files, string dest)
{
var sameVolume = files.All(f => PathRules.IsSameVolume(f, dest));
return DragDropPolicy.Resolve(
(e.KeyStates & DragDropKeyStates.ControlKey) != 0,
(e.KeyStates & DragDropKeyStates.ShiftKey) != 0,
(e.KeyStates & DragDropKeyStates.AltKey) != 0,
sameVolume);
}
private bool IsRightDrop(DragEventArgs e)
=> _sourceRightDrag || _incomingRightDrag || (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
private void ShowDropMenu(FrameworkElement target, Point position, IReadOnlyList<string> files, string dest)
{
var menu = new ContextMenu
{
PlacementTarget = target,
Placement = PlacementMode.RelativePoint,
HorizontalOffset = position.X,
VerticalOffset = position.Y
};
void Add(string header, DropAction action)
{
var item = new MenuItem { Header = header };
item.Click += async (_, _) => await ApplyDropAsync(files, dest, action).ConfigureAwait(true);
menu.Items.Add(item);
}
Add("Copy here", DropAction.Copy);
Add("Move here", DropAction.Move);
Add("Create shortcuts here", DropAction.Link);
menu.Items.Add(new Separator());
menu.Items.Add(new MenuItem { Header = "Cancel" });
menu.IsOpen = true;
}
private async Task ApplyDropAsync(IReadOnlyList<string> files, string dest, DropAction action)
{
if (action == DropAction.None)
{
return;
}
await Vm.DropAsync(files, dest, action).ConfigureAwait(true);
await Vm.RefreshAsync().ConfigureAwait(true);
}
private void OnTreeDragOver(object sender, DragEventArgs e)
{
if (!TryGetDropFiles(e, out var files))
{
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
var dest = HitTestTreePath(e);
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
{
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
e.Effects = _incomingRightDrag || _sourceRightDrag
? DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link
: ToEffects(ResolveDropAction(e, files, dest));
e.Handled = true;
}
private async void OnTreeDrop(object sender, DragEventArgs e)
{
if (!TryGetDropFiles(e, out var files))
{
return;
}
var dest = HitTestTreePath(e);
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
{
return;
}
var right = IsRightDrop(e);
_incomingRightDrag = false;
_sourceRightDrag = false;
if (right)
{
ShowDropMenu(NavTree, e.GetPosition(NavTree), files, dest);
return;
}
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
}
private string? HitTestTreePath(DragEventArgs e)
{
var origin = NavTree.InputHitTest(e.GetPosition(NavTree)) as DependencyObject
?? e.OriginalSource as DependencyObject;
var node = FindTreeNode(origin);
if (node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
{
return null;
}
return node.Path;
}
private async void OnSearchDoubleClick(object sender, MouseButtonEventArgs e)
@@ -492,6 +760,33 @@ public partial class MainWindow : Window
private void OnCtxOpen(object sender, RoutedEventArgs e) => Vm.OpenSelectedCommand.Execute(null);
private async void OnCtxDelete(object sender, RoutedEventArgs e)
=> await DeleteSelectedAsync().ConfigureAwait(true);
private async Task DeleteSelectedAsync()
{
if (Vm.ActivePane.SelectedItems.Count == 0)
{
return;
}
var permanent = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
if (permanent)
{
var count = Vm.ActivePane.SelectedItems.Count;
var name = Vm.ActivePane.SelectedItems[0].Name;
var text = count == 1
? $"Are you sure you want to permanently delete '{name}'?"
: $"Are you sure you want to permanently delete these {count} items?";
if (MessageBox.Show(this, text, "Delete Permanently", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
{
return;
}
}
await Vm.DeleteSelectedAsync(permanent).ConfigureAwait(true);
}
private void OnCtxRename(object sender, RoutedEventArgs e)
{
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
@@ -576,7 +871,8 @@ public partial class MainWindow : Window
}
else if (e.Key == Key.Delete)
{
await Vm.DeleteAsync().ConfigureAwait(true);
await DeleteSelectedAsync().ConfigureAwait(true);
e.Handled = true;
}
else if (ctrl && e.Key == Key.T)
{