801 lines
33 KiB
C#
801 lines
33 KiB
C#
using System.Collections.Specialized;
|
|
using FluidExplorer.Helpers;
|
|
using FluidExplorer.Models;
|
|
using FluidExplorer.Navigation;
|
|
using FluidExplorer.Services;
|
|
using FluidExplorer.Services.Operations;
|
|
using FluidExplorer.Services.Shell;
|
|
using FluidExplorer.ViewModels;
|
|
using Microsoft.UI.Input;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
|
using Microsoft.UI.Xaml.Input;
|
|
using Microsoft.UI.Xaml.Media;
|
|
using Windows.ApplicationModel.DataTransfer;
|
|
using Windows.System;
|
|
using Windows.UI.Core;
|
|
|
|
namespace FluidExplorer.Views;
|
|
|
|
/// <summary>
|
|
/// 单个浏览窗格的视图:命令栏 + 地址栏 + 内容区 + 状态栏。
|
|
/// 所有交互都委托给 <see cref="ExplorerPaneViewModel"/>,视图本身不碰文件系统。
|
|
/// </summary>
|
|
public sealed partial class ExplorerPaneView : UserControl
|
|
{
|
|
private static IReadOnlyList<string> _draggedPaths = [];
|
|
|
|
public ExplorerPaneView()
|
|
{
|
|
InitializeComponent();
|
|
Loaded += OnLoaded;
|
|
Unloaded += OnUnloaded;
|
|
}
|
|
|
|
public static readonly DependencyProperty PaneProperty = DependencyProperty.Register(
|
|
nameof(Pane),
|
|
typeof(ExplorerPaneViewModel),
|
|
typeof(ExplorerPaneView),
|
|
new PropertyMetadata(null, OnPaneChanged));
|
|
|
|
public ExplorerPaneViewModel? Pane
|
|
{
|
|
get => (ExplorerPaneViewModel?)GetValue(PaneProperty);
|
|
set => SetValue(PaneProperty, value);
|
|
}
|
|
|
|
public MainViewModel Main => App.Services.Main;
|
|
|
|
private static void OnPaneChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var view = (ExplorerPaneView)d;
|
|
if (e.OldValue is ExplorerPaneViewModel old)
|
|
{
|
|
old.PropertyChanged -= view.OnPanePropertyChanged;
|
|
old.RenameRequested -= view.OnRenameRequested;
|
|
old.ScrollIntoViewRequested -= view.OnScrollIntoViewRequested;
|
|
old.FocusSearchRequested -= view.OnFocusSearchRequested;
|
|
old.OpenInNewTabRequested -= view.OnOpenInNewTabRequested;
|
|
}
|
|
if (e.NewValue is ExplorerPaneViewModel pane)
|
|
{
|
|
pane.PropertyChanged += view.OnPanePropertyChanged;
|
|
pane.RenameRequested += view.OnRenameRequested;
|
|
pane.ScrollIntoViewRequested += view.OnScrollIntoViewRequested;
|
|
pane.FocusSearchRequested += view.OnFocusSearchRequested;
|
|
pane.OpenInNewTabRequested += view.OnOpenInNewTabRequested;
|
|
view.ApplyIconSize();
|
|
}
|
|
}
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
ApplyIconSize();
|
|
UpdateRecycleBinUi();
|
|
ApplySidebarLayout();
|
|
if (Pane is not null) SyncSidebarSelection(Pane.CurrentPath);
|
|
}
|
|
|
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (Pane is not null) Pane.PropertyChanged -= OnPanePropertyChanged;
|
|
}
|
|
|
|
private void OnPanePropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
switch (e.PropertyName)
|
|
{
|
|
case nameof(ExplorerPaneViewModel.IconSize):
|
|
ApplyIconSize();
|
|
break;
|
|
case nameof(ExplorerPaneViewModel.IsRecycleBin):
|
|
UpdateRecycleBinUi();
|
|
break;
|
|
case nameof(ExplorerPaneViewModel.CurrentPath):
|
|
if (Pane is not null) SyncSidebarSelection(Pane.CurrentPath);
|
|
break;
|
|
case nameof(ExplorerPaneViewModel.ShowSidebar):
|
|
ApplySidebarLayout();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UpdateRecycleBinUi()
|
|
{
|
|
RestoreButton.Visibility = Pane is { IsRecycleBin: true } ? Visibility.Visible : Visibility.Collapsed;
|
|
HiddenToggle.IsChecked = Pane?.ShowHidden ?? false;
|
|
ExtensionToggle.IsChecked = Main.ShowFileExtensions;
|
|
}
|
|
|
|
/// <summary>图标/磁贴尺寸跟随视图方式(网格用 ItemsWrapGrid 的单元格尺寸,避免布局跳动)。</summary>
|
|
private void ApplyIconSize()
|
|
{
|
|
var pane = Pane;
|
|
if (pane is null) return;
|
|
|
|
foreach (var item in pane.Items) item.IconSize = Math.Min(pane.IconSize, 96);
|
|
|
|
if (IconsGrid.ItemsPanelRoot is ItemsWrapGrid wrap)
|
|
{
|
|
wrap.ItemWidth = pane.TileWidth;
|
|
wrap.ItemHeight = pane.TileHeight;
|
|
}
|
|
|
|
if (pane.IsIconsView && pane.Items.Count > 0)
|
|
{
|
|
// 视图切换后重新为可见项请求合适尺寸的缩略图
|
|
foreach (var item in pane.Items.Take(120)) pane.Visuals.RequestThumbnail(item, pane.IconSize);
|
|
}
|
|
}
|
|
|
|
// ── 导航窗格(侧边栏) ──────────────────────────────────────────────────
|
|
private bool _sidebarCollapsed;
|
|
|
|
private void OnToggleSidebarClick(object sender, RoutedEventArgs e)
|
|
{
|
|
_sidebarCollapsed = !_sidebarCollapsed;
|
|
ApplySidebarLayout();
|
|
}
|
|
|
|
private void ApplySidebarLayout()
|
|
{
|
|
var show = Pane?.ShowSidebar != false && !_sidebarCollapsed;
|
|
SidebarHost.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
|
SidebarColumn.Width = show ? new GridLength(242) : new GridLength(0);
|
|
}
|
|
|
|
private void OnSidebarItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
|
|
{
|
|
if (args.InvokedItem is SidebarNode node && Pane is not null)
|
|
_ = Pane.NavigateAsync(node.Location);
|
|
}
|
|
|
|
private void OnSidebarExpanding(TreeView sender, TreeViewExpandingEventArgs args)
|
|
{
|
|
// 子项已在构建侧边栏时填充(驱动器数量少且读取廉价)
|
|
}
|
|
|
|
/// <summary>当前路径变化时高亮侧边栏对应节点(对齐资源管理器)。</summary>
|
|
private void SyncSidebarSelection(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path)) return;
|
|
foreach (var node in EnumerateNodes(SidebarTree.RootNodes))
|
|
{
|
|
if (node.Content is SidebarNode sidebarNode && sidebarNode.Contains(path))
|
|
{
|
|
SidebarTree.SelectedNode = node;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<TreeViewNode> EnumerateNodes(IEnumerable<TreeViewNode> nodes)
|
|
{
|
|
foreach (var node in nodes)
|
|
{
|
|
yield return node;
|
|
foreach (var child in EnumerateNodes(node.Children)) yield return child;
|
|
}
|
|
}
|
|
|
|
// ── 地址栏导航按钮 ──────────────────────────────────────────────────────
|
|
private void OnBackClick(object sender, RoutedEventArgs e) => Pane?.BackCommand.Execute(null);
|
|
|
|
private void OnForwardClick(object sender, RoutedEventArgs e) => Pane?.ForwardCommand.Execute(null);
|
|
|
|
private void OnUpClick(object sender, RoutedEventArgs e) => Pane?.UpCommand.Execute(null);
|
|
|
|
private void OnRefreshClick(object sender, RoutedEventArgs e) => Pane?.RefreshCommand.Execute(null);
|
|
|
|
// ── 命令栏 ──────────────────────────────────────────────────────────────
|
|
private void OnNewFolderClick(object sender, RoutedEventArgs e) => Pane?.NewFolderCommand.Execute(null);
|
|
|
|
private void OnNewTextFileClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (Pane is null || Pane.Location.Kind is not (LocationKind.Folder or LocationKind.Drive)) return;
|
|
try
|
|
{
|
|
var path = Path.Combine(Pane.CurrentPath, "新建文本文档.txt");
|
|
var i = 2;
|
|
while (File.Exists(path)) path = Path.Combine(Pane.CurrentPath, $"新建文本文档 ({i++}).txt");
|
|
File.WriteAllText(path, string.Empty);
|
|
_ = Pane.RefreshCommand.ExecuteAsync(null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Pane.ReportError($"无法新建文件:{ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void OnNewTabClick(object sender, RoutedEventArgs e)
|
|
=> Main.AddTab(NavigationLocation.FromPath(Pane?.CurrentPath ?? string.Empty));
|
|
|
|
private void OnCutClick(object sender, RoutedEventArgs e) => Pane?.CutSelectedCommand.Execute(SelectedItems());
|
|
|
|
private void OnCopyClick(object sender, RoutedEventArgs e) => Pane?.CopySelectedCommand.Execute(SelectedItems());
|
|
|
|
private void OnPasteClick(object sender, RoutedEventArgs e) => Pane?.PasteCommand.Execute(null);
|
|
|
|
private void OnRenameClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var item = SelectedItems().FirstOrDefault();
|
|
if (item is not null) Pane?.StartRenameCommand.Execute(item);
|
|
}
|
|
|
|
private void OnDeleteClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var items = SelectedItems();
|
|
if (items.Count == 0) return;
|
|
var shift = IsKeyDown(VirtualKey.Shift);
|
|
if (shift) Pane?.PermanentDeleteSelectedCommand.Execute(items);
|
|
else Pane?.DeleteSelectedCommand.Execute(items);
|
|
}
|
|
|
|
private void OnUndoClick(object sender, RoutedEventArgs e) => Main.UndoCommand.Execute(null);
|
|
|
|
private void OnRestoreClick(object sender, RoutedEventArgs e) => Pane?.RestoreSelectedCommand.Execute(SelectedItems());
|
|
|
|
private void OnSortClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is FrameworkElement { Tag: string tag } && Enum.TryParse<SortColumn>(tag, out var column))
|
|
Pane?.SortByCommand.Execute(column);
|
|
}
|
|
|
|
private void OnSortDirectionClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (Pane is null || sender is not FrameworkElement { Tag: string tag }) return;
|
|
Pane.Sort = Pane.Sort with { Direction = tag == "Descending" ? SortDirection.Descending : SortDirection.Ascending };
|
|
Pane.ApplySortAndRefresh();
|
|
}
|
|
|
|
private void OnViewModeClick(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is FrameworkElement { Tag: string tag }) Pane?.SetViewModeCommand.Execute(tag);
|
|
}
|
|
|
|
private void OnCycleViewClick(object sender, RoutedEventArgs e) => Pane?.CycleViewModeCommand.Execute(null);
|
|
|
|
private void OnToggleDualPaneClick(object sender, RoutedEventArgs e) => Main.ToggleDualPaneCommand.Execute(null);
|
|
|
|
private void OnToggleHiddenClick(object sender, RoutedEventArgs e) => Pane?.ToggleHiddenCommand.Execute(null);
|
|
|
|
private void OnToggleExtensionClick(object sender, RoutedEventArgs e) => Main.ShowFileExtensions = !Main.ShowFileExtensions;
|
|
|
|
private async void OnShellContextMenuClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var paths = SelectedItems().Select(i => i.FullPath).ToList();
|
|
if (paths.Count == 0)
|
|
{
|
|
var dir = Pane?.CurrentPath;
|
|
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) paths.Add(dir);
|
|
}
|
|
if (paths.Count > 0) await ShowShellContextMenuAsync(paths);
|
|
}
|
|
|
|
private async Task ShowShellContextMenuAsync(IReadOnlyList<string> paths)
|
|
{
|
|
if (paths.Count == 0) return;
|
|
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindowInstance);
|
|
var anchor = DetailsList.TransformToVisual(null).TransformPoint(new Windows.Foundation.Point(48, 48));
|
|
var scale = XamlRoot?.RasterizationScale ?? 1.0;
|
|
var origin = App.MainWindowInstance.AppWindow.Position;
|
|
var screen = new Windows.Graphics.PointInt32(
|
|
origin.X + (int)(anchor.X * scale),
|
|
origin.Y + (int)(anchor.Y * scale));
|
|
|
|
await ShellContextMenu.ShowAsync(hwnd, paths, screen);
|
|
}
|
|
|
|
// ── 地址栏 ──────────────────────────────────────────────────────────────
|
|
private async void OnBreadcrumbClicked(BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs args)
|
|
{
|
|
if (args.Item is BreadcrumbSegment segment && Pane is not null)
|
|
await Pane.NavigateAsync(new NavigationLocation(
|
|
segment.IsDriveRoot ? LocationKind.Drive : LocationKind.Folder,
|
|
segment.Path,
|
|
segment.DisplayName,
|
|
"\uE8B7"));
|
|
}
|
|
|
|
private void OnAddressEditorKeyDown(object sender, KeyRoutedEventArgs e)
|
|
{
|
|
if (e.Key == VirtualKey.Enter)
|
|
{
|
|
e.Handled = true;
|
|
var text = AddressEditor.Text;
|
|
AddressEditor.Visibility = Visibility.Collapsed;
|
|
AddressBreadcrumb.Visibility = Visibility.Visible;
|
|
_ = Pane?.NavigatePathCommand.ExecuteAsync(text);
|
|
}
|
|
else if (e.Key == VirtualKey.Escape)
|
|
{
|
|
e.Handled = true;
|
|
AddressEditor.Visibility = Visibility.Collapsed;
|
|
AddressBreadcrumb.Visibility = Visibility.Visible;
|
|
}
|
|
}
|
|
|
|
private void OnAddressEditorLostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
AddressEditor.Visibility = Visibility.Collapsed;
|
|
AddressBreadcrumb.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
/// <summary>Ctrl+L / F4:切换到可编辑的地址输入(资源管理器行为)。</summary>
|
|
private void BeginAddressEdit()
|
|
{
|
|
AddressEditor.Text = Pane?.CurrentPath ?? string.Empty;
|
|
AddressEditor.Visibility = Visibility.Visible;
|
|
AddressBreadcrumb.Visibility = Visibility.Collapsed;
|
|
AddressEditor.Focus(FocusState.Programmatic);
|
|
AddressEditor.SelectAll();
|
|
}
|
|
|
|
// ── 搜索 ────────────────────────────────────────────────────────────────
|
|
private async void OnSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
|
{
|
|
// SuggestionChosen 是我们自己回填的,不重复搜索;其余变化(含程序化写入、粘贴)都要触发
|
|
if (args.Reason == AutoSuggestionBoxTextChangeReason.SuggestionChosen) return;
|
|
if (Pane is null) return;
|
|
if (!Main.Settings.SearchAsYouType && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
|
|
{
|
|
// 关闭"键入即搜"时只在回车/提交时搜索
|
|
return;
|
|
}
|
|
await Pane.SearchAsync(sender.Text);
|
|
|
|
// 顶部建议:直接把最快命中的前 8 条塞进下拉框(Everything 式即时反馈)
|
|
var suggestions = Pane.SearchResults.Take(8).Select(r => r.FullPath).ToList();
|
|
sender.ItemsSource = suggestions;
|
|
}
|
|
|
|
private async void OnSearchQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
|
{
|
|
if (Pane is null) return;
|
|
await Pane.SearchAsync(args.QueryText);
|
|
}
|
|
|
|
private async void OnSearchSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
|
|
{
|
|
if (Pane is null || args.SelectedItem is not string path) return;
|
|
sender.Text = path;
|
|
await Pane.SearchAsync(path);
|
|
}
|
|
|
|
private void OnDismissErrorClick(InfoBar sender, object args) => Pane?.DismissError();
|
|
|
|
// ── 列表交互 ────────────────────────────────────────────────────────────
|
|
private List<ExplorerItem> SelectedItems()
|
|
=> Pane is null
|
|
? []
|
|
: (Pane.IsDetailsView || Pane.IsIconsView
|
|
? (Pane.IsDetailsView ? DetailsList.SelectedItems : IconsGrid.SelectedItems).OfType<ExplorerItem>().ToList()
|
|
: []);
|
|
|
|
private void OnListSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (Pane is null) return;
|
|
var listView = sender as ListViewBase;
|
|
var selected = listView?.SelectedItems.OfType<ExplorerItem>().ToList() ?? [];
|
|
Pane.OnSelectionChanged(selected);
|
|
}
|
|
|
|
private void OnDetailsContainerChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
|
{
|
|
if (args.InRecycleQueue || args.Item is not ExplorerItem item) return;
|
|
Pane?.Visuals.RequestIcon(item, 16);
|
|
}
|
|
|
|
private void OnIconsContainerChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
|
{
|
|
if (args.InRecycleQueue || args.Item is not ExplorerItem item || Pane is null) return;
|
|
item.IconSize = Math.Min(Pane.IconSize, 96);
|
|
Pane.Visuals.RequestThumbnail(item, Pane.IconSize);
|
|
}
|
|
|
|
private void OnSearchContainerChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
|
{
|
|
if (args.InRecycleQueue || args.Item is not SearchResultItem item) return;
|
|
Pane?.Visuals.RequestIcon(item, 16);
|
|
}
|
|
|
|
private void OnItemDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
|
{
|
|
if (Pane is null) return;
|
|
var item = ResolveItemAt(e.OriginalSource as DependencyObject);
|
|
if (item is not null) _ = Pane.OpenItemAsync(item);
|
|
}
|
|
|
|
private void OnSearchResultDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
|
{
|
|
var item = ResolveSearchItemAt(e.OriginalSource as DependencyObject);
|
|
if (item is not null) _ = Pane!.OpenSearchResultAsync(item);
|
|
}
|
|
|
|
private static ExplorerItem? ResolveItemAt(DependencyObject? source)
|
|
{
|
|
while (source is not null)
|
|
{
|
|
if (source is FrameworkElement { DataContext: ExplorerItem item }) return item;
|
|
source = VisualTreeHelper.GetParent(source);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static SearchResultItem? ResolveSearchItemAt(DependencyObject? source)
|
|
{
|
|
while (source is not null)
|
|
{
|
|
if (source is FrameworkElement { DataContext: SearchResultItem item }) return item;
|
|
source = VisualTreeHelper.GetParent(source);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void OnListKeyDown(object sender, KeyRoutedEventArgs e)
|
|
{
|
|
if (Pane is null) return;
|
|
var ctrl = IsKeyDown(VirtualKey.Control);
|
|
var shift = IsKeyDown(VirtualKey.Shift);
|
|
|
|
switch (e.Key)
|
|
{
|
|
case VirtualKey.Enter when !ctrl:
|
|
e.Handled = true;
|
|
Pane.OpenSelectedCommand.Execute(SelectedItems());
|
|
break;
|
|
case VirtualKey.F2:
|
|
e.Handled = true;
|
|
Pane.StartRenameCommand.Execute(SelectedItems().FirstOrDefault());
|
|
break;
|
|
case VirtualKey.Back when !ctrl:
|
|
e.Handled = true;
|
|
Pane.UpCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.Delete:
|
|
e.Handled = true;
|
|
OnDeleteClick(sender, e);
|
|
break;
|
|
case VirtualKey.F5:
|
|
e.Handled = true;
|
|
Pane.RefreshCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.C when ctrl && shift:
|
|
e.Handled = true;
|
|
Pane.CopyPathSelectedCommand.Execute(SelectedItems());
|
|
break;
|
|
case VirtualKey.C when ctrl:
|
|
e.Handled = true;
|
|
Pane.CopySelectedCommand.Execute(SelectedItems());
|
|
break;
|
|
case VirtualKey.X when ctrl:
|
|
e.Handled = true;
|
|
Pane.CutSelectedCommand.Execute(SelectedItems());
|
|
break;
|
|
case VirtualKey.V when ctrl:
|
|
e.Handled = true;
|
|
Pane.PasteCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.N when ctrl && shift:
|
|
e.Handled = true;
|
|
Pane.NewFolderCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.L when ctrl:
|
|
e.Handled = true;
|
|
BeginAddressEdit();
|
|
break;
|
|
case VirtualKey.Up when IsKeyDown(VirtualKey.Menu):
|
|
e.Handled = true;
|
|
Pane.UpCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.Left when IsKeyDown(VirtualKey.Menu):
|
|
e.Handled = true;
|
|
Pane.BackCommand.Execute(null);
|
|
break;
|
|
case VirtualKey.Right when IsKeyDown(VirtualKey.Menu):
|
|
e.Handled = true;
|
|
Pane.ForwardCommand.Execute(null);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static bool IsKeyDown(VirtualKey key)
|
|
=> InputKeyboardSource.GetKeyStateForCurrentThread(key).HasFlag(CoreVirtualKeyStates.Down);
|
|
|
|
// ── 重命名输入框 ────────────────────────────────────────────────────────
|
|
private void OnRenameRequested(object? sender, ExplorerItem item)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() => FocusRenameBox(item));
|
|
}
|
|
|
|
private void FocusRenameBox(ExplorerItem item)
|
|
{
|
|
var host = Pane?.IsDetailsView == true ? (DependencyObject)DetailsList : IconsGrid;
|
|
var container = host is ListViewBase list
|
|
? list.ContainerFromItem(item) as DependencyObject
|
|
: null;
|
|
if (container is null) return;
|
|
if (FindRenameBox(container) is { } box)
|
|
{
|
|
box.Focus(FocusState.Programmatic);
|
|
box.SelectAll();
|
|
}
|
|
}
|
|
|
|
private static TextBox? FindRenameBox(DependencyObject root)
|
|
{
|
|
if (root is TextBox { Tag: "rename" } box) return box;
|
|
var count = VisualTreeHelper.GetChildrenCount(root);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(root, i);
|
|
if (FindRenameBox(child) is { } found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void OnRenameBoxKeyDown(object sender, KeyRoutedEventArgs e)
|
|
{
|
|
if (sender is not TextBox box || box.DataContext is not ExplorerItem item) return;
|
|
if (e.Key == VirtualKey.Enter)
|
|
{
|
|
e.Handled = true;
|
|
Pane?.CommitRenameCommand.Execute(item);
|
|
}
|
|
else if (e.Key == VirtualKey.Escape)
|
|
{
|
|
e.Handled = true;
|
|
Pane?.CancelRenameCommand.Execute(item);
|
|
}
|
|
}
|
|
|
|
private void OnRenameBoxLostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is TextBox { DataContext: ExplorerItem item } && item.IsRenaming)
|
|
Pane?.CommitRenameCommand.Execute(item);
|
|
}
|
|
|
|
private void OnScrollIntoViewRequested(object? sender, ExplorerItem item)
|
|
{
|
|
if (Pane?.IsDetailsView == true) DetailsList.ScrollIntoView(item);
|
|
else IconsGrid.ScrollIntoView(item);
|
|
}
|
|
|
|
private void OnFocusSearchRequested(object? sender, EventArgs e)
|
|
{
|
|
SearchBox.Focus(FocusState.Programmatic);
|
|
}
|
|
|
|
private void OnOpenInNewTabRequested(object? sender, NavigationLocation location)
|
|
{
|
|
Main.AddTab(location);
|
|
}
|
|
|
|
// ── 右键菜单(应用内 + 系统原版) ───────────────────────────────────────
|
|
private void OnContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
|
{
|
|
if (Pane is null || sender is not FrameworkElement target) return;
|
|
var selected = SelectedItems();
|
|
var flyout = BuildContextFlyout(selected);
|
|
if (args.TryGetPosition(sender, out var position)) flyout.ShowAt(target, new FlyoutShowOptions { Position = position });
|
|
else flyout.ShowAt(target);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnSearchResultContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
|
{
|
|
if (sender is not FrameworkElement target) return;
|
|
var flyout = new MenuFlyout();
|
|
var openItem = new MenuFlyoutItem { Text = "打开" };
|
|
openItem.Click += (_, _) => _ = Pane?.OpenSearchResultCommand.ExecuteAsync(SingleSearchResult(sender));
|
|
var openFolder = new MenuFlyoutItem { Text = "打开所在文件夹" };
|
|
openFolder.Click += (_, _) => _ = Pane?.OpenSearchResultFolderCommand.ExecuteAsync(SingleSearchResult(sender));
|
|
var copyPath = new MenuFlyoutItem { Text = "复制完整路径" };
|
|
copyPath.Click += (_, _) =>
|
|
{
|
|
var item = SingleSearchResult(sender);
|
|
if (item is null) return;
|
|
var package = new DataPackage();
|
|
package.SetText(item.FullPath);
|
|
Clipboard.SetContent(package);
|
|
};
|
|
flyout.Items.Add(openItem);
|
|
flyout.Items.Add(openFolder);
|
|
flyout.Items.Add(copyPath);
|
|
if (args.TryGetPosition(sender, out var position)) flyout.ShowAt(target, new FlyoutShowOptions { Position = position });
|
|
else flyout.ShowAt(target);
|
|
args.Handled = true;
|
|
|
|
SearchResultItem? SingleSearchResult(UIElement element)
|
|
=> (element as ListViewBase)?.SelectedItems.OfType<SearchResultItem>().FirstOrDefault();
|
|
}
|
|
|
|
private MenuFlyout BuildContextFlyout(IReadOnlyList<ExplorerItem> selection)
|
|
{
|
|
var flyout = new MenuFlyout();
|
|
|
|
if (selection.Count > 0)
|
|
{
|
|
var open = new MenuFlyoutItem { Text = "打开", Icon = new FontIcon { Glyph = "\uE8E5" } };
|
|
open.Click += (_, _) => Pane?.OpenSelectedCommand.Execute(selection);
|
|
flyout.Items.Add(open);
|
|
|
|
if (selection.Count == 1 && !selection[0].IsDirectory)
|
|
{
|
|
var openWith = new MenuFlyoutItem { Text = "打开方式" };
|
|
openWith.Click += (_, _) => Pane?.OpenWithSelectedCommand.Execute(selection);
|
|
flyout.Items.Add(openWith);
|
|
}
|
|
|
|
if (selection.Count == 1 && selection[0].IsDirectory)
|
|
{
|
|
var newTab = new MenuFlyoutItem { Text = "在新标签页中打开" };
|
|
newTab.Click += (_, _) => Pane?.OpenInNewTabCommand.Execute(selection[0]);
|
|
flyout.Items.Add(newTab);
|
|
}
|
|
|
|
flyout.Items.Add(new MenuFlyoutSeparator());
|
|
|
|
var cut = new MenuFlyoutItem { Text = "剪切", Icon = new FontIcon { Glyph = "\uE8C6" } };
|
|
cut.Click += (_, _) => Pane?.CutSelectedCommand.Execute(selection);
|
|
var copy = new MenuFlyoutItem { Text = "复制", Icon = new FontIcon { Glyph = "\uE8C8" } };
|
|
copy.Click += (_, _) => Pane?.CopySelectedCommand.Execute(selection);
|
|
var copyPath = new MenuFlyoutItem { Text = "复制完整路径" };
|
|
copyPath.Click += (_, _) => Pane?.CopyPathSelectedCommand.Execute(selection);
|
|
var rename = new MenuFlyoutItem { Text = "重命名", Icon = new FontIcon { Glyph = "\uE8AC" } };
|
|
rename.Click += (_, _) => Pane?.StartRenameCommand.Execute(selection[0]);
|
|
var delete = new MenuFlyoutItem { Text = "删除", Icon = new FontIcon { Glyph = "\uE74D" } };
|
|
delete.Click += (_, _) => Pane?.DeleteSelectedCommand.Execute(selection);
|
|
|
|
flyout.Items.Add(cut);
|
|
flyout.Items.Add(copy);
|
|
flyout.Items.Add(copyPath);
|
|
flyout.Items.Add(rename);
|
|
flyout.Items.Add(delete);
|
|
|
|
if (Pane is { IsRecycleBin: true })
|
|
{
|
|
var restore = new MenuFlyoutItem { Text = "还原" };
|
|
restore.Click += (_, _) => Pane?.RestoreSelectedCommand.Execute(selection);
|
|
flyout.Items.Add(restore);
|
|
}
|
|
|
|
flyout.Items.Add(new MenuFlyoutSeparator());
|
|
|
|
var properties = new MenuFlyoutItem { Text = "属性", Icon = new FontIcon { Glyph = "\uE946" } };
|
|
properties.Click += (_, _) => Pane?.ShowPropertiesSelectedCommand.Execute(selection);
|
|
var reveal = new MenuFlyoutItem { Text = "在资源管理器中显示" };
|
|
reveal.Click += (_, _) => Pane?.RevealSelectedCommand.Execute(selection);
|
|
flyout.Items.Add(properties);
|
|
flyout.Items.Add(reveal);
|
|
}
|
|
else
|
|
{
|
|
var refresh = new MenuFlyoutItem { Text = "刷新", Icon = new FontIcon { Glyph = "\uE72C" } };
|
|
refresh.Click += (_, _) => Pane?.RefreshCommand.Execute(null);
|
|
var newFolder = new MenuFlyoutItem { Text = "新建文件夹", Icon = new FontIcon { Glyph = "\uE8B7" } };
|
|
newFolder.Click += (_, _) => Pane?.NewFolderCommand.Execute(null);
|
|
var paste = new MenuFlyoutItem { Text = "粘贴", Icon = new FontIcon { Glyph = "\uE77F" } };
|
|
paste.Click += (_, _) => Pane?.PasteCommand.Execute(null);
|
|
flyout.Items.Add(refresh);
|
|
flyout.Items.Add(newFolder);
|
|
flyout.Items.Add(paste);
|
|
}
|
|
|
|
flyout.Items.Add(new MenuFlyoutSeparator());
|
|
var showMore = new MenuFlyoutItem { Text = "显示更多选项(系统菜单)" };
|
|
showMore.Click += async (_, _) => await ShowShellContextMenuAsync(
|
|
selection.Count > 0
|
|
? selection.Select(i => i.FullPath).ToList()
|
|
: (Pane?.CurrentPath is { Length: > 0 } p && Directory.Exists(p) ? [p] : []));
|
|
flyout.Items.Add(showMore);
|
|
|
|
return flyout;
|
|
}
|
|
|
|
// ── 拖放 ────────────────────────────────────────────────────────────────
|
|
private void OnDragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
|
{
|
|
var items = e.Items.OfType<ExplorerItem>().ToList();
|
|
var paths = items.Select(i => i.FullPath).ToList();
|
|
_draggedPaths = paths;
|
|
e.Data.RequestedOperation = DataPackageOperation.Copy | DataPackageOperation.Move;
|
|
|
|
e.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
|
|
{
|
|
var deferral = request.GetDeferral();
|
|
try
|
|
{
|
|
var storageItems = new List<Windows.Storage.IStorageItem>();
|
|
foreach (var path in paths)
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(path))
|
|
storageItems.Add(await Windows.Storage.StorageFolder.GetFolderFromPathAsync(path));
|
|
else if (File.Exists(path))
|
|
storageItems.Add(await Windows.Storage.StorageFile.GetFileFromPathAsync(path));
|
|
}
|
|
catch
|
|
{
|
|
// 单个项目不可用时跳过,不阻断整次拖放
|
|
}
|
|
}
|
|
request.SetData(storageItems);
|
|
}
|
|
finally
|
|
{
|
|
deferral.Complete();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnDragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (!e.DataView.Contains(StandardDataFormats.StorageItems)) return;
|
|
var copy = IsKeyDown(VirtualKey.Control);
|
|
e.AcceptedOperation = copy ? DataPackageOperation.Copy : DataPackageOperation.Move;
|
|
e.DragUIOverride.Caption = copy ? $"复制到“{Pane?.Title}”" : $"移动到“{Pane?.Title}”";
|
|
e.DragUIOverride.IsCaptionVisible = true;
|
|
}
|
|
|
|
private async void OnDrop(object sender, DragEventArgs e)
|
|
{
|
|
if (Pane is null || !e.DataView.Contains(StandardDataFormats.StorageItems)) return;
|
|
var deferral = e.GetDeferral();
|
|
try
|
|
{
|
|
var target = ResolveDropTarget(e);
|
|
var pathCopy = IsKeyDown(VirtualKey.Control);
|
|
|
|
// 优先使用应用内拖动的路径(避免 StorageItem 往返),否则从数据包读取
|
|
var paths = _draggedPaths.Count > 0
|
|
? _draggedPaths.ToList()
|
|
: (await e.DataView.GetStorageItemsAsync()).Select(i => i.Path).ToList();
|
|
|
|
await Pane.DropIntoAsync(paths, target, pathCopy);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Pane.ReportError($"拖放失败:{ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
deferral.Complete();
|
|
}
|
|
}
|
|
|
|
/// <summary>拖到文件夹行上 → 放进该文件夹;拖到空白处 → 放进当前文件夹。</summary>
|
|
private string ResolveDropTarget(DragEventArgs e)
|
|
{
|
|
var dropPoint = e.GetPosition(DetailsList);
|
|
var elements = VisualTreeHelper.FindElementsInHostCoordinates(dropPoint, DetailsList);
|
|
foreach (var element in elements)
|
|
{
|
|
if (element is FrameworkElement { DataContext: ExplorerItem { IsDirectory: true } folder })
|
|
return folder.FullPath;
|
|
}
|
|
return Pane?.CurrentPath ?? string.Empty;
|
|
}
|
|
|
|
// ── 状态栏 ──────────────────────────────────────────────────────────────
|
|
private void OnIndexChipTapped(object sender, TappedRoutedEventArgs e)
|
|
{
|
|
if (Main.IndexNeedsElevation) Main.RestartElevatedCommand.Execute(null);
|
|
else Main.BuildIndexCommand.Execute(null);
|
|
}
|
|
|
|
private void OnOpenQueueClick(object sender, RoutedEventArgs e)
|
|
{
|
|
Main.IsQueueExpanded = true;
|
|
App.MainWindowInstance.ShowOperationQueue();
|
|
}
|
|
|
|
private async void OnOpenSettingsClick(object sender, RoutedEventArgs e)
|
|
{
|
|
var dialog = new SettingsDialog(Main) { XamlRoot = XamlRoot };
|
|
await dialog.ShowAsync();
|
|
}
|
|
}
|