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:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user