using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Media; using Sonex.Client.Dialogs; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; namespace Sonex.Client.Controls; [ContentProperty(Name = nameof(Items))] public sealed class TabView : UserControl { public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( nameof(SelectedItem), typeof(TabViewItem), typeof(TabView), new PropertyMetadata(null, OnSelectedItemChanged)); public static readonly DependencyProperty TabHeightProperty = DependencyProperty.Register( nameof(TabHeight), typeof(double), typeof(TabView), new PropertyMetadata(double.NaN, OnTabAppearanceChanged)); public static readonly DependencyProperty HeaderCommandBarProperty = DependencyProperty.Register( nameof(HeaderCommandBar), typeof(CommandBar), typeof(TabView), new PropertyMetadata(null, OnHeaderCommandBarChanged)); public static readonly DependencyProperty ItemTypeProperty = DependencyProperty.Register( nameof(ItemType), typeof(Type), typeof(TabView), new PropertyMetadata(null, OnItemTypeChanged)); private readonly TabViewItemCollection _items = []; private readonly List _trackedItems = []; private readonly Dictionary _contentPresenters = []; private readonly SelectorBar _selectorBar; private readonly Grid _contentHost; private readonly ContentPresenter _headerCommandBarPresenter; private readonly PointerEventHandler _tabPointerPressedHandler; private ContentPresenter? _visibleContentPresenter; private bool _isUpdatingSelection; private bool _isSelectionChangeRunning; public TabView() { _tabPointerPressedHandler = Item_PointerPressed; Grid layoutRoot = new() { RowSpacing = 0 }; layoutRoot.CornerRadius = new CornerRadius(10); layoutRoot.Padding = new Thickness(0); layoutRoot.BorderThickness = new Thickness(1); layoutRoot.Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush; layoutRoot.BorderBrush = Application.Current.Resources["CardStrokeColorDefaultBrush"] as Brush; layoutRoot.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); layoutRoot.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); Grid headerGrid = new() { ColumnSpacing = 8, Padding = new Thickness(8, 0, 0, 0), Height = 48d, BorderThickness = new Thickness(0, 0, 0, 1), BorderBrush = Application.Current.Resources["CardStrokeColorDefaultBrush"] as Brush }; headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); _selectorBar = new SelectorBar(); _selectorBar.Resources["SelectorBarItemPadding"] = new Thickness(0); _selectorBar.Resources["SelectorBarPadding"] = new Thickness(0); _selectorBar.Resources["SelectorBarItemPillWidth"] = 36d; _selectorBar.VerticalAlignment = VerticalAlignment.Center; _headerCommandBarPresenter = new ContentPresenter { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(_selectorBar, 0); Grid.SetColumn(_headerCommandBarPresenter, 1); headerGrid.Children.Add(_selectorBar); headerGrid.Children.Add(_headerCommandBarPresenter); _contentHost = new Grid { Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush }; Grid.SetRow(headerGrid, 0); Grid.SetRow(_contentHost, 1); layoutRoot.Children.Add(headerGrid); layoutRoot.Children.Add(_contentHost); base.Content = layoutRoot; _selectorBar.SelectionChanged += SelectorBar_SelectionChanged; _items.CollectionChanged += Items_CollectionChanged; UpdateHeaderCommandBar(); EnsureSelection(); UpdateSelectedContent(); } public TabViewItemCollection Items => _items; public TabViewItem? SelectedItem { get => (TabViewItem?)GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } public double TabHeight { get => (double)GetValue(TabHeightProperty); set => SetValue(TabHeightProperty, value); } public CommandBar? HeaderCommandBar { get => (CommandBar?)GetValue(HeaderCommandBarProperty); set => SetValue(HeaderCommandBarProperty, value); } public Type? ItemType { get => (Type?)GetValue(ItemTypeProperty); set => SetValue(ItemTypeProperty, value); } public object? CurrentItem { get; private set; } public int SelectedIndex { get => SelectedItem is null ? -1 : Items.IndexOf(SelectedItem); set => SelectedItem = value >= 0 && value < Items.Count ? Items[value] : null; } public event EventHandler? SelectionChanged; public void Load(object? item) { CurrentItem = item; Load(); } public void Load() { SelectedItem?.Load(CurrentItem); } private static void OnSelectedItemChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) { if (dependencyObject is TabView tabView) { _ = tabView.HandleSelectedItemChangedAsync((TabViewItem?)args.OldValue, (TabViewItem?)args.NewValue); } } private static void OnTabAppearanceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs _) { if (dependencyObject is TabView tabView) { tabView.ApplyTabAppearance(); } } private static void OnHeaderCommandBarChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs _) { if (dependencyObject is TabView tabView) { tabView.UpdateHeaderCommandBar(); } } private static void OnItemTypeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs _) { if (dependencyObject is TabView tabView) { tabView.ApplyItemType(); } } private async Task HandleSelectedItemChangedAsync(TabViewItem? oldItem, TabViewItem? newItem) { if (_isUpdatingSelection) { return; } if (_isSelectionChangeRunning) { RevertSelection(oldItem); return; } if (newItem is not null && !Items.Contains(newItem)) { RevertSelection(oldItem); return; } _isSelectionChangeRunning = true; try { if (!await CanLeaveCurrentViewAsync(oldItem)) { RevertSelection(oldItem); return; } UpdateSelectorBarSelection(); UpdateSelectedContent(); if (!ReferenceEquals(oldItem, newItem)) { Load(); RaiseItemSelected(newItem); SelectionChanged?.Invoke(this, EventArgs.Empty); } } finally { _isSelectionChangeRunning = false; } } private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs _) { RebuildTrackedItems(); SyncItems(); SyncContentPresenters(); EnsureSelection(); UpdateSelectedContent(); Load(); } private void RebuildTrackedItems() { foreach (TabViewItem item in _trackedItems) { item.ViewChanged -= Item_ViewChanged; item.RemoveHandler(PointerPressedEvent, _tabPointerPressedHandler); } _trackedItems.Clear(); foreach (TabViewItem item in Items) { item.ViewChanged += Item_ViewChanged; item.AddHandler(PointerPressedEvent, _tabPointerPressedHandler, true); _trackedItems.Add(item); } } private void SyncItems() { _selectorBar.Items.Clear(); foreach (TabViewItem item in Items) { ApplyTabAppearance(item); ApplyItemType(item); _selectorBar.Items.Add(item); } } private void SyncContentPresenters() { _visibleContentPresenter = null; _contentPresenters.Clear(); _contentHost.Children.Clear(); foreach (TabViewItem item in Items) { ContentPresenter presenter = new() { Content = item.View, Visibility = Visibility.Collapsed }; _contentPresenters[item] = presenter; _contentHost.Children.Add(presenter); } } private void ApplyTabAppearance() { foreach (TabViewItem item in Items) { ApplyTabAppearance(item); } } private void ApplyTabAppearance(TabViewItem item) { if (double.IsNaN(TabHeight) || TabHeight <= 0) { item.ClearValue(HeightProperty); return; } item.Height = TabHeight; } private void ApplyItemType() { foreach (TabViewItem item in Items) { ApplyItemType(item); } } private void ApplyItemType(TabViewItem item) { item.ItemType = ItemType; } private void EnsureSelection() { if (SelectedItem is not null && Items.Contains(SelectedItem)) { UpdateSelectorBarSelection(); return; } _isUpdatingSelection = true; SetValue(SelectedItemProperty, Items.Count > 0 ? Items[0] : null); _isUpdatingSelection = false; UpdateSelectorBarSelection(); } private void RevertSelection(TabViewItem? item) { _isUpdatingSelection = true; SetValue(SelectedItemProperty, item); _selectorBar.SelectedItem = item; _isUpdatingSelection = false; UpdateSelectedContent(); } private void UpdateSelectorBarSelection() { _isUpdatingSelection = true; _selectorBar.SelectedItem = SelectedItem; _isUpdatingSelection = false; } private void UpdateSelectedContent() { if (SelectedItem is null || !_contentPresenters.TryGetValue(SelectedItem, out ContentPresenter? selectedPresenter)) { if (_visibleContentPresenter is not null) { _visibleContentPresenter.Visibility = Visibility.Collapsed; _visibleContentPresenter = null; } return; } selectedPresenter.Content = SelectedItem.View; if (!ReferenceEquals(_visibleContentPresenter, selectedPresenter)) { if (_visibleContentPresenter is not null) { _visibleContentPresenter.Visibility = Visibility.Collapsed; } selectedPresenter.Visibility = Visibility.Visible; _visibleContentPresenter = selectedPresenter; } } private void UpdateHeaderCommandBar() { if (HeaderCommandBar != null) { HeaderCommandBar.DefaultLabelPosition = CommandBarDefaultLabelPosition.Right; } _headerCommandBarPresenter.Content = HeaderCommandBar; _headerCommandBarPresenter.Visibility = HeaderCommandBar is null ? Visibility.Collapsed : Visibility.Visible; } private void Item_ViewChanged(object? sender, EventArgs e) { if (sender is not TabViewItem item) { return; } if (_contentPresenters.TryGetValue(item, out ContentPresenter? presenter)) { presenter.Content = item.View; } if (ReferenceEquals(item, SelectedItem)) { UpdateSelectedContent(); } } private async void Item_PointerPressed(object sender, PointerRoutedEventArgs e) { if (sender is not TabViewItem item) { return; } var pointerPoint = e.GetCurrentPoint(item); if (!pointerPoint.Properties.IsLeftButtonPressed) { return; } e.Handled = true; await SelectItemAsync(item); } private async void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) { await SelectItemAsync(sender.SelectedItem as TabViewItem); } private async Task SelectItemAsync(TabViewItem? selectedItem) { if (_isUpdatingSelection) { return; } if (ReferenceEquals(SelectedItem, selectedItem)) { return; } if (_isSelectionChangeRunning) { UpdateSelectorBarSelection(); return; } _isSelectionChangeRunning = true; var oldItem = SelectedItem; try { if (!await CanLeaveCurrentViewAsync(oldItem)) { UpdateSelectorBarSelection(); return; } _isUpdatingSelection = true; SetValue(SelectedItemProperty, selectedItem); _isUpdatingSelection = false; UpdateSelectorBarSelection(); UpdateSelectedContent(); Load(); RaiseItemSelected(selectedItem); SelectionChanged?.Invoke(this, EventArgs.Empty); } finally { _isUpdatingSelection = false; _isSelectionChangeRunning = false; } } private async Task CanLeaveCurrentViewAsync(TabViewItem? item) { if (item?.View?.HasChanges != true) { return true; } var result = await this.ShowQuestionAsync( "Unsaved changes", "You have unsaved changes. Are you sure you want to leave this page?", "Yes", "No"); return result == ContentDialogResult.Primary; } private void RaiseItemSelected(TabViewItem? item) { item?.RaiseOnSelect(); } }