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:
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
121
src/Explorer.App/InlineRenameBehavior.cs
Normal file
121
src/Explorer.App/InlineRenameBehavior.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user