Improve file operations, layout memory, and drive status.

Add a sequential file-operations queue with pause, reorder, and optional auto-clear; persist window size and tree width; show free space; and clear leftover indexing status.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 01:52:30 +02:00
parent d79605cde9
commit 9bf451932f
41 changed files with 2855 additions and 275 deletions

View File

@@ -19,11 +19,43 @@
<local:ShellIconConverter x:Key="ShellIcon"/>
<local:ThumbnailConverter x:Key="Thumbnail"/>
<local:TransferActionTextConverter x:Key="TransferAction"/>
<Style x:Key="InlineRenameBox" TargetType="TextBox">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Accent}"/>
<Setter Property="CaretBrush" Value="{DynamicResource Fg}"/>
<Setter Property="MinHeight" Value="22"/>
<Setter Property="Padding" Value="2,1"/>
<Setter Property="Margin" Value="0,1,8,1"/>
<Setter Property="MinWidth" Value="140"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<DataTemplate x:Key="NameWithIcon">
<StackPanel Orientation="Horizontal" ToolTip="{Binding SizeTooltip}">
<Image Width="16" Height="16" Margin="0,0,8,0" RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding Converter={StaticResource ShellIcon}}"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
<TextBlock Tag="ItemName" Text="{Binding Name}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRenaming}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBox Style="{StaticResource InlineRenameBox}"
Tag="InlineRename"
Text="{Binding EditName, UpdateSourceTrigger=PropertyChanged}"
local:InlineRenameBehavior.Enable="True"
Visibility="{Binding IsRenaming, Converter={StaticResource BoolVis}}"/>
<TextBlock Text="{Binding StatusGlyph}" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{DynamicResource FgMuted}"
Visibility="{Binding HasStatusGlyph, Converter={StaticResource BoolVis}}"/>
<TextBlock Text="{Binding CloudStatus}" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="11"
Foreground="{DynamicResource FgMuted}"
Visibility="{Binding HasCloudStatus, Converter={StaticResource BoolVis}}"/>
@@ -62,8 +94,25 @@
</Image.Style>
</Image>
</Grid>
<TextBlock Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
MaxHeight="36" Margin="0,4,0,0" Foreground="{DynamicResource Fg}"/>
<TextBlock Tag="ItemName" Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
MaxHeight="36" Margin="0,4,0,0" Foreground="{DynamicResource Fg}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRenaming}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBox Style="{StaticResource InlineRenameBox}"
Tag="InlineRename"
MinWidth="80"
Text="{Binding EditName, UpdateSourceTrigger=PropertyChanged}"
local:InlineRenameBehavior.Enable="True"
Visibility="{Binding IsRenaming, Converter={StaticResource BoolVis}}"/>
</StackPanel>
</DataTemplate>
@@ -237,6 +286,13 @@
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="0,0,4,0"/>
</Style>
<Style x:Key="QueueActionButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="8,2"/>
<Setter Property="MinWidth" Value="28"/>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="Margin" Value="0,0,4,0"/>
<Setter Property="FontSize" Value="11"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
@@ -578,6 +634,8 @@
<ControlTemplate TargetType="ListViewItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="1"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<Grid>
@@ -605,6 +663,10 @@
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<DataTrigger Binding="{Binding IsDropTarget}" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}"/>
</DataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
</Trigger>
@@ -707,6 +769,8 @@
<Border x:Name="Bd"
Grid.Column="1"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="1"
Padding="{TemplateBinding Padding}"
CornerRadius="3"
SnapsToDevicePixels="True">
@@ -745,6 +809,10 @@
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource TreeSelection}"/>
</MultiTrigger>
<DataTrigger Binding="{Binding IsDropTarget}" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource TreeSelection}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}"/>
</DataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
</Trigger>

View File

@@ -40,6 +40,8 @@ public static class AppServices
services.AddSingleton<IStorageProvider, NextcloudStorageProvider>();
services.AddSingleton<StorageProviderRegistry>();
services.AddSingleton<IHydrationGuard, HydrationGuard>();
services.AddSingleton<IElevatedScanService, WindowsElevatedScanService>();
services.AddSingleton<IRecycleBinCatalog, RecycleBinCatalog>();
services.AddSingleton<SourceManager>();
services.AddSingleton<PathHistoryStore>();
services.AddSingleton<CloudPlaceStore>();

View File

@@ -72,7 +72,7 @@ public sealed class ThumbnailConverter : IValueConverter
public sealed class TransferActionTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is TransferStatus.Failed ? "Dismiss" : "Cancel";
=> value is TransferStatus.Failed or TransferStatus.Done or TransferStatus.Cancelled ? "Dismiss" : "Remove";
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();

View File

@@ -0,0 +1,121 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Explorer.Domain;
using Explorer.Presentation;
namespace Explorer.App;
public static class InlineRenameBehavior
{
public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(InlineRenameBehavior),
new PropertyMetadata(false, OnEnableChanged));
public static bool GetEnable(DependencyObject obj) => (bool)obj.GetValue(EnableProperty);
public static void SetEnable(DependencyObject obj, bool value) => obj.SetValue(EnableProperty, value);
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TextBox box)
{
return;
}
if (e.NewValue is true)
{
box.Loaded += OnLoaded;
box.IsVisibleChanged += OnVisibleChanged;
box.PreviewKeyDown += OnPreviewKeyDown;
box.LostFocus += OnLostFocus;
}
else
{
box.Loaded -= OnLoaded;
box.IsVisibleChanged -= OnVisibleChanged;
box.PreviewKeyDown -= OnPreviewKeyDown;
box.LostFocus -= OnLostFocus;
}
}
private static void OnLoaded(object sender, RoutedEventArgs e) => TryFocus(sender as TextBox);
private static void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TextBox { IsVisible: true } box)
{
TryFocus(box);
}
}
private static void TryFocus(TextBox? box)
{
if (box?.DataContext is not FolderItemViewModel { IsRenaming: true } item)
{
return;
}
box.Focus();
SelectName(box, item);
}
public static void SelectName(TextBox box, FolderItemViewModel item)
{
var name = box.Text ?? "";
if (item.IsDirectory)
{
box.SelectAll();
return;
}
var ext = NameNormalizer.Extension(name);
var length = string.IsNullOrEmpty(ext) ? name.Length : Math.Max(0, name.Length - ext.Length - 1);
box.Select(0, length);
}
private static void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (sender is not TextBox box || box.DataContext is not FolderItemViewModel item)
{
return;
}
if (e.Key == Key.Enter)
{
e.Handled = true;
RequestCommit(box, item);
}
else if (e.Key == Key.Escape)
{
e.Handled = true;
item.CancelRename();
}
}
private static void OnLostFocus(object sender, RoutedEventArgs e)
{
if (sender is not TextBox box || box.DataContext is not FolderItemViewModel { IsRenaming: true } item)
{
return;
}
if (Window.GetWindow(box) is MainWindow { IsInlineRenameStarting: true })
{
box.Dispatcher.BeginInvoke(() => TryFocus(box), System.Windows.Threading.DispatcherPriority.Input);
return;
}
RequestCommit(box, item);
}
private static void RequestCommit(TextBox box, FolderItemViewModel item)
{
if (Window.GetWindow(box) is MainWindow window)
{
window.TryCommitInlineRename(item);
}
}
}

View File

@@ -139,28 +139,39 @@
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="0,1,0,0" Padding="8,6">
<Grid>
<TextBlock Text="{Binding Footer}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<ItemsControl ItemsSource="{Binding Transfers.Jobs}" Margin="0,0,12,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,0,10,0">
<TextBlock VerticalAlignment="Center" FontSize="11" Foreground="{DynamicResource FgMuted}"
Text="{Binding Op, StringFormat={}{0}:}"/>
<TextBlock VerticalAlignment="Center" FontSize="11" Margin="4,0,0,0" Text="{Binding Status}"/>
<Button Margin="6,0,0,0" Padding="6,2" FontSize="11"
Content="{Binding Status, Converter={StaticResource TransferAction}}"
Command="{Binding DataContext.CancelTransferCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Footer}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Margin="0,0,12,0"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Margin="0,0,12,0"
Visibility="{Binding Transfers.HasJobs, Converter={StaticResource BoolVis}}">
<ProgressBar Width="88" Height="6" Minimum="0" Maximum="1" VerticalAlignment="Center"
Value="{Binding Transfers.OverallProgress}"
Visibility="{Binding Transfers.HasOverallProgress, Converter={StaticResource BoolVis}}"/>
<TextBlock VerticalAlignment="Center" FontSize="11" Foreground="{DynamicResource Fg}" Margin="8,0,0,0"
Text="{Binding Transfers.Summary}" MaxWidth="280" TextTrimming="CharacterEllipsis"/>
<Button Style="{StaticResource QueueActionButton}" Content="Pause" Margin="8,0,0,0"
Command="{Binding Transfers.PauseAllCommand}"
Visibility="{Binding Transfers.CanPauseAll, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Content="Resume"
Command="{Binding Transfers.ResumeAllCommand}"
Visibility="{Binding Transfers.CanResumeAll, Converter={StaticResource BoolVis}}"/>
<Button Command="{Binding Transfers.ToggleExpandedCommand}">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource QueueActionButton}">
<Setter Property="Content" Value="File operations ▴"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Transfers.IsExpanded}" Value="False">
<Setter Property="Content" Value="File operations ▾"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
<Button Content="Storage" Command="{Binding Analysis.OpenCommand}" Margin="0,0,6,0"/>
<Button Content="Duplicates" Command="{Binding Duplicates.OpenCommand}" Margin="0,0,6,0"/>
<Button Content="Add network" Click="OnAddNetwork" Margin="0,0,6,0"/>
@@ -171,9 +182,103 @@
</Grid>
</Border>
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Panel}"
BorderBrush="{DynamicResource Stroke}" BorderThickness="0,1,0,0" Padding="10,8"
MaxHeight="280"
Visibility="{Binding Transfers.ShowPanel, Converter={StaticResource BoolVis}}">
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Style="{StaticResource QueueActionButton}" Content="Pause all"
Command="{Binding Transfers.PauseAllCommand}"
Visibility="{Binding Transfers.CanPauseAll, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Content="Resume all"
Command="{Binding Transfers.ResumeAllCommand}"
Visibility="{Binding Transfers.CanResumeAll, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Content="Clear finished"
Command="{Binding Transfers.ClearFinishedCommand}"
Visibility="{Binding Transfers.HasFinishedJobs, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Content="Hide" Margin="0"
Command="{Binding Transfers.ToggleExpandedCommand}"/>
</StackPanel>
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource Fg}" VerticalAlignment="Center" Text="File operations queue"/>
</DockPanel>
<TextBlock DockPanel.Dock="Top" FontSize="11" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8"
Text="Copy, move, and delete run one at a time. Pause a queued step to skip it, or reorder with the arrows."
TextWrapping="Wrap"/>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Transfers.Jobs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{DynamicResource Fill}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1"
CornerRadius="4" Padding="8,6" Margin="0,0,0,6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center" Margin="0,0,8,0">
<Button Style="{StaticResource QueueActionButton}" Content="↑" Margin="0,0,0,2"
ToolTip="Move earlier"
Command="{Binding DataContext.Transfers.MoveUpCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
IsEnabled="{Binding CanMoveUp}"/>
<Button Style="{StaticResource QueueActionButton}" Content="↓" Margin="0"
ToolTip="Move later"
Command="{Binding DataContext.Transfers.MoveDownCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
IsEnabled="{Binding CanMoveDown}"/>
</StackPanel>
<StackPanel Grid.Column="1">
<DockPanel>
<TextBlock DockPanel.Dock="Right" FontSize="11" Foreground="{DynamicResource FgMuted}"
Text="{Binding BytesText}" Margin="8,0,0,0"/>
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource Fg}" Text="{Binding Title}"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
<TextBlock FontSize="11" Foreground="{DynamicResource FgMuted}" Text="{Binding Subtitle}"
TextTrimming="CharacterEllipsis"/>
<DockPanel Margin="0,2,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11" Foreground="{DynamicResource Accent}"
Text="{Binding SpeedText}" Margin="8,0,0,0"/>
<TextBlock FontSize="11" Foreground="{DynamicResource Fg}" Text="{Binding StatusText}"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
<ProgressBar Height="6" Minimum="0" Maximum="1" Margin="0,6,0,0"
Value="{Binding Progress}"
Visibility="{Binding HasProgress, Converter={StaticResource BoolVis}}"/>
<TextBlock FontSize="11" Foreground="{DynamicResource FgMuted}" Margin="0,4,0,0"
Text="{Binding CurrentFile}"
Visibility="{Binding HasCurrentFile, Converter={StaticResource BoolVis}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
<Button Style="{StaticResource QueueActionButton}" Content="Pause"
Command="{Binding DataContext.Transfers.PauseCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Visibility="{Binding CanPause, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Content="Resume"
Command="{Binding DataContext.Transfers.ResumeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Visibility="{Binding CanResume, Converter={StaticResource BoolVis}}"/>
<Button Style="{StaticResource QueueActionButton}" Margin="0"
Content="{Binding Status, Converter={StaticResource TransferAction}}"
Command="{Binding DataContext.Transfers.RemoveCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
IsEnabled="{Binding CanRemove}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260" MinWidth="160"/>
<ColumnDefinition x:Name="TreeColumn" Width="260" MinWidth="160"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
@@ -186,6 +291,7 @@
AllowDrop="True"
DragEnter="OnTreeDragOver"
DragOver="OnTreeDragOver"
DragLeave="OnTreeDragLeave"
Drop="OnTreeDrop"
HorizontalContentAlignment="Stretch">
<TreeView.ContextMenu>
@@ -250,7 +356,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
@@ -266,33 +374,34 @@
<GridViewColumn Header="Date modified" Width="148" DisplayMemberBinding="{Binding ModifiedLabel}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding TypeLabel}"/>
<GridViewColumn Header="Size" Width="110" DisplayMemberBinding="{Binding SizeLabel}"/>
<GridViewColumn Header="Free space" Width="110" DisplayMemberBinding="{Binding FreeSpaceLabel}"/>
</GridView>
</ListView.View>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Open" Click="OnCtxOpen"/>
<Separator/>
<MenuItem Header="Cut" Command="{Binding DataContext.CutCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy" Command="{Binding DataContext.CopyCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Paste" Command="{Binding DataContext.PasteCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Cut" Command="{Binding CutCommand}"/>
<MenuItem Header="Copy" Command="{Binding CopyCommand}"/>
<MenuItem Header="Paste" Command="{Binding PasteCommand}"/>
<MenuItem Header="Delete" Click="OnCtxDelete" InputGestureText="Del"/>
<MenuItem Header="Rename" Click="OnCtxRename"/>
<Separator/>
<MenuItem Header="New folder" Command="{Binding DataContext.NewFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy path" Command="{Binding DataContext.CopyPathCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="New folder" Click="OnCtxNewFolder"/>
<MenuItem Header="Copy path" Click="OnCtxCopyPath"/>
<Separator/>
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Rescan folder" Command="{Binding DataContext.RescanFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Separator Visibility="{Binding DataContext.ShowForgetSource, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Refresh" Command="{Binding RefreshCommand}"/>
<MenuItem Header="Rescan folder" Command="{Binding RescanFolderCommand}"/>
<Separator Visibility="{Binding ShowForgetSource, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Remove from Explorer" Click="OnRemoveLocation"
Visibility="{Binding DataContext.ShowForgetSource, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<Separator Visibility="{Binding DataContext.ShowCloudPin, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
Visibility="{Binding ShowForgetSource, Converter={StaticResource BoolVis}}"/>
<Separator Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Always keep on this device"
Command="{Binding DataContext.PinCloudCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ShowCloudPin, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
Command="{Binding PinCloudCommand}"
Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Free up space"
Command="{Binding DataContext.FreeUpCloudSpaceCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ShowCloudDehydrate, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
Command="{Binding FreeUpCloudSpaceCommand}"
Visibility="{Binding ShowCloudDehydrate, Converter={StaticResource BoolVis}}"/>
</ContextMenu>
</ListView.ContextMenu>
</ListView>
@@ -303,7 +412,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
@@ -328,7 +439,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
GotFocus="OnPaneFocus"
@@ -375,7 +488,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
@@ -391,6 +506,7 @@
<GridViewColumn Header="Date modified" Width="148" DisplayMemberBinding="{Binding ModifiedLabel}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding TypeLabel}"/>
<GridViewColumn Header="Size" Width="110" DisplayMemberBinding="{Binding SizeLabel}"/>
<GridViewColumn Header="Free space" Width="110" DisplayMemberBinding="{Binding FreeSpaceLabel}"/>
</GridView>
</ListView.View>
</ListView>
@@ -401,7 +517,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
@@ -426,7 +544,9 @@
PreviewMouseLeftButtonDown="OnListMouseDown"
PreviewMouseRightButtonDown="OnListMouseDown"
ContextMenuOpening="OnListContextMenuOpening"
GridViewColumnHeader.Click="OnColumnHeaderClick"
DragEnter="OnListDragOver"
DragLeave="OnListDragLeave"
Drop="OnListDrop"
DragOver="OnListDragOver"
GotFocus="OnPaneFocus"
@@ -506,6 +626,7 @@
<GridViewColumn Header="Name" Width="220" CellTemplate="{StaticResource NameWithIcon}"/>
<GridViewColumn Header="Path" Width="420" DisplayMemberBinding="{Binding FullPath}"/>
<GridViewColumn Header="Size" Width="100" DisplayMemberBinding="{Binding SizeLabel}"/>
<GridViewColumn Header="Free space" Width="100" DisplayMemberBinding="{Binding FreeSpaceLabel}"/>
</GridView>
</ListView.View>
</ListView>

View File

@@ -1,9 +1,12 @@
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;
@@ -19,27 +22,48 @@ public partial class MainWindow : Window
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 (DataContext is MainViewModel vm)
if (_wiredVm is not null)
{
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(MainViewModel.Theme))
{
ApplyTheme(vm.Theme);
}
};
_wiredVm.InlineRenameRequested -= OnInlineRenameRequested;
_wiredVm.PropertyChanged -= OnViewModelPropertyChanged;
}
_wiredVm = DataContext as MainViewModel;
if (_wiredVm is not null)
{
_wiredVm.InlineRenameRequested += OnInlineRenameRequested;
_wiredVm.PropertyChanged += OnViewModelPropertyChanged;
RestoreLayout(_wiredVm);
}
};
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;
@@ -75,9 +99,77 @@ public partial class MainWindow : Window
private void ToggleMaximized()
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
MaxRestoreButton.Content = WindowState == 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 })
@@ -342,7 +434,8 @@ public partial class MainWindow : Window
private async void OnItemDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is ListView { SelectedItem: FolderItemViewModel item })
CancelClickRename();
if (sender is ListView { SelectedItem: FolderItemViewModel item } && !item.IsRenaming)
{
ActivatePaneFromList((ListView)sender);
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
@@ -350,6 +443,82 @@ public partial class MainWindow : Window
}
}
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)
@@ -358,6 +527,11 @@ public partial class MainWindow : Window
}
ActivatePaneFromList(list);
if (_clickRenameItem is not null && !list.SelectedItems.Contains(_clickRenameItem))
{
CancelClickRename();
}
Vm.ActivePane.SelectedItems.Clear();
foreach (FolderItemViewModel item in list.SelectedItems)
{
@@ -395,6 +569,7 @@ public partial class MainWindow : Window
_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)
@@ -403,6 +578,12 @@ public partial class MainWindow : Window
{
e.Handled = true;
_suppressItemContextMenu = false;
return;
}
if (sender is FrameworkElement { ContextMenu: { } menu })
{
menu.DataContext = DataContext;
}
}
@@ -410,6 +591,7 @@ public partial class MainWindow : Window
{
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
{
SetListDropTarget(null);
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
@@ -418,6 +600,15 @@ public partial class MainWindow : Window
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;
@@ -439,6 +630,7 @@ public partial class MainWindow : Window
private async void OnListDrop(object sender, DragEventArgs e)
{
ClearDropTargets();
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
{
return;
@@ -463,6 +655,35 @@ public partial class MainWindow : Window
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);
@@ -481,6 +702,7 @@ public partial class MainWindow : Window
}
_dragPending = false;
CancelClickRename();
var selected = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
IReadOnlyList<string> paths;
if (_dragItem is not null && (selected.Count == 0
@@ -504,6 +726,7 @@ public partial class MainWindow : Window
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
_sourceRightDrag = false;
_incomingRightDrag = false;
ClearDropTargets();
}
protected override void OnQueryContinueDrag(QueryContinueDragEventArgs e)
@@ -638,20 +861,27 @@ public partial class MainWindow : Window
{
if (!TryGetDropFiles(e, out var files))
{
SetTreeDropTarget(null);
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
var dest = HitTestTreePath(e);
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));
@@ -660,6 +890,7 @@ public partial class MainWindow : Window
private async void OnTreeDrop(object sender, DragEventArgs e)
{
ClearDropTargets();
if (!TryGetDropFiles(e, out var files))
{
return;
@@ -683,11 +914,20 @@ public partial class MainWindow : Window
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 origin = NavTree.InputHitTest(e.GetPosition(NavTree)) as DependencyObject
?? e.OriginalSource as DependencyObject;
var node = FindTreeNode(origin);
var node = HitTestTreeNode(e);
if (node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
{
return null;
@@ -696,6 +936,57 @@ public partial class MainWindow : Window
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 })
@@ -760,6 +1051,10 @@ public partial class MainWindow : Window
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);
@@ -790,16 +1085,211 @@ public partial class MainWindow : Window
private void OnCtxRename(object sender, RoutedEventArgs e)
{
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
if (item is not null)
{
BeginInlineRename(item);
}
}
private void BeginInlineRenameForPath(string path)
{
var item = Vm.ActivePane.Items.FirstOrDefault(i => NavigationTreeViewModel.PathsEqual(i.FullPath, path));
if (item is null)
{
return;
}
var name = PromptWindow.Ask(this, "Rename", "New name:", item.Name);
if (!string.IsNullOrWhiteSpace(name) && name != item.Name)
BeginInlineRename(item);
}
private void BeginInlineRename(FolderItemViewModel item)
{
foreach (var other in Vm.ActivePane.Items.Where(i => i.IsRenaming && i != item))
{
Vm.RenameSelected(name);
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)
@@ -845,8 +1335,21 @@ public partial class MainWindow : Window
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.F5)
{
await Vm.RefreshAsync().ConfigureAwait(true);

View File

@@ -21,6 +21,9 @@ public sealed class WpfClipboard : IOsClipboard
Clipboard.SetDataObject(data, copy: true);
}
public void SetText(string text)
=> Clipboard.SetText(text);
public bool TryGetFiles(out IReadOnlyList<string> paths, out bool cut)
{
paths = [];

View File

@@ -3,8 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Settings"
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
Height="620" Width="560"
MinHeight="520" MinWidth="480"
Height="720" Width="560"
MinHeight="560" MinWidth="480"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}"
ResizeMode="NoResize">
@@ -38,6 +38,24 @@
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="OneDrive, Google Drive, and Nextcloud become children of a Cloud item. The two options are independent."/>
<TextBlock Text="Filesystem visibility" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,12"
Text="These options only change what Explorer Workbench shows and indexes. Windows settings and files are not modified."/>
<CheckBox x:Name="ShowHidden" Margin="0,0,0,6"
Content="Show hidden files"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,14" FontSize="12"
Text="Items marked Hidden by Windows. Default matches the current Explorer Workbench listing."/>
<CheckBox x:Name="ShowProtected" Margin="0,0,0,6"
Content="Show protected system locations"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="System Volume Information, Recycle Bin, Recovery, and similar locations. When a folder cannot be read, its size is shown as access denied — never as 0 bytes. Administrator rights are detected but never requested automatically."/>
<TextBlock Text="File operations queue" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
<CheckBox x:Name="AutoClearQueue" Margin="0,0,0,6"
Content="Auto clear queue when done"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
Text="Finished copy, move, and delete steps are removed automatically. Failed items stay until you dismiss them."/>
<TextBlock Text="Indexing" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
<CheckBox x:Name="IndexArchives" Margin="0,0,0,6"
Content="Include archive contents in the index"/>

View File

@@ -20,6 +20,9 @@ public partial class SettingsWindow : Window
GroupNetwork.IsChecked = prefs.GroupNetworkPlaces;
GroupCloud.IsChecked = prefs.GroupCloudPlaces;
IndexArchives.IsChecked = prefs.IndexArchiveContents;
ShowHidden.IsChecked = prefs.ShowHiddenFiles;
ShowProtected.IsChecked = prefs.ShowProtectedSystemLocations;
AutoClearQueue.IsChecked = prefs.AutoClearQueueWhenDone;
}
private void OnThemeChanged(object sender, RoutedEventArgs e)
@@ -34,11 +37,16 @@ public partial class SettingsWindow : Window
private async void OnOk(object sender, RoutedEventArgs e)
{
var prefs = new UiPreferences(
ThemeLight.IsChecked == true ? "Light" : "Dark",
GroupNetwork.IsChecked == true,
GroupCloud.IsChecked == true,
IndexArchives.IsChecked == true);
var prefs = _vm.CurrentPreferences() with
{
Theme = ThemeLight.IsChecked == true ? "Light" : "Dark",
GroupNetworkPlaces = GroupNetwork.IsChecked == true,
GroupCloudPlaces = GroupCloud.IsChecked == true,
IndexArchiveContents = IndexArchives.IsChecked == true,
ShowHiddenFiles = ShowHidden.IsChecked == true,
ShowProtectedSystemLocations = ShowProtected.IsChecked == true,
AutoClearQueueWhenDone = AutoClearQueue.IsChecked == true
};
await _vm.ApplyPreferencesAsync(prefs).ConfigureAwait(true);
DialogResult = true;
Close();