diff --git a/.gitignore b/.gitignore index 971c945246..b5a46e16f4 100644 --- a/.gitignore +++ b/.gitignore @@ -198,3 +198,11 @@ info.plist build-intermediate obj-Direct2D1/ obj-Skia/ + +################## +# Vim +################## +.vim +coc-settings.json +.ccls-cache +.ccls diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f4fbb0d50..7e3532ee23 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: - job: Windows pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' steps: - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build-native.sh b/build-native.sh old mode 100644 new mode 100755 diff --git a/build/Base.props b/build/Base.props index e565ab1664..a60373ebb3 100644 --- a/build/Base.props +++ b/build/Base.props @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 4f0b1f0a5b..76abcf6912 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.1 + 0.8.999 Copyright 2019 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 7486aaad69..971bcf24f8 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -26,7 +26,7 @@ const int kVK_ANSI_3 = 0x14; const int kVK_ANSI_4 = 0x15; const int kVK_ANSI_6 = 0x16; const int kVK_ANSI_5 = 0x17; -//const int kVK_ANSI_Equal = 0x18; +const int kVK_ANSI_Equal = 0x18; const int kVK_ANSI_9 = 0x19; const int kVK_ANSI_7 = 0x1A; const int kVK_ANSI_Minus = 0x1B; @@ -45,11 +45,11 @@ const int kVK_ANSI_K = 0x28; const int kVK_ANSI_Semicolon = 0x29; const int kVK_ANSI_Backslash = 0x2A; const int kVK_ANSI_Comma = 0x2B; -//const int kVK_ANSI_Slash = 0x2C; +const int kVK_ANSI_Slash = 0x2C; const int kVK_ANSI_N = 0x2D; const int kVK_ANSI_M = 0x2E; const int kVK_ANSI_Period = 0x2F; -//const int kVK_ANSI_Grave = 0x32; +const int kVK_ANSI_Grave = 0x32; const int kVK_ANSI_KeypadDecimal = 0x41; const int kVK_ANSI_KeypadMultiply = 0x43; const int kVK_ANSI_KeypadPlus = 0x45; @@ -57,7 +57,7 @@ const int kVK_ANSI_KeypadClear = 0x47; const int kVK_ANSI_KeypadDivide = 0x4B; const int kVK_ANSI_KeypadEnter = 0x4C; const int kVK_ANSI_KeypadMinus = 0x4E; -//const int kVK_ANSI_KeypadEquals = 0x51; +const int kVK_ANSI_KeypadEquals = 0x51; const int kVK_ANSI_Keypad0 = 0x52; const int kVK_ANSI_Keypad1 = 0x53; const int kVK_ANSI_Keypad2 = 0x54; @@ -121,7 +121,7 @@ const int kVK_UpArrow = 0x7E; //const int kVK_JIS_Underscore = 0x5E; //const int kVK_JIS_KeypadComma = 0x5F; //const int kVK_JIS_Eisu = 0x66; -//const int kVK_JIS_Kana = 0x68; +const int kVK_JIS_Kana = 0x68; std::map s_KeyMap = { @@ -148,7 +148,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_4, D4}, {kVK_ANSI_6, D6}, {kVK_ANSI_5, D5}, - //{kVK_ANSI_Equal, ?}, + {kVK_ANSI_Equal, OemPlus}, {kVK_ANSI_9, D9}, {kVK_ANSI_7, D7}, {kVK_ANSI_Minus, OemMinus}, @@ -167,11 +167,11 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_Semicolon, OemSemicolon}, {kVK_ANSI_Backslash, OemBackslash}, {kVK_ANSI_Comma, OemComma}, - //{kVK_ANSI_Slash, ?}, + {kVK_ANSI_Slash, Oem2}, {kVK_ANSI_N, N}, {kVK_ANSI_M, M}, {kVK_ANSI_Period, OemPeriod}, - //{kVK_ANSI_Grave, ?}, + {kVK_ANSI_Grave, OemTilde}, {kVK_ANSI_KeypadDecimal, Decimal}, {kVK_ANSI_KeypadMultiply, Multiply}, {kVK_ANSI_KeypadPlus, OemPlus}, @@ -179,7 +179,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_KeypadDivide, Divide}, {kVK_ANSI_KeypadEnter, AvnKeyEnter}, {kVK_ANSI_KeypadMinus, OemMinus}, - //{kVK_ANSI_KeypadEquals, ?}, + {kVK_ANSI_KeypadEquals, OemPlus}, {kVK_ANSI_Keypad0, NumPad0}, {kVK_ANSI_Keypad1, NumPad1}, {kVK_ANSI_Keypad2, NumPad2}, @@ -237,5 +237,6 @@ const int kVK_UpArrow = 0x7E; {kVK_LeftArrow, Left}, {kVK_RightArrow, Right}, {kVK_DownArrow, Down}, - {kVK_UpArrow, Up} + {kVK_UpArrow, Up}, + {kVK_JIS_Kana, AvnKeyKanaMode}, }; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a99ac4d026..dd2f27116d 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,6 +89,29 @@ partial class Build : NukeBuild } + IReadOnlyCollection MsBuildCommon( + string projectFile, + Configure configurator = null) + { + return MSBuild(projectFile, c => + { + // This is required for VS2019 image on Azure Pipelines + if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) + { + var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); + if (javaSdk != null) + c = c.AddProperty("JavaSdkDirectory", javaSdk); + } + + c = c.AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal); + c = configurator?.Invoke(c) ?? c; + return c; + }); + } Target Clean => _ => _.Executes(() => { DeleteDirectories(Parameters.BuildDirs); @@ -105,13 +128,8 @@ partial class Build : NukeBuild .Executes(() => { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c + MsBuildCommon(Parameters.MSBuildSolution, c => c .SetArgumentConfigurator(a => a.Add("/r")) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) .AddTargets("Build") ); @@ -237,12 +255,7 @@ partial class Build : NukeBuild { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) + MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else DotNetPack(Parameters.MSBuildSolution, c => diff --git a/readme.md b/readme.md index 7a03fda384..ee44a0cc3f 100644 --- a/readme.md +++ b/readme.md @@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop ## Bleeding Edge Builds -Try out the latest build of Avalonia available for download here: -https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts - or use nightly build feeds as described here: https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index d8f0f39977..bc76a39f08 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 9c3d4fb3a1..0089ea3b8d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid44 + monoandroid80 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index c2c0cd4301..1b2b205d45 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml @@ -150,6 +150,7 @@ + diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 15de6c4d0d..44b47329ac 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -10,6 +10,8 @@ namespace Avalonia.Data { public static class BindingOperations { + public static readonly object DoNothing = new object(); + /// /// Applies an a property on an . /// diff --git a/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs new file mode 100644 index 0000000000..b190f06be5 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Avalonia.Data.Converters +{ + /// + /// A multi-value converter which calls + /// + public class StringFormatMultiValueConverter : IMultiValueConverter + { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// + public StringFormatMultiValueConverter(string format, IMultiValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// + public IMultiValueConverter Inner { get; } + + /// + /// Gets the format string. + /// + public string Format { get; } + + /// + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return Inner == null + ? string.Format(culture, Format, values.ToArray()) + : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture)); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index f1717bde3b..7f8396cdfa 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -114,6 +114,11 @@ namespace Avalonia.Data.Core /// public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + using (_inner.Subscribe(_ => { })) { var type = _inner.ResultType; @@ -126,6 +131,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = TypeUtilities.Default(type); @@ -186,6 +196,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == BindingOperations.DoNothing) + { + return value; + } + var notification = value as BindingNotification; if (notification == null) @@ -196,6 +211,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return converted; + } + notification = converted as BindingNotification; if (notification?.ErrorType == BindingErrorType.None) @@ -327,7 +347,18 @@ namespace Avalonia.Data.Core public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + var converted = _owner.ConvertValue(value); + + if (converted == BindingOperations.DoNothing) + { + return; + } + _owner._value = new WeakReference(converted); _owner.PublishNext(converted); } diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs new file mode 100644 index 0000000000..f321b2b2f1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// A task-like operation that is guaranteed to finish continuations synchronously, + /// can be used for parametrized one-shot events + /// + public struct SynchronousCompletionAsyncResult : INotifyCompletion + { + private readonly SynchronousCompletionAsyncResultSource _source; + private readonly T _result; + private readonly bool _isValid; + internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource source) + { + _source = source; + _result = default; + _isValid = true; + } + + public SynchronousCompletionAsyncResult(T result) + { + _result = result; + _source = null; + _isValid = true; + } + + static void ThrowNotInitialized() => + throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized"); + + public bool IsCompleted + { + get + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null || _source.IsCompleted; + } + } + + public T GetResult() + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null ? _result : _source.Result; + } + + + public void OnCompleted(Action continuation) + { + if (!_isValid) + ThrowNotInitialized(); + if (_source == null) + continuation(); + else + _source.OnCompleted(continuation); + } + + public SynchronousCompletionAsyncResult GetAwaiter() => this; + } + + /// + /// Source for incomplete SynchronousCompletionAsyncResult + /// + /// + public class SynchronousCompletionAsyncResultSource + { + private T _result; + internal bool IsCompleted { get; private set; } + public SynchronousCompletionAsyncResult AsyncResult => new SynchronousCompletionAsyncResult(this); + + internal T Result => IsCompleted ? + _result : + throw new InvalidOperationException("Asynchronous operation is not yet completed"); + + private List _continuations; + + internal void OnCompleted(Action continuation) + { + if(_continuations==null) + _continuations = new List(); + _continuations.Add(continuation); + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("Asynchronous operation is already completed"); + _result = result; + IsCompleted = true; + if(_continuations!=null) + foreach (var c in _continuations) + c(); + _continuations = null; + } + + public void TrySetResult(T result) + { + if(IsCompleted) + return; + SetResult(result); + } + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index c54d8e19a1..0dd52c9b48 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -75,9 +75,9 @@ namespace Avalonia.Build.Tasks .First(c => c.Parameters.Count == 1)); var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); - var rootServiceProviderField = asm.MainModule.ImportReference( - typeSystem.GetTypeReference(runtimeHelpers).Resolve().Fields - .First(x => x.Name == "RootServiceProviderV1")); + var createRootServiceProviderMethod = asm.MainModule.ImportReference( + typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods + .First(x => x.Name == "CreateRootServiceProviderV2")); var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); @@ -211,7 +211,7 @@ namespace Avalonia.Build.Tasks trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); classTypeDefinition.Methods.Add(trampoline); - var regularStart = Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField); + var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart)); @@ -307,7 +307,7 @@ namespace Avalonia.Build.Tasks i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor)); else { - i.Add(Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField)); + i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod)); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs index d4c3b27f7a..2963c019cc 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs @@ -6,7 +6,7 @@ using System; namespace Avalonia.Controls.ApplicationLifetimes { /// - /// Contains the arguments for the event. + /// Contains the arguments for the event. /// public class ControlledApplicationLifetimeExitEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs index 4c08712707..423832793e 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Avalonia.Controls.ApplicationLifetimes { /// - /// Contains the arguments for the event. + /// Contains the arguments for the event. /// public class ControlledApplicationLifetimeStartupEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b87e10d284..473c4fe21b 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -795,7 +795,7 @@ namespace Avalonia.Controls var template = new FuncDataTemplate( typeof(object), - o => + (o, _) => { var control = new ContentControl(); control.Bind(ContentControl.ContentProperty, value); diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index e147fe1a52..8e23555c2d 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -1,7 +1,12 @@ +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + namespace Avalonia.Controls { - using System; - /// /// Defines the available docking modes for a control in a . /// @@ -70,107 +75,137 @@ namespace Avalonia.Controls set { SetValue(LastChildFillProperty, value); } } - /// + /// + /// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout. + /// + /// + /// Children are measured based on their sizing properties and . + /// Each child is allowed to consume all of the space on the side on which it is docked; Left/Right docked + /// children are granted all vertical space for their entire width, and Top/Bottom docked children are + /// granted all horizontal space for their entire height. + /// + /// Constraint size is an "upper limit" that the return value should not exceed. + /// The Panel's desired size. protected override Size MeasureOverride(Size constraint) { - double usedWidth = 0.0; - double usedHeight = 0.0; - double maximumWidth = 0.0; - double maximumHeight = 0.0; + var children = Children; + + double parentWidth = 0; // Our current required width due to children thus far. + double parentHeight = 0; // Our current required height due to children thus far. + double accumulatedWidth = 0; // Total width consumed by children. + double accumulatedHeight = 0; // Total height consumed by children. - // Measure each of the Children - foreach (Control element in Children) + for (int i = 0, count = children.Count; i < count; ++i) { - // Get the child's desired size - Size remainingSize = new Size( - Math.Max(0.0, constraint.Width - usedWidth), - Math.Max(0.0, constraint.Height - usedHeight)); - element.Measure(remainingSize); - Size desiredSize = element.DesiredSize; - - // Decrease the remaining space for the rest of the children - switch (GetDock(element)) + var child = children[i]; + Size childConstraint; // Contains the suggested input constraint for this child. + Size childDesiredSize; // Contains the return size from child measure. + + if (child == null) + { continue; } + + // Child constraint is the remaining size; this is total size minus size consumed by previous children. + childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), + Math.Max(0.0, constraint.Height - accumulatedHeight)); + + // Measure child. + child.Measure(childConstraint); + childDesiredSize = child.DesiredSize; + + // Now, we adjust: + // 1. Size consumed by children (accumulatedSize). This will be used when computing subsequent + // children to determine how much space is remaining for them. + // 2. Parent size implied by this child (parentSize) when added to the current children (accumulatedSize). + // This is different from the size above in one respect: A Dock.Left child implies a height, but does + // not actually consume any height for subsequent children. + // If we accumulate size in a given dimension, the next child (or the end conditions after the child loop) + // will deal with computing our minimum size (parentSize) due to that accumulation. + // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does + // not accumulate: Width for Top/Bottom, Height for Left/Right. + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: case Dock.Right: - maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height); - usedWidth += desiredSize.Width; + parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); + accumulatedWidth += childDesiredSize.Width; break; + case Dock.Top: case Dock.Bottom: - maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width); - usedHeight += desiredSize.Height; + parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); + accumulatedHeight += childDesiredSize.Height; break; } } - maximumWidth = Math.Max(maximumWidth, usedWidth); - maximumHeight = Math.Max(maximumHeight, usedHeight); - return new Size(maximumWidth, maximumHeight); + // Make sure the final accumulated size is reflected in parentSize. + parentWidth = Math.Max(parentWidth, accumulatedWidth); + parentHeight = Math.Max(parentHeight, accumulatedHeight); + + return (new Size(parentWidth, parentHeight)); } - /// + /// + /// DockPanel computes a position and final size for each of its children based upon their + /// enum and sizing properties. + /// + /// Size that DockPanel will assume to position children. protected override Size ArrangeOverride(Size arrangeSize) { - double left = 0.0; - double top = 0.0; - double right = 0.0; - double bottom = 0.0; - - // Arrange each of the Children var children = Children; - int dockedCount = children.Count - (LastChildFill ? 1 : 0); - int index = 0; + int totalChildrenCount = children.Count; + int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0); + + double accumulatedLeft = 0; + double accumulatedTop = 0; + double accumulatedRight = 0; + double accumulatedBottom = 0; - foreach (Control element in children) + for (int i = 0; i < totalChildrenCount; ++i) { - // Determine the remaining space left to arrange the element - Rect remainingRect = new Rect( - left, - top, - Math.Max(0.0, arrangeSize.Width - left - right), - Math.Max(0.0, arrangeSize.Height - top - bottom)); - - // Trim the remaining Rect to the docked size of the element - // (unless the element should fill the remaining space because - // of LastChildFill) - if (index < dockedCount) + var child = children[i]; + if (child == null) + { continue; } + + Size childDesiredSize = child.DesiredSize; + Rect rcChild = new Rect( + accumulatedLeft, + accumulatedTop, + Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)), + Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom))); + + if (i < nonFillChildrenCount) { - Size desiredSize = element.DesiredSize; - switch (GetDock(element)) + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: - left += desiredSize.Width; - remainingRect = remainingRect.WithWidth(desiredSize.Width); - break; - case Dock.Top: - top += desiredSize.Height; - remainingRect = remainingRect.WithHeight(desiredSize.Height); + accumulatedLeft += childDesiredSize.Width; + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + case Dock.Right: - right += desiredSize.Width; - remainingRect = new Rect( - Math.Max(0.0, arrangeSize.Width - right), - remainingRect.Y, - desiredSize.Width, - remainingRect.Height); + accumulatedRight += childDesiredSize.Width; + rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight)); + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + + case Dock.Top: + accumulatedTop += childDesiredSize.Height; + rcChild = rcChild.WithHeight(childDesiredSize.Height); + break; + case Dock.Bottom: - bottom += desiredSize.Height; - remainingRect = new Rect( - remainingRect.X, - Math.Max(0.0, arrangeSize.Height - bottom), - remainingRect.Width, - desiredSize.Height); + accumulatedBottom += childDesiredSize.Height; + rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom)); + rcChild = rcChild.WithHeight(childDesiredSize.Height); break; } } - element.Arrange(remainingRect); - index++; + child.Arrange(rcChild); } - return arrangeSize; + return (arrangeSize); } } } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 83d1c4aae3..2d48a7d33b 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -8,7 +8,7 @@ using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { - public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable + public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable { public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) { @@ -51,25 +51,6 @@ namespace Avalonia.Controls.Embedding return rv; } - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); - Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() => PlatformImpl?.Dispose(); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index c4f83ffd54..d326ab5734 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -30,25 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen init.EndInit(); } } - - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 304c86dbf7..43d1108fb9 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -92,7 +92,6 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - NameScope.SetNameScope((Control)(object)result, new NameScope()); Index.Add(item, result); return result; @@ -123,11 +122,20 @@ namespace Avalonia.Controls.Generators return false; } + class WrapperTreeDataTemplate : ITreeDataTemplate + { + private readonly IDataTemplate _inner; + public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; + public IControl Build(object param) => _inner.Build(param); + public bool SupportsRecycling => _inner.SupportsRecycling; + public bool Match(object data) => _inner.Match(data); + public InstancedBinding ItemsSelector(object item) => null; + } + private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; - var treeTemplate = template as ITreeDataTemplate ?? - new FuncTreeDataTemplate(typeof(object), template.Build, x => null); + var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); return treeTemplate; } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3dfeae52a4..a292ff7d0a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -36,6 +36,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemCountProperty = + AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount); + /// /// Defines the property. /// @@ -55,6 +61,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(MemberSelector)); private IEnumerable _items = new AvaloniaList(); + private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; private IDisposable _itemsCollectionChangedSubscription; @@ -110,10 +117,13 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + /// + /// Gets the number of items in . + /// public int ItemCount { - get; - private set; + get => _itemCount; + private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); } /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 041b81155a..3150b6be91 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -84,6 +84,16 @@ namespace Avalonia.Controls set { SetValue(VirtualizationModeProperty, value); } } + /// + /// Selects all items in the . + /// + public new void SelectAll() => base.SelectAll(); + + /// + /// Deselects all items in the . + /// + public new void UnselectAll() => base.UnselectAll(); + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -118,7 +128,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 49f268c128..c2690d503d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -325,12 +325,6 @@ namespace Avalonia.Controls.Presenters { _dataTemplate = dataTemplate; newChild = _dataTemplate.Build(content); - - // Give the new control its own name scope. - if (newChild is Control controlResult) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } } } else diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7b91d6235d..91a9fa7e40 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -12,6 +12,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Logging; using Avalonia.Styling; using Avalonia.VisualTree; @@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); + private readonly Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; private IList _selectedItems; @@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action notifyWrapper) => - { - var old = backing; - var effective = (val >= 0 && val < Items?.Cast().Count()) ? val : -1; - - if (old != effective) - { - backing = effective; - notifyWrapper(() => - RaisePropertyChanged( - SelectedIndexProperty, - old, - effective, - BindingPriority.LocalValue)); - SelectedItem = ElementAt(Items, effective); - } - }, value); + var effective = (value >= 0 && value < ItemCount) ? value : -1; + UpdateSelectedItem(effective); } else { @@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - var index = IndexOf(Items, val); - var effective = index != -1 ? val : null; - - if (!object.Equals(effective, old)) - { - backing = effective; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - effective, - BindingPriority.LocalValue)); - - SelectedIndex = index; - - if (effective != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != effective) - { - _syncingSelectedItems = true; - SelectedItems.Clear(); - SelectedItems.Add(effective); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - } - }, value); + UpdateSelectedItem(IndexOf(Items, value)); } else { @@ -354,31 +307,23 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } + else + { + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + UpdateSelectedItem(_selection.First(), false); + } break; case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - var selectedIndex = SelectedIndex; - - if (selectedIndex >= e.OldStartingIndex && - selectedIndex < e.OldStartingIndex + e.OldItems.Count) - { - if (!AlwaysSelected) - { - selectedIndex = SelectedIndex = -1; - } - else - { - LostSelection(); - } - } + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + UpdateSelectedItem(_selection.First(), false); + ResetSelectedItems(); + break; - var items = Items?.Cast(); - if (selectedIndex >= items.Count()) - { - selectedIndex = SelectedIndex = items.Count() - 1; - } + case NotifyCollectionChangedAction.Replace: + UpdateSelectedItem(SelectedIndex, false); + ResetSelectedItems(); break; case NotifyCollectionChangedAction.Move: @@ -439,11 +384,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - var ms = MemberSelector; - bool selected = ms == null ? - SelectedItems.Contains(i.Item) : - SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item)); - + bool selected = _selection.Contains(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -476,9 +417,12 @@ namespace Avalonia.Controls.Primitives var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + if (ItemCount > 0 && + Match(keymap.SelectAll) && + (((SelectionMode & SelectionMode.Multiple) != 0) || + (SelectionMode & SelectionMode.Toggle) != 0)) { - SynchronizeItems(SelectedItems, Items?.Cast()); + SelectAll(); e.Handled = true; } } @@ -520,6 +464,41 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Selects all items in the control. + /// + protected void SelectAll() + { + if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) + { + throw new NotSupportedException("Multiple selection is not enabled on this control."); + } + + UpdateSelectedItems(() => + { + _selection.Clear(); + + for (var i = 0; i < ItemCount; ++i) + { + _selection.Add(i); + } + + UpdateSelectedItem(0, false); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, true); + } + + ResetSelectedItems(); + }); + } + + /// + /// Deselects all items in the control. + /// + protected void UnselectAll() => UpdateSelectedItem(-1); + /// /// Updates the selection for an item based on user interaction. /// @@ -527,51 +506,83 @@ namespace Avalonia.Controls.Primitives /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( int index, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { if (index != -1) { if (select) { var mode = SelectionMode; - var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var multi = (mode & SelectionMode.Multiple) != 0; - var range = multi && SelectedIndex != -1 && rangeModifier; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; - if (!toggle && !range) + if (range) { - SelectedIndex = index; - } - else if (multi && range) - { - SynchronizeItems( - SelectedItems, - GetRange(Items, SelectedIndex, index)); + UpdateSelectedItems(() => + { + var start = SelectedIndex != -1 ? SelectedIndex : 0; + var step = start < index ? 1 : -1; + + _selection.Clear(); + + for (var i = start; i != index; i += step) + { + _selection.Add(i); + } + + _selection.Add(index); + + var first = Math.Min(start, index); + var last = Math.Max(start, index); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected( + container.Index, + container.Index >= first && container.Index <= last); + } + + ResetSelectedItems(); + }); } - else + else if (multi && toggle) { - var item = ElementAt(Items, index); - var i = SelectedItems.IndexOf(item); - - if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) - { - SelectedItems.Remove(item); - } - else + UpdateSelectedItems(() => { - if (multi) + if (!_selection.Contains(index)) { - SelectedItems.Add(item); + _selection.Add(index); + MarkItemSelected(index, true); + SelectedItems.Add(ElementAt(Items, index)); } else { - SelectedIndex = index; + _selection.Remove(index); + MarkItemSelected(index, false); + + if (index == _selectedIndex) + { + UpdateSelectedItem(_selection.First(), false); + } + + SelectedItems.Remove(ElementAt(Items, index)); } - } + }); + } + else if (toggle) + { + SelectedIndex = (SelectedIndex == index) ? -1 : index; + } + else + { + UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); } if (Presenter?.Panel != null) @@ -596,17 +607,19 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; if (index != -1) { - UpdateSelection(index, select, rangeModifier, toggleModifier); + UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton); } } @@ -618,6 +631,7 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -626,51 +640,20 @@ namespace Avalonia.Controls.Primitives IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelection(container, select, rangeModifier, toggleModifier); + UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton); return true; } return false; } - /// - /// Makes a list of objects equal another. - /// - /// The items collection. - /// The desired items. - internal static void SynchronizeItems(IList items, IEnumerable desired) - { - var index = 0; - - foreach (object item in desired) - { - int itemIndex = items.IndexOf(item); - - if (itemIndex == -1) - { - items.Insert(index, item); - } - else if(itemIndex != index) - { - items.RemoveAt(itemIndex); - items.Insert(index, item); - } - - ++index; - } - - while (index < items.Count) - { - items.RemoveAt(items.Count - 1); - } - } - /// /// Gets a range of items from an IEnumerable. /// @@ -678,17 +661,19 @@ namespace Avalonia.Controls.Primitives /// The index of the first item. /// The index of the last item. /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) + private static List GetRange(IEnumerable items, int first, int last) { var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; + var step = first > last ? -1 : 1; + var result = new List(); for (int i = first; i != last; i += step) { - yield return list[i]; + result.Add(list[i]); } - yield return list[last]; + result.Add(list[last]); + return result; } /// @@ -724,19 +709,14 @@ namespace Avalonia.Controls.Primitives private void LostSelection() { var items = Items?.Cast(); + var index = -1; if (items != null && AlwaysSelected) { - var index = Math.Min(SelectedIndex, items.Count() - 1); - - if (index > -1) - { - SelectedItem = items.ElementAt(index); - return; - } + index = Math.Min(SelectedIndex, items.Count() - 1); } - SelectedIndex = -1; + SelectedIndex = index; } /// @@ -793,7 +773,7 @@ namespace Avalonia.Controls.Primitives /// /// The item. /// Whether the item should be selected or deselected. - private void MarkItemSelected(object item, bool selected) + private int MarkItemSelected(object item, bool selected) { var index = IndexOf(Items, item); @@ -801,6 +781,21 @@ namespace Avalonia.Controls.Primitives { MarkItemSelected(index, selected); } + + return index; + } + + private void ResetSelectedItems() + { + UpdateSelectedItems(() => + { + SelectedItems.Clear(); + + foreach (var i in _selection) + { + SelectedItems.Add(ElementAt(Items, i)); + } + }); } /// @@ -810,95 +805,97 @@ namespace Avalonia.Controls.Primitives /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - var generator = ItemContainerGenerator; + if (_syncingSelectedItems) + { + return; + } + + void Add(IList newItems, IList addedItems = null) + { + foreach (var item in newItems) + { + var index = MarkItemSelected(item, true); + + if (index != -1 && _selection.Add(index) && addedItems != null) + { + addedItems.Add(item); + } + } + } + + void UpdateSelection() + { + if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || + (SelectedIndex == -1 && _selection.HasItems)) + { + _selectedIndex = _selection.First(); + _selectedItem = ElementAt(Items, _selectedIndex); + RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); + RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(_selectedIndex); + } + } + } + IList added = null; IList removed = null; switch (e.Action) { case NotifyCollectionChangedAction.Add: - SelectedItemsAdded(e.NewItems.Cast().ToList()); - - if (AutoScrollToSelectedItem) { - ScrollIntoView(e.NewItems[0]); + Add(e.NewItems); + UpdateSelection(); + added = e.NewItems; } - added = e.NewItems; break; case NotifyCollectionChangedAction.Remove: if (SelectedItems.Count == 0) { - if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } + SelectedIndex = -1; } foreach (var item in e.OldItems) { - MarkItemSelected(item, false); + var index = MarkItemSelected(item, false); + _selection.Remove(index); } removed = e.OldItems; break; + case NotifyCollectionChangedAction.Replace: + throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); + + case NotifyCollectionChangedAction.Move: + throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); + case NotifyCollectionChangedAction.Reset: - if (generator != null) { removed = new List(); + added = new List(); - foreach (var item in generator.Containers) + foreach (var index in _selection.ToList()) { - if (item?.ContainerControl != null) + var item = ElementAt(Items, index); + + if (!SelectedItems.Contains(item)) { - if (MarkContainerSelected(item.ContainerControl, false)) - { - removed.Add(item.Item); - } + MarkItemSelected(index, false); + removed.Add(item); + _selection.Remove(index); } } - } - if (SelectedItems.Count > 0) - { - _selectedItem = null; - SelectedItemsAdded(SelectedItems); - added = SelectedItems; - } - else if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } - - break; - - case NotifyCollectionChangedAction.Replace: - foreach (var item in e.OldItems) - { - MarkItemSelected(item, false); - } - - foreach (var item in e.NewItems) - { - MarkItemSelected(item, true); + Add(SelectedItems, added); + UpdateSelection(); } - if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) - { - var oldItem = SelectedItem; - var oldIndex = SelectedIndex; - var item = SelectedItems[0]; - var index = IndexOf(Items, item); - _selectedIndex = index; - _selectedItem = item; - RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue); - } - - added = e.NewItems; - removed = e.OldItems; break; } @@ -912,34 +909,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Called when items are added to the collection. - /// - /// The added items. - private void SelectedItemsAdded(IList items) - { - if (items.Count > 0) - { - foreach (var item in items) - { - MarkItemSelected(item, true); - } - - if (SelectedItem == null && !_syncingSelectedItems) - { - var index = IndexOf(Items, items[0]); - - if (index != -1) - { - _selectedItem = items[0]; - _selectedIndex = index; - RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue); - } - } - } - } - /// /// Subscribes to the CollectionChanged event, if any. /// @@ -970,6 +939,112 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Updates the selection due to a change to or + /// . + /// + /// The new selected index. + /// Whether to clear existing selection. + private void UpdateSelectedItem(int index, bool clear = true) + { + var oldIndex = _selectedIndex; + var oldItem = _selectedItem; + + if (index == -1 && AlwaysSelected) + { + index = Math.Min(SelectedIndex, ItemCount - 1); + } + + var item = ElementAt(Items, index); + var added = -1; + HashSet removed = null; + + _selectedIndex = index; + _selectedItem = item; + + if (oldIndex != index || _selection.HasMultiple) + { + if (clear) + { + removed = _selection.Clear(); + } + + if (index != -1) + { + if (_selection.Add(index)) + { + added = index; + } + + if (removed?.Contains(index) == true) + { + removed.Remove(index); + added = -1; + } + } + + if (removed != null) + { + foreach (var i in removed) + { + MarkItemSelected(i, false); + } + } + + MarkItemSelected(index, true); + + RaisePropertyChanged( + SelectedIndexProperty, + oldIndex, + index); + } + + if (!Equals(item, oldItem)) + { + RaisePropertyChanged( + SelectedItemProperty, + oldItem, + item); + } + + if (removed != null && index != -1) + { + removed.Remove(index); + } + + if (added != -1 || removed?.Count > 0) + { + ResetSelectedItems(); + + var e = new SelectionChangedEventArgs( + SelectionChangedEvent, + added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), + removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); + RaiseEvent(e); + } + } + + private void UpdateSelectedItems(Action action) + { + try + { + _syncingSelectedItems = true; + action(); + } + catch (Exception ex) + { + Logger.Error( + LogArea.Property, + this, + "Error thrown updating SelectedItems: {Error}", + ex); + } + finally + { + _syncingSelectedItems = false; + } + } + private void UpdateFinished() { if (_updateSelectedIndex != int.MinValue) @@ -981,5 +1056,104 @@ namespace Avalonia.Controls.Primitives SelectedItems = _updateSelectedItems; } } + + private class Selection : IEnumerable + { + private readonly List _list = new List(); + private HashSet _set = new HashSet(); + + public bool HasItems => _set.Count > 0; + public bool HasMultiple => _set.Count > 1; + + public bool Add(int index) + { + if (index == -1) + { + throw new ArgumentException("Invalid index", "index"); + } + + if (_set.Add(index)) + { + _list.Add(index); + return true; + } + + return false; + } + + public bool Remove(int index) + { + if (_set.Remove(index)) + { + _list.RemoveAll(x => x == index); + return true; + } + + return false; + } + + public HashSet Clear() + { + var result = _set; + _list.Clear(); + _set = new HashSet(); + return result; + } + + public void ItemsInserted(int index, int count) + { + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index) + { + var newIndex = ix + count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public void ItemsRemoved(int index, int count) + { + var last = (index + count) - 1; + + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index && ix <= last) + { + _list.RemoveAt(i--); + } + else if (ix > last) + { + var newIndex = ix - count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public bool Contains(int index) => _set.Contains(index); + + public int First() => HasItems ? _list[0] : -1; + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 32e220b789..47c3240374 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -257,13 +257,14 @@ namespace Avalonia.Controls.Primitives { Logger.Verbose(LogArea.Control, this, "Creating control template"); - var child = template.Build(this); - var nameScope = new NameScope(); - NameScope.SetNameScope((Control)child, nameScope); + var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); - RegisterNames(child, nameScope); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); + + // Existing code kinda expect to see a NameScope even if it's empty + if (nameScope == null) + nameScope = new NameScope(); OnTemplateApplied(new TemplateAppliedEventArgs(nameScope)); } @@ -342,26 +343,5 @@ namespace Avalonia.Controls.Primitives } } } - - /// - /// Registers each control with its name scope. - /// - /// The control. - /// The name scope. - private void RegisterNames(IControl control, INameScope nameScope) - { - if (control.Name != null) - { - nameScope.Register(control.Name, control); - } - - if (control.TemplatedParent == this) - { - foreach (IControl child in control.GetLogicalChildren()) - { - RegisterNames(child, nameScope); - } - } - } } } diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs index 46e27ff4e1..ee593c28a8 100644 --- a/src/Avalonia.Controls/ShutdownMode.cs +++ b/src/Avalonia.Controls/ShutdownMode.cs @@ -1,10 +1,12 @@ // 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.Controls.ApplicationLifetimes; + namespace Avalonia.Controls { /// - /// Describes the possible values for . + /// Describes the possible values for . /// public enum ShutdownMode { diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 4c3c92309e..19458cc6b8 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -16,9 +16,15 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) + public FuncControlTemplate(Func build) : base(build) { } + + public new ControlTemplateResult Build(ITemplatedControl param) + { + var (control, scope) = BuildWithNameScope(param); + return new ControlTemplateResult(control, scope); + } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs index 8e49b51cb8..eec7a6030f 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs @@ -17,9 +17,9 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) - : base(x => build((T)x)) + public FuncControlTemplate(Func build) + : base((x, s) => build((T)x, s)) { } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 1d90fcd01e..204540e431 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Templates /// public static readonly FuncDataTemplate Default = new FuncDataTemplate( - data => + (data, s) => { if (data != null) { @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Type type, - Func build, + Func build, bool supportsRecycling = false) : this(o => IsInstance(o, type), build, supportsRecycling) { @@ -67,7 +67,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(build) { @@ -105,4 +105,4 @@ namespace Avalonia.Controls.Templates return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 9339aa6924..68b737928d 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Templates /// A function which when passed an object of returns a control. /// /// Whether the control can be recycled. - public FuncDataTemplate(Func build, bool supportsRecycling = false) + public FuncDataTemplate(Func build, bool supportsRecycling = false) : base(typeof(T), CastBuild(build), supportsRecycling) { } @@ -35,12 +35,30 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(CastMatch(match), CastBuild(build), supportsRecycling) { } + /// + /// Initializes a new instance of the class. + /// + /// + /// A function which determines whether the data template matches the specified data. + /// + /// + /// A function which when passed an object of returns a control. + /// + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) + : this(match, (a, _) => build(a), supportsRecycling) + { + } + /// /// Casts a strongly typed match function to a weakly typed one. /// @@ -57,9 +75,9 @@ namespace Avalonia.Controls.Templates /// The strong data type. /// The strongly typed function. /// The weakly typed function. - private static Func CastBuild(Func f) + private static Func CastBuild(Func f) { - return o => f((T)o); + return (o, s) => f((T)o, s); } } } diff --git a/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs new file mode 100644 index 0000000000..76bc298e30 --- /dev/null +++ b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Controls.Templates +{ + public static class FuncTemplateNameScopeExtensions + { + public static T RegisterInNameScope(this T control, INameScope scope) + where T : StyledElement + { + scope.Register(control.Name, control); + return control; + } + } +} diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs index b1ce63fdf1..e0608d0471 100644 --- a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls.Templates public class FuncTemplate : ITemplate where TControl : IControl { - private readonly Func _func; + private readonly Func _func; /// /// Initializes a new instance of the class. /// /// The function used to create the control. - public FuncTemplate(Func func) + public FuncTemplate(Func func) { Contract.Requires(func != null); @@ -35,7 +35,14 @@ namespace Avalonia.Controls.Templates /// public TControl Build(TParam param) { - return _func(param); + return BuildWithNameScope(param).control; + } + + protected (TControl control, INameScope nameScope) BuildWithNameScope(TParam param) + { + var scope = new NameScope(); + var rv = _func(param, scope); + return (rv, scope); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index e7c9cf8608..998ef0e9f2 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Type type, - Func build, + Func build, Func itemsSelector) : this(o => IsInstance(o, type), build, itemsSelector) { @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base(match, build) { diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index 4ca96f60bd..41f870ab42 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Templates /// items. /// public FuncTreeDataTemplate( - Func build, + Func build, Func itemsSelector) : base( typeof(T), @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base( CastMatch(match), @@ -65,6 +65,17 @@ namespace Avalonia.Controls.Templates return o => (o is T) && f((T)o); } + /// + /// Casts a function with a typed parameter to an untyped function. + /// + /// The result. + /// The typed function. + /// The untyped function. + private static Func Cast(Func f) + { + return (o, s) => f((T)o, s); + } + /// /// Casts a function with a typed parameter to an untyped function. /// diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 408329c450..a3ef0fa954 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -9,7 +9,25 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate { } -} \ No newline at end of file + + public class ControlTemplateResult + { + public IControl Control { get; } + public INameScope NameScope { get; } + + public ControlTemplateResult(IControl control, INameScope nameScope) + { + Control = control; + NameScope = nameScope; + } + + public void Deconstruct(out IControl control, out INameScope scope) + { + control = Control; + scope = NameScope; + } + } +} diff --git a/src/Avalonia.Controls/Templates/ITemplate`2.cs b/src/Avalonia.Controls/Templates/ITemplate`2.cs index f61292c64b..a0c0e88eca 100644 --- a/src/Avalonia.Controls/Templates/ITemplate`2.cs +++ b/src/Avalonia.Controls/Templates/ITemplate`2.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.Templates /// /// The type of the parameter. /// The type of control. - public interface ITemplate where TControl : IControl + public interface ITemplate { /// /// Creates the control. @@ -19,4 +19,4 @@ namespace Avalonia.Controls.Templates /// TControl Build(TParam param); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c3fbce1d83..888f4a2013 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -409,7 +409,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); e.Handled = true; } } @@ -521,7 +521,7 @@ namespace Avalonia.Controls } else if (multi && range) { - SelectingItemsControl.SynchronizeItems( + SynchronizeItems( SelectedItems, GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); } @@ -778,5 +778,27 @@ namespace Avalonia.Controls container.Classes.Set(":selected", selected); } } + + /// + /// Makes a list of objects equal another (though doesn't preserve order). + /// + /// The items collection. + /// The desired items. + private static void SynchronizeItems(IList items, IEnumerable desired) + { + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); + + foreach (var i in toRemove) + { + items.Remove(i); + } + + foreach (var i in toAdd) + { + items.Add(i); + } + } } } diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index e42ca5e0e6..4f15839821 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -9,40 +9,8 @@ namespace Avalonia.Controls /// /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic. /// - public class UserControl : ContentControl, IStyleable, INameScope + public class UserControl : ContentControl, IStyleable { - private readonly NameScope _nameScope = new NameScope(); - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 5c117f508b..d2793fe0dd 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls /// /// A top-level window. /// - public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope + public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { /// /// Defines the property. @@ -157,20 +157,6 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); } - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - /// /// Gets the platform-specific window implementation. /// @@ -494,24 +480,6 @@ namespace Avalonia.Controls } } - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } - /// protected override Size MeasureOverride(Size availableSize) { diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 4df1b39400..6f53d853c7 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -1,9 +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.Collections.Generic; -using System.Diagnostics; -using System.Linq; +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using Avalonia.Input; using Avalonia.Utilities; diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 718afc4a94..f556949cfa 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -9,12 +9,14 @@ namespace Avalonia.OpenGL public class GlInterface : GlInterfaceBase { public string Version { get; } + public string Vendor { get; } + public string Renderer { get; } public GlInterface(Func getProcAddress) : base(getProcAddress) { - var versionPtr = GetString(GlConsts.GL_VERSION); - if (versionPtr != IntPtr.Zero) - Version = Marshal.PtrToStringAnsi(versionPtr); + Version = GetString(GlConsts.GL_VERSION); + Renderer = GetString(GlConsts.GL_RENDERER); + Vendor = GetString(GlConsts.GL_VENDOR); } public GlInterface(Func n) : this(ConvertNative(n)) @@ -54,7 +56,15 @@ namespace Avalonia.OpenGL public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] - public GlGetString GetString { get; } + public GlGetString GetStringNative { get; } + + public string GetString(int v) + { + var ptr = GetStringNative(v); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } public delegate void GlGetIntegerv(int name, out int rv); [GlEntryPoint("glGetIntegerv")] diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index 3f41f54363..4881c77034 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,7 +16,7 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate((x, _) => { var control = new ViewModelViewHost(); var context = control.GetObservable(Control.DataContextProperty); @@ -52,4 +52,4 @@ namespace Avalonia.ReactiveUI return true; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs new file mode 100644 index 0000000000..a4f14a4138 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -0,0 +1,87 @@ +// 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; +using Avalonia.VisualTree; +using Avalonia.Controls; +using System.Threading; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Reactive; +using ReactiveUI; +using System; +using Avalonia.Controls.ApplicationLifetimes; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for + /// Avalonia applications. Call its constructor in your app's composition root, + /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. + /// + public sealed class AutoSuspendHelper : IEnableLogger, IDisposable + { + private readonly Subject _shouldPersistState = new Subject(); + private readonly Subject _isLaunchingNew = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// Pass in the Application.ApplicationLifetime property. + public AutoSuspendHelper(IApplicationLifetime lifetime) + { + RxApp.SuspensionHost.IsResuming = Observable.Never(); + RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; + + if (lifetime is IControlledApplicationLifetime controlled) + { + this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); + controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); + RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; + } + else if (lifetime != null) + { + var type = lifetime.GetType().FullName; + var message = $"Don't know how to detect app exit event for {type}."; + throw new NotSupportedException(message); + } + else + { + var message = "ApplicationLifetime is null. " + + "Ensure you are initializing AutoSuspendHelper " + + "when Avalonia application initialization is completed."; + throw new ArgumentNullException(message); + } + + var errored = new Subject(); + AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); + RxApp.SuspensionHost.ShouldInvalidateState = errored; + } + + /// + /// Call this method in your App.OnFrameworkInitializationCompleted method. + /// + public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); + + /// + /// Disposes internally stored observers. + /// + public void Dispose() + { + _shouldPersistState.Dispose(); + _isLaunchingNew.Dispose(); + } + + private void OnControlledApplicationLifetimeExit() + { + this.Log().Debug("Received IControlledApplicationLifetime exit event."); + var manual = new ManualResetEvent(false); + _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); + + manual.WaitOne(); + this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); + } + } +} diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs new file mode 100644 index 0000000000..e6707e71db --- /dev/null +++ b/src/Avalonia.Styling/Controls/ChildNameScope.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class ChildNameScope : INameScope + { + private readonly INameScope _parentScope; + private readonly NameScope _inner = new NameScope(); + + public ChildNameScope(INameScope parentScope) + { + _parentScope = parentScope; + } + + public void Register(string name, object element) => _inner.Register(name, element); + + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + // Not found and both current and parent scope are in completed state + if(IsCompleted) + return new SynchronousCompletionAsyncResult(null); + return DoFindAsync(name); + } + + public SynchronousCompletionAsyncResult DoFindAsync(string name) + { + var src = new SynchronousCompletionAsyncResultSource(); + + void ParentSearch() + { + var parentSearch = _parentScope.FindAsync(name); + if (parentSearch.IsCompleted) + src.SetResult(parentSearch.GetResult()); + else + parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); + } + if (!_inner.IsCompleted) + { + // Guaranteed to be incomplete at this point + var innerSearch = _inner.FindAsync(name); + innerSearch.OnCompleted(() => + { + var value = innerSearch.GetResult(); + if (value != null) + src.SetResult(value); + else ParentSearch(); + }); + } + else + ParentSearch(); + + return src.AsyncResult; + } + + public object Find(string name) + { + var found = _inner.Find(name); + if (found != null) + return found; + if (_inner.IsCompleted) + return _parentScope.Find(name); + return null; + } + + public void Complete() => _inner.Complete(); + + public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; + } +} diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index e4ff90c573..b4f4c8e57b 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/src/Avalonia.Styling/Controls/INameScope.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Threading.Tasks; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -10,16 +12,6 @@ namespace Avalonia.Controls /// public interface INameScope { - /// - /// Raised when an element is registered with the name scope. - /// - event EventHandler Registered; - - /// - /// Raised when an element is unregistered with the name scope. - /// - event EventHandler Unregistered; - /// /// Registers an element in the name scope. /// @@ -28,16 +20,30 @@ namespace Avalonia.Controls void Register(string name, object element); /// - /// Finds a named element in the name scope. + /// Finds a named element in the name scope, waits for the scope to be completely populated before returning null + /// Returned task is configured to run any continuations synchronously. + /// + /// The name. + /// The element, or null if the name was not found. + SynchronousCompletionAsyncResult FindAsync(string name); + + /// + /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack /// /// The name. /// The element, or null if the name was not found. object Find(string name); /// - /// Unregisters an element with the name scope. + /// Marks the name scope as completed, no further registrations will be allowed /// - /// The name. - void Unregister(string name); + void Complete(); + + /// + /// Returns whether further registrations are allowed on the scope + /// + bool IsCompleted { get; } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index e3a29af541..ec01a53cfd 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.LogicalTree; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -18,44 +20,14 @@ namespace Avalonia.Controls public static readonly AttachedProperty NameScopeProperty = AvaloniaProperty.RegisterAttached("NameScope"); + /// + public bool IsCompleted { get; private set; } + private readonly Dictionary _inner = new Dictionary(); - /// - /// Raised when an element is registered with the name scope. - /// - public event EventHandler Registered; - - /// - /// Raised when an element is unregistered with the name scope. - /// - public event EventHandler Unregistered; - - /// - /// Finds the containing name scope for a styled element. - /// - /// The styled element. - /// The containing name scope. - public static INameScope FindNameScope(StyledElement styled) - { - Contract.Requires(styled != null); - - INameScope result; - - while (styled != null) - { - result = styled as INameScope ?? GetNameScope(styled); - - if (result != null) - { - return result; - } - - styled = (styled as ILogical)?.LogicalParent as StyledElement; - } - - return null; - } - + private readonly Dictionary> _pendingSearches = + new Dictionary>(); + /// /// Gets the value of the attached on a styled element. /// @@ -80,13 +52,11 @@ namespace Avalonia.Controls styled.SetValue(NameScopeProperty, value); } - /// - /// Registers an element with the name scope. - /// - /// The element name. - /// The element. + /// public void Register(string name, object element) { + if (IsCompleted) + throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); Contract.Requires(name != null); Contract.Requires(element != null); @@ -102,15 +72,29 @@ namespace Avalonia.Controls else { _inner.Add(name, element); - Registered?.Invoke(this, new NameScopeEventArgs(name, element)); + if (_pendingSearches.TryGetValue(name, out var tcs)) + { + _pendingSearches.Remove(name); + tcs.SetResult(element); + } } } - /// - /// Finds a named element in the name scope. - /// - /// The name. - /// The element, or null if the name was not found. + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + if (IsCompleted) + return new SynchronousCompletionAsyncResult((object)null); + if (!_pendingSearches.TryGetValue(name, out var tcs)) + // We are intentionally running continuations synchronously here + _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); + + return tcs.AsyncResult; + } + + /// public object Find(string name) { Contract.Requires(name != null); @@ -120,21 +104,14 @@ namespace Avalonia.Controls return result; } - /// - /// Unregisters an element with the name scope. - /// - /// The name. - public void Unregister(string name) + public void Complete() { - Contract.Requires(name != null); - - object element; - - if (_inner.TryGetValue(name, out element)) - { - _inner.Remove(name); - Unregistered?.Invoke(this, new NameScopeEventArgs(name, element)); - } + IsCompleted = true; + foreach (var kp in _pendingSearches) + kp.Value.TrySetResult(null); + _pendingSearches.Clear(); } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs index 991a97a614..5921acd3b6 100644 --- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs @@ -37,6 +37,25 @@ namespace Avalonia.Controls return (T)result; } + /// + /// Finds a named element in an . + /// + /// The element type. + /// The control to take the name scope from. + /// The name. + /// The named element or null if not found. + public static T Find(this ILogical anchor, string name) + where T : class + { + Contract.Requires(anchor != null); + Contract.Requires(name != null); + var styledAnchor = anchor as StyledElement; + if (styledAnchor == null) + return null; + var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope(styledAnchor); + return nameScope?.Find(name); + } + /// /// Gets a named element from an or throws if no element of the /// requested name was found. @@ -67,6 +86,28 @@ namespace Avalonia.Controls return (T)result; } + /// + /// Gets a named element from an or throws if no element of the + /// requested name was found. + /// + /// The element type. + /// The control to take the name scope from. + /// The name. + /// The named element. + public static T Get(this ILogical anchor, string name) + where T : class + { + Contract.Requires(anchor != null); + Contract.Requires(name != null); + + var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope((StyledElement)anchor); + if (nameScope == null) + throw new InvalidOperationException( + "The control doesn't have an associated name scope, probably no registrations has been done yet"); + + return nameScope.Get(name); + } + public static INameScope FindNameScope(this ILogical control) { Contract.Requires(control != null); diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs new file mode 100644 index 0000000000..719cf9344b --- /dev/null +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Threading.Tasks; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.LogicalTree; +using Avalonia.Reactive; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class NameScopeLocator + { + /// + /// Tracks a named control relative to another control. + /// + /// + /// The control relative from which the other control should be found. + /// + /// The name of the control to find. + public static IObservable Track(INameScope scope, string name) + { + return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name)); + } + + // This class is implemented in such weird way because for some reason + // our binding system doesn't expect OnCompleted to be ever called and + // seems to treat it as binding cancellation or something + + private class NeverEndingSynchronousCompletionAsyncResultObservable : IObservable + { + private T _value; + private SynchronousCompletionAsyncResult? _asyncResult; + + public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult task) + { + if (task.IsCompleted) + _value = task.GetResult(); + else + _asyncResult = task; + } + + public IDisposable Subscribe(IObserver observer) + { + if (_asyncResult?.IsCompleted == true) + { + _value = _asyncResult.Value.GetResult(); + _asyncResult = null; + } + + if (_asyncResult != null) + _asyncResult.Value.OnCompleted(() => + { + observer.OnNext(_asyncResult.Value.GetResult()); + }); + else + observer.OnNext(_value); + + return Disposable.Empty; + } + } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 7186143bf9..491f38c153 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -15,18 +15,6 @@ namespace Avalonia.LogicalTree /// public static class ControlLocator { - /// - /// Tracks a named control relative to another control. - /// - /// - /// The control relative from which the other control should be found. - /// - /// The name of the control to find. - public static IObservable Track(ILogical relativeTo, string name) - { - return new ControlTracker(relativeTo, name); - } - public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) { return new ControlTracker(relativeTo, ancestorLevel, ancestorType); @@ -35,18 +23,10 @@ namespace Avalonia.LogicalTree private class ControlTracker : LightweightObservableBase { private readonly ILogical _relativeTo; - private readonly string _name; private readonly int _ancestorLevel; private readonly Type _ancestorType; - INameScope _nameScope; ILogical _value; - public ControlTracker(ILogical relativeTo, string name) - { - _relativeTo = relativeTo; - _name = name; - } - public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { _relativeTo = relativeTo; @@ -66,12 +46,6 @@ namespace Avalonia.LogicalTree _relativeTo.AttachedToLogicalTree -= Attached; _relativeTo.DetachedFromLogicalTree -= Detached; - if (_nameScope != null) - { - _nameScope.Registered -= Registered; - _nameScope.Unregistered -= Unregistered; - } - _value = null; } @@ -88,57 +62,15 @@ namespace Avalonia.LogicalTree private void Detached(object sender, LogicalTreeAttachmentEventArgs e) { - if (_nameScope != null) - { - _nameScope.Registered -= Registered; - _nameScope.Unregistered -= Unregistered; - } - _value = null; PublishNext(null); } - private void Registered(object sender, NameScopeEventArgs e) - { - if (e.Name == _name && e.Element is ILogical logical) - { - _value = logical; - PublishNext(logical); - } - } - - private void Unregistered(object sender, NameScopeEventArgs e) - { - if (e.Name == _name) - { - _value = null; - PublishNext(null); - } - } - private void Update() { - if (_name != null) - { - _nameScope = _relativeTo.FindNameScope(); - - if (_nameScope != null) - { - _nameScope.Registered += Registered; - _nameScope.Unregistered += Unregistered; - _value = _nameScope.Find(_name); - } - else - { - _value = null; - } - } - else - { - _value = _relativeTo.GetLogicalAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(_ancestorLevel); - } + _value = _relativeTo.GetLogicalAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); } } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 146a4c75e7..b4aa89d5bf 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -61,7 +61,6 @@ namespace Avalonia private readonly Classes _classes = new Classes(); private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; - private INameScope _nameScope; private IResourceDictionary _resources; private Styles _styles; private bool _styled; @@ -82,7 +81,6 @@ namespace Avalonia /// public StyledElement() { - _nameScope = this as INameScope; _isAttachedToLogicalTree = this is IStyleRoot; } @@ -381,7 +379,6 @@ namespace Avalonia { if (_initCount == 0 && (!_styled || force)) { - RegisterWithNameScope(); ApplyStyling(); _styled = true; } @@ -675,19 +672,6 @@ namespace Avalonia AvaloniaLocator.Current.GetService()?.ApplyStyles(this); } - private void RegisterWithNameScope() - { - if (_nameScope == null) - { - _nameScope = NameScope.GetNameScope(this) ?? ((StyledElement)Parent)?._nameScope; - } - - if (Name != null) - { - _nameScope?.Register(Name, this); - } - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -724,11 +708,6 @@ namespace Avalonia { if (_isAttachedToLogicalTree) { - if (Name != null) - { - _nameScope?.Unregister(Name); - } - _isAttachedToLogicalTree = false; _styleDetach.OnNext(this); OnDetachedFromLogicalTree(e); diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 3702259f35..9312d38c51 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -99,7 +99,6 @@ namespace Avalonia.Styling if (template != null && !isPropertyOfTypeITemplate) { var materialized = template.Build(); - NameScope.SetNameScope((StyledElement)materialized, new NameScope()); value = materialized; } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 789bb6ffd3..a4563110a9 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -180,7 +180,7 @@ namespace Avalonia.Styling /// public bool TryGetResource(object key, out object value) { - if (_resources != null && _resources.TryGetValue(key, out value)) + if (_resources != null && _resources.TryGetResource(key, out value)) { return true; } diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index eb2b6789e1..3d525955b4 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -18,6 +18,5 @@ - diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index 0ff7862ab6..e82934fbad 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -8,8 +8,8 @@ namespace Avalonia.Rendering { public class RenderLayers : IEnumerable { - private List _inner = new List(); - private Dictionary _index = new Dictionary(); + private readonly List _inner = new List(); + private readonly Dictionary _index = new Dictionary(); public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -56,6 +56,7 @@ namespace Avalonia.Rendering } _index.Clear(); + _inner.Clear(); } public bool TryGetValue(IVisual layerRoot, out RenderLayer value) diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 5602b33280..04f2a7137c 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -90,6 +90,19 @@ namespace Avalonia.X11.Glx GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); + if (GlInterface.Renderer == null) + throw new OpenGlException("GL renderer string is null, aborting"); + + if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + { + var blacklist = AvaloniaLocator.Current.GetService() + ?.GlxRendererBlacklist; + if (blacklist != null) + foreach(var item in blacklist) + if (GlInterface.Renderer.Contains(item)) + throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'"); + } + } public void ClearContext() => Glx.MakeContextCurrent(_x11.Display, diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index 26495111d1..c68cb04733 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -221,12 +221,8 @@ namespace Avalonia.X11 //{ X11Key.?, Key.DeadCharProcessed } }; - public static Key ConvertKey(IntPtr key) - { - var ikey = key.ToInt32(); - Key result; - return KeyDic.TryGetValue((X11Key)ikey, out result) ? result : Key.None; - } -} + public static Key ConvertKey(X11Key key) + => KeyDic.TryGetValue(key, out var result) ? result : Key.None; + } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cf5902eff7..7bdc61eb28 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -96,6 +96,14 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + + public List GlxRendererBlacklist { get; set; } = new List + { + // llvmpipe is a software GL rasterizer. If it's returned by glGetString, + // that usually means that something in the system is horribly misconfigured + // and sometimes attempts to use GLX might cause a segfault + "llvmpipe" + }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; public bool? EnableMultiTouch { get; set; } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 01beebfff1..18c23aa31e 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -419,10 +419,21 @@ namespace Avalonia.X11 return; var buffer = stackalloc byte[40]; - var latinKeysym = XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, 0); + var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + + // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway + var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); + + // Manually switch the Shift index for the keypad, + // there should be a proper way to do this + if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + && key > X11Key.Num_Lock && key <= X11Key.KP_9) + key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); + + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - X11KeyTransform.ConvertKey(latinKeysym), TranslateModifiers(ev.KeyEvent.state)), ref ev); + X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); if (ev.type == XEventName.KeyPress) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 726f4221f8..a466714136 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -40,7 +40,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Source = Source, StringFormat = StringFormat, RelativeSource = RelativeSource, - DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)) + DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + NameScope = new WeakReference(serviceProvider.GetService()) }; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index e28828625f..f46ee8787b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -17,6 +17,6 @@ namespace Avalonia.Markup.Xaml.Templates public Type TargetType { get; set; } - public IControl Build(ITemplatedControl control) => TemplateContent.Load(Content); + public ControlTemplateResult Build(ITemplatedControl control) => TemplateContent.Load(Content); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index ec5de8a059..c81a3718a7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -32,6 +32,6 @@ namespace Avalonia.Markup.Xaml.Templates } } - public IControl Build(object data) => TemplateContent.Load(Content); + public IControl Build(object data) => TemplateContent.Load(Content).Control; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index 6e4d2a3980..0dd4e57a7f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -14,8 +14,8 @@ namespace Avalonia.Markup.Xaml.Templates public object Content { get; set; } public IPanel Build() - => (IPanel)TemplateContent.Load(Content); + => (IPanel)TemplateContent.Load(Content).Control; object ITemplate.Build() => Build(); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index b8fec4e8d0..5f36cfe0ac 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -13,8 +13,8 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IControl Build() => TemplateContent.Load(Content); + public IControl Build() => TemplateContent.Load(Content).Control; object ITemplate.Build() => Build(); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index ead373d380..6a30c3861a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -4,17 +4,18 @@ using System; using Avalonia.Controls; using System.Collections.Generic; +using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates { public static class TemplateContent { - public static IControl Load(object templateContent) + public static ControlTemplateResult Load(object templateContent) { if (templateContent is Func direct) { - return (IControl)direct(null); + return (ControlTemplateResult)direct(null); } throw new ArgumentException(nameof(templateContent)); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index bd2b9d2efd..0e2a131afd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -51,9 +51,9 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data) { - var visualTreeForItem = TemplateContent.Load(Content); + var visualTreeForItem = TemplateContent.Load(Content).Control; visualTreeForItem.DataContext = data; return visualTreeForItem; } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 167b75603f..b91d679fba 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -156,7 +156,7 @@ namespace Avalonia.Markup.Xaml.XamlIl { overrideField.SetValue(null, new Action( - target => { populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, target); })); + target => { populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), target); })); try { return Activator.CreateInstance(targetType); @@ -170,11 +170,11 @@ namespace Avalonia.Markup.Xaml.XamlIl var createCb = Expression.Lambda>( Expression.Convert(Expression.Call( created.GetMethod(AvaloniaXamlIlCompiler.BuildName), isp), typeof(object)), isp).Compile(); - return createCb(XamlIlRuntimeHelpers.RootServiceProviderV1); + return createCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2()); } else { - populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, rootInstance); + populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), rootInstance); return rootInstance; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c25e1186d0..ea9eaee73a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -35,6 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"), RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"), + RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject", UriContextProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IUriContext"), ParentStackProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlParentStackProvider"), @@ -53,9 +54,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); + rv.ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c); return rv; } + public const string ContextNameScopeFieldName = "AvaloniaNameScope"; + + private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings, + IXamlIlTypeSystem typeSystem, + IXamlIlTypeBuilder typebuilder, IXamlIlEmitter constructor) + { + + var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope"); + var field = typebuilder.DefineField(nameScopeType, + ContextNameScopeFieldName, true, false); + constructor + .Ldarg_0() + .Ldarg(1) + .Ldtype(nameScopeType) + .EmitCall(mappings.ServiceProvider.GetMethod(new FindMethodMethodSignature("GetService", + typeSystem.FindType("System.Object"), typeSystem.FindType("System.Type")))) + .Stfld(field); + } + + class AttributeResolver : IXamlIlCustomAttributeResolver { private readonly IXamlIlType _typeConverterAttribute; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 33056fa3e8..805b733feb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") { if (context.ParentNodes().FirstOrDefault() is XamlIlManipulationGroupNode mg - && mg.Children.OfType().Any()) + && mg.Children.OfType().Any()) return node; IXamlIlAstValueNode value = null; @@ -41,54 +41,99 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Children = { pa, - new ScopeRegistrationNode(value) + new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes()) } }; } + if (!context.ParentNodes().Any() + && node is XamlIlValueWithManipulationNode mnode) + { + mnode.Manipulation = new XamlIlManipulationGroupNode(mnode, + new[] + { + mnode.Manipulation, + new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) + }); + } return node; } - class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode { - public IXamlIlAstValueNode Value { get; set; } - public ScopeRegistrationNode(IXamlIlAstValueNode value) : base(value) + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) { - Value = value; + _types = types; } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Value = (IXamlIlAstValueNode)Value.Visit(visitor); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) { - var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); - var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); - var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) - using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) + var next = codeGen.DefineLabel(); + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement)) { - var exit = codeGen.DefineLabel(); - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // var scope = target.FindNameScope() - .Ldloc(targetLoc.Local) - .Castclass(findNameScope.Parameters[0]) - .EmitCall(findNameScope) - .Stloc(nameScopeLoc.Local) - // if({scope} != null) goto call; - .Ldloc(nameScopeLoc.Local) - .Brfalse(exit) - .Ldloc(nameScopeLoc.Local); - context.Emit(Value, codeGen, Value.Type.GetClrType()); codeGen - .Ldloc(targetLoc.Local) - .EmitCall(registerMethod) - .MarkLabel(exit); + .Isinst(_types.StyledElement) + .Dup() + .Stloc(local.Local) + .Brfalse(next) + .Ldloc(local.Local) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(_types.NameScopeSetNameScope, true) + .MarkLabel(next) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(_types.INameScopeComplete, true); } + return XamlIlNodeEmitResult.Void(1); + } } } + + class AvaloniaNameScopeRegistrationXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + public IXamlIlAstValueNode Name { get; set; } + + public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name) + { + _types = types; + Name = name; + } + + public override void VisitChildren(IXamlIlAstVisitor visitor) + => Name = (IXamlIlAstValueNode)Name.Visit(visitor); + + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + { + + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // _context.NameScope.Register(Name, target) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField); + + context.Emit(Name, codeGen, Name.Type.GetClrType()); + + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(_types.INameScopeRegister, true); + } + + return XamlIlNodeEmitResult.Void(1); + } + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c054e57380..1efae902c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -16,13 +16,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlMethod AvaloniaObjectSetValueMethod { get; } public IXamlIlType IDisposable { get; } public XamlIlTypeWellKnownTypes XamlIlTypes { get; } + public XamlIlLanguageTypeMappings XamlIlMappings { get; } public IXamlIlType Transitions { get; } public IXamlIlType AssignBindingAttribute { get; } public IXamlIlType UnsetValueType { get; } + public IXamlIlType StyledElement { get; } + public IXamlIlType NameScope { get; } + public IXamlIlMethod NameScopeSetNameScope { get; } + public IXamlIlType INameScope { get; } + public IXamlIlMethod INameScopeRegister { get; } + public IXamlIlMethod INameScopeComplete { get; } public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) { XamlIlTypes = ctx.Configuration.WellKnownTypes; + XamlIlMappings = ctx.Configuration.TypeMappings; AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); @@ -37,8 +45,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers AvaloniaProperty, IBinding, ctx.Configuration.WellKnownTypes.Object); UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); + StyledElement = ctx.Configuration.TypeSystem.GetType("Avalonia.StyledElement"); + INameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.INameScope"); + INameScopeRegister = INameScope.GetMethod( + new FindMethodMethodSignature("Register", XamlIlTypes.Void, + XamlIlTypes.String, XamlIlTypes.Object) + { + IsStatic = false, DeclaringOnly = true, IsExactMatch = true + }); + INameScopeComplete = INameScope.GetMethod( + new FindMethodMethodSignature("Complete", XamlIlTypes.Void) + { + IsStatic = false, DeclaringOnly = true, IsExactMatch = true + }); + NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope"); + NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", + XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true}); + AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 2d8ea643ac..1c2fa17643 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Data; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -17,7 +19,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var resourceNodes = provider.GetService().Parents .OfType().ToList(); var rootObject = provider.GetService().RootObject; - return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject)); + var parentScope = provider.GetService(); + return sp => + { + var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); + var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); + scope.Complete(); + return new ControlTemplateResult((IControl)obj, scope); + }; } class DeferredParentServiceProvider : @@ -27,12 +36,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; + private readonly INameScope _nameScope; public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes, - object rootObject) + object rootObject, INameScope nameScope) { _parentProvider = parentProvider; _parentResourceNodes = parentResourceNodes; + _nameScope = nameScope; RootObject = rootObject; } @@ -48,6 +59,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime public object GetService(Type serviceType) { + if (serviceType == typeof(INameScope)) + return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; if (serviceType == typeof(IRootObjectProvider)) @@ -56,6 +69,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } public object RootObject { get; } + public object IntermediateRootObject => RootObject; } @@ -132,12 +146,28 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } } - public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(); + [Obsolete("Don't use", true)] + public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null); + [DebuggerStepThrough] + public static IServiceProvider CreateRootServiceProviderV2() + { + return new RootServiceProvider(new NameScope()); + } + class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider { + private readonly INameScope _nameScope; + + public RootServiceProvider(INameScope nameScope) + { + _nameScope = nameScope; + } + public object GetService(Type serviceType) { + if (serviceType == typeof(INameScope)) + return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 894b2c0282..c2ec091f79 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 894b2c02827fd5eb16a338de5d5b6c9fbc60fef5 +Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022 diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs index 06cc85101a..5ef3dc9753 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -10,7 +10,15 @@ namespace Avalonia.Markup.Xaml public interface IRootObjectProvider { + /// + /// The root object of the xaml file + /// object RootObject { get; } + /// + /// The "current" root object, contains either the root of the xaml file + /// or the root object of the control/data template + /// + object IntermediateRootObject { get; } } public interface IUriContext diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 0b85e3224d..dbe5800e55 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Reactive; using System.Reactive.Linq; +using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.LogicalTree; @@ -90,6 +91,8 @@ namespace Avalonia.Data public string StringFormat { get; set; } public WeakReference DefaultAnchor { get; set; } + + public WeakReference NameScope { get; set; } /// /// Gets or sets a function used to resolve types from names in the binding path. @@ -110,7 +113,9 @@ namespace Avalonia.Data ExpressionObserver observer; - var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); + INameScope nameScope = null; + NameScope?.TryGetTarget(out nameScope); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); if (ElementName != null) { @@ -254,9 +259,12 @@ namespace Avalonia.Data ExpressionNode node) { Contract.Requires(target != null); - + + NameScope.TryGetTarget(out var scope); + if (scope == null) + throw new InvalidOperationException("Name scope is null or was already collected"); var result = new ExpressionObserver( - ControlLocator.Track(target, elementName), + NameScopeLocator.Track(scope, elementName), node, null); return result; diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index eb2a8df2eb..19f92149ec 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -64,14 +64,19 @@ namespace Avalonia.Data object anchor = null, bool enableDataValidation = false) { - if (Converter == null) + var targetType = targetProperty?.PropertyType ?? typeof(object); + var converter = Converter; + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) { - throw new NotSupportedException("MultiBinding without Converter not currently supported."); + converter = new StringFormatMultiValueConverter(StringFormat, converter); } - - var targetType = targetProperty?.PropertyType ?? typeof(object); + var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); + var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -87,23 +92,19 @@ namespace Avalonia.Data } } - private object ConvertValue(IList values, Type targetType) + private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { var culture = CultureInfo.CurrentCulture; - var converted = Converter.Convert(values, targetType, ConverterParameter, culture); + var converted = converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) + if (converted == BindingOperations.DoNothing) { - converted = FallbackValue; + return converted; } - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) + if (converted == AvaloniaProperty.UnsetValue) { - converted = string.Format(culture, StringFormat, converted); + converted = FallbackValue; } return converted; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 8cb14277ff..f957bcab1e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Reactive; +using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -7,7 +8,8 @@ namespace Avalonia.Markup.Parsers { public static class ExpressionObserverBuilder { - internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null) + internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null, + INameScope nameScope = null) { if (string.IsNullOrWhiteSpace(expression)) { @@ -15,7 +17,7 @@ namespace Avalonia.Markup.Parsers } var reader = new CharacterReader(expression.AsSpan()); - var parser = new ExpressionParser(enableValidation, typeResolver); + var parser = new ExpressionParser(enableValidation, typeResolver, nameScope); var node = parser.Parse(ref reader); if (!reader.End) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index a1350a8393..ed3e65feda 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -7,6 +7,7 @@ using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls; namespace Avalonia.Markup.Parsers { @@ -20,10 +21,12 @@ namespace Avalonia.Markup.Parsers { private readonly bool _enableValidation; private readonly Func _typeResolver; + private readonly INameScope _nameScope; - public ExpressionParser(bool enableValidation, Func typeResolver) + public ExpressionParser(bool enableValidation, Func typeResolver, INameScope nameScope) { _typeResolver = typeResolver; + _nameScope = nameScope; _enableValidation = enableValidation; } @@ -213,7 +216,7 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Element name expected after '#'."); } - nodes.Add(new ElementNameNode(name.ToString())); + nodes.Add(new ElementNameNode(_nameScope, name.ToString())); return State.AfterMember; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f09efca7d0..981e93c534 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.LogicalTree; @@ -6,11 +7,13 @@ namespace Avalonia.Markup.Parsers.Nodes { internal class ElementNameNode : ExpressionNode { + private readonly WeakReference _nameScope; private readonly string _name; private IDisposable _subscription; - public ElementNameNode(string name) + public ElementNameNode(INameScope nameScope, string name) { + _nameScope = new WeakReference(nameScope); _name = name; } @@ -18,14 +21,10 @@ namespace Avalonia.Markup.Parsers.Nodes protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is ILogical logical) - { - _subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged); - } + if (_nameScope.TryGetTarget(out var scope)) + _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged); else - { _subscription = null; - } } protected override void StopListeningCore() diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ee4564ff35..262d87d8b6 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -101,6 +101,7 @@ namespace Avalonia.Skia public SKCanvas Canvas { get; } SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; + GRContext ISkiaDrawingContextImpl.GrContext => _grContext; /// public void Clear(Color color) diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index ff82990c47..a9b91384d7 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -6,5 +6,6 @@ namespace Avalonia.Skia public interface ISkiaDrawingContextImpl : IDrawingContextImpl { SKCanvas SkCanvas { get; } + GRContext GrContext { get; } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index bc7fc1c9fa..097aba6dc9 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -608,6 +608,14 @@ namespace Avalonia.Win32.Interop GWL_USERDATA = -21 } + public enum MenuCharParam + { + MNC_IGNORE = 0, + MNC_CLOSE = 1, + MNC_EXECUTE = 2, + MNC_SELECT = 3 + } + [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5cc148fa0d..2f7805884d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -514,6 +514,10 @@ namespace Avalonia.Win32 KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; + case UnmanagedMethods.WindowsMessage.WM_MENUCHAR: + // mute the system beep + return (IntPtr)((Int32)UnmanagedMethods.MenuCharParam.MNC_CLOSE << 16); + case UnmanagedMethods.WindowsMessage.WM_KEYUP: case UnmanagedMethods.WindowsMessage.WM_SYSKEYUP: e = new RawKeyEventArgs( diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index b10929cbdc..015a122677 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -1012,23 +1012,23 @@ namespace Avalonia.Controls.UnitTests } private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => { var textBox = new TextBox { Name = "PART_TextBox" - }; + }.RegisterInNameScope(scope); var listbox = new ListBox { Name = "PART_SelectingItemsControl" - }; + }.RegisterInNameScope(scope); var popup = new Popup { Name = "PART_Popup" - }; + }.RegisterInNameScope(scope); var panel = new Panel(); panel.Children.Add(textBox); diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 515e18434d..b16ac6bb8e 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("FooBar", target.SelectedItem); } - private Control CreateTemplate(Carousel control) + private Control CreateTemplate(Carousel control, INameScope scope) { return new CarouselPresenter { @@ -312,7 +312,7 @@ namespace Avalonia.Controls.UnitTests [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], - }; + }.RegisterInNameScope(scope); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 70ec6c1408..599e214b31 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Panel { @@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests new ToggleButton { Name = "toggle", - }, + }.RegisterInNameScope(scope), new Popup { Name = "PART_Popup", @@ -102,8 +102,8 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ItemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty], - } - } + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope) } }; }); diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index c17893604c..93355a22f2 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -126,7 +126,7 @@ namespace Avalonia.Controls.UnitTests var target = new ContentControl { Template = GetTemplate(), - ContentTemplate = new FuncDataTemplate(_ => new Canvas()), + ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; target.Content = "Foo"; @@ -302,8 +302,8 @@ namespace Avalonia.Controls.UnitTests var target = new ContentControl { - Template = new FuncControlTemplate(_ => presenter), - ContentTemplate = new FuncDataTemplate(x => new Canvas()), + Template = new FuncControlTemplate((_, __) => presenter), + ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()), Content = "foo", }; @@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -343,7 +343,7 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 936d700ad0..7de80f72cc 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -93,28 +93,28 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => { var textBox = new TextBox { Name = "PART_TextBox" - }; + }.RegisterInNameScope(scope); var button = new Button { Name = "PART_Button" - }; + }.RegisterInNameScope(scope); var calendar = new Calendar { Name = "PART_Calendar" - }; + }.RegisterInNameScope(scope); var popup = new Popup { Name = "PART_Popup" - }; + }.RegisterInNameScope(scope); var panel = new Panel(); panel.Children.Add(textBox); diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a1ba608ec6..a790d2fca1 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Detects_Horizontal_Orientation() { + GridSplitter splitter; var grid = new Grid() { RowDefinitions = new RowDefinitions("*,Auto,*"), @@ -27,7 +28,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.RowProperty] = 0 }, - new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), new Border { [Grid.RowProperty] = 2 } } }; @@ -35,12 +36,13 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":horizontal".Equals); + Assert.Contains(splitter.Classes, ":horizontal".Equals); } [Fact] public void Detects_Vertical_Orientation() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), @@ -48,7 +50,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 2 }, } }; @@ -56,12 +58,13 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals); + Assert.Contains(splitter.Classes, ":vertical".Equals); } [Fact] public void Detects_With_Both_Auto() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), @@ -69,7 +72,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 2 }, } }; @@ -77,7 +80,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals); + Assert.Contains(splitter.Classes, ":vertical".Equals); } [Fact] @@ -129,13 +132,14 @@ namespace Avalonia.Controls.UnitTests [Fact] public void In_First_Position_Doesnt_Throw_Exception() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), RowDefinitions = new RowDefinitions("*,*"), Children = { - new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), new Border { [Grid.ColumnProperty] = 1 }, new Border { [Grid.ColumnProperty] = 2 }, } @@ -144,7 +148,6 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - var splitter = grid.FindControl("splitter"); splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragDeltaEvent, @@ -199,4 +202,4 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs index 66789ef874..fcf7bf3c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs +++ b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -71,7 +71,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_HeaderPresenter", [~ContentPresenter.ContentProperty] = parent[~HeaderedItemsControl.HeaderProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3cf886ade4..2599d24354 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests var target = new ItemsControl { Template = GetTemplate(), - ItemTemplate = new FuncDataTemplate(_ => new Canvas()), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; target.Items = new[] { "Foo" }; @@ -411,7 +411,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, __) => new Button { Content = x }) }, Items = items, }; @@ -472,29 +472,6 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((TextBlock)item)); } - [Fact] - public void DataTemplate_Created_Content_Should_Be_NameScope() - { - var items = new object[] - { - "foo", - }; - - var target = new ItemsControl - { - Template = GetTemplate(), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var container = (ContentPresenter)target.Presenter.Panel.LogicalChildren[0]; - container.UpdateChild(); - - Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child)); - } - [Fact] public void Focuses_Next_Item_On_Key_Down() { @@ -578,7 +555,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -588,7 +565,7 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ItemsPresenter", MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 238e214a5d..9a459328aa 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = new[] { "Foo" }, - ItemTemplate = new FuncDataTemplate(_ => new Canvas()), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; Prepare(target); @@ -113,7 +113,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, _) => new Button { Content = x }) }, Items = items, }; @@ -138,7 +138,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectedIndex = 0, }; @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, }; @@ -181,7 +181,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, }; @@ -205,7 +205,7 @@ namespace Avalonia.Controls.UnitTests Template = ListBoxTemplate(), Items = items, SelectionMode = SelectionMode.Toggle, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }) + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) }; Prepare(target); @@ -239,7 +239,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 11 }) + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 11 }) }; Prepare(target); @@ -287,7 +287,7 @@ namespace Avalonia.Controls.UnitTests target.DataContext = items; target.VirtualizationMode = virtualizationMode; - target.ItemTemplate = new FuncDataTemplate(c => + target.ItemTemplate = new FuncDataTemplate((c, _) => { var tb = new TextBlock() { Height = 10, Width = 30 }; tb.Bind(TextBlock.TextProperty, new Data.Binding()); @@ -334,7 +334,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ScrollViewer { Name = "PART_ScrollViewer", @@ -345,24 +345,24 @@ namespace Avalonia.Controls.UnitTests [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(), - } - }); + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope)); } private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], - }); + }.RegisterInNameScope(scope)); } private FuncControlTemplate ScrollViewerTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ScrollContentPresenter { Name = "PART_ContentPresenter", @@ -370,7 +370,7 @@ namespace Avalonia.Controls.UnitTests [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - }); + }.RegisterInNameScope(scope)); } private void Prepare(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index de34558ad1..2a61ff1566 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -245,7 +245,7 @@ namespace Avalonia.Controls.UnitTests } } - private Control CreateListBoxTemplate(ITemplatedControl parent) + private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope) { return new ScrollViewer { @@ -254,17 +254,18 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), - } + }.RegisterInNameScope(scope) }; } - private Control CreateScrollViewerTemplate(ITemplatedControl parent) + private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope) { return new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), - }; + [~ContentPresenter.ContentProperty] = + parent.GetObservable(ContentControl.ContentProperty).ToBinding(), + }.RegisterInNameScope(scope); } private void ApplyTemplate(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index f06553411c..638443e17f 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -21,15 +21,16 @@ namespace Avalonia.Controls.UnitTests.Mixins { var target = new TestControl() { - Template = new FuncControlTemplate(_ => new Panel + Template = new FuncControlTemplate((_, scope) => new Panel { Children = { - new ContentPresenter { Name = "Content_1_Presenter" }, - new ContentPresenter { Name = "Content_2_Presenter" } + new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope), + new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope) } }) }; + var ex = Record.Exception(() => target.ApplyTemplate()); @@ -43,12 +44,12 @@ namespace Avalonia.Controls.UnitTests.Mixins var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; var target = new TestControl { - Template = new FuncControlTemplate(_ => new Panel + Template = new FuncControlTemplate((_, scope) => new Panel { Children = { - p1, - p2 + p1.RegisterInNameScope(scope), + p2.RegisterInNameScope(scope) } }) }; diff --git a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs b/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs index 802f7717e8..0fdcd3fd34 100644 --- a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs @@ -20,36 +20,146 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Unregister_Unregisters_Element() + public void Cannot_Register_New_Element_With_Existing_Name() + { + var target = new NameScope(); + + target.Register("foo", new object()); + Assert.Throws(() => target.Register("foo", new object())); + } + + [Fact] + public void Can_Register_Same_Element_More_Than_Once() { var target = new NameScope(); var element = new object(); target.Register("foo", element); - target.Unregister("foo"); + target.Register("foo", element); - Assert.Null(target.Find("foo")); + Assert.Same(element, target.Find("foo")); } [Fact] - public void Cannot_Register_New_Element_With_Existing_Name() + public void Cannot_Register_New_Element_For_Completed_Scope() { var target = new NameScope(); + var element = new object(); - target.Register("foo", new object()); - Assert.Throws(() => target.Register("foo", new object())); + target.Register("foo", element); + target.Complete(); + Assert.Throws(() => target.Register("bar", element)); } + + /* + `async void` here is intentional since we expect the continuation to be + executed *synchronously* and behave more like an event handler to make sure that + that the object graph is completely ready to use after it's built + rather than have pending continuations queued by SynchronizationContext. + */ + object _found = null; + async void FindAsync(INameScope scope, string name) + { + _found = await scope.FindAsync(name); + } + [Fact] - public void Can_Register_Same_Element_More_Than_Once() + public void FindAsync_Should_Find_Controls_Added_Earlier() { - var target = new NameScope(); + var scope = new NameScope(); + var element = new object(); + scope.Register("foo", element); + FindAsync(scope, "foo"); + Assert.Same(_found, element); + } + + [Fact] + public void FindAsync_Should_Find_Controls_Added_Later() + { + var scope = new NameScope(); var element = new object(); + + FindAsync(scope, "foo"); + Assert.Null(_found); + scope.Register("foo", element); + Assert.Same(_found, element); + } + + [Fact] + public void FindAsync_Should_Return_Null_After_Scope_Completion() + { + var scope = new NameScope(); + var element = new object(); + bool finished = false; + async void Find(string name) + { + Assert.Null(await scope.FindAsync(name)); + finished = true; + } + Find("foo"); + Assert.False(finished); + scope.Register("bar", element); + Assert.False(finished); + scope.Complete(); + Assert.True(finished); + } - target.Register("foo", element); - target.Register("foo", element); + [Fact] + public void Child_Scope_Should_Not_Find_Control_In_Parent_Scope_Unless_Completed() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + scope.Register("foo", element); + Assert.Null(childScope.Find("foo")); + childScope.Complete(); + Assert.Same(element, childScope.Find("foo")); + } + + [Fact] + public void Child_Scope_Should_Prefer_Own_Elements() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + var childElement = new object(); + scope.Register("foo", element); + childScope.Register("foo", childElement); + childScope.Complete(); + Assert.Same(childElement, childScope.Find("foo")); + } - Assert.Same(element, target.Find("foo")); + [Fact] + public void Child_Scope_FindAsync_Should_Find_Elements_In_Parent_Scope_When_Child_Is_Completed() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + scope.Register("foo", element); + FindAsync(childScope, "foo"); + Assert.Null(_found); + childScope.Complete(); + Assert.Same(element, _found); + } + + + [Fact] + public void Child_Scope_FindAsync_Should_Prefer_Own_Elements() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + var childElement = new object(); + FindAsync(childScope, "foo"); + scope.Register("foo", element); + Assert.Null(_found); + childScope.Register("foo", childElement); + Assert.Same(childElement, childScope.Find("foo")); + childScope.Complete(); + FindAsync(childScope, "foo"); + Assert.Same(childElement, childScope.Find("foo")); } + } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 708e934214..7d05547799 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -136,17 +136,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(NameScope.GetNameScope((Control)target.Child)); } - [Fact] - public void DataTemplate_Created_Control_Should_Be_NameScope() - { - var (target, _) = CreateTarget(); - - target.Content = "Foo"; - - Assert.IsType(target.Child); - Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); - } - [Fact] public void Assigning_Control_To_Content_Should_Not_Set_DataContext() { @@ -170,7 +159,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { var (target, _) = CreateTarget(); - target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()); target.Content = "Foo"; Assert.IsType(target.Child); @@ -184,7 +173,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = "Foo"; Assert.IsType(target.Child); - target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()); Assert.IsType(target.Child); target.ContentTemplate = null; @@ -209,7 +198,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public void Recycles_DataTemplate() { var (target, _) = CreateTarget(); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), true)); target.Content = "foo"; @@ -239,7 +228,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public void Detects_DataTemplate_Doesnt_Support_Recycling() { var (target, _) = CreateTarget(); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), false)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), false)); target.Content = "foo"; @@ -256,7 +245,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var (target, _) = CreateTarget(); target.DataTemplates.Add(new FuncDataTemplate(x => x == "bar", _ => new Canvas(), true)); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), true)); target.Content = "foo"; @@ -278,8 +267,8 @@ namespace Avalonia.Controls.UnitTests.Presenters }; var (target, host) = CreateTarget(); - host.DataTemplates.Add(new FuncDataTemplate(x => textBlock)); - host.DataTemplates.Add(new FuncDataTemplate(x => new Canvas())); + host.DataTemplates.Add(new FuncDataTemplate((_, __) => textBlock)); + host.DataTemplates.Add(new FuncDataTemplate((_, __) => new Canvas())); target.Content = "foo"; Assert.Same(textBlock, target.Child); @@ -296,11 +285,11 @@ namespace Avalonia.Controls.UnitTests.Presenters { var templatedParent = new ContentControl { - Template = new FuncControlTemplate(x => + Template = new FuncControlTemplate((_, s) => new ContentPresenter { Name = "PART_ContentPresenter", - }), + }.RegisterInNameScope(s)), }; var root = new TestRoot { Child = templatedParent }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 2facee16b7..ab75a87110 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -93,14 +93,14 @@ namespace Avalonia.Controls.UnitTests.Presenters { var contentControl = new ContentControl { - Template = new FuncControlTemplate(c => new ContentPresenter() + Template = new FuncControlTemplate((c, scope) => new ContentPresenter() { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = c[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = c[~ContentControl.ContentTemplateProperty] - }), + }.RegisterInNameScope(scope)), ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -144,7 +144,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; target.Content = "foo"; @@ -235,8 +235,8 @@ namespace Avalonia.Controls.UnitTests.Presenters { DataTemplates = { - new FuncDataTemplate(x => textBlock), - new FuncDataTemplate(x => new Canvas()), + new FuncDataTemplate((x, _) => textBlock), + new FuncDataTemplate((x, _) => new Canvas()), }, }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs index 5268c9ac0d..09970926fa 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { DataTemplates = { - new FuncDataTemplate(x => new Decorator()), + new FuncDataTemplate((x, _) => new Decorator()), }, }; @@ -99,4 +99,4 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 3d13e4c32f..7ca11d9bed 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -220,7 +220,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { VirtualizationMode = ItemVirtualizationMode.None, Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 3ea32ed719..269da884ba 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -309,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static IDataTemplate ItemTemplate() { - return new FuncDataTemplate(x => new Canvas + return new FuncDataTemplate((x, _) => new Canvas { Width = 10, Height = 10, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 97d57e9eb6..7a6cf0fba7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -470,7 +470,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { VirtualizationMode = ItemVirtualizationMode.None, Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), }; target.ApplyTemplate(); @@ -1046,7 +1046,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static IDataTemplate StringDataTemplate() { - return new FuncDataTemplate(x => new Canvas + return new FuncDataTemplate((x, _) => new Canvas { Width = 10, Height = 10, @@ -1095,12 +1095,12 @@ namespace Avalonia.Controls.UnitTests.Presenters { public TestContainer() { - Template = new FuncControlTemplate(parent => new ContentPresenter + Template = new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], - }); + }.RegisterInNameScope(scope)); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index d4b5d01a6b..059146f17d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -134,12 +134,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { var result = new PopupRoot { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!PopupRoot.ContentProperty], - }), + }.RegisterInNameScope(scope)), }; result.ApplyTemplate(); @@ -154,7 +154,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public TemplatedControlWithPopup() { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, _) => new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index bd11cf2e3c..e266150901 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -315,16 +315,16 @@ namespace Avalonia.Controls.UnitTests.Primitives return result; } - private static IControl PopupRootTemplate(PopupRoot control) + private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) { return new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); } - private static IControl PopupContentControlTemplate(PopupContentControl control) + private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) { return new Popup { @@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], } - }; + }.RegisterInNameScope(scope); } private class PopupContentControl : ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index d913e3e54f..2eeff4cdf9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestRange() { - Template = new FuncControlTemplate(c => + Template = new FuncControlTemplate((c, scope) => { track = new Track() { @@ -122,7 +122,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "PART_Track", Thumb = new Thumb() - }; + }.RegisterInNameScope(scope); if (useXamlBinding) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs index 672b5c608b..2372987c4c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs @@ -169,7 +169,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(target.IsVisible); } - private static Control Template(ScrollBar control) + private static Control Template(ScrollBar control, INameScope scope) { return new Border { @@ -185,11 +185,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = new FuncControlTemplate(ThumbTemplate), }, - }, + }.RegisterInNameScope(scope), }; } - private static Control ThumbTemplate(Thumb control) + private static Control ThumbTemplate(Thumb control, INameScope scope) { return new Border { @@ -197,4 +197,4 @@ namespace Avalonia.Controls.UnitTests.Primitives }; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 8e421bf0a2..bc002174ec 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -536,6 +536,9 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + var called = false; target.SelectionChanged += (s, e) => @@ -545,8 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives called = true; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedIndex = -1; Assert.True(called); @@ -783,15 +784,125 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(2, vm.Child.SelectedIndex); } + [Fact] + public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + + Assert.Equal(3, target.SelectedIndex); + } + + [Fact] + public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); + } + + [Fact] + public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items.Insert(0, "Qux"); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + + [Fact] + public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + + [Fact] + public void Replacing_Selected_Item_Should_Update_SelectedItem() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 1, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + items[1] = "Qux"; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Qux", target.SelectedItem); + } + private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class Item : Control, ISelectable diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 88cb726469..8618387150 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -84,13 +84,13 @@ namespace Avalonia.Controls.UnitTests.Primitives private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class TestSelector : SelectingItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 9ef7d567c8..9d0cc368e0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -4,12 +4,15 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Markup.Data; using Xunit; @@ -17,6 +20,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests_Multiple { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Setting_SelectedIndex_Should_Add_To_SelectedItems() { @@ -258,31 +263,25 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Replacing_First_SelectedItem_Should_Update_SelectedItem_SelectedIndex() + public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() { - var items = new[] - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }; - var target = new TestSelector { - Items = items, + Items = new[] { "foo", "bar", "baz" }, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedIndex = 1; - target.SelectedItems[0] = items[2]; - Assert.Equal(2, target.SelectedIndex); - Assert.Equal(items[2], target.SelectedItem); - Assert.False(items[0].IsSelected); - Assert.False(items[1].IsSelected); - Assert.True(items[2].IsSelected); + target.SelectedItems.Add("foo"); + target.SelectedItems.Add("bar"); + + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + + target.SelectedIndex = 2; + + Assert.Equal(new[] { 2 }, SelectedContainers(target)); } [Fact] @@ -361,6 +360,52 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast().ToList()); } + [Fact] + public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz", "qux" }, + Template = Template(), + SelectedIndex = 0, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectRange(2); + + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + + target.SelectedIndex = 3; + + Assert.Equal(new[] { 3 }, SelectedContainers(target)); + } + + [Fact] + public void Toggling_Selection_After_Range_Should_Work() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz", "foo", "bar", "baz" }, + Template = Template(), + SelectedIndex = 0, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectRange(3); + + Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedContainers(target)); + + target.Toggle(4); + + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, SelectedContainers(target)); + } + [Fact] public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() { @@ -382,6 +427,40 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { -1, 1, 0 }, selectedIndexes); } + [Fact] + public void Can_Set_SelectedIndex_To_Another_Selected_Item() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedItems.Add("foo"); + target.SelectedItems.Add("bar"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + + var raised = false; + target.SelectionChanged += (s, e) => + { + raised = true; + Assert.Empty(e.AddedItems); + Assert.Equal(new[] { "foo" }, e.RemovedItems); + }; + + target.SelectedIndex = 1; + + Assert.True(raised); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } + /// /// Tests a problem discovered with ListBox with selection. /// @@ -471,6 +550,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { DataContext = items, Template = Template(), + Items = items, }; var called = false; @@ -540,45 +620,428 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(called); } + + [Fact] + public void Shift_Selecting_From_No_Selection_Selects_From_Start() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } [Fact] - public void Replacing_SelectedItems_Should_Raise_SelectionChanged_With_CorrectItems() + public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() { - var items = new[] { "foo", "bar", "baz" }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Baz", target.SelectedItem); + Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + } + + [Fact] + public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + + [Fact] + public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + } + + [Fact] + public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + + var panel = target.Presenter.Panel; + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + } + + [Fact] + public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + } + + [Fact] + public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(new[] { "Foo" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } + + [Fact] + public void SelectAll_Sets_SelectedIndex_And_SelectedItem() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + } + + [Fact] + public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + SelectedIndex = 0, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.UnselectAll(); + + Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(null, target.SelectedItem); + } + + [Fact] + public void SelectAll_Handles_Duplicate_Items() + { var target = new TestSelector { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + } + + [Fact] + public void Adding_Item_Before_SelectedItems_Should_Update_Selection() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + items.Insert(0, "Qux"); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); + } + + [Fact] + public void Removing_Item_Before_SelectedItem_Should_Update_Selection() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new TestSelector + { Template = Template(), - SelectedItem = "bar", + Items = items, + SelectionMode = SelectionMode.Multiple, }; - var called = false; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); - target.SelectionChanged += (s, e) => + target.SelectedIndex = 1; + target.SelectRange(2); + + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + } + + [Fact] + public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() + { + var items = new ObservableCollection { - Assert.Equal(new[] { "foo",}, e.AddedItems.Cast()); - Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); - called = true; + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems[0] = "foo"; - Assert.True(called); + target.SelectAll(); + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } + [Fact] + public void Replacing_Selected_Item_Should_Update_SelectedItems() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.SelectAll(); + items[1] = "Qux"; + + Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems); + } + + [Fact] + public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } + + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + + Assert.Equal(3, target.SelectedItems.Count); + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + + private IEnumerable SelectedContainers(SelectingItemsControl target) + { + return target.Presenter.Panel.Children + .Select((x, i) => x.Classes.Contains(":selected") ? i : -1) + .Where(x => x != -1); + } private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class TestSelector : SelectingItemsControl @@ -598,10 +1061,10 @@ namespace Avalonia.Controls.UnitTests.Primitives set { base.SelectionMode = value; } } - public void SelectRange(int index) - { - UpdateSelection(index, true, true); - } + public new void SelectAll() => base.SelectAll(); + public new void UnselectAll() => base.UnselectAll(); + public void SelectRange(int index) => UpdateSelection(index, true, true); + public void Toggle(int index) => UpdateSelection(index, true, false, true); } private class OldDataContextViewModel diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index 55b3d6f756..a7b90afa70 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -159,14 +159,14 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); } - private Control CreateTabStripTemplate(TabStrip parent) + private Control CreateTabStripTemplate(TabStrip parent, INameScope scope) { return new ItemsPresenter { Name = "itemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], - }; + }.RegisterInNameScope(scope); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 166586ace1..d36d0b609b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { bool executed = false; - var template = new FuncControlTemplate(_ => + var template = new FuncControlTemplate((_, __) => { executed = true; return new Control(); @@ -42,7 +42,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { bool executed = false; - var template = new FuncControlTemplate(_ => + var template = new FuncControlTemplate((_, __) => { executed = true; return new Control(); @@ -63,7 +63,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Panel { @@ -92,35 +92,12 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Empty(target.GetLogicalChildren()); } - [Fact] - public void Templated_Child_Should_Be_NameScope() - { - var target = new TemplatedControl - { - Template = new FuncControlTemplate(_ => new Decorator - { - Child = new Panel - { - Children = - { - new TextBlock(), - new Border(), - } - } - }), - }; - - target.ApplyTemplate(); - - Assert.NotNull(NameScope.GetNameScope((Control)target.GetVisualChildren().Single())); - } - [Fact] public void Templated_Children_Should_Have_TemplatedParent_Set() { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Panel { @@ -149,7 +126,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; target.ApplyTemplate(); @@ -165,14 +142,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; target.ApplyTemplate(); var child = (Decorator)target.GetVisualChildren().Single(); - target.Template = new FuncControlTemplate(_ => new Canvas()); + target.Template = new FuncControlTemplate((_, __) => new Canvas()); target.ApplyTemplate(); Assert.Null(child.Parent); @@ -183,7 +160,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new ScrollViewer()) + Template = new FuncControlTemplate((_, __) => new ScrollViewer()) }; target.ApplyTemplate(); @@ -203,7 +180,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => + Template = new FuncControlTemplate((_, __) => { return new StackPanel { @@ -232,11 +209,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => + Template = new FuncControlTemplate((_, __) => { return new ContentControl { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, ___) => { return new Border { @@ -271,47 +248,12 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, textBlock.TemplatedParent); } - [Fact] - public void Nested_TemplatedControls_Should_Register_With_Correct_NameScope() - { - var target = new ContentControl - { - Template = new FuncControlTemplate(ScrollingContentControlTemplate), - Content = "foo" - }; - - var root = new TestRoot { Child = target }; - target.ApplyTemplate(); - - var border = target.GetVisualChildren().FirstOrDefault(); - Assert.IsType(border); - - var scrollViewer = border.GetVisualChildren().FirstOrDefault(); - Assert.IsType(scrollViewer); - ((ScrollViewer)scrollViewer).ApplyTemplate(); - - var scrollContentPresenter = scrollViewer.GetVisualChildren().FirstOrDefault(); - Assert.IsType(scrollContentPresenter); - ((ContentPresenter)scrollContentPresenter).UpdateChild(); - - var contentPresenter = scrollContentPresenter.GetVisualChildren().FirstOrDefault(); - Assert.IsType(contentPresenter); - - var borderNs = NameScope.GetNameScope((Control)border); - var scrollContentPresenterNs = NameScope.GetNameScope((Control)scrollContentPresenter); - - Assert.NotNull(borderNs); - Assert.Same(scrollViewer, borderNs.Find("ScrollViewer")); - Assert.Same(contentPresenter, borderNs.Find("PART_ContentPresenter")); - Assert.Same(scrollContentPresenter, scrollContentPresenterNs.Find("PART_ContentPresenter")); - } - [Fact] public void ApplyTemplate_Should_Raise_TemplateApplied() { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; var raised = false; @@ -334,7 +276,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), }) @@ -348,7 +290,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, decorator.TemplatedParent); Assert.Equal(target, border.TemplatedParent); - target.Template = new FuncControlTemplate(_ => new Canvas()); + target.Template = new FuncControlTemplate((_, __) => new Canvas()); // Templated children should not be removed here: the control may be re-added // somewhere with the same template, so they could still be of use. @@ -370,7 +312,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = templateChild, }) @@ -392,7 +334,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = templateChild, }) @@ -425,7 +367,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -461,7 +403,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -500,7 +442,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -520,7 +462,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -555,7 +497,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()), + Template = new FuncControlTemplate((_, __) => new Decorator()), } }; @@ -573,7 +515,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - private static IControl ScrollingContentControlTemplate(ContentControl control) + private static IControl ScrollingContentControlTemplate(ContentControl control, INameScope scope) { return new Border { @@ -585,20 +527,20 @@ namespace Avalonia.Controls.UnitTests.Primitives { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = control[!ContentControl.ContentProperty], - } - } + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope) }; } - private static Control ScrollViewerTemplate(ScrollViewer control) + private static Control ScrollViewerTemplate(ScrollViewer control, INameScope scope) { var result = new ScrollContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); return result; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index d1385176c5..75a2f4178b 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(10, 10), target.Offset); } - private Control CreateTemplate(ScrollViewer control) + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid { @@ -88,7 +88,7 @@ namespace Avalonia.Controls.UnitTests [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], - }, + }.RegisterInNameScope(scope), new ScrollBar { Name = "horizontalScrollBar", @@ -98,7 +98,7 @@ namespace Avalonia.Controls.UnitTests [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [Grid.RowProperty] = 1, - }, + }.RegisterInNameScope(scope), new ScrollBar { Name = "verticalScrollBar", @@ -108,9 +108,9 @@ namespace Avalonia.Controls.UnitTests [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [Grid.ColumnProperty] = 1, - }, + }.RegisterInNameScope(scope), }, }; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index e00084eba4..ee8d9cc62e 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests }, }; - var template = new FuncControlTemplate(x => new Decorator()); + var template = new FuncControlTemplate((x, __) => new Decorator()); using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -176,7 +176,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, __) => new Button { Content = x }) }, Items = items, }; @@ -273,7 +273,7 @@ namespace Avalonia.Controls.UnitTests TabControl target = new TabControl { Template = TabControlTemplate(), - ContentTemplate = new FuncDataTemplate(x => + ContentTemplate = new FuncDataTemplate((x, _) => new TextBlock { Tag = "bar", Text = x }), Items = new[] { "Foo" }, }; @@ -289,7 +289,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate TabControlTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new StackPanel { Children = @@ -299,26 +299,26 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ItemsPresenter", [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], [!TabStrip.ItemTemplateProperty] = parent[!TabControl.ItemTemplateProperty], - }, + }.RegisterInNameScope(scope), new ContentPresenter { Name = "PART_SelectedContentHost", [!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty], - } + }.RegisterInNameScope(scope) } }); } private IControlTemplate TabItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty] - }); + }.RegisterInNameScope(scope)); } private void ApplyTemplate(TabControl target) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index cef5fe61fc..35f0b39210 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -472,7 +472,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new TextPresenter { Name = "PART_TextPresenter", @@ -483,7 +483,7 @@ namespace Avalonia.Controls.UnitTests Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), }, - }); + }.RegisterInNameScope(scope)); } private void RaiseKeyEvent(TextBox textBox, Key key, InputModifiers inputModifiers) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 4d79dd557e..4aaf0ab5b7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -93,7 +93,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new TextPresenter { Name = "PART_TextPresenter", @@ -104,7 +104,7 @@ namespace Avalonia.Controls.UnitTests Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), }, - }); + }.RegisterInNameScope(scope)); } private class ExceptionTest diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index f112289460..0ee772425b 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -226,12 +226,12 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate CreateTemplate() { - return new FuncControlTemplate(x => + return new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], - }); + }.RegisterInNameScope(scope)); } private class TestTopLevel : TopLevel diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b66d6ed11c..2868928455 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests Template = CreateTreeViewTemplate(), Items = CreateTestTreeData(), ItemTemplate = new FuncTreeDataTemplate( - _ => new Canvas(), + (_, __) => new Canvas(), x => x.Children), } }; @@ -475,7 +475,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, _) => new Button { Content = x }) }, Items = items, }; @@ -513,27 +513,6 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } - [Fact] - public void DataTemplate_Created_Item_Should_Be_NameScope() - { - var items = new object[] - { - "foo", - }; - - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var item = target.Presenter.Panel.LogicalChildren[0]; - Assert.NotNull(NameScope.GetNameScope((TreeViewItem)item)); - } - [Fact] public void Should_React_To_Children_Changing() { @@ -799,16 +778,16 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTreeViewTemplate() { - return new FuncControlTemplate(parent => new ItemsPresenter + return new FuncControlTemplate((parent, scope) => new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - }); + }.RegisterInNameScope(scope)); } private IControlTemplate CreateTreeViewItemTemplate() { - return new FuncControlTemplate(parent => new Panel + return new FuncControlTemplate((parent, scope) => new Panel { Children = { @@ -816,12 +795,12 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_HeaderPresenter", [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], - }, + }.RegisterInNameScope(scope), new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - } + }.RegisterInNameScope(scope) } }); } diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 6da771217c..9d3e568582 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 641979638a..df522397ee 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls.UnitTests.Utils private FuncControlTemplate CreateWindowTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); }); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 12d29f2e5b..3ee6a50e69 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -253,12 +253,12 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate CreateTemplate() { - return new FuncControlTemplate(x => + return new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], - }); + }.RegisterInNameScope(scope)); } private class TestWindowBase : WindowBase diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index cd25b79f1f..a841174d2d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -67,13 +67,15 @@ namespace Avalonia.LeakTests { Func run = () => { + var scope = new NameScope(); var window = new Window { Content = new Canvas { Name = "foo" - } + }.RegisterInNameScope(scope) }; + NameScope.SetNameScope(window, scope); window.Show(); @@ -84,6 +86,8 @@ namespace Avalonia.LeakTests // Clear the content and ensure the Canvas is removed. window.Content = null; + NameScope.SetNameScope(window, null); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); @@ -279,7 +283,7 @@ namespace Avalonia.LeakTests DataTemplates = { new FuncTreeDataTemplate( - x => new TextBlock { Text = x.Name }, + (x, _) => new TextBlock { Text = x.Name }, x => x.Children) }, Items = nodes diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs index 61df0bffdf..0019b06911 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs @@ -1,6 +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 Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; @@ -34,10 +35,13 @@ namespace Avalonia.Markup.UnitTests.Data } }; + root.RegisterChildrenNames(); + var binding = new Binding { ElementName = "source", Path = "Text", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(TextBox.TextProperty, binding); @@ -69,10 +73,12 @@ namespace Avalonia.Markup.UnitTests.Data } } }; + root.RegisterChildrenNames(); var binding = new Binding { ElementName = "source", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(ContentControl.ContentProperty, binding); @@ -99,11 +105,13 @@ namespace Avalonia.Markup.UnitTests.Data } } }; - + root.RegisterChildrenNames(); + var binding = new Binding { ElementName = "source", Path = "Text", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(TextBox.TextProperty, binding); @@ -113,7 +121,7 @@ namespace Avalonia.Markup.UnitTests.Data Name = "source", Text = "foo", }); - + root.RegisterChildrenNames(); Assert.Equal("foo", target.Text); } @@ -136,10 +144,12 @@ namespace Avalonia.Markup.UnitTests.Data } } }; + root.RegisterChildrenNames(); var binding = new Binding { ElementName = "source", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(ContentControl.ContentProperty, binding); @@ -151,6 +161,7 @@ namespace Avalonia.Markup.UnitTests.Data }; stackPanel.Children.Add(source); + root.RegisterChildrenNames(); Assert.Same(source, target.Content); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs index bd4b5b9d04..ee95463f1f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; -using Avalonia.Data.Core; +using Avalonia.Layout; using Xunit; namespace Avalonia.Markup.UnitTests.Data @@ -21,7 +20,31 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), + }; + + var format = "{0:0.0} + {1:00}"; + var target = new MultiBinding + { + StringFormat = format, + Bindings = + { + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), + } + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + Assert.Equal(string.Format(format, 1, 2), textBlock.Text); + } + + [Fact] + public void StringFormat_Should_Be_Applied_After_Converter() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), }; var target = new MultiBinding @@ -30,8 +53,8 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; @@ -45,7 +68,7 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), }; var target = new MultiBinding @@ -54,12 +77,12 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; - textBlock.Bind(TextBlock.WidthProperty, target); + textBlock.Bind(Layoutable.WidthProperty, target); Assert.Equal(3.0, textBlock.Width); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs index ef3864abd7..51cb75a867 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Markup.UnitTests.Data { var source = new Button { - Template = new FuncControlTemplate