1607 lines
48 KiB
C#
1607 lines
48 KiB
C#
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using Explorer.Domain;
|
|
using Explorer.Presentation;
|
|
using Explorer.Presentation.ViewModels;
|
|
|
|
namespace Explorer.App;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
private DocumentationWindow? _docs;
|
|
private Point _dragStart;
|
|
private bool _dragPending;
|
|
private MouseButton _dragButton;
|
|
private FolderItemViewModel? _dragItem;
|
|
private bool _suppressItemContextMenu;
|
|
private bool _incomingRightDrag;
|
|
private bool _sourceRightDrag;
|
|
private MainViewModel? _wiredVm;
|
|
private long _inlineRenameStarted;
|
|
private DispatcherTimer? _clickRenameTimer;
|
|
private FolderItemViewModel? _clickRenameItem;
|
|
private FolderItemViewModel? _listDropTarget;
|
|
private NavNodeViewModel? _treeDropTarget;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
DataContextChanged += (_, _) =>
|
|
{
|
|
if (_wiredVm is not null)
|
|
{
|
|
_wiredVm.InlineRenameRequested -= OnInlineRenameRequested;
|
|
_wiredVm.PropertyChanged -= OnViewModelPropertyChanged;
|
|
}
|
|
|
|
_wiredVm = DataContext as MainViewModel;
|
|
if (_wiredVm is not null)
|
|
{
|
|
_wiredVm.InlineRenameRequested += OnInlineRenameRequested;
|
|
_wiredVm.PropertyChanged += OnViewModelPropertyChanged;
|
|
RestoreLayout(_wiredVm);
|
|
_ = _wiredVm.RefreshUndoRenameAsync();
|
|
}
|
|
};
|
|
Closing += (_, _) => PersistLayout();
|
|
}
|
|
|
|
private MainViewModel Vm => (MainViewModel)DataContext;
|
|
|
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.Theme) && sender is MainViewModel vm)
|
|
{
|
|
ApplyTheme(vm.Theme);
|
|
}
|
|
}
|
|
|
|
private void OnInlineRenameRequested(object? sender, string path)
|
|
=> Dispatcher.BeginInvoke(() => BeginInlineRenameForPath(path), DispatcherPriority.Loaded);
|
|
|
|
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;
|
|
SyncMaxRestoreButton();
|
|
}
|
|
|
|
private void RestoreLayout(MainViewModel vm)
|
|
{
|
|
var prefs = vm.CurrentPreferences();
|
|
if (prefs.WindowWidth is > 0 && prefs.WindowHeight is > 0)
|
|
{
|
|
Width = Clamp(prefs.WindowWidth.Value, MinWidth, SystemParameters.VirtualScreenWidth);
|
|
Height = Clamp(prefs.WindowHeight.Value, MinHeight, SystemParameters.VirtualScreenHeight);
|
|
}
|
|
|
|
if (prefs.WindowLeft is not null && prefs.WindowTop is not null)
|
|
{
|
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
|
Left = prefs.WindowLeft.Value;
|
|
Top = prefs.WindowTop.Value;
|
|
if (!IsOnVirtualScreen())
|
|
{
|
|
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
|
}
|
|
}
|
|
|
|
if (prefs.WindowMaximized)
|
|
{
|
|
WindowState = WindowState.Maximized;
|
|
}
|
|
|
|
SyncMaxRestoreButton();
|
|
if (prefs.TreeWidth is >= 160)
|
|
{
|
|
var maxTree = Math.Max(160, ActualWidth > 0 ? ActualWidth - 240 : Width - 240);
|
|
TreeColumn.Width = new GridLength(Clamp(prefs.TreeWidth.Value, 160, maxTree));
|
|
}
|
|
}
|
|
|
|
private void PersistLayout()
|
|
{
|
|
if (DataContext is not MainViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var bounds = WindowState == WindowState.Normal ? new Rect(Left, Top, Width, Height) : RestoreBounds;
|
|
if (bounds.Width <= 0 || bounds.Height <= 0)
|
|
{
|
|
bounds = new Rect(Left, Top, Width, Height);
|
|
}
|
|
|
|
var treeWidth = TreeColumn.ActualWidth > 0 ? TreeColumn.ActualWidth : TreeColumn.Width.Value;
|
|
vm.SaveLayout(bounds.Width, bounds.Height, bounds.Left, bounds.Top, WindowState == WindowState.Maximized, treeWidth);
|
|
}
|
|
|
|
private void SyncMaxRestoreButton()
|
|
=> MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
|
|
|
|
private bool IsOnVirtualScreen()
|
|
{
|
|
var virtualArea = new Rect(
|
|
SystemParameters.VirtualScreenLeft,
|
|
SystemParameters.VirtualScreenTop,
|
|
SystemParameters.VirtualScreenWidth,
|
|
SystemParameters.VirtualScreenHeight);
|
|
var window = new Rect(Left, Top, Math.Max(Width, MinWidth), Math.Max(Height, MinHeight));
|
|
window.Intersect(virtualArea);
|
|
return window.Width >= 80 && window.Height >= 80;
|
|
}
|
|
|
|
private static double Clamp(double value, double min, double max)
|
|
=> Math.Min(Math.Max(value, min), Math.Max(min, max));
|
|
|
|
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 async void OnOpenRecycleBin(object sender, RoutedEventArgs e)
|
|
=> await Vm.ActivePane.NavigateAsync(LocationRoots.RecycleBin).ConfigureAwait(true);
|
|
|
|
private async void OnEmptyRecycleBin(object sender, RoutedEventArgs e)
|
|
{
|
|
if (MessageBox.Show(
|
|
this,
|
|
"Empty Recycle Bin?\n\nItems will be permanently deleted. This is queued and can be cancelled until it starts.",
|
|
"Empty Recycle Bin",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning) != MessageBoxResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.EmptyRecycleBinAsync().ConfigureAwait(true);
|
|
}
|
|
|
|
private async void OnImportWindowsLocation(object sender, RoutedEventArgs e)
|
|
{
|
|
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
|
|
if (item is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
|
|
await Vm.Tree.ReloadAsync(item.FullPath).ConfigureAwait(true);
|
|
Vm.Footer = "Added Windows location. Indexing is optional.";
|
|
}
|
|
|
|
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)
|
|
{
|
|
CancelClickRename();
|
|
if (sender is ListView { SelectedItem: FolderItemViewModel item } && !item.IsRenaming)
|
|
{
|
|
ActivatePaneFromList((ListView)sender);
|
|
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
|
|
Vm.PathText = Vm.ActivePane.CurrentPath;
|
|
}
|
|
}
|
|
|
|
private void OnColumnHeaderClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is Thumb || sender is not ListView list)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var header = e.OriginalSource as GridViewColumnHeader
|
|
?? FindGridViewColumnHeader(e.OriginalSource as DependencyObject);
|
|
if (header is null || header.Role == GridViewColumnHeaderRole.Padding)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var key = SortKeyFromHeader(header.Column?.Header);
|
|
if (key is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActivatePaneFromList(list);
|
|
Vm.ActivePane.SortBy(key);
|
|
UpdateSortGlyphs(list, Vm.ActivePane);
|
|
}
|
|
|
|
private static GridViewColumnHeader? FindGridViewColumnHeader(DependencyObject? origin)
|
|
{
|
|
while (origin is not null)
|
|
{
|
|
if (origin is GridViewColumnHeader header)
|
|
{
|
|
return header;
|
|
}
|
|
|
|
origin = origin is Visual
|
|
? VisualTreeHelper.GetParent(origin)
|
|
: LogicalTreeHelper.GetParent(origin);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void UpdateSortGlyphs(ListView list, ExplorerPaneViewModel pane)
|
|
{
|
|
if (list.View is not GridView grid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var column in grid.Columns)
|
|
{
|
|
var title = StripSortGlyph(column.Header?.ToString());
|
|
var key = SortKeyFromHeader(title);
|
|
column.Header = key is not null && string.Equals(pane.SortProperty, key, StringComparison.OrdinalIgnoreCase)
|
|
? title + (pane.SortDescending ? " ▼" : " ▲")
|
|
: title;
|
|
}
|
|
}
|
|
|
|
private static string? SortKeyFromHeader(object? header)
|
|
=> StripSortGlyph(header?.ToString()) switch
|
|
{
|
|
"Name" => "Name",
|
|
"Date modified" => "Modified",
|
|
"Type" => "Type",
|
|
"Size" => "Size",
|
|
"Free space" => "Free",
|
|
_ => null
|
|
};
|
|
|
|
private static string StripSortGlyph(string? header)
|
|
{
|
|
var text = header ?? "";
|
|
return text.Replace(" ▲", "", StringComparison.Ordinal).Replace(" ▼", "", StringComparison.Ordinal).Trim();
|
|
}
|
|
|
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (sender is not ListView list)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActivatePaneFromList(list);
|
|
if (_clickRenameItem is not null && !list.SelectedItems.Contains(_clickRenameItem))
|
|
{
|
|
CancelClickRename();
|
|
}
|
|
|
|
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));
|
|
TryScheduleClickRename(sender as ListView, e);
|
|
}
|
|
|
|
private void OnListContextMenuOpening(object sender, ContextMenuEventArgs e)
|
|
{
|
|
if (_suppressItemContextMenu)
|
|
{
|
|
e.Handled = true;
|
|
_suppressItemContextMenu = false;
|
|
return;
|
|
}
|
|
|
|
if (sender is FrameworkElement { ContextMenu: { } menu })
|
|
{
|
|
menu.DataContext = DataContext;
|
|
_ = FillRunProfileMenuAsync(menu);
|
|
}
|
|
}
|
|
|
|
private void OnListDragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
|
|
{
|
|
SetListDropTarget(null);
|
|
e.Effects = DragDropEffects.None;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
ActivatePaneFromList(list);
|
|
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
|
|
var dest = ResolveDropDirectory(list, e);
|
|
var hover = HitTestFolderItem(list, e.GetPosition(list));
|
|
var folderTarget = hover is { IsDirectory: true }
|
|
&& dest is not null
|
|
&& NavigationTreeViewModel.PathsEqual(hover.FullPath, dest)
|
|
&& !DragDropPolicy.IsInvalidTarget(files, dest)
|
|
? hover
|
|
: null;
|
|
SetListDropTarget(folderTarget);
|
|
SetTreeDropTarget(null);
|
|
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)
|
|
{
|
|
ClearDropTargets();
|
|
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);
|
|
}
|
|
|
|
private void OnListDragLeave(object sender, DragEventArgs e)
|
|
{
|
|
if (sender is ListView list)
|
|
{
|
|
var pos = e.GetPosition(list);
|
|
if (pos.X >= 0 && pos.Y >= 0 && pos.X <= list.ActualWidth && pos.Y <= list.ActualHeight)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
SetListDropTarget(null);
|
|
}
|
|
|
|
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
|
|
{
|
|
if (!IsInsideInlineRenameBox(e.OriginalSource as DependencyObject))
|
|
{
|
|
CommitOpenInlineRename();
|
|
}
|
|
|
|
if (!IsClickOnItemName(e.OriginalSource as DependencyObject))
|
|
{
|
|
CancelClickRename();
|
|
}
|
|
|
|
base.OnPreviewMouseDown(e);
|
|
}
|
|
|
|
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;
|
|
CancelClickRename();
|
|
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;
|
|
ClearDropTargets();
|
|
}
|
|
|
|
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))
|
|
{
|
|
SetTreeDropTarget(null);
|
|
e.Effects = DragDropEffects.None;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
|
|
var node = HitTestTreeNode(e);
|
|
var dest = node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path)
|
|
? null
|
|
: node.Path;
|
|
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
|
|
{
|
|
SetTreeDropTarget(null);
|
|
e.Effects = DragDropEffects.None;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
SetListDropTarget(null);
|
|
SetTreeDropTarget(node);
|
|
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)
|
|
{
|
|
ClearDropTargets();
|
|
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 void OnTreeDragLeave(object sender, DragEventArgs e)
|
|
{
|
|
var pos = e.GetPosition(NavTree);
|
|
if (pos.X >= 0 && pos.Y >= 0 && pos.X <= NavTree.ActualWidth && pos.Y <= NavTree.ActualHeight)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SetTreeDropTarget(null);
|
|
}
|
|
|
|
private string? HitTestTreePath(DragEventArgs e)
|
|
{
|
|
var node = HitTestTreeNode(e);
|
|
if (node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return node.Path;
|
|
}
|
|
|
|
private NavNodeViewModel? HitTestTreeNode(DragEventArgs e)
|
|
{
|
|
var origin = NavTree.InputHitTest(e.GetPosition(NavTree)) as DependencyObject
|
|
?? e.OriginalSource as DependencyObject;
|
|
return FindTreeNode(origin);
|
|
}
|
|
|
|
private void SetListDropTarget(FolderItemViewModel? item)
|
|
{
|
|
if (ReferenceEquals(_listDropTarget, item))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_listDropTarget is not null)
|
|
{
|
|
_listDropTarget.IsDropTarget = false;
|
|
}
|
|
|
|
_listDropTarget = item;
|
|
if (item is not null)
|
|
{
|
|
item.IsDropTarget = true;
|
|
}
|
|
}
|
|
|
|
private void SetTreeDropTarget(NavNodeViewModel? node)
|
|
{
|
|
if (ReferenceEquals(_treeDropTarget, node))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_treeDropTarget is not null)
|
|
{
|
|
_treeDropTarget.IsDropTarget = false;
|
|
}
|
|
|
|
_treeDropTarget = node;
|
|
if (node is not null)
|
|
{
|
|
node.IsDropTarget = true;
|
|
}
|
|
}
|
|
|
|
private void ClearDropTargets()
|
|
{
|
|
SetListDropTarget(null);
|
|
SetTreeDropTarget(null);
|
|
}
|
|
|
|
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 OnBuildIndex(object sender, RoutedEventArgs e) => Vm.BuildIndexCommand.Execute(null);
|
|
|
|
private void OnCtxOpen(object sender, RoutedEventArgs e) => Vm.OpenSelectedCommand.Execute(null);
|
|
|
|
private void OnCtxNewFolder(object sender, RoutedEventArgs e) => Vm.NewFolderCommand.Execute(null);
|
|
|
|
private void OnCtxCopyPath(object sender, RoutedEventArgs e) => Vm.CopyPathCommand.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)
|
|
{
|
|
if (Vm.ActivePane.SelectedItems.Count >= 2)
|
|
{
|
|
OnBatchRename(sender, e);
|
|
return;
|
|
}
|
|
|
|
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
|
|
if (item is not null)
|
|
{
|
|
BeginInlineRename(item);
|
|
}
|
|
}
|
|
|
|
private async void OnBatchRename(object sender, RoutedEventArgs e)
|
|
{
|
|
var vm = Vm.CreateBatchRenameViewModel();
|
|
if (vm is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dlg = new BatchRenameWindow(vm) { Owner = this };
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
Vm.Footer = "Rename queued.";
|
|
await Vm.RefreshUndoRenameAsync().ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
private async void OnExtractHere(object sender, RoutedEventArgs e)
|
|
=> await Vm.ExtractSelectedAsync(null).ConfigureAwait(true);
|
|
|
|
private async void OnExtractTo(object sender, RoutedEventArgs e)
|
|
{
|
|
var picker = new Microsoft.Win32.OpenFolderDialog
|
|
{
|
|
Title = "Extract to",
|
|
Multiselect = false
|
|
};
|
|
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.ExtractSelectedAsync(picker.FolderName).ConfigureAwait(true);
|
|
}
|
|
|
|
private async void OnCompressZip(object sender, RoutedEventArgs e)
|
|
=> await Vm.CompressSelectedAsync(ArchiveFormat.Zip).ConfigureAwait(true);
|
|
|
|
private async void OnCompressSevenZip(object sender, RoutedEventArgs e)
|
|
=> await Vm.CompressSelectedAsync(ArchiveFormat.SevenZip).ConfigureAwait(true);
|
|
|
|
private async void OnAddToArchive(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFileDialog
|
|
{
|
|
Title = "Add to archive",
|
|
Filter = "Archives|*.zip;*.7z;*.zipx;*.cbz;*.cb7|All files|*.*",
|
|
CheckFileExists = true
|
|
};
|
|
if (dlg.ShowDialog(this) != true || string.IsNullOrWhiteSpace(dlg.FileName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Vm.AddSelectedToArchiveAsync(dlg.FileName).ConfigureAwait(true);
|
|
}
|
|
|
|
private async void OnVerifyArchive(object sender, RoutedEventArgs e)
|
|
=> await Vm.VerifySelectedAsync().ConfigureAwait(true);
|
|
|
|
private async void OnFolderSync(object sender, RoutedEventArgs e)
|
|
{
|
|
var vm = Vm.CreateFolderSyncViewModel();
|
|
await vm.LoadAsync().ConfigureAwait(true);
|
|
var dlg = new FolderSyncWindow(vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
}
|
|
|
|
private async void OnOperationProfiles(object sender, RoutedEventArgs e)
|
|
{
|
|
var vm = Vm.CreateOperationProfilesViewModel();
|
|
await vm.LoadAsync().ConfigureAwait(true);
|
|
var dlg = new OperationProfilesWindow(vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
}
|
|
|
|
private void OnOrganizeFolder(object sender, RoutedEventArgs e)
|
|
{
|
|
var vm = Vm.CreateReorganizeViewModel();
|
|
var dlg = new ReorganizeWindow(vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
}
|
|
|
|
private async Task FillRunProfileMenuAsync(ContextMenu menu)
|
|
{
|
|
var host = menu.Items.OfType<MenuItem>().FirstOrDefault(i => Equals(i.Tag, "RunProfileMenu"));
|
|
if (host is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
host.Items.Clear();
|
|
IReadOnlyList<OperationProfile> profiles;
|
|
try
|
|
{
|
|
profiles = await Vm.ListOperationProfilesAsync().ConfigureAwait(true);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
host.Items.Add(new MenuItem { Header = "Could not load profiles", IsEnabled = false });
|
|
return;
|
|
}
|
|
|
|
if (profiles.Count == 0)
|
|
{
|
|
host.Items.Add(new MenuItem { Header = "No profiles yet", IsEnabled = false });
|
|
return;
|
|
}
|
|
|
|
foreach (var profile in profiles)
|
|
{
|
|
var item = new MenuItem { Header = profile.Name, Tag = profile.Id };
|
|
item.Click += OnRunProfile;
|
|
host.Items.Add(item);
|
|
}
|
|
}
|
|
|
|
private async void OnRunProfile(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not MenuItem { Tag: long id })
|
|
{
|
|
return;
|
|
}
|
|
|
|
var sources = Vm.SelectedRealPaths();
|
|
if (sources.Count == 0)
|
|
{
|
|
Vm.Footer = "Select files or folders to run a profile.";
|
|
return;
|
|
}
|
|
|
|
var vm = Vm.CreateOperationProfilesViewModel();
|
|
await vm.LoadAsync().ConfigureAwait(true);
|
|
await vm.RunOnAsync(id, sources).ConfigureAwait(true);
|
|
var dlg = new OperationProfilesWindow(vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
}
|
|
|
|
private void BeginInlineRenameForPath(string path)
|
|
{
|
|
var item = Vm.ActivePane.Items.FirstOrDefault(i => NavigationTreeViewModel.PathsEqual(i.FullPath, path));
|
|
if (item is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BeginInlineRename(item);
|
|
}
|
|
|
|
private void BeginInlineRename(FolderItemViewModel item)
|
|
{
|
|
foreach (var other in Vm.ActivePane.Items.Where(i => i.IsRenaming && i != item))
|
|
{
|
|
other.CancelRename();
|
|
}
|
|
|
|
var list = FindActiveFileList();
|
|
if (list is not null)
|
|
{
|
|
list.SelectedItem = item;
|
|
list.ScrollIntoView(item);
|
|
list.UpdateLayout();
|
|
}
|
|
|
|
CancelClickRename();
|
|
_inlineRenameStarted = Environment.TickCount64;
|
|
item.BeginRename();
|
|
}
|
|
|
|
private void TryScheduleClickRename(ListView? list, MouseButtonEventArgs e)
|
|
{
|
|
if (list is null
|
|
|| e.ChangedButton != MouseButton.Left
|
|
|| _dragItem is null
|
|
|| _dragItem.IsRenaming
|
|
|| Keyboard.Modifiers is not ModifierKeys.None
|
|
|| !IsClickOnItemName(e.OriginalSource as DependencyObject)
|
|
|| list.SelectedItems.Count != 1
|
|
|| !list.SelectedItems.Contains(_dragItem))
|
|
{
|
|
CancelClickRename();
|
|
return;
|
|
}
|
|
|
|
CancelClickRename();
|
|
_clickRenameItem = _dragItem;
|
|
_clickRenameTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromMilliseconds(Math.Max(GetDoubleClickTime(), 1))
|
|
};
|
|
_clickRenameTimer.Tick += OnClickRenameTick;
|
|
_clickRenameTimer.Start();
|
|
}
|
|
|
|
private void OnClickRenameTick(object? sender, EventArgs e)
|
|
{
|
|
var item = _clickRenameItem;
|
|
CancelClickRename();
|
|
if (item is not null && Vm.ActivePane.Items.Contains(item) && !item.IsRenaming)
|
|
{
|
|
BeginInlineRename(item);
|
|
}
|
|
}
|
|
|
|
private void CancelClickRename()
|
|
{
|
|
if (_clickRenameTimer is not null)
|
|
{
|
|
_clickRenameTimer.Tick -= OnClickRenameTick;
|
|
_clickRenameTimer.Stop();
|
|
_clickRenameTimer = null;
|
|
}
|
|
|
|
_clickRenameItem = null;
|
|
}
|
|
|
|
private static bool IsClickOnItemName(DependencyObject? origin)
|
|
{
|
|
while (origin is not null)
|
|
{
|
|
if (origin is TextBox { Tag: "InlineRename" } || origin is FrameworkElement { Tag: "ItemName" })
|
|
{
|
|
return true;
|
|
}
|
|
|
|
origin = origin is Visual
|
|
? VisualTreeHelper.GetParent(origin)
|
|
: LogicalTreeHelper.GetParent(origin);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint GetDoubleClickTime();
|
|
|
|
internal bool IsInlineRenameStarting
|
|
=> Environment.TickCount64 - _inlineRenameStarted < 300;
|
|
|
|
private void CommitOpenInlineRename()
|
|
{
|
|
if (DataContext is not MainViewModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var pane in new[] { Vm.ActiveTab.Left, Vm.ActiveTab.Right })
|
|
{
|
|
foreach (var item in pane.Items.Where(i => i.IsRenaming).ToList())
|
|
{
|
|
TryCommitInlineRename(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsInsideInlineRenameBox(DependencyObject? origin)
|
|
{
|
|
while (origin is not null)
|
|
{
|
|
if (origin is TextBox { Tag: "InlineRename" })
|
|
{
|
|
return true;
|
|
}
|
|
|
|
origin = origin is Visual
|
|
? VisualTreeHelper.GetParent(origin)
|
|
: LogicalTreeHelper.GetParent(origin);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void TryCommitInlineRename(FolderItemViewModel item)
|
|
{
|
|
if (!item.IsRenaming)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var newName = item.EditName.Trim();
|
|
if (string.IsNullOrWhiteSpace(newName) || newName.Equals(item.Item.Name, StringComparison.Ordinal))
|
|
{
|
|
item.CancelRename();
|
|
return;
|
|
}
|
|
|
|
if (newName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
|
{
|
|
MessageBox.Show(
|
|
this,
|
|
"A file name can't contain any of the following characters:\n\\ / : * ? \" < > |",
|
|
"Rename",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Warning);
|
|
item.BeginRename();
|
|
item.EditName = newName;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
item.IsRenaming = false;
|
|
Vm.RenameItem(item, newName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, ex.Message, "Rename", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
item.BeginRename();
|
|
item.EditName = newName;
|
|
}
|
|
}
|
|
|
|
private ListView? FindActiveFileList()
|
|
{
|
|
var items = Vm.ActivePane.Items;
|
|
return FindVisibleList(this, items);
|
|
}
|
|
|
|
private static ListView? FindVisibleList(DependencyObject root, object items)
|
|
{
|
|
var count = VisualTreeHelper.GetChildrenCount(root);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(root, i);
|
|
if (child is ListView { IsVisible: true } list && ReferenceEquals(list.ItemsSource, items))
|
|
{
|
|
return list;
|
|
}
|
|
|
|
var nested = FindVisibleList(child, items);
|
|
if (nested is not null)
|
|
{
|
|
return nested;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async void OnOpenSettings(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new SettingsWindow(Vm) { Owner = this };
|
|
dlg.ShowDialog();
|
|
await Vm.RefreshForgetActionAsync().ConfigureAwait(true);
|
|
}
|
|
|
|
private void OnDocumentation(object sender, RoutedEventArgs e) => ShowDocumentation();
|
|
|
|
private void OnAbout(object sender, RoutedEventArgs e)
|
|
=> new AboutWindow { Owner = this }.ShowDialog();
|
|
|
|
private void ShowDocumentation()
|
|
{
|
|
if (_docs is { IsVisible: true })
|
|
{
|
|
_docs.Activate();
|
|
return;
|
|
}
|
|
|
|
_docs = new DocumentationWindow { Owner = this };
|
|
_docs.Closed += (_, _) => _docs = null;
|
|
_docs.Show();
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (e.OriginalSource is TextBox { Tag: "InlineRename" })
|
|
{
|
|
return;
|
|
}
|
|
|
|
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
|
|
var shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
|
var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
|
|
if (ctrl && shift && e.Key == Key.N)
|
|
{
|
|
Vm.NewFolderCommand.Execute(null);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Key == Key.F1)
|
|
{
|
|
ShowDocumentation();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|