903 lines
27 KiB
C#
903 lines
27 KiB
C#
using System.ComponentModel;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using Explorer.Domain;
|
|
using Explorer.Presentation;
|
|
using Explorer.Presentation.ViewModels;
|
|
|
|
namespace Explorer.App;
|
|
|
|
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()
|
|
{
|
|
InitializeComponent();
|
|
DataContextChanged += (_, _) =>
|
|
{
|
|
if (DataContext is MainViewModel vm)
|
|
{
|
|
vm.PropertyChanged += (_, e) =>
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.Theme))
|
|
{
|
|
ApplyTheme(vm.Theme);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
private MainViewModel Vm => (MainViewModel)DataContext;
|
|
|
|
private void ApplyTheme(string theme)
|
|
{
|
|
var dicts = System.Windows.Application.Current.Resources.MergedDictionaries;
|
|
dicts.Clear();
|
|
var uri = theme == "Light"
|
|
? new Uri("Themes/Light.xaml", UriKind.Relative)
|
|
: new Uri("Themes/Dark.xaml", UriKind.Relative);
|
|
dicts.Add(new ResourceDictionary { Source = uri });
|
|
}
|
|
|
|
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.ChangedButton != MouseButton.Left)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.ClickCount == 2)
|
|
{
|
|
ToggleMaximized();
|
|
return;
|
|
}
|
|
|
|
DragMove();
|
|
}
|
|
|
|
private void OnMinimize(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
|
|
|
private void OnMaxRestore(object sender, RoutedEventArgs e) => ToggleMaximized();
|
|
|
|
private void OnCloseWindow(object sender, RoutedEventArgs e) => Close();
|
|
|
|
private void ToggleMaximized()
|
|
{
|
|
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
|
MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
|
|
}
|
|
|
|
private void OnPaneChromeMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is FrameworkElement { DataContext: ExplorerPaneViewModel pane })
|
|
{
|
|
Vm.ActiveTab.Activate(pane);
|
|
}
|
|
}
|
|
|
|
private void OnPathKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Enter)
|
|
{
|
|
Vm.GoCommand.Execute(null);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void OnPathHistorySelected(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (sender is not ComboBox combo || e.AddedItems.Count == 0 || e.AddedItems[0] is not string path)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (NavigationTreeViewModel.PathsEqual(path, Vm.ActivePane.CurrentPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (combo.IsDropDownOpen || combo.IsKeyboardFocusWithin)
|
|
{
|
|
Vm.PathText = path;
|
|
Vm.GoCommand.Execute(null);
|
|
}
|
|
}
|
|
|
|
private void OnSearchKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Enter)
|
|
{
|
|
Vm.SearchCommand.Execute(null);
|
|
}
|
|
}
|
|
|
|
private async void OnTreeSelected(object sender, RoutedPropertyChangedEventArgs<object> e)
|
|
{
|
|
if (Vm.Tree.IsRevealing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.NewValue is NavNodeViewModel node)
|
|
{
|
|
await Vm.TreeSelectAsync(node).ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
node.IsSelected = true;
|
|
RemoveLocationMenu.Tag = node.Path;
|
|
}
|
|
|
|
private async void OnRemoveLocation(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = (sender as FrameworkElement)?.Tag as string
|
|
?? RemoveLocationMenu.Tag as string
|
|
?? Vm.ActivePane.SelectedItems.FirstOrDefault()?.FullPath
|
|
?? (Vm.Tree.Roots.SelectMany(FlattenTree).FirstOrDefault(n => n.IsSelected && n.CanRemove)?.Path);
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var label = Vm.Tree.Roots.SelectMany(FlattenTree).FirstOrDefault(n => NavigationTreeViewModel.PathsEqual(n.Path, path))?.Label
|
|
?? path;
|
|
var confirm = MessageBox.Show(
|
|
this,
|
|
$"Remove “{label}” from Explorer?\n\nThe link and its index data will be deleted. Files on disk or the server are not touched.\n\nIf Windows still has this location, it will appear again.",
|
|
"Remove from Explorer",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Question);
|
|
if (confirm != MessageBoxResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.ForgetSourceAsync(path).ConfigureAwait(true);
|
|
}
|
|
|
|
private static IEnumerable<NavNodeViewModel> FlattenTree(NavNodeViewModel node)
|
|
{
|
|
yield return node;
|
|
foreach (var child in node.Children.Where(c => !c.IsPlaceholder).SelectMany(FlattenTree))
|
|
{
|
|
yield return child;
|
|
}
|
|
}
|
|
|
|
private static NavNodeViewModel? FindTreeNode(DependencyObject? origin)
|
|
{
|
|
while (origin is not null)
|
|
{
|
|
if (origin is TreeViewItem { DataContext: NavNodeViewModel node })
|
|
{
|
|
return node;
|
|
}
|
|
|
|
origin = origin is Visual ? VisualTreeHelper.GetParent(origin) : LogicalTreeHelper.GetParent(origin);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async void OnTreeExpanded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is TreeViewItem { DataContext: NavNodeViewModel node })
|
|
{
|
|
await Vm.Tree.EnsureChildrenAsync(node).ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
private void OnTabChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (Tabs.SelectedItem is ExplorerTabViewModel tab)
|
|
{
|
|
Vm.ActiveTab = tab;
|
|
}
|
|
}
|
|
|
|
private void OnPaneSplitLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not Grid grid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplySplitFromGrid(grid);
|
|
if (Equals(grid.Tag, "split-wired"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
grid.Tag = "split-wired";
|
|
ExplorerTabViewModel? current = null;
|
|
PropertyChangedEventHandler? handler = null;
|
|
|
|
void Wire(ExplorerTabViewModel? tab)
|
|
{
|
|
if (current is not null && handler is not null)
|
|
{
|
|
current.PropertyChanged -= handler;
|
|
}
|
|
|
|
current = tab;
|
|
if (tab is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
handler = (_, args) =>
|
|
{
|
|
if (args.PropertyName is nameof(ExplorerTabViewModel.IsSplit)
|
|
or nameof(ExplorerTabViewModel.SplitRatio))
|
|
{
|
|
ApplySplitLayout(grid, tab);
|
|
}
|
|
};
|
|
tab.PropertyChanged += handler;
|
|
ApplySplitLayout(grid, tab);
|
|
}
|
|
|
|
Wire(grid.DataContext as ExplorerTabViewModel);
|
|
grid.DataContextChanged += (_, _) => Wire(grid.DataContext as ExplorerTabViewModel);
|
|
grid.Unloaded += (_, _) => Wire(null);
|
|
}
|
|
|
|
private void OnSplitDragCompleted(object sender, DragCompletedEventArgs e)
|
|
{
|
|
if (sender is not GridSplitter splitter || splitter.Parent is not Grid grid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (grid.DataContext is not ExplorerTabViewModel tab || !tab.IsSplit)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var leftWidth = grid.ColumnDefinitions[0].ActualWidth;
|
|
var rightWidth = grid.ColumnDefinitions[2].ActualWidth;
|
|
var total = leftWidth + rightWidth;
|
|
if (total < 8)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tab.SetSplitRatio(leftWidth / total);
|
|
ApplySplitLayout(grid, tab);
|
|
}
|
|
|
|
private static void ApplySplitFromGrid(Grid grid)
|
|
{
|
|
if (grid.DataContext is ExplorerTabViewModel tab)
|
|
{
|
|
ApplySplitLayout(grid, tab);
|
|
}
|
|
}
|
|
|
|
private static void ApplySplitLayout(Grid grid, ExplorerTabViewModel tab)
|
|
{
|
|
var left = grid.ColumnDefinitions[0];
|
|
var mid = grid.ColumnDefinitions[1];
|
|
var right = grid.ColumnDefinitions[2];
|
|
if (tab.IsSplit)
|
|
{
|
|
var ratio = Math.Clamp(tab.SplitRatio, ExplorerTabViewModel.MinSplitRatio, ExplorerTabViewModel.MaxSplitRatio);
|
|
left.Width = new GridLength(ratio, GridUnitType.Star);
|
|
left.MinWidth = 140;
|
|
mid.Width = new GridLength(6);
|
|
mid.MinWidth = 6;
|
|
right.Width = new GridLength(1.0 - ratio, GridUnitType.Star);
|
|
right.MinWidth = 140;
|
|
}
|
|
else
|
|
{
|
|
left.Width = new GridLength(1, GridUnitType.Star);
|
|
left.MinWidth = 0;
|
|
mid.Width = new GridLength(0);
|
|
mid.MinWidth = 0;
|
|
right.Width = new GridLength(0);
|
|
right.MinWidth = 0;
|
|
}
|
|
|
|
foreach (var splitter in grid.Children.OfType<GridSplitter>())
|
|
{
|
|
splitter.IsHitTestVisible = tab.IsSplit;
|
|
}
|
|
}
|
|
|
|
private void OnCloseTab(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is Button { Tag: ExplorerTabViewModel tab })
|
|
{
|
|
Vm.CloseTab(tab);
|
|
}
|
|
}
|
|
|
|
private async void OnItemDoubleClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is ListView { SelectedItem: FolderItemViewModel item })
|
|
{
|
|
ActivatePaneFromList((ListView)sender);
|
|
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
|
|
Vm.PathText = Vm.ActivePane.CurrentPath;
|
|
}
|
|
}
|
|
|
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (sender is not ListView list)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActivatePaneFromList(list);
|
|
Vm.ActivePane.SelectedItems.Clear();
|
|
foreach (FolderItemViewModel item in list.SelectedItems)
|
|
{
|
|
Vm.ActivePane.SelectedItems.Add(item);
|
|
}
|
|
|
|
Vm.RefreshCloudActions();
|
|
}
|
|
|
|
private void OnPaneFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is ListView list)
|
|
{
|
|
ActivatePaneFromList(list);
|
|
}
|
|
}
|
|
|
|
private void ActivatePaneFromList(ListView list)
|
|
{
|
|
var tab = Vm.ActiveTab;
|
|
if (list.ItemsSource == tab.Right.Items)
|
|
{
|
|
tab.Activate(tab.Right);
|
|
}
|
|
else
|
|
{
|
|
tab.Activate(tab.Left);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
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 (!TryGetDropFiles(e, out var files) || sender is not ListView list)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActivatePaneFromList(list);
|
|
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);
|
|
var held = _dragButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed
|
|
|| _dragButton == MouseButton.Right && e.RightButton == MouseButtonState.Pressed;
|
|
if (!_dragPending || !held)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var pos = e.GetPosition(null);
|
|
if (Math.Abs(pos.X - _dragStart.X) < SystemParameters.MinimumHorizontalDragDistance
|
|
&& Math.Abs(pos.Y - _dragStart.Y) < SystemParameters.MinimumVerticalDragDistance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dragPending = false;
|
|
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;
|
|
}
|
|
|
|
_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)
|
|
{
|
|
if (sender is ListView { SelectedItem: FolderItemViewModel item })
|
|
{
|
|
Vm.Search.IsOpen = false;
|
|
if (item.IsDirectory)
|
|
{
|
|
await Vm.ActivePane.NavigateAsync(item.FullPath).ConfigureAwait(true);
|
|
}
|
|
else
|
|
{
|
|
await Vm.ActivePane.NavigateAsync(PathRules.Parent(item.FullPath)).ConfigureAwait(true);
|
|
}
|
|
|
|
Vm.PathText = Vm.ActivePane.CurrentPath;
|
|
}
|
|
}
|
|
|
|
private async void OnStorageTreeDoubleClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.OriginalSource is DependencyObject origin && IsInsideButton(origin))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Vm.Analysis.CanAct)
|
|
{
|
|
await Vm.OpenStorageHereAsync().ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
private static bool IsInsideButton(DependencyObject origin)
|
|
{
|
|
for (var current = origin; current is not null;)
|
|
{
|
|
if (current is Button)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
current = current is System.Windows.Media.Visual
|
|
? System.Windows.Media.VisualTreeHelper.GetParent(current)
|
|
: LogicalTreeHelper.GetParent(current);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async void OnStorageRankDoubleClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (Vm.Analysis.CanAct)
|
|
{
|
|
await Vm.OpenStorageHereAsync().ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
private void OnCloseSearch(object sender, RoutedEventArgs e) => Vm.Search.IsOpen = false;
|
|
private void OnCloseAnalysis(object sender, RoutedEventArgs e) => Vm.Analysis.Close();
|
|
private void OnCloseDuplicates(object sender, RoutedEventArgs e) => Vm.Duplicates.IsOpen = false;
|
|
|
|
private void OnBuildIndex(object sender, RoutedEventArgs e) => Vm.BuildIndexCommand.Execute(null);
|
|
|
|
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();
|
|
if (item is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var name = PromptWindow.Ask(this, "Rename", "New name:", item.Name);
|
|
if (!string.IsNullOrWhiteSpace(name) && name != item.Name)
|
|
{
|
|
Vm.RenameSelected(name);
|
|
}
|
|
}
|
|
|
|
private async void OnOpenSettings(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new SettingsWindow(Vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
await Vm.RefreshForgetActionAsync().ConfigureAwait(true);
|
|
}
|
|
|
|
private void OnAddNetwork(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = PromptWindow.Ask(this, "Add network location", "Network path (\\\\server\\share):", @"\\");
|
|
if (!string.IsNullOrWhiteSpace(path) && path != @"\\")
|
|
{
|
|
Vm.PromptUnc = path;
|
|
Vm.AddNetworkCommand.Execute(null);
|
|
}
|
|
}
|
|
|
|
private async void OnAddOneDrive(object sender, RoutedEventArgs e)
|
|
=> await AddCloudFolderAsync("Add OneDrive folder", MainViewModel.OneDriveProviderId).ConfigureAwait(true);
|
|
|
|
private async void OnAddGoogleDrive(object sender, RoutedEventArgs e)
|
|
=> await AddCloudFolderAsync("Add Google Drive folder", MainViewModel.GoogleDriveProviderId).ConfigureAwait(true);
|
|
|
|
private async void OnAddNextcloud(object sender, RoutedEventArgs e)
|
|
=> await AddCloudFolderAsync("Add Nextcloud folder", MainViewModel.NextcloudProviderId).ConfigureAwait(true);
|
|
|
|
private async Task AddCloudFolderAsync(string title, string providerId)
|
|
{
|
|
var picker = new Microsoft.Win32.OpenFolderDialog
|
|
{
|
|
Title = title,
|
|
Multiselect = false
|
|
};
|
|
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.AddCloudFolderAsync(picker.FolderName, providerId).ConfigureAwait(true);
|
|
}
|
|
|
|
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
|
|
var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
|
|
if (e.Key == Key.F5)
|
|
{
|
|
await Vm.RefreshAsync().ConfigureAwait(true);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F2)
|
|
{
|
|
OnCtxRename(sender, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (ctrl && e.Key == Key.C)
|
|
{
|
|
Vm.Copy();
|
|
}
|
|
else if (ctrl && e.Key == Key.X)
|
|
{
|
|
Vm.Cut();
|
|
}
|
|
else if (ctrl && e.Key == Key.V)
|
|
{
|
|
await Vm.PasteAsync().ConfigureAwait(true);
|
|
}
|
|
else if (e.Key == Key.Delete)
|
|
{
|
|
await DeleteSelectedAsync().ConfigureAwait(true);
|
|
e.Handled = true;
|
|
}
|
|
else if (ctrl && e.Key == Key.T)
|
|
{
|
|
await Vm.NewTabAsync().ConfigureAwait(true);
|
|
}
|
|
else if (ctrl && e.Key == Key.W)
|
|
{
|
|
Vm.CloseTab(Vm.ActiveTab);
|
|
}
|
|
else if (alt && e.Key == Key.Left)
|
|
{
|
|
await Vm.BackAsync().ConfigureAwait(true);
|
|
}
|
|
else if (alt && e.Key == Key.Right)
|
|
{
|
|
await Vm.ForwardAsync().ConfigureAwait(true);
|
|
}
|
|
else if (alt && e.Key == Key.Up)
|
|
{
|
|
await Vm.UpAsync().ConfigureAwait(true);
|
|
}
|
|
else if (e.Key == Key.Enter)
|
|
{
|
|
await Vm.OpenSelectedAsync().ConfigureAwait(true);
|
|
}
|
|
}
|
|
}
|