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 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 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()) { 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 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 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 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 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().FirstOrDefault(i => Equals(i.Tag, "RunProfileMenu")); if (host is null) { return; } host.Items.Clear(); IReadOnlyList 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); } } }