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; /// /// 窗口内容宿主:标签栏(位于标题栏区域)+ 文件操作队列面板。 /// 标签容器由代码手工创建与同步,不依赖 TabView 的 ItemsSource 绑定(后者在增删标签时不稳定)。 /// public sealed partial class ShellView : UserControl { private readonly Dictionary _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); } /// 标题栏拖动区(宿主窗口用 SetTitleBar 指向它)。 public FrameworkElement DragRegion => TitleBarDragRegion; /// 供主题切换使用的根元素。 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); } }