Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -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