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; } } 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.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/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}" /> + + + + diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 78f6d10063..ab59882962 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -97,7 +97,7 @@ namespace Avalonia.X11 _popup = popupParent != null; _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; - _mouse = new MouseDevice(); + _mouse = Avalonia.Input.MouseDevice.Primary; _pen = new PenDevice(); _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; @@ -1074,7 +1074,7 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); var handle = _handle; _handle = IntPtr.Zero; - _mouse.Dispose(); + _pen.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 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); +} 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); + } }