diff --git a/packages.cake b/packages.cake index d633230189..9defa3004c 100644 --- a/packages.cake +++ b/packages.cake @@ -303,7 +303,7 @@ public class Packages { new NuSpecContent { Source = "Avalonia.Android.dll", Target = "lib/MonoAndroid10" } }, - BasePath = context.Directory("./src/Android/Avalonia.Android/bin/" + parameters.DirSuffix + "/monoandroid44/"), + BasePath = context.Directory("./src/Android/Avalonia.Android/bin/" + parameters.DirSuffix + "/monoandroid44/MonoAndroid44/"), OutputDirectory = parameters.NugetRoot }, /////////////////////////////////////////////////////////////////////////////// diff --git a/readme.md b/readme.md index 345ad7fe9b..f345cbd9df 100644 --- a/readme.md +++ b/readme.md @@ -35,6 +35,9 @@ Install-Package Avalonia.Desktop 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 + ## Documentation As mentioned above, Avalonia is still in beta and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index a03d442bff..6de50f978d 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,5 +1,6 @@ +copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ diff --git a/src/Avalonia.Base/AvaloniaLocator.cs b/src/Avalonia.Base/AvaloniaLocator.cs index cd398c53ed..f9bbe38bec 100644 --- a/src/Avalonia.Base/AvaloniaLocator.cs +++ b/src/Avalonia.Base/AvaloniaLocator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; #pragma warning disable CS1591 // Enable me later diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 35e189e6a4..7e8d733f1b 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; -using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index f7dabd3a43..4b0116a536 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -231,7 +231,7 @@ namespace Avalonia } /// - /// Tests two s for unequality. + /// Tests two s for inequality. /// /// The first property. /// The second property. @@ -558,4 +558,4 @@ namespace Avalonia public override string ToString() => "(unset)"; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 41c2ad6e54..d5a4e63003 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -8,7 +8,6 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using Avalonia.Diagnostics; -using Avalonia.Platform; namespace Avalonia.Collections { @@ -551,4 +550,4 @@ namespace Avalonia.Collections NotifyCountChanged(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/AvaloniaListConverter.cs b/src/Avalonia.Base/Collections/AvaloniaListConverter.cs index 7f5442857c..63c2e07ecf 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListConverter.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListConverter.cs @@ -40,4 +40,4 @@ namespace Avalonia.Collections return result; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index b27b06a277..1731950222 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -6,7 +6,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Linq; using System.Reactive.Disposables; namespace Avalonia.Collections diff --git a/src/Avalonia.Base/Collections/IAvaloniaList.cs b/src/Avalonia.Base/Collections/IAvaloniaList.cs index 48c36976a5..b7ed9b1ad6 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaList.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Avalonia.Collections { /// - /// A notiftying list. + /// A notifying list. /// /// The type of the items in the list. public interface IAvaloniaList : IList, IAvaloniaReadOnlyList @@ -64,4 +64,4 @@ namespace Avalonia.Collections /// The number of items to remove. void RemoveRange(int index, int count); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index 1aa4ff34d5..755efa3eaf 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -8,10 +8,10 @@ using System.ComponentModel; namespace Avalonia.Collections { /// - /// A read-only notiftying list. + /// A read-only notifying list. /// /// The type of the items in the list. public interface IAvaloniaReadOnlyList : IReadOnlyList, INotifyCollectionChanged, INotifyPropertyChanged { } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index 3a355bcb48..ba84c37e4a 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -25,7 +25,7 @@ namespace Avalonia.Collections } /// - /// Subcribes to the CollectionChanged event using a weak subscription. + /// Subscribes to the CollectionChanged event using a weak subscription. /// /// The collection. /// @@ -44,7 +44,7 @@ namespace Avalonia.Collections } /// - /// Subcribes to the CollectionChanged event using a weak subscription. + /// Subscribes to the CollectionChanged event using a weak subscription. /// /// The collection. /// diff --git a/src/Avalonia.Base/Data/BindingChainException.cs b/src/Avalonia.Base/Data/BindingChainException.cs index 97b0d3ba8b..a609ace4ec 100644 --- a/src/Avalonia.Base/Data/BindingChainException.cs +++ b/src/Avalonia.Base/Data/BindingChainException.cs @@ -6,7 +6,7 @@ using System; namespace Avalonia.Data { /// - /// An exception returned through signalling that a + /// An exception returned through signaling that a /// requested binding expression could not be evaluated because of a null in one of the links /// of the binding chain. /// @@ -15,14 +15,14 @@ namespace Avalonia.Data private string _message; /// - /// Initalizes a new instance of the class. + /// Initializes a new instance of the class. /// public BindingChainException() { } /// - /// Initalizes a new instance of the class. + /// Initializes a new instance of the class. /// /// The error message. public BindingChainException(string message) @@ -31,7 +31,7 @@ namespace Avalonia.Data } /// - /// Initalizes a new instance of the class. + /// Initializes a new instance of the class. /// /// The error message. /// The expression. @@ -51,7 +51,7 @@ namespace Avalonia.Data public string Expression { get; protected set; } /// - /// Gets the point in the expression at which the error occured. + /// Gets the point in the expression at which the error occurred. /// public string ExpressionErrorPoint { get; protected set; } diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 5d3e6b26f4..7c55321a80 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Logging; namespace Avalonia.Data { diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index eb7c449bec..ca148659e6 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data /// An optional anchor from which to locate required context. When binding to objects that /// are not in the logical tree, certain types of binding need an anchor into the tree in /// order to locate named controls or resources. The parameter - /// can be used to provice this context. + /// can be used to provide this context. /// /// An which can be used to cancel the binding. public static IDisposable Apply( diff --git a/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs b/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs index d2fb48ffb8..7f4c83772d 100644 --- a/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs +++ b/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs @@ -1,10 +1,8 @@ -using Avalonia.Utilities; -using System; -using System.Collections.Generic; +using System; using System.Globalization; using System.Reflection; -using System.Text; using System.Windows.Input; +using Avalonia.Utilities; namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index ec75076892..990a4b04f2 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -3,9 +3,8 @@ using System; using System.Globalization; -using Avalonia.Data; -using Avalonia.Utilities; using System.Windows.Input; +using Avalonia.Utilities; namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Converters/IValueConverter.cs b/src/Avalonia.Base/Data/Converters/IValueConverter.cs index b55a2c4fe8..754aa7975d 100644 --- a/src/Avalonia.Base/Data/Converters/IValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/IValueConverter.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Data; namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Converters/StringConverters.cs b/src/Avalonia.Base/Data/Converters/StringConverters.cs index 470f0d2289..0101cf5d29 100644 --- a/src/Avalonia.Base/Data/Converters/StringConverters.cs +++ b/src/Avalonia.Base/Data/Converters/StringConverters.cs @@ -1,9 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Globalization; -using Avalonia.Utilities; namespace Avalonia.Data.Converters { diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index f7e5de2fe2..28c0dce518 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Reactive.Linq; -using System.Text; using Avalonia.Reactive; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 0e2c3c035c..d5658b124b 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,9 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive.Linq; - namespace Avalonia.Data.Core { public class EmptyExpressionNode : ExpressionNode diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 773049d3a5..65f26df011 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; -using Avalonia.Data; using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; using Avalonia.Reactive; diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 195c9b7660..0365ead24d 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data.Core.Parsers; namespace Avalonia.Data.Core { diff --git a/src/Avalonia.Base/Data/Core/ITransformNode.cs b/src/Avalonia.Base/Data/Core/ITransformNode.cs index 7638db8302..843d09fc19 100644 --- a/src/Avalonia.Base/Data/Core/ITransformNode.cs +++ b/src/Avalonia.Base/Data/Core/ITransformNode.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Data.Core +namespace Avalonia.Data.Core { interface ITransformNode { diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 04412b61ef..4206a99e3d 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; -using System.Text; -using Avalonia.Data; namespace Avalonia.Data.Core { diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 5c3295a9d8..5e09bbcc2f 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -3,12 +3,8 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Globalization; using System.Linq; using System.Reactive.Linq; -using System.Reflection; -using System.Text; -using Avalonia.Data; using Avalonia.Utilities; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 20f1bcd21e..6d58b18a9f 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Data; namespace Avalonia.Data.Core { diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs index db5d117687..d2035a592f 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Linq.Expressions; -using System.Text; namespace Avalonia.Data.Core.Parsers { diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 1b4d1c200d..bcb6eeca41 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Text; namespace Avalonia.Data.Core.Parsers { diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index ee91f964ff..8d2ed905ee 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index 47e4d91e13..33c40abea8 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -31,7 +30,7 @@ namespace Avalonia.Data.Core.Plugins return new Accessor(reference, name, inner); } - private class Accessor : DataValidatiorBase + private class Accessor : DataValidationBase { private ValidationContext _context; diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs similarity index 95% rename from src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs rename to src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs index 03ab7712bd..faec981eed 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs @@ -15,15 +15,15 @@ namespace Avalonia.Data.Core.Plugins /// and convert any values received from the inner property accessor into /// s. /// - public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver + public abstract class DataValidationBase : PropertyAccessorBase, IObserver { private readonly IPropertyAccessor _inner; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The inner property accessor. - protected DataValidatiorBase(IPropertyAccessor inner) + protected DataValidationBase(IPropertyAccessor inner) { _inner = inner; } @@ -77,4 +77,4 @@ namespace Avalonia.Data.Core.Plugins PublishValue(notification); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 4507b32e0c..eabfa31d4b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, inner); } - private class Validator : DataValidatiorBase + private class Validator : DataValidationBase { public Validator(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index c55917b088..2c3a9a53b4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 4d6fc01229..2e83e0c25e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -23,7 +23,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, accessor); } - private class Validator : DataValidatiorBase, IWeakSubscriber + private class Validator : DataValidationBase, IWeakSubscriber { WeakReference _reference; string _name; diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index dab32b639a..4047489ccc 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -6,8 +6,6 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Reflection; -using Avalonia.Data; -using Avalonia.Logging; using Avalonia.Utilities; namespace Avalonia.Data.Core.Plugins diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index cf0abc6f35..135935498c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Data; -using System.Reflection; using System.Linq; +using System.Reflection; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index cc9b3abd56..16862f576d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -2,12 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; using System.Threading.Tasks; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index e7c6ab766f..7c839acb78 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -1,9 +1,4 @@ -using Avalonia.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; namespace Avalonia.Data.Core { diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs index fa8b56765c..fa2929c6ef 100644 --- a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Avalonia diff --git a/src/Avalonia.Base/Data/IBinding.cs b/src/Avalonia.Base/Data/IBinding.cs index 70447ad3eb..95239cae06 100644 --- a/src/Avalonia.Base/Data/IBinding.cs +++ b/src/Avalonia.Base/Data/IBinding.cs @@ -17,7 +17,7 @@ namespace Avalonia.Data /// An optional anchor from which to locate required context. When binding to objects that /// are not in the logical tree, certain types of binding need an anchor into the tree in /// order to locate named controls or resources. The parameter - /// can be used to provice this context. + /// can be used to provide this context. /// /// Whether data validation should be enabled. /// diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index 2c60336816..b1132ff4a9 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -11,7 +11,7 @@ namespace Avalonia.Logging public static class Logger { /// - /// Gets or sets the application-defined sink that recieves the messages. + /// Gets or sets the application-defined sink that receives the messages. /// public static ILogSink Sink { get; set; } diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 66253dc5b2..9adb98e689 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Platform { diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs index b25247deaf..96661bd7ea 100644 --- a/src/Avalonia.Base/PriorityLevel.cs +++ b/src/Avalonia.Base/PriorityLevel.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Data; -using Avalonia.Logging; namespace Avalonia { diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 03094e2236..c8b434c6f9 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -12,12 +12,12 @@ using Avalonia.Utilities; namespace Avalonia { /// - /// Maintains a list of prioritised bindings together with a current value. + /// Maintains a list of prioritized bindings together with a current value. /// /// /// Bindings, in the form of s are added to the object using /// the method. With the observable is passed a priority, where lower values - /// represent higher priorites. The current is selected from the highest + /// represent higher priorities. The current is selected from the highest /// priority binding that doesn't return . Where there /// are multiple bindings registered with the same priority, the most recently added binding /// has a higher priority. Each time the value changes, the @@ -236,7 +236,7 @@ namespace Avalonia } /// - /// Updates the current and notifies all subscibers. + /// Updates the current and notifies all subscribers. /// /// The value to set. /// The priority level that the value came from. diff --git a/src/Avalonia.Base/PropertyMetadata.cs b/src/Avalonia.Base/PropertyMetadata.cs index 395aad53e4..bec562354d 100644 --- a/src/Avalonia.Base/PropertyMetadata.cs +++ b/src/Avalonia.Base/PropertyMetadata.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Avalonia.Data; namespace Avalonia diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 6af5ab63cf..278efbcd0d 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -1,11 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Threading; namespace Avalonia.Threading @@ -48,4 +43,4 @@ namespace Avalonia.Threading Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 55be8db043..56f3f7abd4 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Globalization; -using System.Text; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index ae6f599005..1b1324b1c5 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Text; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index 0a2e8e1e1b..09c50bf147 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Globalization; -using System.Text; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 81eb05b957..ab21e5fda0 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,10 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs index fd984d758a..bda85f90e2 100644 --- a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs +++ b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs @@ -2,8 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/SingleOrQueue.cs b/src/Avalonia.Base/Utilities/SingleOrQueue.cs index 4a66b72a56..643b0ba04d 100644 --- a/src/Avalonia.Base/Utilities/SingleOrQueue.cs +++ b/src/Avalonia.Base/Utilities/SingleOrQueue.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index dd93811498..7b76457049 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; @@ -103,8 +102,8 @@ namespace Avalonia.Utilities /// The type to cast to. /// The value to cast. /// The culture to use. - /// If sucessful, contains the cast value. - /// True if the cast was sucessful, otherwise false. + /// If successful, contains the cast value. + /// True if the cast was successful, otherwise false. public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) { if (value == null) @@ -208,8 +207,8 @@ namespace Avalonia.Utilities /// /// The type to cast to. /// The value to cast. - /// If sucessful, contains the cast value. - /// True if the cast was sucessful, otherwise false. + /// If successful, contains the cast value. + /// True if the cast was successful, otherwise false. public static bool TryConvertImplicit(Type to, object value, out object result) { if (value == null) @@ -315,7 +314,7 @@ namespace Avalonia.Utilities /// Determines if a type is numeric. Nullable numeric types are considered numeric. /// /// - /// True if the type is numberic; otherwise false. + /// True if the type is numeric; otherwise false. /// /// /// Boolean is not considered numeric. diff --git a/src/Avalonia.Base/Utilities/WeakTimer.cs b/src/Avalonia.Base/Utilities/WeakTimer.cs index 5c44a6d122..a4d18ca13b 100644 --- a/src/Avalonia.Base/Utilities/WeakTimer.cs +++ b/src/Avalonia.Base/Utilities/WeakTimer.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Threading; namespace Avalonia.Utilities diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index ab80e74923..adbe89aceb 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -168,14 +168,14 @@ namespace Avalonia return value; } - private DeferredSetter _defferedSetter; + private DeferredSetter _deferredSetter; public DeferredSetter Setter { get { - return _defferedSetter ?? - (_defferedSetter = new DeferredSetter()); + return _deferredSetter ?? + (_deferredSetter = new DeferredSetter()); } } } diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 83763c0836..c92d5d7694 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -57,14 +57,14 @@ namespace Avalonia.Controls public Action AfterSetupCallback { get; private set; } = builder => { }; /// - /// Gets or sets a method to call before Startis called on the . + /// Gets or sets a method to call before Start is called on the . /// public Action BeforeStartCallback { get; private set; } = builder => { }; - protected AppBuilderBase(IRuntimePlatform platform, Action platformSevices) + protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { RuntimePlatform = platform; - RuntimePlatformServicesInitializer = () => platformSevices((TAppBuilder)this); + RuntimePlatformServicesInitializer = () => platformServices((TAppBuilder)this); } /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 499b65c5b7..4c549ac7d4 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -9,7 +9,6 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; -using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 96fb9be8ac..1bc402bc2f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -352,8 +352,8 @@ namespace Avalonia.Controls private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; - private bool _itemTemplateIsFromValueMemeberBinding = true; - private bool _settingItemTemplateFromValueMemeberBinding; + private bool _itemTemplateIsFromValueMemberBinding = true; + private bool _settingItemTemplateFromValueMemberBinding; private object _selectedItem; private bool _isDropDownOpen; @@ -788,12 +788,12 @@ namespace Avalonia.Controls private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_settingItemTemplateFromValueMemeberBinding) - _itemTemplateIsFromValueMemeberBinding = false; + if (!_settingItemTemplateFromValueMemberBinding) + _itemTemplateIsFromValueMemberBinding = false; } private void OnValueMemberBindingChanged(IBinding value) { - if(_itemTemplateIsFromValueMemeberBinding) + if(_itemTemplateIsFromValueMemberBinding) { var template = new FuncDataTemplate( @@ -805,9 +805,9 @@ namespace Avalonia.Controls return control; }); - _settingItemTemplateFromValueMemeberBinding = true; + _settingItemTemplateFromValueMemberBinding = true; ItemTemplate = template; - _settingItemTemplateFromValueMemeberBinding = false; + _settingItemTemplateFromValueMemberBinding = false; } } diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 0382c8d675..5f84421c64 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; @@ -115,4 +114,4 @@ namespace Avalonia.Controls return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 029e4dadc8..8f5a32634e 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -3,14 +3,14 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using System; -using System.Collections.ObjectModel; -using System.Diagnostics; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index 0d48418683..5d883f2d14 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -3,11 +3,10 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Threading; using System; using System.Collections.ObjectModel; using System.Linq; -using System.Threading; +using Avalonia.Threading; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs index 718cc7142b..47a0ad047b 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -4,7 +4,6 @@ // All other rights reserved. using System; -using System.Diagnostics; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index f6d0fbba62..1b36f92fd3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -3,11 +3,9 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Input; using System; -using System.Collections.Generic; using System.Globalization; -using System.Text; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs index 4fda02bff3..73de1c38f3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs +++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs @@ -3,10 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using System; -using System.Collections.Generic; using Avalonia.Input; -using System.Diagnostics; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index b0cbd0be53..577555333f 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -3,13 +3,13 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using System; +using System.Diagnostics; +using System.Globalization; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using System; -using System.Diagnostics; -using System.Globalization; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 08608ad359..2270598623 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -3,15 +3,14 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -1083,7 +1082,7 @@ namespace Avalonia.Controls else { // If parse error: TextBox should have the latest valid - // selecteddate value: + // SelectedDate value: if (SelectedDate != null) { string newtext = this.DateTimeToString(SelectedDate.Value); diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index 8a80d6bdf7..5c9a97cb27 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -136,8 +136,9 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { // TODO: Implement this return null; diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 13f00bdc87..a69152c42b 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,16 +1,18 @@ +using System; +using System.Reactive.Linq; +using System.Linq; +using System.ComponentModel; +using Avalonia.Controls.Platform; +using System.Collections.Generic; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Controls.Primitives; + namespace Avalonia.Controls { - using Input; - using Interactivity; - using LogicalTree; - using Primitives; - using System; - using System.Reactive.Linq; - using System.Linq; - using System.ComponentModel; - - public class ContextMenu : SelectingItemsControl + public class ContextMenu : SelectingItemsControl, IMenu { + private readonly IMenuInteractionHandler _interaction; private bool _isOpen; private Popup _popup; @@ -20,6 +22,25 @@ namespace Avalonia.Controls public static readonly DirectProperty IsOpenProperty = AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); + /// + /// Initializes a new instance of the class. + /// + public ContextMenu() + { + _interaction = AvaloniaLocator.Current.GetService() ?? + new DefaultMenuInteractionHandler(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The menu interaction handler. + public ContextMenu(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + _interaction = interactionHandler; + } /// /// Initializes static members of the class. @@ -27,8 +48,6 @@ namespace Avalonia.Controls static ContextMenu() { ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); - - MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } /// @@ -36,6 +55,36 @@ namespace Avalonia.Controls /// public bool IsOpen => _isOpen; + /// + IMenuInteractionHandler IMenu.InteractionHandler => _interaction; + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + /// /// Occurs when the value of the /// @@ -50,7 +99,6 @@ namespace Avalonia.Controls /// public event CancelEventHandler ContextMenuClosing; - /// /// Called when the property changes on a control. /// @@ -71,62 +119,53 @@ namespace Avalonia.Controls } /// - /// Called when a submenu is clicked somewhere in the menu. + /// Opens the menu. /// - /// The event args. - private void OnContextMenuClick(RoutedEventArgs e) - { - Hide(); - FocusManager.Instance.Focus(null); - e.Handled = true; - } + public void Open() => Open(null); /// - /// Closes the menu. + /// Opens a context menu on the specified control. /// - public void Hide() + /// The control. + public void Open(Control control) { - if (_popup != null && _popup.IsVisible) + if (_popup == null) { - _popup.IsOpen = false; + _popup = new Popup() + { + PlacementMode = PlacementMode.Pointer, + PlacementTarget = control, + StaysOpen = false, + ObeyScreenEdges = true + }; + + _popup.Closed += PopupClosed; + _interaction.Attach(this); } - SelectedIndex = -1; + ((ISetLogicalParent)_popup).SetParent(control); + _popup.Child = this; + _popup.IsOpen = true; - SetAndRaise(IsOpenProperty, ref _isOpen, false); + SetAndRaise(IsOpenProperty, ref _isOpen, true); } /// - /// Shows a context menu for the specified control. + /// Closes the menu. /// - /// The control. - private void Show(Control control) + public void Close() { - if (control != null) + if (_popup != null && _popup.IsVisible) { - if (_popup == null) - { - _popup = new Popup() - { - PlacementMode = PlacementMode.Pointer, - PlacementTarget = control, - StaysOpen = false, - ObeyScreenEdges = true - }; - - _popup.Closed += PopupClosed; - } - - ((ISetLogicalParent)_popup).SetParent(control); - _popup.Child = this; + _popup.IsOpen = false; + } - _popup.IsOpen = true; + SelectedIndex = -1; - SetAndRaise(IsOpenProperty, ref _isOpen, true); - } + SetAndRaise(IsOpenProperty, ref _isOpen, false); } - private static void PopupClosed(object sender, EventArgs e) + private void PopupClosed(object sender, EventArgs e) { var contextMenu = (sender as Popup)?.Child as ContextMenu; @@ -152,7 +191,7 @@ namespace Avalonia.Controls if (contextMenu.CancelClosing()) return; - control.ContextMenu.Hide(); + control.ContextMenu.Close(); e.Handled = true; } @@ -161,7 +200,7 @@ namespace Avalonia.Controls if (contextMenu.CancelOpening()) return; - contextMenu.Show(control); + contextMenu.Open(control); e.Handled = true; } } @@ -179,5 +218,10 @@ namespace Avalonia.Controls ContextMenuOpening?.Invoke(this, eventArgs); return eventArgs.Cancel; } + + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 67288972b6..a00d586233 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,22 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Logging; -using Avalonia.LogicalTree; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; @@ -134,9 +122,9 @@ namespace Avalonia.Controls } /// - /// Gets the element that recieves the focus adorner. + /// Gets the element that receives the focus adorner. /// - /// The control that recieves the focus adorner. + /// The control that receives the focus adorner. protected virtual IControl GetTemplateFocusTarget() { return this; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index a55bd63aa8..f0d7f8257e 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index c177d43917..224af979ab 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Input; -using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Styling; using JetBrains.Annotations; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 5becdc0f61..8b39cc03b8 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Layout; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index c986c5d07c..37bb72e75a 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 5323939b50..1049d742f3 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,5 @@ using Avalonia.Animation; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index d139c95fd4..653a4f5dcb 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Generators Type ContainerType { get; } /// - /// Signalled whenever new containers are materialized. + /// Signaled whenever new containers are materialized. /// event EventHandler Materialized; @@ -110,4 +110,4 @@ namespace Avalonia.Controls.Generators /// The index of the container, or -1 if not found. int IndexFromContainer(IControl container); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs index 224fc9d17e..e2e591215e 100644 --- a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Generic; - namespace Avalonia.Controls.Generators { /// @@ -15,4 +13,4 @@ namespace Avalonia.Controls.Generators /// TreeContainerIndex Index { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 259c524d59..320d6c8faf 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq.Expressions; -using System.Reflection; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs index c9b3a55aaa..d3cf70e0f8 100644 --- a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Controls.Generators +namespace Avalonia.Controls.Generators { public class MenuItemContainerGenerator : ItemContainerGenerator { diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index 08c11e2965..24b3fc1f32 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Generators private readonly Dictionary _containerToItem = new Dictionary(); /// - /// Signalled whenever new containers are materialized. + /// Signaled whenever new containers are materialized. /// public event EventHandler Materialized; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index abcecb5b82..304c86dbf7 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index 608879812c..f6a608cd71 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.cs @@ -1,11 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -218,4 +217,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 8112b2babd..1e4c6f2c2a 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -116,27 +116,27 @@ namespace Avalonia.Controls _orientation = DetectOrientation(); - int defenitionIndex; //row or col + int definitionIndex; //row or col if (_orientation == Orientation.Vertical) { Cursor = new Cursor(StandardCursorType.SizeWestEast); _definitions = _grid.ColumnDefinitions.Cast().ToList(); - defenitionIndex = GetValue(Grid.ColumnProperty); + definitionIndex = GetValue(Grid.ColumnProperty); PseudoClasses.Add(":vertical"); } else { Cursor = new Cursor(StandardCursorType.SizeNorthSouth); - defenitionIndex = GetValue(Grid.RowProperty); + definitionIndex = GetValue(Grid.RowProperty); _definitions = _grid.RowDefinitions.Cast().ToList(); PseudoClasses.Add(":horizontal"); } - if (defenitionIndex > 0) - _prevDefinition = _definitions[defenitionIndex - 1]; + if (definitionIndex > 0) + _prevDefinition = _definitions[definitionIndex - 1]; - if (defenitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[defenitionIndex + 1]; + if (definitionIndex < _definitions.Count - 1) + _nextDefinition = _definitions[definitionIndex + 1]; } private Orientation DetectOrientation() @@ -167,4 +167,4 @@ namespace Avalonia.Controls return Orientation.Vertical; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index a59fb86fb7..95752e7875 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Input; -using Avalonia.Controls; using Avalonia.Controls.Utils; using Avalonia.Input; diff --git a/src/Avalonia.Controls/IApplicationLifecycle.cs b/src/Avalonia.Controls/IApplicationLifecycle.cs index beb97a44ae..51f554c078 100644 --- a/src/Avalonia.Controls/IApplicationLifecycle.cs +++ b/src/Avalonia.Controls/IApplicationLifecycle.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index e7f2903249..87b66d5f81 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -1,12 +1,9 @@ // 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.Templates; using Avalonia.Input; using Avalonia.Layout; -using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -23,4 +20,4 @@ namespace Avalonia.Controls { new IControl Parent { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs new file mode 100644 index 0000000000..0722a22f08 --- /dev/null +++ b/src/Avalonia.Controls/IMenu.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls.Platform; + +namespace Avalonia.Controls +{ + /// + /// Represents a or . + /// + public interface IMenu : IMenuElement + { + /// + /// Gets the menu interaction handler. + /// + IMenuInteractionHandler InteractionHandler { get; } + + /// + /// Gets a value indicating whether the menu is open. + /// + bool IsOpen { get; } + } +} diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs new file mode 100644 index 0000000000..ee9d0fd6b6 --- /dev/null +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Avalonia.Input; + +namespace Avalonia.Controls +{ + /// + /// Represents an or . + /// + public interface IMenuElement : IControl + { + /// + /// Gets or sets the currently selected submenu item. + /// + IMenuItem SelectedItem { get; set; } + + /// + /// Gets the submenu items. + /// + IEnumerable SubItems { get; } + + /// + /// Opens the menu or menu item. + /// + void Open(); + + /// + /// Closes the menu or menu item. + /// + void Close(); + + /// + /// Moves the submenu selection in the specified direction. + /// + /// The direction. + /// Whether to wrap after the first or last item. + /// True if the selection was moved; otherwise false. + bool MoveSelection(NavigationDirection direction, bool wrap); + } +} diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs new file mode 100644 index 0000000000..132d565cb7 --- /dev/null +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Controls +{ + /// + /// Represents a . + /// + public interface IMenuItem : IMenuElement + { + /// + /// Gets or sets a value that indicates whether the item has a submenu. + /// + bool HasSubMenu { get; } + + /// + /// Gets a value indicating whether the mouse is currently over the menu item's submenu. + /// + bool IsPointerOverSubMenu { get; } + + /// + /// Gets or sets a value that indicates whether the submenu of the is + /// open. + /// + bool IsSubMenuOpen { get; set; } + + /// + /// Gets a value that indicates whether the is a top-level main menu item. + /// + bool IsTopLevel { get; } + + /// + /// Gets the parent . + /// + new IMenuElement Parent { get; } + + /// + /// Raises a click event on the menu item. + /// + void RaiseClick(); + } +} diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs index 9bbd0d8518..204e918d7b 100644 --- a/src/Avalonia.Controls/IScrollable.cs +++ b/src/Avalonia.Controls/IScrollable.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 2d8dcb42e3..cba196dfa8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// Gets or sets the controller for the virtualizing panel. /// /// - /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// A virtualizing controller is responsible for maintaining the controls in the virtualizing /// panel. This property will be set by the controller when virtualization is initialized. /// Note that this property may remain null if the panel is added to a control that does /// not act as a virtualizing controller. diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3cb997f615..9d4cbb9260 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -15,6 +15,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -324,7 +325,47 @@ namespace Avalonia.Controls } /// - /// Caled when the property changes. + /// Handles directional navigation within the . + /// + /// The key events. + protected override void OnKeyDown(KeyEventArgs e) + { + if (!e.Handled) + { + var focus = FocusManager.Instance; + var direction = e.Key.ToNavigationDirection(); + var container = Presenter?.Panel as INavigableContainer; + + if (container == null || + focus.Current == null || + direction == null || + direction.Value.IsTab()) + { + return; + } + + var current = focus.Current + .GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.VisualParent == container); + + if (current != null) + { + var next = GetNextControl(container, direction.Value, current, false); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + } + } + + base.OnKeyDown(e); + } + + /// + /// Called when the property changes. /// /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) @@ -335,6 +376,7 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); SubscribeToItems(newValue); @@ -358,10 +400,8 @@ namespace Avalonia.Controls RemoveControlItemsFromLogicalChildren(e.OldItems); break; } - - int? count = (Items as IList)?.Count; - if (count != null) - ItemCount = (int)count; + + UpdateItemCount(); var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); @@ -445,5 +485,44 @@ namespace Avalonia.Controls // TODO: Rebuild the item containers. } } + + private void UpdateItemCount() + { + if (Items == null) + { + ItemCount = 0; + } + else if (Items is IList list) + { + ItemCount = list.Count; + } + else + { + ItemCount = Items.Count(); + } + } + + protected static IInputElement GetNextControl( + INavigableContainer container, + NavigationDirection direction, + IInputElement from, + bool wrap) + { + IInputElement result; + + do + { + result = container.GetControl(direction, from, wrap); + + if (result?.Focusable == true) + { + return result; + } + + from = result; + } while (from != null); + + return null; + } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 994af9dab8..00fca385a0 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -2,30 +2,23 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Rendering; namespace Avalonia.Controls { /// /// A top-level menu control. /// - public class Menu : SelectingItemsControl, IFocusScope, IMainMenu + public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu { - /// - /// Defines the default items panel used by a . - /// - private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - /// /// Defines the property. /// @@ -34,12 +27,42 @@ namespace Avalonia.Controls nameof(IsOpen), o => o.IsOpen); + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuOpenedEvent = + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuClosedEvent = + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + + private static readonly ITemplate DefaultPanel = + new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); + private readonly IMenuInteractionHandler _interaction; private bool _isOpen; /// - /// Tracks event handlers added to the root of the visual tree. + /// Initializes a new instance of the class. + /// + public Menu() + { + _interaction = AvaloniaLocator.Current.GetService() ?? + new DefaultMenuInteractionHandler(); + } + + /// + /// Initializes a new instance of the class. /// - private IDisposable _subscription; + /// The menu interaction handler. + public Menu(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + _interaction = interactionHandler; + } /// /// Initializes static members of the class. @@ -47,7 +70,6 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick, handledEventsToo: true); MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); } @@ -60,18 +82,52 @@ namespace Avalonia.Controls private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } } - /// - /// Gets the selected container. - /// - private MenuItem SelectedMenuItem + /// + IMenuInteractionHandler IMenu.InteractionHandler => _interaction; + + /// + IMenuItem IMenuElement.SelectedItem { get { var index = SelectedIndex; return (index != -1) ? - (MenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : null; } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Occurs when a is opened. + /// + public event EventHandler MenuOpened + { + add { AddHandler(MenuOpenedEvent, value); } + remove { RemoveHandler(MenuOpenedEvent, value); } + } + + /// + /// Occurs when a is closed. + /// + public event EventHandler MenuClosed + { + add { AddHandler(MenuClosedEvent, value); } + remove { RemoveHandler(MenuClosedEvent, value); } } /// @@ -79,13 +135,22 @@ namespace Avalonia.Controls /// public void Close() { - foreach (MenuItem i in this.GetLogicalChildren()) + if (IsOpen) { - i.IsSubMenuOpen = false; - } + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); + } + + IsOpen = false; + SelectedIndex = -1; - IsOpen = false; - SelectedIndex = -1; + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } } /// @@ -93,9 +158,25 @@ namespace Avalonia.Controls /// public void Open() { - SelectedIndex = 0; - SelectedMenuItem.Focus(); - IsOpen = true; + if (!IsOpen) + { + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); + } + } + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); } /// @@ -103,79 +184,27 @@ namespace Avalonia.Controls { base.OnAttachedToVisualTree(e); - var topLevel = (TopLevel)e.Root; - var window = e.Root as Window; - - if (window != null) - window.Deactivated += Deactivated; - - var pointerPress = topLevel.AddHandler( - PointerPressedEvent, - TopLevelPreviewPointerPress, - RoutingStrategies.Tunnel); - - _subscription = new CompositeDisposable( - pointerPress, - Disposable.Create(() => - { - if (window != null) - window.Deactivated -= Deactivated; - }), - InputManager.Instance.Process.Subscribe(ListenForNonClientClick)); - var inputRoot = e.Root as IInputRoot; if (inputRoot?.AccessKeyHandler != null) { inputRoot.AccessKeyHandler.MainMenu = this; } + + _interaction.Attach(this); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _subscription.Dispose(); + _interaction.Detach(this); } /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); - } - - /// - /// Called when a key is pressed within the menu. - /// - /// The event args. protected override void OnKeyDown(KeyEventArgs e) { - bool menuWasOpen = SelectedMenuItem?.IsSubMenuOpen ?? false; - - base.OnKeyDown(e); - - if (menuWasOpen) - { - // If a menu item was open and we navigate to a new one with the arrow keys, open - // that menu and select the first item. - var selection = SelectedMenuItem; - - if (selection != null && !selection.IsSubMenuOpen) - { - selection.IsSubMenuOpen = true; - selection.SelectedIndex = 0; - } - } - } - - /// - /// Called when the menu loses focus. - /// - /// The event args. - protected override void OnLostFocus(RoutedEventArgs e) - { - base.OnLostFocus(e); - SelectedItem = null; + // Don't handle here: let the interaction handler handle it. } /// @@ -184,9 +213,7 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnSubmenuOpened(RoutedEventArgs e) { - var menuItem = e.Source as MenuItem; - - if (menuItem != null && menuItem.Parent == this) + if (e.Source is MenuItem menuItem && menuItem.Parent == this) { foreach (var child in this.GetLogicalChildren().OfType()) { @@ -199,58 +226,5 @@ namespace Avalonia.Controls IsOpen = true; } - - /// - /// Called when the top-level window is deactivated. - /// - /// The sender. - /// The event args. - private void Deactivated(object sender, EventArgs e) - { - Close(); - } - - /// - /// Listens for non-client clicks and closes the menu when one is detected. - /// - /// The raw event. - private void ListenForNonClientClick(RawInputEventArgs e) - { - var mouse = e as RawMouseEventArgs; - - if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) - { - Close(); - } - } - - /// - /// Called when a submenu is clicked somewhere in the menu. - /// - /// The event args. - private void OnMenuClick(RoutedEventArgs e) - { - Close(); - FocusManager.Instance.Focus(null); - e.Handled = true; - } - - /// - /// Called when the pointer is pressed anywhere on the window. - /// - /// The sender. - /// The event args. - private void TopLevelPreviewPointerPress(object sender, PointerPressedEventArgs e) - { - if (IsOpen) - { - var control = e.Source as ILogical; - - if (!this.IsLogicalParentOf(control)) - { - Close(); - } - } - } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 96f6fb59b0..7b57783c5a 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; @@ -11,14 +12,13 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Threading; namespace Avalonia.Controls { /// /// A menu item control. /// - public class MenuItem : HeaderedSelectingItemsControl, ISelectable + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// /// Defines the property. @@ -62,6 +62,18 @@ namespace Avalonia.Controls public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerEnterItemEvent = + RoutedEvent.Register(nameof(PointerEnterItem), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerLeaveItemEvent = + RoutedEvent.Register(nameof(PointerLeaveItem), RoutingStrategies.Bubble); + /// /// Defines the event. /// @@ -72,15 +84,7 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - }); - - /// - /// The timer used to display submenus. - /// - private IDisposable _submenuTimer; + new FuncTemplate(() => new StackPanel()); /// /// The submenu popup. @@ -96,16 +100,15 @@ namespace Avalonia.Controls CommandProperty.Changed.Subscribe(CommandChanged); FocusableProperty.OverrideDefaultValue(true); IconProperty.Changed.AddClassHandler(x => x.IconChanged); + IsSelectedProperty.Changed.AddClassHandler(x => x.IsSelectedChanged); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); - AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler(x => x.AccessKeyPressed); } public MenuItem() { - } /// @@ -117,6 +120,30 @@ namespace Avalonia.Controls remove { RemoveHandler(ClickEvent, value); } } + /// + /// Occurs when the pointer enters a menu item. + /// + /// + /// A bubbling version of the event for menu items. + /// + public event EventHandler PointerEnterItem + { + add { AddHandler(PointerEnterItemEvent, value); } + remove { RemoveHandler(PointerEnterItemEvent, value); } + } + + /// + /// Raised when the pointer leaves a menu item. + /// + /// + /// A bubbling version of the event for menu items. + /// + public event EventHandler PointerLeaveItem + { + add { AddHandler(PointerLeaveItemEvent, value); } + remove { RemoveHandler(PointerLeaveItemEvent, value); } + } + /// /// Occurs when a 's submenu is opened. /// @@ -188,10 +215,71 @@ namespace Avalonia.Controls public bool HasSubMenu => !Classes.Contains(":empty"); /// - /// Gets a value that indicates whether the is a top-level menu item. + /// Gets a value that indicates whether the is a top-level main menu item. /// public bool IsTopLevel => Parent is Menu; + /// + bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false; + + /// + IMenuElement IMenuItem.Parent => Parent as IMenuElement; + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Opens the submenu. + /// + /// + /// This has the same effect as setting to true. + /// + public void Open() => IsSubMenuOpen = true; + + /// + /// Closes the submenu. + /// + /// + /// This has the same effect as setting to false. + /// + public void Close() => IsSubMenuOpen = false; + + /// + void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } + /// /// Called when the is clicked. /// @@ -205,163 +293,43 @@ namespace Avalonia.Controls } } - /// - /// Called when the recieves focus. - /// - /// The event args. + /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - IsSelected = true; + e.Handled = UpdateSelectionFromEventSource(e.Source, true); } /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new MenuItemContainerGenerator(this); - } - - /// - /// Called when a key is pressed in the . - /// - /// The event args. protected override void OnKeyDown(KeyEventArgs e) { - // Some keypresses we want to pass straight to the parent MenuItem/Menu without giving - // this MenuItem the chance to handle them. This is usually e.g. when the submenu is - // closed so passing them to the base would try to move the selection in a hidden - // submenu. - var passStraightToParent = true; - - switch (e.Key) - { - case Key.Left: - if (!IsTopLevel && IsSubMenuOpen) - { - IsSubMenuOpen = false; - e.Handled = true; - } - - passStraightToParent = IsTopLevel || !IsSubMenuOpen; - break; - - case Key.Right: - if (!IsTopLevel && HasSubMenu && !IsSubMenuOpen) - { - SelectedIndex = 0; - IsSubMenuOpen = true; - e.Handled = true; - } - - passStraightToParent = IsTopLevel || !IsSubMenuOpen; - break; - - case Key.Enter: - if (HasSubMenu) - { - goto case Key.Right; - } - else - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - e.Handled = true; - } - - break; - - case Key.Escape: - if (IsSubMenuOpen) - { - IsSubMenuOpen = false; - e.Handled = true; - } - - break; - } - - if (!passStraightToParent) - { - base.OnKeyDown(e); - } + // Don't handle here: let event bubble up to menu. } - /// - /// Called when the pointer enters the . - /// - /// The event args. + /// protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); - var menu = Parent as Menu; - - if (menu != null) - { - if (menu.IsOpen) - { - IsSubMenuOpen = true; - } - } - else if (HasSubMenu && !IsSubMenuOpen) + RaiseEvent(new PointerEventArgs { - _submenuTimer = DispatcherTimer.Run( - () => IsSubMenuOpen = true, - TimeSpan.FromMilliseconds(400)); - } - else - { - var parentItem = Parent as MenuItem; - if (parentItem != null) - { - foreach (var sibling in parentItem.Items - .OfType() - .Where(x => x != this && x.IsSubMenuOpen)) - { - sibling.CloseSubmenus(); - sibling.IsSubMenuOpen = false; - sibling.IsSelected = false; - } - } - } + Device = e.Device, + RoutedEvent = PointerEnterItemEvent, + Source = this, + }); } - /// - /// Called when the pointer leaves the . - /// - /// The event args. + /// protected override void OnPointerLeave(PointerEventArgs e) { base.OnPointerLeave(e); - if (_submenuTimer != null) + RaiseEvent(new PointerEventArgs { - _submenuTimer.Dispose(); - _submenuTimer = null; - } - } - - /// - /// Called when the pointer is pressed over the . - /// - /// The event args. - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - if (!HasSubMenu) - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - else if (IsTopLevel) - { - IsSubMenuOpen = !IsSubMenuOpen; - } - else - { - IsSubMenuOpen = true; - } - - e.Handled = true; + Device = e.Device, + RoutedEvent = PointerLeaveItemEvent, + Source = this, + }); } /// @@ -374,7 +342,7 @@ namespace Avalonia.Controls if (menuItem != null && menuItem.Parent == this) { - foreach (var child in Items.OfType()) + foreach (var child in ((IMenuItem)this).SubItems) { if (child != menuItem && child.IsSubMenuOpen) { @@ -395,31 +363,12 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } - /// - /// Called when the menu item's access key is pressed. - /// - /// The event args. - private void AccessKeyPressed(RoutedEventArgs e) - { - if (HasSubMenu) - { - SelectedIndex = 0; - IsSubMenuOpen = true; - } - else - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - - e.Handled = true; - } - /// /// Closes all submenus of the menu item. /// private void CloseSubmenus() { - foreach (var child in Items.OfType()) + foreach (var child in ((IMenuItem)this).SubItems) { child.IsSubMenuOpen = false; } @@ -479,6 +428,18 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The property change event. + private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + { + Focus(); + } + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index 95193c0432..c4da00f5d0 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -3,13 +3,13 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Styling; namespace Avalonia.Controls.Mixins { @@ -75,6 +75,12 @@ namespace Avalonia.Controls.Mixins null, presenter.GetValue(ContentPresenter.ChildProperty)); + if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription)) + { + subscription = new CompositeDisposable(previousSubscription, subscription); + subscriptions.Value.Remove(sender); + } + subscriptions.Value.Add(sender, subscription); } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 59d2949b81..de68eb0ab0 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -526,7 +526,7 @@ namespace Avalonia.Controls return result; } - // Since the conversion from Value to text using a FormartString may not be parsable, + // Since the conversion from Value to text using a FormatString may not be parsable, // we verify that the already existing text is not the exact same value. var currentValueText = ConvertValueToText(); if (Equals(currentValueText, text)) @@ -571,7 +571,7 @@ namespace Avalonia.Controls } /// - /// Called by OnSpin when the spin direction is SpinDirection.Descrease. + /// Called by OnSpin when the spin direction is SpinDirection.Decrease. /// private void OnDecrement() { @@ -890,7 +890,7 @@ namespace Avalonia.Controls if (_isTextChangedFromUI && !parsedTextIsValid) { // Text input was made from the user and the text - // repesents an invalid value. Disable the spinner in this case. + // represents an invalid value. Disable the spinner in this case. if (Spinner != null) { Spinner.ValidSpinDirection = ValidSpinDirections.None; @@ -995,4 +995,4 @@ namespace Avalonia.Controls return false; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs new file mode 100644 index 0000000000..a44495b90c --- /dev/null +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -0,0 +1,459 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.Controls.Platform +{ + /// + /// Provides the default keyboard and pointer interaction for menus. + /// + public class DefaultMenuInteractionHandler : IMenuInteractionHandler + { + private IDisposable _inputManagerSubscription; + private IRenderRoot _root; + + public DefaultMenuInteractionHandler() + : this(Input.InputManager.Instance, DefaultDelayRun) + { + } + + public DefaultMenuInteractionHandler( + IInputManager inputManager, + Action delayRun) + { + InputManager = inputManager; + DelayRun = delayRun; + } + + public virtual void Attach(IMenu menu) + { + if (Menu != null) + { + throw new NotSupportedException("DefaultMenuInteractionHandler is already attached."); + } + + Menu = menu; + Menu.GotFocus += GotFocus; + Menu.LostFocus += LostFocus; + Menu.KeyDown += KeyDown; + Menu.PointerPressed += PointerPressed; + Menu.PointerReleased += PointerReleased; + Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); + Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); + Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + + _root = Menu.VisualRoot; + + if (_root is InputElement inputRoot) + { + inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel); + } + + if (_root is WindowBase window) + { + window.Deactivated += WindowDeactivated; + } + + _inputManagerSubscription = InputManager.Process.Subscribe(RawInput); + } + + public virtual void Detach(IMenu menu) + { + if (Menu != menu) + { + throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu."); + } + + Menu.GotFocus -= GotFocus; + Menu.LostFocus -= LostFocus; + Menu.KeyDown -= KeyDown; + Menu.PointerPressed -= PointerPressed; + Menu.PointerReleased -= PointerReleased; + Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); + Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); + Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + + if (_root is InputElement inputRoot) + { + inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed); + } + + if (_root is WindowBase root) + { + root.Deactivated -= WindowDeactivated; + } + + _inputManagerSubscription.Dispose(); + + Menu = null; + _root = null; + } + + protected Action DelayRun { get; } + + protected IInputManager InputManager { get; } + + protected IMenu Menu { get; private set; } + + protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); + + protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent != null) + { + item.SelectedItem = item; + } + } + + protected internal virtual void LostFocus(object sender, RoutedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item != null) + { + item.SelectedItem = null; + } + } + + protected internal virtual void KeyDown(object sender, KeyEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item != null) + { + KeyDown(item, e); + } + } + + protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) + { + Contract.Requires(item != null); + + switch (e.Key) + { + case Key.Up: + case Key.Down: + if (item.IsTopLevel) + { + if (item.HasSubMenu && !item.IsSubMenuOpen) + { + Open(item, true); + e.Handled = true; + } + } + else + { + goto default; + } + break; + + case Key.Left: + if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + { + parent.Close(); + parent.Focus(); + e.Handled = true; + } + else + { + goto default; + } + break; + + case Key.Right: + if (!item.IsTopLevel && item.HasSubMenu) + { + Open(item, true); + e.Handled = true; + } + else + { + goto default; + } + break; + + case Key.Enter: + if (!item.HasSubMenu) + { + Click(item); + } + else + { + Open(item, true); + } + + e.Handled = true; + break; + + case Key.Escape: + if (item.Parent != null) + { + item.Parent.Close(); + item.Parent.Focus(); + e.Handled = true; + } + break; + + default: + var direction = e.Key.ToNavigationDirection(); + + if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true) + { + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) + { + item.Close(); + Open(item.Parent.SelectedItem, true); + } + e.Handled = true; + } + + break; + } + + if (!e.Handled && item.Parent is IMenuItem parentItem) + { + KeyDown(parentItem, e); + } + } + + protected internal virtual void AccessKeyPressed(object sender, RoutedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item == null) + { + return; + } + + if (item.HasSubMenu) + { + Open(item, true); + } + else + { + Click(item); + } + + e.Handled = true; + } + + protected internal virtual void PointerEnter(object sender, PointerEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent == null) + { + return; + } + + if (item.IsTopLevel) + { + if (item.Parent.SelectedItem?.IsSubMenuOpen == true) + { + item.Parent.SelectedItem.Close(); + SelectItemAndAncestors(item); + Open(item, false); + } + else + { + SelectItemAndAncestors(item); + } + } + else + { + SelectItemAndAncestors(item); + + if (item.HasSubMenu) + { + OpenWithDelay(item); + } + else if (item.Parent != null) + { + foreach (var sibling in item.Parent.SubItems) + { + if (sibling.IsSubMenuOpen) + { + CloseWithDelay(sibling); + } + } + } + } + } + + protected internal virtual void PointerLeave(object sender, PointerEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent == null) + { + return; + } + + if (item.Parent.SelectedItem == item) + { + if (item.IsTopLevel) + { + if (!((IMenu)item.Parent).IsOpen) + { + item.Parent.SelectedItem = null; + } + } + else if (!item.HasSubMenu) + { + item.Parent.SelectedItem = null; + } + } + } + + protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == true) + { + Open(item, false); + e.Handled = true; + } + } + + protected internal virtual void PointerReleased(object sender, PointerReleasedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (e.MouseButton == MouseButton.Left && item.HasSubMenu == false) + { + Click(item); + e.Handled = true; + } + } + + protected internal virtual void MenuOpened(object sender, RoutedEventArgs e) + { + if (e.Source == Menu) + { + Menu.MoveSelection(NavigationDirection.First, true); + } + } + + protected internal virtual void RawInput(RawInputEventArgs e) + { + var mouse = e as RawMouseEventArgs; + + if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + { + Menu.Close(); + } + } + + protected internal virtual void RootPointerPressed(object sender, PointerPressedEventArgs e) + { + if (Menu?.IsOpen == true) + { + var control = e.Source as ILogical; + + if (!Menu.IsLogicalParentOf(control)) + { + Menu.Close(); + } + } + } + + protected internal virtual void WindowDeactivated(object sender, EventArgs e) + { + Menu.Close(); + } + + protected void Click(IMenuItem item) + { + item.RaiseClick(); + CloseMenu(item); + } + + protected void CloseMenu(IMenuItem item) + { + var current = (IMenuElement)item; + + while (current != null && !(current is IMenu)) + { + current = (current as IMenuItem)?.Parent; + } + + current?.Close(); + } + + protected void CloseWithDelay(IMenuItem item) + { + void Execute() + { + if (item.Parent?.SelectedItem != item) + { + item.Close(); + } + } + + DelayRun(Execute, MenuShowDelay); + } + + protected void Open(IMenuItem item, bool selectFirst) + { + item.Open(); + + if (selectFirst) + { + item.MoveSelection(NavigationDirection.First, true); + } + } + + protected void OpenWithDelay(IMenuItem item) + { + void Execute() + { + if (item.Parent?.SelectedItem == item) + { + Open(item, false); + } + } + + DelayRun(Execute, MenuShowDelay); + } + + protected void SelectItemAndAncestors(IMenuItem item) + { + var current = item; + + while (current?.Parent != null) + { + current.Parent.SelectedItem = current; + current = current.Parent as IMenuItem; + } + } + + protected static IMenuItem GetMenuItem(IControl item) + { + while (true) + { + if (item == null) + return null; + if (item is IMenuItem menuItem) + return menuItem; + item = item.Parent; + } + } + + private static void DefaultDelayRun(Action action, TimeSpan timeSpan) + { + DispatcherTimer.RunOnce(action, timeSpan); + } + } +} diff --git a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs index 420c56111f..e958b7aa15 100644 --- a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Platform { diff --git a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs new file mode 100644 index 0000000000..dd8503f768 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Controls.Platform +{ + /// + /// Handles user interaction for menus. + /// + public interface IMenuInteractionHandler + { + /// + /// Attaches the interaction handler to a menu. + /// + /// The menu. + void Attach(IMenu menu); + + /// + /// Detaches the interaction handler from the attached menu. + /// + void Detach(IMenu menu); + } +} diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index a84a58906e..ecbc6d2234 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; +using System.IO; namespace Avalonia.Platform { diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 9ba68f584e..e788b3c73e 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform public interface IWindowBaseImpl : ITopLevelImpl { /// - /// Shows the toplevel. + /// Shows the top level. /// void Show(); @@ -62,7 +62,7 @@ namespace Avalonia.Platform Size MaxClientSize { get; } /// - /// Sets the client size of the toplevel. + /// Sets the client size of the top level. /// void Resize(Size clientSize); @@ -82,4 +82,4 @@ namespace Avalonia.Platform /// IScreenImpl Screen { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5dcd0a39e8..5c2c1a8da3 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Avalonia.Platform { public interface IWindowingPlatform diff --git a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs index 4dc96a074d..62cd012d51 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; +using Avalonia.Platform; namespace Avalonia.Controls.Platform.Surfaces { diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 1d5a187a73..ba2c7c91d8 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Animation; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Data; @@ -251,4 +250,4 @@ namespace Avalonia.Controls.Presenters } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/IPresenter.cs b/src/Avalonia.Controls/Presenters/IPresenter.cs index e06be4e82b..f78ba4c19d 100644 --- a/src/Avalonia.Controls/Presenters/IPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IPresenter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls.Primitives; -using Avalonia.Styling; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index e293cff211..c5344b29d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,7 +8,6 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; -using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b98f26b87f..f31a48b2a0 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -390,7 +390,7 @@ namespace Avalonia.Controls.Presenters /// The delta of the move. /// /// If the move is less than a page, then this method moves the containers for the items - /// that are still visible to the correct place, and recyles and moves the others. For + /// that are still visible to the correct place, and recycles and moves the others. For /// example: if there are 20 items and 10 containers visible and the user scrolls 5 /// items down, then the bottom 5 containers will be moved to the top and the top 5 will /// be moved to the bottom and recycled to display the newly visible item. Updates diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index f8d62a1cbf..500c7aa187 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -143,13 +143,6 @@ namespace Avalonia.Controls.Presenters Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); - if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) - { - KeyboardNavigation.SetDirectionalNavigation( - (InputElement)Panel, - KeyboardNavigationMode.Contained); - } - KeyboardNavigation.SetTabNavigation( (InputElement)Panel, KeyboardNavigation.GetTabNavigation(this)); @@ -175,4 +168,4 @@ namespace Avalonia.Controls.Presenters ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 8d0c6f16cb..2ef7941b55 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,7 +7,6 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -319,4 +318,4 @@ namespace Avalonia.Controls.Presenters } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 636c836da5..3181e76c3f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using Avalonia.Media; -using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; @@ -116,7 +115,7 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - // issue #600: set constaint before any FormattedText manipulation + // issue #600: set constraint before any FormattedText manipulation // see base.Render(...) implementation FormattedText.Constraint = Bounds.Size; diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 82ab929d53..c5aa73e56a 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Linq; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 005717d681..5e79711f5a 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,11 +6,10 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Rendering; using Avalonia.VisualTree; -using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -473,4 +472,4 @@ namespace Avalonia.Controls.Primitives } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 0ae4be5550..fdec9febd3 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,12 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; using Avalonia.Interactivity; -using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index b46562e99e..76df94cdb8 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -130,7 +130,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double valus is NaN or Inf. + /// Throws an exception if the double value is NaN or Inf. /// /// The value. /// The name of the property being set. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c8425a0f80..bb39b005cc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -11,7 +11,6 @@ using Avalonia.Controls.Generators; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Metadata; using Avalonia.Styling; using Avalonia.VisualTree; @@ -457,6 +456,42 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Moves the selection in the specified direction relative to the current selection. + /// + /// The direction to move. + /// Whether to wrap when the selection reaches the first or last item. + /// True if the selection was moved; otherwise false. + protected bool MoveSelection(NavigationDirection direction, bool wrap) + { + var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null; + return MoveSelection(from, direction, wrap); + } + + /// + /// Moves the selection in the specified direction relative to the specified container. + /// + /// The container which serves as a starting point for the movement. + /// The direction to move. + /// Whether to wrap when the selection reaches the first or last item. + /// True if the selection was moved; otherwise false. + protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap) + { + if (Presenter?.Panel is INavigableContainer container && + GetNextControl(container, direction, from, wrap) is IControl next) + { + var index = ItemContainerGenerator.IndexFromContainer(next); + + if (index != -1) + { + SelectedIndex = index; + return true; + } + } + + return false; + } + /// /// Updates the selection for an item based on user interaction. /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index b0f9ab1f85..b01ddd5dba 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -4,7 +4,6 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index b7db352c74..085db75ce1 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,12 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive.Linq; -using Avalonia.Animation; using Avalonia.Controls.Primitives; -using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index 9c0f3464a5..e116316904 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Embedding; using Avalonia.Controls.Remote.Server; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index ea8c3ebe52..2d4f2e6b52 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Remote.Protocol; @@ -76,4 +75,4 @@ namespace Avalonia.Controls.Remote base.Render(context); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index cf4cec9268..b302f2f5ec 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -94,10 +91,10 @@ namespace Avalonia.Controls.Remote.Server RenderIfNeeded(); } - protected virtual Size Measure(Size constaint) + protected virtual Size Measure(Size constraint) { var l = (ILayoutable) InputRoot; - l.Measure(constaint); + l.Measure(constraint); return l.DesiredSize; } diff --git a/src/Avalonia.Controls/Separator.cs b/src/Avalonia.Controls/Separator.cs index 2028a5cfbb..84b2a33d7b 100644 --- a/src/Avalonia.Controls/Separator.cs +++ b/src/Avalonia.Controls/Separator.cs @@ -1,11 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Primitives; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 08bed79b3a..e0952d3e9b 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/src/Avalonia.Controls/Shapes/Path.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Avalonia.Media; namespace Avalonia.Controls.Shapes diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index a1848a95b1..604051ef28 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -4,7 +4,6 @@ using System; using System.Reflection; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Media; namespace Avalonia.Controls.Shapes diff --git a/src/Avalonia.Controls/Spinner.cs b/src/Avalonia.Controls/Spinner.cs index e00ff3823c..5ee13f45c4 100644 --- a/src/Avalonia.Controls/Spinner.cs +++ b/src/Avalonia.Controls/Spinner.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Interactivity; namespace Avalonia.Controls @@ -54,7 +52,7 @@ namespace Avalonia.Controls public SpinDirection Direction { get; } /// - /// Get or set whheter the spin event originated from a mouse wheel event. + /// Get or set whether the spin event originated from a mouse wheel event. /// public bool UsingMouseWheel{ get; } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index b0ccd8a3d1..645cdbd926 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -56,11 +56,49 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { - var fromControl = from as IControl; - return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + var result = GetControlInDirection(direction, from as IControl); + + if (result == null && wrap) + { + if (Orientation == Orientation.Vertical) + { + switch (direction) + { + case NavigationDirection.Up: + case NavigationDirection.Previous: + case NavigationDirection.PageUp: + result = GetControlInDirection(NavigationDirection.Last, null); + break; + case NavigationDirection.Down: + case NavigationDirection.Next: + case NavigationDirection.PageDown: + result = GetControlInDirection(NavigationDirection.First, null); + break; + } + } + else + { + switch (direction) + { + case NavigationDirection.Left: + case NavigationDirection.Previous: + case NavigationDirection.PageUp: + result = GetControlInDirection(NavigationDirection.Last, null); + break; + case NavigationDirection.Right: + case NavigationDirection.Next: + case NavigationDirection.PageDown: + result = GetControlInDirection(NavigationDirection.First, null); + break; + } + } + } + + return result; } /// @@ -72,7 +110,7 @@ namespace Avalonia.Controls protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; - int index = Children.IndexOf((IControl)from); + int index = from != null ? Children.IndexOf(from) : -1; switch (direction) { @@ -83,22 +121,22 @@ namespace Avalonia.Controls index = Children.Count - 1; break; case NavigationDirection.Next: - ++index; + if (index != -1) ++index; break; case NavigationDirection.Previous: - --index; + if (index != -1) --index; break; case NavigationDirection.Left: - index = horiz ? index - 1 : -1; + if (index != -1) index = horiz ? index - 1 : -1; break; case NavigationDirection.Right: - index = horiz ? index + 1 : -1; + if (index != -1) index = horiz ? index + 1 : -1; break; case NavigationDirection.Up: - index = horiz ? -1 : index - 1; + if (index != -1) index = horiz ? -1 : index - 1; break; case NavigationDirection.Down: - index = horiz ? -1 : index + 1; + if (index != -1) index = horiz ? -1 : index + 1; break; default: index = -1; diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 7154c0e558..9339aa6924 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Templates } /// - /// Casts a stongly typed match function to a weakly typed one. + /// Casts a strongly typed match function to a weakly typed one. /// /// The strongly typed function. /// The weakly typed function. @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Templates } /// - /// Casts a stongly typed build function to a weakly typed one. + /// Casts a strongly typed build function to a weakly typed one. /// /// The strong data type. /// The strongly typed function. @@ -62,4 +62,4 @@ namespace Avalonia.Controls.Templates return o => f((T)o); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs index b8f2c2e311..5ab186261e 100644 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs @@ -25,11 +25,11 @@ namespace Avalonia.Controls.Templates /// /// Selects a member of an object. /// - /// The obeject. + /// The object. /// The selected member. public object Select(object o) { return (o is TObject) ? _selector((TObject)o) : default(TMember); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index 2e55dd3552..e7c9cf8608 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -9,7 +9,7 @@ using Avalonia.Data; namespace Avalonia.Controls.Templates { /// - /// A template used to build hierachical data. + /// A template used to build hierarchical data. /// public class FuncTreeDataTemplate : FuncDataTemplate, ITreeDataTemplate { diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index d7d6a87769..4ca96f60bd 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -7,7 +7,7 @@ using System.Collections; namespace Avalonia.Controls.Templates { /// - /// A template used to build hierachical data. + /// A template used to build hierarchical data. /// /// The type of the template's data. public class FuncTreeDataTemplate : FuncTreeDataTemplate diff --git a/src/Avalonia.Controls/Templates/IDataTemplateHost.cs b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs index 5cc12581d4..267938b3bd 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplateHost.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; namespace Avalonia.Controls.Templates { diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs index b7172fa492..e1ec42a849 100644 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ b/src/Avalonia.Controls/Templates/IMemberSelector.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls.Templates /// /// Selects a member of an object. /// - /// The obeject. + /// The object. /// The selected member. object Select(object o); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs b/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs index 580839be97..9ed8987681 100644 --- a/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs @@ -6,7 +6,7 @@ using Avalonia.Data; namespace Avalonia.Controls.Templates { /// - /// Interface representing a template used to build hierachical data. + /// Interface representing a template used to build hierarchical data. /// public interface ITreeDataTemplate : IDataTemplate { @@ -20,4 +20,4 @@ namespace Avalonia.Controls.Templates /// InstancedBinding ItemsSelector(object item); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index fd46efa76f..e91d2e8fa7 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); - // TODO: Define these attached properties elswhere (e.g. on a Text class) and AddOwner + // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // them into TextBlock. /// @@ -406,4 +406,4 @@ namespace Avalonia.Controls InvalidateMeasure(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 10e964d014..28d1ba5e0f 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached("ShowDelay", 400); /// - /// Stores the curernt instance in the control. + /// Stores the current instance in the control. /// private static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index bfd7ef0f33..384a9db0cf 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -5,7 +5,7 @@ using Avalonia.Threading; namespace Avalonia.Controls { /// - /// Handeles interaction with controls. + /// Handles interaction with controls. /// internal sealed class ToolTipService { @@ -95,4 +95,4 @@ namespace Avalonia.Controls _timer = null; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 9fdc097c3f..1161ded25f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -225,7 +223,7 @@ namespace Avalonia.Controls protected virtual IRenderTarget CreateRenderTarget() { if(PlatformImpl == null) - throw new InvalidOperationException("Cann't create render target, PlatformImpl is null (might be already disposed)"); + throw new InvalidOperationException("Can't create render target, PlatformImpl is null (might be already disposed)"); return _renderInterface.CreateRenderTarget(PlatformImpl.Surfaces); } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2e1c011685..3003dab85e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; @@ -14,7 +13,7 @@ using Avalonia.VisualTree; namespace Avalonia.Controls { /// - /// Displays a hierachical tree of data. + /// Displays a hierarchical tree of data. /// public class TreeView : ItemsControl, ICustomKeyboardNavigation { @@ -136,6 +135,92 @@ namespace Avalonia.Controls } } + protected override void OnKeyDown(KeyEventArgs e) + { + var direction = e.Key.ToNavigationDirection(); + + if (direction?.IsDirectional() == true && !e.Handled) + { + if (SelectedItem != null) + { + var next = GetContainerInDirection( + GetContainerFromEventSource(e.Source) as TreeViewItem, + direction.Value, + true); + + if (next != null) + { + FocusManager.Instance.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + } + else + { + SelectedItem = ElementAt(Items, 0); + } + } + } + + private TreeViewItem GetContainerInDirection( + TreeViewItem from, + NavigationDirection direction, + bool intoChildren) + { + IItemContainerGenerator parentGenerator; + + if (from?.Parent is TreeView treeView) + { + parentGenerator = treeView.ItemContainerGenerator; + } + else if (from?.Parent is TreeViewItem item) + { + parentGenerator = item.ItemContainerGenerator; + } + else + { + return null; + } + + var index = parentGenerator.IndexFromContainer(from); + var parent = from.Parent as ItemsControl; + TreeViewItem result = null; + + switch (direction) + { + case NavigationDirection.Up: + if (index > 0) + { + var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1); + result = previous.IsExpanded ? + (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) : + previous; + } + else + { + result = from.Parent as TreeViewItem; + } + + break; + + case NavigationDirection.Down: + if (from.IsExpanded && intoChildren) + { + result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0); + } + else if (index < parent?.ItemCount - 1) + { + result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1); + } + else if (parent is TreeViewItem parentItem) + { + return GetContainerInDirection(parentItem, direction, false); + } + break; + } + + return result; + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index bed27ef033..0886c05038 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -32,10 +32,7 @@ namespace Avalonia.Controls ListBoxItem.IsSelectedProperty.AddOwner(); private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - }); + new FuncTemplate(() => new StackPanel()); private TreeView _treeView; private bool _isExpanded; @@ -127,7 +124,7 @@ namespace Avalonia.Controls } } - base.OnKeyDown(e); + // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } } } diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 6ee284e05d..d02202ca02 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 10a94a8c82..363428b289 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -376,7 +376,7 @@ namespace Avalonia.Controls.Utils // 1. Determine all one-span column's desired widths or row's desired heights. // 2. Order the multi-span conventions by its last index // (Notice that the sorted data is much smaller than the source.) - // 3. Determine each multi-span last index by calculating the maximun desired size. + // 3. Determine each multi-span last index by calculating the maximum desired size. // Before we determine the behavior of this method, we just aggregate the one-span * columns. diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 361857aeb7..40ebd406c3 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Globalization; using System.Linq; namespace Avalonia.Controls.Utils diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 43c8a5aa6c..4d814170c6 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -4,15 +4,12 @@ // All other rights reserved. using System; +using System.Collections; using System.Linq; -using System.Collections.Generic; -using System.Text; using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; -using System.Collections; -using System.Diagnostics; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index bd07cf6740..39b4f05545 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Icon)); /// - /// Defines the proeprty. + /// Defines the property. /// public static readonly DirectProperty WindowStartupLocationProperty = AvaloniaProperty.RegisterDirect( @@ -100,7 +100,7 @@ namespace Avalonia.Controls private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; - private WindowStartupLocation _windowStartupLoction; + private WindowStartupLocation _windowStartupLocation; /// /// Initializes static members of the class. @@ -237,8 +237,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get { return _windowStartupLoction; } - set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLoction, value); } + get { return _windowStartupLocation; } + set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } } /// @@ -409,7 +409,7 @@ namespace Avalonia.Controls /// The type of the result produced by the dialog. /// /// . - /// A task that can be used to retrive the result of the dialog when it closes. + /// A task that can be used to retrieve the result of the dialog when it closes. /// public Task ShowDialog() { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c0b664ebc3..f609432545 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; diff --git a/src/Avalonia.Controls/WindowIcon.cs b/src/Avalonia.Controls/WindowIcon.cs index 26195e6d76..9e4329372a 100644 --- a/src/Avalonia.Controls/WindowIcon.cs +++ b/src/Avalonia.Controls/WindowIcon.cs @@ -1,11 +1,6 @@ -using Avalonia.Platform; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 745de95bca..8ee0636124 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -47,8 +47,9 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); @@ -131,7 +132,7 @@ namespace Avalonia.Controls double accumulatedV = 0; var uvFinalSize = CreateUVSize(finalSize); var lineSize = CreateUVSize(); - int firstChildInLineindex = 0; + int firstChildInLineIndex = 0; for (int index = 0; index < Children.Count; index++) { var child = Children[index]; @@ -143,32 +144,32 @@ namespace Avalonia.Controls } else // moving to next line { - var controlsInLine = GetContolsBetween(firstChildInLineindex, index); + var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); ArrangeLine(accumulatedV, lineSize.V, controlsInLine); accumulatedV += lineSize.V; lineSize = childSize; - firstChildInLineindex = index; + firstChildInLineIndex = index; } } - if (firstChildInLineindex < Children.Count) + if (firstChildInLineIndex < Children.Count) { - var controlsInLine = GetContolsBetween(firstChildInLineindex, Children.Count); + var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); ArrangeLine(accumulatedV, lineSize.V, controlsInLine); } return finalSize; } - private IEnumerable GetContolsBetween(int first, int last) + private IEnumerable GetControlsBetween(int first, int last) { return Children.Skip(first).Take(last - first); } - private void ArrangeLine(double v, double lineV, IEnumerable contols) + private void ArrangeLine(double v, double lineV, IEnumerable controls) { double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - foreach (var child in contols) + foreach (var child in controls) { var childSize = CreateUVSize(child.DesiredSize); var x = isHorizontal ? u : v; @@ -180,9 +181,9 @@ namespace Avalonia.Controls } } /// - /// Used to not not write sepearate code for horizontal and vertical orientation. + /// Used to not not write separate code for horizontal and vertical orientation. /// U is direction in line. (x if orientation is horizontal) - /// V is direction of lines. (y if orientation is horizonral) + /// V is direction of lines. (y if orientation is horizontal) /// [DebuggerDisplay("U = {U} V = {V}")] private struct UVSize @@ -250,4 +251,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 7baa4103d7..b78e5a9f98 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -53,10 +53,32 @@ namespace Avalonia.Input /// private IInputElement _restoreFocusElement; + /// + /// The window's main menu. + /// + private IMainMenu _mainMenu; + /// /// Gets or sets the window's main menu. /// - public IMainMenu MainMenu { get; set; } + public IMainMenu MainMenu + { + get => _mainMenu; + set + { + if (_mainMenu != null) + { + _mainMenu.MenuClosed -= MainMenuClosed; + } + + _mainMenu = value; + + if (_mainMenu != null) + { + _mainMenu.MenuClosed += MainMenuClosed; + } + } + } /// /// Sets the owner of the access key handler. @@ -160,13 +182,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.Key == Key.Escape && menuIsOpen) - { - // When the Escape key is pressed with the main menu open, close it. - CloseMenu(); - e.Handled = true; - } - else if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. @@ -245,5 +261,10 @@ namespace Avalonia.Input MainMenu.Close(); _owner.ShowAccessKeys = _showingAccessKeys = false; } + + private void MainMenuClosed(object sender, EventArgs e) + { + _owner.ShowAccessKeys = false; + } } } diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 5acb6aa777..c9924dbffb 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -15,7 +15,7 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - event EventHandler GotFocus; + event EventHandler GotFocus; /// /// Occurs when the control loses focus. diff --git a/src/Avalonia.Input/IMainMenu.cs b/src/Avalonia.Input/IMainMenu.cs index 39d19a0a76..a3373191a8 100644 --- a/src/Avalonia.Input/IMainMenu.cs +++ b/src/Avalonia.Input/IMainMenu.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -24,5 +26,10 @@ namespace Avalonia.Input /// Opens the menu in response to the Alt/F10 key. /// void Open(); + + /// + /// Occurs when the main menu closes. + /// + event EventHandler MenuClosed; } } diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index 13d734bd0b..df434bca70 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -13,7 +13,8 @@ namespace Avalonia.Input /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement GetControl(NavigationDirection direction, IInputElement from); + IInputElement GetControl(NavigationDirection direction, IInputElement from, bool wrap); } } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 82e626f9c6..3aff5d0a8b 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -177,7 +177,7 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - public event EventHandler GotFocus + public event EventHandler GotFocus { add { AddHandler(GotFocusEvent, value); } remove { RemoveHandler(GotFocusEvent, value); } diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index cbd9a74f4c..0277876e24 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -8,19 +8,6 @@ namespace Avalonia.Input /// public static class KeyboardNavigation { - /// - /// Defines the DirectionalNavigation attached property. - /// - /// - /// The DirectionalNavigation attached property defines how pressing arrow keys causes - /// focus to be navigated between the children of the container. - /// - public static readonly AttachedProperty DirectionalNavigationProperty = - AvaloniaProperty.RegisterAttached( - "DirectionalNavigation", - typeof(KeyboardNavigation), - KeyboardNavigationMode.None); - /// /// Defines the TabNavigation attached property. /// @@ -46,26 +33,6 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); - /// - /// Gets the for a container. - /// - /// The container. - /// The for the container. - public static KeyboardNavigationMode GetDirectionalNavigation(InputElement element) - { - return element.GetValue(DirectionalNavigationProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The for the container. - public static void SetDirectionalNavigation(InputElement element, KeyboardNavigationMode value) - { - element.SetValue(DirectionalNavigationProperty, value); - } - /// /// Gets the for a container. /// diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index bf2b61d08b..bc3098a7fb 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -85,7 +85,7 @@ namespace Avalonia.Input } else { - return DirectionalNavigation.GetNext(element, direction); + throw new NotSupportedException(); } } @@ -122,47 +122,12 @@ namespace Avalonia.Input { var current = FocusManager.Instance.Current; - if (current != null) + if (current != null && e.Key == Key.Tab) { - NavigationDirection? direction = null; - - switch (e.Key) - { - case Key.Tab: - direction = (e.Modifiers & InputModifiers.Shift) == 0 ? - NavigationDirection.Next : NavigationDirection.Previous; - break; - case Key.Up: - direction = NavigationDirection.Up; - break; - case Key.Down: - direction = NavigationDirection.Down; - break; - case Key.Left: - direction = NavigationDirection.Left; - break; - case Key.Right: - direction = NavigationDirection.Right; - break; - case Key.PageUp: - direction = NavigationDirection.PageUp; - break; - case Key.PageDown: - direction = NavigationDirection.PageDown; - break; - case Key.Home: - direction = NavigationDirection.First; - break; - case Key.End: - direction = NavigationDirection.Last; - break; - } - - if (direction.HasValue) - { - Move(current, direction.Value, e.Modifiers); - e.Handled = true; - } + var direction = (e.Modifiers & InputModifiers.Shift) == 0 ? + NavigationDirection.Next : NavigationDirection.Previous; + Move(current, direction, e.Modifiers); + e.Handled = true; } } } diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs deleted file mode 100644 index 75cb3a39e8..0000000000 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Input.Navigation -{ - /// - /// The implementation for default directional navigation. - /// - public static class DirectionalNavigation - { - /// - /// Gets the next control in the specified navigation direction. - /// - /// The element. - /// The navigation direction. - /// - /// The next element in the specified direction, or null if - /// was the last in the requested direction. - /// - public static IInputElement GetNext( - IInputElement element, - NavigationDirection direction) - { - Contract.Requires(element != null); - Contract.Requires( - direction != NavigationDirection.Next && - direction != NavigationDirection.Previous); - - var container = element.GetVisualParent(); - - if (container != null) - { - var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); - - switch (mode) - { - case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, element, direction); - case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction) ?? - GetFocusableDescendant(container, direction); - case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction); - default: - return null; - } - } - else - { - return GetFocusableDescendants(element).FirstOrDefault(); - } - } - - /// - /// Returns a value indicting whether the specified direction is forward. - /// - /// The direction. - /// True if the direction is forward. - private static bool IsForward(NavigationDirection direction) - { - return direction == NavigationDirection.Next || - direction == NavigationDirection.Last || - direction == NavigationDirection.Right || - direction == NavigationDirection.Down; - } - - /// - /// Gets the first or last focusable descendant of the specified element. - /// - /// The element. - /// The direction to search. - /// The element or null if not found.## - private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) - { - return IsForward(direction) ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); - } - - /// - /// Gets the focusable descendants of the specified element. - /// - /// The element. - /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element) - { - var children = element.GetVisualChildren().OfType(); - - foreach (var child in children) - { - if (child.CanFocus()) - { - yield return child; - } - - if (child.CanFocusDescendants()) - { - foreach (var descendant in GetFocusableDescendants(child)) - { - yield return descendant; - } - } - } - } - - /// - /// Gets the next item that should be focused in the specified container. - /// - /// The starting element/ - /// The container. - /// The direction. - /// The next element, or null if the element is the last. - private static IInputElement GetNextInContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction) - { - if (direction == NavigationDirection.Down) - { - var descendant = GetFocusableDescendants(element).FirstOrDefault(); - - if (descendant != null) - { - return descendant; - } - } - - if (container != null) - { - var navigable = container as INavigableContainer; - - if (navigable != null) - { - while (element != null) - { - element = navigable.GetControl(direction, element); - - if (element != null && element.CanFocus()) - { - break; - } - } - } - else - { - // TODO: Do a spatial search here if the container doesn't implement - // INavigableContainer. - element = null; - } - - if (element != null && direction == NavigationDirection.Up) - { - var descendant = GetFocusableDescendants(element).LastOrDefault(); - - if (descendant != null) - { - return descendant; - } - } - - return element; - } - - return null; - } - - /// - /// Gets the first item that should be focused in the next container. - /// - /// The element being navigated away from. - /// The container. - /// The direction of the search. - /// The first element, or null if there are no more elements. - private static IInputElement GetFirstInNextContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction) - { - var parent = container.GetVisualParent(); - var isForward = IsForward(direction); - IInputElement next = null; - - if (parent != null) - { - if (!isForward && parent.CanFocus()) - { - return parent; - } - - var siblings = parent.GetVisualChildren() - .OfType() - .Where(FocusExtensions.CanFocusDescendants); - var sibling = isForward ? - siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : - siblings.TakeWhile(x => x != container).LastOrDefault(); - - if (sibling != null) - { - if (sibling is ICustomKeyboardNavigation custom) - { - var (handled, customNext) = custom.GetNext(element, direction); - - if (handled) - { - return customNext; - } - } - - if (sibling.CanFocus()) - { - next = sibling; - } - else - { - next = isForward ? - GetFocusableDescendants(sibling).FirstOrDefault() : - GetFocusableDescendants(sibling).LastOrDefault(); - } - } - - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } - } - else - { - next = isForward ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); - } - - return next; - } - } -} diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index a9d5b83073..18db2a9173 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -168,7 +168,7 @@ namespace Avalonia.Input.Navigation { while (element != null) { - element = navigable.GetControl(direction, element); + element = navigable.GetControl(direction, element, false); if (element != null && element.CanFocus()) { diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index fbaa7e74c7..406890b767 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -58,4 +58,74 @@ namespace Avalonia.Input /// PageDown, } + + public static class NavigationDirectionExtensions + { + /// + /// Checks whether a represents a tab movement. + /// + /// The direction. + /// + /// True if the direction represents a tab movement ( + /// or ); otherwise false. + /// + public static bool IsTab(this NavigationDirection direction) + { + return direction == NavigationDirection.Next || + direction == NavigationDirection.Previous; + } + + /// + /// Checks whether a represents a directional movement. + /// + /// The direction. + /// + /// True if the direction represents a directional movement (any value except + /// and ); + /// otherwise false. + /// + public static bool IsDirectional(this NavigationDirection direction) + { + return direction > NavigationDirection.Previous || + direction <= NavigationDirection.PageDown; + } + + /// + /// Converts a keypress into a . + /// + /// The key. + /// The keyboard modifiers. + /// + /// A if the keypress represents a navigation keypress. + /// + public static NavigationDirection? ToNavigationDirection( + this Key key, + InputModifiers modifiers = InputModifiers.None) + { + switch (key) + { + case Key.Tab: + return (modifiers & InputModifiers.Shift) != 0 ? + NavigationDirection.Next : NavigationDirection.Previous; + case Key.Up: + return NavigationDirection.Up; + case Key.Down: + return NavigationDirection.Down; + case Key.Left: + return NavigationDirection.Left; + case Key.Right: + return NavigationDirection.Right; + case Key.Home: + return NavigationDirection.First; + case Key.End: + return NavigationDirection.Last; + case Key.PageUp: + return NavigationDirection.PageUp; + case Key.PageDown: + return NavigationDirection.PageDown; + default: + return null; + } + } + } } diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 61bb567d54..2d752133c1 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -72,7 +72,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, typeof(TOwner)); + var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); + RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); + return routedEvent; } public static RoutedEvent Register( @@ -83,7 +85,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, ownerType); + var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); + RoutedEventRegistry.Instance.Register(ownerType, routedEvent); + return routedEvent; } public IDisposable AddClassHandler( diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs new file mode 100644 index 0000000000..34c970a806 --- /dev/null +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -0,0 +1,90 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Interactivity +{ + /// + /// Tracks registered s. + /// + public class RoutedEventRegistry + { + private readonly Dictionary> _registeredRoutedEvents = + new Dictionary>(); + + /// + /// Gets the instance. + /// + public static RoutedEventRegistry Instance { get; } + = new RoutedEventRegistry(); + + /// + /// Registers a on a type. + /// + /// The type. + /// The event. + /// + /// You won't usually want to call this method directly, instead use the + /// + /// method. + /// + public void Register(Type type, RoutedEvent @event) + { + Contract.Requires(type != null); + Contract.Requires(@event != null); + + if (!_registeredRoutedEvents.TryGetValue(type, out var list)) + { + list = new List(); + _registeredRoutedEvents.Add(type, list); + } + list.Add(@event); + } + + /// + /// Returns all routed events, that are currently registered in the event registry. + /// + /// All routed events, that are currently registered in the event registry. + public IEnumerable GetAllRegistered() + { + foreach (var events in _registeredRoutedEvents.Values) + { + foreach (var e in events) + { + yield return e; + } + } + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered(Type type) + { + Contract.Requires(type != null); + + if (_registeredRoutedEvents.TryGetValue(type, out var events)) + { + return events; + } + + return Array.Empty(); + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered() + { + return GetRegistered(typeof(TOwner)); + } + } +} diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 53965db016..8a2ed2a802 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -127,11 +127,6 @@ - - @@ -139,4 +134,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index 3b3a9e9749..6312a14df5 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -1,6 +1,7 @@ - \ No newline at end of file + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 4f18c682b4..cb43873cee 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -150,7 +150,9 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver(target, node); + observer = CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 9ef1e9f0d2..3cf886ade4 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; +using Avalonia.Input; namespace Avalonia.Controls.UnitTests { @@ -494,6 +495,77 @@ namespace Avalonia.Controls.UnitTests Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child)); } + [Fact] + public void Focuses_Next_Item_On_Key_Down() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new object[] + { + new Button(), + new Button(), + }; + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Presenter.Panel.Children[0].Focus(); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + + Assert.Equal( + target.Presenter.Panel.Children[1], + FocusManager.Instance.Current); + } + } + + [Fact] + public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new object[] + { + new Button(), + new Button { Focusable = false }, + new Button(), + }; + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Presenter.Panel.Children[0].Focus(); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + + Assert.Equal( + target.Presenter.Panel.Children[2], + FocusManager.Instance.Current); + } + } + private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs new file mode 100644 index 0000000000..f06553411c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Mixins +{ + public class ContentControlMixinTests + { + [Fact] + public void Multiple_Mixin_Usages_Should_Not_Throw() + { + var target = new TestControl() + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + new ContentPresenter { Name = "Content_1_Presenter" }, + new ContentPresenter { Name = "Content_2_Presenter" } + } + }) + }; + + var ex = Record.Exception(() => target.ApplyTemplate()); + + Assert.Null(ex); + } + + [Fact] + public void Replacing_Template_Releases_Events() + { + var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; + var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; + var target = new TestControl + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + p1, + p2 + } + }) + }; + target.ApplyTemplate(); + + Control tc; + + p1.Content = tc = new Control(); + p1.UpdateChild(); + Assert.Contains(tc, target.GetLogicalChildren()); + + p2.Content = tc = new Control(); + p2.UpdateChild(); + Assert.Contains(tc, target.GetLogicalChildren()); + + target.Template = null; + + p1.Content = tc = new Control(); + p1.UpdateChild(); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); + + p2.Content = tc = new Control(); + p2.UpdateChild(); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); + + } + + private class TestControl : TemplatedControl + { + public static readonly StyledProperty Content1Property = + AvaloniaProperty.Register(nameof(Content1)); + + public static readonly StyledProperty Content2Property = + AvaloniaProperty.Register(nameof(Content2)); + + static TestControl() + { + ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); + ContentControlMixin.Attach(Content2Property, x => x.LogicalChildren, "Content_2_Presenter"); + } + + public object Content1 + { + get { return GetValue(Content1Property); } + set { SetValue(Content1Property, value); } + } + + public object Content2 + { + get { return GetValue(Content2Property); } + set { SetValue(Content2Property, value); } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs new file mode 100644 index 0000000000..e17279013d --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -0,0 +1,507 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Platform +{ + public class DefaultMenuInteractionHandlerTests + { + public class TopLevel + { + [Fact] + public void Up_Opens_MenuItem_With_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Up, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Down_Opens_MenuItem_With_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Down, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Right_Selects_Next_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Right, true) == true); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Right, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Left_Selects_Previous_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Left, true) == true); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Left, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Left, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_SubMenu_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Escape_Closes_Parent_Menu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Escape, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void PointerEnter_Opens_Item_When_Old_Item_Is_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => + x.IsSubMenuOpen == true && + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var nextItem = Mock.Of(x => + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; + + menu.SetupGet(x => x.SelectedItem).Returns(item); + + target.PointerEnter(nextItem, e); + + Mock.Get(item).Verify(x => x.Close()); + menu.VerifySet(x => x.SelectedItem = nextItem); + Mock.Get(nextItem).Verify(x => x.Open()); + Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); + Assert.False(e.Handled); + + } + + [Fact] + public void PointerLeave_Deselects_Item_When_Menu_Not_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + menu.SetupGet(x => x.SelectedItem).Returns(item); + target.PointerLeave(item, e); + + menu.VerifySet(x => x.SelectedItem = null); + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + menu.SetupGet(x => x.IsOpen).Returns(true); + menu.SetupGet(x => x.SelectedItem).Returns(item); + target.PointerLeave(item, e); + + menu.VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + } + + public class NonTopLevel + { + [Fact] + public void Up_Selects_Previous_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Up, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Up, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Down_Selects_Next_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Down, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Down, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Left_Closes_Parent_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.HasSubMenu == true && x.IsSubMenuOpen == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Left, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(parentItem).Verify(x => x.Focus()); + Assert.True(e.Handled); + } + + [Fact] + public void Right_With_SubMenu_Items_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var parentItem = Mock.Of(x => + x.IsSubMenuOpen == true && + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var nextItem = Mock.Of(x => + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + menu.Setup(x => x.MoveSelection(NavigationDirection.Right, true)) + .Callback(() => menu.SetupGet(x => x.SelectedItem).Returns(nextItem)) + .Returns(true); + + target.KeyDown(item, e); + + menu.Verify(x => x.MoveSelection(NavigationDirection.Right, true)); + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(nextItem).Verify(x => x.Open()); + Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_SubMenu_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Escape_Closes_Parent_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Escape, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(parentItem).Verify(x => x.Focus()); + Assert.True(e.Handled); + } + + [Fact] + public void PointerEnter_Selects_Item() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + target.PointerEnter(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Assert.False(e.Handled); + } + + [Fact] + public void PointerEnter_Opens_Submenu_After_Delay() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + target.PointerEnter(item, e); + Mock.Get(item).Verify(x => x.Open(), Times.Never); + + timer.Pulse(); + Mock.Get(item).Verify(x => x.Open()); + + Assert.False(e.Handled); + } + + [Fact] + public void PointerEnter_Closes_Sibling_Submenu_After_Delay() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var sibling = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); + + target.PointerEnter(item, e); + Mock.Get(sibling).Verify(x => x.Close(), Times.Never); + + timer.Pulse(); + Mock.Get(sibling).Verify(x => x.Close()); + + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Deselects_Item() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item); + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null); + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Doesnt_Deselect_Sibling() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var sibling = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling); + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + + [Fact] + public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + target.PointerReleased(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var childItem = Mock.Of(x => x.Parent == item); + var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + // Pointer enters item; item is selected. + target.PointerEnter(item, enter); + Assert.True(timer.ActionIsQueued); + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Mock.Get(parentItem).ResetCalls(); + + // SubMenu shown after a delay. + timer.Pulse(); + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).SetupGet(x => x.IsSubMenuOpen).Returns(true); + Mock.Get(item).ResetCalls(); + + // Pointer briefly exits item, but submenu remains open. + target.PointerLeave(item, leave); + Mock.Get(item).Verify(x => x.Close(), Times.Never); + Mock.Get(item).ResetCalls(); + + // Pointer enters child item; is selected. + enter.Source = childItem; + target.PointerEnter(childItem, enter); + Mock.Get(item).VerifySet(x => x.SelectedItem = childItem); + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Mock.Get(item).ResetCalls(); + Mock.Get(parentItem).ResetCalls(); + } + + [Fact] + public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + target.PointerPressed(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); + Assert.True(e.Handled); + } + } + + private class TestTimer + { + private Action _action; + + public bool ActionIsQueued => _action != null; + + public void Pulse() + { + _action(); + _action = null; + } + + public void RunOnce(Action action, TimeSpan timeSpan) + { + if (_action != null) + { + throw new NotSupportedException("Action already set."); + } + + _action = action; + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index b0ae3df8a2..70a40faed3 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -219,7 +219,7 @@ namespace Avalonia.Controls.UnitTests scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); ((ISetLogicalParent)target).SetParent(presenter.Object); - ((INavigableContainer)target).GetControl(NavigationDirection.Next, from); + ((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false); scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs deleted file mode 100644 index b81b724e2a..0000000000 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ /dev/null @@ -1,799 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls; -using Xunit; - -namespace Avalonia.Input.UnitTests -{ - public class KeyboardNavigationTests_Arrows - { - [Fact] - public void Down_Continue_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_First_Control_In_Down_Sibling_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_Down_Sibling() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - (next = new Button { Name = "Button4" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_First_Control_In_Down_Uncle_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_Child_Of_Top_Level() - { - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button1" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Wraps() - { - Button current; - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - (current = new Button { Name = "Button6" }), - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Cycle_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Cycle_Wraps_To_First() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (next = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Contained_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Contained_Stops_At_End() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Null(result); - } - - [Fact] - public void Down_None_Does_Nothing() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.None, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Null(result); - } - - [Fact] - public void Up_Continue_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - (next = new Button { Name = "Button2" }), - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Control_In_Up_Sibling_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Child_Of_Sibling() - { - Button current; - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - (current = new Button { Name = "Button4" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Control_In_Up_Nephew_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Wraps() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - (next = new Button { Name = "Button6" }), - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Parent() - { - Button current; - - var top = new Decorator - { - Focusable = true, - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Child = current = new Button - { - Name = "Button", - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(top, result); - } - - [Fact] - public void Up_Cycle_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (next = new Button { Name = "Button1" }), - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Cycle_Wraps_To_Last() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Contained_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (next = new Button { Name = "Button1" }), - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Contained_Stops_At_Beginning() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Null(result); - } - - [Fact] - public void Up_Contained_Doesnt_Return_Child_Control() - { - Decorator current; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (current = new Decorator - { - Focusable = true, - Child = new Button(), - }) - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Null(result); - } - } -} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs index a090dcd18d..ab0f5e2155 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs @@ -140,37 +140,6 @@ namespace Avalonia.Input.UnitTests Assert.Same(next, result); } - [Fact] - public void Right_Should_Custom_Navigate_From_Outside() - { - Button current; - Button next; - var target = new CustomNavigatingStackPanel - { - Children = - { - new Button { Content = "Button 1" }, - new Button { Content = "Button 2" }, - (next = new Button { Content = "Button 3" }), - }, - NextControl = next, - }; - - var root = new StackPanel - { - Children = - { - (current = new Button { Content = "Outside" }), - target, - }, - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); - - Assert.Same(next, result); - } - [Fact] public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next() { diff --git a/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs new file mode 100644 index 0000000000..b9ebdea064 --- /dev/null +++ b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs @@ -0,0 +1,49 @@ +// 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 Avalonia.Controls; +using Avalonia.Input; +using Xunit; + +namespace Avalonia.Interactivity.UnitTests +{ + public class RoutedEventRegistryTests + { + [Fact] + public void Pointer_Events_Should_Be_Registered() + { + var expectedEvents = new List { InputElement.PointerPressedEvent, InputElement.PointerReleasedEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered(); + Assert.Contains(registeredEvents, expectedEvents.Contains); + } + + [Fact] + public void ClickEvent_Should_Be_Registered_On_Button() + { + var expectedEvents = new List { Button.ClickEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered