diff --git a/Avalonia.sln b/Avalonia.sln index d5419365ac..98ad0cadae 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -91,7 +91,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject - build\ApiDiff.props = build\ApiDiff.props build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props diff --git a/NuGet.Config b/NuGet.Config index 7d2bd8abd2..2042fea360 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,8 +3,7 @@ - - + diff --git a/build/ApiDiff.props b/build/ApiDiff.props deleted file mode 100644 index b0b1942f60..0000000000 --- a/build/ApiDiff.props +++ /dev/null @@ -1,13 +0,0 @@ - - - 0.10.0 - $(PackageId) - Avalonia - false - - - - - - - diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index a4999b8df3..ea6ba93fdb 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -538,10 +538,10 @@ { _lastKeyHandled = false; - [[self inputContext] handleEvent:event]; + [self keyboardEvent:event withType:KeyDown]; if(!_lastKeyHandled){ - [self keyboardEvent:event withType:KeyDown]; + [[self inputContext] handleEvent:event]; } } diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index d0c8d7a9db..9fe0ff3c60 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -73,6 +73,13 @@ private: if (peer->IsRootProvider()) { auto window = peer->RootProvider_GetWindow(); + + if (window == nullptr) + { + NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl."); + return nil; + } + auto holder = dynamic_cast(window); auto view = holder->GetNSView(); return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; @@ -284,8 +291,8 @@ private: - (id)accessibilityWindow { - id topLevel = [self accessibilityTopLevelUIElement]; - return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; + auto rootPeer = _peer->GetVisualRoot(); + return [AvnAccessibilityElement acquire:rootPeer]; } - (BOOL)isAccessibilityExpanded diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index d80df68fea..56b7ce97e0 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -17,7 +17,6 @@ public: Cancelled = true; if(Running) { - Running = false; if(![NSThread isMainThread]) { AddRef(); @@ -28,22 +27,22 @@ public: }); return; }; + + Running = false; if(IsApp) [NSApp stop:nil]; - else - { - // Wakeup the event loop - NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined - location:NSMakePoint(0, 0) - modifierFlags:0 - timestamp:0 - windowNumber:0 - context:nil - subtype:0 - data1:0 - data2:0]; - [NSApp postEvent:event atStart:YES]; - } + + // Wakeup the event loop + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; } }; }; diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4258572df7..ebfa325067 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 16eb09de65..9aeb71f649 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs index 639498543b..1ac2520919 100644 --- a/src/Avalonia.Base/Media/RenderOptions.cs +++ b/src/Avalonia.Base/Media/RenderOptions.cs @@ -8,12 +8,13 @@ namespace Avalonia.Media public EdgeMode EdgeMode { get; init; } public TextRenderingMode TextRenderingMode { get; init; } public BitmapBlendingMode BitmapBlendingMode { get; init; } + public bool? RequiresFullOpacityHandling { get; init; } /// /// Gets the value of the BitmapInterpolationMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual) { return visual.RenderOptions.BitmapInterpolationMode; @@ -23,7 +24,7 @@ namespace Avalonia.Media /// Sets the value of the BitmapInterpolationMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value) { visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value }; @@ -33,7 +34,7 @@ namespace Avalonia.Media /// Gets the value of the BitmapBlendingMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual) { return visual.RenderOptions.BitmapBlendingMode; @@ -53,7 +54,7 @@ namespace Avalonia.Media /// Gets the value of the EdgeMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static EdgeMode GetEdgeMode(Visual visual) { return visual.RenderOptions.EdgeMode; @@ -63,7 +64,7 @@ namespace Avalonia.Media /// Sets the value of the EdgeMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetEdgeMode(Visual visual, EdgeMode value) { visual.RenderOptions = visual.RenderOptions with { EdgeMode = value }; @@ -73,7 +74,7 @@ namespace Avalonia.Media /// Gets the value of the TextRenderingMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static TextRenderingMode GetTextRenderingMode(Visual visual) { return visual.RenderOptions.TextRenderingMode; @@ -83,12 +84,32 @@ namespace Avalonia.Media /// Sets the value of the TextRenderingMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) { visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value }; } + /// + /// Gets the value of the RequiresFullOpacityHandling attached property for a visual. + /// + /// The control. + /// The value. + public static bool? GetRequiresFullOpacityHandling(Visual visual) + { + return visual.RenderOptions.RequiresFullOpacityHandling; + } + + /// + /// Sets the value of the RequiresFullOpacityHandling attached property for a visual. + /// + /// The control. + /// The value. + public static void SetRequiresFullOpacityHandling(Visual visual, bool? value) + { + visual.RenderOptions = visual.RenderOptions with { RequiresFullOpacityHandling = value }; + } + public RenderOptions MergeWith(RenderOptions other) { var bitmapInterpolationMode = BitmapInterpolationMode; @@ -119,12 +140,20 @@ namespace Avalonia.Media bitmapBlendingMode = other.BitmapBlendingMode; } + var requiresFullOpacityHandling = RequiresFullOpacityHandling; + + if (requiresFullOpacityHandling == null) + { + requiresFullOpacityHandling = other.RequiresFullOpacityHandling; + } + return new RenderOptions { BitmapInterpolationMode = bitmapInterpolationMode, EdgeMode = edgeMode, TextRenderingMode = textRenderingMode, - BitmapBlendingMode = bitmapBlendingMode + BitmapBlendingMode = bitmapBlendingMode, + RequiresFullOpacityHandling = requiresFullOpacityHandling }; } } diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs index c830ca2c49..27aca436b8 100644 --- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs @@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition; internal interface ICompositionTargetDebugEvents { + int RenderedVisuals { get; } + void IncrementRenderedVisuals(); void RectInvalidated(Rect rc); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs index c1037d5c67..51414c2250 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs @@ -38,6 +38,7 @@ partial class ServerCompositionVisual CompositionVisualChangedFields.Size | CompositionVisualChangedFields.SizeAnimated | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.Clip | CompositionVisualChangedFields.ClipToBoundsAnimated; partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 45515a37e2..aeb228282e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; + Root!.DebugEvents?.IncrementRenderedVisuals(); var boundsRect = new Rect(new Size(Size.X, Size.Y)); @@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server if (_clipSizeDirty || positionChanged) { - _transformedClipBounds = ClipToBounds - ? new Rect(new Size(Size.X, Size.Y)) - .TransformToAABB(GlobalTransformMatrix) - : null; + Rect? transformedVisualBounds = null; + Rect? transformedClipBounds = null; + if (ClipToBounds) + transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix); + + if (Clip != null) + transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix); + + if (transformedVisualBounds != null && transformedClipBounds != null) + _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value); + else if (transformedVisualBounds != null) + _transformedClipBounds = transformedVisualBounds; + else if (transformedClipBounds != null) + _transformedClipBounds = transformedClipBounds; + else + _transformedClipBounds = null; + _clipSizeDirty = false; } diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 5a31053bdc..8a36e6900a 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index 6556ce721e..6c1e019603 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 0cc620dae9..2d40721bbf 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -170,26 +170,28 @@ CornerRadius="{TemplateBinding CornerRadius}"> - - - - - - - - - - + + + + + + + + + + + - diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 3d3fe35d29..fb7cdd87ed 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Automation.Provider; namespace Avalonia.Automation.Peers { @@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers /// /// Gets the that is the parent of this . /// - /// public AutomationPeer? GetParent() => GetParentCore(); + /// + /// Gets the that is the root of this 's + /// visual tree. + /// + public AutomationPeer? GetVisualRoot() => GetVisualRootCore(); + /// /// Gets a value that indicates whether the element that is associated with this automation /// peer currently has keyboard focus. @@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } + protected virtual AutomationPeer? GetVisualRootCore() + { + var peer = this; + var parent = peer.GetParent(); + + while (peer.GetProvider() is null && parent is not null) + { + peer = parent; + parent = peer.GetParent(); + } + + return peer; + } + + protected virtual bool IsContentElementOverrideCore() { return IsControlElement() && IsContentElementCore(); diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index c19d887230..69f267a605 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers return _parent; } + protected override AutomationPeer? GetVisualRootCore() + { + if (Owner.GetVisualRoot() is Control c) + return CreatePeerForElement(c); + return null; + } + /// /// Invalidates the peer's children and causes a re-read from . /// diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index db16bf0a53..64727c43c5 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers if (!_searchedForScrollable) { if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) - _scroller = GetOrCreate(scrollable) as IScrollProvider; + _scroller = GetOrCreate(scrollable).GetProvider(); _searchedForScrollable = true; } diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index aea91b5e26..dab8c45567 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is Control parent) { var parentPeer = GetOrCreate(parent); - return parentPeer as ISelectionProvider; + return parentPeer.GetProvider(); } return null; diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index 9ec65592fa..ceb695422d 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Globalization; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Input; @@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers public AutomationPeer? GetPeerFromPoint(Point p) { var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); - return hit is object ? GetOrCreate(hit) : null; + + if (hit is null) + return null; + + var peer = GetOrCreate(hit); + + while (peer != this && peer.GetProvider() is { } embedded) + { + var embeddedHit = embedded.GetPeerFromPoint(p); + if (embeddedHit is null) + break; + peer = embeddedHit; + } + + return peer; } protected void StartTrackingFocus() diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs new file mode 100644 index 0000000000..1b1caef182 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposure methods and properties to support UI Automation client access to the root of an + /// automation tree hosted by another UI framework. + /// + /// + /// This interface is implemented by the class, and can be used + /// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's + /// automation support. + /// + public interface IEmbeddedRootProvider + { + /// + /// Gets the currently focused element. + /// + AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. + AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// + event EventHandler? FocusChanged; + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index ce38059559..6a266da5c5 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -4,11 +4,36 @@ using Avalonia.Platform; namespace Avalonia.Automation.Provider { + /// + /// Exposes methods and properties to support UI Automation client access to the root of an + /// automation tree. + /// + /// + /// This interface is implemented by the class, and should only + /// be implemented on true root elements, such as Windows. To embed an automation tree, use + /// instead. + /// public interface IRootProvider { + /// + /// Gets the platform implementation of the TopLevel for the element. + /// ITopLevelImpl? PlatformImpl { get; } + + /// + /// Gets the currently focused element. + /// AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// event EventHandler? FocusChanged; } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 304454777f..0dda861448 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,7 +6,6 @@ - diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 867dca2be1..3564ce1faa 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -40,11 +40,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalContentAlignment)); - static ContentControl() - { - ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - } - /// /// Gets or sets the content to display. /// @@ -100,6 +95,16 @@ namespace Avalonia.Controls { return RegisterContentPresenter(presenter); } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ContentProperty) + { + ContentChanged(change); + } + } /// /// Called when an is registered with the control. diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index e3a419d7b3..78f6c5f77d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -114,7 +114,6 @@ namespace Avalonia.Controls /// static ContextMenu() { - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); PlacementProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); @@ -216,18 +215,23 @@ namespace Avalonia.Controls if (e.OldValue is ContextMenu oldMenu) { control.ContextRequested -= ControlContextRequested; + control.AttachedToVisualTree -= ControlOnAttachedToVisualTree; control.DetachedFromVisualTree -= ControlDetachedFromVisualTree; oldMenu._attachedControls?.Remove(control); ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null); } - if (e.NewValue is ContextMenu newMenu) + if (e.NewValue is ContextMenu) { - newMenu._attachedControls ??= new List(); - newMenu._attachedControls.Add(control); control.ContextRequested += ControlContextRequested; + control.AttachedToVisualTree += ControlOnAttachedToVisualTree; control.DetachedFromVisualTree += ControlDetachedFromVisualTree; } + + if (control.IsAttachedToVisualTree) + { + AttachControlToContextMenu(control); + } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -428,11 +432,25 @@ namespace Avalonia.Controls e.Handled = true; } } + + + private static void ControlOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + AttachControlToContextMenu(sender); + } + + private static void AttachControlToContextMenu(object? sender) + { + if (sender is Control { ContextMenu: { } contextMenu } control) + { + contextMenu._attachedControls ??= new List(); + contextMenu._attachedControls.Add(control); + } + } private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { - if (sender is Control control - && control.ContextMenu is ContextMenu contextMenu) + if (sender is Control { ContextMenu: { } contextMenu } control) { if (contextMenu._popup?.Parent == control) { @@ -440,6 +458,7 @@ namespace Avalonia.Controls } contextMenu.Close(); + contextMenu._attachedControls?.Remove(control); } } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index f9aa5fc7a0..d823455e13 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); - AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); + AffectsArrange(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } public Track() diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index ff8bd5dc0b..c493445ba1 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Styling; @@ -36,8 +37,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsCheckedProperty = - AvaloniaProperty.Register( - nameof(IsChecked)); + AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay); /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 98b3b13c17..8a5ac4e495 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1879,6 +1879,37 @@ namespace Avalonia.Controls return text.Substring(start, end - start); } + /// + /// Returns the sum of any vertical whitespace added between the and in the control template. + /// + /// The total vertical whitespace. + private double GetVerticalSpaceBetweenScrollViewerAndPresenter() + { + var verticalSpace = 0.0; + if (_presenter != null) + { + Visual? visual = _presenter; + while ((visual != null) && (visual != this)) + { + if (visual == _scrollViewer) + { + // ScrollViewer is a stopping point and should only include the Padding + verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom; + break; + } + + var margin = visual.GetValue(Layoutable.MarginProperty); + var padding = visual.GetValue(Decorator.PaddingProperty); + + verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom; + + visual = visual.VisualParent; + } + } + + return verticalSpace; + } + /// /// Raises both the and events. /// @@ -2032,8 +2063,9 @@ namespace Avalonia.Controls var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties); + var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); - maxHeight = Math.Ceiling(textLayout.Height); + maxHeight = Math.Ceiling(textLayout.Height + verticalSpace); } _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index bf540698f1..0b04866d64 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -79,8 +79,12 @@ public class TransitioningContentControl : ContentControl protected override bool RegisterContentPresenter(ContentPresenter presenter) { - if (!base.RegisterContentPresenter(presenter) && - presenter is ContentPresenter p && + if (base.RegisterContentPresenter(presenter)) + { + return true; + } + + if (presenter is ContentPresenter p && p.Name == "PART_ContentPresenter2") { _presenter2 = p; @@ -94,12 +98,13 @@ public class TransitioningContentControl : ContentControl protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(change); - if (change.Property == ContentProperty) { UpdateContent(true); + return; } + + base.OnPropertyChanged(change); } private void UpdateContent(bool withTransition) diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index c5255b22cd..0c1b3e8a45 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,5 +19,4 @@ - diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index d180e6e34d..12cc977c84 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 65d1bea298..135bc1b685 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index be4bae26f1..3a2c18ac24 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 6c4e96b31b..af4958b02f 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -22,8 +22,8 @@ namespace Avalonia.Native { _inner = inner; _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); - if (inner is WindowBaseAutomationPeer window) - window.FocusChanged += (_, _) => Node?.FocusChanged(); + if (inner is IRootProvider root) + root.FocusChanged += (_, _) => Node?.FocusChanged(); } ~AvnAutomationPeer() => Node?.Dispose(); @@ -39,6 +39,7 @@ namespace Avalonia.Native public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); public IAvnString Name => _inner.GetName().ToAvnString(); public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot()); public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); public int IsContentElement() => _inner.IsContentElement().AsComBool(); @@ -48,6 +49,13 @@ namespace Avalonia.Native public void SetFocus() => _inner.SetFocus(); public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + public IAvnAutomationPeer? RootPeer { get @@ -55,7 +63,7 @@ namespace Avalonia.Native var peer = _inner; var parent = peer.GetParent(); - while (peer is not IRootProvider && parent is not null) + while (peer.GetProvider() is null && parent is not null) { peer = parent; parent = peer.GetParent(); @@ -65,26 +73,23 @@ namespace Avalonia.Native } } - public void SetNode(IAvnAutomationNode node) - { - if (Node is not null) - throw new InvalidOperationException("The AvnAutomationPeer already has a node."); - Node = node; - } - - public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider(); + private IExpandCollapseProvider ExpandCollapseProvider => GetProvider(); + private IInvokeProvider InvokeProvider => GetProvider(); + private IRangeValueProvider RangeValueProvider => GetProvider(); + private IRootProvider RootProvider => GetProvider(); + private ISelectionItemProvider SelectionItemProvider => GetProvider(); + private IToggleProvider ToggleProvider => GetProvider(); + private IValueProvider ValueProvider => GetProvider(); - public IAvnWindowBase RootProvider_GetWindow() - { - var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; - return ((WindowBaseImpl)window.PlatformImpl!).Native; - } - - public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); + public int IsRootProvider() => IsProvider(); + + public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native; + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus()); public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) { - var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); if (result is null) return null; @@ -103,46 +108,80 @@ namespace Avalonia.Native return Wrap(result); } - public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); - public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + public int IsEmbeddedRootProvider() => IsProvider(); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus()); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is not null) + result = parent; + else + break; + } + + return Wrap(result); + } + + public int IsExpandCollapseProvider() => IsProvider(); + + public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch { ExpandCollapseState.Expanded => 1, ExpandCollapseState.PartiallyExpanded => 1, _ => 0, }; - public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); - public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); - public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand(); + public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse(); - public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); - public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + public int IsInvokeProvider() => IsProvider(); + public void InvokeProvider_Invoke() => InvokeProvider.Invoke(); - public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); - public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; - public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; - public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; - public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; - public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; - public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + public int IsRangeValueProvider() => IsProvider(); + public double RangeValueProvider_GetValue() => RangeValueProvider.Value; + public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum; + public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum; + public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange; + public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange; + public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value); - public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); - public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); + public int IsSelectionItemProvider() => IsProvider(); + public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool(); - public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); - public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; - public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + public int IsToggleProvider() => IsProvider(); + public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState; + public void ToggleProvider_Toggle() => ToggleProvider.Toggle(); - public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); - public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); - public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); + public int IsValueProvider() => IsProvider(); + public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value); [return: NotNullIfNotNull("peer")] public static AvnAutomationPeer? Wrap(AutomationPeer? peer) { return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); } + + private T GetProvider() + { + return _inner.GetProvider() ?? throw new InvalidOperationException( + $"The peer {_inner} does not implement {typeof(T)}."); + } + + private int IsProvider() => (_inner.GetProvider() is not null).AsComBool(); } internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index bc372bbcb5..0911e5ffff 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -921,6 +921,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetLabeledBy(); IAvnString* GetName(); IAvnAutomationPeer* GetParent(); + IAvnAutomationPeer* GetVisualRoot(); bool HasKeyboardFocus(); bool IsContentElement(); bool IsControlElement(); @@ -935,7 +936,11 @@ interface IAvnAutomationPeer : IUnknown IAvnWindowBase* RootProvider_GetWindow(); IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); - + + bool IsEmbeddedRootProvider(); + IAvnAutomationPeer* EmbeddedRootProvider_GetFocus(); + IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point); + bool IsExpandCollapseProvider(); bool ExpandCollapseProvider_GetIsExpanded(); bool ExpandCollapseProvider_GetShowsMenu(); diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 4cae8e82df..30b5d80fbc 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index baeb8820f8..13d1ff9651 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -10,6 +10,5 @@ - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 660661fc94..e62abff18b 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 39da9a747f..9864cf24e0 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj index 49f1de31f2..d176d74b5f 100644 --- a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj +++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj @@ -12,8 +12,7 @@ - - + diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 1f06f28687..2020dca482 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj index 4ab70eb07d..85879faa4c 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index 893cb0074c..fe071b594f 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 618379757b..d827530207 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -65,7 +65,6 @@ - diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index d041e7d2e6..29583b6cb8 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index fbff4ab4e7..b1b1de0ffe 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -189,7 +189,8 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity))); + + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); @@ -375,7 +376,7 @@ namespace Avalonia.Skia { if (boxShadow != default && !boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -432,7 +433,7 @@ namespace Avalonia.Skia { if (boxShadow != default && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; @@ -592,8 +593,16 @@ namespace Avalonia.Skia { CheckLease(); - if(_useOpacitySaveLayer) + _opacityStack.Push(_currentOpacity); + + var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true; + + if (useOpacitySaveLayer) { + opacity = _currentOpacity * opacity; //Take current multiplied opacity + + _currentOpacity = 1; //Opacity is applied via layering + if (bounds.HasValue) { var rect = bounds.Value.ToSKRect(); @@ -606,7 +615,6 @@ namespace Avalonia.Skia } else { - _opacityStack.Push(_currentOpacity); _currentOpacity *= opacity; } } @@ -616,14 +624,14 @@ namespace Avalonia.Skia { CheckLease(); - if(_useOpacitySaveLayer) + var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true; + + if (useOpacitySaveLayer) { Canvas.Restore(); } - else - { - _currentOpacity = _opacityStack.Pop(); - } + + _currentOpacity = _opacityStack.Pop(); } /// diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 3eeedc4b5d..569f7da738 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); - peer.ChildrenChanged += Peer_ChildrenChanged; - peer.PropertyChanged += Peer_PropertyChanged; - } - - private void Peer_ChildrenChanged(object? sender, EventArgs e) - { - ChildrenChanged(); - } + peer.ChildrenChanged += OnPeerChildrenChanged; + peer.PropertyChanged += OnPeerPropertyChanged; - private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) - { - if (s_propertyMap.TryGetValue(e.Property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( - this, - (int)id, - e.OldValue as IConvertible, - e.NewValue as IConvertible); - } + if (Peer.GetProvider() is { } embeddedRoot) + embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged; } public AutomationPeer Peer { get; protected set; } @@ -89,21 +75,12 @@ namespace Avalonia.Win32.Automation public virtual IRawElementProviderFragmentRoot? FragmentRoot { - get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + get => InvokeSync(() => GetRoot()); } public virtual IRawElementProviderSimple? HostRawElementProvider => null; public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; - public void ChildrenChanged() - { - UiaCoreProviderApi.UiaRaiseStructureChangedEvent( - this, - StructureChangeType.ChildrenInvalidated, - null, - 0); - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -250,21 +227,49 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } + protected void RaiseChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + null, + 0); + } - private AutomationNode? GetRoot() + protected void RaiseFocusChanged(AutomationNode? focused) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + + private RootAutomationNode? GetRoot() { Dispatcher.UIThread.VerifyAccess(); + return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode; + } - var peer = Peer; - var parent = peer.GetParent(); + private void OnPeerChildrenChanged(object? sender, EventArgs e) + { + RaiseChildrenChanged(); + } - while (peer.GetProvider() is null && parent is object) + private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) { - peer = parent; - parent = peer.GetParent(); + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); } + } - return peer is object ? GetOrCreate(peer) : null; + private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e) + { + if (Peer.GetProvider() is { } embeddedRoot) + RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus())); } private static AutomationNode Create(AutomationPeer peer) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 739f0ac251..7334186c80 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation; namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] - internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot, - IRawElementProviderAdviseEvents + internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot { - private int _raiseFocusChanged; - public RootAutomationNode(AutomationPeer peer) : base(peer) { Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); - Peer.FocusChanged += FocusChanged; + Peer.FocusChanged += OnRootFocusChanged; } public override IRawElementProviderFragmentRoot? FragmentRoot => this; @@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } - - public void FocusChanged(object? sender, EventArgs e) - { - RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); - } - public Rect ToScreen(Rect rect) { if (WindowImpl is null) @@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation return result; } } + + private void OnRootFocusChanged(object? sender, EventArgs e) + { + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); + } } } diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index a215a0a322..df19cdc329 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -103,6 +103,7 @@ namespace Avalonia.Win32 private IDataObject _wrapped; + public IDataObject Wrapped => _wrapped; public DataObject(IDataObject wrapped) { diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index a1e9165117..f55d7a5f07 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1509,7 +1509,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); - [DllImport("ole32.dll", PreserveSig = false)] + [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleGetClipboard(out IntPtr dataObject); [DllImport("ole32.dll", PreserveSig = true)] diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index a81652ffc2..94d744301a 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -217,7 +217,7 @@ namespace Avalonia.Win32 if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) { - return dataObject; + return dataObject.Wrapped; } return new OleDataObject(pDataObj); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 154b20ef84..e9156e553d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -683,9 +683,10 @@ namespace Avalonia.Win32 if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) { parentHwnd = OffscreenParentWindow.Handle; - _hiddenWindowIsParent = true; } + _hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle; + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs new file mode 100644 index 0000000000..c158ff4e75 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs @@ -0,0 +1,68 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering; +/// +/// Test class that verifies how clipping influences rendering in the compositor +/// +public class CompositorInvalidationClippingTests : CompositorTestsBase +{ + [Fact] + // Test case: When the ClipToBounds is false, all visuals should be rendered + public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds() + { + AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4); + } + + [Fact] + // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered + public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds() + { + AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3); + } + + [Fact] + // Test case: When the Clip is used, only visuals within the clip geometry should be rendered + public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip() + { + AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3); + } + + private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount) + { + using (var s = new CompositorCanvas()) + { + //#1 visual is top level + //#2 visual is s.Canvas + + //#3 visual is border1 + s.Canvas.Children.Add(new Border() + { + [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0, + Width = 20, Height = 10, + Background = Brushes.Red, + ClipToBounds = clipToBounds, + Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null + }); + + //#4 visual is border2 + s.Canvas.Children.Add(new Border() + { + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, + Width = 20, Height = 10, + Background = Brushes.Red, + ClipToBounds = clipToBounds, + Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null + }); + s.RunJobs(); + s.Events.Reset(); + + //invalidate border1 + s.Canvas.Children[0].IsVisible = false; + s.RunJobs(); + + s.AssertRenderedVisuals(expectedRenderedVisualsCount); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d4558c9e04..3c7fb6f9c0 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -15,6 +15,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -916,6 +917,82 @@ namespace Avalonia.Controls.UnitTests } } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal(maxLines * target.LineHeight, scrollViewer.MaxHeight); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3); + textPresenter.Margin = textPresenterMargin; + + target.InvalidateMeasure(); + target.Measure(Size.Infinity); + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight); + } + } + [Fact] public void CanUndo_CanRedo_Is_False_When_Initialized() { @@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests return new FuncControlTemplate((control, scope) => new ScrollViewer { - Name = "Part_ScrollViewer", + Name = "PART_ScrollViewer", Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate), Content = new TextPresenter { diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 02dd4e6c03..47e53676d8 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -183,6 +183,34 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("bar", presenter2.Content); } + [Fact] + public void Logical_Children_Should_Not_Be_Duplicated() + { + using var app = Start(); + var (target, transition) = CreateTarget(""); + target.PageTransition = null; + + var childControl = new Control(); + target.Content = childControl; + + Assert.Equal(1, target.LogicalChildren.Count); + Assert.Equal(target.LogicalChildren[0], childControl); + } + + [Fact] + public void First_Presenter_Should_Register_TCC_As_His_Host() + { + using var app = Start(); + var (target, transition) = CreateTarget(""); + target.PageTransition = null; + + var childControl = new Control(); + target.Presenter!.Content = childControl; + + Assert.Equal(1, target.LogicalChildren.Count); + Assert.Equal(target.LogicalChildren[0], childControl); + } + private static IDisposable Start() { return UnitTestApplication.Start( diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ab8e4377b6..c6f9c5eae0 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -583,6 +583,46 @@ namespace Avalonia.LeakTests Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } + + [Fact] + public void Attached_Control_From_ContextMenu_Is_Freed() + { + using (Start()) + { + var contextMenu = new ContextMenu(); + Func run = () => + { + var window = new Window + { + Content = new TextBlock + { + ContextMenu = contextMenu + } + }; + + window.Show(); + + // Do a layout and make sure that TextBlock gets added to visual tree. + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + + // Clear the content and ensure the TextBlock is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } [Fact] public void Standalone_ContextMenu_Is_Freed() diff --git a/tests/Avalonia.LeakTests/DataContextTests.cs b/tests/Avalonia.LeakTests/DataContextTests.cs new file mode 100644 index 0000000000..d16f8c1f57 --- /dev/null +++ b/tests/Avalonia.LeakTests/DataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class DataContextTests +{ + public DataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 53fd610a17..00645259a5 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable Events.Rects.Clear(); } + public void AssertRenderedVisuals(int renderVisuals) + { + RunJobs(); + Assert.Equal(Events.RenderedVisuals, renderVisuals); + Events.Rects.Clear(); + } + public void AssertHitTest(double x, double y, Func filter, params object[] expected) => AssertHitTest(new Point(x, y), filter, expected); @@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable { public List Rects = new(); + public int RenderedVisuals { get; private set; } + + public void IncrementRenderedVisuals() + { + RenderedVisuals++; + } + public void RectInvalidated(Rect rc) { Rects.Add(rc); @@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable public void Reset() { Rects.Clear(); + RenderedVisuals = 0; } } @@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler { Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender); } -} \ No newline at end of file +}