From 1a938de66d476bf6fd2bbdee9892bfaf5f3af1a6 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:03:07 +0100 Subject: [PATCH 1/6] Add file locations to diagnostics reported by AvaloniaNameIncrementalGenerator (#19934) Add a pure XML parsing fallback which allows InitializeContext to be generated even if XAML parsing failed Improve diagnostic messages and make user errors warnings, so that the compile can continue and fail "properly" later --- .../Common/EquatableList.cs | 2 + .../GeneratorExtensions.cs | 28 ---- .../AvaloniaNameIncrementalGenerator.cs | 144 +++++++++++++----- .../NameGenerator/NameGeneratorDiagnostics.cs | 39 +++++ 4 files changed, 146 insertions(+), 67 deletions(-) create mode 100644 src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs diff --git a/src/tools/Avalonia.Generators/Common/EquatableList.cs b/src/tools/Avalonia.Generators/Common/EquatableList.cs index 2b4c8a184d..fd65c668f0 100644 --- a/src/tools/Avalonia.Generators/Common/EquatableList.cs +++ b/src/tools/Avalonia.Generators/Common/EquatableList.cs @@ -9,6 +9,8 @@ namespace Avalonia.Generators.Common; internal class EquatableList(IList collection) : ReadOnlyCollection(collection), IEquatable> { + public static readonly EquatableList Empty = new([]); + public bool Equals(EquatableList? other) { // If the other list is null or a different size, they're not equal diff --git a/src/tools/Avalonia.Generators/GeneratorExtensions.cs b/src/tools/Avalonia.Generators/GeneratorExtensions.cs index 9553dddc46..8911ca2b20 100644 --- a/src/tools/Avalonia.Generators/GeneratorExtensions.cs +++ b/src/tools/Avalonia.Generators/GeneratorExtensions.cs @@ -1,14 +1,9 @@ -using System; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace Avalonia.Generators; internal static class GeneratorExtensions { - private const string UnhandledErrorDescriptorId = "AXN0002"; - private const string InvalidTypeDescriptorId = "AXN0001"; - public static string GetMsBuildProperty( this AnalyzerConfigOptions options, string name, @@ -17,27 +12,4 @@ internal static class GeneratorExtensions options.TryGetValue($"build_property.{name}", out var value); return value ?? defaultValue; } - - public static DiagnosticDescriptor NameGeneratorUnhandledError(Exception error) => new( - UnhandledErrorDescriptorId, - title: "Unhandled exception occurred while generating typed Name references. " + - "Please file an issue: https://github.com/avaloniaui/Avalonia", - messageFormat: error.Message, - description: error.ToString(), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static DiagnosticDescriptor NameGeneratorInvalidType(string typeName) => new( - InvalidTypeDescriptorId, - title: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + - $"The type '{typeName}' does not exist in the assembly.", - messageFormat: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + - $"The type '{typeName}' does not exist in the assembly.", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static void Report(this SourceProductionContext context, DiagnosticDescriptor diagnostics) => - context.ReportDiagnostic(Diagnostic.Create(diagnostics, Location.None)); } diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs index ba0d0d7579..a1cec53ed9 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; +using System.Xml; +using System.Xml.Linq; using Avalonia.Generators.Common; using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using Microsoft.CodeAnalysis; -using XamlX.Transform; +using Microsoft.CodeAnalysis.Text; +using XamlX; namespace Avalonia.Generators.NameGenerator; @@ -62,39 +64,52 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator .Select(static (file, cancellationToken) => { cancellationToken.ThrowIfCancellationRequested(); - var text = file.GetText(cancellationToken); - var diagnostics = new List(); - if (text is not null) + var xaml = file.GetText(cancellationToken)?.ToString(); + if (xaml is null) { - try - { - var xaml = text.ToString(); - var viewResolver = new XamlXViewResolver(s_noopCompiler); - var view = viewResolver.ResolveView(xaml, cancellationToken); - if (view is null) - { - return null; - } - - var nameResolver = new XamlXNameResolver(); - var xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken); + return null; + } - return new XmlClassInfo( - new ResolvedXmlView(view, xmlNames), - new EquatableList(diagnostics)); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) + ResolvedXmlView? resolvedXmlView; + DiagnosticFactory? diagnosticFactory = null; + var location = new FileLinePositionSpan(file.Path, default); + try + { + var viewResolver = new XamlXViewResolver(s_noopCompiler); + var view = viewResolver.ResolveView(xaml, cancellationToken); + if (view is null) { - diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); - return new XmlClassInfo(null, new EquatableList(diagnostics)); + return null; } + + var xmlNames = EquatableList.Empty; + var nameResolver = new XamlXNameResolver(); + xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken); + + resolvedXmlView = new ResolvedXmlView(view, xmlNames); + } + catch (OperationCanceledException) + { + throw; + } + catch (XmlException ex) + { + diagnosticFactory = new(NameGeneratorDiagnostics.ParseFailed, new(file.Path, GetLinePositionSpan(ex)), new([ex.Message])); + + resolvedXmlView = ex is XamlParseException ? TryExtractTypeFromXml(xaml) : null; + } + catch (XamlTypeSystemException ex) + { + diagnosticFactory = new(NameGeneratorDiagnostics.ParseFailed, location, new([ex.Message])); + resolvedXmlView = TryExtractTypeFromXml(xaml); + } + catch (Exception ex) + { + diagnosticFactory = GetInternalErrorDiagnostic(location, ex); + resolvedXmlView = null; } - return null; + return new XmlClassInfo(file.Path, resolvedXmlView, diagnosticFactory); }) .Where(request => request is not null) .WithTrackingName(TrackingNames.ParsedXamlClasses); @@ -119,15 +134,20 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator var hasDevToolsReference = compiler.TypeSystem.FindAssembly("Avalonia.Diagnostics") is not null; var nameResolver = new XamlXNameResolver(); - var diagnostics = new List(classInfo!.Diagnostics); + var diagnostics = new List(2); + if (classInfo?.Diagnostic != null) + { + diagnostics.Add(classInfo.Diagnostic); + } + ResolvedView? view = null; - if (classInfo.XmlView is { } xmlView) + if (classInfo?.XmlView is { } xmlView) { var type = compiler.TypeSystem.FindType(xmlView.FullName); if (type is null) { - diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(xmlView.FullName)); + diagnostics.Add(new(NameGeneratorDiagnostics.InvalidType, new(classInfo.FilePath, default), new([xmlView.FullName]))); } else if (type.IsAvaloniaStyledElement()) { @@ -147,17 +167,22 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator resolvedNames.Add(nameResolver .ResolveName(clrType, xmlName.Name, xmlName.FieldModifier)); } + catch (XmlException ex) + { + diagnostics.Add(new(NameGeneratorDiagnostics.NamedElementFailed, + new(classInfo.FilePath, GetLinePositionSpan(ex)), new([xmlName.Name, ex.Message]))); + } catch (Exception ex) { - diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); + diagnostics.Add(GetInternalErrorDiagnostic(new(classInfo.FilePath, default), ex)); } } - view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new EquatableList(resolvedNames)); + view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new(resolvedNames)); } } - return new ResolvedClassInfo(view, hasDevToolsReference, new EquatableList(diagnostics)); + return new ResolvedClassInfo(view, hasDevToolsReference, new(diagnostics)); }) .WithTrackingName(TrackingNames.ResolvedNamesProvider); @@ -165,9 +190,9 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator { var (info, options) = pair; - foreach (var diagnostic in info!.Diagnostics) + foreach (var diagnostic in info.Diagnostics) { - context.Report(diagnostic); + context.ReportDiagnostic(diagnostic.Create()); } if (info.View is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace)) @@ -200,12 +225,53 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator }); } + private static DiagnosticFactory GetInternalErrorDiagnostic(FileLinePositionSpan location, Exception ex) => + new(NameGeneratorDiagnostics.InternalError, location, new([ex.ToString().Replace('\n', '*').Replace('\r', '*')])); + + /// + /// Fallback in case XAML parsing fails. Extracts just the class name and namespace of the root element. + /// + private static ResolvedXmlView? TryExtractTypeFromXml(string xaml) + { + try + { + var document = XDocument.Parse(xaml); + var classValue = document.Root.Attribute(XName.Get("Class", XamlNamespaces.Xaml2006))?.Value; + if (classValue?.LastIndexOf('.') is { } lastDotIndex && lastDotIndex != -1) + { + return new(classValue.Substring(lastDotIndex + 1), classValue.Substring(0, lastDotIndex), EquatableList.Empty); + } + } + catch + { + // ignore + } + return null; + } + + private static LinePositionSpan GetLinePositionSpan(XmlException ex) + { + var position = new LinePosition(Math.Max(0, ex.LineNumber - 1), Math.Max(0, ex.LinePosition - 1)); + return new(position, position); + } + internal record XmlClassInfo( + string FilePath, ResolvedXmlView? XmlView, - EquatableList Diagnostics); + DiagnosticFactory? Diagnostic); internal record ResolvedClassInfo( ResolvedView? View, bool CanAttachDevTools, - EquatableList Diagnostics); + EquatableList Diagnostics); + + /// + /// Avoid holding references to because it can hold references to , , etc. + /// + internal record DiagnosticFactory(DiagnosticDescriptor Descriptor, FileLinePositionSpan LinePosition, EquatableList FormatArguments) + { + public Diagnostic Create() => Diagnostic.Create(Descriptor, + Location.Create(LinePosition.Path, default, new(LinePosition.StartLinePosition, LinePosition.EndLinePosition)), + messageArgs: [.. FormatArguments]); + } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs b/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs new file mode 100644 index 0000000000..53c1a43855 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal static class NameGeneratorDiagnostics +{ + private const string Category = "Avalonia.NameGenerator"; + private const string BugReportLink = "https://github.com/AvaloniaUI/Avalonia/issues/new/choose"; + + // Name generation errors should typicially be warnings, because that allows the compile to proceed and + // reach the point at which code errors are reported. These can give the user actionable information + // about what they need to fix, which the name generator doesn't have. + + public static readonly DiagnosticDescriptor InvalidType = new( + "AXN0001", $"Invalid type", + "Avalonia could not generate code-behind properties or the InitializeContext method because the x:Class type '{0}' was not found in the project", + Category, + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + [SuppressMessage("MicrosoftCodeAnalysisDesign", "RS1032:Define diagnostic message correctly", Justification = "Printing internal exception")] + public static readonly DiagnosticDescriptor InternalError = new( + "AXN0002", "Internal error", + messageFormat: $"Avalonia encountered an internal error while generating code-behind properties and/or the InitializeContext method. " + + $"Please file a bug report at {BugReportLink}. The exception is {{0}}", + Category, + DiagnosticSeverity.Error, true, + helpLinkUri: BugReportLink); + + public static readonly DiagnosticDescriptor ParseFailed = new( + "AXN0003", $"XAML error", + "Avalonia could not generate code-behind properties for named elements due to a XAML error: {0}", + Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor NamedElementFailed = new( + "AXN0004", $"XAML error", + "Avalonia could not generate code-behind property for '{0}' due to a XAML error: {1}", + Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); +} From c530f7b2089fb78486e4ef8ce4c24ab62550ce0b Mon Sep 17 00:00:00 2001 From: Matt Lacey Date: Mon, 3 Nov 2025 15:44:45 +0000 Subject: [PATCH 2/6] Don't let the PreviewerWindow grow without limit (#19957) --- src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 61924f53ab..7733bcd4ff 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -64,6 +64,12 @@ namespace Avalonia.DesignerSupport.Remote public void Resize(Size clientSize, WindowResizeReason reason) { + // Don't let it clientSize be unconstrained or risk running Out Of Memory + clientSize = new Size( + Math.Min(clientSize.Width, MaxAutoSizeHint.Width), + Math.Min(clientSize.Height, MaxAutoSizeHint.Height) + ); + _transport.Send(new RequestViewportResizeMessage { Width = Math.Ceiling(clientSize.Width * RenderScaling), From e294245f011ce8ceb2cb3c2e7aa1a90a48e18a30 Mon Sep 17 00:00:00 2001 From: startewho Date: Tue, 4 Nov 2025 16:44:43 +0800 Subject: [PATCH 3/6] Handle long file paths in UriFromFilePath (#19955) * fix long path * Update UriExtensionsTests.cs * Update StorageProviderHelpers.cs --- .../Storage/FileIO/StorageProviderHelpers.cs | 18 +++++++++++++----- .../Utilities/UriExtensionsTests.cs | 11 +++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 989ec23a29..16ac8e87d9 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -37,11 +37,19 @@ internal static class StorageProviderHelpers public static Uri UriFromFilePath(string path, bool isDirectory) { - var uriPath = new StringBuilder(path) - .Replace("%", $"%{(int)'%':X2}") - .Replace("[", $"%{(int)'[':X2}") - .Replace("]", $"%{(int)']':X2}"); - + var uriPath = new StringBuilder(); + bool isLongPath = path.StartsWith(@"\\?\", StringComparison.Ordinal);//Windows long path prefix + if (isLongPath) + { + uriPath.Append(path, 4, path.Length - 4); + } + else + { + uriPath.Append(path); + } + uriPath = uriPath.Replace("%", $"%{(int)'%':X2}") + .Replace("[", $"%{(int)'[':X2}") + .Replace("]", $"%{(int)']':X2}"); if (!path.EndsWith('/') && isDirectory) { uriPath.Append('/'); diff --git a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs index 83380bf6a0..e74851ef09 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs @@ -40,4 +40,15 @@ public class UriExtensionsTests Assert.Equal(path, uri.LocalPath); } + + [Theory] + + [InlineData(@"\\?\D:\abcdefgh\abcdefgh\abcdefabcdefgh\abcdefghabcdefghabcdefgha\bcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh\abcdefghabcdefghabcdefgha\bcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + @"D:\abcdefgh\abcdefgh\abcdefabcdefgh\abcdefghabcdefghabcdefgha\bcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh\abcdefghabcdefghabcdefgha\bcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh")] + public void Should_Convert_Long_File_Path_To_Uri_And_Back(string prepath,string path) + { + var uri = StorageProviderHelpers.UriFromFilePath(prepath, false); + + Assert.Equal(path, uri.LocalPath); + } } From 4d3a28114ea0fa0d4998915dd2767c7e94a48e9a Mon Sep 17 00:00:00 2001 From: Bobby Cannon Date: Tue, 4 Nov 2025 05:27:25 -0500 Subject: [PATCH 4/6] fixed (#19985) --- .../Automation/ToggleNodeInfoProvider.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs b/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs index 569714c88a..907bd2e3a0 100644 --- a/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs +++ b/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs @@ -1,4 +1,5 @@ -using Android.OS; +using System.Reflection; +using Android.OS; using AndroidX.Core.View.Accessibility; using AndroidX.CustomView.Widget; using Avalonia.Automation.Peers; @@ -8,6 +9,8 @@ namespace Avalonia.Android.Automation { internal class ToggleNodeInfoProvider : NodeInfoProvider { + private PropertyInfo? _checkedProperty; + public ToggleNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : base(owner, peer, virtualViewId) { @@ -32,7 +35,25 @@ namespace Avalonia.Android.Automation nodeInfo.Clickable = true; IToggleProvider provider = GetProvider(); - nodeInfo.Checked = provider.ToggleState == ToggleState.On; + + _checkedProperty ??= nodeInfo.GetType().GetProperty(nameof(nodeInfo.Checked)); + if (_checkedProperty?.PropertyType == typeof(int)) + { + // Needed for Xamarin.AndroidX.Core 1.17+ + _checkedProperty.SetValue(this, + provider.ToggleState switch + { + ToggleState.On => 1, + ToggleState.Indeterminate => 2, + _ => 0 + }); + } + else if (_checkedProperty?.PropertyType == typeof(bool)) + { + // Needed for Xamarin.AndroidX.Core < 1.17 + _checkedProperty.SetValue(this, provider.ToggleState == ToggleState.On); + } + nodeInfo.Checkable = true; } } From 99db0e1eca160a35e2cc24f8bf0f6e5cdda29b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:37:29 +0000 Subject: [PATCH 5/6] Primary mouse device (#19898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Primary mouse device * Addressing feedback --------- Co-authored-by: Jan Kučera Co-authored-by: Julien Lebosquain --- src/Avalonia.Base/Input/MouseDevice.cs | 15 +++++++++ src/Avalonia.Native/TopLevelImpl.cs | 4 +-- src/Avalonia.X11/X11Window.cs | 3 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 31 ++++++++++++------- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 ++-- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 49945f1e8a..5a0cdad755 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -18,6 +18,9 @@ namespace Avalonia.Input [PrivateApi] public class MouseDevice : IMouseDevice, IDisposable { + private static MouseDevice? _primary; + internal static MouseDevice Primary => _primary ??= new MouseDevice(); + private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; @@ -31,6 +34,16 @@ namespace Avalonia.Input _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } + internal static TMouseDevice GetOrCreatePrimary() where TMouseDevice : MouseDevice, new() + { + if (_primary is TMouseDevice device) + return device; + + device = new TMouseDevice(); + _primary = device; + return device; + } + public void ProcessRawEvent(RawInputEventArgs e) { if (!e.Handled && e is RawPointerEventArgs margs) @@ -300,6 +313,8 @@ namespace Avalonia.Input public void Dispose() { + System.Diagnostics.Debug.Assert(this != _primary, "Disposing primary mouse device."); + _disposed = true; _pointer?.Dispose(); } diff --git a/src/Avalonia.Native/TopLevelImpl.cs b/src/Avalonia.Native/TopLevelImpl.cs index 52ad0bc30f..05cbb15765 100644 --- a/src/Avalonia.Native/TopLevelImpl.cs +++ b/src/Avalonia.Native/TopLevelImpl.cs @@ -89,7 +89,7 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface Factory = factory; _keyboard = AvaloniaLocator.Current.GetService(); - _mouse = new MouseDevice(); + _mouse = Avalonia.Input.MouseDevice.Primary; _pen = new PenDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); } @@ -387,8 +387,6 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface _nativeControlHost?.Dispose(); _nativeControlHost = null; - - _mouse?.Dispose(); } protected virtual bool ChromeHitTest(RawPointerEventArgs e) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index eee61a707f..2f2542a150 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -96,7 +96,7 @@ namespace Avalonia.X11 _popup = popupParent != null; _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; - _mouse = new MouseDevice(); + _mouse = Avalonia.Input.MouseDevice.Primary; _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; @@ -1072,7 +1072,6 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); var handle = _handle; _handle = IntPtr.Zero; - _mouse.Dispose(); _touch.Dispose(); if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0f79a26ece..662f07623e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -114,9 +114,9 @@ namespace Avalonia.Win32 //Window doesn't exist anymore _hwnd = IntPtr.Zero; //Remove root reference to this class, so unmanaged delegate can be collected - s_instances.Remove(this); + lock (s_instances) + s_instances.Remove(this); - _mouseDevice.Dispose(); _touchDevice.Dispose(); //Free other resources Dispose(); @@ -280,14 +280,6 @@ namespace Avalonia.Win32 DipFromLParam(lParam), GetMouseModifiers(wParam)); break; } - // Mouse capture is lost - case WindowsMessage.WM_CANCELMODE: - if (!IsMouseInPointerEnabled) - { - _mouseDevice.Capture(null); - } - - break; case WindowsMessage.WM_MOUSEMOVE: { @@ -394,13 +386,14 @@ namespace Avalonia.Win32 break; } + // covers WM_CANCELMODE which sends WM_CAPTURECHANGED in DefWindowProc case WindowsMessage.WM_CAPTURECHANGED: { if (IsMouseInPointerEnabled) { break; } - if (_hwnd != lParam) + if (!IsOurWindow(lParam)) { _trackingMouse = false; e = new RawPointerEventArgs( @@ -907,6 +900,22 @@ namespace Avalonia.Win32 return DefWindowProc(hWnd, msg, wParam, lParam); } + private bool IsOurWindow(IntPtr hwnd) + { + if (hwnd == IntPtr.Zero) + return false; + + if (hwnd == _hwnd) + return true; + + lock (s_instances) + for (int i = 0; i < s_instances.Count; i++) + if (s_instances[i]._hwnd == hwnd) + return true; + + return false; + } + private void OnShowHideMessage(bool shown) { _shown = shown; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6dc1b3cebc..323539106a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Win32 public WindowImpl() { _touchDevice = new TouchDevice(); - _mouseDevice = new WindowsMouseDevice(); + _mouseDevice = Avalonia.Input.MouseDevice.GetOrCreatePrimary(); _penDevice = new PenDevice(); #if USE_MANAGED_DRAG @@ -177,7 +177,9 @@ namespace Avalonia.Win32 _nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap); _defaultTransparencyLevel = UseRedirectionBitmap ? WindowTransparencyLevel.None : WindowTransparencyLevel.Transparent; _transparencyLevel = _defaultTransparencyLevel; - s_instances.Add(this); + + lock (s_instances) + s_instances.Add(this); } internal IInputRoot Owner From 7f69ff1a936cd7c5b7d12d77e3eb3babdbe24582 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Nov 2025 16:03:40 +0100 Subject: [PATCH 6/6] Make ContentPresenter.RecognizesAccessKey stylable (#19893) * Make HyperlinkButton's ContentPresenter.RecognizesAccessKey stylable * Adjust all controls that are using RecognizesAccessKey --- .../Themes/Fluent/ColorView.xaml | 5 ++++- .../Themes/Simple/ColorView.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/Button.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml | 5 ++++- .../Controls/DropDownButton.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml | 11 +++++++++-- .../Controls/HeaderedContentControl.xaml | 11 +++++++++-- .../Controls/HyperlinkButton.xaml | 7 +++++-- src/Avalonia.Themes.Fluent/Controls/Label.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml | 7 +++++-- src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/TabItem.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml | 7 +++++-- src/Avalonia.Themes.Simple/Controls/Button.xaml | 6 +++++- src/Avalonia.Themes.Simple/Controls/CheckBox.xaml | 6 +++++- .../Controls/DropDownButton.xaml | 7 +++++-- src/Avalonia.Themes.Simple/Controls/GroupBox.xaml | 10 ++++++++-- .../Controls/HeaderedContentControl.xaml | 10 ++++++++-- .../Controls/HyperlinkButton.xaml | 5 ++++- src/Avalonia.Themes.Simple/Controls/Label.xaml | 7 +++++-- src/Avalonia.Themes.Simple/Controls/RadioButton.xaml | 7 +++++-- src/Avalonia.Themes.Simple/Controls/SplitButton.xaml | 5 ++++- src/Avalonia.Themes.Simple/Controls/TabItem.xaml | 6 +++++- src/Avalonia.Themes.Simple/Controls/ToggleButton.xaml | 6 +++++- src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml | 5 ++++- 28 files changed, 140 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index e4a8ecace4..ad517e8e80 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -185,12 +185,15 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" - RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/HeaderedContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/HeaderedContentControl.xaml index d2ccf2b0c1..4474ac07ab 100644 --- a/src/Avalonia.Themes.Fluent/Controls/HeaderedContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/HeaderedContentControl.xaml @@ -15,7 +15,6 @@ @@ -31,5 +29,14 @@ + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/HyperlinkButton.xaml b/src/Avalonia.Themes.Fluent/Controls/HyperlinkButton.xaml index a4fc791744..8b390a988a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/HyperlinkButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/HyperlinkButton.xaml @@ -5,7 +5,7 @@ - + @@ -42,12 +42,15 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" - RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index 2d326c36f4..a0b0ce762f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -31,7 +31,6 @@ ContentTemplate="{TemplateBinding HeaderTemplate}" VerticalAlignment="Center" HorizontalAlignment="Stretch" - RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"/> + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml index 75be504e59..07ab1d3950 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml @@ -71,13 +71,16 @@ VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" - Foreground="{TemplateBinding Foreground}" - RecognizesAccessKey="True" /> + Foreground="{TemplateBinding Foreground}" /> + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/Button.xaml b/src/Avalonia.Themes.Simple/Controls/Button.xaml index 5599e49ae1..e99884221f 100644 --- a/src/Avalonia.Themes.Simple/Controls/Button.xaml +++ b/src/Avalonia.Themes.Simple/Controls/Button.xaml @@ -22,10 +22,14 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" CornerRadius="{TemplateBinding CornerRadius}" - RecognizesAccessKey="True" TextElement.Foreground="{TemplateBinding Foreground}" /> + + + diff --git a/src/Avalonia.Themes.Simple/Controls/CheckBox.xaml b/src/Avalonia.Themes.Simple/Controls/CheckBox.xaml index 68e969c140..47c03bcce5 100644 --- a/src/Avalonia.Themes.Simple/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CheckBox.xaml @@ -49,11 +49,15 @@ ContentTemplate="{TemplateBinding ContentTemplate}" IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}" - RecognizesAccessKey="True" TextElement.Foreground="{TemplateBinding Foreground}" /> + + + diff --git a/src/Avalonia.Themes.Simple/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Simple/Controls/DropDownButton.xaml index 6d79d420f3..e58dd07812 100644 --- a/src/Avalonia.Themes.Simple/Controls/DropDownButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DropDownButton.xaml @@ -31,8 +31,7 @@ HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Content}" - ContentTemplate="{TemplateBinding ContentTemplate}" - RecognizesAccessKey="True" /> + ContentTemplate="{TemplateBinding ContentTemplate}" /> + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/HeaderedContentControl.xaml b/src/Avalonia.Themes.Simple/Controls/HeaderedContentControl.xaml index 5b306a831d..af4171f66c 100644 --- a/src/Avalonia.Themes.Simple/Controls/HeaderedContentControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/HeaderedContentControl.xaml @@ -15,7 +15,6 @@ + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/HyperlinkButton.xaml b/src/Avalonia.Themes.Simple/Controls/HyperlinkButton.xaml index ef344d6696..7239d19474 100644 --- a/src/Avalonia.Themes.Simple/Controls/HyperlinkButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/HyperlinkButton.xaml @@ -33,12 +33,15 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" - RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> + + diff --git a/src/Avalonia.Themes.Simple/Controls/Label.xaml b/src/Avalonia.Themes.Simple/Controls/Label.xaml index 2c9bb15742..23e0db8467 100644 --- a/src/Avalonia.Themes.Simple/Controls/Label.xaml +++ b/src/Avalonia.Themes.Simple/Controls/Label.xaml @@ -15,9 +15,12 @@ BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" - CornerRadius="{TemplateBinding CornerRadius}" - RecognizesAccessKey="True" /> + CornerRadius="{TemplateBinding CornerRadius}" /> + + diff --git a/src/Avalonia.Themes.Simple/Controls/RadioButton.xaml b/src/Avalonia.Themes.Simple/Controls/RadioButton.xaml index ea138d8806..0cfbe6ca57 100644 --- a/src/Avalonia.Themes.Simple/Controls/RadioButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RadioButton.xaml @@ -36,12 +36,15 @@ Margin="4,0,0,0" VerticalAlignment="Center" Content="{TemplateBinding Content}" - ContentTemplate="{TemplateBinding ContentTemplate}" - RecognizesAccessKey="True" /> + ContentTemplate="{TemplateBinding ContentTemplate}" /> + + diff --git a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml index 7562c9151e..ba99e8bfb2 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml @@ -38,12 +38,15 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" - RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> + + + diff --git a/src/Avalonia.Themes.Simple/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Simple/Controls/ToggleButton.xaml index ad05f15f84..b908df5f4f 100644 --- a/src/Avalonia.Themes.Simple/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ToggleButton.xaml @@ -22,10 +22,14 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" CornerRadius="{TemplateBinding CornerRadius}" - RecognizesAccessKey="True" TextElement.Foreground="{TemplateBinding Foreground}" /> + + + +