Files

238 lines
8.3 KiB
C#

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