mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-03-15 07:43:20 +08:00
JS的设置UI添加级联选择 (#2667)
This commit is contained in:
@@ -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<string>? Options { get; set; }
|
||||
|
||||
public Dictionary<string, List<string>>? CascadeOptions { get; set; }
|
||||
|
||||
public object? Default { get; set; }
|
||||
|
||||
public List<UIElement> 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<string, object?> 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}");
|
||||
}
|
||||
|
||||
100
BetterGenshinImpact/View/Controls/CascadeSelector.xaml
Normal file
100
BetterGenshinImpact/View/Controls/CascadeSelector.xaml
Normal file
@@ -0,0 +1,100 @@
|
||||
<UserControl x:Class="BetterGenshinImpact.View.Controls.CascadeSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="40" d:DesignWidth="300"
|
||||
Name="Root">
|
||||
<Grid>
|
||||
<ToggleButton x:Name="MainToggle"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Padding="12,6"
|
||||
Height="34"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1">
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding SelectedValue, ElementName=Root, TargetNullValue='请选择', FallbackValue='请选择'}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0,0,0"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="1" Text="▼" FontSize="10"
|
||||
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
</ToggleButton>
|
||||
|
||||
<Popup x:Name="MainPopup"
|
||||
IsOpen="{Binding IsChecked, ElementName=MainToggle, Mode=TwoWay}"
|
||||
PlacementTarget="{Binding ElementName=MainToggle}"
|
||||
StaysOpen="False"
|
||||
AllowsTransparency="True"
|
||||
PopupAnimation="Slide">
|
||||
<Border x:Name="PopupBorder"
|
||||
Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Margin="0,4,0,0"
|
||||
Padding="4"
|
||||
MinHeight="100"
|
||||
MaxHeight="300"
|
||||
MinWidth="120"
|
||||
MaxWidth="600">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Direction="270" Color="Black" Opacity="0.2"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="100"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto" MinWidth="100"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:ListView x:Name="FirstLevelListView"
|
||||
ItemsSource="{Binding FirstLevelOptions, ElementName=Root}"
|
||||
BorderThickness="0"
|
||||
SelectionChanged="FirstLevelListView_SelectionChanged"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ui:ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" Margin="8,4"/>
|
||||
</DataTemplate>
|
||||
</ui:ListView.ItemTemplate>
|
||||
</ui:ListView>
|
||||
|
||||
<Rectangle Grid.Column="1" Width="1"
|
||||
Fill="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
Margin="2,0"/>
|
||||
|
||||
<ui:ListView Grid.Column="2" x:Name="SecondLevelListView"
|
||||
ItemsSource="{Binding SecondLevelOptions, ElementName=Root}"
|
||||
BorderThickness="0"
|
||||
SelectionChanged="SecondLevelListView_SelectionChanged"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ui:ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" Margin="8,4"/>
|
||||
</DataTemplate>
|
||||
</ui:ListView.ItemTemplate>
|
||||
</ui:ListView>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
280
BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs
Normal file
280
BetterGenshinImpact/View/Controls/CascadeSelector.xaml.cs
Normal file
@@ -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<string, List<string>>? CascadeOptions
|
||||
{
|
||||
get { return (Dictionary<string, List<string>>?)GetValue(CascadeOptionsProperty); }
|
||||
set { SetValue(CascadeOptionsProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CascadeOptionsProperty =
|
||||
DependencyProperty.Register("CascadeOptions", typeof(Dictionary<string, List<string>>), typeof(CascadeSelector),
|
||||
new PropertyMetadata(null, OnCascadeOptionsChanged));
|
||||
|
||||
private static void OnCascadeOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (CascadeSelector)d;
|
||||
control.UpdateFirstLevelOptions();
|
||||
}
|
||||
|
||||
public List<string> FirstLevelOptions
|
||||
{
|
||||
get { return (List<string>)GetValue(FirstLevelOptionsProperty); }
|
||||
set { SetValue(FirstLevelOptionsProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty FirstLevelOptionsProperty =
|
||||
DependencyProperty.Register("FirstLevelOptions", typeof(List<string>), typeof(CascadeSelector), new PropertyMetadata(null));
|
||||
|
||||
public List<string> SecondLevelOptions
|
||||
{
|
||||
get { return (List<string>)GetValue(SecondLevelOptionsProperty); }
|
||||
set { SetValue(SecondLevelOptionsProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SecondLevelOptionsProperty =
|
||||
DependencyProperty.Register("SecondLevelOptions", typeof(List<string>), 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<string>();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user