diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 741570061b..4a7a329fc6 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -42,6 +42,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", @@ -61,4 +62,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index 525e01c891..56847bae31 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" EndProject @@ -548,7 +555,6 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU - {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU @@ -560,6 +566,10 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -626,6 +636,7 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 3b14f0ce12..ac78d9c739 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -6,4 +6,5 @@ 11 + diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 622181dba0..a37fa6fd32 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -361,11 +361,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index b7abda2c61..7b02649b6c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -74,10 +74,12 @@ namespace Avalonia.Media public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -284,7 +286,7 @@ namespace Avalonia.Media throw new NotImplementedException(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 732f1957d0..07fd2d56fa 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -12,6 +12,6 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap Source { get; } + IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 2f2a0fb627..718ebf1686 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -11,8 +11,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); static ImageBrush() { @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap source) + public ImageBrush(IBitmap? source) { Source = source; } @@ -38,7 +38,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap Source + public IBitmap? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7d9534c414..2564d89bac 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index f9892bf60c..668a907fdf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// The bitmap interpolation mode. public ImmutableImageBrush( - IBitmap source, + IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap Source { get; } + public IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 0b625080e3..e9086eee37 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// Controls the quality of interpolation. public ImmutableVisualBrush( - Visual visual, + Visual? visual, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8509067cd0..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 5826cfb2ff..d1964bf07e 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Metadata; @@ -47,7 +48,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(IPen pen, Point point); + bool StrokeContains(IPen? pen, Point point); /// /// Makes a clone of the geometry with the specified transform. @@ -87,6 +88,7 @@ namespace Avalonia.Platform /// If ture, the resulting snipped path will start with a BeginFigure call. /// The resulting snipped path. /// If the snipping operation is successful. - bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index b75d080cfd..6b380608fe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity, bounds)) { - Add(new OpacityNode(opacity)); + Add(new OpacityNode(opacity, bounds)); } else { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 50df8bd32b..08e506536f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopClip(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { - _impl.PushOpacity(opacity); + _impl.PushOpacity(opacity, bounds); } public void PopOpacity() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 98be861afa..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,9 +41,9 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; @@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 8e5dc38317..09d2d55ce3 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -117,7 +117,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 454f7eac9d..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f8ce5d23f6..6603e20a2a 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls point = new Point( MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)), - MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0))); + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0))); var hit = TextLayout.HitTestPoint(point); var textPosition = hit.TextPosition; diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index ed11dec1d0..eea175eb3c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -405,7 +405,31 @@ namespace Avalonia.Controls { return visual?.VisualRoot as TopLevel; } - + + /// + /// Requests a to be inhibited. + /// The behavior remains inhibited until the return value is disposed. + /// The available set of s depends on the platform. + /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. + /// + public async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) + { + var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); + if (platformBehaviorInhibition == null) + { + return Disposable.Create(() => { }); + } + + switch (type) + { + case PlatformInhibitionType.AppSleep: + await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); + return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); + default: + return Disposable.Create(() => { }); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -571,30 +595,6 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); - /// - /// Requests a to be inhibited. - /// The behavior remains inhibited until the return value is disposed. - /// The available set of s depends on the platform. - /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. - /// - protected async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) - { - var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); - if (platformBehaviorInhibition == null) - { - return Disposable.Create(() => { }); - } - - switch (type) - { - case PlatformInhibitionType.AppSleep: - await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); - return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); - default: - return Disposable.Create(() => { }); - } - } - /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 9f8e3e38c0..e9abfef673 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -190,7 +190,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = true; + treeViewItem.SetCurrentValue(IsExpandedProperty, true); return true; } @@ -201,7 +201,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); return true; } @@ -214,7 +214,7 @@ namespace Avalonia.Controls { if (treeViewItem.IsFocused) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); } else { @@ -265,7 +265,7 @@ namespace Avalonia.Controls { if (ItemCount > 0) { - IsExpanded = !IsExpanded; + SetCurrentValue(IsExpandedProperty, !IsExpanded); e.Handled = true; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 634efbd699..8e7690aa6c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls if (firstIndex == -1) { estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); firstIndexU = firstIndex * estimatedElementSize; } @@ -411,13 +411,13 @@ namespace Avalonia.Controls { if (estimatedElementSize == -1) estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); + lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); } return new MeasureViewport { - firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), - lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), + firstIndex = firstIndex, + lastIndex = lastIndex, viewportUStart = viewportStart, viewportUEnd = viewportEnd, startU = firstIndexU, @@ -1131,6 +1131,7 @@ namespace Avalonia.Controls // The removed range was before the realized elements. Update the first index and // the indexes of the realized elements. _firstIndex -= count; + _startUUnstable = true; var newIndex = _firstIndex; for (var i = 0; i < _elements.Count; ++i) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 3b1c6cc7b1..51af5862cf 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,16 +5,40 @@ enable + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index e7c07dcbf9..b853626d45 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -7,19 +7,20 @@ namespace Avalonia.FreeDesktop class DBusCallQueue { private readonly Func _errorHandler; + private readonly Queue _q = new(); - record Item(Func Callback) + private bool _processing; + + private record Item(Func Callback) { public Action? OnFinish; } - private Queue _q = new Queue(); - private bool _processing; public DBusCallQueue(Func errorHandler) { _errorHandler = errorHandler; } - + public void Enqueue(Func cb) { _q.Enqueue(new Item(cb)); @@ -42,7 +43,7 @@ namespace Avalonia.FreeDesktop Process(); return tcs.Task; } - + public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -62,7 +63,7 @@ namespace Avalonia.FreeDesktop return tcs.Task; } - async void Process() + private async void Process() { if(_processing) return; diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs deleted file mode 100644 index 24db614a02..0000000000 --- a/src/Avalonia.FreeDesktop/DBusFileChooser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.FileChooser")] - internal interface IFileChooser : IDBusObject - { - Task OpenFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - internal class FileChooserProperties - { - public uint Version { get; set; } - } - - internal static class FileChooserExtensions - { - public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version"); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index fac77521dc..da74f15a3e 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,51 +1,12 @@ using System; using System.Threading; using Avalonia.Logging; -using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { internal static class DBusHelper { - /// - /// This class uses synchronous execution at DBus connection establishment stage - /// then switches to using AvaloniaSynchronizationContext - /// - private class DBusSyncContext : SynchronizationContext - { - private readonly object _lock = new(); - private SynchronizationContext? _ctx; - - public override void Post(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Post(d, state); - else - d(state); - } - } - - public override void Send(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Send(d, state); - else - d(state); - } - } - - public void Initialized() - { - lock (_lock) - _ctx = new AvaloniaSynchronizationContext(); - } - } - public static Connection? Connection { get; private set; } public static Connection? TryInitialize(string? dbusAddress = null) @@ -56,19 +17,14 @@ namespace Avalonia.FreeDesktop var oldContext = SynchronizationContext.Current; try { - - var dbusContext = new DBusSyncContext(); - SynchronizationContext.SetSynchronizationContext(dbusContext); - var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) { - AutoConnect = false, - SynchronizationContext = dbusContext + AutoConnect = false }); + // Connect synchronously - conn.ConnectAsync().Wait(); + conn.ConnectAsync().GetAwaiter().GetResult(); - // Initialize a brand new sync-context - dbusContext.Initialized(); Connection = conn; } catch (Exception e) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 7d97c7cd36..d3c14f285d 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme { @@ -46,20 +47,25 @@ namespace Avalonia.FreeDesktop.DBusIme public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { - _queue = new DBusCallQueue(QueueOnError); + _queue = new DBusCallQueue(QueueOnErrorAsync); Connection = connection; _knownNames = knownNames; - Watch(); + _ = WatchAsync(); } public ITextInputMethodClient Client => _client; - public bool IsActive => _client != null; + public bool IsActive => _client is not null; - async void Watch() + private async Task WatchAsync() { foreach (var name in _knownNames) - _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + { + var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } } protected abstract Task Connect(string name); @@ -67,9 +73,12 @@ namespace Avalonia.FreeDesktop.DBusIme protected string GetAppName() => Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - private async void OnNameChange(ServiceOwnerChangedEventArgs args) + private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { - if (args.NewOwner != null && _currentName == null) + if (e is not null) + return; + + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); if (!_connecting) @@ -89,10 +98,10 @@ namespace Avalonia.FreeDesktop.DBusIme return; } } - catch (Exception e) + catch (Exception ex) { Logger.TryGet(LogEventLevel.Error, "IME") - ?.Log(this, "Unable to create IME input context:\n" + e); + ?.Log(this, "Unable to create IME input context:\n" + ex); } } } @@ -105,7 +114,7 @@ namespace Avalonia.FreeDesktop.DBusIme } // IME has crashed - if (args.NewOwner == null && args.ServiceName == _currentName) + if (args.NewOwner is null && args.ServiceName == _currentName) { _currentName = null; foreach (var s in _disposables) @@ -116,11 +125,11 @@ namespace Avalonia.FreeDesktop.DBusIme Reset(); // Watch again - Watch(); + _ = WatchAsync(); } } - protected virtual Task Disconnect() + protected virtual Task DisconnectAsync() { return Task.CompletedTask; } @@ -136,13 +145,13 @@ namespace Avalonia.FreeDesktop.DBusIme _imeActive = null; } - async Task QueueOnError(Exception e) + private async Task QueueOnErrorAsync(Exception e) { Logger.TryGet(LogEventLevel.Error, "IME") ?.Log(this, "Error:\n" + e); try { - await Disconnect(); + await DisconnectAsync(); } catch (Exception ex) { @@ -157,23 +166,16 @@ namespace Avalonia.FreeDesktop.DBusIme protected void AddDisposable(IDisposable? d) { - if(d is { }) + if (d is { }) _disposables.Add(d); } - + public void Dispose() { foreach(var d in _disposables) d.Dispose(); _disposables.Clear(); - try - { - Disconnect().ContinueWith(_ => { }); - } - catch - { - // fire and forget - } + _ = DisconnectAsync(); _currentName = null; } @@ -182,13 +184,13 @@ namespace Avalonia.FreeDesktop.DBusIme protected abstract Task ResetContextCore(); protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); - void UpdateActive() + private void UpdateActive() { _queue.Enqueue(async () => { if(!IsConnected) return; - + var active = _windowActive && IsActive; if (active != _imeActive) { @@ -204,7 +206,7 @@ namespace Avalonia.FreeDesktop.DBusIme _windowActive = active; UpdateActive(); } - + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _client = client; @@ -227,7 +229,7 @@ namespace Avalonia.FreeDesktop.DBusIme // Error, disconnect catch (Exception e) { - await QueueOnError(e); + await QueueOnErrorAsync(e); return false; } } @@ -240,7 +242,7 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireCommit(string s) => _onCommit?.Invoke(s); - + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { @@ -249,8 +251,8 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); - - void UpdateCursorRect() + + private void UpdateCursorRect() { _queue.Enqueue(async () => { @@ -265,7 +267,7 @@ namespace Avalonia.FreeDesktop.DBusIme } }); } - + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) { _windowPosition = position; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs deleted file mode 100644 index 06afacaa29..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - [DBusInterface("org.fcitx.Fcitx.InputMethod")] - interface IFcitxInputMethod : IDBusObject - { - Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( - string Appname, int Pid); - } - - - [DBusInterface("org.fcitx.Fcitx.InputContext")] - interface IFcitxInputContext : IDBusObject - { - Task EnableICAsync(); - Task CloseICAsync(); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task MouseEventAsync(int X); - Task SetCursorLocationAsync(int X, int Y); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapacityAsync(uint Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action? onError = null); - Task WatchCloseIMAsync(Action handler, Action? onError = null); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputContext1")] - interface IFcitxInputContext1 : IDBusObject - { - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapabilityAsync(ulong Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputMethod1")] - interface IFcitxInputMethod1 : IDBusObject - { - Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs index 6510a5877a..fafc66b6b9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -2,45 +2,45 @@ using System; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { - enum FcitxKeyEventType + internal enum FcitxKeyEventType { FCITX_PRESS_KEY, FCITX_RELEASE_KEY - }; - + } + [Flags] - enum FcitxCapabilityFlags + internal enum FcitxCapabilityFlags { CAPACITY_NONE = 0, - CAPACITY_CLIENT_SIDE_UI = (1 << 0), - CAPACITY_PREEDIT = (1 << 1), - CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), - CAPACITY_PASSWORD = (1 << 3), - CAPACITY_FORMATTED_PREEDIT = (1 << 4), - CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), - CAPACITY_SURROUNDING_TEXT = (1 << 6), - CAPACITY_EMAIL = (1 << 7), - CAPACITY_DIGIT = (1 << 8), - CAPACITY_UPPERCASE = (1 << 9), - CAPACITY_LOWERCASE = (1 << 10), - CAPACITY_NOAUTOUPPERCASE = (1 << 11), - CAPACITY_URL = (1 << 12), - CAPACITY_DIALABLE = (1 << 13), - CAPACITY_NUMBER = (1 << 14), - CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), - CAPACITY_SPELLCHECK = (1 << 16), - CAPACITY_NO_SPELLCHECK = (1 << 17), - CAPACITY_WORD_COMPLETION = (1 << 18), - CAPACITY_UPPERCASE_WORDS = (1 << 19), - CAPACITY_UPPERCASE_SENTENCES = (1 << 20), - CAPACITY_ALPHA = (1 << 21), - CAPACITY_NAME = (1 << 22), - CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), - CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), - }; + CAPACITY_CLIENT_SIDE_UI = 1 << 0, + CAPACITY_PREEDIT = 1 << 1, + CAPACITY_CLIENT_SIDE_CONTROL_STATE = 1 << 2, + CAPACITY_PASSWORD = 1 << 3, + CAPACITY_FORMATTED_PREEDIT = 1 << 4, + CAPACITY_CLIENT_UNFOCUS_COMMIT = 1 << 5, + CAPACITY_SURROUNDING_TEXT = 1 << 6, + CAPACITY_EMAIL = 1 << 7, + CAPACITY_DIGIT = 1 << 8, + CAPACITY_UPPERCASE = 1 << 9, + CAPACITY_LOWERCASE = 1 << 10, + CAPACITY_NOAUTOUPPERCASE = 1 << 11, + CAPACITY_URL = 1 << 12, + CAPACITY_DIALABLE = 1 << 13, + CAPACITY_NUMBER = 1 << 14, + CAPACITY_NO_ON_SCREEN_KEYBOARD = 1 << 15, + CAPACITY_SPELLCHECK = 1 << 16, + CAPACITY_NO_SPELLCHECK = 1 << 17, + CAPACITY_WORD_COMPLETION = 1 << 18, + CAPACITY_UPPERCASE_WORDS = 1 << 19, + CAPACITY_UPPERCASE_SENTENCES = 1 << 20, + CAPACITY_ALPHA = 1 << 21, + CAPACITY_NAME = 1 << 22, + CAPACITY_GET_IM_INFO_ON_FOCUS = 1 << 23, + CAPACITY_RELATIVE_CURSOR_RECT = 1 << 24 + } [Flags] - enum FcitxKeyState + internal enum FcitxKeyState { FcitxKeyState_None = 0, FcitxKeyState_Shift = 1 << 0, @@ -63,5 +63,5 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx FcitxKeyState_Hyper = 1 << 27, FcitxKeyState_Meta = 1 << 28, FcitxKeyState_UsedMask = 0x5c001fff - }; + } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 6c503edb41..00d05e59a3 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -1,19 +1,20 @@ using System; using System.Threading.Tasks; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1? _modern; - private readonly IFcitxInputContext? _old; + private readonly OrgFcitxFcitxInputContext1? _modern; + private readonly OrgFcitxFcitxInputContext? _old; - public FcitxICWrapper(IFcitxInputContext old) + public FcitxICWrapper(OrgFcitxFcitxInputContext old) { _old = old; } - public FcitxICWrapper(IFcitxInputContext1 modern) + public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern) { _modern = modern; } @@ -21,32 +22,30 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { - if(_old!=null) + if (_old is not null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) - ?? _modern?.WatchCommitStringAsync(handler) - ?? Task.FromResult(default(IDisposable?)); + public ValueTask WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? new ValueTask(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) - { - return _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))) - ?? Task.FromResult(default(IDisposable?)); - } + public ValueTask WatchForwardKeyAsync(Action handler) => + _old?.WatchForwardKeyAsync(handler) + ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0))) + ?? new ValueTask(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 0f499c6066..1cf3507cc2 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,11 +1,11 @@ using System; using System.Diagnostics; -using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -14,32 +14,24 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; - public FcitxX11TextInputMethod(Connection connection) : base(connection, - "org.fcitx.Fcitx", - "org.freedesktop.portal.Fcitx" - ) - { - - } + public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } protected override async Task Connect(string name) { if (name == "org.fcitx.Fcitx") { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = Connection.CreateProxy(name, - "/inputcontext_" + resp.icid); - + var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = Connection.CreateProxy(name, resp.path); + var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } @@ -48,7 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -63,14 +55,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Math.Max(1, cursorRect.Height)) ?? Task.CompletedTask; - protected override Task SetActiveCore(bool active)=> (active + protected override Task SetActiveCore(bool active)=> (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) ?? Task.CompletedTask; - - protected override Task ResetContextCore() => _context?.ResetAsync() - ?? Task.CompletedTask; + protected override Task ResetContextCore() => _context?.ResetAsync() ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -87,17 +77,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - if (_context is { }) - { + if (_context is not null) return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, (uint)args.Timestamp).ConfigureAwait(false); - } - else - { - return false; - } + + return false; } - + public override void SetOptions(TextInputOptions options) => Enqueue(async () => { @@ -127,7 +113,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } }); - private void OnForward((uint keyval, uint state, int type) ev) + private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) { var state = (FcitxKeyState)ev.state; KeyModifiers mods = default; @@ -149,6 +135,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) + { + if (e is not null) + return; + + FireCommit(s); + } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs deleted file mode 100644 index 4ef034adb9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - [DBusInterface("org.freedesktop.IBus.InputContext")] - interface IIBusInputContext : IDBusObject - { - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); - Task SetCursorLocationAsync(int X, int Y, int W, int H); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCapabilitiesAsync(uint Caps); - Task PropertyActivateAsync(string Name, int State); - Task SetEngineAsync(string Name); - Task GetEngineAsync(); - Task DestroyAsync(); - Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action? onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); - Task WatchShowLookupTableAsync(Action handler, Action? onError = null); - Task WatchHideLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); - } - - - [DBusInterface("org.freedesktop.IBus.Portal")] - interface IIBusPortal : IDBusObject - { - Task CreateInputContextAsync(string Name); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index 3070f51a8e..1b430f9c90 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Button3Mask = 1 << 10, Button4Mask = 1 << 11, Button5Mask = 1 << 12, - + HandledMask = 1 << 24, ForwardMask = 1 << 25, IgnoredMask = ForwardMask, @@ -40,6 +40,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus CapLookupTable = 1 << 2, CapFocus = 1 << 3, CapProperty = 1 << 4, - CapSurroundingText = 1 << 5, + CapSurroundingText = 1 << 5 } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 2324ca44a7..59e9ecd1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -1,36 +1,38 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; + namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext? _context; + private OrgFreedesktopIBusService? _service; + private OrgFreedesktopIBusInputContext? _context; - public IBusX11TextInputMethod(Connection connection) : base(connection, - "org.freedesktop.portal.IBus") - { - } + public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var path = - await Connection.CreateProxy(name, "/org/freedesktop/IBus") - .CreateInputContextAsync(GetAppName()); - - _context = Connection.CreateProxy(name, path); + var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus"); + var path = await portal.CreateInputContextAsync(GetAppName()); + _service = new OrgFreedesktopIBusService(Connection, name, path); + _context = new OrgFreedesktopIBusInputContext(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); return true; } - private void OnForwardKey((uint keyval, uint keycode, uint state) k) + private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { + if (e is not null) + return; + var state = (IBusModifierMask)k.state; KeyModifiers mods = default; if (state.HasAllFlags(IBusModifierMask.ControlMask)) @@ -49,28 +51,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - - private void OnCommitText(object wtf) + private void OnCommitText(Exception? e, DBusVariantItem variantItem) { - // Hello darkness, my old friend - if (wtf.GetType().GetField("Item3") is { } prop) - { - var text = prop.GetValue(wtf) as string; - if (!string.IsNullOrEmpty(text)) - FireCommit(text!); - } + if (e is not null) + return; + + if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem) + FireCommit(stringItem.Value); } - protected override Task Disconnect() => _context?.DestroyAsync() - ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; protected override void OnDisconnected() { + _service = null; _context = null; base.OnDisconnected(); } - protected override Task SetCursorRectCore(PixelRect rect) + protected override Task SetCursorRectCore(PixelRect rect) => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) ?? Task.CompletedTask; @@ -96,20 +95,12 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - if(_context is { }) - { - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); - } - else - { - return Task.FromResult(false); - } - + return _context is not null ? _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state) : Task.FromResult(false); } public override void SetOptions(TextInputOptions options) { - // No-op, because ibus + // No-op, because ibus } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index d8874b6fae..8042d3bff2 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -2,44 +2,43 @@ using System; using System.Collections.Generic; using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.FreeDesktop.DBusIme.IBus; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { internal class X11DBusImeHelper { - private static readonly Dictionary> KnownMethods = - new Dictionary> + private static readonly Dictionary> KnownMethods = new() { - ["fcitx"] = conn => + ["fcitx"] = static conn => new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), - ["ibus"] = conn => + ["ibus"] = static conn => new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - - static Func? DetectInputMethod() + + private static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { var value = Environment.GetEnvironmentVariable(name); - + if (value == "none") return null; - - if (value != null && KnownMethods.TryGetValue(value, out var factory)) + + if (value is not null && KnownMethods.TryGetValue(value, out var factory)) return factory; } return null; } - + public static bool DetectAndRegister() { var factory = DetectInputMethod(); - if (factory != null) + if (factory is not null) { var conn = DBusHelper.TryInitialize(); - if (conn != null) + if (conn is not null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); return true; diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index 7e22988270..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusMenu -{ - - [DBusInterface("org.freedesktop.DBus.Properties")] - interface IFreeDesktopDBusProperties : IDBusObject - { - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [DBusInterface("com.canonical.dbusmenu")] - interface IDBusMenu : IFreeDesktopDBusProperties - { - Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); - Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); - Task GetPropertyAsync(int Id, string Name); - Task EventAsync(int Id, string EventId, object Data, uint Timestamp); - Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); - Task AboutToShowAsync(int Id); - Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); - } - - [Dictionary] - class DBusMenuProperties - { - public uint Version { get; set; } = default; - public string? TextDirection { get; set; } = default; - public string? Status { get; set; } = default; - public string[]? IconThemePath { get; set; } = default; - } - - - [DBusInterface("com.canonical.AppMenu.Registrar")] - interface IRegistrar : IDBusObject - { - Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); - Task UnregisterWindowAsync(uint WindowId); - Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); - Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index cfbafc53e5..d08828f796 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,97 +2,116 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using Avalonia.Reactive; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus; -#pragma warning disable 1998 +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusMenuExporter { - public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) - { - if (DBusHelper.Connection == null) - return null; + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => + DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid); - return new DBusMenuExporterImpl(DBusHelper.Connection, xid); - } - - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection) - { - return new DBusMenuExporterImpl(currentConnection, path); - } + public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => + new DBusMenuExporterImpl(currentConnection, path); - public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString("N"); + public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { - private readonly Connection _dbus; + private readonly Dictionary _idsToItems = new(); + private readonly Dictionary _itemsToIds = new(); + private readonly HashSet _menus = new(); private readonly uint _xid; - private IRegistrar? _registrar; + private readonly bool _appMenu = true; + private ComCanonicalAppMenuRegistrar? _registrar; + private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; - private NativeMenu? _menu; - private readonly Dictionary _idsToItems = new Dictionary(); - private readonly Dictionary _itemsToIds = new Dictionary(); - private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool _appMenu = true; - - public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + + public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _dbus = dbus; + Connection = connection; _xid = (uint)xid.ToInt32(); - ObjectPath = GenerateDBusMenuObjPath; + Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + public DBusMenuExporterImpl(Connection connection, string path) { - _dbus = dbus; + Connection = connection; _appMenu = false; - ObjectPath = path; + Path = path; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } - - async void Init() + + protected override Connection Connection { get; } + + public override string Path { get; } + + protected override (uint revision, (int, Dictionary, DBusVariantItem[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + { + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + return (_revision, layout); + } + + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); + + protected override DBusVariantItem OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0)); + + protected override void OnEvent(int id, string eventId, DBusVariantItem data, uint timestamp) => + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); + + protected override int[] OnEventGroup((int, string, DBusVariantItem, uint)[] events) + { + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); + return Array.Empty(); + } + + protected override bool OnAboutToShow(int id) => false; + + protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => + (Array.Empty(), Array.Empty()); + + private async Task InitializeAsync() { + Connection.AddMethodHandler(this); + if (!_appMenu) + return; + + _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); try { - if (_appMenu) - { - await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection?.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed && _registrar is { }) - await _registrar.RegisterWindowAsync(_xid, ObjectPath); - } - else - { - await _dbus.RegisterObjectAsync(this); - } + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, Path); } - catch (Exception e) + catch { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) - ?.Log(this, e.Message); - // It's not really important if this code succeeds, // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that // menu will be actually exported + _registrar = null; } } @@ -101,29 +120,28 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - _dbus.UnregisterObject(this); // Fire and forget - _registrar?.UnregisterWindowAsync(_xid); + _ = _registrar?.UnregisterWindowAsync(_xid); } public bool IsNativeMenuExported { get; private set; } + public event EventHandler? OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu? menu) { - if (menu == null) - menu = new NativeMenu(); + menu ??= new NativeMenu(); - if (_menu != null) + if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; _menu = menu; ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; - + DoLayoutReset(); } - + /* This is basic initial implementation, so we don't actually track anything and just reset the whole layout on *ANY* change @@ -131,10 +149,10 @@ namespace Avalonia.FreeDesktop This is not how it should work and will prevent us from implementing various features, but that's the fastest way to get things working, so... */ - void DoLayoutReset() + private void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; foreach(var menu in _menus) ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; @@ -142,10 +160,10 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - LayoutUpdated?.Invoke((_revision, 0)); + EmitLayoutUpdated(_revision, 0); } - void QueueReset() + private void QueueReset() { if(_resetQueued) return; @@ -163,10 +181,10 @@ namespace Avalonia.FreeDesktop private void EnsureSubscribed(NativeMenu? menu) { - if(menu!=null && _menus.Add(menu)) + if (menu is not null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; } - + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -190,258 +208,137 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public ObjectPath ObjectPath { get; } - - - async Task IFreeDesktopDBusProperties.GetAsync(string prop) - { - if (prop == "Version") - return 2; - if (prop == "Status") - return "normal"; - return 0; - } - - async Task IFreeDesktopDBusProperties.GetAllAsync() - { - return new DBusMenuProperties - { - Version = 2, - Status = "normal", - }; - } - - private static string[] AllProperties = new[] - { + private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - - object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + + private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return "separator"; + return new DBusVariantItem("s", new DBusStringItem("separator")); } else if (it is NativeMenuItem item) { if (name == "type") - { return null; - } if (name == "label") - return item?.Header ?? ""; + return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "")); if (name == "enabled") { - if (item == null) - return null; - if (item.Menu != null && item.Menu.Items.Count == 0) - return false; - if (item.IsEnabled == false) - return false; + if (item.Menu is not null && item.Menu.Items.Count == 0) + return new DBusVariantItem("b", new DBusBoolItem(false)); + if (!item.IsEnabled) + return new DBusVariantItem("b", new DBusBoolItem(false)); return null; } if (name == "shortcut") { - if (item?.Gesture == null) + if (item.Gesture is null) return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new List(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - lst.Add("Control"); + lst.Add(new DBusStringItem("Control")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - lst.Add("Alt"); + lst.Add(new DBusStringItem("Alt")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - lst.Add("Shift"); + lst.Add(new DBusStringItem("Shift")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - lst.Add("Super"); - lst.Add(item.Gesture.Key.ToString()); - return new[] { lst.ToArray() }; + lst.Add(new DBusStringItem("Super")); + lst.Add(new DBusStringItem(item.Gesture.Key.ToString())); + return new DBusVariantItem("aas", new DBusArrayItem(DBusType.Array, new[] { new DBusArrayItem(DBusType.String, lst) })); } if (name == "toggle-type") { if (item.ToggleType == NativeMenuItemToggleType.CheckBox) - return "checkmark"; + return new DBusVariantItem("s", new DBusStringItem("checkmark")); if (item.ToggleType == NativeMenuItemToggleType.Radio) - return "radio"; + return new DBusVariantItem("s", new DBusStringItem("radio")); } - if (name == "toggle-state") - { - if (item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; - } - + if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) + return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0)); + if (name == "icon-data") { - if (item.Icon != null) + if (item.Icon is not null) { var loader = AvaloniaLocator.Current.GetService(); - if (loader != null) + if (loader is not null) { var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); using var ms = new MemoryStream(); icon.Save(ms); - return ms.ToArray(); + return new DBusVariantItem("ay", + new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x)))); } } } - + if (name == "children-display") - return menu != null ? "submenu" : null; + return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null; } return null; } - private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { - if (names?.Length > 0 != true) - names = AllProperties; - _reusablePropertyList.Clear(); + if (names.Length == 0) + names = s_allProperties; + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); - if (v != null) - _reusablePropertyList.Add(new KeyValuePair(n, v)); + if (v is not null) + properties.Add(n, v); } - return _reusablePropertyList.ToArray(); - } - - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( - int ParentId, int RecursionDepth, string[] PropertyNames) - { - var menu = GetMenu(ParentId); - var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => - { - OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); - }); - } - return Task.FromResult(rv); + return properties; } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private (int, Dictionary, DBusVariantItem[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { - var id = item == null ? 0 : GetId(item); + var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = (depth == 0 || menu == null) ? Array.Empty() : new object[menu.Items.Count]; - if(menu != null) + var children = depth == 0 || menu is null ? Array.Empty() : new DBusVariantItem[menu.Items.Count]; + if (menu is not null) + { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - - children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[] + { + new DBusInt32Item(layout.Item1), + new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))), + new DBusArrayItem(DBusType.Variant, layout.Item3) + })); } - - return (id, props, children); - } - - public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) - { - var arr = new (int, KeyValuePair[])[Ids.Length]; - for (var c = 0; c < Ids.Length; c++) - { - var id = Ids[c]; - var item = GetMenu(id); - var props = GetProperties(item, PropertyNames); - arr[c] = (id, props); } - return Task.FromResult(arr); - } - - public async Task GetPropertyAsync(int Id, string Name) - { - return GetProperty(GetMenu(Id), Name) ?? 0; + return (id, props, children); } - - public void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId) { if (eventId == "clicked") { var item = GetMenu(id).item; - - if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) - { - if (menuItem?.IsEnabled == true) - bridge?.RaiseClicked(); - } + if (item is NativeMenuItem { IsEnabled: true } and INativeMenuItemExporterEventsImplBridge bridge) + bridge.RaiseClicked(); } } - - public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) - { - HandleEvent(Id, EventId, Data, Timestamp); - return Task.CompletedTask; - } - - public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) - { - foreach (var e in Events) - HandleEvent(e.id, e.eventId, e.data, e.timestamp); - return Task.FromResult(Array.Empty()); - } - - public async Task AboutToShowAsync(int Id) - { - return false; - } - - public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) - { - return (Array.Empty(), Array.Empty()); - } - - #region Events - - private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> - ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)>? LayoutUpdated; - private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } - private event Action PropertiesChanged { add { } remove { } } - - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) - { - ItemsPropertiesUpdated += handler; - return Disposable.Create(() => ItemsPropertiesUpdated -= handler); - } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) - { - LayoutUpdated += handler; - return Disposable.Create(() => LayoutUpdated -= handler); - } - - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) - { - ItemActivationRequested+= handler; - return Disposable.Create(() => ItemActivationRequested -= handler); - } - - async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) - { - PropertiesChanged += handler; - return Disposable.Create(() => PropertiesChanged -= handler); - } - - #endregion } } } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 039fc7c088..a25bb68458 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -2,44 +2,35 @@ using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Tmds.DBus.SourceGenerator; -namespace Avalonia.FreeDesktop; - -internal class DBusPlatformSettings : DefaultPlatformSettings +namespace Avalonia.FreeDesktop { - private readonly IDBusSettings? _settings; - private PlatformColorValues? _lastColorValues; - - public DBusPlatformSettings() + internal class DBusPlatformSettings : DefaultPlatformSettings { - _settings = DBusHelper.TryInitialize()? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; - if (_settings is not null) + public DBusPlatformSettings() { - _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + if (DBusHelper.Connection is null) + return; - _ = TryGetInitialValue(); + _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + _ = TryGetInitialValueAsync(); } - } - - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } - private async Task TryGetInitialValue() - { - var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - if (colorSchemeTask.Status == TaskStatus.RanToCompletion) + public override PlatformColorValues GetColorValues() { - _lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); + return _lastColorValues ?? base.GetColorValues(); } - else + + private async Task TryGetInitialValueAsync() { try { - var value = await colorSchemeTask; + var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); _lastColorValues = GetColorValuesFromSetting(value); OnColorValuesChanged(_lastColorValues); } @@ -49,29 +40,31 @@ internal class DBusPlatformSettings : DefaultPlatformSettings Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); } } - } - - private void SettingsChangedHandler((string @namespace, string key, object value) tuple) - { - if (tuple.@namespace == "org.freedesktop.appearance" - && tuple.key == "color-scheme") + + private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(tuple.value); - OnColorValuesChanged(_lastColorValues); + if (exception is not null) + return; + + if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _lastColorValues = GetColorValuesFromSetting(value); + OnColorValuesChanged(_lastColorValues); + } } - } - - private static PlatformColorValues GetColorValuesFromSetting(object value) - { - var isDark = value?.ToString() == "1"; - return new PlatformColorValues + + private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } } } diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs deleted file mode 100644 index d84905324f..0000000000 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.Request")] - internal interface IRequest : IDBusObject - { - Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusSettings.cs b/src/Avalonia.FreeDesktop/DBusSettings.cs deleted file mode 100644 index 05911981c7..0000000000 --- a/src/Avalonia.FreeDesktop/DBusSettings.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus; - -namespace Avalonia.FreeDesktop; - -[DBusInterface("org.freedesktop.portal.Settings")] -internal interface IDBusSettings : IDBusObject -{ - Task<(string @namespace, IDictionary)> ReadAllAsync(string[] namespaces); - - Task ReadAsync(string @namespace, string key); - - Task WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action? onError = null); -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 905ce1f272..20583dd6ac 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -2,46 +2,42 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; - -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); - - internal static async Task TryCreate(IPlatformHandle handle) + internal static async Task TryCreateAsync(IPlatformHandle handle) { - if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) + if (DBusHelper.Connection is null) + return null; + + var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + try { - try - { - await fileChooser.GetVersionAsync(); - return new DBusSystemDialog(fileChooser, handle); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); - return null; - } + await dbusFileChooser.GetVersionAsync(); + } + catch + { + return null; } - return null; + return new DBusSystemDialog(DBusHelper.Connection, handle, dbusFileChooser); } - private readonly IFileChooser _fileChooser; + private readonly Connection _connection; + private readonly OrgFreedesktopPortalFileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle) + private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooser fileChooser) { + _connection = connection; _fileChooser = fileChooser; _handle = handle; } @@ -56,115 +52,124 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); - if (filters.Any()) - { + if (filters is not null) chooserOptions.Add("filters", filters); - } - chooserOptions.Add("multiple", options.AllowMultiple); + chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple))); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); - return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); + var uris = await tsc.Task ?? Array.Empty(); + return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); } public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); - if (filters.Any()) - { + if (filters is not null) chooserOptions.Add("filters", filters); - } if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", currentName); - if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath)); - objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); + if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) + chooserOptions.Add("current_folder", new DBusVariantItem("s", new DBusStringItem(folderPath))); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); + var uris = await tsc.Task; var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null; if (path is null) - { return null; - } - else - { - // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. - path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); - return new BclStorageFile(new FileInfo(path)); - } + // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. + path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); + return new BclStorageFile(new FileInfo(path)); } public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; - var chooserOptions = new Dictionary + var chooserOptions = new Dictionary { - { "directory", true }, - { "multiple", options.AllowMultiple } + { "directory", new DBusVariantItem("b", new DBusBoolItem(true)) }, + { "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) } }; + var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); + var uris = await tsc.Task ?? Array.Empty(); return uris - .Select(path => new Uri(path).LocalPath) + .Select(static path => new Uri(path).LocalPath) // WSL2 freedesktop allows to select files as well in directory picker, filter it out. .Where(Directory.Exists) - .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); + .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } - - private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) + + private static DBusVariantItem? ParseFilters(IReadOnlyList? fileTypes) { - // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; + // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] if (fileTypes is null) - { - return Array.Empty<(string name, (uint style, string extension)[])>(); - } + return null; + + var filters = new DBusArrayItem(DBusType.Struct, new List()); - var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint globStyle = 0u; - const uint mimeStyle = 1u; - - var extensions = Enumerable.Empty<(uint, string)>(); - - if (fileType.Patterns is { } patterns) - { - extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - } - else if (fileType.MimeTypes is { } mimeTypes) - { - extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); - } - - if (extensions.Any()) - { - filters.Add((fileType.Name, extensions.ToArray())); - } + var extensions = new List(); + if (fileType.Patterns?.Count > 0) + extensions.AddRange( + fileType.Patterns.Select(static pattern => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) }))); + else if (fileType.MimeTypes?.Count > 0) + extensions.AddRange( + fileType.MimeTypes.Select(static mimeType => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) }))); + else + continue; + + filters.Add(new DBusStructItem( + new DBusItem[] + { + new DBusStringItem(fileType.Name), + new DBusArrayItem(DBusType.Struct, extensions) + })); } - return filters.ToArray(); + return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index b44762161b..afbee77067 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,30 +1,26 @@ -#nullable enable - -using System; +using System; using System.Diagnostics; -using Avalonia.Reactive; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusTrayIconImpl : ITrayIconImpl { private static int s_trayIconInstanceId; + public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private IDisposable? _serviceWatchDisposable; + private readonly OrgFreedesktopDBus? _dBus; + private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private DbusPixmap _icon; + private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; + private (int, int, byte[]) _icon; private string? _sysTrayServiceName; private string? _tooltipText; @@ -51,6 +47,7 @@ namespace Avalonia.FreeDesktop IsActive = true; + _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -60,23 +57,10 @@ namespace Avalonia.FreeDesktop private void InitializeSNWService() { - if (_connection is null || _isDisposed) return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); - + if (_connection is null || _isDisposed) return; - } + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); _serviceConnected = true; } @@ -84,23 +68,24 @@ namespace Avalonia.FreeDesktop { try { - _serviceWatchDisposable = - await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2)); + var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); + OnNameChange(nameOwner); } - catch (Exception e) + catch { + _serviceWatchDisposable = null; Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); + ?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable."); } } - private void OnNameChange(ServiceOwnerChangedEventArgs obj) + private void OnNameChange(string? newOwner) { if (_isDisposed) return; - if (!_serviceConnected & obj.NewOwner != null) + if (!_serviceConnected & newOwner is not null) { _serviceConnected = true; InitializeSNWService(); @@ -108,55 +93,45 @@ namespace Avalonia.FreeDesktop DestroyTrayIcon(); if (_isVisible) - { CreateTrayIcon(); - } } - else if (_serviceConnected & obj.NewOwner is null) + else if (_serviceConnected & newOwner is null) { DestroyTrayIcon(); _serviceConnected = false; } } - private void CreateTrayIcon() + private async void CreateTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null) return; +#if NET5_0_OR_GREATER + var pid = Environment.ProcessId; +#else var pid = Process.GetCurrentProcess().Id; +#endif var tid = s_trayIconInstanceId++; _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath); - try - { - _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - _connection.RegisterServiceAsync(_sysTrayServiceName); - _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, $"Error creating a DBus tray icon: {e}."); - - _serviceConnected = false; - } + _connection.AddMethodHandler(_statusNotifierItemDbusObj); + await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; } private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - _connection.UnregisterServiceAsync(_sysTrayServiceName); + _dBus!.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -164,7 +139,6 @@ namespace Avalonia.FreeDesktop IsActive = false; _isDisposed = true; DestroyTrayIcon(); - _connection?.Dispose(); _serviceWatchDisposable?.Dispose(); } @@ -175,13 +149,14 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon(EmptyPixmap); return; } var x11iconData = IconConverterDelegate(icon); - if (x11iconData.Length == 0) return; + if (x11iconData.Length == 0) + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -199,7 +174,7 @@ namespace Avalonia.FreeDesktop pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } - _icon = new DbusPixmap(w, h, pixByteArray); + _icon = (w, h, pixByteArray); _statusNotifierItemDbusObj?.SetIcon(_icon); } @@ -237,113 +212,50 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem + internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { - private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); - - _backingProperties = new StatusNotifierItemProperties - { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") - }; - + Connection = connection; + BackingProperties.Menu = dbusMenuPath; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); + BackingProperties.IconName = string.Empty; + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.IconThemePath = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; InvalidateAll(); } - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; + protected override Connection Connection { get; } - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } - - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + public override string Path => "/StatusNotifierItem"; - public void InvalidateAll() - { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } + public event Action? ActivationDelegate; - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } + protected override void OnContextMenu(int x, int y) { } - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } + protected override void OnActivate(int x, int y) => ActivationDelegate?.Invoke(); - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); - } + protected override void OnSecondaryActivate(int x, int y) { } - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } + protected override void OnScroll(int delta, string orientation) { } - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) + public void InvalidateAll() { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + EmitNewTitle(); + EmitNewIcon(); + EmitNewAttentionIcon(); + EmitNewOverlayIcon(); + EmitNewToolTip(); + EmitNewStatus(BackingProperties.Status); } - public Task GetAsync(string prop) + public void SetIcon((int, int, byte[]) dbusPixmap) { - return Task.FromResult(prop switch - { - nameof(_backingProperties.Category) => _backingProperties.Category, - nameof(_backingProperties.Id) => _backingProperties.Id, - nameof(_backingProperties.Menu) => _backingProperties.Menu, - nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, - nameof(_backingProperties.Status) => _backingProperties.Status, - nameof(_backingProperties.Title) => _backingProperties.Title, - nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, - _ => null - }); - } - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) - { - _backingProperties.IconPixmap = new[] { dbusPixmap }; + BackingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); } @@ -352,102 +264,12 @@ namespace Avalonia.FreeDesktop if (text is null) return; - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - + BackingProperties.Id = text; + BackingProperties.Category = "ApplicationStatus"; + BackingProperties.Status = text; + BackingProperties.Title = text; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - [Dictionary] - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; - - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; - - public ToolTip ToolTip; - } - - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; - - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { - } - - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) - { - First = first; - Second = second; - Third = third; - Fourth = fourth; - } - } - - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - - public DbusPixmap(int width, int height, byte[] data) - { - Width = width; - Height = height; - Data = data; - } - - public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); - } } diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBus.xml b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml new file mode 100644 index 0000000000..a7ecce70f2 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml new file mode 100644 index 0000000000..de6868cb3e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml @@ -0,0 +1,437 @@ + + + + + + + + Name + Type + Description + Default Value + + + type + String + Can be one of: + - "standard": an item which can be clicked to trigger an action or + show another menu + - "separator": a separator + + Vendor specific types can be added by prefixing them with + "x--". + + "standard" + + + label + string + Text of the item, except that: + -# two consecutive underscore characters "__" are displayed as a + single underscore, + -# any remaining underscore characters are not displayed at all, + -# the first of those remaining underscore characters (unless it is + the last character in the string) indicates that the following + character is the access key. + + "" + + + enabled + boolean + Whether the item can be activated or not. + true + + + visible + boolean + True if the item is visible in the menu. + true + + + icon-name + string + Icon name of the item, following the freedesktop.org icon spec. + "" + + + icon-data + binary + PNG data of the icon. + Empty + + + shortcut + array of arrays of strings + The shortcut of the item. Each array represents the key press + in the list of keypresses. Each list of strings contains a list of + modifiers and then the key that is used. The modifier strings + allowed are: "Control", "Alt", "Shift" and "Super". + + - A simple shortcut like Ctrl+S is represented as: + [["Control", "S"]] + - A complex shortcut like Ctrl+Q, Alt+X is represented as: + [["Control", "Q"], ["Alt", "X"]] + Empty + + + toggle-type + string + + If the item can be toggled, this property should be set to: + - "checkmark": Item is an independent togglable item + - "radio": Item is part of a group where only one item can be + toggled at a time + - "": Item cannot be toggled + + "" + + + toggle-state + int + + Describe the current state of a "togglable" item. Can be one of: + - 0 = off + - 1 = on + - anything else = indeterminate + + Note: + The implementation does not itself handle ensuring that only one + item in a radio group is set to "on", or that a group does not have + "on" and "indeterminate" items simultaneously; maintaining this + policy is up to the toolkit wrappers. + + -1 + + + children-display + string + + If the menu item has children this property should be set to + "submenu". + + "" + + + disposition + string + + How the menuitem feels the information it's displaying to the + user should be presented. + - "normal" a standard menu item + - "informative" providing additional information to the user + - "warning" looking at potentially harmful results + - "alert" something bad could potentially happen + + "normal" + + + + Vendor specific properties can be added by prefixing them with + "x--". + ]]> + + + + + Provides the version of the DBusmenu API that this API is + implementing. + + + + + + Represents the way the text direction of the application. This + allows the server to handle mismatches intelligently. For left- + to-right the string is "ltr" for right-to-left it is "rtl". + + + + + + Tells if the menus are in a normal state or they believe that they + could use some attention. Cases for showing them would be if help + were referring to them or they accessors were being highlighted. + This property can have two values: "normal" in almost all cases and + "notice" when they should have a higher priority to be shown. + + + + + + A list of directories that should be used for finding icons using + the icon naming spec. Idealy there should only be one for the icon + theme, but additional ones are often added by applications for + app specific icons. + + + + + + + + Provides the layout and propertiers that are attached to the entries + that are in the layout. It only gives the items that are children + of the item that is specified in @a parentId. It will return all of the + properties or specific ones depending of the value in @a propertyNames. + + The format is recursive, where the second 'v' is in the same format + as the original 'a(ia{sv}av)'. Its content depends on the value + of @a recursionDepth. + + + The ID of the parent node for the layout. For + grabbing the layout from the root node use zero. + + + + The amount of levels of recursion to use. This affects the + content of the second variant array. + - -1: deliver all the items under the @a parentId. + - 0: no recursion, the array will be empty. + - n: array will contains items up to 'n' level depth. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + The revision number of the layout. For matching + with layoutUpdated signals. + + + The layout, as a recursive structure. + + + + + + Returns the list of items which are children of @a parentId. + + + + A list of ids that we should be finding the properties + on. If the list is empty, all menu items should be sent. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + + An array of property values. + An item in this area is represented as a struct following + this format: + @li id unsigned the item id + @li properties map(string => variant) the requested item properties + + + + + + + Get a signal property on a single item. This is not useful if you're + going to implement this interface, it should only be used if you're + debugging via a commandline tool. + + + the id of the item which received the event + + + the name of the property to get + + + the value of the property + + + + + -" + ]]> + + the id of the item which received the event + + + the type of event + + + event-specific data + + + The time that the event occured if available or the time the message was sent if not + + + + + + Used to pass a set of events as a single message for possibily several + different menuitems. This is done to optimize DBus traffic. + + + + An array of all the events that should be passed. This tuple should + match the parameters of the 'Event' signal. Which is roughly: + id, eventID, data and timestamp. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + This is called by the applet to notify the application that it is about + to show the menu under the specified item. + + + + Which menu item represents the parent of the item about to be shown. + + + + + Whether this AboutToShow event should result in the menu being updated. + + + + + + + A function to tell several menus being shown that they are about to + be shown to the user. This is likely only useful for programitc purposes + so while the return values are returned, in general, the singular function + should be used in most user interacation scenarios. + + + + The IDs of the menu items who's submenus are being shown. + + + + + The IDs of the menus that need updates. Note: if no update information + is needed the DBus message should set the no reply flag. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + + Triggered when there are lots of property updates across many items + so they all get grouped into a single dbus message. The format is + the ID of the item with a hashtable of names and values for those + properties. + + + + + + + Triggered by the application to notify display of a layout update, up to + revision + + + The revision of the layout that we're currently on + + + + If the layout update is only of a subtree, this is the + parent item for the entries that have changed. It is zero if + the whole layout should be considered invalid. + + + + + + The server is requesting that all clients displaying this + menu open it to the user. This would be for things like + hotkeys that when the user presses them the menu should + open and display itself to the user. + + + ID of the menu that should be activated + + + The time that the event occured + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml new file mode 100644 index 0000000000..7866a74639 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml new file mode 100644 index 0000000000..2eb1a7a0b8 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000000..42a71707b6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window.  This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml new file mode 100644 index 0000000000..b30d94cebf --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml new file mode 100644 index 0000000000..6cb130d48a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml new file mode 100644 index 0000000000..b8d60f0d37 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml new file mode 100644 index 0000000000..0cc358a09a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml new file mode 100644 index 0000000000..376ad424d4 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml new file mode 100644 index 0000000000..2ae3546955 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000000..c1abb4eb7b --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000000..669997a3df --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 147955b6a3..df22f46323 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -15,18 +15,18 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { var symlinkSize = Encoding.UTF8.GetByteCount(path); - var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated + const int BufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); - var buffer = ArrayPool.Shared.Rent(bufferSize); + var buffer = ArrayPool.Shared.Rent(BufferSize); try { Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; - var size = readlink(symlink, buffer, bufferSize); - Debug.Assert(size < bufferSize); // if this fails, we need to increase the buffer size (dynamically?) + var size = readlink(symlink, buffer, BufferSize); + Debug.Assert(size < BufferSize); // if this fails, we need to increase the buffer size (dynamically?) return Encoding.UTF8.GetString(buffer, 0, (int)size); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 68466fe381..5b84ceef7f 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -392,7 +392,7 @@ namespace Avalonia.Headless } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect rect) { } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 9f09212814..6634ab4d7b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -212,10 +212,10 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - _storageProvider = new CompositeStorageProvider(new Func>[] + _storageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null), - () => GtkSystemDialog.TryCreate(this), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => GtkSystemDialog.TryCreate(this) }); } diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index 3c04935f0d..a169966188 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -21,7 +21,7 @@ namespace Avalonia.Browser.Skia return null; } - public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session) { return null; } diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4c3cfe2ef4..ab9f9ea413 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 40d7e10ae3..170cc9d420 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; using Avalonia.Media; using SkiaSharp; -#nullable enable - namespace Avalonia.Skia { /// @@ -13,23 +10,24 @@ namespace Avalonia.Skia { public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath; - var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath; + var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; + var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, GeometryCombineMode.Xor => SKPathOp.Xor, GeometryCombineMode.Exclude => SKPathOp.Difference, - _ => SKPathOp.Union, + _ => SKPathOp.Union }; - var path = path1.Op(path2, op); + var path = path1?.Op(path2, op); EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = path?.Bounds.ToAvaloniaRect() ?? default; } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath? EffectivePath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index eededb2836..969f0b5e2a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -19,27 +19,28 @@ namespace Avalonia.Skia /// internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { - private IDisposable[] _disposables; + private IDisposable?[]? _disposables; private readonly Vector _dpi; - private readonly Stack _maskStack = new Stack(); - private readonly Stack _opacityStack = new Stack(); - private readonly Stack _blendingModeStack = new Stack(); + private readonly Stack _maskStack = new(); + private readonly Stack _opacityStack = new(); + private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer _visualBrushRenderer; + private readonly IVisualBrushRenderer? _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; private Matrix _currentTransform; private bool _disposed; - private GRContext _grContext; - public GRContext GrContext => _grContext; - private ISkiaGpu _gpu; + private GRContext? _grContext; + public GRContext? GrContext => _grContext; + private readonly ISkiaGpu? _gpu; private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get(); private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get(); private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get(); - private static SKShader s_acrylicNoiseShader; - private readonly ISkiaGpuRenderSession _session; - private bool _leased = false; + private static SKShader? s_acrylicNoiseShader; + private readonly ISkiaGpuRenderSession? _session; + private bool _leased; + private bool _useOpacitySaveLayer; /// /// Context create info. @@ -49,12 +50,12 @@ namespace Avalonia.Skia /// /// Canvas to draw to. /// - public SKCanvas Canvas; + public SKCanvas? Canvas; /// /// Surface to draw to. /// - public SKSurface Surface; + public SKSurface? Surface; /// /// Dpi of drawings. @@ -64,7 +65,7 @@ namespace Avalonia.Skia /// /// Visual brush renderer. /// - public IVisualBrushRenderer VisualBrushRenderer; + public IVisualBrushRenderer? VisualBrushRenderer; /// /// Render text without Lcd rendering. @@ -74,17 +75,17 @@ namespace Avalonia.Skia /// /// GPU-accelerated context (optional) /// - public GRContext GrContext; + public GRContext? GrContext; /// /// Skia GPU provider context (optional) /// - public ISkiaGpu Gpu; + public ISkiaGpu? Gpu; - public ISkiaGpuRenderSession CurrentSession; + public ISkiaGpuRenderSession? CurrentSession; } - class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature + private class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature { private readonly DrawingContextImpl _context; @@ -99,10 +100,11 @@ namespace Avalonia.Skia return new ApiLease(_context); } - class ApiLease : ISkiaSharpApiLease + private class ApiLease : ISkiaSharpApiLease { - private DrawingContextImpl _context; + private readonly DrawingContextImpl _context; private readonly SKMatrix _revertTransform; + private bool _isDisposed; public ApiLease(DrawingContextImpl context) { @@ -112,15 +114,18 @@ namespace Avalonia.Skia } public SKCanvas SkCanvas => _context.Canvas; - public GRContext GrContext => _context.GrContext; - public SKSurface SkSurface => _context.Surface; + public GRContext? GrContext => _context.GrContext; + public SKSurface? SkSurface => _context.Surface; public double CurrentOpacity => _context._currentOpacity; public void Dispose() { - _context.Canvas.SetMatrix(_revertTransform); - _context._leased = false; - _context = null; + if (!_isDisposed) + { + _context.Canvas.SetMatrix(_revertTransform); + _context._leased = false; + _isDisposed = true; + } } } } @@ -130,8 +135,11 @@ namespace Avalonia.Skia /// /// Create info. /// Array of elements to dispose after drawing has finished. - public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) + public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposables) { + Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas + ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + _dpi = createInfo.Dpi; _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; @@ -141,15 +149,9 @@ namespace Avalonia.Skia if (_grContext != null) Monitor.Enter(_grContext); Surface = createInfo.Surface; - Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas; _session = createInfo.CurrentSession; - if (Canvas == null) - { - throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); - } - if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) { _postTransform = @@ -157,13 +159,20 @@ namespace Avalonia.Skia } Transform = Matrix.Identity; + + var options = AvaloniaLocator.Current.GetService(); + + if(options != null) + { + _useOpacitySaveLayer = options.UseOpacitySaveLayer; + } } /// /// Skia canvas. /// public SKCanvas Canvas { get; } - public SKSurface Surface { get; } + public SKSurface? Surface { get; } private void CheckLease() { @@ -187,7 +196,7 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity))); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); @@ -205,87 +214,89 @@ namespace Avalonia.Skia } /// - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { CheckLease(); - if (pen is null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))) is { } stroke) { - return; - } - - using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) - { - if (paint.Paint is object) + using (stroke) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, stroke.Paint); } } } /// - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { CheckLease(); var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, - size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0))) : default) + if (brush is not null) { - if (fill.Paint != null) + using (var fill = CreatePaint(_fillPaint, brush, size)) { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + } - if (stroke.Paint != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) + { + using (stroke) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); } } } - struct BoxShadowFilter : IDisposable + private struct BoxShadowFilter : IDisposable { - public SKPaint Paint; - private SKImageFilter _filter; - public SKClipOperation ClipOperation; + public readonly SKPaint Paint; + private readonly SKImageFilter? _filter; + public readonly SKClipOperation ClipOperation; - static float SkBlurRadiusToSigma(double radius) { + private BoxShadowFilter(SKPaint paint, SKImageFilter? filter, SKClipOperation clipOperation) + { + Paint = paint; + _filter = filter; + ClipOperation = clipOperation; + } + + private static float SkBlurRadiusToSigma(double radius) { if (radius <= 0) return 0.0f; return 0.288675f * (float)radius + 0.5f; } + public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity) { var ac = shadow.Color; - SKImageFilter filter = null; - filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); + var filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)); paint.Reset(); paint.IsAntialias = true; paint.Color = color; paint.ImageFilter = filter; - - return new BoxShadowFilter - { - Paint = paint, _filter = filter, - ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference - }; + + var clipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference; + + return new BoxShadowFilter(paint, filter, clipOperation); } public void Dispose() { - Paint.Reset(); - Paint = null; + Paint?.Reset(); _filter?.Dispose(); } } - static SKRect AreaCastingShadowInHole( + private static SKRect AreaCastingShadowInHole( SKRect hole_rect, float shadow_blur, float shadow_spread, @@ -306,18 +317,16 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + public void DrawRectangle(IExperimentalAcrylicMaterial? material, RoundedRect rect) { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; CheckLease(); var rc = rect.Rect.ToSKRect(); - var isRounded = rect.IsRounded; - var needRoundRect = rect.IsRounded; - SKRoundRect skRoundRect = null; + SKRoundRect? skRoundRect = null; - if (needRoundRect) + if (rect.IsRounded) { skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, @@ -334,7 +343,7 @@ namespace Avalonia.Skia { using (var paint = CreateAcrylicPaint(_fillPaint, material)) { - if (isRounded) + if (skRoundRect is not null) { Canvas.DrawRoundRect(skRoundRect, paint.Paint); SKRoundRectCache.Shared.Return(skRoundRect); @@ -349,7 +358,7 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; @@ -362,7 +371,7 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); - SKRoundRect skRoundRect = null; + SKRoundRect? skRoundRect = null; if (needRoundRect) { skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect); @@ -372,7 +381,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && !boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -412,15 +421,15 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) + using (var fill = CreatePaint(_fillPaint, brush, rect.Rect.Size)) { if (isRounded) { - Canvas.DrawRoundRect(skRoundRect, paint.Paint); + Canvas.DrawRoundRect(skRoundRect, fill.Paint); } else { - Canvas.DrawRect(rc, paint.Paint); + Canvas.DrawRect(rc, fill.Paint); } } } @@ -429,7 +438,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; @@ -454,30 +463,28 @@ namespace Avalonia.Skia } } - if (pen?.Brush != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) + using (stroke) { - if (paint.Paint is object) + if (isRounded) { - if (isRounded) - { - Canvas.DrawRoundRect(skRoundRect, paint.Paint); - } - else - { - Canvas.DrawRect(rc, paint.Paint); - } + Canvas.DrawRoundRect(skRoundRect, stroke.Paint); + } + else + { + Canvas.DrawRect(rc, stroke.Paint); } } } - if(isRounded) + if (skRoundRect is not null) SKRoundRectCache.Shared.Return(skRoundRect); } /// - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { if (rect.Height <= 0 || rect.Width <= 0) return; @@ -487,26 +494,24 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) + using (var fill = CreatePaint(_fillPaint, brush, rect.Size)) { - Canvas.DrawOval(rc, paint.Paint); + Canvas.DrawOval(rc, fill.Paint); } } - if (pen?.Brush != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) + using (stroke) { - if (paint.Paint is object) - { - Canvas.DrawOval(rc, paint.Paint); - } + Canvas.DrawOval(rc, stroke.Paint); } } } /// - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { CheckLease(); @@ -571,18 +576,35 @@ namespace Avalonia.Skia } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { CheckLease(); - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; + + if(_useOpacitySaveLayer) + { + var rect = bounds.ToSKRect(); + Canvas.SaveLayer(rect, new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity)}); + } + else + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } } /// public void PopOpacity() { CheckLease(); - _currentOpacity = _opacityStack.Pop(); + + if(_useOpacitySaveLayer) + { + Canvas.Restore(); + } + else + { + _currentOpacity = _opacityStack.Pop(); + } } /// @@ -660,7 +682,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); - Canvas.SaveLayer(paint); + Canvas.SaveLayer(bounds.ToSKRect(), paint); _maskStack.Push(CreatePaint(paint, mask, bounds.Size)); } @@ -711,14 +733,12 @@ namespace Avalonia.Skia } } -#nullable enable public object? GetFeature(Type t) { if (t == typeof(ISkiaSharpApiLeaseFeature)) return new SkiaLeaseFeature(this); return null; } -#nullable restore /// /// Configure paint wrapper for using gradient brush. @@ -957,9 +977,10 @@ namespace Avalonia.Skia /// Visual brush. /// Visual brush renderer. /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, + IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage) { - if (_visualBrushRenderer == null) + if (visualBrushRenderer == null) { throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); } @@ -982,7 +1003,7 @@ namespace Avalonia.Skia } } - static SKColorFilter CreateAlphaColorFilter(double opacity) + private static SKColorFilter CreateAlphaColorFilter(double opacity) { if (opacity > 1) opacity = 1; @@ -997,7 +1018,7 @@ namespace Avalonia.Skia return SKColorFilter.CreateTable(a, c, c, c); } - static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + private static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) { var ca = leftColor / 255d; var aa = leftAlpha / 255d; @@ -1007,7 +1028,7 @@ namespace Avalonia.Skia return (byte)(r * 255); } - static Color Blend(Color left, Color right) + private static Color Blend(Color left, Color right) { var aa = left.A / 255d; var ab = right.A / 255d; @@ -1025,8 +1046,6 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = _currentOpacity; - var tintOpacity = material.BackgroundSource == AcrylicBackgroundSource.Digger ? material.TintOpacity : 1; @@ -1075,7 +1094,7 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = brush.Opacity * _currentOpacity; + double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity); if (brush is ISolidColorBrush solid) { @@ -1103,7 +1122,7 @@ namespace Avalonia.Skia } else { - tileBrushImage = (IDrawableBitmapImpl)(tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; + tileBrushImage = (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item as IDrawableBitmapImpl; } if (tileBrush != null && tileBrushImage != null) @@ -1125,16 +1144,16 @@ namespace Avalonia.Skia /// Source pen. /// Target size. /// - private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize) + private PaintWrapper? TryCreatePaint(SKPaint paint, IPen pen, Size targetSize) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. - if (pen.Thickness == 0d) + if (pen.Brush is not { } brush || pen.Thickness == 0d) { - return default; + return null; } - var rv = CreatePaint(paint, pen.Brush, targetSize); + var rv = CreatePaint(paint, brush, targetSize); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -1253,9 +1272,9 @@ namespace Avalonia.Skia //We are saving memory allocations there public readonly SKPaint Paint; - private IDisposable _disposable1; - private IDisposable _disposable2; - private IDisposable _disposable3; + private IDisposable? _disposable1; + private IDisposable? _disposable2; + private IDisposable? _disposable3; public PaintWrapper(SKPaint paint) { diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 90ff9652d8..d53dcd2df3 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -26,11 +26,11 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - [ThreadStatic] private static string[] t_languageTagBuffer; + [ThreadStatic] private static string[]? t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) + FontFamily? fontFamily, CultureInfo? culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -53,20 +53,13 @@ namespace Avalonia.Skia break; } - if (culture == null) - { - culture = CultureInfo.CurrentUICulture; - } - - if (t_languageTagBuffer == null) - { - t_languageTagBuffer = new string[2]; - } + culture ??= CultureInfo.CurrentUICulture; + t_languageTagBuffer ??= new string[2]; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + if (fontFamily is not null && fontFamily.FamilyNames.HasFallbacks) { var familyNames = fontFamily.FamilyNames; @@ -104,9 +97,9 @@ namespace Avalonia.Skia public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) { - SKTypeface skTypeface = null; + SKTypeface? skTypeface = null; - if (typeface.FontFamily.Key == null) + if (typeface.FontFamily.Key is null) { var defaultName = SKTypeface.Default.FamilyName; diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 05fad25f1b..f1216100bc 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Reactive; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; @@ -15,9 +16,9 @@ namespace Avalonia.Skia private readonly IFramebufferPlatformSurface _platformSurface; private SKImageInfo _currentImageInfo; private IntPtr _currentFramebufferAddress; - private SKSurface _framebufferSurface; - private PixelFormatConversionShim _conversionShim; - private IDisposable _preFramebufferCopyHandler; + private SKSurface? _framebufferSurface; + private PixelFormatConversionShim? _conversionShim; + private IDisposable? _preFramebufferCopyHandler; /// /// Create new framebuffer render target using a target surface. @@ -35,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -81,6 +82,7 @@ namespace Avalonia.Skia /// /// Desired image info. /// Backing framebuffer. + [MemberNotNull(nameof(_framebufferSurface))] private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) { if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index d6f19612c1..2828f9a9c1 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using Avalonia.Media; using SkiaSharp; -#nullable enable - namespace Avalonia.Skia { /// @@ -22,8 +20,10 @@ namespace Avalonia.Skia for (var i = 0; i < count; ++i) { - if (children[i]?.PlatformImpl is GeometryImpl child) - path.AddPath(child.EffectivePath); + if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + { + path.AddPath(effectivePath); + } } EffectivePath = path; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 51386d2a45..34270c2078 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -11,20 +12,9 @@ namespace Avalonia.Skia internal abstract class GeometryImpl : IGeometryImpl { private PathCache _pathCache; - private SKPathMeasure _pathMeasureCache; + private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure - { - get - { - if (_pathMeasureCache is null) - { - _pathMeasureCache = new SKPathMeasure(EffectivePath); - } - - return _pathMeasureCache; - } - } + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); /// public abstract Rect Bounds { get; } @@ -37,11 +27,11 @@ namespace Avalonia.Skia if (EffectivePath is null) return 0; - return (double)CachedPathMeasure?.Length; + return CachedPathMeasure.Length; } } - public abstract SKPath EffectivePath { get; } + public abstract SKPath? EffectivePath { get; } /// public bool FillContains(Point point) @@ -50,7 +40,7 @@ namespace Avalonia.Skia } /// - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { // Skia requires to compute stroke path to check for point containment. // Due to that we are caching using stroke width. @@ -98,21 +88,26 @@ namespace Avalonia.Skia /// Path to check. /// Point. /// True, if point is contained in a path. - private static bool PathContainsCore(SKPath path, Point point) + private static bool PathContainsCore(SKPath? path, Point point) { - return path.Contains((float)point.X, (float)point.Y); + return path is not null && path.Contains((float)point.X, (float)point.Y); } /// - public IGeometryImpl Intersect(IGeometryImpl geometry) + public IGeometryImpl? Intersect(IGeometryImpl geometry) { - var result = EffectivePath.Op(((GeometryImpl)geometry).EffectivePath, SKPathOp.Intersect); + if (EffectivePath is { } path + && (geometry as GeometryImpl)?.EffectivePath is { } otherPath + && path.Op(otherPath, SKPathOp.Intersect) is { } result) + { + return new StreamGeometryImpl(result); + } - return result == null ? null : new StreamGeometryImpl(result); + return null; } /// - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); @@ -161,7 +156,7 @@ namespace Avalonia.Skia } public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, - out IGeometryImpl segmentGeometry) + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { if (EffectivePath is null) { @@ -203,7 +198,7 @@ namespace Avalonia.Skia /// /// Cached contour path. /// - public SKPath CachedStrokePath { get; private set; } + public SKPath? CachedStrokePath { get; private set; } /// /// Cached geometry render bounds. @@ -244,6 +239,7 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); + CachedStrokePath = null; CachedGeometryRenderBounds = default; _cachedStrokeWidth = default; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index cfd6fc12f8..079eea7bef 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Avalonia.Platform; using SkiaSharp; -#nullable enable namespace Avalonia.Skia { @@ -10,7 +9,7 @@ namespace Avalonia.Skia { public GlyphRunImpl(SKTextBlob textBlob, Size size, Point baselineOrigin) { - TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); + TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); Size = size; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index a8dd289a13..3093455bec 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -140,7 +140,7 @@ namespace Avalonia.Skia return Font.GetHorizontalGlyphAdvances(glyphIndices); } - private Blob GetTable(Face face, Tag tag) + private Blob? GetTable(Face face, Tag tag) { var size = Typeface.GetTableSize(tag); @@ -166,8 +166,8 @@ namespace Avalonia.Skia return; } - Font?.Dispose(); - Face?.Dispose(); + Font.Dispose(); + Face.Dispose(); } public void Dispose() diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index a5782037f3..e6e30a1203 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -15,14 +15,14 @@ namespace Avalonia.Skia /// /// Surfaces. /// Created render target or if it fails. - ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); + ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces); /// /// Creates an offscreen render target surface /// /// size in pixels. /// An optional custom render session. - ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session); + ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session); } public interface ISkiaSurface : IDisposable diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index e19379df09..4a3031d9ad 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; + namespace Avalonia.Skia { internal class FboSkiaSurface : ISkiaSurface @@ -14,6 +15,7 @@ namespace Avalonia.Skia private int _fbo; private int _depthStencil; private int _texture; + private SKSurface? _surface; private static readonly bool[] TrueFalse = new[] { true, false }; public FboSkiaSurface(GlSkiaGpu gpu, GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) @@ -89,7 +91,7 @@ namespace Avalonia.Skia var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 8, new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); - Surface = SKSurface.Create(_grContext, target, + _surface = SKSurface.Create(_grContext, target, surfaceOrigin, SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); CanBlit = gl.IsBlitFramebufferAvailable; } @@ -100,8 +102,8 @@ namespace Avalonia.Skia { using (_glContext.EnsureCurrent()) { - Surface?.Dispose(); - Surface = null; + _surface?.Dispose(); + _surface = null; var gl = _glContext.GlInterface; if (_fbo != 0) { @@ -113,11 +115,11 @@ namespace Avalonia.Skia } catch (PlatformGraphicsContextLostException) { - if (Surface != null) + if (_surface != null) // We need to dispose SKSurface _after_ GRContext.Abandon was called, // otherwise it will try to do OpenGL calls without a proper context - _gpu.AddPostDispose(Surface.Dispose); - Surface = null; + _gpu.AddPostDispose(_surface.Dispose); + _surface = null; } finally { @@ -125,8 +127,10 @@ namespace Avalonia.Skia } } - public SKSurface Surface { get; private set; } + public SKSurface Surface => _surface ?? throw new ObjectDisposedException(nameof(FboSkiaSurface)); + public bool CanBlit { get; } + public void Blit(SKCanvas canvas) { // This should set the render target as the current FBO diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs index 4bf43634ef..2b6caf34dc 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using Avalonia.OpenGL; @@ -150,6 +149,11 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) { + if (_image is null) + { + throw new NotSupportedException("Only supported with an external image"); + } + using (_gpu.EnsureCurrent()) { _image.AcquireKeyedMutex(acquireIndex); @@ -167,6 +171,11 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, IPlatformRenderInterfaceImportedSemaphore signalSemaphore) { + if (_image is null) + { + throw new NotSupportedException("Only supported with an external image"); + } + var wait = (GlSkiaImportedSemaphore)waitForSemaphore; var signal = (GlSkiaImportedSemaphore)signalSemaphore; using (_gpu.EnsureCurrent()) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index bf3e950e81..d403855094 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -10,15 +10,15 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + internal class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { - private GRContext _grContext; - private IGlContext _glContext; + private readonly GRContext _grContext; + private readonly IGlContext _glContext; public GRContext GrContext => _grContext; public IGlContext GlContext => _glContext; - private List _postDisposeCallbacks = new(); + private readonly List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; - private IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; + private readonly IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { @@ -41,7 +41,7 @@ namespace Avalonia.Skia } } - class SurfaceWrapper : IGlPlatformSurface + private class SurfaceWrapper : IGlPlatformSurface { private readonly object _surface; @@ -57,7 +57,7 @@ namespace Avalonia.Skia } } - public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { var customRenderTargetFactory = _glContext.TryGetFeature(); foreach (var surface in surfaces) @@ -75,7 +75,7 @@ namespace Avalonia.Skia return null; } - public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session) { // Only windows platform needs our FBO trickery if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -106,7 +106,7 @@ namespace Avalonia.Skia public bool CanCreateSharedContext => _glContext.CanCreateSharedContext; - public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + public IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null) => _glContext.CreateSharedContext(preferredVersions); public ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size) @@ -153,7 +153,7 @@ namespace Avalonia.Skia public bool IsLost => _glContext.IsLost; public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) return this; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6b4a7a3409..7f9108481d 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) { var session = _renderTarget.BeginRenderingSession(); diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index ec33770356..4d8afe9830 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -15,7 +15,7 @@ namespace Avalonia.Skia.Helpers /// /// /// DrawingContext - public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer visualBrushRenderer = null) + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null) { var createInfo = new DrawingContextImpl.CreateInfo { diff --git a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs index b3966c0324..66abd818e6 100644 --- a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs +++ b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs @@ -14,7 +14,7 @@ public interface ISkiaSharpApiLeaseFeature public interface ISkiaSharpApiLease : IDisposable { SKCanvas SkCanvas { get; } - GRContext GrContext { get; } - SKSurface SkSurface { get; } + GRContext? GrContext { get; } + SKSurface? SkSurface { get; } double CurrentOpacity { get; } -} \ No newline at end of file +} diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 4ab873fd8d..0627407509 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -100,7 +100,7 @@ namespace Avalonia.Skia _bitmap = scaledBmp; } - _bitmap!.SetImmutable(); + _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); @@ -134,7 +134,7 @@ namespace Avalonia.Skia data); _bitmap = tmp.Copy(); } - _bitmap!.SetImmutable(); + _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); if (_image == null) @@ -179,10 +179,13 @@ namespace Avalonia.Skia public PixelFormat? Format => _bitmap?.ColorType.ToAvalonia(); public ILockedFramebuffer Lock() { - if (_bitmap == null) - throw new NotSupportedException(); - return new LockedFramebuffer(_bitmap.GetPixels(), PixelSize, _bitmap.RowBytes, Dpi, - _bitmap.ColorType.ToAvalonia().Value, null); + if (_bitmap is null) + throw new NotSupportedException("A bitmap is needed for locking"); + + if (_bitmap.ColorType.ToAvalonia() is not { } format) + throw new NotSupportedException($"Unsupported format {_bitmap.ColorType}"); + + return new LockedFramebuffer(_bitmap.GetPixels(), PixelSize, _bitmap.RowBytes, Dpi, format, null); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 8e9a19239b..ab1c6b8816 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -24,7 +24,7 @@ namespace Avalonia.Skia } - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) { if (graphicsContext == null) return new SkiaContext(null); @@ -225,7 +225,7 @@ namespace Avalonia.Skia throw new ArgumentNullException(nameof(glyphInfos)); } - var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; + var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; var font = SKFontCache.Shared.Get(); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 73f58e66bc..9ee17a09d6 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using SkiaSharp; @@ -6,20 +7,19 @@ namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _typefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typefaces = new(); public void AddTypeface(Typeface key, SKTypeface typeface) { _typefaces.TryAdd(key, typeface); } - public SKTypeface Get(Typeface typeface) + public SKTypeface? Get(Typeface typeface) { return GetNearestMatch(typeface); } - private SKTypeface GetNearestMatch(Typeface key) + private SKTypeface? GetNearestMatch(Typeface key) { if (_typefaces.Count == 0) { @@ -70,7 +70,7 @@ namespace Avalonia.Skia return typeface; } - SKTypeface skTypeface = null; + SKTypeface? skTypeface = null; foreach(var pair in _typefaces) { @@ -85,7 +85,7 @@ namespace Avalonia.Skia return skTypeface; } - private bool TryFindStretchFallback(Typeface key, out SKTypeface typeface) + private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) { typeface = null; var stretch = (int)key.Stretch; @@ -114,7 +114,7 @@ namespace Avalonia.Skia return false; } - private bool TryFindWeightFallback(Typeface key, out SKTypeface typeface) + private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) { typeface = null; var weight = (int)key.Weight; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index b49efd59cd..d064f49ae4 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -23,7 +23,7 @@ namespace Avalonia.Skia /// public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) { - return s_cachedCollections.GetOrAdd(fontFamily, x => CreateCustomFontCollection(fontFamily)); + return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); } /// @@ -33,10 +33,15 @@ namespace Avalonia.Skia /// private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) { - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); - var typeFaceCollection = new SKTypefaceCollection(); + if (fontFamily.Key is not { } fontFamilyKey) + { + return typeFaceCollection; + } + + var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); foreach (var asset in fontAssets) diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 0cf66767cb..51e182f7e3 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -9,9 +9,9 @@ namespace Avalonia.Skia; internal class SkiaContext : IPlatformRenderInterfaceContext { - private ISkiaGpu _gpu; + private ISkiaGpu? _gpu; - public SkiaContext(ISkiaGpu gpu) + public SkiaContext(ISkiaGpu? gpu) { _gpu = gpu; } @@ -25,10 +25,10 @@ internal class SkiaContext : IPlatformRenderInterfaceContext /// public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - if (!(surfaces is IList)) + if (surfaces is not IList) surfaces = surfaces.ToList(); - var gpuRenderTarget = _gpu?.TryCreateRenderTarget(surfaces); - if (gpuRenderTarget != null) + + if (_gpu?.TryCreateRenderTarget(surfaces) is { } gpuRenderTarget) { return new SkiaGpuRenderTarget(_gpu, gpuRenderTarget); } @@ -43,7 +43,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } - public bool IsLost => _gpu.IsLost; + public bool IsLost => _gpu?.IsLost ?? false; - public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); + public object? TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); } diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index b3c3056a58..84ad547d6c 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,5 +16,13 @@ namespace Avalonia /// Setting this to null will give you the default Skia value. /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. + + /// + /// Use Skia's SaveLayer API to handling opacity. + /// + /// + /// Enabling this might have performance implications. + /// + public bool UseOpacitySaveLayer { get; set; } = false; } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 20dde27e9a..c66b53284a 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; @@ -111,7 +112,7 @@ namespace Avalonia.Skia return sm; } - public static SKColor ToSKColor(this Media.Color c) + public static SKColor ToSKColor(this Color c) { return new SKColor(c.R, c.G, c.B, c.A); } @@ -171,14 +172,14 @@ namespace Avalonia.Skia }; } - public static SKShaderTileMode ToSKShaderTileMode(this Media.GradientSpreadMethod m) + public static SKShaderTileMode ToSKShaderTileMode(this GradientSpreadMethod m) { switch (m) { default: - case Media.GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp; - case Media.GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror; - case Media.GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat; + case GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp; + case GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror; + case GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat; } } @@ -215,7 +216,8 @@ namespace Avalonia.Skia }; } - public static SKPath Clone(this SKPath src) + [return: NotNullIfNotNull(nameof(src))] + public static SKPath? Clone(this SKPath? src) { return src != null ? new SKPath(src) : null; } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index df847d2224..0c3289767e 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); + return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); } /// diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index e5fb182a3b..f88e74d738 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,7 +1,6 @@ using System; using System.IO; using Avalonia.Reactive; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Skia.Helpers; @@ -17,24 +16,26 @@ namespace Avalonia.Skia private readonly ISkiaSurface _surface; private readonly SKCanvas _canvas; private readonly bool _disableLcdRendering; - private readonly GRContext _grContext; - private readonly ISkiaGpu _gpu; + private readonly GRContext? _grContext; + private readonly ISkiaGpu? _gpu; - class SkiaSurfaceWrapper : ISkiaSurface + private class SkiaSurfaceWrapper : ISkiaSurface { - public SKSurface Surface { get; private set; } + private SKSurface? _surface; + + public SKSurface Surface => _surface ?? throw new ObjectDisposedException(nameof(SkiaSurfaceWrapper)); public bool CanBlit => false; public void Blit(SKCanvas canvas) => throw new NotSupportedException(); public SkiaSurfaceWrapper(SKSurface surface) { - Surface = surface; + _surface = surface; } public void Dispose() { - Surface?.Dispose(); - Surface = null; + _surface?.Dispose(); + _surface = null; } } @@ -51,18 +52,25 @@ namespace Avalonia.Skia _grContext = createInfo.GrContext; _gpu = createInfo.Gpu; - if (!createInfo.DisableManualFbo) - _surface = _gpu?.TryCreateSurface(PixelSize, createInfo.Session); - if (_surface == null) - _surface = new SkiaSurfaceWrapper(CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, - createInfo.Format)); + ISkiaSurface? surface = null; - _canvas = _surface?.Surface.Canvas; + if (!createInfo.DisableManualFbo) + surface = _gpu?.TryCreateSurface(PixelSize, createInfo.Session); - if (_surface == null || _canvas == null) + if (surface is null) { - throw new InvalidOperationException("Failed to create Skia render target surface"); + if (CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, createInfo.Format) + is { } skSurface) + { + surface = new SkiaSurfaceWrapper(skSurface); + } } + + if (surface?.Surface.Canvas is not { } canvas) + throw new InvalidOperationException("Failed to create Skia render target surface"); + + _surface = surface; + _canvas = canvas; } /// @@ -73,7 +81,7 @@ namespace Avalonia.Skia /// Height. /// Format. /// - private static SKSurface CreateSurface(GRContext gpu, int width, int height, PixelFormat? format) + private static SKSurface? CreateSurface(GRContext? gpu, int width, int height, PixelFormat? format) { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) @@ -89,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -218,11 +226,11 @@ namespace Avalonia.Skia /// /// GPU-accelerated context (optional) /// - public GRContext GrContext; + public GRContext? GrContext; - public ISkiaGpu Gpu; + public ISkiaGpu? Gpu; - public ISkiaGpuRenderSession Session; + public ISkiaGpuRenderSession? Session; public bool DisableManualFbo; } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index a21038839c..e7dd4fb6da 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia var usedCulture = culture ?? CultureInfo.CurrentCulture; - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, i => new Language(usedCulture)); + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; @@ -170,7 +170,7 @@ namespace Avalonia.Skia return segment.Array.AsMemory(); } - if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager? memoryManager, out start, out length)) { return memoryManager.Memory; } diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index 64d5b58970..fb3c2e403f 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -19,14 +19,14 @@ namespace Avalonia.Skia Transform = transform; var transformedPath = source.EffectivePath.Clone(); - transformedPath.Transform(transform.ToSKMatrix()); + transformedPath?.Transform(transform.ToSKMatrix()); EffectivePath = transformedPath; - Bounds = transformedPath.TightBounds.ToAvaloniaRect(); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; } /// - public override SKPath EffectivePath { get; } + public override SKPath? EffectivePath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 56e627f2d8..8ea7434c23 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -15,7 +15,7 @@ namespace Avalonia.Skia { private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; private readonly SKBitmap _bitmap; - private readonly object _lock = new object(); + private readonly object _lock = new(); /// /// Create a WriteableBitmap from given stream. @@ -205,8 +205,8 @@ namespace Avalonia.Skia _bitmap.NotifyPixelsChanged(); _parent.Version++; Monitor.Exit(_parent._lock); - _bitmap = null; - _parent = null; + _bitmap = null!; + _parent = null!; } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 3506abc63b..0dd9c155bb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -441,14 +441,15 @@ namespace Avalonia.Direct2D1.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { if (opacity < 1) { var parameters = new LayerParameters { - ContentBounds = PrimitiveExtensions.RectangleInfinite, + ContentBounds = bounds.ToDirect2D(), MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = (float)opacity, }; diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index e5c2fc6cf6..2d9331b5dc 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 30da940514..7e63987d1b 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -7,11 +7,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj new file mode 100644 index 0000000000..31b8d08541 --- /dev/null +++ b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs new file mode 100644 index 0000000000..0a27602604 --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -0,0 +1,795 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Avalonia.Analyzers; + +public partial class AvaloniaPropertyAnalyzer +{ + public class CompileAnalyzer + { + /// + /// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them. + /// + private ImmutableDictionary _avaloniaPropertyDescriptions = null!; + + /// + /// Maps properties onto all AvaloniaProperty objects that they may be intended to represent. + /// + private ImmutableDictionary> _clrPropertyToAvaloniaProperties = null!; + + private readonly INamedTypeSymbol _stringType; + private readonly INamedTypeSymbol _avaloniaObjectType; + private readonly ImmutableHashSet _getValueMethods; + private readonly ImmutableHashSet _setValueMethods; + private readonly ImmutableHashSet _allGetSetMethods; + private readonly INamedTypeSymbol _avaloniaPropertyType; + private readonly INamedTypeSymbol _styledPropertyType; + private readonly INamedTypeSymbol _attachedPropertyType; + private readonly INamedTypeSymbol _directPropertyType; + private readonly INamedTypeSymbol? _userControlType; + private readonly INamedTypeSymbol? _topLevelType; + private readonly ImmutableHashSet _allAvaloniaPropertyTypes; + private readonly ImmutableDictionary _propertyValueTypeParams; + private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; + private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; + private readonly ImmutableHashSet _allAvaloniaPropertyMethods; + private readonly ImmutableDictionary _ownerTypeParams; + private readonly ImmutableDictionary _valueTypeParams; + private readonly ImmutableDictionary _hostTypeParams; + private readonly ImmutableDictionary _inheritsParams; + private readonly ImmutableDictionary _ownerParams; + + public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) + { + var methodComparer = SymbolEqualityComparer.Default; + + _stringType = GetTypeOrThrow("System.String"); + _avaloniaObjectType = avaloniaObjectType; + _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(methodComparer); + _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(methodComparer); + _allGetSetMethods = _getValueMethods.Concat(_setValueMethods).ToImmutableHashSet(methodComparer); + + _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); + _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); + _attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1"); + _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2"); + + _userControlType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl"); + _topLevelType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.TopLevel"); + + _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() + .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer); + + _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableHashSet(SymbolEqualityComparer.Default); + + _propertyValueTypeParams = _allAvaloniaPropertyTypes.Select(p => p.TypeParameters.First(t => t.Name == "TValue")) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => p.ContainingType, SymbolEqualityComparer.Default); + + _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes + .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); + + _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(methodComparer); + + _ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters); + _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters); + _hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters); + _inheritsParams = GetParamDictionary("inherits", m => m.Parameters); + _ownerParams = GetParamDictionary("ownerType", m => m.Parameters); + + RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); + + context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); + context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); + context.RegisterOperationAction(AnalyzePropertyStorageAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzePropertyWrapperAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); + + context.RegisterSymbolAction(AnalyzeWrapperCrlProperty, SymbolKind.Property); + + if (context.Compilation.Language == LanguageNames.CSharp) + { + context.RegisterCodeBlockAction(AnalyzePropertyMethods); + } + + INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); + + ImmutableDictionary GetParamDictionary(string name, Func> methodSymbolSelector) where TSymbol : ISymbol => _allAvaloniaPropertyMethods + .Select(m => methodSymbolSelector(m).SingleOrDefault(p => p.Name == name)) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => (IMethodSymbol)p.ContainingSymbol, SymbolEqualityComparer.Default); + } + + private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + private bool IsAvaloniaPropertyStorage(IPropertySymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + + private void RegisterAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) + { + var namespaceStack = new Stack(); + namespaceStack.Push(compilation.GlobalNamespace); + + var types = new List(); + + while (namespaceStack.Count > 0) + { + var current = namespaceStack.Pop(); + + types.AddRange(current.GetTypeMembers()); + + foreach (var child in current.GetNamespaceMembers()) + { + namespaceStack.Push(child); + } + } + + var avaloniaPropertyStorageSymbols = new ConcurrentBag(); + + var propertyDescriptions = new ConcurrentDictionary(SymbolEqualityComparer.Default); + + // key initializes value + var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); + + var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; + + var semanticModels = new ConcurrentDictionary(); + + Parallel.ForEach(types, parallelOptions, type => + { + try + { + foreach (var member in type.GetMembers()) + { + switch (member) + { + case IFieldSymbol fieldSymbol when IsAvaloniaPropertyStorage(fieldSymbol): + avaloniaPropertyStorageSymbols.Add(fieldSymbol); + break; + case IPropertySymbol propertySymbol when IsAvaloniaPropertyStorage(propertySymbol): + avaloniaPropertyStorageSymbols.Add(propertySymbol); + break; + } + } + + foreach (var constructor in type.StaticConstructors) + { + foreach (var syntaxRef in constructor.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) + { + var (node, model) = GetNodeAndModel(syntaxRef); + + foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression))) + { + var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; + + if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target) + { + RegisterAssignment(target, assignmentOperation.Value); + } + } + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new AvaloniaAnalysisException($"Failed to find AvaloniaProperty objects in {type}.", ex); + } + }); + + Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol => + { + foreach (var syntaxRef in symbol.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) + { + var (node, model) = GetNodeAndModel(syntaxRef); + + var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault(); + + if (operation == null) + { + return; + } + + RegisterAssignment(symbol, operation.Value); + } + }); + + // we have recorded every Register and AddOwner call. Now follow assignment chains. + Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root => + { + var propertyDescription = propertyDescriptions[root]; + var owner = propertyDescription.AssignedTo[root]; + + var current = root; + do + { + var target = fieldInitializations[current]; + + propertyDescription.SetAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type + propertyDescriptions[target] = propertyDescription; + + fieldInitializations.TryGetValue(target, out current); + } + while (current != null); + }); + + var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); + + var propertyDescriptionsByName = propertyDescriptions.Values.ToLookup(p => p.Name, p => (property: p, owners: p.OwnerTypes.Select(t => t.Type).ToImmutableHashSet(SymbolEqualityComparer.Default))); + + // Detect CLR properties that provide syntatic wrapping around an AvaloniaProperty (or potentially multiple, which leads to a warning diagnostic) + Parallel.ForEach(propertyDescriptions.Values, propertyDescription => + { + var nameMatches = propertyDescriptionsByName[propertyDescription.Name]; + + foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default)) + { + if (ownerType.GetMembers(propertyDescription.Name).OfType().SingleOrDefault() is not { IsStatic: false } clrProperty) + { + continue; + } + + propertyDescription.AddPropertyWrapper(clrProperty); + clrPropertyWrapCandidates.Add((clrProperty, propertyDescription)); + + var current = ownerType.BaseType; + while (current != null) + { + foreach (var otherProp in nameMatches.Where(t => t.owners.Contains(current)).Select(t => t.property)) + { + clrPropertyWrapCandidates.Add((clrProperty, otherProp)); + } + + current = current.BaseType; + } + } + }); + + // convert our dictionaries to immutable form + _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer.Default) + .ToImmutableDictionary(g => g.Key, g => g.Distinct().ToImmutableArray(), SymbolEqualityComparer.Default); + _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); + + void RegisterAssignment(ISymbol target, IOperation value) + { + switch (ResolveOperationSource(value)) + { + case IInvocationOperation invocation: + RegisterInitializer_Invocation(invocation, target, propertyDescriptions); + break; + case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field): + fieldInitializations[fieldRef.Field] = target; + break; + case IPropertyReferenceOperation propRef when IsAvaloniaPropertyStorage(propRef.Property): + fieldInitializations[propRef.Property] = target; + break; + } + } + + (SyntaxNode, SemanticModel) GetNodeAndModel(SyntaxReference syntaxRef) => + (syntaxRef.GetSyntax(cancellationToken), semanticModels.GetOrAdd(syntaxRef.SyntaxTree, st => compilation.GetSemanticModel(st))); + } + + // This method handles registration of a new AvaloniaProperty, and calls to AddOwner. + private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions) + { + try + { + if (invocation.TargetMethod.ReturnType is not INamedTypeSymbol propertyType) + { + return; + } + + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_avaloniaPropertyRegisterMethods.Contains(originalMethod)) // This is a call to one of the AvaloniaProperty.Register* methods + { + TypeReference ownerTypeRef; + + if (_ownerTypeParams.TryGetValue(originalMethod, out var ownerTypeParam)) + { + ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); + } + else if (_ownerParams.TryGetValue(originalMethod, out var ownerParam) && // try extracting the runtime argument + ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value) is ITypeOfOperation { Type: ITypeSymbol type } typeOf) + { + ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); + } + else + { + return; + } + + TypeReference valueTypeRef; + if (_valueTypeParams.TryGetValue(originalMethod, out var valueTypeParam)) + { + valueTypeRef = TypeReference.FromInvocationTypeParameter(invocation, valueTypeParam); + } + else + { + return; + } + + string name; + switch (ResolveOperationSource(invocation.Arguments[0].Value)) + { + case ILiteralOperation literal when SymbolEquals(literal.Type, _stringType): + name = (string)literal.ConstantValue.Value!; + break; + case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference: + name = propertyReference.Property.Name; + break; + case IFieldReferenceOperation fieldRef when SymbolEquals(fieldRef.Type, _stringType) && fieldRef.ConstantValue is { HasValue: true } constantValue: + name = (string)fieldRef.ConstantValue.Value!; + break; + default: + return; + } + + var inherits = false; + if (_inheritsParams.TryGetValue(originalMethod, out var inheritsParam) && + invocation.Arguments[inheritsParam.Ordinal].Value is ILiteralOperation literalOp && + literalOp.ConstantValue.Value is bool constValue) + { + inherits = constValue; + } + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + if (_hostTypeParams.TryGetValue(originalMethod, out var hostTypeParam)) + { + hostTypeRef = TypeReference.FromInvocationTypeParameter(invocation, hostTypeParam); + } + else + { + hostTypeRef = new(_avaloniaObjectType, Location.None); + } + } + + var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, valueTypeRef.Type)); + description.Name = name; + description.HostType = hostTypeRef; + description.Inherits = inherits; + description.SetAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); + } + else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods + { + if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam)) + { + return; + } + + if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol) + { + return; + } + + var description = propertyDescriptions[target] = propertyDescriptions.GetOrAdd(sourceSymbol, s => + { + string inferredName = s.Name; + + var match = Regex.Match(s.Name, "(?.*)Property$"); + if (match.Success) + { + inferredName = match.Groups["name"].Value; + } + + if (!_propertyValueTypeParams.TryGetValue(propertyType.OriginalDefinition, out var propertyValueType)) + { + throw new InvalidOperationException($"{propertyType} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType})."); + } + + var valueType = propertyType.TypeArguments[propertyValueType.Ordinal]; + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration + } + + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + + // assume that the property is owned by its containing type at the point of assignment, until we find its registration + result.SetAssignment(s, new(s.ContainingType, Location.None)); + + return result; + }); + + var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); + description.SetAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to register the initializer of '{target}'.", ex); + } + } + + /// + private void AnalyzeFieldInitializer(OperationAnalysisContext context) + { + var operation = (IFieldInitializerOperation)context.Operation; + + foreach (var field in operation.InitializedFields) + { + try + { + if (!_avaloniaPropertyDescriptions.TryGetValue(field, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(field)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, field.Locations[0], field)); + } + + AnalyzeInitializer_Shared(context, field, description); + + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of field '{field}'.", ex); + } + } + } + + /// + private void AnalyzePropertyInitializer(OperationAnalysisContext context) + { + var operation = (IPropertyInitializerOperation)context.Operation; + + foreach (var property in operation.InitializedProperties) + { + try + { + if (!_avaloniaPropertyDescriptions.TryGetValue(property, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(property)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, property.Locations[0], property)); + } + + AnalyzeInitializer_Shared(context, property, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of property '{property}'.", ex); + } + } + } + + /// + private void AnalyzePropertyStorageAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + try + { + var (target, isValid) = ResolveOperationSource(operation.Target) switch + { + IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), + IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), + _ => (default(ISymbol), false), + }; + + if (target == null || !_avaloniaPropertyDescriptions.TryGetValue(target, out var description)) + { + return; + } + + if (!isValid) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, target.Locations[0], target)); + } + + AnalyzeInitializer_Shared(context, target, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process assignment '{operation}'.", ex); + } + } + + /// + /// + private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description) + { + if (!assignmentSymbol.Name.Contains(description.Name) && assignmentSymbol.DeclaredAccessibility != Accessibility.Private) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0], + description.Name, assignmentSymbol)); + } + + try + { + var ownerType = description.AssignedTo[assignmentSymbol]; + + if (ownerType.Type.TypeKind != TypeKind.Error && + !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && + !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) + { + context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type)); + } + } + catch (KeyNotFoundException) + { + throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded."); + } + } + + /// + private void AnalyzePropertyWrapperAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + if (ResolveOperationSource(operation) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) + { + // We can consider `new MyType(myValue)` functionally equivalent to `new MyType() { Value = myValue }`. Both set a local value with an external parameter. + return; + } + + if (ResolveOperationTarget(operation) is IPropertyReferenceOperation propertyRef && + propertyRef.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + _clrPropertyToAvaloniaProperties.TryGetValue(propertyRef.Property, out var propertyDescriptions) && + propertyDescriptions.Any(p => !SymbolEquals(p.PropertyType.OriginalDefinition, _directPropertyType))) + { + if (DerivesFrom(propertyRef.Instance.Type, _userControlType) || DerivesFrom(propertyRef.Instance.Type, _topLevelType)) + { + // Special case: don't warn about local value assignment on a UserControl or TopLevel type. + // 1. We don't want to annoy new users, who start with these two types and don't understand binding priorities yet + // 2. Such controls either have no consumers, or are treated largely as a black box (i.e. no styles setting dynamic values) + return; + } + + context.ReportDiagnostic(Diagnostic.Create(SettingOwnStyledPropertyValue, operation.Syntax.GetLocation())); + } + } + + /// + /// + /// + private void AnalyzeMethodInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_allGetSetMethods.Contains(originalMethod)) + { + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + GetReferencedProperty(invocation.Arguments[0]) is { } refProp && + refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && + !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && + !DerivesFrom(context.ContainingSymbol.ContainingType, refProp.description.HostType?.Type)) + { + context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(), + refProp.storageSymbol, context.ContainingSymbol.ContainingType)); + } + } + else if (_allAvaloniaPropertyMethods.Contains(originalMethod)) + { + if (!IsStaticConstructorOrInitializer()) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(), + originalMethod.ToDisplayString(TypeQualifiedName))); + } + + if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) && + invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is { } newOwnerType) + { + if (newOwnerType is INamedTypeSymbol { IsGenericType: true }) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); + } + + if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!) is { } refProp) + { + var ownerMatches = refProp.description.AssignedTo.Where(kvp => !SymbolEquals(kvp.Key, context.ContainingSymbol) && DerivesFrom(newOwnerType, kvp.Value.Type)).ToArray(); + + if (ownerMatches.Any()) + { + var ownerMatchesExceptBaseTypes = ownerMatches.Where(m => !DerivesFrom(context.ContainingSymbol.ContainingType, m.Key.ContainingType, includeSelf: false)).ToArray(); + var routesMessage = ownerMatchesExceptBaseTypes.Length switch + { + 0 => "its base type", + 1 => ownerMatchesExceptBaseTypes.Single().Key.ToString(), + _ => $"{ownerMatches.Length} routes\n\t{string.Join("\n\t", ownerMatches.Select(kvp => kvp.Key))}" + }; + + context.ReportDiagnostic(Diagnostic.Create(SuperfluousAddOwnerCall, invocation.Syntax.GetLocation(), ownerMatches.Select(kvp => kvp.Value.Location), + newOwnerType, refProp.storageSymbol, routesMessage)); + } + } + } + } + + bool IsStaticConstructorOrInitializer() => + context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.StaticConstructor } || + ResolveOperationTarget(invocation.Parent!) switch + { + IFieldInitializerOperation fieldInit when fieldInit.InitializedFields.All(f => f.IsStatic) => true, + IPropertyInitializerOperation propInit when propInit.InitializedProperties.All(p => p.IsStatic) => true, + _ => false, + }; + } + + private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation) + { + if (GetReferencedFieldOrProperty(operation) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result)) + { + return (result, storageSymbol); + } + else + { + return null; + } + } + + /// + /// + /// + /// + /// + private void AnalyzeWrapperCrlProperty(SymbolAnalysisContext context) + { + var property = (IPropertySymbol)context.Symbol; + + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) + { + return; // does not refer to an AvaloniaProperty + } + + try + { + if (candidateTargetProperties.Length > 1) + { + var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null).OrderBy(s => s!.Name); + context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations), + property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}")); + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties[0]; + var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType); + + if (avaloniaPropertyStorage == null) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] }, + avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage)); + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true)) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0], + avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}")); + } + + if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage)); + } + + VerifyAccessor(property.GetMethod, "readable", "get"); + + if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + VerifyAccessor(property.SetMethod, "writeable", "set"); + } + + void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) + { + if (method == null) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } + else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); + } + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to analyse wrapper property '{property}'.", ex); + } + } + + /// + private void AnalyzePropertyMethods(CodeBlockAnalysisContext context) + { + if (context.OwningSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property } method) + { + return; + } + + try + { + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) || + candidateTargetProperties.Length != 1) // a diagnostic about multiple candidates will have already been reported + { + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties.Single(); + + if (IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + return; + } + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType)) + { + return; // a diagnostic about this will have already been reported, and if the cast is implicit then this message would be confusing anyway + } + + var bodyNode = context.CodeBlock.ChildNodes().Single(); + + var operation = bodyNode.DescendantNodes() + .Where(n => n.IsKind(SyntaxKind.InvocationExpression)) // this line is specific to C# + .Select(n => (IInvocationOperation)context.SemanticModel.GetOperation(n)!) + .FirstOrDefault(); + + var isGetMethod = method.MethodKind == MethodKind.PropertyGet; + + var expectedInvocations = isGetMethod ? _getValueMethods : _setValueMethods; + + if (operation == null || bodyNode.ChildNodes().Count() != 1 || !expectedInvocations.Contains(operation.TargetMethod.OriginalDefinition)) + { + ReportSideEffects(); + return; + } + + if (operation.Arguments.Length != 0) + { + switch (ResolveOperationSource(operation.Arguments[0].Value)) + { + case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field): + case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property): + break; // the argument is a reference to the correct AvaloniaProperty object + default: + ReportSideEffects(operation.Arguments[0].Value.Syntax.GetLocation()); + return; + } + } + + if (!isGetMethod && + operation.Arguments.Length >= 2 && + operation.Arguments[1].Value.Kind != OperationKind.ParameterReference) // passing something other than `value` to SetValue + { + ReportSideEffects(operation.Arguments[1].Syntax.GetLocation()); + } + + void ReportSideEffects(Location? locationOverride = null) + { + var propertySourceName = avaloniaPropertyDescription.ClosestAssignmentFor(method.ContainingType)?.Name ?? "[unknown]"; + + context.ReportDiagnostic(Diagnostic.Create(AccessorSideEffects, locationOverride ?? context.CodeBlock.GetLocation(), + avaloniaPropertyDescription.Name, + isGetMethod ? "read" : "written to", + isGetMethod ? "get" : "set", + isGetMethod ? $"GetValue({propertySourceName})" : $"SetValue({propertySourceName}, value)")); + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex); + } + } + } +} diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs new file mode 100644 index 0000000000..d1d9071d17 --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Avalonia.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] +public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer +{ + private const string Category = "AvaloniaProperty"; + + private const string TypeMismatchTag = "TypeMismatch"; + private const string NameCollisionTag = "NameCollision"; + private const string AssociatedClrPropertyTag = "AssociatedClrProperty"; + private const string InappropriateReadWriteTag = "InappropriateReadWrite"; + + private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( + "AVP0001", + "Identification of the AvaloniaProperty associated with a CLR property", + "Associated AvaloniaProperty: {0} {1}", + Category, + DiagnosticSeverity.Info, + isEnabledByDefault: false, + "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( + "AVP1000", + "AvaloniaProperty objects should be stored appropriately", + "Incorrect AvaloniaProperty storage: {0} should be static and readonly", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be stored accordingly."); + + private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new( + "AVP1001", + "The same AvaloniaProperty should not be registered twice", + "Unsafe registration: {0} should be called only in static constructors or static initializers", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be created only once. To ensure this, only call Register or AddOwner in static constructors or static initializers."); + + private static readonly DiagnosticDescriptor PropertyOwnedByGenericType = new( + "AVP1002", + "AvaloniaProperty objects should not be owned by a generic type", + "Inadvisable registration: Generic types cannot be referenced from XAML. Create a non-generic type to be the owner of this AvaloniaProperty.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is sometimes necessary to refer to an AvaloniaProperty in XAML by providing its class name. This cannot be achieved if property's owner is a generic type." + + " Additionally, a new AvaloniaProperty object will be generated each time a new version of the generic owner type is constructed, which may be unexpected."); + + private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( + "AVP1010", + "AvaloniaProperty objects should be owned by the type in which they are stored", + "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", + TypeMismatchTag); + + private static readonly DiagnosticDescriptor UnexpectedPropertyAccess = new( + "AVP1011", + "An AvaloniaObject should own each AvaloniaProperty it reads or writes on itself", + "Unexpected property use: {0} is neither owned by nor attached to {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor SettingOwnStyledPropertyValue = new( + "AVP1012", + "An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", + "Inappropriate assignment: An AvaloniaObject should use SetCurrentValue when setting its own StyledProperty or AttachedProperty values", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The standard means of setting an AvaloniaProperty is to call the SetValue method (often via a CLR property setter). This will forcibly overwrite values from sources like styles and templates, " + + "which is something that should only be done by consumers of the control, not the control itself. Controls which want to set their own values should instead call the SetCurrentValue method, or " + + "refactor the property into a DirectProperty. An assignment is exempt from this diagnostic in two scenarios: when it is forwarding a constructor parameter, and when the target object is derived " + + "from UserControl or TopLevel.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new( + "AVP1013", + "AvaloniaProperty owners should not be added superfluously", + "Superfluous owner: {0} is already an owner of {1} via {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Ownership of an AvaloniaProperty is inherited along the type hierarchy. There is no need for a derived type to assert ownership over a base type's properties. This diagnostic can be a symptom of an incorrect property owner elsewhere.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( + "AVP1020", + "AvaloniaProperty names should be unique within each class", + "Name collision: {0} has the same name as {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Querying for an AvaloniaProperty by name requires that each property associated with a type have a unique name.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AmbiguousPropertyName = new( + "AVP1021", + "There should be an unambiguous relationship between the CLR properties and Avalonia properties of a class", + "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its OverrideMetadata or OverrideDefaultValue methods.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor PropertyNameMismatch = new( + "AVP1022", + "An AvaloniaProperty object should be stored in a field or CLR property which reflects its name", + "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".\nPrivate symbols are exempt from this diagnostic.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AccessorSideEffects = new( + "AVP1030", + "StyledProperty accessors should not have side effects", + "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, consider: 1) adding a Coercion method, b) adding a static observer with AvaloniaProperty.Changed.AddClassHandler, and/or c) overriding the AvaloniaObject.OnPropertyChanged method.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor MissingAccessor = new( + "AVP1031", + "A CLR property should support the same get/set operations as its associated AvaloniaProperty", + "Missing accessor: {0} is {1}, but this CLR property lacks a {2} accessor", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor InconsistentAccessibility = new( + "AVP1032", + "A CLR property and its accessors should be equally accessible as its associated AvaloniaProperty", + "Inconsistent accessibility: CLR {0} accessibility does not match accessibility of {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different accessibility from its associated AvaloniaProperty is ineffective.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( + "AVP1040", + "A CLR property type should match the associated AvaloniaProperty type", + "Type mismatch: CLR property type differs from the value type of {0} {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", + TypeMismatchTag, AssociatedClrPropertyTag); + + private static readonly SymbolDisplayFormat TypeQualifiedName = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + AssociatedAvaloniaProperty, + InappropriatePropertyAssignment, + InappropriatePropertyRegistration, + PropertyOwnedByGenericType, + OwnerDoesNotMatchOuterType, + UnexpectedPropertyAccess, + SettingOwnStyledPropertyValue, + SuperfluousAddOwnerCall, + DuplicatePropertyName, + AmbiguousPropertyName, + PropertyNameMismatch, + AccessorSideEffects, + MissingAccessor, + InconsistentAccessibility, + PropertyTypeMismatch); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName("Avalonia.AvaloniaObject") is { } avaloniaObjectType) + { + new CompileAnalyzer(c, avaloniaObjectType); + } + }); + } + + private static bool IsAvaloniaPropertyType(ITypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); + + private static bool IsAvaloniaPropertyType(ITypeSymbol type, IEnumerable propertyTypes) + { + type = type.OriginalDefinition; + + return propertyTypes.Any(t => SymbolEquals(type, t)); + } + + private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType, bool includeSelf = true) + { + if (baseType != null) + { + if (!includeSelf) + { + type = type?.BaseType; + } + + while (type != null) + { + if (SymbolEquals(type, baseType)) + { + return true; + } + + type = type.BaseType; + } + } + return false; + } + + /// + /// Follows assignments and conversions back to their source. + /// + private static IOperation ResolveOperationSource(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Operand; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Value; + break; + default: + return operation; + } + } + } + + private static IOperation ResolveOperationTarget(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Parent!; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Target; + break; + default: + return operation; + } + } + } + + private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation) => operation == null ? null : ResolveOperationSource(operation) switch + { + IFieldReferenceOperation fieldRef => fieldRef.Field, + IPropertyReferenceOperation propertyRef => propertyRef.Property, + IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value), + _ => null, + }; + + private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; + private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; + + private static bool SymbolEquals(ISymbol? x, ISymbol? y, bool includeNullability = false) + { + // The current version of Microsoft.CodeAnalysis includes an "IncludeNullability" comparer, + // but it overshoots the target and tries to compare EVERYTHING. This leads to two symbols for + // the same type not being equal if they were imported into different compile units (i.e. assemblies). + // So for now, we will just discard this parameter. + _ = includeNullability; + + return SymbolEqualityComparer.Default.Equals(x, y); + } + + private class AvaloniaPropertyDescription + { + /// + /// Gets the name that was assigned to this property when it was registered. + /// + /// + /// If the property was not registered within the current compile context, this value will be inferred from + /// the name of the field (or CLR property) in which the AvaloniaProperty object is stored. + /// + public string Name { get; set; } + + /// + /// Gets the type of the AvaloniaProperty itself: Styled, Direct, or Attached + /// + public INamedTypeSymbol PropertyType { get; } + + /// + /// Gets the TValue type that the property stores. + /// + public ITypeSymbol ValueType { get; } + + /// + /// Gets whether the value of this property is inherited from the parent AvaloniaObject. + /// + public bool Inherits { get; set; } + + /// + /// Gets the type which registered the property, and all types which have added themselves as owners. + /// + public IReadOnlyCollection OwnerTypes { get; private set; } + private ConcurrentBag? _ownerTypes = new(); + + /// + /// Gets the type to which an AttachedProperty is attached, or null if the property is StyledProperty or DirectProperty. + /// + public TypeReference? HostType { get; set; } + + /// + /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. + /// + public IReadOnlyDictionary AssignedTo { get; private set; } + private ConcurrentDictionary? _assignedTo = new(SymbolEqualityComparer.Default); + + /// + /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject. + /// + public IReadOnlyCollection PropertyWrappers { get; private set; } + private ConcurrentBag? _propertyWrappers = new(); + + public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, ITypeSymbol valueType) + { + Name = name; + PropertyType = propertyType; + ValueType = valueType; + + OwnerTypes = _ownerTypes; + PropertyWrappers = _propertyWrappers; + AssignedTo = _assignedTo; + } + + private const string SealedError = "PropertyDescription has been sealed."; + + public void AddOwner(TypeReference owner) => (_ownerTypes ?? throw new InvalidOperationException(SealedError)).Add(owner); + + public void AddPropertyWrapper(IPropertySymbol property) => (_propertyWrappers ?? throw new InvalidOperationException(SealedError)).Add(property); + + public void SetAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError))[assignmentTarget] = ownerType; + + public AvaloniaPropertyDescription Seal() + { + if (_ownerTypes == null || _propertyWrappers == null || _assignedTo == null) + { + return this; + } + + OwnerTypes = _ownerTypes.ToImmutableHashSet(); + _ownerTypes = null; + + PropertyWrappers = _propertyWrappers.ToImmutableHashSet(SymbolEqualityComparer.Default); + _propertyWrappers = null; + + AssignedTo = new ReadOnlyDictionary(_assignedTo); + _assignedTo = null; + + return this; + } + + /// + /// Searches the inheritance hierarchy of the given type for a field or property to which this AvaloniaProperty is assigned. + /// + public ISymbol? ClosestAssignmentFor(ITypeSymbol? type) + { + var assignmentsByType = AssignedTo.Keys.ToLookup(s => s.ContainingType, SymbolEqualityComparer.Default); + + while (type != null) + { + if (assignmentsByType.Contains(type)) + { + return assignmentsByType[type].First(); + } + type = type.BaseType; + } + + return null; + } + } + + private readonly struct TypeReference + { + public ITypeSymbol Type { get; } + public Location Location { get; } + + public TypeReference(ITypeSymbol type, Location location) + { + Type = type; + Location = location; + } + + public static TypeReference FromInvocationTypeParameter(IInvocationOperation invocation, ITypeParameterSymbol typeParameter) + { + var argument = invocation.TargetMethod.TypeArguments[typeParameter.Ordinal]; + + var typeArgumentSyntax = invocation.Syntax; + if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them + { + try + { + typeArgumentSyntax = invocation.Syntax.DescendantNodes() + .First(n => n.IsKind(SyntaxKind.TypeArgumentList)) + .DescendantNodes().ElementAt(typeParameter.Ordinal); + } + catch + { + // ignore, this is just a nicety + } + } + + return new TypeReference(argument, typeArgumentSyntax.GetLocation()); + } + } + + private class SymbolEqualityComparer : IEqualityComparer where T : ISymbol + { + public bool Equals(T x, T y) => SymbolEqualityComparer.Default.Equals(x, y); + public int GetHashCode(T obj) => SymbolEqualityComparer.Default.GetHashCode(obj); + + public static SymbolEqualityComparer Default { get; } = new(); + } +} + +[Serializable] +public class AvaloniaAnalysisException : Exception +{ + public AvaloniaAnalysisException(string message, Exception? innerException = null) : base(message, innerException) + { + } + + protected AvaloniaAnalysisException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +} diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index e83b2d7598..40d504a0ac 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -65,7 +65,7 @@ namespace Avalonia.Benchmarks { } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { } diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index c3d35653cc..d3eb42f147 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -237,6 +237,40 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Reducing_Extent_Should_Constrain_Offset() + { + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + }; + var root = new TestRoot(target); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(50, 50); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(-30, -30), e.ExtentDelta); + Assert.Equal(new Vector(-30, -30), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); + + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(1, raised); + Assert.Equal(new Vector(20, 20), target.Offset); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index f1dd874c71..55c43f6f96 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -278,6 +279,82 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [Fact] + public void Removing_Range_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 80); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 95); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -378,5 +455,24 @@ namespace Avalonia.Controls.UnitTests } private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus); + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(IEnumerable items) + { + AddRange(items); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + } } } diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs index 14f5b7c6c7..1199184d14 100644 --- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs @@ -141,7 +141,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls new Rect(control.Bounds.Size), 4); - using (context.PushOpacity(0.5)) + using (context.PushOpacity(0.5, control.Bounds)) { context.FillRectangle( Brushes.Blue, diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index c63a876d81..05e160dca8 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -79,7 +79,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media using (var ctx = target.CreateDrawingContext(null)) { ctx.Clear(Colors.Transparent); - ctx.PushOpacity(0.8); + ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); ctx.DrawRectangle(Brushes.Chartreuse, null, new Rect(0, 0, 20, 100)); ctx.DrawRectangle(Brushes.Crimson, null, new Rect(20, 0, 20, 100)); ctx.DrawRectangle(Brushes.Gold,null, new Rect(40, 0, 20, 100));