Browse Source
* Init work with Toggle and RadioMenuItem * Add tests, fix couple of bugs, simplify IGroupRadioButton * Add more tests, specify ToggleType * Fix TrayIcon sync * Remove unused ToggleMenuItemCheckedCommand * Revert unused MenuItem.OnClick * Rename IGroupRadioButton to IRadioButton * Revert DevTools changes * Remove NativeMenuBar styles, unify Win32.TrayIcon and NativeMenuBar items generation --------- Co-authored-by: Steven Kirk <grokys@users.noreply.github.com>pull/14621/head
committed by
GitHub
28 changed files with 1015 additions and 407 deletions
@ -0,0 +1,22 @@ |
|||
namespace Avalonia.Controls; |
|||
|
|||
/// <summary>
|
|||
/// Defines how a <see cref="MenuItem"/> or <see cref="NativeMenuItem"/> reacts to clicks.
|
|||
/// </summary>
|
|||
public enum MenuItemToggleType |
|||
{ |
|||
/// <summary>
|
|||
/// Normal menu item.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Toggleable menu item with a checkbox.
|
|||
/// </summary>
|
|||
CheckBox, |
|||
|
|||
/// <summary>
|
|||
/// Menu item representing single option of radio group.
|
|||
/// </summary>
|
|||
Radio |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
using System; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Data; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Controls; |
|||
|
|||
internal class NativeMenuBarPresenter : Menu |
|||
{ |
|||
protected override Type StyleKeyOverride => typeof(Menu); |
|||
|
|||
internal static Control? CreateContainerForNativeItem(object? item, int index, object? recycleKey) |
|||
{ |
|||
if (item is NativeMenuItemSeparator) |
|||
{ |
|||
return new Separator(); |
|||
} |
|||
else if (item is NativeMenuItem nativeItem) |
|||
{ |
|||
var newItem = new NativeMenuItemPresenter |
|||
{ |
|||
ItemsSource = nativeItem.Menu?.Items, |
|||
[!HeaderedSelectingItemsControl.HeaderProperty] = |
|||
nativeItem.GetObservable(NativeMenuItem.HeaderProperty).ToBinding(), |
|||
[!MenuItem.IconProperty] = nativeItem.GetObservable(NativeMenuItem.IconProperty) |
|||
.Select(i => i is { } bitmap ? new Image { Source = bitmap } : null).ToBinding(), |
|||
[!MenuItem.IsEnabledProperty] = nativeItem.GetObservable(NativeMenuItem.IsEnabledProperty).ToBinding(), |
|||
[!MenuItem.CommandProperty] = nativeItem.GetObservable(NativeMenuItem.CommandProperty).ToBinding(), |
|||
[!MenuItem.CommandParameterProperty] = |
|||
nativeItem.GetObservable(NativeMenuItem.CommandParameterProperty).ToBinding(), |
|||
[!MenuItem.InputGestureProperty] = nativeItem.GetObservable(NativeMenuItem.GestureProperty).ToBinding(), |
|||
[!MenuItem.ToggleTypeProperty] = nativeItem.GetObservable(NativeMenuItem.ToggleTypeProperty) |
|||
// TODO12 remove NativeMenuItemToggleType
|
|||
.Select(v => (MenuItemToggleType)v).ToBinding() |
|||
}; |
|||
|
|||
BindingOperations.Apply(newItem, MenuItem.IsCheckedProperty, InstancedBinding.TwoWay( |
|||
nativeItem.GetObservable(NativeMenuItem.IsCheckedProperty).Select(v => (object)v), |
|||
new AnonymousObserver<object?>(v => nativeItem.SetValue(NativeMenuItem.IsCheckedProperty, v)))); |
|||
|
|||
newItem.Click += MenuItemOnClick; |
|||
|
|||
return newItem; |
|||
} |
|||
|
|||
return null; |
|||
|
|||
static void MenuItemOnClick(object? sender, RoutedEventArgs e) |
|||
{ |
|||
if (((MenuItem)sender!).DataContext is NativeMenuItem item |
|||
&& item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) |
|||
{ |
|||
bridge.RaiseClicked(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) |
|||
{ |
|||
return CreateContainerForNativeItem(item, index, recycleKey) |
|||
?? base.CreateContainerForItemOverride(item, index, recycleKey); |
|||
} |
|||
|
|||
private class NativeMenuItemPresenter : MenuItem |
|||
{ |
|||
protected override Type StyleKeyOverride => typeof(MenuItem); |
|||
|
|||
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) |
|||
{ |
|||
return CreateContainerForNativeItem(item, index, recycleKey) |
|||
?? base.CreateContainerForItemOverride(item, index, recycleKey); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia.Controls; |
|||
|
|||
internal interface IRadioButton : ILogical |
|||
{ |
|||
string? GroupName { get; } |
|||
MenuItemToggleType ToggleType { get; } |
|||
bool IsChecked { get; set; } |
|||
} |
|||
|
|||
internal class RadioButtonGroupManager |
|||
{ |
|||
private static readonly RadioButtonGroupManager s_default = new(); |
|||
private static readonly ConditionalWeakTable<IRenderRoot, RadioButtonGroupManager> s_registeredVisualRoots = new(); |
|||
|
|||
private readonly Dictionary<string, List<WeakReference<IRadioButton>>> _registeredGroups = new(); |
|||
private bool _ignoreCheckedChanges; |
|||
|
|||
public static RadioButtonGroupManager GetOrCreateForRoot(IRenderRoot? root) |
|||
{ |
|||
if (root == null) |
|||
return s_default; |
|||
return s_registeredVisualRoots.GetValue(root, key => new RadioButtonGroupManager()); |
|||
} |
|||
|
|||
public void Add(IRadioButton radioButton) |
|||
{ |
|||
var groupName = radioButton.GroupName; |
|||
if (groupName is not null && radioButton.ToggleType == MenuItemToggleType.Radio) |
|||
{ |
|||
if (!_registeredGroups.TryGetValue(groupName, out var group)) |
|||
{ |
|||
group = new List<WeakReference<IRadioButton>>(); |
|||
_registeredGroups.Add(groupName, group); |
|||
} |
|||
|
|||
group.Add(new WeakReference<IRadioButton>(radioButton)); |
|||
} |
|||
} |
|||
|
|||
public void Remove(IRadioButton radioButton, string? oldGroupName) |
|||
{ |
|||
if (!string.IsNullOrEmpty(oldGroupName) && _registeredGroups.TryGetValue(oldGroupName, out var group)) |
|||
{ |
|||
int i = 0; |
|||
while (i < group.Count) |
|||
{ |
|||
if (!group[i].TryGetTarget(out var button) || button == radioButton) |
|||
{ |
|||
group.RemoveAt(i); |
|||
continue; |
|||
} |
|||
|
|||
i++; |
|||
} |
|||
|
|||
if (group.Count == 0) |
|||
{ |
|||
_registeredGroups.Remove(oldGroupName); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void OnCheckedChanged(IRadioButton radioButton) |
|||
{ |
|||
if (_ignoreCheckedChanges || radioButton.ToggleType != MenuItemToggleType.Radio) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_ignoreCheckedChanges = true; |
|||
try |
|||
{ |
|||
var groupName = radioButton.GroupName; |
|||
if (!string.IsNullOrEmpty(groupName)) |
|||
{ |
|||
if (_registeredGroups.TryGetValue(groupName, out var group)) |
|||
{ |
|||
var i = 0; |
|||
while (i < group.Count) |
|||
{ |
|||
if (!group[i].TryGetTarget(out var current)) |
|||
{ |
|||
group.RemoveAt(i); |
|||
continue; |
|||
} |
|||
|
|||
if (current != radioButton && current.IsChecked) |
|||
current.IsChecked = false; |
|||
i++; |
|||
} |
|||
|
|||
if (group.Count == 0) |
|||
{ |
|||
_registeredGroups.Remove(groupName); |
|||
} |
|||
|
|||
var parent = radioButton.LogicalParent as IRadioButton; |
|||
while (parent is not null && parent.GroupName == groupName) |
|||
{ |
|||
parent.IsChecked = true; |
|||
parent = parent.LogicalParent as IRadioButton; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (radioButton.LogicalParent is { } parent) |
|||
{ |
|||
foreach (var sibling in parent.LogicalChildren) |
|||
{ |
|||
if (sibling != radioButton |
|||
&& sibling is IRadioButton { ToggleType: MenuItemToggleType.Radio } button |
|||
&& string.IsNullOrEmpty(button.GroupName) |
|||
&& button.IsChecked) |
|||
{ |
|||
button.IsChecked = false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreCheckedChanges = false; |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:local="using:Avalonia.Themes.Fluent" |
|||
x:ClassModifier="internal"> |
|||
<local:IBitmapToImageConverter x:Key="AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter"/> |
|||
<ControlTheme x:Key="{x:Type NativeMenuBar}" TargetType="NativeMenuBar"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Menu |
|||
IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}" |
|||
ItemsSource="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> |
|||
<Menu.Styles> |
|||
<Style Selector="MenuItem" x:DataType="NativeMenuItem"> |
|||
<Setter Property="Header" Value="{Binding Header}"/> |
|||
<Setter Property="ToolTip.Tip" Value="{Binding ToolTip}"/> |
|||
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> |
|||
<Setter Property="InputGesture" Value="{Binding Gesture}"/> |
|||
<Setter Property="ItemsSource" Value="{Binding Menu.Items}"/> |
|||
<Setter Property="Command" Value="{Binding Command}"/> |
|||
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> |
|||
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> |
|||
<!--NativeMenuItem is IBitmap and MenuItem is Image--> |
|||
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter}}"/> |
|||
</Style> |
|||
</Menu.Styles> |
|||
</Menu> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
@ -1,24 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media.Imaging; |
|||
|
|||
namespace Avalonia.Themes.Fluent |
|||
{ |
|||
internal class IBitmapToImageConverter : IValueConverter |
|||
{ |
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value != null && value is Bitmap bm) |
|||
return new Image { Source = bm }; |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:default="using:Avalonia.Themes.Simple" |
|||
x:ClassModifier="internal"> |
|||
<default:IBitmapToImageConverter x:Key="AvaloniaThemesSimpleNativeMenuBarIBitmapToImageConverter" /> |
|||
<ControlTheme x:Key="{x:Type NativeMenuBar}" |
|||
TargetType="NativeMenuBar"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Menu IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}" |
|||
ItemsSource="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> |
|||
<Menu.Styles> |
|||
<Style Selector="MenuItem" x:DataType="NativeMenuItem"> |
|||
<Setter Property="Header" Value="{Binding Header}"/> |
|||
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> |
|||
<Setter Property="InputGesture" Value="{Binding Gesture}"/> |
|||
<Setter Property="ItemsSource" Value="{Binding Menu.Items}"/> |
|||
<Setter Property="Command" Value="{Binding Command}"/> |
|||
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> |
|||
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> |
|||
<!--NativeMenuItem is IBitmap and MenuItem is Image--> |
|||
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesSimpleNativeMenuBarIBitmapToImageConverter}}"/> |
|||
</Style> |
|||
</Menu.Styles> |
|||
</Menu> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
@ -1,28 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media.Imaging; |
|||
|
|||
namespace Avalonia.Themes.Simple |
|||
{ |
|||
internal class IBitmapToImageConverter : IValueConverter |
|||
{ |
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value != null && value is Bitmap bm) |
|||
return new Image { Source=bm }; |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,66 +1,16 @@ |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
internal class Win32NativeToManagedMenuExporter : INativeMenuExporter |
|||
{ |
|||
private NativeMenu? _nativeMenu; |
|||
|
|||
public void SetNativeMenu(NativeMenu? nativeMenu) |
|||
{ |
|||
_nativeMenu = nativeMenu; |
|||
} |
|||
|
|||
private static AvaloniaList<MenuItem> Populate(NativeMenu nativeMenu) |
|||
{ |
|||
var result = new AvaloniaList<MenuItem>(); |
|||
|
|||
foreach (var menuItem in nativeMenu.Items) |
|||
{ |
|||
if (menuItem is NativeMenuItemSeparator) |
|||
{ |
|||
result.Add(new MenuItem { Header = "-" }); |
|||
} |
|||
else if (menuItem is NativeMenuItem item) |
|||
{ |
|||
var newItem = new MenuItem |
|||
{ |
|||
[!MenuItem.HeaderProperty] = item.GetObservable(NativeMenuItem.HeaderProperty).ToBinding(), |
|||
[!MenuItem.IconProperty] = item.GetObservable(NativeMenuItem.IconProperty) |
|||
.Select(i => i is {} bitmap ? new Image { Source = bitmap } : null).ToBinding(), |
|||
[!MenuItem.IsEnabledProperty] = item.GetObservable(NativeMenuItem.IsEnabledProperty).ToBinding(), |
|||
[!MenuItem.CommandProperty] = item.GetObservable(NativeMenuItem.CommandProperty).ToBinding(), |
|||
[!MenuItem.CommandParameterProperty] = item.GetObservable(NativeMenuItem.CommandParameterProperty).ToBinding(), |
|||
[!MenuItem.InputGestureProperty] = item.GetObservable(NativeMenuItem.GestureProperty).ToBinding() |
|||
}; |
|||
|
|||
if (item.Menu != null) |
|||
{ |
|||
newItem.ItemsSource = Populate(item.Menu); |
|||
} |
|||
else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) |
|||
{ |
|||
newItem.Click += (_, _) => bridge.RaiseClicked(); |
|||
} |
|||
namespace Avalonia.Win32; |
|||
|
|||
result.Add(newItem); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public AvaloniaList<MenuItem>? GetMenu() |
|||
{ |
|||
if (_nativeMenu != null) |
|||
{ |
|||
return Populate(_nativeMenu); |
|||
} |
|||
internal class Win32NativeToManagedMenuExporter : INativeMenuExporter |
|||
{ |
|||
private NativeMenu? _nativeMenu; |
|||
|
|||
return null; |
|||
} |
|||
public void SetNativeMenu(NativeMenu? nativeMenu) |
|||
{ |
|||
_nativeMenu = nativeMenu; |
|||
} |
|||
|
|||
internal NativeMenu? GetNativeMenu() => _nativeMenu; |
|||
} |
|||
|
|||
Loading…
Reference in new issue