Add Explorer Workbench with hierarchical, off-UI Storage analysis.

Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

View File

@@ -0,0 +1,525 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using Explorer.Domain;
using Explorer.Presentation;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
public partial class MainWindow : Window
{
private Point _dragStart;
private bool _dragPending;
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 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)
{
_dragStart = e.GetPosition(null);
_dragPending = true;
}
private void OnListDragOver(object sender, DragEventArgs e)
{
e.Effects = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 ? DragDropEffects.Move : DragDropEffects.Copy;
e.Handled = true;
}
private async void OnListDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop) || sender is not ListView list)
{
return;
}
ActivatePaneFromList(list);
var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
var move = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 || e.AllowedEffects == DragDropEffects.Move;
await Vm.DropAsync(files, Vm.ActivePane.CurrentPath, move).ConfigureAwait(true);
await Vm.RefreshAsync().ConfigureAwait(true);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (!_dragPending || e.LeftButton != MouseButtonState.Pressed)
{
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 paths = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToArray();
if (paths.Length == 0)
{
return;
}
var data = new DataObject(DataFormats.FileDrop, paths);
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move);
}
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 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 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)
{
var picker = new Microsoft.Win32.OpenFolderDialog
{
Title = "Add OneDrive folder",
Multiselect = false
};
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
{
return;
}
await Vm.AddCloudFolderAsync(picker.FolderName).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 Vm.DeleteAsync().ConfigureAwait(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);
}
}
}