From ea3f653d954db0cdc19a8b2dc55c0e1bc4bf28cb Mon Sep 17 00:00:00 2001 From: DarkFlameMaster <1004452714@qq.com> Date: Wed, 21 Jan 2026 15:23:02 +0800 Subject: [PATCH] =?UTF-8?q?JS=E7=9A=84=E8=AE=BE=E7=BD=AEUI=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=BA=A7=E8=81=94=E9=80=89=E6=8B=A9=20(#2667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BetterGenshinImpact/Model/SettingItem.cs | 34 ++- .../View/Controls/CascadeSelector.xaml | 100 +++++++ .../View/Controls/CascadeSelector.xaml.cs | 280 ++++++++++++++++++ 3 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 BetterGenshinImpact/View/Controls/CascadeSelector.xaml create mode 100644 BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs diff --git a/BetterGenshinImpact/Model/SettingItem.cs b/BetterGenshinImpact/Model/SettingItem.cs index fd79e80d..bdc9fa32 100644 --- a/BetterGenshinImpact/Model/SettingItem.cs +++ b/BetterGenshinImpact/Model/SettingItem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -6,6 +6,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; +using BetterGenshinImpact.View.Controls; namespace BetterGenshinImpact.Model; @@ -18,6 +19,8 @@ public class SettingItem public List? Options { get; set; } + public Dictionary>? CascadeOptions { get; set; } + public object? Default { get; set; } public List ToControl(dynamic context) @@ -171,6 +174,35 @@ public class SettingItem break; } + case "cascade-select": + { + if (CascadeOptions == null || CascadeOptions.Count == 0) + { + break; + } + + var cascadeSelector = new CascadeSelector + { + CascadeOptions = CascadeOptions, + DefaultValue = Default?.ToString(), + Margin = new Thickness(0, 0, 0, 10) + }; + + if (Default != null) + { + if (context is IDictionary ctx) + { + ctx.TryAdd(Name, Default.ToString()); + } + } + + BindingOperations.SetBinding(cascadeSelector, CascadeSelector.SelectedValueProperty, + new Binding(Name) { Source = context, Mode = BindingMode.TwoWay }); + + list.Add(cascadeSelector); + break; + } + default: throw new Exception($"Unknown setting type: {Type}"); } diff --git a/BetterGenshinImpact/View/Controls/CascadeSelector.xaml b/BetterGenshinImpact/View/Controls/CascadeSelector.xaml new file mode 100644 index 00000000..c0b874bb --- /dev/null +++ b/BetterGenshinImpact/View/Controls/CascadeSelector.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs b/BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs new file mode 100644 index 00000000..de66604e --- /dev/null +++ b/BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs @@ -0,0 +1,280 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace BetterGenshinImpact.View.Controls; + +public partial class CascadeSelector : UserControl +{ + private const double HorizontalMargin = 40; + private const double MinFirstLevelWidth = 100; + private const double MaxFirstLevelWidth = 300; + private const double MinSecondLevelWidth = 100; + private const double MaxSecondLevelWidth = 300; + private const double MinPopupWidth = 120; + private const double MaxPopupWidth = 600; + + public CascadeSelector() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + UpdateFirstLevelOptions(); + UpdatePopupWidth(); + } + + public Dictionary>? CascadeOptions + { + get { return (Dictionary>?)GetValue(CascadeOptionsProperty); } + set { SetValue(CascadeOptionsProperty, value); } + } + + public static readonly DependencyProperty CascadeOptionsProperty = + DependencyProperty.Register("CascadeOptions", typeof(Dictionary>), typeof(CascadeSelector), + new PropertyMetadata(null, OnCascadeOptionsChanged)); + + private static void OnCascadeOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (CascadeSelector)d; + control.UpdateFirstLevelOptions(); + } + + public List FirstLevelOptions + { + get { return (List)GetValue(FirstLevelOptionsProperty); } + set { SetValue(FirstLevelOptionsProperty, value); } + } + + public static readonly DependencyProperty FirstLevelOptionsProperty = + DependencyProperty.Register("FirstLevelOptions", typeof(List), typeof(CascadeSelector), new PropertyMetadata(null)); + + public List SecondLevelOptions + { + get { return (List)GetValue(SecondLevelOptionsProperty); } + set { SetValue(SecondLevelOptionsProperty, value); } + } + + public static readonly DependencyProperty SecondLevelOptionsProperty = + DependencyProperty.Register("SecondLevelOptions", typeof(List), typeof(CascadeSelector), + new PropertyMetadata(null, OnSecondLevelOptionsChanged)); + + private static void OnSecondLevelOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (CascadeSelector)d; + control.Dispatcher.BeginInvoke(() => + { + control.AdjustSecondLevelListWidth(); + }, System.Windows.Threading.DispatcherPriority.Render); + } + + public string? SelectedValue + { + get { return (string?)GetValue(SelectedValueProperty); } + set { SetValue(SelectedValueProperty, value); } + } + + public static readonly DependencyProperty SelectedValueProperty = + DependencyProperty.Register("SelectedValue", typeof(string), typeof(CascadeSelector), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedValueChanged)); + + private static void OnSelectedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (CascadeSelector)d; + control.HandleSelectedValueChanged((string?)e.NewValue); + } + + public string? DefaultValue + { + get { return (string?)GetValue(DefaultValueProperty); } + set { SetValue(DefaultValueProperty, value); } + } + + public static readonly DependencyProperty DefaultValueProperty = + DependencyProperty.Register("DefaultValue", typeof(string), typeof(CascadeSelector), new PropertyMetadata(null)); + + private double MeasureTextWidth(string text, double fontSize) + { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + + var formattedText = new FormattedText( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + fontSize, + Brushes.Black, + new NumberSubstitution(), + TextFormattingMode.Display, + VisualTreeHelper.GetDpi(this).PixelsPerDip); + return formattedText.WidthIncludingTrailingWhitespace; + } + + private void AdjustFirstLevelListWidth() + { + if (FirstLevelOptions == null || FirstLevelOptions.Count == 0) + { + return; + } + + double maxWidth = MinFirstLevelWidth; + foreach (var option in FirstLevelOptions) + { + double textWidth = MeasureTextWidth(option, FontSize) + HorizontalMargin; + if (textWidth > maxWidth) + { + maxWidth = textWidth; + } + } + + if (maxWidth > MaxFirstLevelWidth) + { + maxWidth = MaxFirstLevelWidth; + } + + var grid = PopupBorder?.Child as Grid; + var firstColumn = grid?.ColumnDefinitions[0]; + if (firstColumn != null) + { + firstColumn.Width = new GridLength(maxWidth); + } + } + + private void AdjustSecondLevelListWidth() + { + if (SecondLevelOptions == null || SecondLevelOptions.Count == 0) + { + UpdatePopupWidth(); + return; + } + + double maxWidth = MinSecondLevelWidth; + foreach (var option in SecondLevelOptions) + { + double textWidth = MeasureTextWidth(option, FontSize) + HorizontalMargin; + if (textWidth > maxWidth) + { + maxWidth = textWidth; + } + } + + if (maxWidth > MaxSecondLevelWidth) + { + maxWidth = MaxSecondLevelWidth; + } + + var grid = PopupBorder?.Child as Grid; + var thirdColumn = grid?.ColumnDefinitions[2]; + if (thirdColumn != null) + { + thirdColumn.Width = new GridLength(maxWidth); + } + + Dispatcher.BeginInvoke(() => UpdatePopupWidth(), System.Windows.Threading.DispatcherPriority.Render); + } + + private void UpdatePopupWidth() + { + var grid = PopupBorder?.Child as Grid; + if (grid == null || grid.ColumnDefinitions.Count < 3) + { + return; + } + + double totalWidth = 0; + + var firstColumn = grid.ColumnDefinitions[0]; + var secondColumn = grid.ColumnDefinitions[1]; + var thirdColumn = grid.ColumnDefinitions[2]; + + if (firstColumn.Width.IsAuto) + { + firstColumn.Width = new GridLength(MinFirstLevelWidth); + } + totalWidth += firstColumn.Width.Value; + + totalWidth += secondColumn.Width.Value; + totalWidth += thirdColumn.Width.Value; + + totalWidth += 8; + + if (totalWidth < MinPopupWidth) + { + totalWidth = MinPopupWidth; + } + if (totalWidth > MaxPopupWidth) + { + totalWidth = MaxPopupWidth; + } + + PopupBorder.Width = totalWidth; + } + + private void UpdateFirstLevelOptions() + { + if (CascadeOptions != null) + { + FirstLevelOptions = CascadeOptions.Keys.ToList(); + Dispatcher.BeginInvoke(() => + { + AdjustFirstLevelListWidth(); + UpdatePopupWidth(); + }, System.Windows.Threading.DispatcherPriority.Render); + } + else + { + FirstLevelOptions = new List(); + } + } + + private void HandleSelectedValueChanged(string? newValue) + { + if (string.IsNullOrEmpty(newValue) || CascadeOptions == null) + { + return; + } + + foreach (var kvp in CascadeOptions) + { + if (kvp.Value.Contains(newValue)) + { + FirstLevelListView.SelectedItem = kvp.Key; + SecondLevelOptions = kvp.Value; + SecondLevelListView.SelectedItem = newValue; + break; + } + } + } + + private void FirstLevelListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (FirstLevelListView.SelectedItem is string selectedFirstLevel) + { + if (CascadeOptions != null && CascadeOptions.TryGetValue(selectedFirstLevel, out var secondLevelOptions)) + { + SecondLevelOptions = secondLevelOptions; + SecondLevelListView.SelectedItem = null; + } + } + } + + private void SecondLevelListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (SecondLevelListView.SelectedItem is string selectedSecondLevel) + { + SelectedValue = selectedSecondLevel; + if (MainToggle.IsChecked == true) + { + MainToggle.IsChecked = false; + } + } + } +}