Improve file operations, layout memory, and drive status.
Add a sequential file-operations queue with pause, reorder, and optional auto-clear; persist window size and tree width; show free space; and clear leftover indexing status. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Presentation;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
@@ -19,27 +22,48 @@ public partial class MainWindow : Window
|
||||
private bool _suppressItemContextMenu;
|
||||
private bool _incomingRightDrag;
|
||||
private bool _sourceRightDrag;
|
||||
private MainViewModel? _wiredVm;
|
||||
private long _inlineRenameStarted;
|
||||
private DispatcherTimer? _clickRenameTimer;
|
||||
private FolderItemViewModel? _clickRenameItem;
|
||||
private FolderItemViewModel? _listDropTarget;
|
||||
private NavNodeViewModel? _treeDropTarget;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += (_, _) =>
|
||||
{
|
||||
if (DataContext is MainViewModel vm)
|
||||
if (_wiredVm is not null)
|
||||
{
|
||||
vm.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.Theme))
|
||||
{
|
||||
ApplyTheme(vm.Theme);
|
||||
}
|
||||
};
|
||||
_wiredVm.InlineRenameRequested -= OnInlineRenameRequested;
|
||||
_wiredVm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
_wiredVm = DataContext as MainViewModel;
|
||||
if (_wiredVm is not null)
|
||||
{
|
||||
_wiredVm.InlineRenameRequested += OnInlineRenameRequested;
|
||||
_wiredVm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
RestoreLayout(_wiredVm);
|
||||
}
|
||||
};
|
||||
Closing += (_, _) => PersistLayout();
|
||||
}
|
||||
|
||||
private MainViewModel Vm => (MainViewModel)DataContext;
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.Theme) && sender is MainViewModel vm)
|
||||
{
|
||||
ApplyTheme(vm.Theme);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInlineRenameRequested(object? sender, string path)
|
||||
=> Dispatcher.BeginInvoke(() => BeginInlineRenameForPath(path), DispatcherPriority.Loaded);
|
||||
|
||||
private void ApplyTheme(string theme)
|
||||
{
|
||||
var dicts = System.Windows.Application.Current.Resources.MergedDictionaries;
|
||||
@@ -75,9 +99,77 @@ public partial class MainWindow : Window
|
||||
private void ToggleMaximized()
|
||||
{
|
||||
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
||||
MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
|
||||
SyncMaxRestoreButton();
|
||||
}
|
||||
|
||||
private void RestoreLayout(MainViewModel vm)
|
||||
{
|
||||
var prefs = vm.CurrentPreferences();
|
||||
if (prefs.WindowWidth is > 0 && prefs.WindowHeight is > 0)
|
||||
{
|
||||
Width = Clamp(prefs.WindowWidth.Value, MinWidth, SystemParameters.VirtualScreenWidth);
|
||||
Height = Clamp(prefs.WindowHeight.Value, MinHeight, SystemParameters.VirtualScreenHeight);
|
||||
}
|
||||
|
||||
if (prefs.WindowLeft is not null && prefs.WindowTop is not null)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
Left = prefs.WindowLeft.Value;
|
||||
Top = prefs.WindowTop.Value;
|
||||
if (!IsOnVirtualScreen())
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefs.WindowMaximized)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
SyncMaxRestoreButton();
|
||||
if (prefs.TreeWidth is >= 160)
|
||||
{
|
||||
var maxTree = Math.Max(160, ActualWidth > 0 ? ActualWidth - 240 : Width - 240);
|
||||
TreeColumn.Width = new GridLength(Clamp(prefs.TreeWidth.Value, 160, maxTree));
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistLayout()
|
||||
{
|
||||
if (DataContext is not MainViewModel vm)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bounds = WindowState == WindowState.Normal ? new Rect(Left, Top, Width, Height) : RestoreBounds;
|
||||
if (bounds.Width <= 0 || bounds.Height <= 0)
|
||||
{
|
||||
bounds = new Rect(Left, Top, Width, Height);
|
||||
}
|
||||
|
||||
var treeWidth = TreeColumn.ActualWidth > 0 ? TreeColumn.ActualWidth : TreeColumn.Width.Value;
|
||||
vm.SaveLayout(bounds.Width, bounds.Height, bounds.Left, bounds.Top, WindowState == WindowState.Maximized, treeWidth);
|
||||
}
|
||||
|
||||
private void SyncMaxRestoreButton()
|
||||
=> MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
|
||||
|
||||
private bool IsOnVirtualScreen()
|
||||
{
|
||||
var virtualArea = new Rect(
|
||||
SystemParameters.VirtualScreenLeft,
|
||||
SystemParameters.VirtualScreenTop,
|
||||
SystemParameters.VirtualScreenWidth,
|
||||
SystemParameters.VirtualScreenHeight);
|
||||
var window = new Rect(Left, Top, Math.Max(Width, MinWidth), Math.Max(Height, MinHeight));
|
||||
window.Intersect(virtualArea);
|
||||
return window.Width >= 80 && window.Height >= 80;
|
||||
}
|
||||
|
||||
private static double Clamp(double value, double min, double max)
|
||||
=> Math.Min(Math.Max(value, min), Math.Max(min, max));
|
||||
|
||||
private void OnPaneChromeMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement { DataContext: ExplorerPaneViewModel pane })
|
||||
@@ -342,7 +434,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void OnItemDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is ListView { SelectedItem: FolderItemViewModel item })
|
||||
CancelClickRename();
|
||||
if (sender is ListView { SelectedItem: FolderItemViewModel item } && !item.IsRenaming)
|
||||
{
|
||||
ActivatePaneFromList((ListView)sender);
|
||||
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
|
||||
@@ -350,6 +443,82 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnColumnHeaderClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is Thumb || sender is not ListView list)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var header = e.OriginalSource as GridViewColumnHeader
|
||||
?? FindGridViewColumnHeader(e.OriginalSource as DependencyObject);
|
||||
if (header is null || header.Role == GridViewColumnHeaderRole.Padding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = SortKeyFromHeader(header.Column?.Header);
|
||||
if (key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivatePaneFromList(list);
|
||||
Vm.ActivePane.SortBy(key);
|
||||
UpdateSortGlyphs(list, Vm.ActivePane);
|
||||
}
|
||||
|
||||
private static GridViewColumnHeader? FindGridViewColumnHeader(DependencyObject? origin)
|
||||
{
|
||||
while (origin is not null)
|
||||
{
|
||||
if (origin is GridViewColumnHeader header)
|
||||
{
|
||||
return header;
|
||||
}
|
||||
|
||||
origin = origin is Visual
|
||||
? VisualTreeHelper.GetParent(origin)
|
||||
: LogicalTreeHelper.GetParent(origin);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void UpdateSortGlyphs(ListView list, ExplorerPaneViewModel pane)
|
||||
{
|
||||
if (list.View is not GridView grid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var column in grid.Columns)
|
||||
{
|
||||
var title = StripSortGlyph(column.Header?.ToString());
|
||||
var key = SortKeyFromHeader(title);
|
||||
column.Header = key is not null && string.Equals(pane.SortProperty, key, StringComparison.OrdinalIgnoreCase)
|
||||
? title + (pane.SortDescending ? " ▼" : " ▲")
|
||||
: title;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SortKeyFromHeader(object? header)
|
||||
=> StripSortGlyph(header?.ToString()) switch
|
||||
{
|
||||
"Name" => "Name",
|
||||
"Date modified" => "Modified",
|
||||
"Type" => "Type",
|
||||
"Size" => "Size",
|
||||
"Free space" => "Free",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string StripSortGlyph(string? header)
|
||||
{
|
||||
var text = header ?? "";
|
||||
return text.Replace(" ▲", "", StringComparison.Ordinal).Replace(" ▼", "", StringComparison.Ordinal).Trim();
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ListView list)
|
||||
@@ -358,6 +527,11 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
ActivatePaneFromList(list);
|
||||
if (_clickRenameItem is not null && !list.SelectedItems.Contains(_clickRenameItem))
|
||||
{
|
||||
CancelClickRename();
|
||||
}
|
||||
|
||||
Vm.ActivePane.SelectedItems.Clear();
|
||||
foreach (FolderItemViewModel item in list.SelectedItems)
|
||||
{
|
||||
@@ -395,6 +569,7 @@ public partial class MainWindow : Window
|
||||
_dragPending = true;
|
||||
_dragButton = e.ChangedButton;
|
||||
_dragItem = HitTestFolderItem(sender as DependencyObject, e.GetPosition((IInputElement)sender));
|
||||
TryScheduleClickRename(sender as ListView, e);
|
||||
}
|
||||
|
||||
private void OnListContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
@@ -403,6 +578,12 @@ public partial class MainWindow : Window
|
||||
{
|
||||
e.Handled = true;
|
||||
_suppressItemContextMenu = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is FrameworkElement { ContextMenu: { } menu })
|
||||
{
|
||||
menu.DataContext = DataContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +591,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
|
||||
{
|
||||
SetListDropTarget(null);
|
||||
e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
return;
|
||||
@@ -418,6 +600,15 @@ public partial class MainWindow : Window
|
||||
ActivatePaneFromList(list);
|
||||
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
|
||||
var dest = ResolveDropDirectory(list, e);
|
||||
var hover = HitTestFolderItem(list, e.GetPosition(list));
|
||||
var folderTarget = hover is { IsDirectory: true }
|
||||
&& dest is not null
|
||||
&& NavigationTreeViewModel.PathsEqual(hover.FullPath, dest)
|
||||
&& !DragDropPolicy.IsInvalidTarget(files, dest)
|
||||
? hover
|
||||
: null;
|
||||
SetListDropTarget(folderTarget);
|
||||
SetTreeDropTarget(null);
|
||||
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
@@ -439,6 +630,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void OnListDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
ClearDropTargets();
|
||||
if (!TryGetDropFiles(e, out var files) || sender is not ListView list)
|
||||
{
|
||||
return;
|
||||
@@ -463,6 +655,35 @@ public partial class MainWindow : Window
|
||||
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnListDragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
if (sender is ListView list)
|
||||
{
|
||||
var pos = e.GetPosition(list);
|
||||
if (pos.X >= 0 && pos.Y >= 0 && pos.X <= list.ActualWidth && pos.Y <= list.ActualHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetListDropTarget(null);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!IsInsideInlineRenameBox(e.OriginalSource as DependencyObject))
|
||||
{
|
||||
CommitOpenInlineRename();
|
||||
}
|
||||
|
||||
if (!IsClickOnItemName(e.OriginalSource as DependencyObject))
|
||||
{
|
||||
CancelClickRename();
|
||||
}
|
||||
|
||||
base.OnPreviewMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnPreviewMouseMove(e);
|
||||
@@ -481,6 +702,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
_dragPending = false;
|
||||
CancelClickRename();
|
||||
var selected = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToList();
|
||||
IReadOnlyList<string> paths;
|
||||
if (_dragItem is not null && (selected.Count == 0
|
||||
@@ -504,6 +726,7 @@ public partial class MainWindow : Window
|
||||
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
|
||||
_sourceRightDrag = false;
|
||||
_incomingRightDrag = false;
|
||||
ClearDropTargets();
|
||||
}
|
||||
|
||||
protected override void OnQueryContinueDrag(QueryContinueDragEventArgs e)
|
||||
@@ -638,20 +861,27 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (!TryGetDropFiles(e, out var files))
|
||||
{
|
||||
SetTreeDropTarget(null);
|
||||
e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_incomingRightDrag = (e.KeyStates & DragDropKeyStates.RightMouseButton) != 0;
|
||||
var dest = HitTestTreePath(e);
|
||||
var node = HitTestTreeNode(e);
|
||||
var dest = node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path)
|
||||
? null
|
||||
: node.Path;
|
||||
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
|
||||
{
|
||||
SetTreeDropTarget(null);
|
||||
e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
SetListDropTarget(null);
|
||||
SetTreeDropTarget(node);
|
||||
e.Effects = _incomingRightDrag || _sourceRightDrag
|
||||
? DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link
|
||||
: ToEffects(ResolveDropAction(e, files, dest));
|
||||
@@ -660,6 +890,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void OnTreeDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
ClearDropTargets();
|
||||
if (!TryGetDropFiles(e, out var files))
|
||||
{
|
||||
return;
|
||||
@@ -683,11 +914,20 @@ public partial class MainWindow : Window
|
||||
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnTreeDragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(NavTree);
|
||||
if (pos.X >= 0 && pos.Y >= 0 && pos.X <= NavTree.ActualWidth && pos.Y <= NavTree.ActualHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetTreeDropTarget(null);
|
||||
}
|
||||
|
||||
private string? HitTestTreePath(DragEventArgs e)
|
||||
{
|
||||
var origin = NavTree.InputHitTest(e.GetPosition(NavTree)) as DependencyObject
|
||||
?? e.OriginalSource as DependencyObject;
|
||||
var node = FindTreeNode(origin);
|
||||
var node = HitTestTreeNode(e);
|
||||
if (node is null || node.IsPlaceholder || node.IsGroup || LocationRoots.IsVirtual(node.Path))
|
||||
{
|
||||
return null;
|
||||
@@ -696,6 +936,57 @@ public partial class MainWindow : Window
|
||||
return node.Path;
|
||||
}
|
||||
|
||||
private NavNodeViewModel? HitTestTreeNode(DragEventArgs e)
|
||||
{
|
||||
var origin = NavTree.InputHitTest(e.GetPosition(NavTree)) as DependencyObject
|
||||
?? e.OriginalSource as DependencyObject;
|
||||
return FindTreeNode(origin);
|
||||
}
|
||||
|
||||
private void SetListDropTarget(FolderItemViewModel? item)
|
||||
{
|
||||
if (ReferenceEquals(_listDropTarget, item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_listDropTarget is not null)
|
||||
{
|
||||
_listDropTarget.IsDropTarget = false;
|
||||
}
|
||||
|
||||
_listDropTarget = item;
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsDropTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTreeDropTarget(NavNodeViewModel? node)
|
||||
{
|
||||
if (ReferenceEquals(_treeDropTarget, node))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_treeDropTarget is not null)
|
||||
{
|
||||
_treeDropTarget.IsDropTarget = false;
|
||||
}
|
||||
|
||||
_treeDropTarget = node;
|
||||
if (node is not null)
|
||||
{
|
||||
node.IsDropTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearDropTargets()
|
||||
{
|
||||
SetListDropTarget(null);
|
||||
SetTreeDropTarget(null);
|
||||
}
|
||||
|
||||
private async void OnSearchDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is ListView { SelectedItem: FolderItemViewModel item })
|
||||
@@ -760,6 +1051,10 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnCtxOpen(object sender, RoutedEventArgs e) => Vm.OpenSelectedCommand.Execute(null);
|
||||
|
||||
private void OnCtxNewFolder(object sender, RoutedEventArgs e) => Vm.NewFolderCommand.Execute(null);
|
||||
|
||||
private void OnCtxCopyPath(object sender, RoutedEventArgs e) => Vm.CopyPathCommand.Execute(null);
|
||||
|
||||
private async void OnCtxDelete(object sender, RoutedEventArgs e)
|
||||
=> await DeleteSelectedAsync().ConfigureAwait(true);
|
||||
|
||||
@@ -790,16 +1085,211 @@ public partial class MainWindow : Window
|
||||
private void OnCtxRename(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
|
||||
if (item is not null)
|
||||
{
|
||||
BeginInlineRename(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginInlineRenameForPath(string path)
|
||||
{
|
||||
var item = Vm.ActivePane.Items.FirstOrDefault(i => NavigationTreeViewModel.PathsEqual(i.FullPath, path));
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var name = PromptWindow.Ask(this, "Rename", "New name:", item.Name);
|
||||
if (!string.IsNullOrWhiteSpace(name) && name != item.Name)
|
||||
BeginInlineRename(item);
|
||||
}
|
||||
|
||||
private void BeginInlineRename(FolderItemViewModel item)
|
||||
{
|
||||
foreach (var other in Vm.ActivePane.Items.Where(i => i.IsRenaming && i != item))
|
||||
{
|
||||
Vm.RenameSelected(name);
|
||||
other.CancelRename();
|
||||
}
|
||||
|
||||
var list = FindActiveFileList();
|
||||
if (list is not null)
|
||||
{
|
||||
list.SelectedItem = item;
|
||||
list.ScrollIntoView(item);
|
||||
list.UpdateLayout();
|
||||
}
|
||||
|
||||
CancelClickRename();
|
||||
_inlineRenameStarted = Environment.TickCount64;
|
||||
item.BeginRename();
|
||||
}
|
||||
|
||||
private void TryScheduleClickRename(ListView? list, MouseButtonEventArgs e)
|
||||
{
|
||||
if (list is null
|
||||
|| e.ChangedButton != MouseButton.Left
|
||||
|| _dragItem is null
|
||||
|| _dragItem.IsRenaming
|
||||
|| Keyboard.Modifiers is not ModifierKeys.None
|
||||
|| !IsClickOnItemName(e.OriginalSource as DependencyObject)
|
||||
|| list.SelectedItems.Count != 1
|
||||
|| !list.SelectedItems.Contains(_dragItem))
|
||||
{
|
||||
CancelClickRename();
|
||||
return;
|
||||
}
|
||||
|
||||
CancelClickRename();
|
||||
_clickRenameItem = _dragItem;
|
||||
_clickRenameTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(Math.Max(GetDoubleClickTime(), 1))
|
||||
};
|
||||
_clickRenameTimer.Tick += OnClickRenameTick;
|
||||
_clickRenameTimer.Start();
|
||||
}
|
||||
|
||||
private void OnClickRenameTick(object? sender, EventArgs e)
|
||||
{
|
||||
var item = _clickRenameItem;
|
||||
CancelClickRename();
|
||||
if (item is not null && Vm.ActivePane.Items.Contains(item) && !item.IsRenaming)
|
||||
{
|
||||
BeginInlineRename(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelClickRename()
|
||||
{
|
||||
if (_clickRenameTimer is not null)
|
||||
{
|
||||
_clickRenameTimer.Tick -= OnClickRenameTick;
|
||||
_clickRenameTimer.Stop();
|
||||
_clickRenameTimer = null;
|
||||
}
|
||||
|
||||
_clickRenameItem = null;
|
||||
}
|
||||
|
||||
private static bool IsClickOnItemName(DependencyObject? origin)
|
||||
{
|
||||
while (origin is not null)
|
||||
{
|
||||
if (origin is TextBox { Tag: "InlineRename" } || origin is FrameworkElement { Tag: "ItemName" })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
origin = origin is Visual
|
||||
? VisualTreeHelper.GetParent(origin)
|
||||
: LogicalTreeHelper.GetParent(origin);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetDoubleClickTime();
|
||||
|
||||
internal bool IsInlineRenameStarting
|
||||
=> Environment.TickCount64 - _inlineRenameStarted < 300;
|
||||
|
||||
private void CommitOpenInlineRename()
|
||||
{
|
||||
if (DataContext is not MainViewModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pane in new[] { Vm.ActiveTab.Left, Vm.ActiveTab.Right })
|
||||
{
|
||||
foreach (var item in pane.Items.Where(i => i.IsRenaming).ToList())
|
||||
{
|
||||
TryCommitInlineRename(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInsideInlineRenameBox(DependencyObject? origin)
|
||||
{
|
||||
while (origin is not null)
|
||||
{
|
||||
if (origin is TextBox { Tag: "InlineRename" })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
origin = origin is Visual
|
||||
? VisualTreeHelper.GetParent(origin)
|
||||
: LogicalTreeHelper.GetParent(origin);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void TryCommitInlineRename(FolderItemViewModel item)
|
||||
{
|
||||
if (!item.IsRenaming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newName = item.EditName.Trim();
|
||||
if (string.IsNullOrWhiteSpace(newName) || newName.Equals(item.Item.Name, StringComparison.Ordinal))
|
||||
{
|
||||
item.CancelRename();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||
{
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"A file name can't contain any of the following characters:\n\\ / : * ? \" < > |",
|
||||
"Rename",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
item.BeginRename();
|
||||
item.EditName = newName;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
item.IsRenaming = false;
|
||||
Vm.RenameItem(item, newName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Rename", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
item.BeginRename();
|
||||
item.EditName = newName;
|
||||
}
|
||||
}
|
||||
|
||||
private ListView? FindActiveFileList()
|
||||
{
|
||||
var items = Vm.ActivePane.Items;
|
||||
return FindVisibleList(this, items);
|
||||
}
|
||||
|
||||
private static ListView? FindVisibleList(DependencyObject root, object items)
|
||||
{
|
||||
var count = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
if (child is ListView { IsVisible: true } list && ReferenceEquals(list.ItemsSource, items))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
var nested = FindVisibleList(child, items);
|
||||
if (nested is not null)
|
||||
{
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async void OnOpenSettings(object sender, RoutedEventArgs e)
|
||||
@@ -845,8 +1335,21 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is TextBox { Tag: "InlineRename" })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
|
||||
var shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
||||
var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
|
||||
if (ctrl && shift && e.Key == Key.N)
|
||||
{
|
||||
Vm.NewFolderCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.F5)
|
||||
{
|
||||
await Vm.RefreshAsync().ConfigureAwait(true);
|
||||
|
||||
Reference in New Issue
Block a user