From f90afbb01aabceef65a9ae744d192e443b460710 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2024 03:34:52 +0200 Subject: [PATCH 01/77] Fix BindingNotification handling in MultiBinding (#16102) * Added failing test for #16070. * Handle BindingNotifications in UntypedObservableBindingExpression. Fixes #16070 --- .../UntypedObservableBindingExpression.cs | 15 ++++++++- .../Data/MultiBindingTests.cs | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs index 1e26caa051..df529d8675 100644 --- a/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs @@ -30,5 +30,18 @@ internal class UntypedObservableBindingExpression : UntypedBindingExpressionBase void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object? value) => PublishValue(value); + + void IObserver.OnNext(object? value) + { + if (value is BindingNotification n) + { + var v = n.Value; + var e = n.Error is not null ? new BindingError(n.Error, n.ErrorType) : null; + PublishValue(v, e); + } + else + { + PublishValue(value); + } + } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index f6a4437a9e..0a20c25b15 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -180,6 +180,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(target.ItemsView[2], source.C); } + [Fact] + public void Converter_Can_Return_BindingNotification() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new TextBlock { DataContext = source }; + + var binding = new MultiBinding + { + Converter = new BindingNotificationConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, + }, + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("1,2,3-BindingNotification", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) @@ -203,5 +225,16 @@ namespace Avalonia.Markup.UnitTests.Data return null; } } + + private class BindingNotificationConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return new BindingNotification( + new ArgumentException(), + BindingErrorType.Error, + string.Join(",", values) + "-BindingNotification"); + } + } } } From 133d7ca7484e2a4e74bb70ae05b9d9c9660f98fa Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:50:55 +0800 Subject: [PATCH 02/77] Update build.md (#16116) --- Documentation/build.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/build.md b/Documentation/build.md index ea91f035f8..065d2ee960 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -100,7 +100,14 @@ On macOS it is necessary to build and manually install the respective native lib # Building Avalonia into a local NuGet cache It is possible to build Avalonia locally and generate NuGet packages that can be used locally to test local changes. -To do so you need to run: + +First, install Nuke's dotnet global tool like so: + +```bash +dotnet tool install Nuke.GlobalTool --global +``` + +Then you need to run: ```bash nuke --target BuildToNuGetCache --configuration Release ``` From aab93ff16e5d63463fd694546f6c66fc6d267615 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2024 13:49:37 +0200 Subject: [PATCH 03/77] macOS: Don't include two windows in a11y tree. (#15899) * Don't include two windows in a11y tree. `AvnRootAccessibilityElement` has been removed and now `AvnWindow` handles the accessibility protocol itself, exposing its children via the `AvnView`. * Remove hack now that issue is fixed. * Fix build errors after merge. --- .../project.pbxproj | 6 + .../src/OSX/AvnAccessibility.h | 13 ++ .../src/OSX/AvnAutomationNode.h | 18 ++ native/Avalonia.Native/src/OSX/AvnView.h | 1 + native/Avalonia.Native/src/OSX/AvnView.mm | 77 +++++++-- native/Avalonia.Native/src/OSX/AvnWindow.mm | 47 ++++- .../src/OSX/WindowInterfaces.h | 8 +- .../Avalonia.Native/src/OSX/WindowProtocol.h | 2 + native/Avalonia.Native/src/OSX/automation.h | 5 +- native/Avalonia.Native/src/OSX/automation.mm | 163 +++--------------- .../WindowTests.cs | 4 +- .../WindowTests_MacOS.cs | 5 +- 12 files changed, 182 insertions(+), 167 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnAccessibility.h create mode 100644 native/Avalonia.Native/src/OSX/AvnAutomationNode.h diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 5dd69e7009..734f126a1b 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; + BC7C33822C066DBF00945A48 /* AvnAutomationNode.h in Headers */ = {isa = PBXBuildFile; fileRef = BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; }; EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; @@ -122,6 +123,8 @@ AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; + BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnAutomationNode.h; sourceTree = ""; }; + BC7C33832C066F1100945A48 /* AvnAccessibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnAccessibility.h; sourceTree = ""; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = ""; }; EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; }; @@ -167,6 +170,8 @@ isa = PBXGroup; children = ( F10084852BFF1FB40024303E /* TopLevelImpl.mm */, + BC7C33832C066F1100945A48 /* AvnAccessibility.h */, + BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */, ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */, 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, @@ -245,6 +250,7 @@ 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */, 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, + BC7C33822C066DBF00945A48 /* AvnAutomationNode.h in Headers */, 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */, 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */, diff --git a/native/Avalonia.Native/src/OSX/AvnAccessibility.h b/native/Avalonia.Native/src/OSX/AvnAccessibility.h new file mode 100644 index 0000000000..6658d8523e --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnAccessibility.h @@ -0,0 +1,13 @@ +#pragma once +#import +#import "avalonia-native.h" + +// Defines the interface between AvnAutomationNode and objects which implement +// NSAccessibility such as AvnAccessibilityElement or AvnWindow. +@protocol AvnAccessibility +@required +- (void) raiseChildrenChanged; +@optional +- (void) raiseFocusChanged; +- (void) raisePropertyChanged:(AvnAutomationProperty)property; +@end diff --git a/native/Avalonia.Native/src/OSX/AvnAutomationNode.h b/native/Avalonia.Native/src/OSX/AvnAutomationNode.h new file mode 100644 index 0000000000..58bea8caf9 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnAutomationNode.h @@ -0,0 +1,18 @@ +#pragma once +#include "avalonia-native.h" +#include "AvnAccessibility.h" + +// Defines a means for managed code to raise accessibility events. +class AvnAutomationNode : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + AvnAutomationNode(id owner) { _owner = owner; } + AvnAccessibilityElement* GetOwner() { return _owner; } + virtual void Dispose() override { _owner = nil; } + virtual void ChildrenChanged () override { [_owner raiseChildrenChanged]; } + virtual void PropertyChanged (AvnAutomationProperty property) override { [_owner raisePropertyChanged:property]; } + virtual void FocusChanged () override { [_owner raiseFocusChanged]; } +private: + __strong id _owner; +}; diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h index 4f673fceef..5e77873761 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.h +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -22,5 +22,6 @@ -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; -(void) setRenderTarget:(NSObject* _Nonnull)target; +-(void) raiseAccessibilityChildrenChanged; + (AvnPoint)toAvnPoint:(CGPoint)p; @end diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index ab52bcc4a9..89ac933eec 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -19,12 +19,12 @@ AvnPixelSize _lastPixelSize; NSObject* _currentRenderTarget; AvnPlatformResizeReason _resizeReason; - AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; NSMutableAttributedString* _text; NSRange _selectedRange; NSRange _markedRange; NSEvent* _lastKeyDownEvent; + NSMutableArray* _accessibilityChildren; } - (void)onClosed @@ -801,35 +801,74 @@ _resizeReason = reason; } -- (AvnAccessibilityElement *) accessibilityChild +- (NSArray *)accessibilityChildren { - if (_accessibilityChild == nil) - { - auto peer = _parent->TopLevelEvents->GetAutomationPeer(); + if (_accessibilityChildren == nil) + [self recalculateAccessibiltyChildren]; + return _accessibilityChildren; +} - if (peer == nil) - return nil; +- (id _Nullable) accessibilityHitTest:(NSPoint)point +{ + if (![[self window] isKindOfClass:[AvnWindow class]]) + return self; - _accessibilityChild = [AvnAccessibilityElement acquire:peer]; - } + auto window = (AvnWindow*)[self window]; + auto peer = [window automationPeer]; - return _accessibilityChild; -} + if (!peer->IsRootProvider()) + return nil; -- (NSArray *)accessibilityChildren -{ - auto child = [self accessibilityChild]; - return NSAccessibilityUnignoredChildrenForOnlyChild(child); + auto clientPoint = [window convertPointFromScreen:point]; + auto localPoint = [self translateLocalPoint:ToAvnPoint(clientPoint)]; + auto hit = peer->RootProvider_GetPeerFromPoint(localPoint); + return [AvnAccessibilityElement acquire:hit]; } -- (id)accessibilityHitTest:(NSPoint)point +- (void)raiseAccessibilityChildrenChanged { - return [[self accessibilityChild] accessibilityHitTest:point]; + auto changed = _accessibilityChildren ? [NSMutableSet setWithArray:_accessibilityChildren] : [NSMutableSet set]; + + [self recalculateAccessibiltyChildren]; + + if (_accessibilityChildren) + [changed addObjectsFromArray:_accessibilityChildren]; + + NSAccessibilityPostNotificationWithUserInfo( + self, + NSAccessibilityLayoutChangedNotification, + @{ NSAccessibilityUIElementsKey: [changed allObjects]}); } -- (id)accessibilityFocusedUIElement +- (void)recalculateAccessibiltyChildren { - return [[self accessibilityChild] accessibilityFocusedUIElement]; + _accessibilityChildren = [[NSMutableArray alloc] init]; + + if (![[self window] isKindOfClass:[AvnWindow class]]) + { + return; + } + + // The accessibility children of the Window are exposed as children + // of the AvnView. + auto window = (AvnWindow*)[self window]; + auto peer = [window automationPeer]; + auto childPeers = peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + id element = [AvnAccessibilityElement acquire:child]; + [_accessibilityChildren addObject:element]; + } + } + } } - (void) setText:(NSString *)text{ diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 86d412c76a..5b5f3abb76 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -24,6 +24,8 @@ #include "WindowImpl.h" #include "AvnView.h" #include "WindowInterfaces.h" +#include "AvnAutomationNode.h" +#include "AvnString.h" @implementation CLASS_NAME { @@ -34,6 +36,13 @@ bool _isExtended; bool _isTransitioningToFullScreen; AvnMenu* _menu; + IAvnAutomationPeer* _automationPeer; + AvnAutomationNode* _automationNode; +} + +-(AvnView* _Nullable) view +{ + return _parent->View; } -(void) setIsExtended:(bool)value; @@ -208,7 +217,7 @@ ComPtr parent = _parent; _parent = NULL; - auto window = dynamic_cast(parent.getRaw()); + auto window = dynamic_cast(parent.getRaw()); if(window != nullptr) { @@ -489,5 +498,41 @@ _parent = nullptr; } +- (id _Nullable) accessibilityFocusedUIElement +{ + if (![self automationPeer]->IsRootProvider()) + return nil; + auto focusedPeer = [self automationPeer]->RootProvider_GetFocus(); + return [AvnAccessibilityElement acquire:focusedPeer]; +} + +- (NSString * _Nullable) accessibilityIdentifier +{ + return GetNSStringAndRelease([self automationPeer]->GetAutomationId()); +} + +- (IAvnAutomationPeer* _Nonnull) automationPeer +{ + if (_automationPeer == nullptr) + { + _automationPeer = _parent->BaseEvents->GetAutomationPeer(); + _automationNode = new AvnAutomationNode(self); + _automationPeer->SetNode(_automationNode); + } + + return _automationPeer; +} + +- (void)raiseChildrenChanged +{ + [_parent->View raiseAccessibilityChildrenChanged]; +} + +- (void)raiseFocusChanged +{ + id focused = [self accessibilityFocusedUIElement]; + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); +} + @end diff --git a/native/Avalonia.Native/src/OSX/WindowInterfaces.h b/native/Avalonia.Native/src/OSX/WindowInterfaces.h index 6e6d62e85e..d920183435 100644 --- a/native/Avalonia.Native/src/OSX/WindowInterfaces.h +++ b/native/Avalonia.Native/src/OSX/WindowInterfaces.h @@ -7,11 +7,13 @@ #import #include "WindowProtocol.h" #include "WindowBaseImpl.h" +#include "AvnAccessibility.h" -@interface AvnWindow : NSWindow +@interface AvnWindow : NSWindow -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +-(AvnView* _Nullable) view; @end -@interface AvnPanel : NSPanel +@interface AvnPanel : NSPanel -(AvnPanel* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; -@end \ No newline at end of file +@end diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index cb5f86bdb9..5d1df951a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -8,6 +8,7 @@ #import @class AvnMenu; +struct IAvnAutomationPeer; @protocol AvnWindowProtocol -(void) pollModalSession: (NSModalSession _Nonnull) session; @@ -16,6 +17,7 @@ -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(AvnMenu* _Nullable)menu; +-(IAvnAutomationPeer* _Nonnull) automationPeer; -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 367df3619d..5d96637241 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,12 +1,13 @@ #pragma once #import +#include "AvnAccessibility.h" NS_ASSUME_NONNULL_BEGIN class IAvnAutomationPeer; -@interface AvnAccessibilityElement : NSAccessibilityElement -+ (AvnAccessibilityElement *) acquire:(IAvnAutomationPeer *) peer; +@interface AvnAccessibilityElement : NSAccessibilityElement ++ (id _Nullable) acquire:(IAvnAutomationPeer *) peer; @end NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 0f5f294329..543aa78cbe 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,66 +1,19 @@ #include "common.h" #include "automation.h" +#include "AvnAutomationNode.h" #include "AvnString.h" #include "INSWindowHolder.h" #include "AvnView.h" - -@interface AvnAccessibilityElement (Events) -- (void) raiseChildrenChanged; -@end - -@interface AvnRootAccessibilityElement : AvnAccessibilityElement -- (AvnView *) ownerView; -- (AvnRootAccessibilityElement *) initWithPeer:(IAvnAutomationPeer *) peer owner:(AvnView*) owner; -- (void) raiseFocusChanged; -@end - -class AutomationNode : public ComSingleObject -{ -public: - FORWARD_IUNKNOWN() - - AutomationNode(AvnAccessibilityElement* owner) - { - _owner = owner; - } - - AvnAccessibilityElement* GetOwner() - { - return _owner; - } - - virtual void Dispose() override - { - _owner = nil; - } - - virtual void ChildrenChanged () override - { - [_owner raiseChildrenChanged]; - } - - virtual void PropertyChanged (AvnAutomationProperty property) override - { - - } - - virtual void FocusChanged () override - { - [(AvnRootAccessibilityElement*)_owner raiseFocusChanged]; - } - -private: - __strong AvnAccessibilityElement* _owner; -}; +#include "WindowInterfaces.h" @implementation AvnAccessibilityElement { IAvnAutomationPeer* _peer; - AutomationNode* _node; + AvnAutomationNode* _node; NSMutableArray* _children; } -+ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer ++ (id _Nullable)acquire:(IAvnAutomationPeer *)peer { if (peer == nullptr) return nil; @@ -68,7 +21,7 @@ private: auto instance = peer->GetNode(); if (instance != nullptr) - return dynamic_cast(instance)->GetOwner(); + return dynamic_cast(instance)->GetOwner(); if (peer->IsRootProvider()) { @@ -82,7 +35,7 @@ private: auto holder = dynamic_cast(window); auto view = holder->GetNSView(); - return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; + return [view window]; } else { @@ -94,7 +47,7 @@ private: { self = [super init]; _peer = peer; - _node = new AutomationNode(self); + _node = new AvnAutomationNode(self); _peer->SetNode(_node); return self; } @@ -256,25 +209,8 @@ private: - (NSRect)accessibilityFrame { - id topLevel = [self accessibilityTopLevelUIElement]; - auto result = NSZeroRect; - - if ([topLevel isKindOfClass:[AvnRootAccessibilityElement class]]) - { - auto root = (AvnRootAccessibilityElement*)topLevel; - auto view = [root ownerView]; - - if (view) - { - auto window = [view window]; - auto bounds = ToNSRect(_peer->GetBoundingRectangle()); - auto windowBounds = [view convertRect:bounds toView:nil]; - auto screenBounds = [window convertRectToScreen:windowBounds]; - result = screenBounds; - } - } - - return result; + auto bounds = _peer->GetBoundingRectangle(); + return [self rectToScreen:bounds]; } - (id)accessibilityParent @@ -389,6 +325,24 @@ private: return [super isAccessibilitySelectorAllowed:selector]; } +- (NSRect)rectToScreen:(AvnRect)rect +{ + id topLevel = [self accessibilityTopLevelUIElement]; + + if (![topLevel isKindOfClass:[AvnWindow class]]) + return NSZeroRect; + + auto window = (AvnWindow*)topLevel; + auto view = [window view]; + + if (view == nil) + return NSZeroRect; + + auto nsRect = ToNSRect(rect); + auto windowRect = [view convertRect:nsRect toView:nil]; + return [window convertRectToScreen:windowRect]; +} + - (void)raiseChildrenChanged { auto changed = _children ? [NSMutableSet setWithArray:_children] : [NSMutableSet set]; @@ -429,7 +383,7 @@ private: if (childPeers->Get(i, &child) == S_OK) { - auto element = [AvnAccessibilityElement acquire:child]; + id element = [AvnAccessibilityElement acquire:child]; [_children addObject:element]; } } @@ -441,64 +395,3 @@ private: } @end - -@implementation AvnRootAccessibilityElement -{ - AvnView* _owner; -} - -- (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:(AvnView *)owner -{ - self = [super initWithPeer:peer]; - _owner = owner; - - // Seems we need to raise a focus changed notification here if we have focus - auto focusedPeer = [self peer]->RootProvider_GetFocus(); - id focused = [AvnAccessibilityElement acquire:focusedPeer]; - - if (focused) - NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); - - return self; -} - -- (AvnView *)ownerView -{ - return _owner; -} - -- (id)accessibilityFocusedUIElement -{ - auto focusedPeer = [self peer]->RootProvider_GetFocus(); - return [AvnAccessibilityElement acquire:focusedPeer]; -} - -- (id)accessibilityHitTest:(NSPoint)point -{ - auto clientPoint = [[_owner window] convertPointFromScreen:point]; - auto localPoint = [_owner translateLocalPoint:ToAvnPoint(clientPoint)]; - auto hit = [self peer]->RootProvider_GetPeerFromPoint(localPoint); - return [AvnAccessibilityElement acquire:hit]; -} - -- (id)accessibilityParent -{ - return _owner; -} - -- (void)raiseFocusChanged -{ - id focused = [self accessibilityFocusedUIElement]; - NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); -} - -// Although this method is marked as deprecated we get runtime warnings if we don't handle it. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)accessibilityPerformAction:(NSAccessibilityActionName)action -{ - [_owner accessibilityPerformAction:action]; -} -#pragma clang diagnostic pop - -@end diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index e172980981..761fa0ee67 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -417,10 +417,8 @@ namespace Avalonia.IntegrationTests.Appium { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed - // but in the meantime use the `parent::' selector to return the parent "real" window. return _session.FindElementByXPath( - $"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow"); + $"XCUIElementTypeWindow[@identifier='{identifier}']"); } else { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 53a41f5d46..4a1e338da5 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -448,10 +448,7 @@ namespace Avalonia.IntegrationTests.Appium private AppiumWebElement GetWindow(string identifier) { - // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed - // but in the meantime use the `parent::' selector to return the parent "real" window. - return _session.FindElementByXPath( - $"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow"); + return _session.FindElementByXPath($"XCUIElementTypeWindow[@identifier='{identifier}']"); } private int GetWindowOrder(string identifier) From 1eb3a5c54bec71ec909f5fbe5c6f6289531a15fa Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 25 Jun 2024 21:34:37 +0200 Subject: [PATCH 04/77] Make HorizontalHeadTable.Load nullable so we can handle font that do not have that table (#16064) --- .../Media/Fonts/Tables/HorizontalHeadTable.cs | 4 ++-- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs index 10b831dd1e..0942296536 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs @@ -59,11 +59,11 @@ namespace Avalonia.Media.Fonts.Tables public short XMaxExtent { get; } - public static HorizontalHeadTable Load(IGlyphTypeface glyphTypeface) + public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface) { if (!glyphTypeface.TryGetTable(Tag, out var table)) { - throw new MissingFontTableException("Could not load table", "name"); + return null; } using var stream = new MemoryStream(table); diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index f563143507..e1fe0251d2 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia private readonly SKTypeface _typeface; private readonly NameTable _nameTable; private readonly OS2Table? _os2Table; - private readonly HorizontalHeadTable _hhTable; + private readonly HorizontalHeadTable? _hhTable; private IReadOnlyList? _supportedFeatures; public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) @@ -38,9 +38,9 @@ namespace Avalonia.Skia _os2Table = OS2Table.Load(this); _hhTable = HorizontalHeadTable.Load(this); - int ascent; - int descent; - int lineGap; + var ascent = 0; + var descent = 0; + var lineGap = 0; if (_os2Table != null && (_os2Table.FontStyle & OS2Table.FontStyleSelection.USE_TYPO_METRICS) != 0) { @@ -50,9 +50,12 @@ namespace Avalonia.Skia } else { - ascent = -_hhTable.Ascender; - descent = -_hhTable.Descender; - lineGap = _hhTable.LineGap; + if (_hhTable != null) + { + ascent = -_hhTable.Ascender; + descent = -_hhTable.Descender; + lineGap = _hhTable.LineGap; + } } if (_os2Table != null && (ascent == 0 || descent == 0)) From 6660d7572b9382ce96590dfa61e5436d39f2dd51 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 25 Jun 2024 12:35:24 -0700 Subject: [PATCH 05/77] Fix compiled binding indexer always forcing integer boxing (#16109) --- .../XamlIlClrPropertyInfoHelper.cs | 12 ++++---- .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 161bbb09ce..16e66c5b72 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -34,9 +34,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return baseKey + $"[{indexerArgumentsKey}]"; } - public IXamlType Emit(XamlEmitContext context, IXamlILEmitter codeGen, IXamlProperty property, IEnumerable indexerArguments = null, string indexerArgumentsKey = null) + public IXamlType Emit(XamlEmitContext context, IXamlILEmitter codeGen, IXamlProperty property, IReadOnlyCollection indexerArguments = null, string indexerArgumentsKey = null) { - indexerArguments = indexerArguments ?? Enumerable.Empty(); + indexerArguments = indexerArguments ?? Array.Empty(); var types = context.GetAvaloniaTypes(); IXamlMethod Get() { @@ -101,10 +101,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Load(property.Setter, setter.Generator, !property.Getter.IsStatic); setter.Generator.Ldarg(1); - if (property.Setter.Parameters[0].IsValueType) - setter.Generator.Unbox_Any(property.Setter.Parameters[0]); + + var valueIndex = indexerArguments.Count; + if (property.Setter.Parameters[valueIndex].IsValueType) + setter.Generator.Unbox_Any(property.Setter.Parameters[valueIndex]); else - setter.Generator.Castclass(property.Setter.Parameters[0]); + setter.Generator.Castclass(property.Setter.Parameters[valueIndex]); setter.Generator .EmitCall(property.Setter, true) .Ret(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 7e25215f70..b48b4508e3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -305,6 +305,36 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void IndexerSetterBindsCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + +"); + var textBox = window.GetControl("textBox"); + + var dataContext = new TestDataContext + { + ListProperty = { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty[3], textBox.Text); + + textBox.Text = "Z"; + + Assert.Equal("Z", dataContext.ListProperty[3]); + Assert.Equal(dataContext.ListProperty[3], textBox.Text); + } + } + [Fact] public void ResolvesArrayIndexerBindingCorrectly() { From b2f3fad445cf643a4bebae0f5801bbf205b08a38 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 25 Jun 2024 21:35:37 +0200 Subject: [PATCH 06/77] Respect global packages folder in BuildToNuGetCache (#15706) --- nukebuild/Build.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 9157e4c44b..d1d6e043c1 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -19,6 +19,7 @@ using static Nuke.Common.Tools.Xunit.XunitTasks; using static Nuke.Common.Tools.VSWhere.VSWhereTasks; using static Serilog.Log; using MicroCom.CodeGenerator; +using NuGet.Configuration; using Nuke.Common.IO; /* @@ -366,6 +367,9 @@ partial class Build : NukeBuild { if (!Parameters.IsPackingToLocalCache) throw new InvalidOperationException(); + + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder( + Settings.LoadDefaultSettings(RootDirectory)); foreach (var path in Parameters.NugetRoot.GlobFiles("*.nupkg")) { @@ -376,11 +380,11 @@ partial class Build : NukeBuild .Elements().First(x => x.Name.LocalName == "metadata") .Elements().First(x => x.Name.LocalName == "id").Value; - var packagePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".nuget", - "packages", + var packagePath = Path.Combine( + globalPackagesFolder, packageId.ToLowerInvariant(), BuildParameters.LocalBuildVersion); + if (Directory.Exists(packagePath)) Directory.Delete(packagePath, true); Directory.CreateDirectory(packagePath); From 47cbad7fd475e2d3cf9c6215e8e637a8670e9de0 Mon Sep 17 00:00:00 2001 From: Meloman19 Date: Tue, 25 Jun 2024 22:36:02 +0300 Subject: [PATCH 07/77] Remove from logical children and clear item container on reset (#15855) * Remove from logical children and clear item container on reset (similarly like OnItemsChanged.Remove) * Add simple test --------- Co-authored-by: Meloman19 <23280622+Meloman19@users.noreply.github.com> --- .../Presenters/PanelContainerGenerator.cs | 5 +++- .../ItemsControlTests.cs | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index cba729d9d2..4027cd252f 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -136,12 +136,15 @@ namespace Avalonia.Controls.Presenters return; var itemsControl = _presenter.ItemsControl; + var generator = itemsControl.ItemContainerGenerator; var panel = _presenter.Panel; foreach (var c in panel.Children) { + itemsControl.RemoveLogicalChild(c); + if (!c.IsSet(ItemIsOwnContainerProperty)) - itemsControl.RemoveLogicalChild(c); + generator.ClearItemContainer(c); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 40a7c21dbd..3983ac503d 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -976,6 +976,29 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void ContainerClearing_Is_Raised_When_ItemsSource_Is_Cleared() + { + using var app = Start(); + var itemsSource = new ObservableCollection { "Foo", "Bar", "Baz" }; + var target = CreateTarget(itemsSource: itemsSource); + + var expectedContainers = itemsSource.Select(x => target.ContainerFromItem(x)).ToArray(); + var actualContainers = new List(); + var raised = 0; + + target.ContainerClearing += (s, e) => + { + actualContainers.Add(e.Container); + ++raised; + }; + + itemsSource.Clear(); + + Assert.Equal(3, raised); + Assert.Equal(expectedContainers, actualContainers); + } + [Fact] public void Handles_Recycling_Control_Items_Inside_Containers() { From 7e8c69ec82647ab57e031f9dadd7e5e838c89c52 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 25 Jun 2024 22:36:26 +0300 Subject: [PATCH 08/77] DBus init and usage fixes (#16119) --- src/Avalonia.FreeDesktop/DBusHelper.cs | 30 ++++++++++++++----- .../DBusIme/X11DBusImeHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- .../DBusPlatformSettings.cs | 6 ++-- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 9 ++++-- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 ++ src/Avalonia.X11/X11Platform.cs | 3 -- 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index da74f15a3e..b733176243 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -7,37 +7,51 @@ namespace Avalonia.FreeDesktop { internal static class DBusHelper { - public static Connection? Connection { get; private set; } + private static Connection? s_defaultConntection; + private static bool s_defaultConnectionFailed; + public static Connection? DefaultConnection + { + get + { + if (s_defaultConntection == null && !s_defaultConnectionFailed) + { + s_defaultConntection = TryCreateNewConnection(); + if (s_defaultConntection == null) + s_defaultConnectionFailed = true; + } - public static Connection? TryInitialize(string? dbusAddress = null) - => Connection ?? TryCreateNewConnection(dbusAddress); + return s_defaultConntection; + } + } public static Connection? TryCreateNewConnection(string? dbusAddress = null) { var oldContext = SynchronizationContext.Current; + Connection? conn = null; try { - var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) + SynchronizationContext.SetSynchronizationContext(null); + conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) { - AutoConnect = false + AutoConnect = false, }); // Connect synchronously conn.ConnectAsync().GetAwaiter().GetResult(); - - Connection = conn; + return conn; } catch (Exception e) { Logger.TryGet(LogEventLevel.Error, "DBUS") ?.Log(null, "Unable to connect to DBus: " + e); + conn?.Dispose(); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } - return Connection; + return null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 8042d3bff2..babf5cf417 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -37,7 +37,7 @@ namespace Avalonia.FreeDesktop.DBusIme var factory = DetectInputMethod(); if (factory is not null) { - var conn = DBusHelper.TryInitialize(); + var conn = DBusHelper.DefaultConnection; if (conn is not null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index fcc118bd31..e7bd23e965 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop internal class DBusMenuExporter { public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => - DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid); + DBusHelper.DefaultConnection is {} conn ? new DBusMenuExporterImpl(conn, xid) : null; public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => new DBusMenuExporterImpl(currentConnection, path); diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 852ed90764..ee59982ba7 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Threading; using Tmds.DBus.Protocol; using Tmds.DBus.SourceGenerator; @@ -17,10 +18,11 @@ namespace Avalonia.FreeDesktop public DBusPlatformSettings() { - if (DBusHelper.Connection is null) + if (DBusHelper.DefaultConnection is not {} conn) return; + using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input); - _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _settings = new OrgFreedesktopPortalSettings(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); _ = TryGetInitialValuesAsync(); } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 7ee963fc17..50033fff29 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; +using Avalonia.Threading; using Tmds.DBus.Protocol; using Tmds.DBus.SourceGenerator; @@ -16,10 +17,12 @@ namespace Avalonia.FreeDesktop { internal static async Task TryCreateAsync(IPlatformHandle handle) { - if (DBusHelper.Connection is null) + if (DBusHelper.DefaultConnection is not {} conn) return null; - var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input); + + var dbusFileChooser = new OrgFreedesktopPortalFileChooser(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); uint version; try { @@ -30,7 +33,7 @@ namespace Avalonia.FreeDesktop return null; } - return new DBusSystemDialog(DBusHelper.Connection, handle, dbusFileChooser, version); + return new DBusSystemDialog(conn, handle, dbusFileChooser, version); } private readonly Connection _connection; diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index f7e95b83d0..d6773559dd 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Threading; using Tmds.DBus.Protocol; using Tmds.DBus.SourceGenerator; @@ -38,6 +39,7 @@ namespace Avalonia.FreeDesktop public DBusTrayIconImpl() { + using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input); _connection = DBusHelper.TryCreateNewConnection(); if (_connection is null) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e16cb0d98c..905e9e1346 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -68,9 +68,6 @@ namespace Avalonia.X11 Info = new X11Info(Display, DeferredDisplay, useXim); Globals = new X11Globals(this); Resources = new XResources(this); - //TODO: log - if (options.UseDBusMenu) - DBusHelper.TryInitialize(); IRenderTimer timer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) From fb4a83903e5aaedad185ceecb242dd1984935f19 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 25 Jun 2024 19:45:23 +0000 Subject: [PATCH 09/77] don't show selector when no text is ready (#15770) --- .../Primitives/TextSelectionCanvas.cs | 25 +++++++++++- .../Primitives/TextSelectionHandle.cs | 40 ++++++++++++------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs index 07e48653d7..3ec9221539 100644 --- a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs +++ b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives private TextPresenter? _presenter; private TextBox? _textBox; private bool _showHandle; + private bool _canShowContextMenu = true; internal bool ShowHandles { @@ -33,7 +35,7 @@ namespace Avalonia.Controls.Primitives _caretHandle.IsVisible = false; } - IsVisible = value; + IsVisible = !string.IsNullOrEmpty(_presenter?.Text) && value; } } @@ -65,11 +67,20 @@ namespace Avalonia.Controls.Primitives _caretHandle.SetTopLeft(default); _endHandle.SetTopLeft(default); + _startHandle.PointerReleased += Handle_PointerReleased; + _caretHandle.PointerReleased += Handle_PointerReleased; + _endHandle.PointerReleased += Handle_PointerReleased; + IsVisible = ShowHandles; ClipToBounds = false; } + private void Handle_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + ShowContextMenu(); + } + private void Handle_DragStarted(object? sender, VectorEventArgs e) { if (_textBox?.ContextFlyout is { } flyout) @@ -92,6 +103,7 @@ namespace Avalonia.Controls.Primitives private void CaretHandle_DragDelta(object? sender, VectorEventArgs e) { + _canShowContextMenu = false; if (_presenter != null && _textBox != null) { var point = ToPresenter(_caretHandle.IndicatorPosition); @@ -267,6 +279,7 @@ namespace Avalonia.Controls.Primitives _textBox.PropertyChanged += TextBoxPropertyChanged; _textBox.EffectiveViewportChanged += TextBoxEffectiveViewportChanged; + _textBox.SizeChanged += TextBox_SizeChanged; } } else @@ -281,12 +294,18 @@ namespace Avalonia.Controls.Primitives _textBox.PropertyChanged -= TextBoxPropertyChanged; _textBox.EffectiveViewportChanged -= TextBoxEffectiveViewportChanged; + _textBox.SizeChanged -= TextBox_SizeChanged; } _textBox = null; } } + private void TextBox_SizeChanged(object? sender, SizeChangedEventArgs e) + { + InvalidateMeasure(); + } + private void TextBoxEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) { if (ShowHandles) @@ -304,7 +323,7 @@ namespace Avalonia.Controls.Primitives internal bool ShowContextMenu() { - if (_textBox != null) + if (_textBox != null && _canShowContextMenu) { if (_textBox.ContextFlyout is PopupFlyoutBase flyout) { @@ -345,6 +364,8 @@ namespace Avalonia.Controls.Primitives } } + _canShowContextMenu = true; + return false; } diff --git a/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs index d9bf8b5529..550f82fe84 100644 --- a/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs +++ b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs @@ -134,32 +134,42 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerMoved(PointerEventArgs e) { - if (_lastPoint.HasValue) + VectorEventArgs ev; + + if (!_lastPoint.HasValue) { - var ev = new VectorEventArgs + _lastPoint = e.GetPosition(VisualRoot as Visual); + e.Pointer.Capture(this); + + ev = new VectorEventArgs { - RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(VisualRoot as Visual) - _lastPoint.Value, + RoutedEvent = DragStartedEvent, + Vector = (Vector)_lastPoint, }; + } + else + { + var vector = e.GetPosition(VisualRoot as Visual) - _lastPoint.Value; - RaiseEvent(ev); + var tapSize = TopLevel.GetTopLevel(this)?.PlatformSettings?.GetTapSize(PointerType.Touch) ?? new Size(10, 10); + + if (Math.Abs(vector.X) < tapSize.Width && Math.Abs(vector.Y) < tapSize.Height) + return; + + ev = new VectorEventArgs + { + RoutedEvent = DragDeltaEvent, + Vector = vector, + }; } + + RaiseEvent(ev); } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(VisualRoot as Visual); - - var ev = new VectorEventArgs - { - RoutedEvent = DragStartedEvent, - Vector = (Vector)_lastPoint, - }; - PseudoClasses.Add(":pressed"); - - RaiseEvent(ev); } protected override void OnPointerReleased(PointerReleasedEventArgs e) From b1e96ad6ef09d012be30efcbf050f47cb4e7fbf4 Mon Sep 17 00:00:00 2001 From: Lubomir Tetak <50887170+ltetak@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:45:51 +0200 Subject: [PATCH 10/77] Notify window on mouseEntered (#15251) --- native/Avalonia.Native/src/OSX/AvnView.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 89ac933eec..554f1b6deb 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -442,6 +442,7 @@ - (void)mouseEntered:(NSEvent *)event { + [self mouseEvent:event withType:Move]; [super mouseEntered:event]; } From 0b5adbdceb2c0b43b5a2dde032abb9ef40dcac39 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jun 2024 04:49:39 +0200 Subject: [PATCH 11/77] Fix `ICommand` bindings in style setters (#16122) * Add failing test for #16113. * Convert delegate to ICommand in style setter. When compiling a binding to e.g. `Button.Command` in a style `Setter`, we were not converting `XamlIlClrMethodPathElementNode` to `XamlIlClrMethodAsCommandPathElementNode` as we were only testing whether the property that the binding was being assigned to is an `ICommand`. If we detect that we're assigning the binding to a `Setter.Value` then we need to look in the `Setter.Property` to see check whether the property is an `ICommand` too. Fixes #16113 --- .../AvaloniaXamlIlWellKnownTypes.cs | 2 ++ .../XamlIlBindingPathHelper.cs | 27 +++++++++++++++-- .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 7d50e80c45..9bad038d42 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -108,6 +108,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType TextDecorations { get; } public IXamlType TextTrimming { get; } public IXamlType SetterBase { get; } + public IXamlType Setter { get; } public IXamlType IStyle { get; } public IXamlType StyleInclude { get; } public IXamlType ResourceInclude { get; } @@ -297,6 +298,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations"); TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); SetterBase = cfg.TypeSystem.GetType("Avalonia.Styling.SetterBase"); + Setter = cfg.TypeSystem.GetType("Avalonia.Styling.Setter"); IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 51675bbb83..21a3f4eae2 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -85,10 +85,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { return transformed; } - + var lastElement = transformed.Elements.LastOrDefault(); - if (parentNode.Property?.Getter?.ReturnType == context.GetAvaloniaTypes().ICommand && lastElement is XamlIlClrMethodPathElementNode methodPathElement) + if (GetPropertyType(context, parentNode) == context.GetAvaloniaTypes().ICommand && lastElement is XamlIlClrMethodPathElementNode methodPathElement) { IXamlMethod executeMethod = methodPathElement.Method; IXamlMethod canExecuteMethod = executeMethod.DeclaringType.FindMethod(new FindMethodMethodSignature($"Can{executeMethod.Name}", context.Configuration.WellKnownTypes.Boolean, context.Configuration.WellKnownTypes.Object)); @@ -110,6 +110,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return transformed; } + private static IXamlType GetPropertyType(AstTransformationContext context, XamlPropertyAssignmentNode node) + { + var setterType = context.GetAvaloniaTypes().Setter; + + if (node.Property.DeclaringType == setterType && node.Property.Name == "Value") + { + // The property is a Setter.Value property. We need to get the type of the property that the Setter.Value property is setting. + var setter = context.ParentNodes() + .SkipWhile(x => x != node) + .OfType() + .Take(1) + .FirstOrDefault(x => x.Type.GetClrType() == setterType); + var propertyAssignment = setter?.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); + var property = propertyAssignment?.Values.FirstOrDefault() as IXamlIlAvaloniaPropertyNode; + + if (property.AvaloniaPropertyType is { } propertyType) + return propertyType; + } + + return node.Property?.Getter?.ReturnType; + } + private static XamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, Func startTypeResolver, IXamlType selfType, IEnumerable bindingExpression) { List transformNodes = new List(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index b48b4508e3..5e0813c9e6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1958,6 +1958,36 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binding_Method_To_Command_In_Style_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +