diff --git a/Documentation/build.md b/Documentation/build.md index a7d68eb599..9f5436e68e 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on ``` git clone https://github.com/AvaloniaUI/Avalonia.git +cd Avalonia git submodule update --init ``` diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b2663f3213..c2d20495ef 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -32,8 +32,8 @@ namespace Avalonia.Controls /// public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private protected readonly IList _inner; - private INotifyCollectionChanged? _notifyCollectionChanged; + private IList? _inner; + private NotifyCollectionChangedEventHandler? _collectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -42,27 +42,22 @@ namespace Avalonia.Controls public ItemsSourceView(IEnumerable source) { source = source ?? throw new ArgumentNullException(nameof(source)); - - if (source is IList list) - { - _inner = list; - } - else if (source is IEnumerable objectEnumerable) + _inner = source switch { - _inner = new List(objectEnumerable); - } - else - { - _inner = new List(source.Cast()); - } - - ListenToCollectionChanges(); + ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged _ => throw new ArgumentException( + "Collection implements INotifyCollectionChanged by not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + _ => new List(source.Cast()) + }; } /// /// Gets the number of items in the collection. /// - public int Count => _inner.Count; + public int Count => Inner.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -72,6 +67,19 @@ namespace Avalonia.Controls /// public bool HasKeyIndexMapping => false; + /// + /// Gets the inner collection. + /// + public IList Inner + { + get + { + if (_inner is null) + ThrowDisposed(); + return _inner!; + } + } + /// /// Retrieves the item at the specified index. /// @@ -82,15 +90,38 @@ namespace Avalonia.Controls /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add + { + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + } + + _collectionChanged += value; + } + + remove + { + _collectionChanged -= value; + + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnCollectionChanged; + } + } + } /// public void Dispose() { - if (_notifyCollectionChanged != null) + if (_inner is INotifyCollectionChanged incc) { - _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + incc.CollectionChanged -= OnCollectionChanged; } + + _inner = null; } /// @@ -98,9 +129,9 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public object? GetAt(int index) => _inner[index]; + public object? GetAt(int index) => Inner[index]; - public int IndexOf(object? item) => _inner.IndexOf(item); + public int IndexOf(object? item) => Inner.IndexOf(item); public static ItemsSourceView GetOrCreate(IEnumerable? items) { @@ -146,7 +177,7 @@ namespace Avalonia.Controls internal void AddListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, listener); } @@ -154,7 +185,7 @@ namespace Avalonia.Controls internal void RemoveListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, listener); } @@ -162,22 +193,15 @@ namespace Avalonia.Controls protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { - CollectionChanged?.Invoke(this, args); - } - - private void ListenToCollectionChanges() - { - if (_inner is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - _notifyCollectionChanged = incc; - } + _collectionChanged?.Invoke(this, args); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnItemsSourceChanged(e); } + + private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView)); } public class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -216,10 +240,10 @@ namespace Avalonia.Controls /// The index. /// The item. [return: MaybeNull] - public new T GetAt(int index) => (T)_inner[index]; + public new T GetAt(int index) => (T)Inner[index]; - public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public IEnumerator GetEnumerator() => Inner.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); public static new ItemsSourceView GetOrCreate(IEnumerable? items) { diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 726d2c596c..e361e7b736 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform return; } - if (item.HasSubMenu) + if (item.HasSubMenu && item.IsEffectivelyEnabled) { Open(item, true); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index ea06c33e4d..73d867bf10 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -160,13 +160,19 @@ namespace Avalonia.Diagnostics.Views return; } + var root = Root; + if (root is null) + { + return; + } + switch (e.Modifiers) { case RawInputModifiers.Control | RawInputModifiers.Shift: { IControl? control = null; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { control = GetHoveredControl(popupRoot); @@ -176,7 +182,7 @@ namespace Avalonia.Diagnostics.Views } } - control ??= GetHoveredControl(Root); + control ??= GetHoveredControl(root); if (control != null) { @@ -190,7 +196,7 @@ namespace Avalonia.Diagnostics.Views { vm.FreezePopups = !vm.FreezePopups; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { if (popupRoot.Parent is Popup popup) { diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 55e30396e1..5d7619d184 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -30,13 +30,13 @@ namespace Avalonia.Dialogs } else { - using (Process process = Process.Start(new ProcessStartInfo + using Process process = Process.Start(new ProcessStartInfo { FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open", Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "", CreateNoWindow = true, UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - })); + }); } } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index f9f38ac474..9058c405a3 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs @@ -1,13 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml; namespace Avalonia.Dialogs { @@ -35,7 +33,9 @@ namespace Avalonia.Dialogs if (_quickLinksRoot != null) { var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control); +#pragma warning disable CS0618 // Type or member is obsolete if (e.ClickCount == 2 || isQuickLink) +#pragma warning restore CS0618 // Type or member is obsolete { if (model.ItemType == ManagedFileChooserItemType.File) { diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index 050d618ce1..a217a67bc6 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -67,7 +67,7 @@ namespace Avalonia.Dialogs { Directory.GetFiles(x.VolumePath); } - catch (Exception _) + catch (Exception) { return null; } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index f2cc9e9072..8d74001309 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -81,17 +81,21 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (IVisual)ev.Source; - if (e.ClickCount <= 1) +#pragma warning disable CS0618 // Type or member is obsolete + var clickCount = e.ClickCount; +#pragma warning restore CS0618 // Type or member is obsolete + if (clickCount <= 1) { s_lastPress = new WeakReference(ev.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } + } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf..401c6cb2ac 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -75,7 +75,9 @@ namespace Avalonia.Input throw new InvalidOperationException("Control is not attached to visual tree."); } +#pragma warning disable CS0618 // Type or member is obsolete var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index a843be4c4b..ddffbe7500 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -200,7 +200,7 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - if (field != null && field.Equals(value)) + if (EqualityComparer.Default.Equals(field, value)) { return; } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 85feb06c44..52427c4ae6 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -289,11 +289,14 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (clipToBounds - ? visual is IVisualWithRoundRectClip roundClipVisual + using (clipToBounds +#pragma warning disable CS0618 // Type or member is obsolete + ? visual is IVisualWithRoundRectClip roundClipVisual ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) : context.PushClip(bounds) : default(DrawingContext.PushedState)) +#pragma warning restore CS0618 // Type or member is obsolete + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs index 15e5660671..b7311936d3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs @@ -17,7 +17,12 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { - return Custom.HitTest(p * Transform); + if (Transform.HasInverse) + { + return Custom.HitTest(p * Transform.Invert()); + } + + return false; } public override void Render(IDrawingContextImpl context) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index c6cdf474bb..b9131c26f4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -164,10 +164,12 @@ namespace Avalonia.Rendering.SceneGraph var visual = node.Visual; var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; +#pragma warning disable CS0618 // Type or member is obsolete var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? roundRectClip.ClipToBoundsRadius : default; - +#pragma warning restore CS0618 // Type or member is obsolete + var bounds = new Rect(visual.Bounds.Size); var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 79c4202be4..810530066f 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -175,7 +175,7 @@ namespace Avalonia MathUtilities.AreClose(_y, other._y); } - public override bool Equals(object obj) => obj is Vector other && Equals(other); + public override bool Equals(object? obj) => obj is Vector other && Equals(other); public override int GetHashCode() { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index a191dc59fb..1ca7be67a7 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -14,7 +14,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class AvaloniaXamlIlCompiler : XamlILCompiler { - private readonly TransformerConfiguration _configuration; private readonly IXamlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; @@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) : base(configuration, emitMappings, true) { - _configuration = configuration; - void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index 250eca1852..bf11a02fee 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -184,6 +184,9 @@ namespace Avalonia.Markup.Parsers } + // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the + // only reason they have overridden Equals methods is for unit testing. +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() public class PropertySyntax : ISyntax { public string Name { get; set; } = string.Empty; @@ -205,7 +208,7 @@ namespace Avalonia.Markup.Parsers && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; } - + public class ChildTraversalSyntax : ISyntax { public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); @@ -231,5 +234,6 @@ namespace Avalonia.Markup.Parsers && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; } +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs new file mode 100644 index 0000000000..529b3b1aa8 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Collections; +using Avalonia.Diagnostics; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsSourceViewTests + { + [Fact] + public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed() + { + var source = new AvaloniaList(); + var target = new ItemsSourceView(source); + var debug = (INotifyCollectionChangedDebug)source; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + + void Handler(object sender, NotifyCollectionChangedEventArgs e) { } + target.CollectionChanged += Handler; + + Assert.NotNull(debug.GetCollectionChangedSubscribers()); + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + + target.CollectionChanged -= Handler; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + } + + [Fact] + public void Cannot_Wrap_An_ItemsSourceView_In_Another() + { + var source = new ItemsSourceView(new string[0]); + Assert.Throws(() => new ItemsSourceView(source)); + } + + [Fact] + public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List() + { + var source = new InvalidCollection(); + Assert.Throws(() => new ItemsSourceView(source)); + } + + private class InvalidCollection : INotifyCollectionChanged, IEnumerable + { + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public IEnumerator GetEnumerator() + { + yield break; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + } + } +}