Browse Source

Merge branch 'master' into reactiveui-update

pull/2930/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
76e76341bf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      build/SharedVersion.props
  2. 4
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  3. 21
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  4. 1
      scripts/ReplaceNugetCache.sh
  5. 17
      src/Avalonia.Animation/Animatable.cs
  6. 37
      src/Avalonia.Base/AvaloniaObject.cs
  7. 44
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 28
      src/Avalonia.Base/BoxedValue.cs
  9. 16
      src/Avalonia.Base/PriorityBindingEntry.cs
  10. 14
      src/Avalonia.Base/PriorityValue.cs
  11. 11
      src/Avalonia.Base/StyledPropertyBase.cs
  12. 14
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  13. 30
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  14. 50
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  15. 32
      src/Avalonia.Controls/ContentControl.cs
  16. 13
      src/Avalonia.Controls/ItemsControl.cs
  17. 6
      src/Avalonia.Controls/ListBox.cs
  18. 166
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  19. 5
      src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs
  20. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  21. 47
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  22. 13
      src/Avalonia.Controls/Presenters/IContentPresenter.cs
  23. 13
      src/Avalonia.Controls/Presenters/IContentPresenterHost.cs
  24. 11
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  25. 26
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  26. 32
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  27. 32
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  28. 6
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  29. 20
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  30. 25
      src/Avalonia.Controls/RadioButton.cs
  31. 2
      src/Avalonia.Controls/StackPanel.cs
  32. 36
      src/Avalonia.Controls/TabControl.cs
  33. 7
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  34. 5
      src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
  35. 9
      src/Avalonia.Input/AccessKeyHandler.cs
  36. 30
      src/Avalonia.Input/Gestures.cs
  37. 12
      src/Avalonia.Input/InputElement.cs
  38. 23
      src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs
  39. 19
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  40. 30
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  41. 160
      src/Avalonia.Visuals/Vector.cs
  42. 9
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  43. 1
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  44. 40
      tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
  45. 12
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  46. 107
      tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs
  47. 13
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  48. 2
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  49. 47
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  50. 25
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  51. 12
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  52. 3
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  53. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  54. 3
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
  55. 46
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
  56. 112
      tests/Avalonia.Visuals.UnitTests/VectorTests.cs

9
build/SharedVersion.props

@ -4,11 +4,16 @@
<Product>Avalonia</Product>
<Version>0.8.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
<LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIconUrl>https://avatars2.githubusercontent.com/u/14075148?s=200</PackageIconUrl>
<PackageDescription>Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS.</PackageDescription>
<PackageTags>avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin</PackageTags>
<PackageReleaseNotes>https://github.com/AvaloniaUI/Avalonia/releases</PackageReleaseNotes>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
</Project>

4
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,12 +10,14 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem>
<ComboBoxItem>Multiple</ComboBoxItem>

21
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
@ -27,7 +28,7 @@ namespace ControlCatalog.Pages
public PageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10).Select(i => GenerateItem()));
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
SelectedItems = new ObservableCollection<string>();
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
@ -39,16 +40,34 @@ namespace ControlCatalog.Pages
Items.Remove(SelectedItems[0]);
}
});
SelectRandomItemCommand = ReactiveCommand.Create(() =>
{
var random = new Random();
SelectedItem = Items[random.Next(Items.Count - 1)];
});
}
public ObservableCollection<string> Items { get; }
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
}
public ObservableCollection<string> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;

1
scripts/ReplaceNugetCache.sh

@ -2,7 +2,6 @@
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

17
src/Avalonia.Animation/Animatable.cs

@ -45,16 +45,17 @@ namespace Avalonia.Animation
{
get
{
if (_transitions == null)
if (_transitions is null)
_transitions = new Transitions();
if (_previousTransitions == null)
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
return _transitions;
}
set
{
SetAndRaise(TransitionsProperty, ref _transitions, value);
}
}
@ -66,18 +67,20 @@ namespace Avalonia.Animation
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null)
{
var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
if (match != null)
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in Transitions)
{
if (transition.Property == e.Property)
{
if (_previousTransitions.TryGetValue(e.Property, out var dispose))
dispose.Dispose();
var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
_previousTransitions[e.Property] = instance;
return;
}
}
}

37
src/Avalonia.Base/AvaloniaObject.cs

@ -82,6 +82,7 @@ namespace Avalonia
set
{
VerifyAccess();
if (_inheritanceParent != value)
{
if (_inheritanceParent != null)
@ -89,25 +90,33 @@ namespace Avalonia
_inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
}
var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType()));
var inherited = (from property in properties
where property.Inherits
select new
{
Property = property,
Value = GetValue(property),
}).ToList();
var oldInheritanceParent = _inheritanceParent;
_inheritanceParent = value;
var valuestore = _values;
foreach (var i in inherited)
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
{
object newValue = GetValue(i.Property);
if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
{
// if local value set there can be no change
continue;
}
// get the value as it would have been with the previous InheritanceParent
object oldValue;
if (oldInheritanceParent is AvaloniaObject aobj)
{
oldValue = aobj.GetValueOrDefaultUnchecked(property);
}
else
{
oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
object newValue = GetDefaultValue(property);
if (!Equals(i.Value, newValue))
if (!Equals(oldValue, newValue))
{
RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue);
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
}

44
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -26,6 +26,8 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
/// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -103,6 +105,46 @@ namespace Avalonia
return result;
}
/// <summary>
/// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
if (_inheritedCache.TryGetValue(type, out var result))
{
return result;
}
result = new List<AvaloniaProperty>();
var visited = new HashSet<AvaloniaProperty>();
foreach (var property in GetRegistered(type))
{
if (property.Inherits)
{
result.Add(property);
visited.Add(property);
}
}
foreach (var property in GetRegisteredAttached(type))
{
if (property.Inherits)
{
if (!visited.Contains(property))
{
result.Add(property);
}
}
}
_inheritedCache.Add(type, result);
return result;
}
/// <summary>
/// Gets all <see cref="AvaloniaProperty"/>s registered on a object.
/// </summary>
@ -230,6 +272,7 @@ namespace Avalonia
_registeredCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
/// <summary>
@ -266,6 +309,7 @@ namespace Avalonia
_attachedCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
internal void NotifyInitialized(AvaloniaObject o)

28
src/Avalonia.Base/BoxedValue.cs

@ -0,0 +1,28 @@
// 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.
namespace Avalonia
{
/// <summary>
/// Represents boxed value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type of stored value.</typeparam>
internal readonly struct BoxedValue<T>
{
public BoxedValue(T value)
{
Boxed = value;
Typed = value;
}
/// <summary>
/// Boxed value.
/// </summary>
public object Boxed { get; }
/// <summary>
/// Typed value.
/// </summary>
public T Typed { get; }
}
}

16
src/Avalonia.Base/PriorityBindingEntry.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.ExceptionServices;
using Avalonia.Data;
using Avalonia.Threading;
@ -10,9 +11,9 @@ namespace Avalonia
/// <summary>
/// A registered binding in a <see cref="PriorityValue"/>.
/// </summary>
internal class PriorityBindingEntry : IDisposable
internal class PriorityBindingEntry : IDisposable, IObserver<object>
{
private PriorityLevel _owner;
private readonly PriorityLevel _owner;
private IDisposable _subscription;
/// <summary>
@ -85,7 +86,7 @@ namespace Avalonia
Description = ((IDescription)binding).Description;
}
_subscription = binding.Subscribe(ValueChanged, Completed);
_subscription = binding.Subscribe(this);
}
/// <summary>
@ -96,7 +97,7 @@ namespace Avalonia
_subscription?.Dispose();
}
private void ValueChanged(object value)
void IObserver<object>.OnNext(object value)
{
void Signal()
{
@ -132,7 +133,7 @@ namespace Avalonia
}
}
private void Completed()
void IObserver<object>.OnCompleted()
{
HasCompleted = true;
@ -145,5 +146,10 @@ namespace Avalonia
Dispatcher.UIThread.Post(() => _owner.Completed(this));
}
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}

14
src/Avalonia.Base/PriorityValue.cs

@ -24,13 +24,11 @@ namespace Avalonia
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
{
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
@ -52,7 +50,6 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -257,10 +254,15 @@ namespace Avalonia
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
_setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
}
void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
{
SetAndNotify(ref backing, value);
}
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;

11
src/Avalonia.Base/StyledPropertyBase.cs

@ -68,7 +68,7 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue;
return GetMetadata(type).DefaultValue.Typed;
}
/// <summary>
@ -164,7 +164,14 @@ namespace Avalonia
}
/// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type);
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
private object GetDefaultBoxedValue(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue.Boxed;
}
[DebuggerHidden]
private Func<IAvaloniaObject, TValue, TValue> Cast<THost>(Func<THost, TValue, TValue> validate)

14
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -19,26 +19,26 @@ namespace Avalonia
/// <param name="validate">A validation function.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
public StyledPropertyMetadata(
TValue defaultValue = default(TValue),
TValue defaultValue = default,
Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default)
: base(defaultBindingMode)
{
DefaultValue = defaultValue;
DefaultValue = new BoxedValue<TValue>(defaultValue);
Validate = validate;
}
/// <summary>
/// Gets the default value for the property.
/// </summary>
public TValue DefaultValue { get; private set; }
internal BoxedValue<TValue> DefaultValue { get; private set; }
/// <summary>
/// Gets the validation callback.
/// </summary>
public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
@ -47,11 +47,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
var src = baseMetadata as StyledPropertyMetadata<TValue>;
if (src != null)
if (baseMetadata is StyledPropertyMetadata<TValue> src)
{
if (DefaultValue == null)
if (DefaultValue.Boxed == null)
{
DefaultValue = src.DefaultValue;
}

30
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -5,15 +5,6 @@ using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
@ -70,14 +61,14 @@ namespace Avalonia.Utilities
return false;
}
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, ISetAndNotifyHandler<TValue> setAndNotifyHandler, ref TValue backing, TValue value)
where TValue : TSetRecord
{
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value);
}
if (!_pendingValues.Empty)
@ -86,7 +77,7 @@ namespace Avalonia.Utilities
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue());
}
}
}
@ -119,4 +110,19 @@ namespace Avalonia.Utilities
}
}
}
/// <summary>
/// Handler for set and notify requests.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
internal interface ISetAndNotifyHandler<TValue>
{
/// <summary>
/// Handles deferred setter requests to set a value.
/// </summary>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value);
}
}

50
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -13,7 +13,7 @@ namespace Avalonia.Utilities
/// </summary>
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;
}
}
}

32
src/Avalonia.Controls/ContentControl.cs

@ -1,11 +1,13 @@
// 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 Avalonia.Collections;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
namespace Avalonia.Controls
@ -39,12 +41,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
AvaloniaProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static ContentControl()
{
ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
ContentProperty.Changed.AddClassHandler<ContentControl>(x => x.ContentChanged);
}
/// <summary>
@ -95,20 +94,39 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
{
RegisterContentPresenter(presenter);
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual void RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
{
if (presenter.Name == "PART_ContentPresenter")
{
Presenter = presenter;
return true;
}
return false;
}
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
}

13
src/Avalonia.Controls/ItemsControl.cs

@ -302,19 +302,6 @@ namespace Avalonia.Controls
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
{
var toRemove = new List<ILogical>();
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be removed from the logical tree
// when it is removed from the Items collection.
if (container?.ContainerControl != container?.Item)
{
toRemove.Add(container.ContainerControl);
}
}
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>

6
src/Avalonia.Controls/ListBox.cs

@ -66,7 +66,11 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
public new IList SelectedItems
{
get => base.SelectedItems;
set => base.SelectedItems = value;
}
/// <summary>
/// Gets or sets the selection mode.

166
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@ -1,166 +0,0 @@
// 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.Linq;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Mixins
{
/// <summary>
/// Adds content control functionality to control classes.
/// </summary>
/// <para>
/// The <see cref="ContentControlMixin"/> adds behavior to a control which acts as a content
/// control such as <see cref="ContentControl"/> and <see cref="HeaderedItemsControl"/>. It
/// keeps the control's logical children in sync with the content being displayed by the
/// control.
/// </para>
public class ContentControlMixin
{
private static Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>> subscriptions =
new Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>>(() =>
new ConditionalWeakTable<TemplatedControl, IDisposable>());
/// <summary>
/// Initializes a new instance of the <see cref="SelectableMixin"/> class.
/// </summary>
/// <typeparam name="TControl">The control type.</typeparam>
/// <param name="content">The content property.</param>
/// <param name="logicalChildrenSelector">
/// Given an control of <typeparamref name="TControl"/> should return the control's
/// logical children collection.
/// </param>
/// <param name="presenterName">
/// The name of the content presenter in the control's template.
/// </param>
public static void Attach<TControl>(
AvaloniaProperty content,
Func<TControl, IAvaloniaList<ILogical>> logicalChildrenSelector,
string presenterName = "PART_ContentPresenter")
where TControl : TemplatedControl
{
Contract.Requires<ArgumentNullException>(content != null);
Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e)
{
if (s is IControl sender && sender?.TemplatedParent is TControl parent)
{
UpdateLogicalChild(
sender,
logicalChildrenSelector(parent),
e.OldValue,
null);
}
}
void TemplateApplied(object s, RoutedEventArgs ev)
{
if (s is TControl sender)
{
var e = (TemplateAppliedEventArgs)ev;
var presenter = e.NameScope.Find(presenterName) as IContentPresenter;
if (presenter != null)
{
presenter.ApplyTemplate();
var logicalChildren = logicalChildrenSelector(sender);
var subscription = new CompositeDisposable();
presenter.ChildChanging += ChildChanging;
subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging));
subscription.Add(presenter
.GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(c => UpdateLogicalChild(
sender,
logicalChildren,
null,
c.NewValue)));
UpdateLogicalChild(
sender,
logicalChildren,
null,
presenter.GetValue(ContentPresenter.ChildProperty));
if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription))
{
subscription = new CompositeDisposable(previousSubscription, subscription);
subscriptions.Value.Remove(sender);
}
subscriptions.Value.Add(sender, subscription);
}
}
}
TemplatedControl.TemplateAppliedEvent.AddClassHandler(
typeof(TControl),
TemplateApplied,
RoutingStrategies.Direct);
content.Changed.Subscribe(e =>
{
if (e.Sender is TControl sender)
{
var logicalChildren = logicalChildrenSelector(sender);
UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue);
}
});
Control.TemplatedParentProperty.Changed.Subscribe(e =>
{
if (e.Sender is TControl sender)
{
var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl;
logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent);
}
});
TemplatedControl.TemplateProperty.Changed.Subscribe(e =>
{
if (e.Sender is TControl sender)
{
if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription))
{
subscription.Dispose();
subscriptions.Value.Remove(sender);
}
}
});
}
private static void UpdateLogicalChild(
IControl control,
IAvaloniaList<ILogical> logicalChildren,
object oldValue,
object newValue)
{
if (oldValue != newValue)
{
if (oldValue is IControl child)
{
logicalChildren.Remove(child);
((ISetInheritanceParent)child).SetParent(child.Parent);
}
child = newValue as IControl;
if (child != null && !logicalChildren.Contains(child))
{
child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent);
logicalChildren.Add(child);
}
}
}
}
}

5
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;

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -377,7 +377,7 @@ namespace Avalonia.Controls.Platform
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Menu.Close();
Menu?.Close();
}
}

47
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
@ -83,7 +84,6 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
EventHandler<AvaloniaPropertyChangedEventArgs> _childChanging;
private IDataTemplate _dataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
@ -190,12 +190,10 @@ namespace Avalonia.Controls.Presenters
set { SetValue(PaddingProperty, value); }
}
/// <inheritdoc/>
event EventHandler<AvaloniaPropertyChangedEventArgs> IContentPresenter.ChildChanging
{
add => _childChanging += value;
remove => _childChanging -= value;
}
/// <summary>
/// Gets the host content control.
/// </summary>
internal IContentPresenterHost Host { get; private set; }
/// <inheritdoc/>
public sealed override void ApplyTemplate()
@ -222,34 +220,16 @@ namespace Avalonia.Controls.Presenters
var content = Content;
var oldChild = Child;
var newChild = CreateChild();
var logicalChildren = Host?.LogicalChildren ?? LogicalChildren;
// Remove the old child if we're not recycling it.
if (newChild != oldChild)
{
if (oldChild != null)
{
VisualChildren.Remove(oldChild);
}
if (oldChild?.Parent == this)
{
// If we're the child's parent then the presenter isn't in a ContentControl's
// template.
LogicalChildren.Remove(oldChild);
}
else if (TemplatedParent != null)
{
// If we're in a ContentControl's template then invoke ChildChanging to let
// ContentControlMixin handle removing the logical child.
_childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs(
this,
ChildProperty,
oldChild,
newChild,
BindingPriority.LocalValue));
}
else if (oldChild != null)
{
logicalChildren.Remove(oldChild);
((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
}
}
@ -272,15 +252,11 @@ namespace Avalonia.Controls.Presenters
else if (newChild != oldChild)
{
((ISetInheritanceParent)newChild).SetParent(this);
Child = newChild;
// If we're in a ContentControl's template then the child's parent will have been
// set by ContentControlMixin in response to Child changing. If not, then we're
// standalone and should make the control our own logical child.
if (newChild.Parent == null && TemplatedParent == null)
if (!logicalChildren.Contains(newChild))
{
LogicalChildren.Add(newChild);
logicalChildren.Add(newChild);
}
VisualChildren.Add(newChild);
@ -459,7 +435,8 @@ namespace Avalonia.Controls.Presenters
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)
{
(e.NewValue as IContentPresenterHost)?.RegisterContentPresenter(this);
var host = e.NewValue as IContentPresenterHost;
Host = host?.RegisterContentPresenter(this) == true ? host : null;
}
}
}

13
src/Avalonia.Controls/Presenters/IContentPresenter.cs

@ -1,8 +1,6 @@
// 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 Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Presenters
@ -22,16 +20,5 @@ namespace Avalonia.Controls.Presenters
/// Gets or sets the content to be displayed by the presenter.
/// </summary>
object Content { get; set; }
/// <summary>
/// Raised when <see cref="Child"/> property is about to change.
/// </summary>
/// <remarks>
/// This event should be raised after the child has been removed from the visual tree,
/// but before the <see cref="Child"/> property has changed. It is intended for consumption
/// by <see cref="ContentControlMixin"/> in order to update the host control's logical
/// children.
/// </remarks>
event EventHandler<AvaloniaPropertyChangedEventArgs> ChildChanging;
}
}

13
src/Avalonia.Controls/Presenters/IContentPresenterHost.cs

@ -1,6 +1,8 @@
// 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 Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
@ -18,10 +20,19 @@ namespace Avalonia.Controls.Presenters
/// </remarks>
public interface IContentPresenterHost : ITemplatedControl
{
/// <summary>
/// Gets a collection describing the logical children of the host control.
/// </summary>
IAvaloniaList<ILogical> LogicalChildren { get; }
/// <summary>
/// Registers an <see cref="IContentPresenter"/> with a host control.
/// </summary>
/// <param name="presenter">The content presenter.</param>
void RegisterContentPresenter(IContentPresenter presenter);
/// <returns>
/// True if the content presenter should add its child to the logical children of the
/// host; otherwise false.
/// </returns>
bool RegisterContentPresenter(IContentPresenter presenter);
}
}

11
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -295,11 +295,14 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
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);
}
}
}

26
src/Avalonia.Controls/Primitives/HeaderedContentControl.cs

@ -4,6 +4,7 @@
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
{
@ -29,10 +30,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
static HeaderedContentControl()
{
ContentControlMixin.Attach<HeaderedContentControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
ContentProperty.Changed.AddClassHandler<HeaderedContentControl>(x => x.HeaderChanged);
}
/// <summary>
@ -63,13 +61,29 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override void RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(IContentPresenter presenter)
{
base.RegisterContentPresenter(presenter);
var result = base.RegisterContentPresenter(presenter);
if (presenter.Name == "PART_HeaderPresenter")
{
HeaderPresenter = presenter;
result = true;
}
return result;
}
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
}

32
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -1,8 +1,10 @@
// 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 Avalonia.Collections;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
{
@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
static HeaderedItemsControl()
{
ContentControlMixin.Attach<HeaderedItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
HeaderProperty.Changed.AddClassHandler<HeaderedItemsControl>(x => x.HeaderChanged);
}
/// <summary>
@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
{
RegisterContentPresenter(presenter);
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual void RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
{
if (presenter.Name == "PART_HeaderPresenter")
{
HeaderPresenter = presenter;
return true;
}
return false;
}
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
}

32
src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@ -1,8 +1,10 @@
// 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 Avalonia.Collections;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
{
@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
static HeaderedSelectingItemsControl()
{
ContentControlMixin.Attach<HeaderedSelectingItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
HeaderProperty.Changed.AddClassHandler<HeaderedSelectingItemsControl>(x => x.HeaderChanged);
}
/// <summary>
@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
{
RegisterContentPresenter(presenter);
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual void RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
{
if (presenter.Name == "PART_HeaderPresenter")
{
HeaderPresenter = presenter;
return true;
}
return false;
}
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
}

6
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);
}

20
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;
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> 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<object>(value);
_updateSelectedItem = value;
_updateSelectedIndex = int.MinValue;
}
}
@ -855,11 +855,6 @@ namespace Avalonia.Controls.Primitives
_selectedItem = ElementAt(Items, _selectedIndex);
RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue);
if (AutoScrollToSelectedItem)
{
ScrollIntoView(_selectedIndex);
}
}
}
@ -1046,6 +1041,11 @@ namespace Avalonia.Controls.Primitives
removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
RaiseEvent(e);
}
if (AutoScrollToSelectedItem && _selectedIndex != -1)
{
ScrollIntoView(_selectedItem);
}
}
private void UpdateSelectedItems(Action action)
@ -1075,9 +1075,9 @@ namespace Avalonia.Controls.Primitives
{
SelectedIndex = _updateSelectedIndex;
}
else if (_updateSelectedItems != null)
else if (_updateSelectedItem != null)
{
SelectedItems = _updateSelectedItems;
SelectedItem = _updateSelectedItem;
}
}

25
src/Avalonia.Controls/RadioButton.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
public static readonly RadioButtonGroupManager Default = new RadioButtonGroupManager();
static readonly ConditionalWeakTable<IRenderRoot, RadioButtonGroupManager> s_registeredVisualRoots
= new ConditionalWeakTable<IRenderRoot, RadioButtonGroupManager>();
readonly Dictionary<string, List<WeakReference<RadioButton>>> s_registeredGroups
= new Dictionary<string, List<WeakReference<RadioButton>>>();
@ -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<RadioButton>()
.Where(x => x != this);
foreach (var sibling in siblings)
{
if (sibling.IsChecked.GetValueOrDefault())

2
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)

36
src/Avalonia.Controls/TabControl.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
@ -9,6 +10,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -16,7 +18,7 @@ namespace Avalonia.Controls
/// <summary>
/// A tab control that displays a tab strip along with the content of the selected tab.
/// </summary>
public class TabControl : SelectingItemsControl
public class TabControl : SelectingItemsControl, IContentPresenterHost
{
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
@ -68,10 +70,6 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty);
ContentControlMixin.Attach<TabControl>(
SelectedContentProperty,
x => x.LogicalChildren,
"PART_SelectedContentHost");
}
/// <summary>
@ -136,7 +134,31 @@ namespace Avalonia.Controls
internal ItemsPresenter ItemsPresenterPart { get; private set; }
internal ContentPresenter ContentPart { get; private set; }
internal IContentPresenter ContentPart { get; private set; }
/// <inheritdoc/>
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
{
if (presenter.Name == "PART_SelectedContentHost")
{
ContentPart = presenter;
return true;
}
return false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@ -148,8 +170,6 @@ namespace Avalonia.Controls
base.OnTemplateApplied(e);
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
ContentPart = e.NameScope.Get<ContentPresenter>("PART_SelectedContentHost");
}
/// <inheritdoc/>

7
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -28,6 +28,11 @@ namespace Avalonia
{
Diagnostics.DevTools.Attach(control, gesture);
}
public static void OpenDevTools(this TopLevel control)
{
Diagnostics.DevTools.OpenDevTools(control);
}
}
}
@ -73,7 +78,7 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel);
}
private static void OpenDevTools(TopLevel control)
internal static void OpenDevTools(TopLevel control)
{
if (s_open.TryGetValue(control, out var devToolsWindow))
{

5
src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs

@ -70,7 +70,10 @@ namespace Avalonia.Diagnostics.Views
private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
var item = (TreeViewItem)sender;
var header = item.HeaderPresenter.Child;
var headerPresenter = item.HeaderPresenter;
headerPresenter.ApplyTemplate();
var header = headerPresenter.Child;
header.PointerEnter += AddAdorner;
header.PointerLeave += RemoveAdorner;
item.TemplateApplied -= TreeViewItemTemplateApplied;

9
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;
}
}

30
src/Avalonia.Input/Gestures.cs

@ -39,6 +39,36 @@ namespace Avalonia.Input
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
}
public static void AddTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TappedEvent, handler);
}
public static void AddDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(DoubleTappedEvent, handler);
}
public static void AddRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(RightTappedEvent, handler);
}
public static void RemoveTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(TappedEvent, handler);
}
public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(DoubleTappedEvent, handler);
}
public static void RemoveRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(RightTappedEvent, handler);
}
private static void PointerPressed(RoutedEventArgs ev)
{
if (ev.Route == RoutingStrategies.Bubble)

12
src/Avalonia.Input/InputElement.cs

@ -565,9 +565,17 @@ namespace Avalonia.Input
{
IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true);
foreach (var child in this.GetVisualChildren().OfType<InputElement>())
// PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
// will cause extra allocations and overhead.
var children = VisualChildren;
// ReSharper disable once ForCanBeConvertedToForeach
for (int i = 0; i < children.Count; ++i)
{
child.UpdateIsEffectivelyEnabled(this);
var child = children[i] as InputElement;
child?.UpdateIsEffectivelyEnabled(this);
}
}
}

23
src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs

@ -1,5 +1,8 @@
// 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.Reactive.Disposables;
using System.Threading;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
@ -13,11 +16,27 @@ namespace Avalonia.Native
{
_window = window;
}
public IDisposable TryLock()
{
if (_window.TryLock())
return Disposable.Create(() => _window.Unlock());
return new UnlockDisposable(_window);
return null;
}
private sealed class UnlockDisposable : IDisposable
{
private IAvnWindowBase _window;
public UnlockDisposable(IAvnWindowBase window)
{
_window = window;
}
public void Dispose()
{
Interlocked.Exchange(ref _window, null)?.Unlock();
}
}
}
}

19
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@ -8,16 +8,16 @@ using Avalonia.Controls.Platform;
namespace Avalonia.Native
{
internal class WindowsMountedVolumeInfoListener : IDisposable
internal class MacOSMountedVolumeInfoListener : IDisposable
{
private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs;
private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives;
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
public MacOSMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
this.mountedDrives = mountedDrives;
_disposables = new CompositeDisposable();
var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
@ -30,7 +30,8 @@ namespace Avalonia.Native
private void Poll(long _)
{
var mountVolInfos = Directory.GetDirectories("/Volumes")
var mountVolInfos = Directory.GetDirectories("/Volumes/")
.Where(p=> p != null)
.Select(p => new MountedVolumeInfo()
{
VolumeLabel = Path.GetFileName(p),
@ -38,15 +39,15 @@ namespace Avalonia.Native
VolumeSizeBytes = 0
})
.ToArray();
if (_targetObs.SequenceEqual(mountVolInfos))
if (mountedDrives.SequenceEqual(mountVolInfos))
return;
else
{
_targetObs.Clear();
mountedDrives.Clear();
foreach (var i in mountVolInfos)
_targetObs.Add(i);
mountedDrives.Add(i);
}
}
@ -72,7 +73,7 @@ namespace Avalonia.Native
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new WindowsMountedVolumeInfoListener(mountedDrives);
return new MacOSMountedVolumeInfoListener(mountedDrives);
}
}
}

30
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -1,5 +1,7 @@
// 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.Reactive.Disposables;
using System.Threading;
namespace Avalonia.Rendering
@ -7,7 +9,7 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to lock the target surface or window
/// </summary>
@ -15,7 +17,7 @@ namespace Avalonia.Rendering
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return new UnlockDisposable(_lock);
return null;
}
@ -25,7 +27,27 @@ namespace Avalonia.Rendering
public IDisposable Lock()
{
Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock));
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

160
src/Avalonia.Visuals/Vector.cs

@ -65,9 +65,7 @@ namespace Avalonia
/// <param name="b">Second vector</param>
/// <returns>The dot product</returns>
public static double operator *(Vector a, Vector b)
{
return a.X * b.X + a.Y * b.Y;
}
=> Dot(a, b);
/// <summary>
/// Scales a vector.
@ -76,9 +74,7 @@ namespace Avalonia
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale)
{
return new Vector(vector._x * scale, vector._y * scale);
}
=> Multiply(vector, scale);
/// <summary>
/// Scales a vector.
@ -87,14 +83,17 @@ namespace Avalonia
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
{
return new Vector(vector._x / scale, vector._y / scale);
}
=> Divide(vector, scale);
/// <summary>
/// Length of the vector
/// </summary>
public double Length => Math.Sqrt(X * X + Y * Y);
public double Length => Math.Sqrt(SquaredLength);
/// <summary>
/// Squared Length of the vector
/// </summary>
public double SquaredLength => _x * _x + _y * _y;
/// <summary>
/// Negates a vector.
@ -102,9 +101,7 @@ namespace Avalonia
/// <param name="a">The vector.</param>
/// <returns>The negated vector.</returns>
public static Vector operator -(Vector a)
{
return new Vector(-a._x, -a._y);
}
=> Negate(a);
/// <summary>
/// Adds two vectors.
@ -113,9 +110,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the addition.</returns>
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a._x + b._x, a._y + b._y);
}
=> Add(a, b);
/// <summary>
/// Subtracts two vectors.
@ -124,9 +119,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the subtraction.</returns>
public static Vector operator -(Vector a, Vector b)
{
return new Vector(a._x - b._x, a._y - b._y);
}
=> Subtract(a, b);
/// <summary>
/// Check if two vectors are equal (bitwise).
@ -155,7 +148,8 @@ namespace Avalonia
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(null, obj))
return false;
return obj is Vector vector && Equals(vector);
}
@ -206,5 +200,131 @@ namespace Avalonia
{
return new Vector(_x, y);
}
/// <summary>
/// Returns a normalized version of this vector.
/// </summary>
/// <returns>The normalized vector.</returns>
public Vector Normalize()
=> Normalize(this);
/// <summary>
/// Returns a negated version of this vector.
/// </summary>
/// <returns>The negated vector.</returns>
public Vector Negate()
=> Negate(this);
/// <summary>
/// Returns the dot product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The dot product.</returns>
public static double Dot(Vector a, Vector b)
=> a._x * b._x + a._y * b._y;
/// <summary>
/// Returns the cross product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The cross product.</returns>
public static double Cross(Vector a, Vector b)
=> a._x * b._y - a._y * b._x;
/// <summary>
/// Normalizes the given vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <returns>The normalized vector.</returns>
public static Vector Normalize(Vector vector)
=> Divide(vector, vector.Length);
/// <summary>
/// Divides the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector a, Vector b)
=> new Vector(a._x / b._x, a._y / b._y);
/// <summary>
/// Divides the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector vector, double scalar)
=> new Vector(vector._x / scalar, vector._y / scalar);
/// <summary>
/// Multiplies the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector a, Vector b)
=> new Vector(a._x * b._x, a._y * b._y);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector vector, double scalar)
=> new Vector(vector._x * scalar, vector._y * scalar);
/// <summary>
/// Adds the second to the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The summed vector.</returns>
public static Vector Add(Vector a, Vector b)
=> new Vector(a._x + b._x, a._y + b._y);
/// <summary>
/// Subtracts the second from the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The difference vector.</returns>
public static Vector Subtract(Vector a, Vector b)
=> new Vector(a._x - b._x, a._y - b._y);
/// <summary>
/// Negates the vector
/// </summary>
/// <param name="vector">The vector to negate.</param>
/// <returns>The scaled vector.</returns>
public static Vector Negate(Vector vector)
=> new Vector(-vector._x, -vector._y);
/// <summary>
/// Returnes the vector (0.0, 0.0)
/// </summary>
public static Vector Zero
=> new Vector(0, 0);
/// <summary>
/// Returnes the vector (1.0, 1.0)
/// </summary>
public static Vector One
=> new Vector(1, 1);
/// <summary>
/// Returnes the vector (1.0, 0.0)
/// </summary>
public static Vector UnitX
=> new Vector(1, 0);
/// <summary>
/// Returnes the vector (0.0, 1.0)
/// </summary>
public static Vector UnitY
=> new Vector(0, 1);
}
}

9
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

@ -10,8 +10,7 @@ namespace Avalonia.Win32
{
internal class WindowsMountedVolumeInfoListener : IDisposable
{
private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs = new ObservableCollection<MountedVolumeInfo>();
private readonly CompositeDisposable _disposables;
private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives;
@ -41,14 +40,14 @@ namespace Avalonia.Win32
})
.ToArray();
if (_targetObs.SequenceEqual(mountVolInfos))
if (mountedDrives.SequenceEqual(mountVolInfos))
return;
else
{
_targetObs.Clear();
mountedDrives.Clear();
foreach (var i in mountVolInfos)
_targetObs.Add(i);
mountedDrives.Add(i);
}
}

1
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -984,6 +984,7 @@ namespace Avalonia.Controls.UnitTests
TextBox textBox = GetTextBox(control);
var window = new Window {Content = control};
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
test.Invoke(control, textBox);
}

40
tests/Avalonia.Controls.UnitTests/ContentControlTests.cs

@ -50,6 +50,7 @@ namespace Avalonia.Controls.UnitTests
root.Child = target;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
styler.Verify(x => x.ApplyStyles(It.IsAny<ContentControl>()), Times.Once());
styler.Verify(x => x.ApplyStyles(It.IsAny<Border>()), Times.Once());
@ -331,6 +332,45 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(textBlock.GetLogicalParent());
}
[Fact]
public void Should_Set_Child_LogicalParent_After_Removing_And_Adding_Back_To_Logical_Tree()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = new ContentControl();
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<ContentControl>())
{
Setters =
{
new Setter(ContentControl.TemplateProperty, GetTemplate()),
}
}
},
Child = target
};
target.Content = "Foo";
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(target, target.Presenter.Child.LogicalParent);
root.Child = null;
Assert.Null(target.Template);
target.Content = null;
root.Child = target;
target.Content = "Bar";
Assert.Equal(target, target.Presenter.Child.LogicalParent);
}
}
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate<ContentControl>((parent, scope) =>

12
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -27,7 +27,9 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
new Window { Content = target }.ApplyTemplate();
var window = new Window { Content = target };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
int openedCount = 0;
@ -53,7 +55,9 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
new Window { Content = target }.ApplyTemplate();
var window = new Window { Content = target };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
sut.Open(target);
@ -86,6 +90,7 @@ namespace Avalonia.Controls.UnitTests
var window = new Window {Content = target};
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouse.Click(target, MouseButton.Right);
@ -115,7 +120,8 @@ namespace Avalonia.Controls.UnitTests
var window = new Window {Content = target};
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);

107
tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs

@ -1,107 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Mixins
{
public class ContentControlMixinTests
{
[Fact]
public void Multiple_Mixin_Usages_Should_Not_Throw()
{
var target = new TestControl()
{
Template = new FuncControlTemplate((_, scope) => new Panel
{
Children =
{
new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope),
new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope)
}
})
};
var ex = Record.Exception(() => target.ApplyTemplate());
Assert.Null(ex);
}
[Fact]
public void Replacing_Template_Releases_Events()
{
var p1 = new ContentPresenter { Name = "Content_1_Presenter" };
var p2 = new ContentPresenter { Name = "Content_2_Presenter" };
var target = new TestControl
{
Template = new FuncControlTemplate((_, scope) => new Panel
{
Children =
{
p1.RegisterInNameScope(scope),
p2.RegisterInNameScope(scope)
}
})
};
target.ApplyTemplate();
Control tc;
p1.Content = tc = new Control();
p1.UpdateChild();
Assert.Contains(tc, target.GetLogicalChildren());
p2.Content = tc = new Control();
p2.UpdateChild();
Assert.Contains(tc, target.GetLogicalChildren());
target.Template = null;
p1.Content = tc = new Control();
p1.UpdateChild();
Assert.DoesNotContain(tc, target.GetLogicalChildren());
p2.Content = tc = new Control();
p2.UpdateChild();
Assert.DoesNotContain(tc, target.GetLogicalChildren());
}
private class TestControl : TemplatedControl
{
public static readonly StyledProperty<object> Content1Property =
AvaloniaProperty.Register<TestControl, object>(nameof(Content1));
public static readonly StyledProperty<object> Content2Property =
AvaloniaProperty.Register<TestControl, object>(nameof(Content2));
static TestControl()
{
ContentControlMixin.Attach<TestControl>(Content1Property, x => x.LogicalChildren, "Content_1_Presenter");
ContentControlMixin.Attach<TestControl>(Content2Property, x => x.LogicalChildren, "Content_2_Presenter");
}
public object Content1
{
get { return GetValue(Content1Property); }
set { SetValue(Content1Property, value); }
}
public object Content2
{
get { return GetValue(Content2Property); }
set { SetValue(Content2Property, value); }
}
}
}
}

13
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs

@ -343,6 +343,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
}
[Fact]
public void Should_Clear_Host_When_Host_Template_Cleared()
{
var (target, host) = CreateTarget();
Assert.Same(host, target.Host);
host.Template = null;
host.ApplyTemplate();
Assert.Null(target.Host);
}
(ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
{
var templatedParent = new ContentControl

2
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -51,6 +51,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
window.Content = target;
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
target.ApplyTemplate();
target.Popup.Open();
@ -167,6 +168,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
window.Content = target;
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
target.ApplyTemplate();
target.Popup.Open();
target.PopupContent = null;

47
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -894,6 +894,53 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("Qux", target.SelectedItem);
}
[Fact]
public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Assert.True(raised);
}
[Fact]
public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
{
// Issue #2969.
var target = new ListBox();
var selectedItems = new List<object>();
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<SelectingItemsControl>((control, scope) =>

25
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; }

12
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -183,27 +183,27 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
var dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
Assert.Equal(items[0], dataContext);
target.SelectedIndex = 1;
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
dataContext = ((Button)target.ContentPart.Child).DataContext;
Assert.Equal(items[1], dataContext);
target.SelectedIndex = 2;
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
Assert.Equal("Base", dataContext);
target.SelectedIndex = 3;
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
Assert.Equal("Qux", dataContext);
target.SelectedIndex = 4;
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
dataContext = target.ContentPart.DataContext;
Assert.Equal("Base", dataContext);
}
@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests
};
ApplyTemplate(target);
target.ContentPart.UpdateChild();
((ContentPresenter)target.ContentPart).UpdateChild();
var content = Assert.IsType<TextBlock>(target.ContentPart.Child);
Assert.Equal("bar", content.Tag);

3
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -202,8 +202,9 @@ namespace Avalonia.Controls.UnitTests
target.Template = CreateTemplate();
target.Content = child;
target.ApplyTemplate();
Assert.Throws<InvalidOperationException>(() => target.ApplyTemplate());
Assert.Throws<InvalidOperationException>(() => target.Presenter.ApplyTemplate());
}
}

1
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -142,6 +142,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
window.DataContext = new { Foo = "foo" };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal("foo", border.DataContext);
}

3
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@ -73,6 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal("border2", button.Content);
}
@ -169,6 +170,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal("border1", button.Content);
}
@ -293,6 +295,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal("title", button.Content);
}

46
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

@ -1,7 +1,6 @@
// 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 Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -12,45 +11,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public class EventTests : XamlTestBase
{
[Fact]
public void Event_Is_Attached()
public void Event_Is_Assigned()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
loader.Load(xaml, rootInstance: target);
RaiseClick(target);
Assert.True(target.Clicked);
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.ClickEvent,
});
Assert.True(target.WasClicked);
}
[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
public void Attached_Event_Is_Assigned()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Gestures.Tapped='OnTapped'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
}
loader.Load(xaml, rootInstance: target);
private void RaiseClick(MyButton target)
{
target.RaiseEvent(new KeyEventArgs
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.KeyDownEvent,
Key = Key.Enter,
RoutedEvent = Gestures.TappedEvent,
});
Assert.True(target.WasTapped);
}
[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
}
public class MyButton : Button
{
public bool Clicked { get; private set; }
public bool WasClicked { get; private set; }
public bool WasTapped { get; private set; }
public void OnClick(object sender, RoutedEventArgs e)
{
Clicked = true;
}
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
}
}
}

112
tests/Avalonia.Visuals.UnitTests/VectorTests.cs

@ -0,0 +1,112 @@
// 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 Xunit;
using Avalonia;
using System;
namespace Avalonia.Visuals.UnitTests
{
public class VectorTests
{
[Fact]
public void Length_Should_Return_Correct_Length_Of_Vector()
{
var vector = new Vector(2, 4);
var length = Math.Sqrt(2 * 2 + 4 * 4);
Assert.Equal(length, vector.Length);
}
[Fact]
public void Length_Squared_Should_Return_Correct_Length_Of_Vector()
{
var vectorA = new Vector(2, 4);
var squaredLengthA = 2 * 2 + 4 * 4;
Assert.Equal(squaredLengthA, vectorA.SquaredLength);
}
[Fact]
public void Normalize_Should_Return_Normalized_Vector()
{
// the length of a normalized vector must be 1
var vectorA = new Vector(13, 84);
var vectorB = new Vector(-34, 345);
var vectorC = new Vector(-34, -84);
Assert.Equal(1.0, vectorA.Normalize().Length);
Assert.Equal(1.0, vectorB.Normalize().Length);
Assert.Equal(1.0, vectorC.Normalize().Length);
}
[Fact]
public void Negate_Should_Return_Negated_Vector()
{
var vector = new Vector(2, 4);
var negated = new Vector(-2, -4);
Assert.Equal(negated, vector.Negate());
}
[Fact]
public void Dot_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(66.0, Vector.Dot(a, b));
}
[Fact]
public void Cross_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(-112.0, Vector.Cross(a, b));
}
[Fact]
public void Divied_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(5, 2);
var expected = new Vector(2, 1);
Assert.Equal(expected, Vector.Divide(a, b));
}
[Fact]
public void Divied_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(5, 1);
Assert.Equal(expected, Vector.Divide(vector, 2));
}
[Fact]
public void Multiply_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(2, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(a, b));
}
[Fact]
public void Multiply_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(vector, 2));
}
}
}
Loading…
Cancel
Save