Files
fluid-explorer/Views/ExplorerPaneView.xaml.cs

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();
}
}