diff --git a/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataEntry/RadioButtonViewModel.cs b/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataEntry/RadioButtonViewModel.cs index 3732fe5af..2e554e8da 100644 --- a/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataEntry/RadioButtonViewModel.cs +++ b/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataEntry/RadioButtonViewModel.cs @@ -1,4 +1,5 @@ using AtomUI.Controls; +using AtomUI.Desktop.Controls; using ReactiveUI; namespace AtomUIGallery.ShowCases.ViewModels; @@ -10,6 +11,14 @@ public class RadioButtonViewModel : ReactiveObject, IRoutableViewModel public IScreen HostScreen { get; } public string UrlPathSegment { get; } = ID.ToString(); + + private IList? _radioOptions; + + public IList? RadioOptions + { + get => _radioOptions; + set => this.RaiseAndSetIfChanged(ref _radioOptions, value); + } public RadioButtonViewModel(IScreen screen) { diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml index 6095a6010..6f3a6f4b6 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml @@ -6,11 +6,6 @@ xmlns:gallery="https://atomui.net/oss-controls/gallery" xmlns:showCase="clr-namespace:AtomUIGallery.ShowCases.Views"> - - - @@ -40,7 +35,7 @@ - + @@ -65,18 +60,24 @@ PieChart - + - + Option A Option B Option C Option D - + + + + + Pear Orange - + Apple Pear @@ -116,19 +117,19 @@ Linux Windows - + macOS Linux Windows - + macOS Linux Windows - + macOS Linux diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml.cs b/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml.cs index 9ea9a5ad9..96a9a01c5 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/RadioButtonShowCase.axaml.cs @@ -1,4 +1,5 @@ -using AtomUIGallery.ShowCases.ViewModels; +using AtomUI.Desktop.Controls; +using AtomUIGallery.ShowCases.ViewModels; using ReactiveUI; using ReactiveUI.Avalonia; @@ -8,9 +9,26 @@ public partial class RadioButtonShowCase : ReactiveUserControl { }); + this.WhenActivated(disposables => + { + if (DataContext is RadioButtonViewModel viewModel) + { + ConfigureRadioOptions(viewModel); + } + }); InitializeComponent(); } + + private void ConfigureRadioOptions(RadioButtonViewModel viewModel) + { + viewModel.RadioOptions = new List + { + new () { Content = "Option A"}, + new () { Content = "Option B", IsChecked = true}, + new () { Content = "Option C"}, + new () { Content = "Option D", IsEnabled = false}, + }; + } public void ToggleDisabledStatus(object arg) { diff --git a/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxGroup.cs b/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxGroup.cs new file mode 100644 index 000000000..19f457d08 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxGroup.cs @@ -0,0 +1,6 @@ +namespace AtomUI.Desktop.Controls; + +public class CheckBoxGroup +{ + +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxOption.cs b/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxOption.cs new file mode 100644 index 000000000..953b3a623 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/CheckBox/CheckBoxOption.cs @@ -0,0 +1,11 @@ +namespace AtomUI.Desktop.Controls; + +public interface ICheckBoxOption +{ + +} + +public class CheckBoxOption : ICheckBoxOption +{ + +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroup.cs b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroup.cs new file mode 100644 index 000000000..61037603e --- /dev/null +++ b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroup.cs @@ -0,0 +1,233 @@ +using System.Collections.Specialized; +using System.Reactive.Disposables; +using AtomUI.Controls; +using AtomUI.Data; +using AtomUI.Theme; +using AtomUI.Utils; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; + +namespace AtomUI.Desktop.Controls; + +public class RadioButtonGroup : ItemsControl, + IMotionAwareControl, + IControlSharedTokenResourcesHost +{ + #region 公共属性定义 + public static readonly StyledProperty CheckedItemProperty = + AvaloniaProperty.Register(nameof (CheckedItem)); + + public static readonly StyledProperty ItemSpacingProperty = + AvaloniaProperty.Register(nameof (ItemSpacing)); + + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(); + + public static readonly StyledProperty IsMotionEnabledProperty = + MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); + + public object? CheckedItem + { + get => GetValue(CheckedItemProperty); + set => SetValue(CheckedItemProperty, value); + } + + public double ItemSpacing + { + get => GetValue(ItemSpacingProperty); + set => SetValue(ItemSpacingProperty, value); + } + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public bool IsMotionEnabled + { + get => GetValue(IsMotionEnabledProperty); + set => SetValue(IsMotionEnabledProperty, value); + } + + #endregion + + #region 公共事件定义 + + public event EventHandler? CheckedChanged; + + #endregion + + #region 内部属性定义 + + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => RadioButtonToken.ID; + + #endregion + + private readonly Dictionary _itemsBindingDisposables = new(); + private bool _ignoreSyncChecked; + + static RadioButtonGroup() + { + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); + RadioButton.IsCheckedChangedEvent.AddClassHandler((group, args) => group.HandleRadioButtonCheckedChanged(args)); + CheckedItemProperty.Changed.AddClassHandler((group, args) => group.HandleCheckedItemChanged(args)); + } + + public RadioButtonGroup() + { + this.RegisterResources(); + LogicalChildren.CollectionChanged += HandleCollectionChanged; + } + + private void HandleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems.Count > 0) + { + foreach (var item in e.OldItems) + { + if (item is RadioButton radioButton) + { + if (_itemsBindingDisposables.TryGetValue(radioButton, out var disposable)) + { + disposable.Dispose(); + _itemsBindingDisposables.Remove(radioButton); + } + } + } + } + } + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new RadioButton(); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if (container is RadioButton radioButton) + { + var disposables = new CompositeDisposable(4); + + if (item != null && item is not Visual) + { + { + if (ItemTemplate != null) + { + radioButton.SetCurrentValue(RadioButton.ContentProperty, item); + } + else + { + if (item is IRadioButtonOption radioButtonOption) + { + radioButton.SetCurrentValue(RadioButton.ContentProperty, radioButtonOption.Content); + } + } + } + + { + if (item is IRadioButtonOption radioButtonOption) + { + radioButton.SetCurrentValue(RadioButton.IsEnabledProperty, radioButtonOption.IsEnabled); + radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, radioButtonOption.IsChecked); + } + } + if (ItemTemplate != null) + { + disposables.Add(BindUtils.RelayBind(this, ItemTemplateProperty, radioButton, RadioButton.ContentTemplateProperty)); + } + } + disposables.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, radioButton, RadioButton.IsMotionEnabledProperty)); + + PrepareRadioButton(radioButton, item, index, disposables); + + if (_itemsBindingDisposables.TryGetValue(radioButton, out var oldDisposables)) + { + oldDisposables.Dispose(); + _itemsBindingDisposables.Remove(radioButton); + } + _itemsBindingDisposables.Add(radioButton, disposables); + } + else + { + throw new ArgumentOutOfRangeException(nameof(container), "The container type is incorrect, it must be type RadioButton."); + } + } + + protected virtual void PrepareRadioButton(RadioButton radioButton, object? item, int index, CompositeDisposable disposables) + { + } + + private void HandleRadioButtonCheckedChanged(RoutedEventArgs args) + { + if (args.Source is RadioButton radioButton && radioButton.IsChecked == true) + { + _ignoreSyncChecked = true; + if (ItemsSource != null) + { + SetCurrentValue(CheckedItemProperty, radioButton.DataContext); + } + else + { + SetCurrentValue(CheckedItemProperty, radioButton); + } + } + } + + private void SyncCheckedState() + { + var isSourceMode = ItemsSource != null; + foreach (var radioButton in LogicalChildren.OfType()) + { + if (isSourceMode) + { + if (radioButton.DataContext == CheckedItem) + { + radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true); + } + } + else + { + if (radioButton == CheckedItem) + { + radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true); + } + } + } + } + + private void HandleCheckedItemChanged(AvaloniaPropertyChangedEventArgs args) + { + CheckedChanged?.Invoke(this, new RadioButtonGroupCheckedChangedEventArgs(args.OldValue, args.NewValue)); + if (_ignoreSyncChecked) + { + _ignoreSyncChecked = false; + return; + } + SyncCheckedState(); + } + + protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + base.ContainerForItemPreparedOverride(container, item, index); + if (container is RadioButton radioButton) + { + if (item == CheckedItem) + { + radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true); + } + } + } +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroupCheckedChangedEventArgs.cs b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroupCheckedChangedEventArgs.cs new file mode 100644 index 000000000..a4f095a55 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonGroupCheckedChangedEventArgs.cs @@ -0,0 +1,13 @@ +namespace AtomUI.Desktop.Controls; + +public class RadioButtonGroupCheckedChangedEventArgs : EventArgs +{ + public object? OldCheckedItem { get; } + public object? NewCheckedItem { get; } + + public RadioButtonGroupCheckedChangedEventArgs(object? oldCheckedItem, object? newCheckedItem) + { + OldCheckedItem = oldCheckedItem; + NewCheckedItem = newCheckedItem; + } +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonOption.cs b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonOption.cs new file mode 100644 index 000000000..7c6363dcd --- /dev/null +++ b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonOption.cs @@ -0,0 +1,15 @@ +namespace AtomUI.Desktop.Controls; + +public interface IRadioButtonOption +{ + bool IsEnabled { get; } + object? Content { get; } + bool IsChecked { get; } +} + +public class RadioButtonOption : IRadioButtonOption +{ + public bool IsEnabled { get; set; } = true; + public object? Content { get; set; } + public bool IsChecked { get; set; } = false; +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonToken.cs b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonToken.cs index 906458016..008d2923a 100644 --- a/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonToken.cs +++ b/src/AtomUI.Desktop.Controls/RadioButton/RadioButtonToken.cs @@ -59,6 +59,6 @@ internal class RadioButtonToken : AbstractControlDesignToken // internal RadioColor = colorWhite; RadioBgColor = colorPrimary; - TextMargin = new Thickness(SharedToken.UniformlyMarginXXS, 0, 0, 0); + TextMargin = new Thickness(SharedToken.UniformlyMarginXS, 0, SharedToken.UniformlyMarginXS, 0); } } \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonGroupTheme.axaml b/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonGroupTheme.axaml new file mode 100644 index 000000000..0a52b2844 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonGroupTheme.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonTheme.axaml b/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonTheme.axaml index dac8aaa01..1b7c5d61d 100644 --- a/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonTheme.axaml +++ b/src/AtomUI.Desktop.Controls/RadioButton/Themes/RadioButtonTheme.axaml @@ -41,7 +41,7 @@ Value="{DynamicResource {x:Static atom:SharedTokenKey.EnableWaveSpirit}}" />