Show folder names immediately and refresh stale index sizes without walking the whole drive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,9 +7,11 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Themes/Dark.xaml"/>
|
||||
<ResourceDictionary Source="Settings/SettingsStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<FontFamily x:Key="Symbol">Segoe MDL2 Assets</FontFamily>
|
||||
<BooleanToVisibilityConverter x:Key="BoolVis"/>
|
||||
<local:InverseBooleanToVisibilityConverter x:Key="InvBoolVis"/>
|
||||
<local:ActiveThicknessConverter x:Key="ActiveThickness"/>
|
||||
<local:FractionWidthConverter x:Key="FractionWidth"/>
|
||||
<local:IndentConverter x:Key="Indent"/>
|
||||
@@ -141,67 +143,6 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<DataTemplate x:Key="PaneAddressBar">
|
||||
<DockPanel LastChildFill="True" Margin="6,6,6,4">
|
||||
<Button DockPanel.Dock="Left" Content="↑" Width="32" Height="28" Margin="0,0,6,0"
|
||||
Style="{StaticResource CrumbButton}"
|
||||
Command="{Binding UpCommand}"
|
||||
IsEnabled="{Binding CanGoUp}"
|
||||
ToolTip="Up one folder"/>
|
||||
<Border Background="{DynamicResource InputBg}" BorderBrush="{DynamicResource Stroke}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="4,0" MinHeight="28">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
Focusable="False">
|
||||
<ItemsControl ItemsSource="{Binding Breadcrumb}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Style="{StaticResource CrumbButton}"
|
||||
Command="{Binding DataContext.GoBreadcrumbCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip="{Binding Path}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock FontFamily="{StaticResource Symbol}" Text="" FontSize="14"
|
||||
Margin="0,0,6,0" VerticalAlignment="Center" Foreground="{DynamicResource Accent}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Label}" Value="This PC">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Text="›" Margin="2,0,2,0" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsLast}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
<Style x:Key="PaneChrome" TargetType="Border">
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
|
||||
@@ -706,6 +647,7 @@
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsDropTarget}" Value="True">
|
||||
|
||||
@@ -5,6 +5,15 @@ using Explorer.Domain;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
public sealed class InverseBooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is true ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public sealed class ActiveThicknessConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
||||
79
src/Explorer.App/ExplorerAddressBar.xaml
Normal file
79
src/Explorer.App/ExplorerAddressBar.xaml
Normal file
@@ -0,0 +1,79 @@
|
||||
<UserControl x:Class="Explorer.App.ExplorerAddressBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Focusable="False">
|
||||
<DockPanel LastChildFill="True" Margin="6,6,6,4">
|
||||
<Button DockPanel.Dock="Left" Content="↑" Width="32" Height="28" Margin="0,0,6,0"
|
||||
Style="{StaticResource CrumbButton}"
|
||||
Command="{Binding UpCommand}"
|
||||
IsEnabled="{Binding CanGoUp}"
|
||||
ToolTip="Up one folder"/>
|
||||
<Grid MinHeight="28">
|
||||
<Border Background="{DynamicResource InputBg}" BorderBrush="{DynamicResource Stroke}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="4,0"
|
||||
Cursor="IBeam"
|
||||
MouseLeftButtonDown="OnBreadcrumbMouseDown"
|
||||
Visibility="{Binding IsEditingPath, Converter={StaticResource InvBoolVis}}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
Focusable="False" HorizontalAlignment="Stretch">
|
||||
<ItemsControl ItemsSource="{Binding Breadcrumb}" HorizontalAlignment="Left">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Style="{StaticResource CrumbButton}"
|
||||
Command="{Binding DataContext.GoBreadcrumbCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Cursor="Hand"
|
||||
ToolTip="{Binding Path}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock FontFamily="{StaticResource Symbol}" Text="" FontSize="14"
|
||||
Margin="0,0,6,0" VerticalAlignment="Center" Foreground="{DynamicResource Accent}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Label}" Value="This PC">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Text="›" Margin="2,0,2,0" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsLast}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<ComboBox x:Name="PathCombo"
|
||||
Style="{StaticResource PathComboBox}"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding DataContext.PathHistory, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Text="{Binding PathEditText, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding IsEditingPath, Converter={StaticResource BoolVis}}"
|
||||
PreviewKeyDown="OnPathKeyDown"
|
||||
SelectionChanged="OnPathHistorySelected"
|
||||
LostKeyboardFocus="OnPathLostKeyboardFocus"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
178
src/Explorer.App/ExplorerAddressBar.xaml.cs
Normal file
178
src/Explorer.App/ExplorerAddressBar.xaml.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
public partial class ExplorerAddressBar : UserControl
|
||||
{
|
||||
private bool _suppressLostFocus;
|
||||
|
||||
public ExplorerAddressBar()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
IsVisibleChanged += (_, _) =>
|
||||
{
|
||||
if (IsVisible && DataContext is ExplorerPaneViewModel { IsEditingPath: true })
|
||||
{
|
||||
FocusEditor();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void FocusEditor()
|
||||
{
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
PathCombo.ApplyTemplate();
|
||||
if (PathCombo.Template.FindName("PART_EditableTextBox", PathCombo) is TextBox box)
|
||||
{
|
||||
box.Focus();
|
||||
box.SelectAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
PathCombo.Focus();
|
||||
}
|
||||
}, DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.OldValue is ExplorerPaneViewModel previous)
|
||||
{
|
||||
previous.PropertyChanged -= OnPanePropertyChanged;
|
||||
}
|
||||
|
||||
if (e.NewValue is ExplorerPaneViewModel pane)
|
||||
{
|
||||
pane.PropertyChanged += OnPanePropertyChanged;
|
||||
if (pane.IsEditingPath)
|
||||
{
|
||||
FocusEditor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPanePropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ExplorerPaneViewModel.IsEditingPath)
|
||||
&& sender is ExplorerPaneViewModel { IsEditingPath: true })
|
||||
{
|
||||
FocusEditor();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBreadcrumbMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is DependencyObject origin && IsInsideButton(origin))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is not ExplorerPaneViewModel pane)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pane.BeginEditPath();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPathKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not ExplorerPaneViewModel pane)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
CommitPath();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
_suppressLostFocus = true;
|
||||
pane.CancelEditPath();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPathHistorySelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ComboBox combo || e.AddedItems.Count == 0 || e.AddedItems[0] is not string path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is not ExplorerPaneViewModel pane)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NavigationTreeViewModel.PathsEqual(path, pane.CurrentPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (combo.IsDropDownOpen || combo.IsKeyboardFocusWithin)
|
||||
{
|
||||
pane.PathEditText = path;
|
||||
CommitPath();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPathLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (_suppressLostFocus)
|
||||
{
|
||||
_suppressLostFocus = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (PathCombo.IsDropDownOpen || PathCombo.IsKeyboardFocusWithin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is ExplorerPaneViewModel pane)
|
||||
{
|
||||
pane.CancelEditPath();
|
||||
}
|
||||
}, DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
private void CommitPath()
|
||||
{
|
||||
if (Window.GetWindow(this)?.DataContext is MainViewModel vm)
|
||||
{
|
||||
vm.GoCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInsideButton(DependencyObject origin)
|
||||
{
|
||||
for (var current = origin; current is not null;)
|
||||
{
|
||||
if (current is Button)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current is Visual
|
||||
? VisualTreeHelper.GetParent(current)
|
||||
: LogicalTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
462
src/Explorer.App/ListMarquee.cs
Normal file
462
src/Explorer.App/ListMarquee.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Explorer.Application;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
internal sealed class ListMarquee
|
||||
{
|
||||
private readonly DispatcherTimer _scroll = new() { Interval = TimeSpan.FromMilliseconds(20) };
|
||||
private ListView? _list;
|
||||
private MarqueeAdorner? _adorner;
|
||||
private Point _startContent;
|
||||
private Point _currentList;
|
||||
private object[] _kept = [];
|
||||
private MouseButton _button;
|
||||
|
||||
public ListMarquee() => _scroll.Tick += (_, _) => AutoScroll();
|
||||
|
||||
public bool IsArmed { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public void Arm(ListView list, MouseButtonEventArgs e, bool additive)
|
||||
{
|
||||
Cancel();
|
||||
_list = list;
|
||||
_button = e.ChangedButton;
|
||||
_kept = additive ? list.SelectedItems.Cast<object>().ToArray() : [];
|
||||
_startContent = ToContent(list, e.GetPosition(list));
|
||||
_currentList = e.GetPosition(list);
|
||||
IsArmed = true;
|
||||
list.Focus();
|
||||
if (!additive)
|
||||
{
|
||||
list.UnselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void Disarm()
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
IsArmed = false;
|
||||
_list = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryActivate(Point currentOnList)
|
||||
{
|
||||
if (!IsArmed || IsActive || _list is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Math.Abs(currentOnList.X - _currentList.X) < SystemParameters.MinimumHorizontalDragDistance
|
||||
&& Math.Abs(currentOnList.Y - _currentList.Y) < SystemParameters.MinimumVerticalDragDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IsActive = true;
|
||||
_list.CaptureMouse();
|
||||
_list.Focus();
|
||||
var layer = AdornerLayer.GetAdornerLayer(_list);
|
||||
if (layer is not null)
|
||||
{
|
||||
_adorner = new MarqueeAdorner(_list);
|
||||
layer.Add(_adorner);
|
||||
}
|
||||
|
||||
_scroll.Start();
|
||||
Update(currentOnList);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Update(Point currentOnList)
|
||||
{
|
||||
if (!IsActive || _list is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentList = currentOnList;
|
||||
ApplyHits();
|
||||
UpdateAdorner();
|
||||
}
|
||||
|
||||
public bool TryActivateFromMouse()
|
||||
=> _list is not null && TryActivate(Mouse.GetPosition(_list));
|
||||
|
||||
public void UpdateFromMouse()
|
||||
{
|
||||
if (_list is not null)
|
||||
{
|
||||
Update(Mouse.GetPosition(_list));
|
||||
}
|
||||
}
|
||||
|
||||
public ListView? CompleteForContextMenu()
|
||||
{
|
||||
var list = _list;
|
||||
var openMenu = IsActive && _button == MouseButton.Right;
|
||||
Stop(restore: false);
|
||||
return openMenu ? list : null;
|
||||
}
|
||||
|
||||
public void Cancel() => Stop(restore: IsActive);
|
||||
|
||||
private void Stop(bool restore)
|
||||
{
|
||||
_scroll.Stop();
|
||||
if (_list is not null)
|
||||
{
|
||||
if (_adorner is not null)
|
||||
{
|
||||
AdornerLayer.GetAdornerLayer(_list)?.Remove(_adorner);
|
||||
}
|
||||
|
||||
if (_list.IsMouseCaptured)
|
||||
{
|
||||
_list.ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
if (restore)
|
||||
{
|
||||
Restore(_list, _kept);
|
||||
}
|
||||
}
|
||||
|
||||
_adorner = null;
|
||||
_list = null;
|
||||
_kept = [];
|
||||
IsArmed = false;
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
private void ApplyHits()
|
||||
{
|
||||
if (_list is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var next = new HashSet<object>(_kept);
|
||||
foreach (var item in HitItems(_list, MarqueeListRect()))
|
||||
{
|
||||
next.Add(item);
|
||||
}
|
||||
|
||||
if (next.Count == _list.SelectedItems.Count && next.SetEquals(_list.SelectedItems.Cast<object>()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_list.UnselectAll();
|
||||
foreach (var item in next)
|
||||
{
|
||||
_list.SelectedItems.Add(item);
|
||||
if (_list.ItemContainerGenerator.ContainerFromItem(item) is ListViewItem row)
|
||||
{
|
||||
row.IsSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Rect MarqueeListRect()
|
||||
{
|
||||
var start = FromContent(_list!, _startContent);
|
||||
var rect = new Rect(start, _currentList);
|
||||
if (rect.Width < 1)
|
||||
{
|
||||
rect.Width = 1;
|
||||
}
|
||||
|
||||
if (rect.Height < 1)
|
||||
{
|
||||
rect.Height = 1;
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
private static IEnumerable<object> HitItems(ListView list, Rect marquee)
|
||||
{
|
||||
var wrap = FindWrapPanel(list);
|
||||
var viewport = ViewportBounds(list);
|
||||
var sampleIndex = -1;
|
||||
var sampleBounds = Rect.Empty;
|
||||
var hits = new HashSet<int>();
|
||||
for (var i = 0; i < list.Items.Count; i++)
|
||||
{
|
||||
if (list.ItemContainerGenerator.ContainerFromIndex(i) is not ListViewItem row
|
||||
|| row.ActualHeight < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var origin = row.TranslatePoint(new Point(0, 0), list);
|
||||
var bounds = wrap is null
|
||||
? new Rect(viewport.X, origin.Y, Math.Max(viewport.Width, 1), row.ActualHeight)
|
||||
: new Rect(origin, new Size(Math.Max(row.ActualWidth, 1), Math.Max(row.ActualHeight, 1)));
|
||||
if (sampleIndex < 0)
|
||||
{
|
||||
sampleIndex = i;
|
||||
sampleBounds = bounds;
|
||||
}
|
||||
|
||||
if (bounds.IntersectsWith(marquee))
|
||||
{
|
||||
hits.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (wrap is null && sampleIndex >= 0 && sampleBounds.Height > 0)
|
||||
{
|
||||
var first = (int)Math.Floor((marquee.Top - sampleBounds.Y) / sampleBounds.Height) + sampleIndex;
|
||||
var last = (int)Math.Ceiling((marquee.Bottom - sampleBounds.Y) / sampleBounds.Height) - 1 + sampleIndex;
|
||||
first = Math.Clamp(first, 0, list.Items.Count - 1);
|
||||
last = Math.Clamp(last, 0, list.Items.Count - 1);
|
||||
for (var i = first; i <= last; i++)
|
||||
{
|
||||
hits.Add(i);
|
||||
}
|
||||
}
|
||||
else if (wrap is not null)
|
||||
{
|
||||
foreach (var index in Hits(list, ToContent(list, marquee.TopLeft), ToContent(list, marquee.BottomRight)))
|
||||
{
|
||||
hits.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var index in hits.OrderBy(i => i))
|
||||
{
|
||||
yield return list.Items[index];
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAdorner()
|
||||
{
|
||||
if (_list is null || _adorner is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = MarqueeListRect();
|
||||
rect.Intersect(ViewportBounds(_list));
|
||||
_adorner.Bounds = rect;
|
||||
_adorner.InvalidateVisual();
|
||||
}
|
||||
|
||||
private void AutoScroll()
|
||||
{
|
||||
if (_list is null || Mouse.LeftButton != MouseButtonState.Pressed && Mouse.RightButton != MouseButtonState.Pressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = Mouse.GetPosition(_list);
|
||||
_currentList = pos;
|
||||
var zone = 32d;
|
||||
var viewer = FindScrollViewer(_list);
|
||||
if (viewer is not null)
|
||||
{
|
||||
var local = _list.TranslatePoint(pos, viewer);
|
||||
if (local.Y < zone)
|
||||
{
|
||||
viewer.LineUp();
|
||||
}
|
||||
else if (local.Y > viewer.ActualHeight - zone)
|
||||
{
|
||||
viewer.LineDown();
|
||||
}
|
||||
}
|
||||
|
||||
ApplyHits();
|
||||
UpdateAdorner();
|
||||
}
|
||||
|
||||
public static bool IsBackground(DependencyObject? source)
|
||||
{
|
||||
while (source is not null)
|
||||
{
|
||||
if (source is ListViewItem or GridViewColumnHeader or ScrollBar or Thumb)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source is ListView)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
source = source is Visual
|
||||
? VisualTreeHelper.GetParent(source)
|
||||
: LogicalTreeHelper.GetParent(source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<int> Hits(ListView list, Point startContent, Point currentContent)
|
||||
{
|
||||
var count = list.Items.Count;
|
||||
if (FindWrapPanel(list) is { } wrap)
|
||||
{
|
||||
return MarqueeRange.Wrap(
|
||||
startContent.X, startContent.Y, currentContent.X, currentContent.Y,
|
||||
count, wrap.Columns, wrap.ItemWidth, wrap.ItemHeight);
|
||||
}
|
||||
|
||||
return MarqueeRange.Stack(startContent.Y, currentContent.Y, count, RowHeight(list));
|
||||
}
|
||||
|
||||
private static Point ToContent(ListView list, Point listPoint)
|
||||
{
|
||||
if (FindWrapPanel(list) is { } wrap)
|
||||
{
|
||||
var p = list.TranslatePoint(listPoint, wrap);
|
||||
return new Point(p.X, p.Y + wrap.VerticalOffset);
|
||||
}
|
||||
|
||||
var presenter = (UIElement?)FindItemsPresenter(list) ?? list;
|
||||
var local = list.TranslatePoint(listPoint, presenter);
|
||||
var viewer = FindScrollViewer(list);
|
||||
var height = RowHeight(list);
|
||||
if (viewer is { CanContentScroll: true } && height > 0)
|
||||
{
|
||||
return new Point(local.X, viewer.VerticalOffset * height + local.Y);
|
||||
}
|
||||
|
||||
return new Point(local.X, (viewer?.VerticalOffset ?? 0) + local.Y);
|
||||
}
|
||||
|
||||
private static Point FromContent(ListView list, Point content)
|
||||
{
|
||||
if (FindWrapPanel(list) is { } wrap)
|
||||
{
|
||||
return wrap.TranslatePoint(new Point(content.X, content.Y - wrap.VerticalOffset), list);
|
||||
}
|
||||
|
||||
var presenter = FindItemsPresenter(list);
|
||||
if (presenter is null)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
var viewer = FindScrollViewer(list);
|
||||
var height = RowHeight(list);
|
||||
double y;
|
||||
if (viewer is { CanContentScroll: true } && height > 0)
|
||||
{
|
||||
y = content.Y - viewer.VerticalOffset * height;
|
||||
}
|
||||
else
|
||||
{
|
||||
y = content.Y - (viewer?.VerticalOffset ?? 0);
|
||||
}
|
||||
|
||||
return presenter.TranslatePoint(new Point(content.X, y), list);
|
||||
}
|
||||
|
||||
private static Rect ViewportBounds(ListView list)
|
||||
{
|
||||
var presenter = FindItemsPresenter(list);
|
||||
if (presenter is null)
|
||||
{
|
||||
return new Rect(list.RenderSize);
|
||||
}
|
||||
|
||||
var origin = presenter.TranslatePoint(new Point(0, 0), list);
|
||||
return new Rect(origin, presenter.RenderSize);
|
||||
}
|
||||
|
||||
private static double RowHeight(ListView list)
|
||||
{
|
||||
for (var i = 0; i < list.Items.Count; i++)
|
||||
{
|
||||
if (list.ItemContainerGenerator.ContainerFromIndex(i) is ListViewItem { ActualHeight: > 1 } row)
|
||||
{
|
||||
return row.ActualHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return 28;
|
||||
}
|
||||
|
||||
private static void Restore(ListView list, IReadOnlyList<object> kept)
|
||||
{
|
||||
list.UnselectAll();
|
||||
foreach (var item in kept)
|
||||
{
|
||||
list.SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemsPresenter? FindItemsPresenter(DependencyObject root) => FindChild<ItemsPresenter>(root);
|
||||
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject root) => FindChild<ScrollViewer>(root);
|
||||
|
||||
private static VirtualizingWrapPanel? FindWrapPanel(DependencyObject root) => FindChild<VirtualizingWrapPanel>(root);
|
||||
|
||||
private static T? FindChild<T>(DependencyObject root)
|
||||
where T : DependencyObject
|
||||
{
|
||||
if (root is T match)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
|
||||
{
|
||||
var found = FindChild<T>(VisualTreeHelper.GetChild(root, i));
|
||||
if (found is not null)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class MarqueeAdorner : Adorner
|
||||
{
|
||||
public MarqueeAdorner(UIElement adorned) : base(adorned)
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
public Rect Bounds { get; set; }
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext)
|
||||
{
|
||||
if (Bounds.IsEmpty || Bounds.Width < 1 || Bounds.Height < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var accent = TryAccent();
|
||||
var fill = new SolidColorBrush(Color.FromArgb(0x55, accent.R, accent.G, accent.B));
|
||||
var stroke = new SolidColorBrush(Color.FromArgb(0xE0, accent.R, accent.G, accent.B));
|
||||
fill.Freeze();
|
||||
stroke.Freeze();
|
||||
drawingContext.DrawRectangle(fill, new Pen(stroke, 1), Bounds);
|
||||
}
|
||||
|
||||
private Color TryAccent()
|
||||
{
|
||||
if (AdornedElement is FrameworkElement fe
|
||||
&& fe.TryFindResource("Accent") is SolidColorBrush brush)
|
||||
{
|
||||
return brush.Color;
|
||||
}
|
||||
|
||||
return Color.FromRgb(0x60, 0xCD, 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,81 @@
|
||||
CornerRadius="0"
|
||||
UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
<Window.Resources>
|
||||
<ContextMenu x:Key="FolderListContextMenu" x:Shared="false">
|
||||
<MenuItem Header="Open" Click="OnCtxOpen"/>
|
||||
<Separator/>
|
||||
<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"/>
|
||||
<MenuItem Header="Batch rename…" Click="OnBatchRename"
|
||||
Visibility="{Binding ShowBatchRename, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Run profile" Tag="RunProfileMenu"
|
||||
IsEnabled="{Binding ShowRunProfile}">
|
||||
<MenuItem Header="No profiles yet" IsEnabled="False"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Organize this folder…" Click="OnOrganizeFolder"
|
||||
Visibility="{Binding ShowOrganizeFolder, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Extract here" Click="OnExtractHere"
|
||||
Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Extract to…" Click="OnExtractTo"
|
||||
Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Verify archive" Click="OnVerifyArchive"
|
||||
Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Compress to ZIP" Click="OnCompressZip"
|
||||
Visibility="{Binding ShowCompress, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Compress to 7z" Click="OnCompressSevenZip"
|
||||
Visibility="{Binding ShowCompress, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Add to archive…" Click="OnAddToArchive"
|
||||
Visibility="{Binding ShowAddToArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Convert…" Click="OnConvert"
|
||||
Visibility="{Binding ShowConvert, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="New folder" Click="OnCtxNewFolder"/>
|
||||
<MenuItem Header="Copy path" Click="OnCtxCopyPath"/>
|
||||
<MenuItem Header="View changes…" Click="OnGitChanges"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Commit…" Click="OnGitCommit"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Fetch" Click="OnGitFetch"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Pull (fast-forward)" Click="OnGitPull"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Pull (merge)" Click="OnGitPullMerge"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Push" Click="OnGitPush"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Open terminal here" Command="{Binding OpenTerminalCommand}"
|
||||
Visibility="{Binding ShowOpenTerminal, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Open in Cursor" Command="{Binding OpenInCursorCommand}"
|
||||
Visibility="{Binding ShowOpenInCursor, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Add to Favorites" Click="OnAddFavorite"
|
||||
Visibility="{Binding ShowAddFavorite, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Remove from Favorites" Click="OnRemoveFavorite"
|
||||
Visibility="{Binding ShowRemoveFavorite, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator/>
|
||||
<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 ShowForgetSource, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Add to Workbench" Click="OnImportWindowsLocation"
|
||||
Visibility="{Binding ShowImportWindowsLocation, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Empty Recycle Bin" Click="OnEmptyRecycleBin"
|
||||
Visibility="{Binding ShowEmptyRecycleBin, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Always keep on this device"
|
||||
Command="{Binding PinCloudCommand}"
|
||||
Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Free up space"
|
||||
Command="{Binding FreeUpCloudSpaceCommand}"
|
||||
Visibility="{Binding ShowCloudDehydrate, Converter={StaticResource BoolVis}}"/>
|
||||
</ContextMenu>
|
||||
</Window.Resources>
|
||||
<AdornerDecorator>
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Height="40" Background="{DynamicResource Panel}"
|
||||
BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1"
|
||||
@@ -61,6 +136,7 @@
|
||||
<MenuItem Header="_Storage">
|
||||
<MenuItem Header="Storage _analysis" Command="{Binding Analysis.OpenCommand}"/>
|
||||
<MenuItem Header="_Duplicates" Command="{Binding Duplicates.OpenCommand}"/>
|
||||
<MenuItem Header="Run background _maintenance now" Command="{Binding RunMaintenanceNowCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Locations">
|
||||
<MenuItem Header="_Index this location" Command="{Binding BuildIndexCommand}"/>
|
||||
@@ -184,12 +260,8 @@
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" Margin="12,0" Style="{StaticResource PathComboBox}"
|
||||
ItemsSource="{Binding PathHistory}"
|
||||
Text="{Binding PathText, UpdateSourceTrigger=PropertyChanged}"
|
||||
PreviewKeyDown="OnPathKeyDown"
|
||||
SelectionChanged="OnPathHistorySelected"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding Search.Text, UpdateSourceTrigger=PropertyChanged}"
|
||||
<TextBox Grid.Column="2" Margin="12,0,0,0"
|
||||
Text="{Binding Search.Text, UpdateSourceTrigger=PropertyChanged}"
|
||||
KeyDown="OnSearchKeyDown"/>
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="8,0,0,0">
|
||||
<Button Content="Search" Command="{Binding SearchCommand}"/>
|
||||
@@ -200,12 +272,15 @@
|
||||
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="0,1,0,0" Padding="8,6">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Footer}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"
|
||||
<TextBlock Text="{Binding ActivePane.ListingStatus}" Foreground="{DynamicResource Fg}"
|
||||
VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Footer}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" Margin="0,0,12,0"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding ActivePane.GitBadge}" VerticalAlignment="Center" FontSize="11"
|
||||
Foreground="{DynamicResource FgMuted}" Margin="0,0,16,0"
|
||||
Visibility="{Binding ActivePane.HasGitBadge, Converter={StaticResource BoolVis}}"/>
|
||||
@@ -363,6 +438,8 @@
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<TreeView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem x:Name="AddFavoriteMenu" Header="Add to Favorites" Click="OnAddFavoriteFromTree"/>
|
||||
<MenuItem x:Name="RemoveFavoriteMenu" Header="Remove from Favorites" Click="OnRemoveFavoriteFromTree"/>
|
||||
<MenuItem x:Name="RemoveLocationMenu" Header="Remove from Explorer" Click="OnRemoveLocation"/>
|
||||
</ContextMenu>
|
||||
</TreeView.ContextMenu>
|
||||
@@ -405,8 +482,7 @@
|
||||
<Border Grid.Column="0" DataContext="{Binding Left}" Style="{StaticResource PaneChrome}"
|
||||
PreviewMouseDown="OnPaneChromeMouseDown">
|
||||
<DockPanel LastChildFill="True">
|
||||
<ContentControl DockPanel.Dock="Top" Content="{Binding}" ContentTemplate="{StaticResource PaneAddressBar}"
|
||||
Focusable="False"/>
|
||||
<local:ExplorerAddressBar DockPanel.Dock="Top"/>
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource Banner}" Padding="10,8"
|
||||
Visibility="{Binding ShowIndexBanner, Converter={StaticResource BoolVis}}">
|
||||
<DockPanel>
|
||||
@@ -417,6 +493,9 @@
|
||||
</Border>
|
||||
<Grid>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
AllowDrop="True"
|
||||
@@ -445,78 +524,11 @@
|
||||
<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 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"/>
|
||||
<MenuItem Header="Batch rename…" Click="OnBatchRename"
|
||||
Visibility="{Binding ShowBatchRename, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Run profile" Tag="RunProfileMenu"
|
||||
IsEnabled="{Binding ShowRunProfile}">
|
||||
<MenuItem Header="No profiles yet" IsEnabled="False"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Organize this folder…" Click="OnOrganizeFolder"
|
||||
Visibility="{Binding ShowOrganizeFolder, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Extract here" Click="OnExtractHere"
|
||||
Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Extract to…" Click="OnExtractTo"
|
||||
Visibility="{Binding ShowExtractArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Verify archive" Click="OnVerifyArchive"
|
||||
Visibility="{Binding ShowVerifyArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Compress to ZIP" Click="OnCompressZip"
|
||||
Visibility="{Binding ShowCompress, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Compress to 7z" Click="OnCompressSevenZip"
|
||||
Visibility="{Binding ShowCompress, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Add to archive…" Click="OnAddToArchive"
|
||||
Visibility="{Binding ShowAddToArchive, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Convert…" Click="OnConvert"
|
||||
Visibility="{Binding ShowConvert, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="New folder" Click="OnCtxNewFolder"/>
|
||||
<MenuItem Header="Copy path" Click="OnCtxCopyPath"/>
|
||||
<MenuItem Header="View changes…" Click="OnGitChanges"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Commit…" Click="OnGitCommit"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Fetch" Click="OnGitFetch"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Pull (fast-forward)" Click="OnGitPull"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Pull (merge)" Click="OnGitPullMerge"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Push" Click="OnGitPush"
|
||||
Visibility="{Binding ShowGitActions, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Open terminal here" Command="{Binding OpenTerminalCommand}"
|
||||
Visibility="{Binding ShowOpenTerminal, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Open in Cursor" Command="{Binding OpenInCursorCommand}"
|
||||
Visibility="{Binding ShowOpenInCursor, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator/>
|
||||
<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 ShowForgetSource, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Add to Workbench" Click="OnImportWindowsLocation"
|
||||
Visibility="{Binding ShowImportWindowsLocation, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Empty Recycle Bin" Click="OnEmptyRecycleBin"
|
||||
Visibility="{Binding ShowEmptyRecycleBin, Converter={StaticResource BoolVis}}"/>
|
||||
<Separator Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Always keep on this device"
|
||||
Command="{Binding PinCloudCommand}"
|
||||
Visibility="{Binding ShowCloudPin, Converter={StaticResource BoolVis}}"/>
|
||||
<MenuItem Header="Free up space"
|
||||
Command="{Binding FreeUpCloudSpaceCommand}"
|
||||
Visibility="{Binding ShowCloudDehydrate, Converter={StaticResource BoolVis}}"/>
|
||||
</ContextMenu>
|
||||
</ListView.ContextMenu>
|
||||
</ListView>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
AllowDrop="True"
|
||||
@@ -543,6 +555,9 @@
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
ItemTemplate="{StaticResource PreviewTile}"
|
||||
ItemContainerStyle="{StaticResource IconListItem}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
@@ -587,8 +602,7 @@
|
||||
PreviewMouseDown="OnPaneChromeMouseDown"
|
||||
Visibility="{Binding DataContext.IsSplit, RelativeSource={RelativeSource AncestorType=Grid}, Converter={StaticResource BoolVis}}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<ContentControl DockPanel.Dock="Top" Content="{Binding}" ContentTemplate="{StaticResource PaneAddressBar}"
|
||||
Focusable="False"/>
|
||||
<local:ExplorerAddressBar DockPanel.Dock="Top"/>
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource Banner}" Padding="10,8"
|
||||
Visibility="{Binding ShowIndexBanner, Converter={StaticResource BoolVis}}">
|
||||
<DockPanel>
|
||||
@@ -599,6 +613,9 @@
|
||||
</Border>
|
||||
<Grid>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
AllowDrop="True"
|
||||
@@ -629,6 +646,9 @@
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
AllowDrop="True"
|
||||
@@ -655,6 +675,9 @@
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<ListView ItemsSource="{Binding Items}"
|
||||
SelectionMode="Extended"
|
||||
IsSynchronizedWithCurrentItem="False"
|
||||
ContextMenu="{StaticResource FolderListContextMenu}"
|
||||
ItemTemplate="{StaticResource PreviewTile}"
|
||||
ItemContainerStyle="{StaticResource IconListItem}"
|
||||
MouseDoubleClick="OnItemDoubleClick"
|
||||
@@ -1007,4 +1030,5 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</AdornerDecorator>
|
||||
</Window>
|
||||
|
||||
@@ -21,6 +21,7 @@ public partial class MainWindow : Window
|
||||
private bool _dragPending;
|
||||
private MouseButton _dragButton;
|
||||
private FolderItemViewModel? _dragItem;
|
||||
private readonly ListMarquee _marquee = new();
|
||||
private bool _suppressItemContextMenu;
|
||||
private bool _incomingRightDrag;
|
||||
private bool _sourceRightDrag;
|
||||
@@ -34,6 +35,8 @@ public partial class MainWindow : Window
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += (_, _) => MaximizedWorkArea.Hook(this);
|
||||
StateChanged += (_, _) => SyncMaxRestoreButton();
|
||||
DataContextChanged += (_, _) =>
|
||||
{
|
||||
if (_wiredVm is not null)
|
||||
@@ -186,34 +189,6 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPathKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
Vm.GoCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPathHistorySelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ComboBox combo || e.AddedItems.Count == 0 || e.AddedItems[0] is not string path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NavigationTreeViewModel.PathsEqual(path, Vm.ActivePane.CurrentPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (combo.IsDropDownOpen || combo.IsKeyboardFocusWithin)
|
||||
{
|
||||
Vm.PathText = path;
|
||||
Vm.GoCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
@@ -245,16 +220,57 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var node = FindTreeNode(e.OriginalSource as DependencyObject);
|
||||
if (node is null || !node.CanRemove)
|
||||
if (node is null)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var canPin = Vm.CanPinFavorite(node);
|
||||
var canUnpin = Vm.CanUnpinFavorite(node.Path);
|
||||
AddFavoriteMenu.Visibility = canPin ? Visibility.Visible : Visibility.Collapsed;
|
||||
RemoveFavoriteMenu.Visibility = canUnpin ? Visibility.Visible : Visibility.Collapsed;
|
||||
RemoveLocationMenu.Visibility = node.CanRemove ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (!canPin && !canUnpin && !node.CanRemove)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
node.IsSelected = true;
|
||||
AddFavoriteMenu.Tag = node.Path;
|
||||
RemoveFavoriteMenu.Tag = node.Path;
|
||||
RemoveLocationMenu.Tag = node.Path;
|
||||
}
|
||||
|
||||
private async void OnAddFavorite(object sender, RoutedEventArgs e)
|
||||
=> await Vm.AddSelectedFavoritesAsync().ConfigureAwait(true);
|
||||
|
||||
private async void OnRemoveFavorite(object sender, RoutedEventArgs e)
|
||||
=> await Vm.RemoveSelectedFavoritesAsync().ConfigureAwait(true);
|
||||
|
||||
private async void OnAddFavoriteFromTree(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var path = (sender as FrameworkElement)?.Tag as string ?? AddFavoriteMenu.Tag as string;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Vm.AddFavoritesAsync([path]).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async void OnRemoveFavoriteFromTree(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var path = (sender as FrameworkElement)?.Tag as string ?? RemoveFavoriteMenu.Tag as string;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Vm.RemoveFavoritesAsync([path]).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async void OnRemoveLocation(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var path = (sender as FrameworkElement)?.Tag as string
|
||||
@@ -560,7 +576,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ListView list)
|
||||
if (sender is not ListView { IsVisible: true } list)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -608,6 +624,18 @@ public partial class MainWindow : Window
|
||||
_dragPending = true;
|
||||
_dragButton = e.ChangedButton;
|
||||
_dragItem = HitTestFolderItem(sender as DependencyObject, e.GetPosition((IInputElement)sender));
|
||||
if (sender is ListView list
|
||||
&& _dragItem is null
|
||||
&& ListMarquee.IsBackground(e.OriginalSource as DependencyObject))
|
||||
{
|
||||
_marquee.Arm(list, e, (Keyboard.Modifiers & ModifierKeys.Control) != 0);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_marquee.Disarm();
|
||||
}
|
||||
|
||||
TryScheduleClickRename(sender as ListView, e);
|
||||
}
|
||||
|
||||
@@ -729,6 +757,34 @@ public partial class MainWindow : Window
|
||||
base.OnPreviewMouseMove(e);
|
||||
var held = _dragButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed
|
||||
|| _dragButton == MouseButton.Right && e.RightButton == MouseButtonState.Pressed;
|
||||
if (_marquee.IsArmed || _marquee.IsActive)
|
||||
{
|
||||
if (!held)
|
||||
{
|
||||
if (!_marquee.IsActive)
|
||||
{
|
||||
_marquee.Disarm();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_marquee.IsActive)
|
||||
{
|
||||
_marquee.UpdateFromMouse();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_marquee.TryActivateFromMouse())
|
||||
{
|
||||
_dragPending = false;
|
||||
CancelClickRename();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_dragPending || !held)
|
||||
{
|
||||
return;
|
||||
@@ -784,15 +840,69 @@ public partial class MainWindow : Window
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (CompleteMarqueeMouseUp(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnPreviewMouseLeftButtonUp(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseRightButtonUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (CompleteMarqueeMouseUp(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnPreviewMouseRightButtonUp(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (CompleteMarqueeMouseUp(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnPreviewMouseUp(e);
|
||||
if (e.ChangedButton == _dragButton)
|
||||
{
|
||||
_dragPending = false;
|
||||
_marquee.Disarm();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CompleteMarqueeMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton != _dragButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_marquee.IsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_dragPending = false;
|
||||
var list = _marquee.CompleteForContextMenu();
|
||||
e.Handled = true;
|
||||
if (list?.ContextMenu is { } menu)
|
||||
{
|
||||
_suppressItemContextMenu = true;
|
||||
menu.DataContext = DataContext;
|
||||
_ = FillRunProfileMenuAsync(menu);
|
||||
menu.PlacementTarget = list;
|
||||
menu.Placement = PlacementMode.MousePoint;
|
||||
menu.IsOpen = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetDropFiles(DragEventArgs e, out string[] files)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] dropped && dropped.Length > 0)
|
||||
@@ -909,6 +1019,15 @@ public partial class MainWindow : Window
|
||||
|
||||
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
|
||||
var node = HitTestTreeNode(e);
|
||||
if (IsFavoritesPinTarget(node) && files.Any(Directory.Exists))
|
||||
{
|
||||
SetListDropTarget(null);
|
||||
SetTreeDropTarget(node);
|
||||
e.Effects = DragDropEffects.Link;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var dest = node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path)
|
||||
? null
|
||||
: node.Path;
|
||||
@@ -936,6 +1055,15 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
var node = HitTestTreeNode(e);
|
||||
if (IsFavoritesPinTarget(node))
|
||||
{
|
||||
_incomingRightDrag = false;
|
||||
_sourceRightDrag = false;
|
||||
await Vm.AddFavoritesAsync(files).ConfigureAwait(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var dest = HitTestTreePath(e);
|
||||
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
|
||||
{
|
||||
@@ -965,6 +1093,9 @@ public partial class MainWindow : Window
|
||||
SetTreeDropTarget(null);
|
||||
}
|
||||
|
||||
private static bool IsFavoritesPinTarget(NavNodeViewModel? node)
|
||||
=> node is { IsGroup: true, Path: LocationRoots.Favorites };
|
||||
|
||||
private string? HitTestTreePath(DragEventArgs e)
|
||||
{
|
||||
var node = HitTestTreeNode(e);
|
||||
@@ -1705,6 +1836,43 @@ public partial class MainWindow : Window
|
||||
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
|
||||
var shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
||||
var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
|
||||
if ((ctrl && e.Key == Key.L) || (alt && (e.Key == Key.D || e.SystemKey == Key.D)))
|
||||
{
|
||||
BeginAddressEdit();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAddressEditor(e.OriginalSource as DependencyObject))
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Vm.ActivePane.CancelEditPath();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Enter)
|
||||
{
|
||||
Vm.GoCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
if (e.Key is not (Key.F1 or Key.F5))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (e.OriginalSource is TextBox && e.Key is not (Key.F1 or Key.F5))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape && (_marquee.IsActive || _marquee.IsArmed))
|
||||
{
|
||||
_marquee.Cancel();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctrl && shift && e.Key == Key.N)
|
||||
{
|
||||
Vm.NewFolderCommand.Execute(null);
|
||||
@@ -1741,6 +1909,11 @@ public partial class MainWindow : Window
|
||||
{
|
||||
await Vm.PasteAsync().ConfigureAwait(true);
|
||||
}
|
||||
else if (ctrl && e.Key == Key.A && e.OriginalSource is not TextBox)
|
||||
{
|
||||
FindActiveFileList()?.SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Delete)
|
||||
{
|
||||
await DeleteSelectedAsync().ConfigureAwait(true);
|
||||
@@ -1771,4 +1944,51 @@ public partial class MainWindow : Window
|
||||
await Vm.OpenSelectedAsync().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginAddressEdit()
|
||||
{
|
||||
Vm.ActivePane.BeginEditPath();
|
||||
FindAddressBar(Vm.ActivePane)?.FocusEditor();
|
||||
}
|
||||
|
||||
private static bool IsAddressEditor(DependencyObject? origin)
|
||||
{
|
||||
for (var current = origin; current is not null;)
|
||||
{
|
||||
if (current is ExplorerAddressBar)
|
||||
{
|
||||
return origin is TextBox;
|
||||
}
|
||||
|
||||
current = current is Visual
|
||||
? VisualTreeHelper.GetParent(current)
|
||||
: LogicalTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ExplorerAddressBar? FindAddressBar(ExplorerPaneViewModel pane)
|
||||
=> FindAddressBar(this, pane);
|
||||
|
||||
private static ExplorerAddressBar? FindAddressBar(DependencyObject root, ExplorerPaneViewModel pane)
|
||||
{
|
||||
var count = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
if (child is ExplorerAddressBar bar && ReferenceEquals(bar.DataContext, pane))
|
||||
{
|
||||
return bar;
|
||||
}
|
||||
|
||||
var nested = FindAddressBar(child, pane);
|
||||
if (nested is not null)
|
||||
{
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
105
src/Explorer.App/MaximizedWorkArea.cs
Normal file
105
src/Explorer.App/MaximizedWorkArea.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
internal static class MaximizedWorkArea
|
||||
{
|
||||
private const int WmGetMinMaxInfo = 0x0024;
|
||||
private const uint MonitorDefaultToNearest = 2;
|
||||
|
||||
public static void Hook(Window window)
|
||||
{
|
||||
if (PresentationSource.FromVisual(window) is not HwndSource source)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
source.AddHook(WndProc);
|
||||
}
|
||||
|
||||
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg != WmGetMinMaxInfo)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var info = Marshal.PtrToStructure<MinMaxInfo>(lParam);
|
||||
var monitor = MonitorFromWindow(hwnd, MonitorDefaultToNearest);
|
||||
if (monitor != IntPtr.Zero)
|
||||
{
|
||||
var monitorInfo = new MonitorInfo { Size = Marshal.SizeOf<MonitorInfo>() };
|
||||
if (GetMonitorInfo(monitor, ref monitorInfo))
|
||||
{
|
||||
var work = monitorInfo.Work;
|
||||
var display = monitorInfo.Monitor;
|
||||
info.MaxPosition = new NativePoint(work.Left - display.Left, work.Top - display.Top);
|
||||
info.MaxSize = new NativePoint(work.Right - work.Left, work.Bottom - work.Top);
|
||||
info.MaxTrackSize = info.MaxSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (HwndSource.FromHwnd(hwnd)?.RootVisual is Window window)
|
||||
{
|
||||
var dpi = VisualTreeHelper.GetDpi(window);
|
||||
info.MinTrackSize = new NativePoint(
|
||||
(int)Math.Ceiling(window.MinWidth * dpi.DpiScaleX),
|
||||
(int)Math.Ceiling(window.MinHeight * dpi.DpiScaleY));
|
||||
}
|
||||
|
||||
Marshal.StructureToPtr(info, lParam, fDeleteOld: false);
|
||||
handled = true;
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativePoint
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public NativePoint(int x, int y)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeRect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MinMaxInfo
|
||||
{
|
||||
public NativePoint Reserved;
|
||||
public NativePoint MaxSize;
|
||||
public NativePoint MaxPosition;
|
||||
public NativePoint MinTrackSize;
|
||||
public NativePoint MaxTrackSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct MonitorInfo
|
||||
{
|
||||
public int Size;
|
||||
public NativeRect Monitor;
|
||||
public NativeRect Work;
|
||||
public uint Flags;
|
||||
}
|
||||
}
|
||||
23
src/Explorer.App/Settings/Pages/AdvancedSettingsPage.xaml
Normal file
23
src/Explorer.App/Settings/Pages/AdvancedSettingsPage.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.AdvancedSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Advanced" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Technical tool paths. Leave a path empty to search Program Files and PATH."/>
|
||||
|
||||
<TextBlock Text="Git" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<TextBlock Style="{StaticResource SettingsPathHelp}"
|
||||
Text="Repository badges and Git actions use git.exe when it is installed. Leave the path empty to look in Program Files and PATH. Git is not bundled. There is no branch UI or credential dialog."/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28"
|
||||
Click="OnBrowseGit" Margin="8,0,0,0"
|
||||
AutomationProperties.Name="Browse for Git"/>
|
||||
<TextBox Text="{Binding GitPath, UpdateSourceTrigger=PropertyChanged}"
|
||||
AutomationProperties.Name="Git path"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
24
src/Explorer.App/Settings/Pages/AdvancedSettingsPage.xaml.cs
Normal file
24
src/Explorer.App/Settings/Pages/AdvancedSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Explorer.App.Settings;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class AdvancedSettingsPage : UserControl
|
||||
{
|
||||
public AdvancedSettingsPage() => InitializeComponent();
|
||||
|
||||
private void OnBrowseGit(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is SettingsDraft draft
|
||||
&& SettingsPathBrowse.TryPick(
|
||||
Window.GetWindow(this),
|
||||
"Git executable",
|
||||
"Git|git.exe|Executables|*.exe|All files|*.*",
|
||||
draft.GitPath,
|
||||
out var path))
|
||||
{
|
||||
draft.GitPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Explorer.App/Settings/Pages/AppearanceSettingsPage.xaml
Normal file
19
src/Explorer.App/Settings/Pages/AppearanceSettingsPage.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.AppearanceSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Appearance" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Visual options for Explorer Workbench."/>
|
||||
|
||||
<TextBlock Text="Theme" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton Content="Dark" GroupName="SettingsTheme" Margin="0,0,16,0"
|
||||
IsChecked="{Binding IsDarkTheme, Mode=TwoWay}"/>
|
||||
<RadioButton Content="Light" GroupName="SettingsTheme"
|
||||
IsChecked="{Binding IsLightTheme, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class AppearanceSettingsPage : UserControl
|
||||
{
|
||||
public AppearanceSettingsPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.FileOperationsSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="File Operations" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Queue behavior and tools used for copy, move, archive, and convert work."/>
|
||||
|
||||
<TextBlock Text="File operations queue" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Auto clear queue when done"
|
||||
IsChecked="{Binding AutoClearQueueWhenDone}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="Finished copy, move, delete, and rename steps are removed automatically. Failed items stay until you dismiss or retry them. The queue is restored after restart."/>
|
||||
|
||||
<TextBlock Text="7-Zip" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<TextBlock Style="{StaticResource SettingsPathHelp}"
|
||||
Text="Extract, compress, add, and verify use 7-Zip when it is installed. Leave the path empty to look in Program Files and PATH. 7-Zip is not bundled with Explorer Workbench."/>
|
||||
<DockPanel Margin="0,0,0,18">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28"
|
||||
Click="OnBrowseSevenZip" Margin="8,0,0,0"
|
||||
AutomationProperties.Name="Browse for 7-Zip"/>
|
||||
<TextBox Text="{Binding SevenZipPath, UpdateSourceTrigger=PropertyChanged}"
|
||||
AutomationProperties.Name="7-Zip path"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="FFmpeg" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<TextBlock Style="{StaticResource SettingsPathHelp}"
|
||||
Text="Convert uses ffmpeg.exe from a Windows zip/build (ffprobe and ffplay are not required). Leave the path empty to look in Program Files\ffmpeg\bin and PATH. FFmpeg is not bundled with Explorer Workbench."/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28"
|
||||
Click="OnBrowseFfmpeg" Margin="8,0,0,0"
|
||||
AutomationProperties.Name="Browse for FFmpeg"/>
|
||||
<TextBox Text="{Binding FfmpegPath, UpdateSourceTrigger=PropertyChanged}"
|
||||
AutomationProperties.Name="FFmpeg path"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Explorer.App.Settings;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class FileOperationsSettingsPage : UserControl
|
||||
{
|
||||
public FileOperationsSettingsPage() => InitializeComponent();
|
||||
|
||||
private void OnBrowseSevenZip(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is SettingsDraft draft
|
||||
&& SettingsPathBrowse.TryPick(
|
||||
Window.GetWindow(this),
|
||||
"7-Zip executable",
|
||||
"7-Zip|7z.exe;7za.exe|Executables|*.exe|All files|*.*",
|
||||
draft.SevenZipPath,
|
||||
out var path))
|
||||
{
|
||||
draft.SevenZipPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseFfmpeg(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is SettingsDraft draft
|
||||
&& SettingsPathBrowse.TryPick(
|
||||
Window.GetWindow(this),
|
||||
"FFmpeg executable",
|
||||
"FFmpeg|ffmpeg.exe|Executables|*.exe|All files|*.*",
|
||||
draft.FfmpegPath,
|
||||
out var path))
|
||||
{
|
||||
draft.FfmpegPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Explorer.App/Settings/Pages/GeneralSettingsPage.xaml
Normal file
19
src/Explorer.App/Settings/Pages/GeneralSettingsPage.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.GeneralSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="General" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Startup and application behavior. These options do not change Windows Explorer settings."/>
|
||||
|
||||
<TextBlock Text="Background host" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Start Explorer.Host.exe at Windows sign-in"
|
||||
IsChecked="{Binding BackgroundHostAtLogon}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}" Margin="24,0,0,0"
|
||||
Text="Adds Explorer.Host.exe to your Windows sign-in programs for this user. No administrator rights. The window connects to Explorer.Host.exe for indexing and the queue. If the host is not running, the window starts it. Only the host opens the index for write. A tray icon stays while the host is running: open the window, or quit the host. File → Stop background host does the same from the window."/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class GeneralSettingsPage : UserControl
|
||||
{
|
||||
public GeneralSettingsPage() => InitializeComponent();
|
||||
}
|
||||
51
src/Explorer.App/Settings/Pages/IndexingSettingsPage.xaml
Normal file
51
src/Explorer.App/Settings/Pages/IndexingSettingsPage.xaml
Normal file
@@ -0,0 +1,51 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.IndexingSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Indexing" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="What Workbench records in the local index. Online-only cloud files are never hydrated."/>
|
||||
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Include archive contents in the index"
|
||||
IsChecked="{Binding IndexArchiveContents}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="When enabled, a scan lists files inside ZIP, RAR, 7z, TAR, and similar archives from the archive catalog — files are not extracted. Individual uncompressed sizes are stored. Folder totals still use the archive’s size on disk. Online-only cloud archives are skipped."/>
|
||||
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Automatically index removable drives when they appear"
|
||||
IsChecked="{Binding AutoIndexRemovable}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="USB and other removable volumes are queued for a full scan when they come online and are not indexed yet. Cloud locations are never auto-indexed. Online-only files are not hydrated."/>
|
||||
|
||||
<TextBlock Text="Idle maintenance" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="When you are not using the PC, the background host can hash duplicates, refresh stale local indexes, and capture history. Copy, move, and other jobs you started keep running. Cloud files are never hydrated. Network and removable locations are not scanned automatically."/>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Enable background maintenance when idle"
|
||||
IsChecked="{Binding BackgroundMaintenanceWhenIdle}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="The host watches Windows idle time even if this window is closed. Conservative defaults wait 10 minutes and prefer AC power."/>
|
||||
<TextBlock Text="Idle for" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8"
|
||||
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"/>
|
||||
<DockPanel LastChildFill="False" Margin="0,0,0,14">
|
||||
<ComboBox Width="80" HorizontalAlignment="Left"
|
||||
ItemsSource="{Binding IdleThresholdChoices}"
|
||||
SelectedItem="{Binding IdleMaintenanceMinutes}"
|
||||
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"
|
||||
AutomationProperties.Name="Idle threshold in minutes"/>
|
||||
<TextBlock Text="minutes" VerticalAlignment="Center" Margin="10,0,0,0"
|
||||
Foreground="{DynamicResource FgMuted}"
|
||||
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"/>
|
||||
</DockPanel>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Only run expensive maintenance on AC power"
|
||||
IsChecked="{Binding IdleMaintenanceAcOnly}"
|
||||
IsEnabled="{Binding BackgroundMaintenanceWhenIdle}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}" Margin="24,0,0,0"
|
||||
Text="Skips hashing and idle rescans on battery so a laptop is not drained in the background."/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class IndexingSettingsPage : UserControl
|
||||
{
|
||||
public IndexingSettingsPage() => InitializeComponent();
|
||||
}
|
||||
47
src/Explorer.App/Settings/Pages/NavigationSettingsPage.xaml
Normal file
47
src/Explorer.App/Settings/Pages/NavigationSettingsPage.xaml
Normal file
@@ -0,0 +1,47 @@
|
||||
<UserControl x:Class="Explorer.App.Settings.Pages.NavigationSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
DataContext="{Binding Draft, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Navigation" Style="{StaticResource SettingsHeading}"
|
||||
AutomationProperties.HeadingLevel="Level1"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Locations tree, grouping, and what Workbench shows while browsing. These options only change what Explorer Workbench shows and indexes. Windows settings and files are not modified."/>
|
||||
|
||||
<TextBlock Text="Locations tree" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<TextBlock Style="{StaticResource SettingsIntro}"
|
||||
Text="Choose whether network and cloud locations appear as their own top-level items, or are collected under a group next to This PC."/>
|
||||
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Group network drives under Network"
|
||||
IsChecked="{Binding GroupNetworkPlaces}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="Mapped letters and UNC shares become children of a Network item. This PC then shows only local and removable drives."/>
|
||||
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Group cloud locations under Cloud"
|
||||
IsChecked="{Binding GroupCloudPlaces}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="OneDrive, Google Drive, and Nextcloud become children of a Cloud item. The two options are independent."/>
|
||||
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Prefer Favorites when synchronizing the locations tree"
|
||||
IsChecked="{Binding PreferFavoritesInTree}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="Off (default): keep the current tree context. Opening a folder under This PC, Home, Network, or Cloud does not switch the tree to Favorites just because that folder is also pinned. When you opened the folder from a Favorite, the tree stays under Favorites. On: if the folder is under a pinned Favorite, the tree selects that Favorite. Breadcrumbs and the filesystem path are unchanged."/>
|
||||
|
||||
<TextBlock Text="Filesystem visibility" Style="{StaticResource SettingsHeading}" Margin="0,8,0,10"
|
||||
AutomationProperties.HeadingLevel="Level2"/>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Show hidden files"
|
||||
IsChecked="{Binding ShowHiddenFiles}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}"
|
||||
Text="Items marked Hidden by Windows. Default matches the current Explorer Workbench listing."/>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="Show protected system locations"
|
||||
IsChecked="{Binding ShowProtectedSystemLocations}"/>
|
||||
<TextBlock Style="{StaticResource SettingsHelp}" Margin="24,0,0,0"
|
||||
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."/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Explorer.App.Settings.Pages;
|
||||
|
||||
public partial class NavigationSettingsPage : UserControl
|
||||
{
|
||||
public NavigationSettingsPage() => InitializeComponent();
|
||||
}
|
||||
20
src/Explorer.App/Settings/SettingsCatalog.cs
Normal file
20
src/Explorer.App/Settings/SettingsCatalog.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Explorer.App.Settings.Pages;
|
||||
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public static class SettingsCatalog
|
||||
{
|
||||
public static IReadOnlyList<SettingsCategory> Create()
|
||||
{
|
||||
// Storage & Analysis and Network & Cloud stay out of the list until they have settings.
|
||||
return
|
||||
[
|
||||
new(SettingsCategoryId.General, "General", new GeneralSettingsPage()),
|
||||
new(SettingsCategoryId.Appearance, "Appearance", new AppearanceSettingsPage()),
|
||||
new(SettingsCategoryId.Navigation, "Navigation", new NavigationSettingsPage()),
|
||||
new(SettingsCategoryId.FileOperations, "File Operations", new FileOperationsSettingsPage()),
|
||||
new(SettingsCategoryId.Indexing, "Indexing", new IndexingSettingsPage()),
|
||||
new(SettingsCategoryId.Advanced, "Advanced", new AdvancedSettingsPage())
|
||||
];
|
||||
}
|
||||
}
|
||||
17
src/Explorer.App/Settings/SettingsCategory.cs
Normal file
17
src/Explorer.App/Settings/SettingsCategory.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public sealed class SettingsCategory
|
||||
{
|
||||
public SettingsCategory(SettingsCategoryId id, string title, FrameworkElement page)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
Page = page;
|
||||
}
|
||||
|
||||
public SettingsCategoryId Id { get; }
|
||||
public string Title { get; }
|
||||
public FrameworkElement Page { get; }
|
||||
}
|
||||
13
src/Explorer.App/Settings/SettingsCategoryId.cs
Normal file
13
src/Explorer.App/Settings/SettingsCategoryId.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public enum SettingsCategoryId
|
||||
{
|
||||
General,
|
||||
Appearance,
|
||||
Navigation,
|
||||
FileOperations,
|
||||
Indexing,
|
||||
StorageAnalysis,
|
||||
NetworkCloud,
|
||||
Advanced
|
||||
}
|
||||
101
src/Explorer.App/Settings/SettingsDraft.cs
Normal file
101
src/Explorer.App/Settings/SettingsDraft.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Explorer.Application;
|
||||
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public sealed partial class SettingsDraft : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _theme = "Dark";
|
||||
[ObservableProperty] private bool _groupNetworkPlaces;
|
||||
[ObservableProperty] private bool _groupCloudPlaces;
|
||||
[ObservableProperty] private bool _preferFavoritesInTree;
|
||||
[ObservableProperty] private bool _showHiddenFiles = true;
|
||||
[ObservableProperty] private bool _showProtectedSystemLocations;
|
||||
[ObservableProperty] private bool _autoClearQueueWhenDone;
|
||||
[ObservableProperty] private bool _indexArchiveContents;
|
||||
[ObservableProperty] private bool _autoIndexRemovable;
|
||||
[ObservableProperty] private bool _backgroundHostAtLogon;
|
||||
[ObservableProperty] private bool _backgroundMaintenanceWhenIdle = true;
|
||||
[ObservableProperty] private int _idleMaintenanceMinutes = 10;
|
||||
[ObservableProperty] private bool _idleMaintenanceAcOnly = true;
|
||||
[ObservableProperty] private string _sevenZipPath = "";
|
||||
[ObservableProperty] private string _gitPath = "";
|
||||
[ObservableProperty] private string _ffmpegPath = "";
|
||||
|
||||
public IReadOnlyList<int> IdleThresholdChoices { get; } = [5, 10, 30];
|
||||
|
||||
public bool IsDarkTheme
|
||||
{
|
||||
get => Theme != "Light";
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Theme = "Dark";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLightTheme
|
||||
{
|
||||
get => Theme == "Light";
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Theme = "Light";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static SettingsDraft From(UiPreferences preferences)
|
||||
=> new()
|
||||
{
|
||||
Theme = UiPreferencesStore.NormalizeTheme(preferences.Theme),
|
||||
GroupNetworkPlaces = preferences.GroupNetworkPlaces,
|
||||
GroupCloudPlaces = preferences.GroupCloudPlaces,
|
||||
PreferFavoritesInTree = preferences.PreferFavoritesInTree,
|
||||
ShowHiddenFiles = preferences.ShowHiddenFiles,
|
||||
ShowProtectedSystemLocations = preferences.ShowProtectedSystemLocations,
|
||||
AutoClearQueueWhenDone = preferences.AutoClearQueueWhenDone,
|
||||
IndexArchiveContents = preferences.IndexArchiveContents,
|
||||
AutoIndexRemovable = preferences.AutoIndexRemovable,
|
||||
BackgroundHostAtLogon = preferences.BackgroundHostAtLogon,
|
||||
BackgroundMaintenanceWhenIdle = preferences.BackgroundMaintenanceWhenIdle,
|
||||
IdleMaintenanceMinutes = UiPreferencesStore.NormalizeIdleMinutes(preferences.IdleMaintenanceMinutes),
|
||||
IdleMaintenanceAcOnly = preferences.IdleMaintenanceAcOnly,
|
||||
SevenZipPath = preferences.SevenZipPath ?? "",
|
||||
GitPath = preferences.GitPath ?? "",
|
||||
FfmpegPath = preferences.FfmpegPath ?? ""
|
||||
};
|
||||
|
||||
public UiPreferences ApplyTo(UiPreferences stored)
|
||||
=> stored with
|
||||
{
|
||||
Theme = UiPreferencesStore.NormalizeTheme(Theme),
|
||||
GroupNetworkPlaces = GroupNetworkPlaces,
|
||||
GroupCloudPlaces = GroupCloudPlaces,
|
||||
PreferFavoritesInTree = PreferFavoritesInTree,
|
||||
ShowHiddenFiles = ShowHiddenFiles,
|
||||
ShowProtectedSystemLocations = ShowProtectedSystemLocations,
|
||||
AutoClearQueueWhenDone = AutoClearQueueWhenDone,
|
||||
IndexArchiveContents = IndexArchiveContents,
|
||||
AutoIndexRemovable = AutoIndexRemovable,
|
||||
BackgroundHostAtLogon = BackgroundHostAtLogon,
|
||||
BackgroundMaintenanceWhenIdle = BackgroundMaintenanceWhenIdle,
|
||||
IdleMaintenanceMinutes = UiPreferencesStore.NormalizeIdleMinutes(IdleMaintenanceMinutes),
|
||||
IdleMaintenanceAcOnly = IdleMaintenanceAcOnly,
|
||||
SevenZipPath = EmptyToNull(SevenZipPath),
|
||||
GitPath = EmptyToNull(GitPath),
|
||||
FfmpegPath = EmptyToNull(FfmpegPath)
|
||||
};
|
||||
|
||||
partial void OnThemeChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsDarkTheme));
|
||||
OnPropertyChanged(nameof(IsLightTheme));
|
||||
}
|
||||
|
||||
private static string? EmptyToNull(string value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
25
src/Explorer.App/Settings/SettingsPathBrowse.cs
Normal file
25
src/Explorer.App/Settings/SettingsPathBrowse.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
internal static class SettingsPathBrowse
|
||||
{
|
||||
public static bool TryPick(Window? owner, string title, string filter, string? current, out string path)
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = title,
|
||||
Filter = filter,
|
||||
FileName = current ?? ""
|
||||
};
|
||||
if (dlg.ShowDialog(owner) == true)
|
||||
{
|
||||
path = dlg.FileName;
|
||||
return true;
|
||||
}
|
||||
|
||||
path = "";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
6
src/Explorer.App/Settings/SettingsSession.cs
Normal file
6
src/Explorer.App/Settings/SettingsSession.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public static class SettingsSession
|
||||
{
|
||||
public static SettingsCategoryId? LastCategory { get; set; }
|
||||
}
|
||||
27
src/Explorer.App/Settings/SettingsShell.cs
Normal file
27
src/Explorer.App/Settings/SettingsShell.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Explorer.App.Settings;
|
||||
|
||||
public sealed partial class SettingsShell : ObservableObject
|
||||
{
|
||||
public SettingsShell(SettingsDraft draft, IReadOnlyList<SettingsCategory> categories)
|
||||
{
|
||||
Draft = draft;
|
||||
Categories = categories;
|
||||
_selectedCategory = Restore(categories);
|
||||
}
|
||||
|
||||
public SettingsDraft Draft { get; }
|
||||
public IReadOnlyList<SettingsCategory> Categories { get; }
|
||||
|
||||
[ObservableProperty] private SettingsCategory _selectedCategory;
|
||||
|
||||
partial void OnSelectedCategoryChanged(SettingsCategory value)
|
||||
=> SettingsSession.LastCategory = value.Id;
|
||||
|
||||
private static SettingsCategory Restore(IReadOnlyList<SettingsCategory> categories)
|
||||
{
|
||||
var last = SettingsSession.LastCategory;
|
||||
return categories.FirstOrDefault(c => c.Id == last) ?? categories[0];
|
||||
}
|
||||
}
|
||||
25
src/Explorer.App/Settings/SettingsStyles.xaml
Normal file
25
src/Explorer.App/Settings/SettingsStyles.xaml
Normal file
@@ -0,0 +1,25 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Style x:Key="SettingsHeading" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||
</Style>
|
||||
<Style x:Key="SettingsIntro" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
<Style x:Key="SettingsHelp" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="24,0,0,14"/>
|
||||
</Style>
|
||||
<Style x:Key="SettingsPathHelp" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -3,99 +3,71 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Settings"
|
||||
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
|
||||
Height="720" Width="560"
|
||||
MinHeight="560" MinWidth="480"
|
||||
Height="640" Width="820"
|
||||
MinHeight="480" MinWidth="700"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}"
|
||||
ResizeMode="NoResize">
|
||||
<DockPanel Margin="20">
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
ResizeMode="CanResize">
|
||||
<DockPanel Margin="16">
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
|
||||
<Button Content="OK" MinWidth="88" Height="32" IsDefault="True" Click="OnOk" Margin="0,0,8,0"/>
|
||||
<Button Content="Cancel" MinWidth="88" Height="32" IsCancel="True" Click="OnCancel"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Appearance" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,10"/>
|
||||
<TextBlock Text="Theme" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,20">
|
||||
<RadioButton x:Name="ThemeDark" Content="Dark" GroupName="Theme" Margin="0,0,16,0"
|
||||
Checked="OnThemeChanged"/>
|
||||
<RadioButton x:Name="ThemeLight" Content="Light" GroupName="Theme"
|
||||
Checked="OnThemeChanged"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Locations tree" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,12"
|
||||
Text="Choose whether network and cloud locations appear as their own top-level items, or are collected under a group next to This PC."/>
|
||||
|
||||
<CheckBox x:Name="GroupNetwork" Margin="0,0,0,6"
|
||||
Content="Group network drives under Network"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,14" FontSize="12"
|
||||
Text="Mapped letters and UNC shares become children of a Network item. This PC then shows only local and removable drives."/>
|
||||
|
||||
<CheckBox x:Name="GroupCloud" Margin="0,0,0,6"
|
||||
Content="Group cloud locations under Cloud"/>
|
||||
<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, delete, and rename steps are removed automatically. Failed items stay until you dismiss or retry them. The queue is restored after restart."/>
|
||||
|
||||
<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"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
|
||||
Text="When enabled, a scan lists files inside ZIP, RAR, 7z, TAR, and similar archives from the archive catalog — files are not extracted. Individual uncompressed sizes are stored. Folder totals still use the archive’s size on disk. Online-only cloud archives are skipped."/>
|
||||
<CheckBox x:Name="AutoIndexRemovable" Margin="0,0,0,6"
|
||||
Content="Automatically index removable drives when they appear"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
|
||||
Text="USB and other removable volumes are queued for a full scan when they come online and are not indexed yet. Cloud locations are never auto-indexed. Online-only files are not hydrated."/>
|
||||
|
||||
<TextBlock Text="Background host" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
|
||||
<CheckBox x:Name="BackgroundHostAtLogon" Margin="0,0,0,6"
|
||||
Content="Start Explorer.Host.exe at Windows sign-in"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
|
||||
Text="Adds Explorer.Host.exe to your Windows sign-in programs for this user. No administrator rights. The window connects to Explorer.Host.exe for indexing and the queue. If the host is not running, the window starts it. Only the host opens the index for write. A tray icon stays while the host is running: open the window, or quit the host. File → Stop background host does the same from the window."/>
|
||||
|
||||
<TextBlock Text="7-Zip" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8" FontSize="12"
|
||||
Text="Extract, compress, add, and verify use 7-Zip when it is installed. Leave the path empty to look in Program Files and PATH. 7-Zip is not bundled with Explorer Workbench."/>
|
||||
<DockPanel Margin="0,0,0,18">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28" Click="OnBrowseSevenZip" Margin="8,0,0,0"/>
|
||||
<TextBox x:Name="SevenZipPath"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="Git" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8" FontSize="12"
|
||||
Text="Repository badges and Git actions use git.exe when it is installed. Leave the path empty to look in Program Files and PATH. Git is not bundled. There is no branch UI or credential dialog."/>
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28" Click="OnBrowseGit" Margin="8,0,0,0"/>
|
||||
<TextBox x:Name="GitPath"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="FFmpeg" FontSize="16" FontWeight="SemiBold" Margin="0,16,0,10"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8" FontSize="12"
|
||||
Text="Convert uses ffmpeg.exe from a Windows zip/build (ffprobe and ffplay are not required). Leave the path empty to look in Program Files\ffmpeg\bin and PATH. FFmpeg is not bundled with Explorer Workbench."/>
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<Button DockPanel.Dock="Right" Content="Browse…" MinWidth="88" Height="28" Click="OnBrowseFfmpeg" Margin="8,0,0,0"/>
|
||||
<TextBox x:Name="FfmpegPath"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200" MinWidth="160"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*" MinWidth="360"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Categories" FontWeight="SemiBold" Margin="0,0,0,8"/>
|
||||
<ListBox x:Name="CategoryList"
|
||||
ItemsSource="{Binding Categories}"
|
||||
SelectedItem="{Binding SelectedCategory}"
|
||||
DisplayMemberPath="Title"
|
||||
AutomationProperties.Name="Settings categories"
|
||||
KeyboardNavigation.TabIndex="0">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}" CornerRadius="3" Margin="0,1">
|
||||
<ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
<GridSplitter Grid.Column="1" Width="8" HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource Stroke}"
|
||||
ResizeBehavior="PreviousAndNext"/>
|
||||
<Border Grid.Column="2" Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}"
|
||||
BorderThickness="1" Padding="16">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
KeyboardNavigation.TabIndex="1">
|
||||
<ContentControl Content="{Binding SelectedCategory.Page}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using Explorer.Application;
|
||||
using Explorer.App.Settings;
|
||||
using Explorer.Hosting;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
|
||||
@@ -8,6 +9,7 @@ namespace Explorer.App;
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _vm;
|
||||
private readonly SettingsDraft _draft;
|
||||
private readonly string _originalTheme;
|
||||
|
||||
public SettingsWindow(MainViewModel vm)
|
||||
@@ -16,48 +18,25 @@ public partial class SettingsWindow : Window
|
||||
_vm = vm;
|
||||
var prefs = vm.CurrentPreferences();
|
||||
_originalTheme = prefs.Theme;
|
||||
ThemeDark.IsChecked = prefs.Theme != "Light";
|
||||
ThemeLight.IsChecked = prefs.Theme == "Light";
|
||||
GroupNetwork.IsChecked = prefs.GroupNetworkPlaces;
|
||||
GroupCloud.IsChecked = prefs.GroupCloudPlaces;
|
||||
IndexArchives.IsChecked = prefs.IndexArchiveContents;
|
||||
AutoIndexRemovable.IsChecked = prefs.AutoIndexRemovable;
|
||||
BackgroundHostAtLogon.IsChecked = prefs.BackgroundHostAtLogon;
|
||||
ShowHidden.IsChecked = prefs.ShowHiddenFiles;
|
||||
ShowProtected.IsChecked = prefs.ShowProtectedSystemLocations;
|
||||
AutoClearQueue.IsChecked = prefs.AutoClearQueueWhenDone;
|
||||
SevenZipPath.Text = prefs.SevenZipPath ?? "";
|
||||
GitPath.Text = prefs.GitPath ?? "";
|
||||
FfmpegPath.Text = prefs.FfmpegPath ?? "";
|
||||
_draft = SettingsDraft.From(prefs);
|
||||
_draft.PropertyChanged += OnDraftChanged;
|
||||
DataContext = new SettingsShell(_draft, SettingsCatalog.Create());
|
||||
Closed += (_, _) => _draft.PropertyChanged -= OnDraftChanged;
|
||||
}
|
||||
|
||||
private void OnThemeChanged(object sender, RoutedEventArgs e)
|
||||
private void OnDraftChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
if (e.PropertyName is nameof(SettingsDraft.Theme)
|
||||
or nameof(SettingsDraft.IsDarkTheme)
|
||||
or nameof(SettingsDraft.IsLightTheme))
|
||||
{
|
||||
return;
|
||||
_vm.Theme = _draft.Theme;
|
||||
}
|
||||
|
||||
_vm.Theme = ThemeLight.IsChecked == true ? "Light" : "Dark";
|
||||
}
|
||||
|
||||
private async void OnOk(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var prefs = _vm.CurrentPreferences() with
|
||||
{
|
||||
Theme = ThemeLight.IsChecked == true ? "Light" : "Dark",
|
||||
GroupNetworkPlaces = GroupNetwork.IsChecked == true,
|
||||
GroupCloudPlaces = GroupCloud.IsChecked == true,
|
||||
IndexArchiveContents = IndexArchives.IsChecked == true,
|
||||
AutoIndexRemovable = AutoIndexRemovable.IsChecked == true,
|
||||
BackgroundHostAtLogon = BackgroundHostAtLogon.IsChecked == true,
|
||||
ShowHiddenFiles = ShowHidden.IsChecked == true,
|
||||
ShowProtectedSystemLocations = ShowProtected.IsChecked == true,
|
||||
AutoClearQueueWhenDone = AutoClearQueue.IsChecked == true,
|
||||
SevenZipPath = string.IsNullOrWhiteSpace(SevenZipPath.Text) ? null : SevenZipPath.Text.Trim(),
|
||||
GitPath = string.IsNullOrWhiteSpace(GitPath.Text) ? null : GitPath.Text.Trim(),
|
||||
FfmpegPath = string.IsNullOrWhiteSpace(FfmpegPath.Text) ? null : FfmpegPath.Text.Trim()
|
||||
};
|
||||
var prefs = _draft.ApplyTo(_vm.CurrentPreferences());
|
||||
await _vm.ApplyPreferencesAsync(prefs).ConfigureAwait(true);
|
||||
ApplyBackgroundHostAutostart(prefs.BackgroundHostAtLogon);
|
||||
DialogResult = true;
|
||||
@@ -94,48 +73,6 @@ public partial class SettingsWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseSevenZip(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "7-Zip executable",
|
||||
Filter = "7-Zip|7z.exe;7za.exe|Executables|*.exe|All files|*.*",
|
||||
FileName = SevenZipPath.Text
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
SevenZipPath.Text = dlg.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseGit(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "Git executable",
|
||||
Filter = "Git|git.exe|Executables|*.exe|All files|*.*",
|
||||
FileName = GitPath.Text
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
GitPath.Text = dlg.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseFfmpeg(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "FFmpeg executable",
|
||||
Filter = "FFmpeg|ffmpeg.exe|Executables|*.exe|All files|*.*",
|
||||
FileName = FfmpegPath.Text
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
FfmpegPath.Text = dlg.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_vm.Theme = _originalTheme;
|
||||
|
||||
@@ -42,6 +42,7 @@ public sealed class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
|
||||
|
||||
public int FirstVisibleIndex => _firstVisible;
|
||||
public int LastVisibleIndex => _lastVisible;
|
||||
public int Columns => Math.Max(1, _columns);
|
||||
|
||||
public ScrollViewer? ScrollOwner { get; set; }
|
||||
public bool CanVerticallyScroll { get; set; } = true;
|
||||
|
||||
Reference in New Issue
Block a user