Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)

This commit is contained in:
WpyQwq
2026-09-19 11:54:03 +08:00
commit 5780fde61a
60 changed files with 15035 additions and 0 deletions
+508
View File
@@ -0,0 +1,508 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="FluidExplorer.Views.ExplorerPaneView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="using:FluidExplorer.Models"
xmlns:vm="using:FluidExplorer.ViewModels"
xmlns:ops="using:FluidExplorer.Services.Operations"
Background="Transparent">
<UserControl.Resources>
<!-- 行高对齐资源管理器(详情 30 / 列表 24),保留 WinUI 原生 ListViewItem 模板与动画 -->
<x:Double x:Key="ListViewItemMinHeight">30</x:Double>
<x:Double x:Key="GridViewItemMinHeight">0</x:Double>
<x:Double x:Key="GridViewItemMinWidth">0</x:Double>
<!-- 资源管理器的"图标+文字"工具按钮(新建 / 排序 / 查看):12px 文字、32 高、4 圆角 -->
<Style x:Key="ToolbarTextButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}">
<Setter Property="Height" Value="32" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="8,0" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="12" />
</Style>
</UserControl.Resources>
<Grid x:Name="PaneRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- ═══ 命令栏(对齐资源管理器:新建 / 剪切复制粘贴 / 重命名 / 删除 / 撤销 / 排序 / 视图) ═══ -->
<Grid Grid.Row="0" Height="48" Padding="8,0" ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="2">
<Button x:Name="SidebarToggle" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="导航窗格" Click="OnToggleSidebarClick">
<FontIcon Glyph="&#xE700;" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarTextButtonStyle}" ToolTipService.ToolTip="新建 (Ctrl+Shift+N)">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="{StaticResource GlyphNew}" FontSize="16" />
<TextBlock Text="新建" VerticalAlignment="Center" />
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuFlyoutItem Text="文件夹" Click="OnNewFolderClick">
<MenuFlyoutItem.Icon><FontIcon Glyph="{StaticResource GlyphFolder}" /></MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem Text="文本文档" Click="OnNewTextFileClick">
<MenuFlyoutItem.Icon><FontIcon Glyph="{StaticResource GlyphExtension}" /></MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem Text="新建标签页" Click="OnNewTabClick" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="2">
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="剪切 (Ctrl+X)" Click="OnCutClick">
<FontIcon Glyph="{StaticResource GlyphCut}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="复制 (Ctrl+C)" Click="OnCopyClick">
<FontIcon Glyph="{StaticResource GlyphCopy}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="粘贴 (Ctrl+V)" Click="OnPasteClick">
<FontIcon Glyph="{StaticResource GlyphPaste}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="重命名 (F2)" Click="OnRenameClick">
<FontIcon Glyph="{StaticResource GlyphRename}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="删除 (Delete)" Click="OnDeleteClick">
<FontIcon Glyph="{StaticResource GlyphDelete}" FontSize="16" />
</Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="2" Margin="8,0,0,0">
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="撤销 (Ctrl+Z)" Click="OnUndoClick">
<FontIcon Glyph="{StaticResource GlyphUndo}" FontSize="16" />
</Button>
<Button x:Name="RestoreButton" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="还原所选项目"
Click="OnRestoreClick" Visibility="Collapsed">
<FontIcon Glyph="{StaticResource GlyphRestore}" FontSize="16" />
</Button>
</StackPanel>
<StackPanel Grid.Column="4" Orientation="Horizontal" Spacing="2">
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="双窗格(Ctrl+Shift+D):左右对拖搬文件"
AutomationProperties.Name="双窗格" Click="OnToggleDualPaneClick">
<FontIcon Glyph="{StaticResource GlyphDualPane}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarTextButtonStyle}" ToolTipService.ToolTip="排序方式">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="{StaticResource GlyphSort}" FontSize="16" />
<TextBlock Text="排序" VerticalAlignment="Center" />
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuFlyoutItem Text="名称" Tag="Name" Click="OnSortClick" />
<MenuFlyoutItem Text="修改日期" Tag="DateModified" Click="OnSortClick" />
<MenuFlyoutItem Text="类型" Tag="Type" Click="OnSortClick" />
<MenuFlyoutItem Text="大小" Tag="Size" Click="OnSortClick" />
<MenuFlyoutSeparator />
<MenuFlyoutItem Text="升序" Tag="Ascending" Click="OnSortDirectionClick" />
<MenuFlyoutItem Text="降序" Tag="Descending" Click="OnSortDirectionClick" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Style="{StaticResource ToolbarTextButtonStyle}" ToolTipService.ToolTip="查看">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="{StaticResource GlyphView}" FontSize="16" />
<TextBlock Text="查看" VerticalAlignment="Center" />
</StackPanel>
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuFlyoutItem Text="超大图标" Tag="ExtraLargeIcons" Click="OnViewModeClick" />
<MenuFlyoutItem Text="大图标" Tag="LargeIcons" Click="OnViewModeClick" />
<MenuFlyoutItem Text="中图标" Tag="MediumIcons" Click="OnViewModeClick" />
<MenuFlyoutItem Text="小图标" Tag="SmallIcons" Click="OnViewModeClick" />
<MenuFlyoutItem Text="列表" Tag="List" Click="OnViewModeClick" />
<MenuFlyoutItem Text="详细信息" Tag="Details" Click="OnViewModeClick" />
<MenuFlyoutSeparator />
<ToggleMenuFlyoutItem x:Name="HiddenToggle" Text="显示隐藏的项目" Click="OnToggleHiddenClick" />
<ToggleMenuFlyoutItem x:Name="ExtensionToggle" Text="显示文件扩展名" Click="OnToggleExtensionClick" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="更多选项" Click="OnShellContextMenuClick">
<FontIcon Glyph="{StaticResource GlyphMore}" FontSize="16" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="设置" Click="OnOpenSettingsClick">
<FontIcon Glyph="{StaticResource GlyphSettings}" FontSize="16" />
</Button>
</StackPanel>
</Grid>
<!-- ═══ 地址栏(后退/前进/向上/刷新 + 面包屑 + 搜索框) ═══ -->
<Grid Grid.Row="1" Height="40" Padding="8,0" ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="后退 (Alt+←)" Click="OnBackClick">
<FontIcon Glyph="{StaticResource GlyphBack}" FontSize="14" />
</Button>
<Button Grid.Column="1" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="前进 (Alt+→)" Click="OnForwardClick">
<FontIcon Glyph="{StaticResource GlyphForward}" FontSize="14" />
</Button>
<Button Grid.Column="2" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="向上 (Alt+↑)" Click="OnUpClick">
<FontIcon Glyph="{StaticResource GlyphUp}" FontSize="14" />
</Button>
<Button Grid.Column="3" Style="{StaticResource ToolbarButtonStyle}" ToolTipService.ToolTip="刷新 (F5)" Click="OnRefreshClick">
<FontIcon Glyph="{StaticResource GlyphRefresh}" FontSize="14" />
</Button>
<Grid Grid.Column="4" Margin="4,4" CornerRadius="4" Background="{ThemeResource ControlFillColorDefaultBrush}">
<BreadcrumbBar x:Name="AddressBreadcrumb"
ItemsSource="{x:Bind Pane.Breadcrumbs, Mode=OneWay}"
ItemClicked="OnBreadcrumbClicked" />
<TextBox x:Name="AddressEditor"
Visibility="Collapsed"
BorderThickness="0"
Background="Transparent"
KeyDown="OnAddressEditorKeyDown"
LostFocus="OnAddressEditorLostFocus" />
</Grid>
<AutoSuggestBox x:Name="SearchBox"
Grid.Column="5"
Width="260"
MinWidth="160"
Height="32"
VerticalAlignment="Center"
QueryIcon="Find"
PlaceholderText="搜索"
TextChanged="OnSearchTextChanged"
QuerySubmitted="OnSearchQuerySubmitted"
SuggestionChosen="OnSearchSuggestionChosen" />
</Grid>
<!-- 加载进度:不遮挡内容,只在标题下方显示一条细线 -->
<ProgressBar Grid.Row="2"
Height="2"
IsIndeterminate="True"
Visibility="{x:Bind Pane.IsLoading, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
<!-- ═══ 内容区(左:导航窗格 / 右:文件列表) ═══ -->
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SidebarColumn" Width="242" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="SidebarHost" Grid.Column="0" Padding="4,0,4,4">
<TreeView x:Name="SidebarTree"
ItemsSource="{x:Bind Main.SidebarRoots, Mode=OneWay}"
SelectionMode="Single"
ItemInvoked="OnSidebarItemInvoked"
Expanding="OnSidebarExpanding"
CanDragItems="False"
CanReorderItems="False">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="vm:SidebarNode">
<TreeViewItem ItemsSource="{x:Bind Children}"
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}"
ToolTipService.ToolTip="{x:Bind Location.Path}">
<StackPanel Orientation="Horizontal" Spacing="10">
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}"
Glyph="{x:Bind Glyph}"
FontSize="14" />
<TextBlock Text="{x:Bind DisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
<!-- 文件列表所在层用系统"图层填充",工具栏/地址栏/导航窗格保持 Mica 透出(对齐资源管理器的层次感) -->
<Grid Grid.Column="1" Background="{ThemeResource LayerFillColorDefaultBrush}">
<!-- 文件夹内容 -->
<Grid Visibility="{x:Bind Pane.IsSearchActive, Mode=OneWay, Converter={StaticResource BoolToVisibility}, ConverterParameter=invert}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<InfoBar Grid.Row="0"
Margin="8,4"
Severity="Error"
IsOpen="{x:Bind Pane.HasError, Mode=OneWay}"
Message="{x:Bind Pane.ErrorMessage, Mode=OneWay}"
CloseButtonClick="OnDismissErrorClick" />
<!-- 详情视图列头 -->
<Grid Grid.Row="1"
Height="28"
Padding="12,0"
ColumnSpacing="8"
Visibility="{x:Bind Pane.IsDetailsView, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="160" />
<ColumnDefinition Width="140" />
<ColumnDefinition Width="140" />
<ColumnDefinition Width="96" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Style="{StaticResource ColumnHeaderButtonStyle}" Content="名称" Tag="Name" Click="OnSortClick" />
<Button Grid.Column="1" Style="{StaticResource ColumnHeaderButtonStyle}" Content="修改日期" Tag="DateModified" Click="OnSortClick" />
<Button Grid.Column="2" Style="{StaticResource ColumnHeaderButtonStyle}" Content="类型" Tag="Type" Click="OnSortClick" />
<Button Grid.Column="3" Style="{StaticResource ColumnHeaderButtonStyle}" Content="大小" Tag="Size" Click="OnSortClick"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Right" />
</Grid>
<!-- 详细信息 / 列表 -->
<ListView x:Name="DetailsList"
Grid.Row="2"
Padding="0,0,0,8"
SelectionMode="Extended"
ItemsSource="{x:Bind Pane.Items, Mode=OneWay}"
SelectionChanged="OnListSelectionChanged"
ContainerContentChanging="OnDetailsContainerChanging"
DoubleTapped="OnItemDoubleTapped"
KeyDown="OnListKeyDown"
ContextRequested="OnContextRequested"
AllowDrop="True"
CanDragItems="True"
DragItemsStarting="OnDragItemsStarting"
DragOver="OnDragOver"
Drop="OnDrop"
Visibility="{x:Bind Pane.IsDetailsView, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:ExplorerItem">
<Grid Height="30" Padding="0,0,8,0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="160" />
<ColumnDefinition Width="140" />
<ColumnDefinition Width="140" />
<ColumnDefinition Width="96" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<!-- 固定 28px 预留给复选框:勾选/取消勾选时文件名不会左右跳动(对齐资源管理器) -->
<ColumnDefinition Width="28" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
MinWidth="0"
Margin="4,0,0,0"
HorizontalAlignment="Center"
IsChecked="{x:Bind IsSelected, Mode=TwoWay}"
Visibility="{x:Bind ShowCheckBox, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
<Image Grid.Column="1"
Width="16"
Height="16"
Source="{x:Bind Icon, Mode=OneWay}" />
<Grid Grid.Column="2">
<TextBlock VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{x:Bind DisplayName, Mode=OneWay}"
Visibility="{x:Bind IsRenaming, Mode=OneWay, Converter={StaticResource BoolToVisibility}, ConverterParameter=invert}" />
<TextBox Tag="rename"
MinWidth="180"
Padding="4,0"
VerticalAlignment="Center"
Text="{x:Bind RenameText, Mode=TwoWay}"
Visibility="{x:Bind IsRenaming, Mode=OneWay, Converter={StaticResource BoolToVisibility}}"
KeyDown="OnRenameBoxKeyDown"
LostFocus="OnRenameBoxLostFocus" />
</Grid>
</Grid>
<TextBlock Grid.Column="1" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ModifiedText, Mode=OneWay}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind TypeText}" />
<TextBlock Grid.Column="3" VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind SizeText}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- 图标 / 网格视图 -->
<GridView x:Name="IconsGrid"
Grid.Row="2"
Padding="12,8,12,12"
SelectionMode="Extended"
ItemsSource="{x:Bind Pane.Items, Mode=OneWay}"
SelectionChanged="OnListSelectionChanged"
ContainerContentChanging="OnIconsContainerChanging"
DoubleTapped="OnItemDoubleTapped"
KeyDown="OnListKeyDown"
ContextRequested="OnContextRequested"
AllowDrop="True"
CanDragItems="True"
DragItemsStarting="OnDragItemsStarting"
DragOver="OnDragOver"
Drop="OnDrop"
Visibility="{x:Bind Pane.IsIconsView, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="models:ExplorerItem">
<Grid Padding="4" CornerRadius="4">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="6">
<Image Width="{x:Bind IconSize, Mode=OneWay}"
Height="{x:Bind IconSize, Mode=OneWay}"
HorizontalAlignment="Center"
Source="{x:Bind Icon, Mode=OneWay}" />
<TextBlock Text="{x:Bind DisplayName, Mode=OneWay}"
TextWrapping="Wrap"
MaxLines="2"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsRenaming, Mode=OneWay, Converter={StaticResource BoolToVisibility}, ConverterParameter=invert}" />
<TextBox Tag="rename"
MinWidth="120"
TextAlignment="Center"
Text="{x:Bind RenameText, Mode=TwoWay}"
Visibility="{x:Bind IsRenaming, Mode=OneWay, Converter={StaticResource BoolToVisibility}}"
KeyDown="OnRenameBoxKeyDown"
LostFocus="OnRenameBoxLostFocus" />
</StackPanel>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<!-- 空文件夹 / 提示 -->
<TextBlock Grid.Row="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
TextAlignment="Center"
Style="{StaticResource PaneHintTextStyle}"
Text="{x:Bind Pane.EmptyMessage, Mode=OneWay}"
Visibility="{x:Bind Pane.EmptyMessage, Mode=OneWay, Converter={StaticResource StringToVisibility}}" />
</Grid>
<!-- 搜索结果 -->
<Grid Visibility="{x:Bind Pane.IsSearchActive, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="12,8" Spacing="4">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Pane.SearchStatusText, Mode=OneWay}" />
<ProgressBar IsIndeterminate="True"
Height="2"
Visibility="{x:Bind Pane.IsSearching, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<InfoBar Grid.Row="1"
Margin="8,0,8,4"
Severity="Informational"
IsOpen="{x:Bind Pane.HasNote, Mode=OneWay}"
Message="{x:Bind Pane.NoteMessage, Mode=OneWay}" />
<ListView Grid.Row="2"
Padding="0,0,0,8"
SelectionMode="Extended"
ItemsSource="{x:Bind Pane.SearchResults, Mode=OneWay}"
ContainerContentChanging="OnSearchContainerChanging"
DoubleTapped="OnSearchResultDoubleTapped"
ContextRequested="OnSearchResultContextRequested">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:SearchResultItem">
<Grid Height="30" Padding="12,0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="160" />
<ColumnDefinition Width="*" MinWidth="180" />
<ColumnDefinition Width="140" />
<ColumnDefinition Width="96" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="16" Height="16" Source="{x:Bind Icon, Mode=OneWay}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Text="{x:Bind Name}" />
</Grid>
<TextBlock Grid.Column="1" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Directory}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ModifiedText}" />
<TextBlock Grid.Column="3" VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind SizeText}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Grid>
</Grid>
<!-- ═══ 状态栏 ═══ -->
<Grid Grid.Row="4" Height="26" Padding="12,0" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Style="{StaticResource StatusBarTextStyle}"
TextTrimming="CharacterEllipsis"
Text="{x:Bind Pane.StatusText, Mode=OneWay}" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<TextBlock x:Name="IndexChip"
Style="{StaticResource StatusBarTextStyle}"
Text="{x:Bind Main.IndexSummary, Mode=OneWay}"
ToolTipService.ToolTip="点击建立/重建文件索引(Everything 级搜索的前提)"
Tapped="OnIndexChipTapped" />
<Button Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="22"
ToolTipService.ToolTip="显示隐藏的项目" Click="OnToggleHiddenClick">
<FontIcon Glyph="{StaticResource GlyphHidden}" FontSize="12" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="22"
ToolTipService.ToolTip="切换视图方式" Click="OnCycleViewClick">
<FontIcon Glyph="{StaticResource GlyphView}" FontSize="12" />
</Button>
</StackPanel>
<Button Grid.Column="2"
Style="{StaticResource ToolbarButtonStyle}"
Height="22"
Width="Auto"
Padding="8,0"
ToolTipService.ToolTip="文件操作队列"
Click="OnOpenQueueClick">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="{StaticResource GlyphQueue}" FontSize="12" />
<TextBlock Style="{StaticResource StatusBarTextStyle}" Text="{x:Bind Main.ActiveJobCount, Mode=OneWay}" />
</StackPanel>
</Button>
</Grid>
</Grid>
</UserControl>
+800
View File
@@ -0,0 +1,800 @@
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();
}
}
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="FluidExplorer.Views.ExplorerTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="using:FluidExplorer.Views">
<!-- 单窗格 / 双窗格:双窗格是"搬文件"最快的形态(左右对拖,还可以跨标签页用剪贴板) -->
<Grid x:Name="LayoutRoot" Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="FirstColumn" Width="*" />
<ColumnDefinition x:Name="SplitterColumn" Width="0" />
<ColumnDefinition x:Name="SecondColumn" Width="0" />
</Grid.ColumnDefinitions>
<views:ExplorerPaneView x:Name="PrimaryPaneView"
Grid.Column="0"
Pane="{x:Bind TabViewModel.Primary, Mode=OneWay}" />
<Border x:Name="Splitter"
Grid.Column="1"
Background="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="Collapsed"
PointerPressed="OnSplitterPointerPressed"
PointerMoved="OnSplitterPointerMoved"
PointerReleased="OnSplitterPointerReleased"
PointerCaptureLost="OnSplitterPointerReleased" />
<views:ExplorerPaneView x:Name="SecondaryPaneView"
Grid.Column="2"
Pane="{x:Bind TabViewModel.Secondary, Mode=OneWay}" />
</Grid>
</UserControl>
+98
View File
@@ -0,0 +1,98 @@
using System.ComponentModel;
using FluidExplorer.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace FluidExplorer.Views;
/// <summary>一个标签页的宿主:负责单/双窗格布局与中间分隔条。</summary>
public sealed partial class ExplorerTabView : UserControl
{
private bool _dragging;
private double _startX;
private double _startFirstWidth;
private double _startSecondWidth;
public ExplorerTabView()
{
InitializeComponent();
Loaded += (_, _) => UpdatePaneLayout();
}
public static readonly DependencyProperty TabViewModelProperty = DependencyProperty.Register(
nameof(TabViewModel),
typeof(ExplorerTabViewModel),
typeof(ExplorerTabView),
new PropertyMetadata(null, OnTabChanged));
public ExplorerTabViewModel? TabViewModel
{
get => (ExplorerTabViewModel?)GetValue(TabViewModelProperty);
set => SetValue(TabViewModelProperty, value);
}
private static void OnTabChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = (ExplorerTabView)d;
if (e.OldValue is ExplorerTabViewModel old) old.PropertyChanged -= view.OnTabPropertyChanged;
if (e.NewValue is ExplorerTabViewModel tab) tab.PropertyChanged += view.OnTabPropertyChanged;
view.UpdatePaneLayout();
}
private void OnTabPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ExplorerTabViewModel.IsDualPane)) UpdatePaneLayout();
}
private void UpdatePaneLayout()
{
var dual = TabViewModel?.IsDualPane == true;
if (dual)
{
Splitter.Visibility = Visibility.Visible;
SecondaryPaneView.Visibility = Visibility.Visible;
SplitterColumn.Width = new GridLength(4);
if (FirstColumn.Width.IsAbsolute && SecondColumn.Width.IsAbsolute) return;
FirstColumn.Width = new GridLength(1, GridUnitType.Star);
SecondColumn.Width = new GridLength(1, GridUnitType.Star);
}
else
{
Splitter.Visibility = Visibility.Collapsed;
SecondaryPaneView.Visibility = Visibility.Collapsed;
SplitterColumn.Width = new GridLength(0);
FirstColumn.Width = new GridLength(1, GridUnitType.Star);
SecondColumn.Width = new GridLength(0);
}
}
private void OnSplitterPointerPressed(object sender, PointerRoutedEventArgs e)
{
_dragging = true;
_startX = e.GetCurrentPoint(LayoutRoot).Position.X;
_startFirstWidth = FirstColumn.ActualWidth;
_startSecondWidth = SecondColumn.ActualWidth;
Splitter.CapturePointer(e.Pointer);
}
private void OnSplitterPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (!_dragging) return;
var delta = e.GetCurrentPoint(LayoutRoot).Position.X - _startX;
var total = _startFirstWidth + _startSecondWidth;
const double min = 240;
var first = Math.Clamp(_startFirstWidth + delta, min, total - min);
FirstColumn.Width = new GridLength(first, GridUnitType.Pixel);
SecondColumn.Width = new GridLength(total - first, GridUnitType.Pixel);
}
private void OnSplitterPointerReleased(object sender, PointerRoutedEventArgs e)
{
if (!_dragging) return;
_dragging = false;
Splitter.ReleasePointerCapture(e.Pointer);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentDialog
x:Class="FluidExplorer.Views.SettingsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="设置"
PrimaryButtonText="完成"
CloseButtonText="取消"
DefaultButton="Primary">
<ScrollViewer MaxHeight="520">
<StackPanel Spacing="16" Width="460">
<TextBlock Text="外观" Style="{StaticResource BodyStrongTextBlockStyle}" />
<ComboBox x:Name="ThemeBox" Header="主题" HorizontalAlignment="Stretch" SelectionChanged="OnThemeChanged">
<ComboBoxItem Content="跟随系统" />
<ComboBoxItem Content="浅色" />
<ComboBoxItem Content="深色" />
</ComboBox>
<ToggleSwitch x:Name="AnimationSwitch" Header="界面动画" OnContent="使用系统原生动画" OffContent="关闭动画" Toggled="OnAnimationToggled" />
<TextBlock Text="浏览" Style="{StaticResource BodyStrongTextBlockStyle}" Margin="0,4,0,0" />
<ToggleSwitch x:Name="HiddenSwitch" Header="显示隐藏的项目" Toggled="OnHiddenToggled" />
<ToggleSwitch x:Name="SystemSwitch" Header="显示受保护的操作系统文件" Toggled="OnSystemToggled" />
<ToggleSwitch x:Name="ExtensionSwitch" Header="显示文件扩展名" Toggled="OnExtensionToggled" />
<ToggleSwitch x:Name="CheckBoxSwitch" Header="始终显示项目复选框" Toggled="OnCheckBoxToggled" />
<ToggleSwitch x:Name="NewTabSwitch" Header="在新标签页中打开文件夹" Toggled="OnNewTabToggled" />
<TextBlock Text="搜索" Style="{StaticResource BodyStrongTextBlockStyle}" Margin="0,4,0,0" />
<ToggleSwitch x:Name="ScopeSwitch" Header="默认仅搜索当前文件夹" OnContent="仅当前文件夹" OffContent="全盘(NTFS 索引)" Toggled="OnScopeToggled" />
<ToggleSwitch x:Name="AsYouTypeSwitch" Header="键入即搜索(无需回车)" Toggled="OnAsYouTypeToggled" />
<ToggleSwitch x:Name="IndexStartupSwitch" Header="启动时建立索引" Toggled="OnIndexStartupToggled" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="立即建立/重建索引" Click="OnBuildIndexClick" />
<Button Content="以管理员身份重启" Click="OnRestartElevatedClick" />
</StackPanel>
<TextBlock x:Name="IndexStatusText" Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
<TextBlock Text="文件操作" Style="{StaticResource BodyStrongTextBlockStyle}" Margin="0,4,0,0" />
<ToggleSwitch x:Name="RecycleSwitch" Header="删除时移入回收站"
OnContent="移入回收站(可撤销)" OffContent="直接永久删除" Toggled="OnRecycleToggled" />
<ComboBox x:Name="ConflictBox" Header="遇到同名文件时" HorizontalAlignment="Stretch" SelectionChanged="OnConflictChanged">
<ComboBoxItem Content="每次都询问" />
<ComboBoxItem Content="替换目标文件" />
<ComboBoxItem Content="跳过" />
<ComboBoxItem Content="两个都保留" />
</ComboBox>
</StackPanel>
</ScrollViewer>
</ContentDialog>
+140
View File
@@ -0,0 +1,140 @@
using FluidExplorer.Services;
using FluidExplorer.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace FluidExplorer.Views;
/// <summary>设置对话框:改动即时生效并写入 settings.json。</summary>
public sealed partial class SettingsDialog : ContentDialog
{
private readonly MainViewModel _main;
private bool _initializing = true;
public SettingsDialog(MainViewModel main)
{
_main = main;
InitializeComponent();
ThemeBox.SelectedIndex = main.ThemeMode switch
{
AppThemeMode.Light => 1,
AppThemeMode.Dark => 2,
_ => 0
};
AnimationSwitch.IsOn = main.AnimationsEnabled;
HiddenSwitch.IsOn = main.ShowHiddenFiles;
SystemSwitch.IsOn = main.ShowSystemFiles;
ExtensionSwitch.IsOn = main.ShowFileExtensions;
CheckBoxSwitch.IsOn = main.AlwaysShowCheckBoxes;
NewTabSwitch.IsOn = main.Settings.OpenFoldersInNewTab;
ScopeSwitch.IsOn = main.SearchInCurrentFolderOnly;
AsYouTypeSwitch.IsOn = main.Settings.SearchAsYouType;
IndexStartupSwitch.IsOn = main.Settings.IndexOnStartup;
RecycleSwitch.IsOn = main.DeleteToRecycleBin;
ConflictBox.SelectedIndex = main.Settings.DefaultConflictPolicy switch
{
ConflictPolicySetting.Replace => 1,
ConflictPolicySetting.Skip => 2,
ConflictPolicySetting.KeepBoth => 3,
_ => 0
};
IndexStatusText.Text = $"{main.IndexSummary}({Environment.ProcessorCount} 核并行扫描;NTFS 索引可让全盘搜索稳定在毫秒级)";
_initializing = false;
}
private void OnThemeChanged(object sender, SelectionChangedEventArgs e)
{
if (_initializing) return;
_main.ThemeMode = ThemeBox.SelectedIndex switch
{
1 => AppThemeMode.Light,
2 => AppThemeMode.Dark,
_ => AppThemeMode.System
};
}
private void OnAnimationToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.AnimationsEnabled = AnimationSwitch.IsOn;
}
private void OnHiddenToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.ShowHiddenFiles = HiddenSwitch.IsOn;
}
private void OnSystemToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.ShowSystemFiles = SystemSwitch.IsOn;
}
private void OnExtensionToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.ShowFileExtensions = ExtensionSwitch.IsOn;
}
private void OnCheckBoxToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.AlwaysShowCheckBoxes = CheckBoxSwitch.IsOn;
}
private void OnNewTabToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.Settings.OpenFoldersInNewTab = NewTabSwitch.IsOn;
_main.Settings.Save();
}
private void OnScopeToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.SearchInCurrentFolderOnly = ScopeSwitch.IsOn;
}
private void OnAsYouTypeToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.Settings.SearchAsYouType = AsYouTypeSwitch.IsOn;
_main.Settings.Save();
}
private void OnIndexStartupToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.Settings.IndexOnStartup = IndexStartupSwitch.IsOn;
_main.Settings.Save();
}
private void OnRecycleToggled(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (_initializing) return;
_main.DeleteToRecycleBin = RecycleSwitch.IsOn;
}
private void OnConflictChanged(object sender, SelectionChangedEventArgs e)
{
if (_initializing) return;
_main.Settings.DefaultConflictPolicy = ConflictBox.SelectedIndex switch
{
1 => ConflictPolicySetting.Replace,
2 => ConflictPolicySetting.Skip,
3 => ConflictPolicySetting.KeepBoth,
_ => ConflictPolicySetting.Ask
};
_main.Settings.Save();
}
private void OnBuildIndexClick(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
_main.BuildIndexCommand.Execute(null);
IndexStatusText.Text = "正在建立索引…(首次读取 MFT 通常几秒内完成,之后靠 USN 增量保持实时)";
}
private void OnRestartElevatedClick(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
=> _main.RestartElevatedCommand.Execute(null);
}
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="FluidExplorer.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FluidExplorer.ViewModels"
xmlns:views="using:FluidExplorer.Views">
<!--
窗口内容放在 UserControl 而不是直接放在 Window 里:WinUI 的 x:Bind 带 Converter 时
生成的代码要求绑定根是 FrameworkElement,而 Window 不是,因此 Window 里不放任何 x:Bind。
视觉结构对齐 Windows 11 资源管理器:标签栏位于标题栏区域,Mica 由宿主窗口设置。
-->
<Grid x:Name="RootGrid">
<!--
标签页刻意不用 TabItemsSource/TabItemTemplate 绑定:TabView 的 TabItemsSource 与
TwoWay SelectedItem 组合在增删标签时会触发容器重建异常(实测出现过整个标签栏与内容区消失)。
这里由代码手工管理 TabItems,行为完全可控。
-->
<TabView
x:Name="TabHost"
IsAddTabButtonVisible="True"
TabWidthMode="SizeToContent"
CanDragTabs="True"
CanReorderTabs="True"
AllowDropTabs="True"
CloseButtonOverlayMode="OnPointerOver"
AddTabButtonClick="OnAddTabButtonClick"
TabCloseRequested="OnTabCloseRequested"
TabDroppedOutside="OnTabDroppedOutside"
SelectionChanged="OnTabSelectionChanged">
<TabView.TabStripHeader>
<Grid Width="8" />
</TabView.TabStripHeader>
<!-- 标签右侧的空白区域 = 窗口拖动区(宿主窗口的 SetTitleBar 指向它) -->
<TabView.TabStripFooter>
<Grid x:Name="TitleBarDragRegion" MinWidth="120" Height="40" Background="Transparent" />
</TabView.TabStripFooter>
</TabView>
<!-- ═══ 文件操作队列(常驻右下角,不打断操作,可展开/收起) ═══ -->
<Border x:Name="QueuePanel"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0,0,16,40"
Width="420"
MaxHeight="360"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Visibility="{x:Bind ViewModel.IsQueueExpanded, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Padding="12,8" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Style="{StaticResource BodyStrongTextBlockStyle}" Text="文件操作队列" VerticalAlignment="Center" />
<Button Grid.Column="1" Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="24"
ToolTipService.ToolTip="清除已完成" Click="OnClearFinishedJobsClick">
<FontIcon Glyph="{StaticResource GlyphClear}" FontSize="12" />
</Button>
<Button Grid.Column="2" Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="24"
ToolTipService.ToolTip="收起" Click="OnCollapseQueueClick">
<FontIcon Glyph="&#xE70D;" FontSize="12" />
</Button>
</Grid>
<ListView Grid.Row="1"
Padding="4,0,4,8"
SelectionMode="None"
ItemsSource="{x:Bind ViewModel.JobRows, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:JobRowViewModel">
<Grid Padding="8,6" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Spacing="3">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{x:Bind Title, Mode=OneWay}" Style="{StaticResource BodyStrongTextBlockStyle}" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{x:Bind StateText, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ProgressBar Value="{x:Bind Progress, Mode=OneWay}" Maximum="1"
IsIndeterminate="{x:Bind IsIndeterminate, Mode=OneWay}" />
<TextBlock Text="{x:Bind ProgressText, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{x:Bind CurrentItem, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
<Button Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="28"
Tag="{x:Bind}" Click="OnPauseJobClick"
Visibility="{x:Bind CanPause, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<FontIcon Glyph="{x:Bind PauseGlyph, Mode=OneWay}" FontSize="12" />
</Button>
<Button Style="{StaticResource ToolbarButtonStyle}" Width="28" Height="28"
Tag="{x:Bind}" Click="OnCancelJobClick"
Visibility="{x:Bind CanCancel, Mode=OneWay, Converter={StaticResource BoolToVisibility}}">
<FontIcon Glyph="{StaticResource GlyphStop}" FontSize="12" />
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</Grid>
</UserControl>
+237
View File
@@ -0,0 +1,237 @@
using System.Collections.Specialized;
using System.ComponentModel;
using FluidExplorer.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
namespace FluidExplorer.Views;
/// <summary>
/// 窗口内容宿主:标签栏(位于标题栏区域)+ 文件操作队列面板。
/// 标签容器由代码手工创建与同步,不依赖 TabView 的 ItemsSource 绑定(后者在增删标签时不稳定)。
/// </summary>
public sealed partial class ShellView : UserControl
{
private readonly Dictionary<ExplorerTabViewModel, TabViewItem> _containers = [];
private bool _syncingSelection;
public ShellView()
{
InitializeComponent();
}
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
nameof(ViewModel),
typeof(MainViewModel),
typeof(ShellView),
new PropertyMetadata(null, OnViewModelChanged));
public MainViewModel? ViewModel
{
get => (MainViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>标题栏拖动区(宿主窗口用 SetTitleBar 指向它)。</summary>
public FrameworkElement DragRegion => TitleBarDragRegion;
/// <summary>供主题切换使用的根元素。</summary>
public FrameworkElement ThemedRoot => RootGrid;
public void ShowOperationQueue()
{
if (ViewModel is not null) ViewModel.IsQueueExpanded = true;
QueuePanel.Visibility = Visibility.Visible;
}
// ── 标签页容器同步 ──────────────────────────────────────────────────────
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var shell = (ShellView)d;
if (e.OldValue is MainViewModel old)
{
old.Tabs.CollectionChanged -= shell.OnTabsChanged;
old.PropertyChanged -= shell.OnViewModelPropertyChanged;
}
if (e.NewValue is MainViewModel fresh)
{
fresh.Tabs.CollectionChanged += shell.OnTabsChanged;
fresh.PropertyChanged += shell.OnViewModelPropertyChanged;
shell.SyncAllTabs();
}
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(MainViewModel.SelectedTab) || ViewModel?.SelectedTab is not { } tab) return;
if (_containers.TryGetValue(tab, out var container) && !ReferenceEquals(TabHost.SelectedItem, container))
{
_syncingSelection = true;
TabHost.SelectedItem = container;
_syncingSelection = false;
}
}
private void OnTabsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
try
{
ApplyTabsChanged(e);
}
catch (Exception ex)
{
// 标签容器同步失败不能让异常逃进 WinRT 回调(会变成 stowed exception 结束进程)
App.Log($"标签页容器同步失败: {ex}");
}
}
private void ApplyTabsChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
SyncAllTabs();
return;
}
if (e.OldItems is not null)
{
foreach (ExplorerTabViewModel tab in e.OldItems)
{
if (_containers.Remove(tab, out var container)) TabHost.TabItems.Remove(container);
}
}
if (e.NewItems is not null)
{
foreach (ExplorerTabViewModel tab in e.NewItems) AddContainer(tab);
}
if (ViewModel?.SelectedTab is { } selected && _containers.TryGetValue(selected, out var target))
{
_syncingSelection = true;
TabHost.SelectedItem = target;
_syncingSelection = false;
}
}
private void SyncAllTabs()
{
TabHost.TabItems.Clear();
_containers.Clear();
if (ViewModel is null) return;
foreach (var tab in ViewModel.Tabs) AddContainer(tab);
if (ViewModel.SelectedTab is { } selected && _containers.TryGetValue(selected, out var target))
{
_syncingSelection = true;
TabHost.SelectedItem = target;
_syncingSelection = false;
}
}
private void AddContainer(ExplorerTabViewModel tab)
{
if (_containers.ContainsKey(tab)) return;
var header = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Center };
var icon = new FontIcon { FontSize = 14 };
if (Application.Current.Resources.TryGetValue("SymbolThemeFontFamily", out var family) && family is FontFamily fontFamily)
icon.FontFamily = fontFamily;
icon.SetBinding(FontIcon.GlyphProperty, new Binding
{
Path = new PropertyPath(nameof(ExplorerTabViewModel.Glyph)),
Source = tab,
Mode = BindingMode.OneWay
});
var text = new TextBlock { MaxWidth = 200, TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center };
text.SetBinding(TextBlock.TextProperty, new Binding
{
Path = new PropertyPath(nameof(ExplorerTabViewModel.Header)),
Source = tab,
Mode = BindingMode.OneWay
});
header.Children.Add(icon);
header.Children.Add(text);
var container = new TabViewItem
{
Header = header,
Content = new ExplorerTabView { TabViewModel = tab },
DataContext = tab
};
AutomationProperties.SetName(container, tab.Header);
_containers[tab] = container;
TabHost.TabItems.Add(container);
}
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_syncingSelection || ViewModel is null) return;
if (TabHost.SelectedItem is TabViewItem { DataContext: ExplorerTabViewModel tab })
ViewModel.SelectedTab = tab;
}
private void OnAddTabButtonClick(TabView sender, object args)
{
// 延后到 TabView 自身的点击/布局处理完成后再增删标签集合,并吞掉任何异常:
// 从 DispatcherQueue 回调里逃出去的异常会变成 stowed exception 直接结束进程。
DispatcherQueue.TryEnqueue(() =>
{
try
{
ViewModel?.NewTabCommand.Execute(null);
}
catch (Exception ex)
{
App.Log($"添加标签页失败: {ex}");
}
});
}
private void OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
{
if (ViewModel is null) return;
if (args.Item is TabViewItem { DataContext: ExplorerTabViewModel tab })
{
if (ViewModel.Tabs.Count <= 1)
{
App.MainWindowInstance.Close();
return;
}
ViewModel.CloseTabCommand.Execute(tab);
return;
}
App.MainWindowInstance.Close();
}
private void OnTabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args)
{
// 单窗口策略:拖出标签时仅保留在当前窗口,避免状态分散
if (args.Tab is TabViewItem { DataContext: ExplorerTabViewModel tab } && ViewModel is not null)
ViewModel.SelectedTab = tab;
}
private void OnClearFinishedJobsClick(object sender, RoutedEventArgs e) => ViewModel?.ClearFinishedJobsCommand.Execute(null);
private void OnCollapseQueueClick(object sender, RoutedEventArgs e)
{
if (ViewModel is not null) ViewModel.IsQueueExpanded = false;
QueuePanel.Visibility = Visibility.Collapsed;
}
private void OnPauseJobClick(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement { Tag: JobRowViewModel row }) ViewModel?.PauseJobCommand.Execute(row);
}
private void OnCancelJobClick(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement { Tag: JobRowViewModel row }) ViewModel?.CancelJobCommand.Execute(row);
}
}