Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -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="" 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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="" 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user