diff --git a/build/NetFX.props b/build/NetFX.props index 4d2841714b..ed5cb6dd69 100644 --- a/build/NetFX.props +++ b/build/NetFX.props @@ -1,11 +1,7 @@ - - - /usr/lib/mono/4.6.1-api - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.6.1-api - - - /usr/lib/mono/4.7-api/ - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.7-api - + + + + + diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index bfa33eb259..340a04c02d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -369,12 +369,9 @@ public: virtual void UpdateCursor() { - [View resetCursorRects]; if (cursor != nil) { - auto rect = [Window frame]; - [View addCursorRect:rect cursor:cursor]; - [cursor set]; + [cursor set]; } } @@ -425,6 +422,7 @@ private: { WindowEvents = events; [Window setCanBecomeKeyAndMain]; + [Window disableCursorRects]; } virtual HRESULT Show () override diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a232a06383..b57a9a0a9e 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -24,6 +24,9 @@ + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index c35f8a3c0c..c99a6b117b 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -24,7 +24,6 @@ - @@ -34,12 +33,15 @@ - + + + - + @@ -50,12 +52,12 @@ - + Light Dark - + diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ce40b3e517..c2e5c8e4f3 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -8,9 +8,13 @@ namespace Avalonia.Data.Core public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); + protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); + protected static readonly WeakReference NullReference = + new WeakReference(null); + private WeakReference _target = UnsetReference; private Action _subscriber; private bool _listening; @@ -98,7 +102,7 @@ namespace Avalonia.Data.Core if (notification == null) { - LastValue = new WeakReference(value); + LastValue = value != null ? new WeakReference(value) : NullReference; if (Next != null) { @@ -111,7 +115,7 @@ namespace Avalonia.Data.Core } else { - LastValue = new WeakReference(notification.Value); + LastValue = notification.Value != null ? new WeakReference(notification.Value) : NullReference; if (Next != null) { @@ -136,8 +140,8 @@ namespace Avalonia.Data.Core } else if (target != AvaloniaProperty.UnsetValue) { - StartListeningCore(_target); _listening = true; + StartListeningCore(_target); } else { diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 4716b45340..cbceb58204 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -103,8 +103,8 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - SendCurrentValue(); SubscribeToChanges(); + SendCurrentValue(); } protected override void UnsubscribeCore() diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index eb98b9e8d6..d0a918dc88 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -29,6 +29,11 @@ namespace Avalonia.Data.Core if (!isLastValueAlive) { + if (value == null && LastValue == NullReference) + { + return true; + } + return false; } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 7b76457049..d85eb4cd76 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities /// public static class TypeUtilities { - private static int[] Conversions = + private static readonly int[] Conversions = { 0b101111111111101, // Boolean 0b100001111111110, // Char @@ -32,7 +32,7 @@ namespace Avalonia.Utilities 0b111111111111111, // String }; - private static int[] ImplicitConversions = + private static readonly int[] ImplicitConversions = { 0b000000000000001, // Boolean 0b001110111100010, // Char @@ -51,7 +51,7 @@ namespace Avalonia.Utilities 0b100000000000000, // String }; - private static Type[] InbuiltTypes = + private static readonly Type[] InbuiltTypes = { typeof(Boolean), typeof(Char), @@ -70,7 +70,7 @@ namespace Avalonia.Utilities typeof(String), }; - private static readonly Type[] NumericTypes = new[] + private static readonly Type[] NumericTypes = { typeof(Byte), typeof(Decimal), @@ -188,8 +188,7 @@ namespace Avalonia.Utilities } } - var cast = from.GetRuntimeMethods() - .FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to); + var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) { @@ -253,8 +252,7 @@ namespace Avalonia.Utilities } } - var cast = from.GetRuntimeMethods() - .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to); + var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit); if (cast != null) { @@ -335,5 +333,41 @@ namespace Avalonia.Utilities return NumericTypes.Contains(type); } } + + [Flags] + private enum OperatorType + { + Implicit = 1, + Explicit = 2 + } + + private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType) + { + const string implicitName = "op_Implicit"; + const string explicitName = "op_Explicit"; + + bool allowImplicit = (operatorType & OperatorType.Implicit) != 0; + bool allowExplicit = (operatorType & OperatorType.Explicit) != 0; + + foreach (MethodInfo method in fromType.GetMethods()) + { + if (!method.IsSpecialName || method.ReturnType != toType) + { + continue; + } + + if (allowImplicit && method.Name == implicitName) + { + return method; + } + + if (allowExplicit && method.Name == explicitName) + { + return method; + } + } + + return null; + } } } diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index b59ed166bc..f4cec98628 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -161,9 +161,8 @@ namespace Avalonia.Utilities for (int c = 0; c < _count; ++c) { var reference = _data[c].Subscriber; - TSubscriber instance; - if (reference != null && reference.TryGetTarget(out instance) && instance == s) + if (reference != null && reference.TryGetTarget(out TSubscriber instance) && Equals(instance, s.Target)) { _data[c] = default; removed = true; diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f26cd47bcb..449ca18465 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -66,7 +66,11 @@ namespace Avalonia.Controls } /// - public new IList SelectedItems => base.SelectedItems; + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } /// /// Gets or sets the selection mode. diff --git a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs b/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs index 768d87c63e..9edf9848fd 100644 --- a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs +++ b/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs @@ -39,6 +39,11 @@ namespace Avalonia.Controls foreach (Control child in children) { + if (!child.IsVisible) + { + continue; + } + double childWidth = child.DesiredSize.Width; double childHeight = child.DesiredSize.Height; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b8b8094582..cd14211075 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -295,11 +295,14 @@ namespace Avalonia.Controls.Presenters /// public override void ScrollIntoView(object item) { - var index = Items.IndexOf(item); - - if (index != -1) + if (Items != null) { - ScrollIntoView(index); + var index = Items.IndexOf(item); + + if (index != -1) + { + ScrollIntoView(index); + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index d428952bb9..07348cdf78 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -100,6 +100,12 @@ namespace Avalonia.Controls.Primitives.PopupPositioning ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); + + if (targetScreen != null && targetScreen.WorkingArea.IsEmpty) + { + return targetScreen.Bounds; + } + return targetScreen?.WorkingArea ?? new Rect(0, 0, double.MaxValue, double.MaxValue); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 340947d2e1..cc0c5f52be 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives private bool _syncingSelectedItems; private int _updateCount; private int _updateSelectedIndex; - private IList _updateSelectedItems; + private object _updateSelectedItem; /// /// Initializes static members of the class. @@ -160,7 +160,7 @@ namespace Avalonia.Controls.Primitives else { _updateSelectedIndex = value; - _updateSelectedItems = null; + _updateSelectedItem = null; } } } @@ -183,7 +183,7 @@ namespace Avalonia.Controls.Primitives } else { - _updateSelectedItems = new AvaloniaList(value); + _updateSelectedItem = value; _updateSelectedIndex = int.MinValue; } } @@ -1042,7 +1042,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(e); } - if (AutoScrollToSelectedItem) + if (AutoScrollToSelectedItem && _selectedIndex != -1) { ScrollIntoView(_selectedItem); } @@ -1075,9 +1075,9 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = _updateSelectedIndex; } - else if (_updateSelectedItems != null) + else if (_updateSelectedItem != null) { - SelectedItems = _updateSelectedItems; + SelectedItem = _updateSelectedItem; } } diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index a1d353f135..54afde7da2 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls public static readonly RadioButtonGroupManager Default = new RadioButtonGroupManager(); static readonly ConditionalWeakTable s_registeredVisualRoots = new ConditionalWeakTable(); - + readonly Dictionary>> s_registeredGroups = new Dictionary>>(); @@ -127,13 +127,11 @@ namespace Avalonia.Controls { if (!string.IsNullOrEmpty(GroupName)) { - var manager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root); - if (manager != _groupManager) - { - _groupManager.Remove(this, _groupName); - _groupManager = manager; - manager.Add(this); - } + _groupManager?.Remove(this, _groupName); + + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root); + + _groupManager.Add(this); } base.OnAttachedToVisualTree(e); } @@ -141,9 +139,10 @@ namespace Avalonia.Controls protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - if (!string.IsNullOrEmpty(GroupName) && _groupManager != null) + + if (!string.IsNullOrEmpty(GroupName)) { - _groupManager.Remove(this, _groupName); + _groupManager?.Remove(this, _groupName); } } @@ -152,9 +151,9 @@ namespace Avalonia.Controls string oldGroupName = GroupName; if (newGroupName != oldGroupName) { - if (!string.IsNullOrEmpty(oldGroupName) && _groupManager != null) + if (!string.IsNullOrEmpty(oldGroupName)) { - _groupManager.Remove(this, oldGroupName); + _groupManager?.Remove(this, oldGroupName); } _groupName = newGroupName; if (!string.IsNullOrEmpty(newGroupName)) @@ -181,7 +180,7 @@ namespace Avalonia.Controls .GetVisualChildren() .OfType() .Where(x => x != this); - + foreach (var sibling in siblings) { if (sibling.IsChecked.GetValueOrDefault()) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index bd3441078d..9e087b7fd3 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -251,7 +251,7 @@ namespace Avalonia.Controls { var child = children[i]; - if (child == null) + if (child == null || !child.IsVisible) { continue; } if (fHorizontal) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 29768513f3..9e4b2b84e0 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -231,15 +231,6 @@ namespace Avalonia.Input } break; - - case Key.F10: - _owner.ShowAccessKeys = _showingAccessKeys = true; - if (MainMenu != null) - { - MainMenu.Open(); - e.Handled = true; - } - break; } } diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 2377edf640..5eaee4833c 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,41 +1,56 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.Linq; namespace Avalonia.Input { + /// + /// Defines a keyboard input combination. + /// public sealed class KeyGesture : IEquatable { - public KeyGesture() + private static readonly Dictionary s_keySynonyms = new Dictionary + { + { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod } + }; + + [Obsolete("Use constructor taking KeyModifiers")] + public KeyGesture(Key key, InputModifiers modifiers) { - + Key = key; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf); } - public KeyGesture(Key key, InputModifiers modifiers = InputModifiers.None) + public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None) { Key = key; - Modifiers = modifiers; + KeyModifiers = modifiers; } - + public bool Equals(KeyGesture other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Key == other.Key && Modifiers == other.Modifiers; + + return Key == other.Key && KeyModifiers == other.KeyModifiers; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj is KeyGesture && Equals((KeyGesture) obj); + + return obj is KeyGesture && Equals((KeyGesture)obj); } public override int GetHashCode() { unchecked { - return ((int) Key*397) ^ (int) Modifiers; + return ((int)Key * 397) ^ (int)KeyModifiers; } } @@ -49,85 +64,85 @@ namespace Avalonia.Input return !Equals(left, right); } - public Key Key { get; set; } + public Key Key { get; } [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers - { - get => (InputModifiers)KeyModifiers; - set => KeyModifiers = (KeyModifiers)(((int)value) & 0xf); - } - - public KeyModifiers KeyModifiers { get; set; } + public InputModifiers Modifiers => (InputModifiers)KeyModifiers; - - static readonly Dictionary KeySynonyms = new Dictionary - { - {"+", Key.OemPlus }, - {"-", Key.OemMinus}, - {".", Key.OemPeriod } - }; - - //TODO: Move that to external key parser - static Key ParseKey(string key) - { - Key rv; - if (KeySynonyms.TryGetValue(key.ToLower(), out rv)) - return rv; - return (Key)Enum.Parse(typeof (Key), key, true); - } - - static InputModifiers ParseModifier(string modifier) - { - if (modifier.Equals("ctrl", StringComparison.OrdinalIgnoreCase)) - return InputModifiers.Control; - return (InputModifiers) Enum.Parse(typeof (InputModifiers), modifier, true); - } + public KeyModifiers KeyModifiers { get; } public static KeyGesture Parse(string gesture) { - //string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture + // string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture - var parts = new List(); + var key = Key.None; + var keyModifiers = KeyModifiers.None; var cstart = 0; + for (var c = 0; c <= gesture.Length; c++) { var ch = c == gesture.Length ? '\0' : gesture[c]; - if (c == gesture.Length || (ch == '+' && cstart != c)) + bool isLast = c == gesture.Length; + + if (isLast || (ch == '+' && cstart != c)) { - parts.Add(gesture.Substring(cstart, c - cstart)); + var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); + + if (isLast) + { + key = ParseKey(partSpan.ToString()); + } + else + { + keyModifiers |= ParseModifier(partSpan); + } + cstart = c + 1; } } - for (var c = 0; c < parts.Count; c++) - parts[c] = parts[c].Trim(); - var rv = new KeyGesture(); - for (var c = 0; c < parts.Count; c++) - { - if (c == parts.Count - 1) - rv.Key = ParseKey(parts[c]); - else - rv.Modifiers |= ParseModifier(parts[c]); - } - return rv; + return new KeyGesture(key, keyModifiers); } public override string ToString() { var parts = new List(); - foreach (var flag in Enum.GetValues(typeof (InputModifiers)).Cast()) + + foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast()) { - if (Modifiers.HasFlag(flag) && flag != InputModifiers.None) + if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None) + { parts.Add(flag.ToString()); + } } + parts.Add(Key.ToString()); + return string.Join(" + ", parts); } - public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.Modifiers == Modifiers; + public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers; + + // TODO: Move that to external key parser + private static Key ParseKey(string key) + { + if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) + return rv; + + return (Key)Enum.Parse(typeof(Key), key, true); + } + + private static KeyModifiers ParseModifier(ReadOnlySpan modifier) + { + if (modifier.Equals("ctrl".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Control; + } + + return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); + } private Key ResolveNumPadOperationKey(Key key) { diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index bf1799bbdc..efcc555159 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -246,22 +246,7 @@ namespace Avalonia.Rendering { try { - IDrawingContextImpl GetContext() - { - if (context != null) - return context; - if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) - { - RenderTarget.Dispose(); - RenderTarget = null; - } - if (RenderTarget == null) - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); - return context = RenderTarget.CreateDrawingContext(this); - - } - - var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); + var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); using (scene) { @@ -271,9 +256,9 @@ namespace Avalonia.Rendering if (DrawDirtyRects) _dirtyRectsDisplay.Tick(); if (overlay) - RenderOverlay(scene.Item, GetContext()); + RenderOverlay(scene.Item, ref context); if (updated || forceComposite || overlay) - RenderComposite(scene.Item, GetContext()); + RenderComposite(scene.Item, ref context); } } } @@ -291,7 +276,7 @@ namespace Avalonia.Rendering } } - private (IRef scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(Func contextFactory, + private (IRef scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl context, bool recursiveCall = false) { IRef sceneRef; @@ -304,7 +289,8 @@ namespace Avalonia.Rendering var scene = sceneRef.Item; if (scene.Generation != _lastSceneId) { - var context = contextFactory(); + EnsureDrawingContext(ref context); + Layers.Update(scene, context); RenderToLayers(scene); @@ -325,7 +311,7 @@ namespace Avalonia.Rendering if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate) { UpdateScene(); - var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(contextFactory, true); + var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); return (rs, true); } @@ -432,8 +418,10 @@ namespace Avalonia.Rendering } - private void RenderOverlay(Scene scene, IDrawingContextImpl parentContent) + private void RenderOverlay(Scene scene, ref IDrawingContextImpl parentContent) { + EnsureDrawingContext(ref parentContent); + if (DrawDirtyRects) { var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); @@ -460,8 +448,10 @@ namespace Avalonia.Rendering } } - private void RenderComposite(Scene scene, IDrawingContextImpl context) + private void RenderComposite(Scene scene, ref IDrawingContextImpl context) { + EnsureDrawingContext(ref context); + context.Clear(Colors.Transparent); var clientRect = new Rect(scene.Size); @@ -503,6 +493,27 @@ namespace Avalonia.Rendering } } + private void EnsureDrawingContext(ref IDrawingContextImpl context) + { + if (context != null) + { + return; + } + + if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + RenderTarget.Dispose(); + RenderTarget = null; + } + + if (RenderTarget == null) + { + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + } + + context = RenderTarget.CreateDrawingContext(this); + } + private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 64a8427290..140688f8bc 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -91,7 +91,19 @@ namespace Avalonia.Rendering { try { - if (_items.Any(item => item.NeedsUpdate) && + bool needsUpdate = false; + + foreach (IRenderLoopTask item in _items) + { + if (item.NeedsUpdate) + { + needsUpdate = true; + + break; + } + } + + if (needsUpdate && Interlocked.CompareExchange(ref _inUpdate, 1, 0) == 0) { _dispatcher.Post(() => diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index 87a4174c06..59f1da564c 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -104,8 +104,8 @@ namespace Avalonia.X11 {X11Key.x, Key.X}, {X11Key.y, Key.Y}, {X11Key.z, Key.Z}, - {X11Key.Meta_L, Key.LWin }, - {X11Key.Meta_R, Key.RWin }, + {X11Key.Super_L, Key.LWin }, + {X11Key.Super_R, Key.RWin }, {X11Key.Menu, Key.Apps}, //{ X11Key.?, Key.Sleep } {X11Key.KP_0, Key.NumPad0}, diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index a17e6b8b51..db4c916052 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -32,9 +32,11 @@ namespace Avalonia.Win32 var allDrives = DriveInfo.GetDrives(); var mountVolInfos = allDrives + .Where(p => p.IsReady) .Select(p => new MountedVolumeInfo() { - VolumeLabel = p.VolumeLabel, + VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName + : $"{p.VolumeLabel} ({p.Name})", VolumePath = p.RootDirectory.FullName, VolumeSizeBytes = (ulong)p.TotalSize }) diff --git a/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs index 9ed6590821..81b882308d 100644 --- a/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void EventShoudBePassedToSubscriber() + public void EventShouldBePassedToSubscriber() { bool handled = false; var subscriber = new Subscriber(() => handled = true); @@ -47,7 +47,23 @@ namespace Avalonia.Base.UnitTests Assert.True(handled); } - + [Fact] + public void EventShouldNotBeRaisedAfterUnsubscribe() + { + bool handled = false; + var subscriber = new Subscriber(() => handled = true); + var source = new EventSource(); + WeakEventHandlerManager.Subscribe(source, "Event", + subscriber.OnEvent); + + WeakEventHandlerManager.Unsubscribe(source, "Event", + subscriber.OnEvent); + + source.Fire(); + + Assert.False(handled); + } + [Fact] public void EventHandlerShouldNotBeKeptAlive() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index f4f500b802..4e4d92afdc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -921,6 +921,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() + { + // Issue #2969. + var target = new ListBox(); + var selectedItems = new List(); + + target.BeginInit(); + target.Template = Template(); + target.Items = new[] { "Foo", "Bar", "Baz" }; + target.SelectedItems = selectedItems; + target.SelectedItem = "Bar"; + target.EndInit(); + + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(selectedItems, target.SelectedItems); + Assert.Equal(new[] { "Bar" }, selectedItems); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index db113f0569..984734d414 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -332,6 +332,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren); } + [Theory] + [InlineData(Orientation.Horizontal)] + [InlineData(Orientation.Vertical)] + public void Only_Arrange_Visible_Children(Orientation orientation) + { + + var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false }; + var panel = new Panel { Width = 10, Height = 10 }; + + var target = new StackPanel + { + Spacing = 40, + Orientation = orientation, + Children = + { + hiddenPanel, + panel + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.Equal(new Rect(0, 0, 10, 10), panel.Bounds); + } + private class TestControl : Control { public Size MeasureConstraint { get; private set; } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index df522397ee..dd3b113d5d 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -24,8 +24,8 @@ namespace Avalonia.Controls.UnitTests.Utils .Bind().ToConstant(new WindowingPlatformMock()) .Bind().ToConstant(styler.Object); - var gesture1 = new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}; - var gesture2 = new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Control}; + var gesture1 = new KeyGesture(Key.A, InputModifiers.Control); + var gesture2 = new KeyGesture(Key.B, InputModifiers.Control); var tl = new Window(); var button = new Button(); diff --git a/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs b/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs index 006ed1140e..95eaab30de 100644 --- a/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Avalonia.Input.UnitTests @@ -11,13 +7,11 @@ namespace Avalonia.Input.UnitTests { public static readonly IEnumerable SampleData = new object[][] { - new object[]{"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}}, - new object[]{" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} }, - new object[]{"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} } + new object[]{"Ctrl+A", new KeyGesture(Key.A, InputModifiers.Control)}, + new object[]{" \tShift\t+Alt +B", new KeyGesture(Key.B, InputModifiers.Shift | InputModifiers.Alt) }, + new object[]{"Control++", new KeyGesture(Key.OemPlus, InputModifiers.Control) } }; - - - + [Theory] [MemberData(nameof(SampleData))] public void Key_Gesture_Is_Able_To_Parse_Sample_Data(string text, KeyGesture gesture) @@ -31,10 +25,8 @@ namespace Avalonia.Input.UnitTests [InlineData(Key.OemPeriod, Key.Decimal)] public void Key_Gesture_Matches_NumPad_To_Regular_Digit(Key gestureKey, Key pressedKey) { - var keyGesture = new KeyGesture - { - Key = gestureKey - }; + var keyGesture = new KeyGesture(gestureKey); + Assert.True(keyGesture.Matches(new KeyEventArgs { Key = pressedKey diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 0ba06980af..7e053392c7 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -154,6 +154,18 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("bar", source.Foo); } + [Fact] + public void OneTime_Binding_Releases_Subscription_If_DataContext_Set_Later() + { + var target = new TextBlock(); + var source = new Source { Foo = "foo" }; + + target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)); + target.DataContext = source; + + Assert.Equal(0, source.SubscriberCount); + } + [Fact] public void OneWayToSource_Binding_Should_Be_Set_Up() { @@ -196,6 +208,30 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", target.Text); } + [Fact] + public void OneWayToSource_Binding_Should_Not_StackOverflow_With_Null_Value() + { + // Issue #2912 + var target = new TextBlock { Text = null }; + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.OneWayToSource, + }; + + target.Bind(TextBox.TextProperty, binding); + + var source = new Source { Foo = "foo" }; + target.DataContext = source; + + Assert.Null(source.Foo); + + // When running tests under NCrunch, NCrunch replaces the standard StackOverflowException + // with its own, which will be caught by our code. Detect the stackoverflow anyway, by + // making sure the target property was only set once. + Assert.Equal(2, source.FooSetCount); + } + [Fact] public void Default_BindingMode_Should_Be_Used() { @@ -543,6 +579,23 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(expected, child.DoubleValue); } + [Fact] + public void Combined_OneTime_And_OneWayToSource_Bindings_Should_Release_Subscriptions() + { + var target1 = new TextBlock(); + var target2 = new TextBlock(); + var root = new Panel { Children = { target1, target2 } }; + var source = new Source { Foo = "foo" }; + + using (target1.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime))) + using (target2.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneWayToSource))) + { + root.DataContext = source; + } + + Assert.Equal(0, source.SubscriberCount); + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty = @@ -622,6 +675,7 @@ namespace Avalonia.Markup.UnitTests.Data public class Source : INotifyPropertyChanged { + private PropertyChangedEventHandler _propertyChanged; private string _foo; public string Foo @@ -630,15 +684,25 @@ namespace Avalonia.Markup.UnitTests.Data set { _foo = value; + ++FooSetCount; RaisePropertyChanged(); } } - public event PropertyChangedEventHandler PropertyChanged; + public int FooSetCount { get; private set; } + + + public int SubscriberCount { get; private set; } + + public event PropertyChangedEventHandler PropertyChanged + { + add { _propertyChanged += value; ++SubscriberCount; } + remove { _propertyChanged += value; --SubscriberCount; } + } private void RaisePropertyChanged([CallerMemberName] string prop = "") { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } }