Compare commits
10 Commits
b57223eb1b
...
fe060db106
| Author | SHA1 | Date | |
|---|---|---|---|
| fe060db106 | |||
| ab5f8fca74 | |||
| 0fb0eb8d61 | |||
| f8a1f2e2aa | |||
| f4b5dc95b7 | |||
| 1e7f817547 | |||
| 2185ce04e7 | |||
| d9a6ae517f | |||
| 79239b5842 | |||
| a48c415026 |
@@ -25,6 +25,60 @@
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.BindingPlugins</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.DataValidationBase</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.ExceptionValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IDataValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IndeiValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IPropertyAccessorPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IStreamPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.PropertyAccessorBase</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.PropertyError</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Diagnostics.AppliedStyle</Target>
|
||||
@@ -253,6 +307,60 @@
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.BindingPlugins</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.DataValidationBase</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.ExceptionValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IDataValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IndeiValidationPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IPropertyAccessorPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.IStreamPlugin</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.PropertyAccessorBase</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Data.Core.Plugins.PropertyError</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:Avalonia.Diagnostics.AppliedStyle</Target>
|
||||
@@ -1111,6 +1219,12 @@
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.AccessText.get_AccessKey</Target>
|
||||
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
|
||||
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.OverlayPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
|
||||
@@ -2023,6 +2137,12 @@
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.AccessText.get_AccessKey</Target>
|
||||
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
|
||||
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.OverlayPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
|
||||
|
||||
@@ -96,8 +96,8 @@
|
||||
case AutomationEdit: return NSAccessibilityTextFieldRole;
|
||||
case AutomationHyperlink: return NSAccessibilityLinkRole;
|
||||
case AutomationImage: return NSAccessibilityImageRole;
|
||||
case AutomationListItem: return NSAccessibilityRowRole;
|
||||
case AutomationList: return NSAccessibilityTableRole;
|
||||
case AutomationListItem: return NSAccessibilityGroupRole;
|
||||
case AutomationList: return NSAccessibilityListRole;
|
||||
case AutomationMenu: return NSAccessibilityMenuBarRole;
|
||||
case AutomationMenuBar: return NSAccessibilityMenuBarRole;
|
||||
case AutomationMenuItem: return NSAccessibilityMenuItemRole;
|
||||
@@ -106,7 +106,7 @@
|
||||
case AutomationScrollBar: return NSAccessibilityScrollBarRole;
|
||||
case AutomationSlider: return NSAccessibilitySliderRole;
|
||||
case AutomationSpinner: return NSAccessibilityIncrementorRole;
|
||||
case AutomationStatusBar: return NSAccessibilityTableRole;
|
||||
case AutomationStatusBar: return NSAccessibilityGroupRole;
|
||||
case AutomationTab: return NSAccessibilityTabGroupRole;
|
||||
case AutomationTabItem: return NSAccessibilityRadioButtonRole;
|
||||
case AutomationText: return NSAccessibilityStaticTextRole;
|
||||
@@ -137,6 +137,11 @@
|
||||
|
||||
- (NSAccessibilitySubrole)accessibilitySubrole
|
||||
{
|
||||
auto controlType = _peer->GetAutomationControlType();
|
||||
switch (controlType) {
|
||||
case AutomationList: return @"AXContentList";
|
||||
}
|
||||
|
||||
auto landmarkType = _peer->GetLandmarkType();
|
||||
switch (landmarkType) {
|
||||
case LandmarkBanner: return @"AXLandmarkBanner";
|
||||
@@ -147,8 +152,9 @@
|
||||
case LandmarkMain: return @"AXLandmarkMain";
|
||||
case LandmarkNavigation: return @"AXLandmarkNavigation";
|
||||
case LandmarkSearch: return @"AXLandmarkSearch";
|
||||
default: return NSAccessibilityUnknownSubrole;
|
||||
}
|
||||
|
||||
return NSAccessibilityUnknownSubrole;
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityRoleDescription
|
||||
|
||||
@@ -44,21 +44,31 @@
|
||||
VerticalContentAlignment="Center" VerticalAlignment="Stretch"
|
||||
Width="200" Height="42">
|
||||
<AdornerLayer.Adorner>
|
||||
<Canvas x:Name="AdornerCanvas"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Cyan"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0.3"
|
||||
IsVisible="True"
|
||||
AdornerLayer.IsClipEnabled="False">
|
||||
<Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
|
||||
</Canvas>
|
||||
<Border AdornerLayer.IsClipEnabled="False" ClipToBounds="False" Background="Red" Opacity="0.3" Margin="-5" Padding="5"
|
||||
IsHitTestVisible="False">
|
||||
<Canvas
|
||||
x:Name="AdornerCanvas"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Cyan"
|
||||
IsHitTestVisible="False"
|
||||
IsVisible="True"
|
||||
AdornerLayer.IsClipEnabled="False">
|
||||
<Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
|
||||
<Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
</AdornerLayer.Adorner>
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu PlacementTarget="{ResolveByName AdornerCanvas}" Placement="TopEdgeAlignedLeft">
|
||||
<MenuItem Header="Menu 1"/>
|
||||
<MenuItem Header="Menu 2"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
|
||||
</LayoutTransformControl>
|
||||
</Grid>
|
||||
|
||||
@@ -135,10 +135,30 @@ namespace Avalonia.Android
|
||||
protected override bool OnPerformActionForVirtualView(int virtualViewId, int action, Bundle? arguments)
|
||||
{
|
||||
return (GetNodeInfoProvidersFromVirtualViewId(virtualViewId) ?? [])
|
||||
.Select(x => x.PerformNodeAction(action, arguments))
|
||||
.Select(x => TryPerformNodeAction(x, action, arguments))
|
||||
.Aggregate(false, (a, b) => a | b);
|
||||
}
|
||||
|
||||
private static bool TryPerformNodeAction(INodeInfoProvider nodeInfoProvider, int action, Bundle? arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
return nodeInfoProvider.PerformNodeAction(action, arguments);
|
||||
}
|
||||
catch (ElementNotEnabledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat? nodeInfo)
|
||||
{
|
||||
if (nodeInfo is null || !_peers.TryGetValue(virtualViewId, out AutomationPeer? peer))
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// Holds a registry of plugins used for bindings.
|
||||
/// </summary>
|
||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
||||
public static class BindingPlugins
|
||||
internal static class BindingPlugins
|
||||
{
|
||||
internal static readonly List<IPropertyAccessorPlugin> s_propertyAccessors = new()
|
||||
{
|
||||
@@ -17,7 +17,6 @@ namespace Avalonia.Data.Core.Plugins
|
||||
|
||||
internal static readonly List<IDataValidationPlugin> s_dataValidators = new()
|
||||
{
|
||||
new DataAnnotationsValidationPlugin(),
|
||||
new IndeiValidationPlugin(),
|
||||
new ExceptionValidationPlugin(),
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// and convert any values received from the inner property accessor into
|
||||
/// <see cref="BindingNotification"/>s.
|
||||
/// </remarks>
|
||||
public abstract class DataValidationBase : PropertyAccessorBase, IObserver<object?>
|
||||
internal abstract class DataValidationBase : PropertyAccessorBase, IObserver<object?>
|
||||
{
|
||||
private readonly IPropertyAccessor _inner;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Validates properties that report errors by throwing exceptions.
|
||||
/// </summary>
|
||||
public class ExceptionValidationPlugin : IDataValidationPlugin
|
||||
internal class ExceptionValidationPlugin : IDataValidationPlugin
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Match(WeakReference<object?> reference, string memberName) => true;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Defines how data validation is observed by an <see cref="BindingExpression"/>.
|
||||
/// </summary>
|
||||
public interface IDataValidationPlugin
|
||||
internal interface IDataValidationPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether this plugin can handle data validation on the specified object.
|
||||
|
||||
@@ -3,8 +3,7 @@ using System;
|
||||
namespace Avalonia.Data.Core.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an accessor to a property on an object returned by a
|
||||
/// <see cref="IPropertyAccessorPlugin"/>
|
||||
/// Defines an accessor to a property on an object.
|
||||
/// </summary>
|
||||
public interface IPropertyAccessor : IDisposable
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Defines how a member is read, written and observed by a binding.
|
||||
/// </summary>
|
||||
public interface IPropertyAccessorPlugin
|
||||
internal interface IPropertyAccessorPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether this plugin can handle accessing the properties of the specified object.
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Defines a plugin that handles the '^' stream binding operator.
|
||||
/// </summary>
|
||||
public interface IStreamPlugin
|
||||
internal interface IStreamPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether this plugin handles the specified value.
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
|
||||
/// </summary>
|
||||
public class IndeiValidationPlugin : IDataValidationPlugin
|
||||
internal class IndeiValidationPlugin : IDataValidationPlugin
|
||||
{
|
||||
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
|
||||
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
|
||||
/// </summary>
|
||||
public abstract class PropertyAccessorBase : IPropertyAccessor
|
||||
internal abstract class PropertyAccessorBase : IPropertyAccessor
|
||||
{
|
||||
private Action<object?>? _listener;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins
|
||||
/// <summary>
|
||||
/// An <see cref="IPropertyAccessor"/> that represents an error.
|
||||
/// </summary>
|
||||
public class PropertyError : IPropertyAccessor
|
||||
internal class PropertyError : IPropertyAccessor
|
||||
{
|
||||
private readonly BindingNotification _error;
|
||||
|
||||
|
||||
@@ -122,9 +122,11 @@ namespace Avalonia.Input
|
||||
/// </summary>
|
||||
/// <param name="accessKey">The access key.</param>
|
||||
/// <param name="element">The input element.</param>
|
||||
public void Register(char accessKey, IInputElement element)
|
||||
public void Register(string accessKey, IInputElement element)
|
||||
{
|
||||
var key = NormalizeKey(accessKey.ToString());
|
||||
ArgumentException.ThrowIfNullOrEmpty(accessKey);
|
||||
|
||||
var key = NormalizeKey(accessKey);
|
||||
|
||||
// remove dead elements with matching key
|
||||
for (var i = _registrations.Count - 1; i >= 0; i--)
|
||||
@@ -224,7 +226,7 @@ namespace Avalonia.Input
|
||||
MainMenu?.IsOpen != true)
|
||||
return;
|
||||
|
||||
e.Handled = ProcessKey(e.Key.ToString(), e.Source as IInputElement);
|
||||
e.Handled = ProcessKey(e.KeySymbol, e.Source as IInputElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -287,8 +289,11 @@ namespace Avalonia.Input
|
||||
/// <param name="key">The access key to process.</param>
|
||||
/// <param name="element">The element to get the targets which are in scope.</param>
|
||||
/// <returns>If there matches <c>true</c>, otherwise <c>false</c>.</returns>
|
||||
protected bool ProcessKey(string key, IInputElement? element)
|
||||
protected bool ProcessKey(string? key, IInputElement? element)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return false;
|
||||
|
||||
key = NormalizeKey(key);
|
||||
var senderInfo = GetTargetForElement(element, key);
|
||||
// Find the possible targets matching the access key
|
||||
@@ -503,20 +508,11 @@ namespace Avalonia.Input
|
||||
/// </summary>
|
||||
internal class AccessKeyPressedEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The constructor for AccessKeyPressed event args
|
||||
/// </summary>
|
||||
public AccessKeyPressedEventArgs()
|
||||
{
|
||||
RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
|
||||
Key = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for AccessKeyPressed event args
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
public AccessKeyPressedEventArgs(string key) : this()
|
||||
public AccessKeyPressedEventArgs(string key)
|
||||
{
|
||||
RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
|
||||
Key = key;
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Avalonia.Input
|
||||
/// </summary>
|
||||
/// <param name="accessKey">The access key.</param>
|
||||
/// <param name="element">The input element.</param>
|
||||
void Register(char accessKey, IInputElement element);
|
||||
void Register(string accessKey, IInputElement element);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the access keys associated with the input element.
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Avalonia.Input.TextInput
|
||||
private IInputElement? _focusedElement;
|
||||
private Interactive? _visualRoot;
|
||||
private TextInputMethodClient? _client;
|
||||
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
|
||||
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(true);
|
||||
|
||||
public TextInputMethodManager()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
@@ -7,13 +9,15 @@ namespace Avalonia.Input.TextInput
|
||||
{
|
||||
class TransformTrackingHelper : IDisposable
|
||||
{
|
||||
private readonly bool _deferAfterRenderPass;
|
||||
private Visual? _visual;
|
||||
private bool _queuedForUpdate;
|
||||
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
|
||||
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
|
||||
|
||||
public TransformTrackingHelper()
|
||||
public TransformTrackingHelper(bool deferAfterRenderPass)
|
||||
{
|
||||
_deferAfterRenderPass = deferAfterRenderPass;
|
||||
_propertyChangedHandler = PropertyChangedHandler;
|
||||
}
|
||||
|
||||
@@ -24,7 +28,7 @@ namespace Avalonia.Input.TextInput
|
||||
if (visual != null)
|
||||
{
|
||||
visual.AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
visual.DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
if (visual.IsAttachedToVisualTree)
|
||||
SubscribeToParents();
|
||||
UpdateMatrix();
|
||||
@@ -70,6 +74,7 @@ namespace Avalonia.Input.TextInput
|
||||
|
||||
void UpdateMatrix()
|
||||
{
|
||||
_queuedForUpdate = false;
|
||||
Matrix? matrix = null;
|
||||
if (_visual != null && _visual.VisualRoot != null)
|
||||
matrix = _visual.TransformToVisual((Visual)_visual.VisualRoot);
|
||||
@@ -91,7 +96,10 @@ namespace Avalonia.Input.TextInput
|
||||
if(_queuedForUpdate)
|
||||
return;
|
||||
_queuedForUpdate = true;
|
||||
Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.AfterRender);
|
||||
if (_deferAfterRenderPass)
|
||||
Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.AfterRender);
|
||||
else
|
||||
MediaContext.Instance.BeginInvokeOnRender(UpdateMatrix);
|
||||
}
|
||||
|
||||
private void PropertyChangedHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
@@ -106,12 +114,23 @@ namespace Avalonia.Input.TextInput
|
||||
UpdateMatrix();
|
||||
}
|
||||
|
||||
public static IDisposable Track(Visual visual, Action<Visual, Matrix?> cb)
|
||||
public static IDisposable Track(Visual visual, bool deferAfterRenderPass, Action<Visual, Matrix?> cb)
|
||||
{
|
||||
var rv = new TransformTrackingHelper();
|
||||
var rv = new TransformTrackingHelper(deferAfterRenderPass);
|
||||
rv.MatrixChanged += () => cb(visual, rv.Matrix);
|
||||
rv.SetVisual(visual);
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static IObservable<Matrix?> Observe(Visual visual, bool deferAfterRenderPass)
|
||||
{
|
||||
return Observable.Create<Matrix?>(observer =>
|
||||
{
|
||||
var rv = new TransformTrackingHelper(deferAfterRenderPass);
|
||||
rv.MatrixChanged += () => observer.OnNext(rv.Matrix);
|
||||
rv.SetVisual(visual);
|
||||
return rv;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -46,20 +46,25 @@ partial class ServerCompositionVisual
|
||||
public void UpdateAdorner()
|
||||
{
|
||||
GetAttHelper().EnqueuedForAdornerUpdate = false;
|
||||
var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
|
||||
RotationAngle, Orientation, Offset);
|
||||
|
||||
|
||||
if (AdornedVisual != null && Parent != null)
|
||||
{
|
||||
// We ignore Visual's RenderTransform completely since it's set by AdornerLayer and can be out of sync
|
||||
// with compositor-driver animations
|
||||
var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, Matrix.Identity, Scale,
|
||||
RotationAngle, Orientation, Offset);
|
||||
if (
|
||||
AdornerLayer_GetExpectedSharedAncestor(this) is {} sharedAncestor
|
||||
&& ComputeTransformFromAncestor(AdornedVisual, sharedAncestor, out var adornerLayerToAdornedVisual))
|
||||
ownTransform = (ownTransform ?? Matrix.Identity) * adornerLayerToAdornedVisual;
|
||||
_ownTransform = (ownTransform ?? Matrix.Identity) * adornerLayerToAdornedVisual;
|
||||
else
|
||||
ownTransform = default(Matrix); // Don't render, something is broken
|
||||
_ownTransform = default(Matrix); // Don't render, something is broken
|
||||
|
||||
}
|
||||
_ownTransform = ownTransform;
|
||||
else
|
||||
_ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
|
||||
RotationAngle, Orientation, Offset);
|
||||
|
||||
|
||||
PropagateFlags(true, true);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,19 @@ namespace Avalonia.VisualTree
|
||||
/// <param name="includeSelf">If given visual should be included in search.</param>
|
||||
/// <returns>First ancestor of given type.</returns>
|
||||
public static T? FindAncestorOfType<T>(this Visual? visual, bool includeSelf = false) where T : class
|
||||
{
|
||||
return FindAncestorOfType<T>(visual, includeSelf, predicate: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds first ancestor of given type that matches a predicate.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Ancestor type.</typeparam>
|
||||
/// <param name="visual">The visual.</param>
|
||||
/// <param name="includeSelf">If given visual should be included in search.</param>
|
||||
/// <param name="predicate">The predicate that the ancestor must match.</param>
|
||||
/// <returns>First ancestor of given type.</returns>
|
||||
public static T? FindAncestorOfType<T>(this Visual? visual, bool includeSelf, Predicate<T>? predicate) where T : class
|
||||
{
|
||||
if (visual is null)
|
||||
{
|
||||
@@ -159,7 +172,10 @@ namespace Avalonia.VisualTree
|
||||
{
|
||||
if (parent is T result)
|
||||
{
|
||||
return result;
|
||||
if (predicate == null || predicate(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
parent = parent.VisualParent;
|
||||
@@ -176,18 +192,31 @@ namespace Avalonia.VisualTree
|
||||
/// <param name="includeSelf">If given visual should be included in search.</param>
|
||||
/// <returns>First descendant of given type.</returns>
|
||||
public static T? FindDescendantOfType<T>(this Visual? visual, bool includeSelf = false) where T : class
|
||||
{
|
||||
return FindDescendantOfType<T>(visual, includeSelf, predicate: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds first descendant of given type that matches given predicate.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Descendant type.</typeparam>
|
||||
/// <param name="visual">The visual.</param>
|
||||
/// <param name="includeSelf">If given visual should be included in search.</param>
|
||||
/// <param name="predicate">The predicate that the descendant must match.</param>
|
||||
/// <returns>First descendant of given type that matches given predicate.</returns>
|
||||
public static T? FindDescendantOfType<T>(this Visual? visual, bool includeSelf, Predicate<T>? predicate) where T : class
|
||||
{
|
||||
if (visual is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (includeSelf && visual is T result)
|
||||
if (includeSelf && visual is T result && (predicate == null || predicate(result)))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return FindDescendantOfTypeCore<T>(visual);
|
||||
return FindDescendantOfTypeCore<T>(visual, predicate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -485,7 +514,7 @@ namespace Avalonia.VisualTree
|
||||
.Select(x => x.Element!);
|
||||
}
|
||||
|
||||
private static T? FindDescendantOfTypeCore<T>(Visual visual) where T : class
|
||||
private static T? FindDescendantOfTypeCore<T>(Visual visual, Predicate<T>? predicate) where T : class
|
||||
{
|
||||
var visualChildren = visual.VisualChildren;
|
||||
var visualChildrenCount = visualChildren.Count;
|
||||
@@ -496,10 +525,13 @@ namespace Avalonia.VisualTree
|
||||
|
||||
if (child is T result)
|
||||
{
|
||||
return result;
|
||||
if (predicate == null || predicate(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
var childResult = FindDescendantOfTypeCore<T>(child);
|
||||
var childResult = FindDescendantOfTypeCore<T>(child, predicate);
|
||||
|
||||
if (!(childResult is null))
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
|
||||
namespace Avalonia
|
||||
{
|
||||
@@ -265,6 +267,17 @@ namespace Avalonia
|
||||
return Self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for validation using <c>System.ComponentModel.DataAnnotations</c>.
|
||||
/// </summary>
|
||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
||||
public AppBuilder WithDataAnnotationsValidation()
|
||||
{
|
||||
if (!BindingPlugins.DataValidators.Any(x => x is DataAnnotationsValidationPlugin))
|
||||
BindingPlugins.DataValidators.Insert(0, new DataAnnotationsValidationPlugin());
|
||||
return Self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an action that is executed with the current font manager.
|
||||
/// </summary>
|
||||
|
||||
@@ -31,6 +31,12 @@ namespace Avalonia.Automation
|
||||
/// </summary>
|
||||
public static AutomationProperty HelpTextProperty { get; } = new AutomationProperty();
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the itemStatus automation property. The class name property value is returned
|
||||
/// by the <see cref="AutomationPeer.GetItemStatus"/> method.
|
||||
/// </summary>
|
||||
public static AutomationProperty ItemStatusProperty { get; } = new AutomationProperty();
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the landmark type automation property. The class name property value is returned
|
||||
/// by the <see cref="AutomationPeer.GetLandmarkType"/> method.
|
||||
|
||||
@@ -319,6 +319,41 @@ namespace Avalonia.Automation.Peers
|
||||
/// </remarks>
|
||||
public int GetHeadingLevel() => GetHeadingLevelCore();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item type that is associated with this automation peer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term>Windows</term>
|
||||
/// <description><c>UIA_ItemTypePropertyId</c></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>macOS</term>
|
||||
/// <description>No mapping.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string? GetItemType() => GetItemTypeCore();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item status that is associated with this automation peer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="table">
|
||||
/// <item>
|
||||
/// <term>Windows</term>
|
||||
/// <description><c>UIA_ItemStatusPropertyId</c></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>macOS</term>
|
||||
/// <description>No mapping.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string? GetItemStatus() => GetItemStatusCore();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
|
||||
/// </summary>
|
||||
@@ -562,6 +597,8 @@ namespace Avalonia.Automation.Peers
|
||||
protected virtual string? GetHelpTextCore() => null;
|
||||
protected virtual AutomationLandmarkType? GetLandmarkTypeCore() => null;
|
||||
protected virtual int GetHeadingLevelCore() => 0;
|
||||
protected virtual string? GetItemTypeCore() => null;
|
||||
protected virtual string? GetItemStatusCore() => null;
|
||||
protected abstract AutomationPeer? GetParentCore();
|
||||
protected abstract bool HasKeyboardFocusCore();
|
||||
protected abstract bool IsContentElementCore();
|
||||
|
||||
@@ -199,6 +199,8 @@ namespace Avalonia.Automation.Peers
|
||||
protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
|
||||
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner);
|
||||
protected override string GetClassNameCore() => Owner.GetType().Name;
|
||||
protected override string? GetItemStatusCore() => AutomationProperties.GetItemStatus(Owner);
|
||||
protected override string? GetItemTypeCore() => AutomationProperties.GetItemType(Owner);
|
||||
protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
|
||||
protected override bool IsContentElementCore() => true;
|
||||
protected override bool IsControlElementCore() => true;
|
||||
@@ -281,6 +283,13 @@ namespace Avalonia.Automation.Peers
|
||||
{
|
||||
InvalidateParent();
|
||||
}
|
||||
else if (e.Property == AutomationProperties.ItemStatusProperty)
|
||||
{
|
||||
RaisePropertyChangedEvent(
|
||||
AutomationElementIdentifiers.ItemStatusProperty,
|
||||
e.OldValue,
|
||||
e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Avalonia.Automation.Peers
|
||||
{
|
||||
if (Owner.HeaderPresenter?.Child is AccessText accessText)
|
||||
{
|
||||
result = accessText.AccessKey.ToString();
|
||||
result = accessText.AccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Avalonia.Controls
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a <see cref="MenuItem"/>.
|
||||
/// </summary>
|
||||
internal interface IMenuItem : IMenuElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the item has a submenu.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="MenuItem.HasSubMenu"/>
|
||||
bool HasSubMenu { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -17,21 +13,13 @@ namespace Avalonia.Controls
|
||||
/// </summary>
|
||||
bool IsPointerOverSubMenu { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
|
||||
/// open.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="MenuItem.IsSubMenuOpen"/>
|
||||
bool IsSubMenuOpen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
|
||||
/// within should not close when this item is clicked.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="MenuItem.StaysOpenOnClick"/>
|
||||
bool StaysOpenOnClick { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="MenuItem.IsTopLevel"/>
|
||||
bool IsTopLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -39,22 +27,15 @@ namespace Avalonia.Controls
|
||||
/// </summary>
|
||||
IMenuElement? Parent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets toggle type of the menu item.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="MenuItem.ToggleType"/>
|
||||
MenuItemToggleType ToggleType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets menu item group name when <see cref="ToggleType"/> is <see cref="MenuItemToggleType.Radio"/>.
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc cref="MenuItem.GroupName"/>
|
||||
string? GroupName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if menu item is checked when <see cref="ToggleType"/> is
|
||||
/// <see cref="MenuItemToggleType.CheckBox"/> or <see cref="MenuItemToggleType.Radio"/>.
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc cref="MenuItem.IsChecked"/>
|
||||
bool IsChecked { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Raises a click event on the menu item.
|
||||
/// </summary>
|
||||
|
||||
@@ -288,6 +288,12 @@ namespace Avalonia.Controls
|
||||
set => SetValue(IsSubMenuOpenProperty, value);
|
||||
}
|
||||
|
||||
bool IMenuItem.IsSubMenuOpen
|
||||
{
|
||||
get => IsSubMenuOpen;
|
||||
set => SetCurrentValue(IsSubMenuOpenProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
|
||||
/// within should not close when this item is clicked.
|
||||
@@ -298,27 +304,46 @@ namespace Avalonia.Controls
|
||||
set => SetValue(StaysOpenOnClickProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMenuItem.ToggleType" />
|
||||
bool IMenuItem.StaysOpenOnClick
|
||||
{
|
||||
get => StaysOpenOnClick;
|
||||
set => SetCurrentValue(StaysOpenOnClickProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets toggle type of the menu item.
|
||||
/// </summary>
|
||||
public MenuItemToggleType ToggleType
|
||||
{
|
||||
get => GetValue(ToggleTypeProperty);
|
||||
set => SetValue(ToggleTypeProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMenuItem.IsChecked"/>
|
||||
/// <summary>
|
||||
/// Gets or sets if menu item is checked when <see cref="ToggleType"/> is
|
||||
/// <see cref="MenuItemToggleType.CheckBox"/> or <see cref="MenuItemToggleType.Radio"/>.
|
||||
/// </summary>
|
||||
public bool IsChecked
|
||||
{
|
||||
get => GetValue(IsCheckedProperty);
|
||||
set => SetValue(IsCheckedProperty, value);
|
||||
}
|
||||
|
||||
bool IMenuItem.IsChecked
|
||||
{
|
||||
get => IsChecked;
|
||||
set => SetCurrentValue(IsCheckedProperty, value);
|
||||
}
|
||||
|
||||
bool IRadioButton.IsChecked
|
||||
{
|
||||
get => IsChecked;
|
||||
set => SetCurrentValue(IsCheckedProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMenuItem.GroupName"/>
|
||||
/// <summary>
|
||||
/// Gets menu item group name when <see cref="ToggleType"/> is <see cref="MenuItemToggleType.Radio"/>.
|
||||
/// </summary>
|
||||
public string? GroupName
|
||||
{
|
||||
get => GetValue(GroupNameProperty);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Avalonia.Reactive;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Avalonia.Controls.Primitives
|
||||
{
|
||||
@@ -42,7 +43,7 @@ namespace Avalonia.Controls.Primitives
|
||||
/// <summary>
|
||||
/// Gets the access key.
|
||||
/// </summary>
|
||||
public char AccessKey
|
||||
public string? AccessKey
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
@@ -93,7 +94,7 @@ namespace Avalonia.Controls.Primitives
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_accessKeys = (e.Root as TopLevel)?.AccessKeyHandler;
|
||||
|
||||
if (_accessKeys != null && AccessKey != 0)
|
||||
if (_accessKeys != null && !string.IsNullOrEmpty(AccessKey))
|
||||
{
|
||||
_accessKeys.Register(AccessKey, this);
|
||||
}
|
||||
@@ -104,7 +105,7 @@ namespace Avalonia.Controls.Primitives
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
if (_accessKeys != null && AccessKey != 0)
|
||||
if (_accessKeys != null && !string.IsNullOrEmpty(AccessKey))
|
||||
{
|
||||
_accessKeys.Unregister(this);
|
||||
_accessKeys = null;
|
||||
@@ -153,7 +154,7 @@ namespace Avalonia.Controls.Primitives
|
||||
/// <param name="text">The new text.</param>
|
||||
private void TextChanged(string? text)
|
||||
{
|
||||
var key = (char)0;
|
||||
string? key = null;
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
@@ -161,13 +162,14 @@ namespace Avalonia.Controls.Primitives
|
||||
|
||||
if (underscore != -1 && underscore < text.Length - 1)
|
||||
{
|
||||
key = text[underscore + 1];
|
||||
var rune = Rune.GetRuneAt(text, underscore + 1);
|
||||
key = rune.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
AccessKey = key;
|
||||
|
||||
if (_accessKeys != null && AccessKey != 0)
|
||||
if (_accessKeys != null && !string.IsNullOrEmpty(AccessKey))
|
||||
{
|
||||
_accessKeys.Register(AccessKey, this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Avalonia.Controls.Primitives;
|
||||
|
||||
class AdornerHelper
|
||||
{
|
||||
|
||||
public static IDisposable SubscribeToAncestorPropertyChanges(Visual visual,
|
||||
bool includeClip, Action changed)
|
||||
{
|
||||
return new AncestorPropertyChangesSubscription(visual, includeClip, changed);
|
||||
}
|
||||
|
||||
private class AncestorPropertyChangesSubscription : IDisposable
|
||||
{
|
||||
private readonly Visual _visual;
|
||||
private readonly bool _includeClip;
|
||||
private readonly Action _changed;
|
||||
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
|
||||
private readonly List<Visual> _subscriptions = new List<Visual>();
|
||||
private bool _isDisposed;
|
||||
|
||||
public AncestorPropertyChangesSubscription(Visual visual, bool includeClip, Action changed)
|
||||
{
|
||||
_visual = visual;
|
||||
_includeClip = includeClip;
|
||||
_changed = changed;
|
||||
_propertyChangedHandler = OnPropertyChanged;
|
||||
|
||||
_visual.AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
_visual.DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
|
||||
if (_visual.IsAttachedToVisualTree)
|
||||
{
|
||||
SubscribeToAncestors();
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToAncestors()
|
||||
{
|
||||
UnsubscribeFromAncestors();
|
||||
|
||||
// Subscribe to the visual's own Bounds property
|
||||
_visual.PropertyChanged += _propertyChangedHandler;
|
||||
_subscriptions.Add(_visual);
|
||||
|
||||
// Walk up the ancestor chain
|
||||
var ancestor = _visual.VisualParent;
|
||||
while (ancestor != null)
|
||||
{
|
||||
if (ancestor is Visual visualAncestor)
|
||||
{
|
||||
visualAncestor.PropertyChanged += _propertyChangedHandler;
|
||||
_subscriptions.Add(visualAncestor);
|
||||
}
|
||||
ancestor = ancestor.VisualParent;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromAncestors()
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
{
|
||||
subscription.PropertyChanged -= _propertyChangedHandler;
|
||||
}
|
||||
_subscriptions.Clear();
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!e.IsEffectiveValueChange)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
|
||||
if (e.Property == Visual.RenderTransformProperty || e.Property == Visual.BoundsProperty)
|
||||
{
|
||||
shouldNotify = true;
|
||||
}
|
||||
else if (_includeClip)
|
||||
{
|
||||
if (e.Property == Visual.ClipToBoundsProperty ||
|
||||
e.Property == Visual.ClipProperty) shouldNotify = true;
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
{
|
||||
_changed();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
SubscribeToAncestors();
|
||||
_changed();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
UnsubscribeFromAncestors();
|
||||
_changed();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
UnsubscribeFromAncestors();
|
||||
_visual.AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
_visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
}
|
||||
}
|
||||
|
||||
public static Geometry? CalculateAdornerClip(Visual adornedElement)
|
||||
{
|
||||
// Walk ancestor stack and calculate clip geometry relative to the current visual.
|
||||
// If ClipToBounds = true, add extra RectangleGeometry for Bounds.Size
|
||||
|
||||
Geometry? result = null;
|
||||
var ancestor = adornedElement;
|
||||
|
||||
while (ancestor != null)
|
||||
{
|
||||
if (ancestor is Visual visualAncestor)
|
||||
{
|
||||
Geometry? ancestorClip = null;
|
||||
|
||||
// Check if ancestor has ClipToBounds enabled
|
||||
if (visualAncestor.ClipToBounds)
|
||||
{
|
||||
ancestorClip = new RectangleGeometry(new Rect(visualAncestor.Bounds.Size));
|
||||
}
|
||||
|
||||
// Check if ancestor has explicit Clip geometry
|
||||
if (visualAncestor.Clip != null)
|
||||
{
|
||||
if (ancestorClip != null)
|
||||
{
|
||||
ancestorClip = new CombinedGeometry(GeometryCombineMode.Intersect, ancestorClip, visualAncestor.Clip);
|
||||
}
|
||||
else
|
||||
{
|
||||
ancestorClip = visualAncestor.Clip;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the clip geometry to adorned element's coordinate space
|
||||
if (ancestorClip != null)
|
||||
{
|
||||
var transform = visualAncestor.TransformToVisual(adornedElement);
|
||||
if (transform.HasValue && !transform.Value.IsIdentity)
|
||||
{
|
||||
ancestorClip = ancestorClip.Clone();
|
||||
var matrix = ancestorClip.Transform is { Value.IsIdentity: false }
|
||||
? transform.Value * ancestorClip.Transform.Value
|
||||
: transform.Value;
|
||||
ancestorClip.Transform = new MatrixTransform(matrix);
|
||||
}
|
||||
|
||||
// Combine with existing result
|
||||
if (result != null)
|
||||
{
|
||||
result = new CombinedGeometry(GeometryCombineMode.Intersect, result, ancestorClip);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ancestorClip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ancestor = ancestor.VisualParent;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia.Input.TextInput;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.VisualTree;
|
||||
@@ -45,15 +46,20 @@ namespace Avalonia.Controls.Primitives
|
||||
private static readonly AttachedProperty<AdornerLayer?> s_savedAdornerLayerProperty =
|
||||
AvaloniaProperty.RegisterAttached<Visual, Visual, AdornerLayer?>("SavedAdornerLayer");
|
||||
|
||||
private TransformTrackingHelper _trackingHelper = new TransformTrackingHelper(false);
|
||||
|
||||
static AdornerLayer()
|
||||
{
|
||||
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
|
||||
AdornerProperty.Changed.Subscribe(AdornerChanged);
|
||||
IsClipEnabledProperty.Changed.Subscribe(AdornerIsClipEnabledChanged);
|
||||
}
|
||||
|
||||
public AdornerLayer()
|
||||
{
|
||||
Children.CollectionChanged += ChildrenCollectionChanged;
|
||||
_trackingHelper.SetVisual(this);
|
||||
_trackingHelper.MatrixChanged += delegate { InvalidateMeasure(); };
|
||||
}
|
||||
|
||||
public static Visual? GetAdornedElement(Visual adorner)
|
||||
@@ -199,9 +205,9 @@ namespace Avalonia.Controls.Primitives
|
||||
{
|
||||
var info = ao.GetValue(s_adornedElementInfoProperty);
|
||||
|
||||
if (info != null && info.Bounds.HasValue)
|
||||
if (info is { AdornedElement: not null })
|
||||
{
|
||||
child.Measure(info.Bounds.Value.Bounds.Size);
|
||||
child.Measure(info.AdornedElement.Bounds.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -223,12 +229,22 @@ namespace Avalonia.Controls.Primitives
|
||||
var info = ao.GetValue(s_adornedElementInfoProperty);
|
||||
var isClipEnabled = ao.GetValue(IsClipEnabledProperty);
|
||||
|
||||
if (info != null && info.Bounds.HasValue)
|
||||
var adorned = info?.AdornedElement;
|
||||
|
||||
if (adorned != null)
|
||||
{
|
||||
child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
|
||||
child.Arrange(new(adorned.Bounds.Size));
|
||||
var transform = adorned.TransformToVisual(this);
|
||||
// If somebody decides that having Margin on an adorner is a good idea,
|
||||
// we need to compensate for element being positioned at non-(0,0) coords.
|
||||
if (transform != null && child.Bounds.Position != default)
|
||||
{
|
||||
transform = Matrix.CreateTranslation(child.Bounds.Position) * transform.Value *
|
||||
Matrix.CreateTranslation(-child.Bounds.Position);
|
||||
}
|
||||
child.RenderTransform = new MatrixTransform(transform ?? default);
|
||||
child.RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Absolute);
|
||||
UpdateClip(child, info.Bounds.Value, isClipEnabled);
|
||||
child.Arrange(info.Bounds.Value.Bounds);
|
||||
UpdateClip(child, adorned, isClipEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -248,29 +264,22 @@ namespace Avalonia.Controls.Primitives
|
||||
layer?.UpdateAdornedElement(adorner, adorned);
|
||||
}
|
||||
|
||||
private void UpdateClip(Control control, TransformedBounds bounds, bool isEnabled)
|
||||
private static void AdornerIsClipEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> e)
|
||||
{
|
||||
var info = ((Visual)e.Sender).GetValue(s_adornedElementInfoProperty);
|
||||
info?.UpdateSubscription();
|
||||
info?.Layer?.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private void UpdateClip(Control control, Visual adorned, bool isEnabled)
|
||||
{
|
||||
if (!isEnabled)
|
||||
{
|
||||
control.Clip = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(control.Clip is RectangleGeometry clip))
|
||||
{
|
||||
clip = new RectangleGeometry();
|
||||
control.Clip = clip;
|
||||
}
|
||||
|
||||
var clipBounds = bounds.Bounds;
|
||||
|
||||
if (bounds.Transform.HasInverse)
|
||||
{
|
||||
clipBounds = bounds.Clip.TransformToAABB(bounds.Transform.Invert());
|
||||
}
|
||||
|
||||
clip.Rect = clipBounds;
|
||||
control.Clip = AdornerHelper.CalculateAdornerClip(adorned);
|
||||
}
|
||||
|
||||
private void ChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -313,24 +322,35 @@ namespace Avalonia.Controls.Primitives
|
||||
{
|
||||
if (info == null)
|
||||
{
|
||||
info = new AdornedElementInfo();
|
||||
info = new AdornedElementInfo(adorner);
|
||||
adorner.SetValue(s_adornedElementInfoProperty, info);
|
||||
}
|
||||
|
||||
if (adorner.CompositionVisual != null)
|
||||
info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x =>
|
||||
{
|
||||
info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity);
|
||||
InvalidateMeasure();
|
||||
});
|
||||
info.Layer = this;
|
||||
info.AdornedElement = adorned;
|
||||
info.UpdateSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
private class AdornedElementInfo
|
||||
private class AdornedElementInfo(Visual adorner)
|
||||
{
|
||||
public AdornerLayer? Layer { get; set; }
|
||||
public IDisposable? Subscription { get; set; }
|
||||
public Visual? AdornedElement { get; set; }
|
||||
|
||||
public TransformedBounds? Bounds { get; set; }
|
||||
public void UpdateSubscription()
|
||||
{
|
||||
Subscription?.Dispose();
|
||||
Subscription = null;
|
||||
if (AdornedElement != null)
|
||||
{
|
||||
Subscription = AdornerHelper.SubscribeToAncestorPropertyChanges(AdornedElement,
|
||||
GetIsClipEnabled(adorner), () =>
|
||||
{
|
||||
Layer?.InvalidateMeasure();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ namespace Avalonia.Controls.Primitives
|
||||
|
||||
if (InheritsTransform)
|
||||
{
|
||||
TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
|
||||
TransformTrackingHelper.Track(placementTarget, true, PlacementTargetTransformChanged)
|
||||
.DisposeWith(handlerCleanup);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -570,16 +570,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
var target = positionRequest.Target;
|
||||
if (target == null)
|
||||
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
|
||||
Matrix? matrix;
|
||||
if (TryGetAdorner(target, out var adorned, out var adornerLayer))
|
||||
{
|
||||
matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
|
||||
}
|
||||
else
|
||||
{
|
||||
matrix = target.TransformToVisual(topLevel);
|
||||
}
|
||||
|
||||
Matrix? matrix = target.TransformToVisual(topLevel);
|
||||
|
||||
if (matrix == null)
|
||||
{
|
||||
if (target.GetVisualRoot() == null)
|
||||
@@ -591,25 +583,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
var anchorRect = positionRequest.AnchorRect ?? bounds;
|
||||
return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
|
||||
}
|
||||
|
||||
private static bool TryGetAdorner(Visual target, out Visual? adorned, out Visual? adornerLayer)
|
||||
{
|
||||
var element = target;
|
||||
while (element != null)
|
||||
{
|
||||
if (AdornerLayer.GetAdornedElement(element) is { } adornedElement)
|
||||
{
|
||||
adorned = adornedElement;
|
||||
adornerLayer = AdornerLayer.GetAdornerLayer(adorned);
|
||||
return true;
|
||||
}
|
||||
element = element.VisualParent;
|
||||
}
|
||||
|
||||
adorned = null;
|
||||
adornerLayer = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -290,6 +290,9 @@ namespace Avalonia.Controls
|
||||
|
||||
_backGestureSubscription = _inputManager?.PreProcess.Subscribe(e =>
|
||||
{
|
||||
if (e.Root != this)
|
||||
return;
|
||||
|
||||
bool backRequested = false;
|
||||
|
||||
if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown)
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Avalonia.Win32.Automation
|
||||
{ AutomationElementIdentifiers.HelpTextProperty, UiaPropertyId.HelpText },
|
||||
{ AutomationElementIdentifiers.LandmarkTypeProperty, UiaPropertyId.LandmarkType },
|
||||
{ AutomationElementIdentifiers.HeadingLevelProperty, UiaPropertyId.HeadingLevel },
|
||||
{ AutomationElementIdentifiers.ItemStatusProperty, UiaPropertyId.ItemStatus },
|
||||
{ ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState },
|
||||
{ RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly},
|
||||
{ RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum },
|
||||
@@ -135,6 +136,8 @@ namespace Avalonia.Win32.Automation
|
||||
UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()),
|
||||
UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()),
|
||||
UiaPropertyId.IsOffscreen => InvokeSync(() => Peer.IsOffscreen()),
|
||||
UiaPropertyId.ItemType => InvokeSync(() => Peer.GetItemType()),
|
||||
UiaPropertyId.ItemStatus => InvokeSync(() => Peer.GetItemStatus()),
|
||||
UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()),
|
||||
UiaPropertyId.Name => InvokeSync(() => Peer.GetName()),
|
||||
UiaPropertyId.HelpText => InvokeSync(() => Peer.GetHelpText()),
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
@@ -273,6 +275,9 @@ public partial class BindingExpressionTests
|
||||
[Fact]
|
||||
public void Updates_Data_Validation_For_Required_DataAnnotation()
|
||||
{
|
||||
if (!BindingPlugins.DataValidators.Any(x => x is DataAnnotationsValidationPlugin))
|
||||
BindingPlugins.DataValidators.Insert(0, new DataAnnotationsValidationPlugin());
|
||||
|
||||
var data = new DataAnnotationsViewModel();
|
||||
var target = CreateTargetWithSource(
|
||||
data,
|
||||
|
||||
@@ -21,8 +21,8 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
root.KeyUp += (s, e) => events.Add($"KeyUp {e.Key}");
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyDown(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(new[]
|
||||
@@ -48,8 +48,8 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
root.KeyUp += (s, e) => events.Add($"KeyUp {e.Key}");
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyDown(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(new[]
|
||||
@@ -122,13 +122,13 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
var events = new List<string>();
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
target.Register("A", button);
|
||||
root.KeyDown += (s, e) => events.Add($"KeyDown {e.Key}");
|
||||
root.KeyUp += (s, e) => events.Add($"KeyUp {e.Key}");
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyDown(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
// This differs from WPF which doesn't raise the `A` key event, but matches UWP.
|
||||
@@ -141,8 +141,12 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
}, events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Raise_AccessKey_For_Registered_Access_Key()
|
||||
[Theory]
|
||||
[InlineData("A", Key.A, "a")]
|
||||
[InlineData("A", Key.Q, "a")]
|
||||
[InlineData("é", Key.D2, "é")]
|
||||
[InlineData("2", Key.D2, "2")]
|
||||
public void Should_Raise_AccessKey_For_Registered_Access_Key_Matching_KeySymbol(string registered, Key key, string keySymbol)
|
||||
{
|
||||
using (UnitTestApplication.Start(TestServices.RealFocus))
|
||||
{
|
||||
@@ -154,22 +158,51 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
target.Register(registered, button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
KeyDown(root, key, keySymbol, KeyModifiers.Alt);
|
||||
Assert.Equal(1, raised);
|
||||
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, key, keySymbol, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Not_Raise_AccessKey_For_Registered_Access_Key_Not_Matching_KeySymbol()
|
||||
{
|
||||
using (UnitTestApplication.Start(TestServices.RealFocus))
|
||||
{
|
||||
var button = new Button();
|
||||
var root = new TestRoot(button);
|
||||
var target = new AccessKeyHandler();
|
||||
var raised = 0;
|
||||
|
||||
KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register("A", button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyEvent, (_, _) => ++raised);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyDown(root, Key.A, "q", KeyModifiers.Alt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyUp(root, Key.A, "q", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(0, raised);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 0)]
|
||||
[InlineData(true, 1)]
|
||||
@@ -185,16 +218,16 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
|
||||
|
||||
target.SetOwner(root);
|
||||
target.Register('A', button);
|
||||
target.Register("A", button);
|
||||
button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
Assert.Equal(0, raised);
|
||||
|
||||
KeyDown(root, Key.A, KeyModifiers.Alt);
|
||||
KeyDown(root, Key.A, "a", KeyModifiers.Alt);
|
||||
Assert.Equal(expected, raised);
|
||||
|
||||
KeyUp(root, Key.A, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.A, "a", KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
Assert.Equal(expected, raised);
|
||||
}
|
||||
@@ -223,22 +256,24 @@ namespace Avalonia.Base.UnitTests.Input
|
||||
}
|
||||
}
|
||||
|
||||
private static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
private static void KeyDown(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyDownEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
private static void KeyUp(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
private static void KeyUp(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyUpEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,23 @@ namespace Avalonia.Base.UnitTests
|
||||
Assert.Equal(root, target.FindAncestorOfType<TestRoot>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAncestorOfType_Finds_Visible_Parent()
|
||||
{
|
||||
StackPanel target;
|
||||
|
||||
var root = new TestRoot
|
||||
{
|
||||
Child = new TestRoot
|
||||
{
|
||||
Child = target = new StackPanel(),
|
||||
IsVisible = false
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal(root, target.FindAncestorOfType<TestRoot>(false, v => v.IsVisible));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child()
|
||||
{
|
||||
@@ -85,6 +102,32 @@ namespace Avalonia.Base.UnitTests
|
||||
Assert.Equal(target, root.FindDescendantOfType<Button>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindDescendantOfType_Finds_Nested_Visible_Child()
|
||||
{
|
||||
Button target;
|
||||
|
||||
var root = new TestRoot
|
||||
{
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new Button { IsVisible = false },
|
||||
(target = new Button())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal(target, root.FindDescendantOfType<Button>(false, v => v.IsVisible));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindCommonVisualAncestor_First_Is_Parent_Of_Second()
|
||||
{
|
||||
|
||||
@@ -345,16 +345,17 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded, TestContext.Current.CancellationToken);
|
||||
|
||||
var accessKey = Key.A;
|
||||
const Key accessKey = Key.A;
|
||||
const string accessKeySymbol = "a";
|
||||
target.CommandParameter = true;
|
||||
|
||||
RaiseAccessKey(root, accessKey);
|
||||
RaiseAccessKey(root, accessKey, accessKeySymbol);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
|
||||
target.CommandParameter = false;
|
||||
|
||||
RaiseAccessKey(root, accessKey);
|
||||
RaiseAccessKey(root, accessKey, accessKeySymbol);
|
||||
|
||||
Assert.Equal(1, raised);
|
||||
|
||||
@@ -379,30 +380,32 @@ namespace Avalonia.Controls.UnitTests
|
||||
return topLevel;
|
||||
}
|
||||
|
||||
static void RaiseAccessKey(IInputElement target, Key accessKey)
|
||||
static void RaiseAccessKey(IInputElement target, Key accessKey, string keySymbol)
|
||||
{
|
||||
KeyDown(target, Key.LeftAlt);
|
||||
KeyDown(target, accessKey, KeyModifiers.Alt);
|
||||
KeyUp(target, accessKey, KeyModifiers.Alt);
|
||||
KeyUp(target, Key.LeftAlt);
|
||||
KeyDown(target, accessKey, keySymbol, KeyModifiers.Alt);
|
||||
KeyUp(target, accessKey, keySymbol, KeyModifiers.Alt);
|
||||
KeyUp(target, Key.LeftAlt, null);
|
||||
}
|
||||
|
||||
static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
static void KeyDown(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyDownEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
static void KeyUp(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
static void KeyUp(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyUpEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Controls.Platform;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.UnitTests;
|
||||
@@ -12,11 +15,11 @@ namespace Avalonia.Controls.UnitTests.Platform
|
||||
public class DefaultMenuInteractionHandlerTests : ScopedTestBase
|
||||
{
|
||||
static PointerPressedEventArgs CreatePressed(object source) => new PointerPressedEventArgs(source,
|
||||
new FakePointer(), (Visual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
|
||||
new FakePointer(), (Visual)source, default, 0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
|
||||
default);
|
||||
|
||||
|
||||
static PointerReleasedEventArgs CreateReleased(object source) => new PointerReleasedEventArgs(source,
|
||||
new FakePointer(), (Visual)source, default,0,
|
||||
new FakePointer(), (Visual)source, default, 0,
|
||||
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased),
|
||||
default, MouseButton.Left);
|
||||
|
||||
@@ -224,6 +227,63 @@ namespace Avalonia.Controls.UnitTests.Platform
|
||||
|
||||
target.KeyDown(menu, e);
|
||||
}
|
||||
|
||||
private class MenuItemVM : INotifyPropertyChanged
|
||||
{
|
||||
public bool IsChecked { get; set { field = value; OnPropertyChanged(); } }
|
||||
public bool IsSubMenuOpen { get; set { field = value; OnPropertyChanged(); } }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new(propertyName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Doesnt_Replace_IsChecked_Binding()
|
||||
{
|
||||
var target = new DefaultMenuInteractionHandler(false);
|
||||
var menu = new Menu();
|
||||
var vm = new MenuItemVM();
|
||||
|
||||
var item = new MenuItem
|
||||
{
|
||||
DataContext = vm,
|
||||
[!MenuItem.IsCheckedProperty] = new Binding(nameof(MenuItemVM.IsChecked)) { Priority = BindingPriority.Style, Mode = BindingMode.TwoWay },
|
||||
ToggleType = MenuItemToggleType.CheckBox,
|
||||
};
|
||||
menu.Items.Add(item);
|
||||
|
||||
target.KeyDown(item, new KeyEventArgs { Key = Key.Enter, Source = menu });
|
||||
|
||||
Assert.True(item.IsChecked);
|
||||
|
||||
vm.IsChecked = false;
|
||||
|
||||
Assert.False(item.IsChecked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Doesnt_Replace_IsSubMenuOpen_Binding()
|
||||
{
|
||||
var target = new DefaultMenuInteractionHandler(false);
|
||||
var menu = new Menu();
|
||||
var vm = new MenuItemVM();
|
||||
|
||||
var item = new MenuItem
|
||||
{
|
||||
DataContext = vm,
|
||||
[!MenuItem.IsSubMenuOpenProperty] = new Binding(nameof(MenuItemVM.IsSubMenuOpen)) { Priority = BindingPriority.Style, Mode = BindingMode.TwoWay },
|
||||
Items = { new MenuItem() }
|
||||
};
|
||||
|
||||
target.KeyDown(item, new KeyEventArgs { Key = Key.Enter, Source = menu });
|
||||
|
||||
Assert.True(item.IsSubMenuOpen);
|
||||
|
||||
vm.IsSubMenuOpen = false;
|
||||
|
||||
Assert.False(item.IsSubMenuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
public class NonTopLevel : ScopedTestBase
|
||||
|
||||
@@ -718,10 +718,10 @@ namespace Avalonia.Controls.UnitTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Key.A, 1)]
|
||||
[InlineData(Key.L, 2)]
|
||||
[InlineData(Key.D, 0)]
|
||||
public void Should_TabControl_Recognizes_AccessKey(Key accessKey, int selectedTabIndex)
|
||||
[InlineData(Key.A, "a", 1)]
|
||||
[InlineData(Key.L, "l", 2)]
|
||||
[InlineData(Key.D, "d", 0)]
|
||||
public void Should_TabControl_Recognizes_AccessKey(Key accessKey, string accessKeySymbol, int selectedTabIndex)
|
||||
{
|
||||
var ah = new AccessKeyHandler();
|
||||
var kd = new KeyboardDevice();
|
||||
@@ -760,8 +760,8 @@ namespace Avalonia.Controls.UnitTests
|
||||
ApplyTemplate(tabControl);
|
||||
|
||||
KeyDown(root, Key.LeftAlt);
|
||||
KeyDown(root, accessKey, KeyModifiers.Alt);
|
||||
KeyUp(root, accessKey, KeyModifiers.Alt);
|
||||
KeyDown(root, accessKey, accessKeySymbol, KeyModifiers.Alt);
|
||||
KeyUp(root, accessKey, accessKeySymbol, KeyModifiers.Alt);
|
||||
KeyUp(root, Key.LeftAlt);
|
||||
|
||||
Assert.Equal(selectedTabIndex, tabControl.SelectedIndex);
|
||||
@@ -788,22 +788,24 @@ namespace Avalonia.Controls.UnitTests
|
||||
return topLevel;
|
||||
}
|
||||
|
||||
static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
static void KeyDown(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyDownEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
static void KeyUp(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
|
||||
static void KeyUp(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None)
|
||||
{
|
||||
target.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyUpEvent,
|
||||
Key = key,
|
||||
KeySymbol = keySymbol,
|
||||
KeyModifiers = modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user