From c23fdc92de962f00ec43e24f4a09fc889dff294b Mon Sep 17 00:00:00 2001 From: Michael Bosschert Date: Fri, 29 Mar 2019 14:06:10 +0100 Subject: [PATCH 001/191] Added unittests for the Select All option of the TreeView. --- .../TreeViewTests.cs | 133 +++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 519872f9f2..15081b184c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Xunit; @@ -425,7 +426,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } - [Fact] public void LogicalChildren_Should_Be_Set() { @@ -623,6 +623,135 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + TreeTestHelper.AssertChildrenSelected(target, rootNode); + } + } + + [Fact] + public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + TreeTestHelper.AssertChildrenSelected(target, rootNode); + } + } + + [Fact] + public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + TreeTestHelper.AssertChildrenSelected(target, rootNode); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -765,7 +894,7 @@ namespace Avalonia.Controls.UnitTests } } - private class Node : NotifyingBase + private class Node : NotifyingBase { private IAvaloniaList _children; From de801ed27edb2c980fc5b81141d50c41a41be871 Mon Sep 17 00:00:00 2001 From: Michael Bosschert Date: Tue, 2 Apr 2019 15:18:50 +0200 Subject: [PATCH 002/191] Fixed SynchronizeItems adding duplicate items. --- .../Primitives/SelectingItemsControl.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a64dbe0546..288d751aa9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -639,20 +639,20 @@ namespace Avalonia.Controls.Primitives /// The desired items. internal static void SynchronizeItems(IList items, IEnumerable desired) { - int index = 0; + var index = 0; - foreach (var i in desired) + foreach (object item in desired) { - if (index < items.Count) + int itemIndex = items.IndexOf(item); + + if (itemIndex == -1) { - if (items[index] != i) - { - items[index] = i; - } + items.Insert(index, item); } - else + else if(itemIndex != index) { - items.Add(i); + items.RemoveAt(itemIndex); + items.Insert(index, item); } ++index; From 1312626aed977f131c1007ba7fa5b73a373e543a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 May 2019 17:32:38 +0200 Subject: [PATCH 003/191] Added failing test for #2512. --- .../Xaml/DataTemplateTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index ce51e7ad72..61155c3c46 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -38,6 +38,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void DataTemplate_Can_Contain_Named_UserControl() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var itemsControl = window.FindControl("itemsControl"); + + window.DataContext = new[] { "item1", "item2" }; + + window.ApplyTemplate(); + itemsControl.ApplyTemplate(); + itemsControl.Presenter.ApplyTemplate(); + + Assert.Equal(2, itemsControl.Presenter.Panel.Children.Count); + } + } + [Fact] public void Can_Set_DataContext_In_DataTemplate() { From 208361b8faa597273d853d8661119fa0ee39043c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 May 2019 17:18:51 +0200 Subject: [PATCH 004/191] Don't register controls with parent namescope. This reverts the changes in #843 because they were causing problems, as described by #2512. This should however not cause #829 to reappear because since #843 was merged we moved to Portable.Xaml and we're now registering controls with the root namescope in the XAML engine: https://github.com/AvaloniaUI/Avalonia/blob/b4577a1631755b391f3768e00264ac86c4300507/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs#L67 Fixes #2512 --- .../Primitives/TemplatedControl.cs | 2 +- src/Avalonia.Styling/StyledElement.cs | 17 -------------- .../Xaml/BasicTests.cs | 16 +++++++++++++ .../StyledElementTests.cs | 7 ++---- .../StyledElementTests_NameScope.cs | 23 ------------------- 5 files changed, 19 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index ba4c5027d0..32e220b789 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -357,7 +357,7 @@ namespace Avalonia.Controls.Primitives if (control.TemplatedParent == this) { - foreach (IControl child in control.GetVisualChildren()) + foreach (IControl child in control.GetLogicalChildren()) { RegisterNames(child, nameScope); } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index d314a8d44e..ae2cec5561 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -677,23 +677,6 @@ namespace Avalonia if (Name != null) { _nameScope?.Register(Name, this); - - var visualParent = Parent as StyledElement; - - if (this is INameScope && visualParent != null) - { - // If we have e.g. a named UserControl in a window then we want that control - // to be findable by name from the Window, so register with both name scopes. - // This differs from WPF's behavior in that XAML manually registers controls - // with name scopes based on the XAML file in which the name attribute appears, - // but we're trying to avoid XAML magic in Avalonia in order to made code- - // created UIs easy. This will cause problems if a UserControl declares a name - // in its XAML and that control is included multiple times in a parent control - // (as the name will be duplicated), however at the moment I'm fine with saying - // "don't do that". - var parentNameScope = NameScope.FindNameScope(visualParent); - parentNameScope?.Register(Name, this); - } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 2e67541c1f..743fc82f29 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -197,6 +197,22 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", button.Content); } + [Fact] + public void Named_UserControl_Is_Added_To_Parent_NameScope() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + + var control = AvaloniaXamlLoader.Parse(xaml); + + Assert.NotNull(control.FindControl("foo")); + } + } + [Fact] public void Direct_Content_In_ItemsControl_Is_Operational() { diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index 4970addd81..7fdd70799f 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -273,13 +273,10 @@ namespace Avalonia.Styling.UnitTests var root = new TestRoot(); var child = new Border(); - ((ISupportInitialize)child).BeginInit(); + child.BeginInit(); root.Child = child; child.Name = "foo"; - Assert.Null(root.FindControl("foo")); - ((ISupportInitialize)child).EndInit(); - - Assert.Same(root.FindControl("foo"), child); + child.EndInit(); } } diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests_NameScope.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests_NameScope.cs index 47c540f44a..47c34dfd38 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests_NameScope.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests_NameScope.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 Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.Rendering; -using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -70,23 +65,5 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((StyledElement)root.Presenter).Find("foo")); } - - [Fact] - public void Control_That_Is_NameScope_Should_Register_With_Parent_NameScope() - { - UserControl userControl; - var root = new TestTemplatedRoot - { - Content = userControl = new UserControl - { - Name = "foo", - } - }; - - root.ApplyTemplate(); - - Assert.Same(userControl, root.FindControl("foo")); - Assert.Same(userControl, userControl.FindControl("foo")); - } } } From 20eddbe6c872fabf50dd751372170d1ee7f3dbd6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:09:09 +0200 Subject: [PATCH 005/191] Added failing test for #2518 --- .../Rendering/DeferredRendererTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 8c103360d4..f094d9c78d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -325,6 +325,52 @@ namespace Avalonia.Visuals.UnitTests.Rendering context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny(), It.IsAny(), BitmapInterpolationMode.Default)); } + [Fact] + public void Can_Dirty_Control_In_SceneInvalidated() + { + Border border1; + Border border2; + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new StackPanel + { + Children = + { + (border1 = new Border + { + Background = Brushes.Red, + Child = new Canvas(), + }), + (border2 = new Border + { + Background = Brushes.Red, + Child = new Canvas(), + }), + } + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var target = CreateTargetAndRunFrame(root); + var invalidated = false; + + target.SceneInvalidated += (s, e) => + { + invalidated = true; + target.AddDirty(border2); + }; + + target.AddDirty(border1); + target.Paint(new Rect(root.DesiredSize)); + + Assert.True(invalidated); + Assert.True(((IRenderLoopTask)target).NeedsUpdate); + } + private DeferredRenderer CreateTargetAndRunFrame( TestRoot root, Mock timer = null, From 2661e939b1d548fcb4116d025c4f7e87f4684a83 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:10:45 +0200 Subject: [PATCH 006/191] Reset dirty rects before calling SceneInvalidated. When cleared after calling `SceneInvalidated`, any control invalidated during `SceneInvalidated` was be lost. Fixes #2518. --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 5d1c66f872..c83a8436b4 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -528,6 +528,8 @@ namespace Avalonia.Rendering oldScene?.Dispose(); } + _dirty.Clear(); + if (SceneInvalidated != null) { var rect = new Rect(); @@ -540,10 +542,9 @@ namespace Avalonia.Rendering } } + System.Diagnostics.Debug.WriteLine("Invalidated " + rect); SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); } - - _dirty.Clear(); } else { From c37f2b2fbcd631574140cbe0eb2022fd34f03948 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:31:25 +0200 Subject: [PATCH 007/191] Added failing test for #2522. --- .../Primitives/SelectingItemsControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 2df925301f..037c16e231 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -341,6 +341,33 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void Moving_Selected_Item_Should_Update_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedIndex = 0; + + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + + items.Move(0, 1); + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + } + [Fact] public void Resetting_Items_Collection_Should_Clear_Selection() { From ba7cec18e37627fe9eb948f3c3870fce447ae13e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:31:44 +0200 Subject: [PATCH 008/191] Handle move in SelectingItemsControl. Fixes #2522. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 280c3ad93a..23d2dc06af 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -380,6 +380,7 @@ namespace Avalonia.Controls.Primitives } break; + case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(Items, SelectedItem); break; From e123a737cc72d11c649c9989229a8851cfc769d3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:55:20 +0200 Subject: [PATCH 009/191] Added failing tests for #2501 --- .../ButtonTests.cs | 15 ++++++++++++ .../MenuItemTests.cs | 24 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9a751d4953..d1872c5b9e 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -31,6 +31,21 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsEnabled); } + [Fact] + public void Button_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new Button + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEnabledCore); + } + [Fact] public void Button_Is_Disabled_When_Bound_Command_Doesnt_Exist() { diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 32d154249c..704c79155a 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; +using Avalonia.Input; using Avalonia.UnitTests; using Xunit; @@ -58,10 +59,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, command.SubscriptionCount); } + [Fact] + public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new MenuItem + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEnabledCore); + } + private class TestCommand : ICommand { + private bool _enabled; private EventHandler _canExecuteChanged; + public TestCommand(bool enabled = true) + { + _enabled = enabled; + } + public int SubscriptionCount { get; private set; } public event EventHandler CanExecuteChanged @@ -70,7 +92,7 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => true; + public bool CanExecute(object parameter) => _enabled; public void Execute(object parameter) { From e6be9b7c5acf1c26d9f519707ac631bb98fe0984 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 15:09:13 +0200 Subject: [PATCH 010/191] Renamed IsEnabledCore -> IsEffectivelyEnabled. I now understand how WPF's `IsEnabledCore` works, and it's not like this. Rename `IsEnabledCore` to `IsEffectivelyEnabled` so that we can add a new `IsEnabledCore` property which works like WPF's. This also aligns with the existing `IsEffectivelyVisible` property. --- src/Avalonia.Controls/ComboBox.cs | 2 +- src/Avalonia.Input/FocusManager.cs | 2 +- src/Avalonia.Input/IInputElement.cs | 6 +-- src/Avalonia.Input/InputElement.cs | 42 +++++++++---------- src/Avalonia.Input/InputExtensions.cs | 2 +- .../Navigation/FocusExtensions.cs | 4 +- .../ButtonTests.cs | 2 +- .../MenuItemTests.cs | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index bf79e192c5..5d427df5a6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls } } - private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible; private void UpdateSelectionBoxItem(object item) { diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 102da6efc4..1603b250b8 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -146,7 +146,7 @@ namespace Avalonia.Input /// /// The element. /// True if the element can be focused. - private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Gets the focus scope ancestors of the specified control, traversing popups. diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c9924dbffb..9247fb48a9 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -83,14 +83,14 @@ namespace Avalonia.Input Cursor Cursor { get; } /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Gets a value indicating whether this control and all its parents are enabled. /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - bool IsEnabledCore { get; } + bool IsEffectivelyEnabled { get; } /// /// Gets a value indicating whether the control is focused. diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 07e04486ec..a1b00f47fb 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -27,10 +27,10 @@ namespace Avalonia.Input AvaloniaProperty.Register(nameof(IsEnabled), true); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty IsEnabledCoreProperty = - AvaloniaProperty.Register(nameof(IsEnabledCore), true); + public static readonly StyledProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.Register(nameof(IsEffectivelyEnabled), true); /// /// Gets or sets associated mouse cursor. @@ -168,7 +168,7 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); - PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); + PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); } @@ -349,23 +349,23 @@ namespace Avalonia.Input /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - bool IInputElement.IsEnabledCore => IsEnabledCore; + bool IInputElement.IsEffectivelyEnabled => IsEffectivelyEnabled; /// /// Gets a value indicating whether the control is effectively enabled for user interaction. /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - protected bool IsEnabledCore + protected bool IsEffectivelyEnabled { - get { return GetValue(IsEnabledCoreProperty); } - set { SetValue(IsEnabledCoreProperty, value); } + get { return GetValue(IsEffectivelyEnabledProperty); } + set { SetValue(IsEffectivelyEnabledProperty, value); } } public List KeyBindings { get; } = new List(); @@ -393,7 +393,7 @@ namespace Avalonia.Input protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); - UpdateIsEnabledCore(); + UpdateIsEffectivelyEnabled(); } /// @@ -488,7 +488,7 @@ namespace Avalonia.Input private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { - ((InputElement)e.Sender).UpdateIsEnabledCore(); + ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); } /// @@ -512,32 +512,32 @@ namespace Avalonia.Input } /// - /// Updates the property value. + /// Updates the property value. /// - private void UpdateIsEnabledCore() + private void UpdateIsEffectivelyEnabled() { - UpdateIsEnabledCore(this.GetVisualParent()); + UpdateIsEffectivelyEnabled(this.GetVisualParent()); } /// - /// Updates the property based on the parent's - /// . + /// Updates the property based on the parent's + /// . /// /// The parent control. - private void UpdateIsEnabledCore(InputElement parent) + private void UpdateIsEffectivelyEnabled(InputElement parent) { if (parent != null) { - IsEnabledCore = IsEnabled && parent.IsEnabledCore; + IsEffectivelyEnabled = IsEnabled && parent.IsEffectivelyEnabled; } else { - IsEnabledCore = IsEnabled; + IsEffectivelyEnabled = IsEnabled; } foreach (var child in this.GetVisualChildren().OfType()) { - child.UpdateIsEnabledCore(this); + child.UpdateIsEffectivelyEnabled(this); } } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f184e41998..c1d0729560 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -45,7 +45,7 @@ namespace Avalonia.Input return element != null && element.IsVisible && element.IsHitTestVisible && - element.IsEnabledCore && + element.IsEffectivelyEnabled && element.IsAttachedToVisualTree; } } diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Input/Navigation/FocusExtensions.cs index 41e7c4cd7b..794dc63f84 100644 --- a/src/Avalonia.Input/Navigation/FocusExtensions.cs +++ b/src/Avalonia.Input/Navigation/FocusExtensions.cs @@ -13,13 +13,13 @@ namespace Avalonia.Input.Navigation /// /// The element. /// True if the element can be focused. - public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Checks if descendants of the specified element can be focused. /// /// The element. /// True if descendants of the element can be focused. - public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible; + public static bool CanFocusDescendants(this IInputElement e) => e.IsEffectivelyEnabled && e.IsVisible; } } diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index d1872c5b9e..9255b00e50 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target }; - Assert.False(((IInputElement)target).IsEnabledCore); + Assert.False(((IInputElement)target).IsEffectivelyEnabled); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 704c79155a..ebf2c72ab4 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -71,7 +71,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target }; - Assert.False(((IInputElement)target).IsEnabledCore); + Assert.False(((IInputElement)target).IsEffectivelyEnabled); } private class TestCommand : ICommand From 38d68865fde632c113833d8148f312300c4cc1c8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 17:14:44 +0200 Subject: [PATCH 011/191] Correctly handle command.CanExecute state. Added a new `IsEnabledCore` property to `InputElement` which is overridden in `Button` and `MenuItem` to override the `IsEffectivelyEnabled` state with the enabled state of the command. Also add data validation of the `Command` property to `MenuItem` to make it behave the same as `Button` when `Command` is bound to a non-existent property. Fixes #2501 --- src/Avalonia.Controls/Button.cs | 23 +++- src/Avalonia.Controls/MenuItem.cs | 36 ++++-- src/Avalonia.Input/InputElement.cs | 67 +++++------ .../ButtonTests.cs | 29 +++-- .../MenuItemTests.cs | 113 +++++++++++++++--- .../InputElement_Enabled.cs | 101 ++++++++++++++++ 6 files changed, 294 insertions(+), 75 deletions(-) create mode 100644 tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index cc9e6b7444..c47413c14b 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -33,8 +33,6 @@ namespace Avalonia.Controls /// public class Button : ContentControl { - private ICommand _command; - /// /// Defines the property. /// @@ -75,6 +73,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); + private ICommand _command; + private bool _commandCanExecute = true; + /// /// Initializes static members of the class. /// @@ -147,6 +148,8 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -289,7 +292,11 @@ namespace Avalonia.Controls { if (status?.ErrorType == BindingErrorType.Error) { - IsEnabled = false; + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } } } } @@ -348,9 +355,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d8473dc613..4cd215c238 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -20,8 +21,6 @@ namespace Avalonia.Controls /// public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { - private ICommand _command; - /// /// Defines the property. /// @@ -91,9 +90,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - /// - /// The submenu popup. - /// + private ICommand _command; + private bool _commandCanExecute = true; private Popup _popup; /// @@ -231,6 +229,8 @@ namespace Avalonia.Controls /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); @@ -400,6 +400,22 @@ namespace Avalonia.Controls } } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + { + base.UpdateDataValidation(property, status); + if (property == CommandProperty) + { + if (status?.ErrorType == BindingErrorType.Error) + { + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } + } + } + } + /// /// Closes all submenus of the menu item. /// @@ -443,9 +459,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index a1b00f47fb..e1183e7154 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -29,8 +29,10 @@ namespace Avalonia.Input /// /// Defines the property. /// - public static readonly StyledProperty IsEffectivelyEnabledProperty = - AvaloniaProperty.Register(nameof(IsEffectivelyEnabled), true); + public static readonly DirectProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsEffectivelyEnabled), + o => o.IsEffectivelyEnabled); /// /// Gets or sets associated mouse cursor. @@ -146,6 +148,7 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isEffectivelyEnabled = true; private bool _isFocused; private bool _isPointerOver; @@ -344,31 +347,25 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } - /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. - /// - /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. - /// - bool IInputElement.IsEffectivelyEnabled => IsEffectivelyEnabled; + /// + public bool IsEffectivelyEnabled + { + get => _isEffectivelyEnabled; + private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + } + + public List KeyBindings { get; } = new List(); /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Allows a derived class to override the enabled state of the control. /// /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. + /// Derived controls may wish to disable the enabled state of the control without overwriting the + /// user-supplied setting. This can be done by overriding this property + /// to return the overridden enabled state. If the value returned from + /// should change, then the derived control should call . /// - protected bool IsEffectivelyEnabled - { - get { return GetValue(IsEffectivelyEnabledProperty); } - set { SetValue(IsEffectivelyEnabledProperty, value); } - } - - public List KeyBindings { get; } = new List(); + protected virtual bool IsEnabledCore => IsEnabled; /// /// Focuses the control. @@ -486,6 +483,15 @@ namespace Avalonia.Input { } + /// + /// Updates the property value according to the parent + /// control's enabled state and . + /// + protected void UpdateIsEffectivelyEnabled() + { + UpdateIsEffectivelyEnabled(this.GetVisualParent()); + } + private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); @@ -511,14 +517,6 @@ namespace Avalonia.Input OnPointerLeave(e); } - /// - /// Updates the property value. - /// - private void UpdateIsEffectivelyEnabled() - { - UpdateIsEffectivelyEnabled(this.GetVisualParent()); - } - /// /// Updates the property based on the parent's /// . @@ -526,14 +524,7 @@ namespace Avalonia.Input /// The parent control. private void UpdateIsEffectivelyEnabled(InputElement parent) { - if (parent != null) - { - IsEffectivelyEnabled = IsEnabled && parent.IsEffectivelyEnabled; - } - else - { - IsEffectivelyEnabled = IsEnabled; - } + IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); foreach (var child in this.GetVisualChildren().OfType()) { diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9255b00e50..fe0ac47a7d 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -24,11 +24,11 @@ namespace Avalonia.Controls.UnitTests }; var root = new TestRoot { Child = target }; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); command.IsEnabled = true; - Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); command.IsEnabled = false; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -54,7 +54,8 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -72,8 +73,12 @@ namespace Avalonia.Controls.UnitTests }; Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + target.DataContext = null; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -90,9 +95,13 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); } [Fact] @@ -109,9 +118,13 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index ebf2c72ab4..34371916df 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; +using Avalonia.Data; using Avalonia.Input; using Avalonia.UnitTests; using Xunit; @@ -26,6 +27,103 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.Focusable); } + + [Fact] + public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new MenuItem + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Doesnt_Exist() + { + var target = new MenuItem + { + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Is_Removed() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = viewModel, + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + + target.DataContext = null; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Enabled_When_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Disabled_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(false), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + [Fact] public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree() { @@ -59,21 +157,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, command.SubscriptionCount); } - [Fact] - public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() - { - var command = new TestCommand(true); - var target = new MenuItem - { - IsEnabled = false, - Command = command, - }; - - var root = new TestRoot { Child = target }; - - Assert.False(((IInputElement)target).IsEffectivelyEnabled); - } - private class TestCommand : ICommand { private bool _enabled; diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs b/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs new file mode 100644 index 0000000000..5dd66e3190 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs @@ -0,0 +1,101 @@ +// 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 InputElement_Enabled + { + [Fact] + public void IsEffectivelyEnabled_Follows_IsEnabled() + { + var target = new Decorator(); + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + + target.IsEnabled = false; + + Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void IsEffectivelyEnabled_Follows_Ancestor_IsEnabled() + { + Decorator child; + Decorator grandchild; + var target = new Decorator + { + Child = child = new Decorator + { + Child = grandchild = new Decorator(), + } + }; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.True(child.IsEffectivelyEnabled); + Assert.True(grandchild.IsEnabled); + Assert.True(grandchild.IsEffectivelyEnabled); + + target.IsEnabled = false; + + Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.False(child.IsEffectivelyEnabled); + Assert.True(grandchild.IsEnabled); + Assert.False(grandchild.IsEffectivelyEnabled); + } + + [Fact] + public void Disabled_Pseudoclass_Follows_IsEffectivelyEnabled() + { + Decorator child; + var target = new Decorator + { + Child = child = new Decorator() + }; + + Assert.DoesNotContain(":disabled", child.Classes); + + target.IsEnabled = false; + + Assert.Contains(":disabled", child.Classes); + } + + [Fact] + public void IsEffectivelyEnabled_Respects_IsEnabledCore() + { + Decorator child; + var target = new TestControl + { + Child = child = new Decorator() + }; + + target.ShouldEnable = false; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.False(child.IsEffectivelyEnabled); + } + + private class TestControl : Decorator + { + private bool _shouldEnable; + + public bool ShouldEnable + { + get => _shouldEnable; + set { _shouldEnable = value; UpdateIsEffectivelyEnabled(); } + } + + protected override bool IsEnabledCore => IsEnabled && _shouldEnable; + } + } +} From 5e5b090602bc748d0a84d0108d76268ccb34a1b0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 17:24:31 +0200 Subject: [PATCH 012/191] Display a disabed menu item in ControlCatalog. --- samples/ControlCatalog/ViewModels/MenuPageViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index 038f3574cc..88b1bf0b6b 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reactive; +using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; using ReactiveUI; @@ -11,7 +12,7 @@ namespace ControlCatalog.ViewModels public MenuPageViewModel() { OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); + SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false)); OpenRecentCommand = ReactiveCommand.Create(OpenRecent); MenuItems = new[] From 9d99cf699a1b53b814879d4dff1cf0b0805fd4b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 May 2019 09:28:14 +0200 Subject: [PATCH 013/191] Remove test that is no longer true. `UserControl`s should no longer be added to parent namescope. --- .../Xaml/BasicTests.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 7bd659b65f..359d2521e0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -208,22 +208,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", button.Content); } - [Fact] - public void Named_UserControl_Is_Added_To_Parent_NameScope() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var xaml = @" - - -"; - - var control = AvaloniaXamlLoader.Parse(xaml); - - Assert.NotNull(control.FindControl("foo")); - } - } - [Fact] public void Direct_Content_In_ItemsControl_Is_Operational() { From 8cfa7c175c4e9094d52448137db7731532a50c26 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Fri, 17 May 2019 15:58:36 +0200 Subject: [PATCH 014/191] Add blank cursor --- src/Avalonia.Input/Cursors.cs | 1 + src/Avalonia.X11/X11CursorFactory.cs | 22 ++++++++++++++++++++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 7 ++++--- src/Gtk/Avalonia.Gtk3/GdkCursor.cs | 1 + src/Windows/Avalonia.Win32/CursorFactory.cs | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..c5566318a2 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..f36e6b986d 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,7 +12,8 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.None, CursorType.Blank}, + { StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -36,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..df413addef 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,7 +41,8 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, 32650}, + {StandardCursorType.None, 0}, + { StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -69,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 4ad4ba4a9ed45415601d5355aa76158cd7b9349d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 May 2019 17:00:20 +0200 Subject: [PATCH 015/191] Set InputModifiers on PointerEnter/Leave. Note that these will not be set when a pointer enter/leave occurs because of a control moving or appearing/disappearing. Fixes #2495 --- src/Avalonia.Input/MouseDevice.cs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index d3e62ece6f..c195209305 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -108,11 +108,11 @@ namespace Avalonia.Input { if (Captured == null) { - SetPointerOver(this, root, clientPoint); + SetPointerOver(this, root, clientPoint, InputModifiers.None); } else { - SetPointerOver(this, root, Captured); + SetPointerOver(this, root, Captured, InputModifiers.None); } } } @@ -128,7 +128,7 @@ namespace Avalonia.Input switch (e.Type) { case RawMouseEventType.LeaveWindow: - LeaveWindow(mouse, e.Root); + LeaveWindow(mouse, e.Root, e.InputModifiers); break; case RawMouseEventType.LeftButtonDown: case RawMouseEventType.RightButtonDown: @@ -157,12 +157,12 @@ namespace Avalonia.Input } } - private void LeaveWindow(IMouseDevice device, IInputRoot root) + private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); - ClearPointerOver(this, root); + ClearPointerOver(this, root, inputModifiers); } private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers) @@ -218,11 +218,11 @@ namespace Avalonia.Input if (Captured == null) { - source = SetPointerOver(this, root, p); + source = SetPointerOver(this, root, p, inputModifiers); } else { - SetPointerOver(this, root, Captured); + SetPointerOver(this, root, Captured, inputModifiers); source = Captured; } @@ -306,7 +306,7 @@ namespace Avalonia.Input return Captured ?? root.InputHitTest(p); } - private void ClearPointerOver(IPointerDevice device, IInputRoot root) + private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -316,6 +316,7 @@ namespace Avalonia.Input { RoutedEvent = InputElement.PointerLeaveEvent, Device = device, + InputModifiers = inputModifiers }; if (element!=null && !element.IsAttachedToVisualTree) @@ -353,7 +354,7 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p) + private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -364,18 +365,18 @@ namespace Avalonia.Input { if (element != null) { - SetPointerOver(device, root, element); + SetPointerOver(device, root, element, inputModifiers); } else { - ClearPointerOver(device, root); + ClearPointerOver(device, root, inputModifiers); } } return element; } - private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element) + private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -383,7 +384,7 @@ namespace Avalonia.Input IInputElement branch = null; - var e = new PointerEventArgs { Device = device, }; + var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers }; var el = element; while (el != null) From 0f25e0548fa8d4d2d1fcec576e6f3ea8129ef53a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 May 2019 17:31:58 +0200 Subject: [PATCH 016/191] Use object for resource keys. `IResourceDictionary` was defined as an `IDictionary` but in various places we only accepted a `string` as the resource key. Fix this inconsistency and always use `object` as a resource key. Fixes #2456 --- src/Avalonia.Controls/Application.cs | 2 +- src/Avalonia.Styling/Controls/IResourceProvider.cs | 2 +- src/Avalonia.Styling/Controls/ResourceDictionary.cs | 2 +- .../Controls/ResourceProviderExtensions.cs | 10 +++++----- src/Avalonia.Styling/StyledElement.cs | 2 +- src/Avalonia.Styling/Styling/Style.cs | 2 +- src/Avalonia.Styling/Styling/Styles.cs | 2 +- .../MarkupExtensions/DynamicResourceExtension.cs | 2 +- .../MarkupExtensions/ResourceInclude.cs | 2 +- .../Avalonia.Markup.Xaml/Styling/StyleInclude.cs | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index bbea3693cc..0e696e0199 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -362,7 +362,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index eec783623c..cbaacee012 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -28,6 +28,6 @@ namespace Avalonia.Controls /// /// True if the resource if found, otherwise false. /// - bool TryGetResource(string key, out object value); + bool TryGetResource(object key, out object value); } } diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 74a861b36b..901e27b7b7 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls } /// - public bool TryGetResource(string key, out object value) + public bool TryGetResource(object key, out object value) { if (TryGetValue(key, out value)) { diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs index 52309b87a2..01112eaf2c 100644 --- a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// The control. /// The resource key. /// The resource, or if not found. - public static object FindResource(this IResourceNode control, string key) + public static object FindResource(this IResourceNode control, object key) { if (control.TryFindResource(key, out var value)) { @@ -28,7 +28,7 @@ namespace Avalonia.Controls /// The resource key. /// On return, contains the resource if found, otherwise null. /// True if the resource was found; otherwise false. - public static bool TryFindResource(this IResourceNode control, string key, out object value) + public static bool TryFindResource(this IResourceNode control, object key, out object value) { Contract.Requires(control != null); Contract.Requires(key != null); @@ -52,7 +52,7 @@ namespace Avalonia.Controls return false; } - public static IObservable GetResourceObservable(this IResourceNode target, string key) + public static IObservable GetResourceObservable(this IResourceNode target, object key) { return new ResourceObservable(target, key); } @@ -60,9 +60,9 @@ namespace Avalonia.Controls private class ResourceObservable : LightweightObservableBase { private readonly IResourceNode _target; - private readonly string _key; + private readonly object _key; - public ResourceObservable(IResourceNode target, string key) + public ResourceObservable(IResourceNode target, object key) { _target = target; _key = key; diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index d314a8d44e..6361763614 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -415,7 +415,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index d799df7ac9..3ce82b4160 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -171,7 +171,7 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(string key, out object result) + public bool TryGetResource(object key, out object result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 288cf35d08..789bb6ffd3 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -178,7 +178,7 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(string key, out object value) + public bool TryGetResource(object key, out object value) { if (_resources != null && _resources.TryGetValue(key, out value)) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 5f0e84c63a..48e55dc251 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -26,7 +26,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public string ResourceKey { get; set; } + public object ResourceKey { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) => ProvideTypedValue(serviceProvider); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 827f58a909..323a341f6a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -47,7 +47,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool IResourceProvider.HasResources => Loaded.HasResources; /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { return Loaded.TryGetResource(key, out value); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 01ec9753bd..7acee50d80 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -86,7 +86,7 @@ namespace Avalonia.Markup.Xaml.Styling } /// - public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); + public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); /// void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) From 38bd934c4ab4c7a7e4a225073fd3a01f54756bf6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 May 2019 17:40:28 +0200 Subject: [PATCH 017/191] Added Window.OnClosing. --- src/Avalonia.Controls/Window.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 01c9a3a110..01614ba87b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -330,8 +330,7 @@ namespace Avalonia.Controls protected virtual bool HandleClosing() { var args = new CancelEventArgs(); - Closing?.Invoke(this, args); - + OnClosing(args); return args.Cancel; } @@ -576,6 +575,17 @@ namespace Avalonia.Controls base.HandleResized(clientSize); } + + /// + /// Raises the event. + /// + /// The event args. + /// + /// A type that derives from may override . The + /// overridden method must call on the base class if the + /// event needs to be raised. + /// + protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); } } From 57af0d55625f10639eafbaf958c6f28f324fd052 Mon Sep 17 00:00:00 2001 From: lindexi Date: Sat, 18 May 2019 19:49:35 +0800 Subject: [PATCH 018/191] Add ignore file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2b2c9c3d0d..9fe4507b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,5 @@ ModuleCache.noindex/ Build/Intermediates.noindex/ info.plist build-intermediate +/tests/Avalonia.RenderTests/obj-Direct2D1 +/tests/Avalonia.RenderTests/obj-Skia From 4ad1acd24eeb836ff3b4fb06cb83011ed635d1e0 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:05:49 +0200 Subject: [PATCH 019/191] Fix X11 NullCursor --- src/Avalonia.X11/X11CursorFactory.cs | 8 ++++---- src/Avalonia.X11/XLib.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c5566318a2..c020c44662 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -66,12 +66,12 @@ namespace Avalonia.X11 } private static IntPtr GetNullCursor(IntPtr display) - { + { XColor color = new XColor(); byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); - IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); From 3860fafa000e9127f436745b998f6d8994cdb715 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:10:49 +0200 Subject: [PATCH 020/191] Fix formatting --- src/Avalonia.X11/X11CursorFactory.cs | 10 +++++----- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 4 ++-- src/Windows/Avalonia.Win32/CursorFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c020c44662..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11 class X11CursorFactory : IStandardCursorFactory { private static IntPtr _nullCursor; - + private readonly IntPtr _display; private Dictionary _cursors; @@ -48,10 +48,10 @@ namespace Avalonia.X11 _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -61,7 +61,7 @@ namespace Avalonia.X11 handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; - } + } return new PlatformHandle(handle, "XCURSOR"); } @@ -71,7 +71,7 @@ namespace Avalonia.X11 byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index f36e6b986d..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -13,7 +13,7 @@ namespace Avalonia.Gtk3 { {StandardCursorType.None, CursorType.Blank}, - { StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -37,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index df413addef..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -42,7 +42,7 @@ namespace Avalonia.Win32 { {StandardCursorType.None, 0}, - { StandardCursorType.AppStarting, 32650}, + {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -70,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From c72195f45b3cb68ac259dba472889827973295ca Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 20 May 2019 15:15:30 +0100 Subject: [PATCH 021/191] Fix use of CoerceCaretIndex on Text change --- src/Avalonia.Controls/TextBox.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 04b088e35c..d43957313e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -214,9 +214,9 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; - SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); - CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0); + SelectionStart = CoerceCaretIndex(SelectionStart, value); + SelectionEnd = CoerceCaretIndex(SelectionEnd, value); + CaretIndex = CoerceCaretIndex(caretIndex, value); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { @@ -677,11 +677,15 @@ namespace Avalonia.Controls } } - private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0); + private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); - private int CoerceCaretIndex(int value, int length) + private int CoerceCaretIndex(int value, string text) { - var text = Text; + if (text == null) + { + return 0; + } + var length = text.Length; if (value < 0) { @@ -691,7 +695,7 @@ namespace Avalonia.Controls { return length; } - else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n') + else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n') { return value + 1; } From 15aa377769628f233742a39c6765735620bc7cad Mon Sep 17 00:00:00 2001 From: lindexi Date: Tue, 21 May 2019 08:52:08 +0800 Subject: [PATCH 022/191] ignore obj folder --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9fe4507b1b..971c945246 100644 --- a/.gitignore +++ b/.gitignore @@ -196,5 +196,5 @@ ModuleCache.noindex/ Build/Intermediates.noindex/ info.plist build-intermediate -/tests/Avalonia.RenderTests/obj-Direct2D1 -/tests/Avalonia.RenderTests/obj-Skia +obj-Direct2D1/ +obj-Skia/ From 8a9e997c6c9b4302c22ebee4cfe729453e83e231 Mon Sep 17 00:00:00 2001 From: ahopper Date: Tue, 21 May 2019 07:13:26 +0100 Subject: [PATCH 023/191] unit test added --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0d87f6d0fe..9b62509138 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,21 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + [Fact] + public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789\r" + }; + target.CaretIndex = 11; + + Assert.True(true); + } + } private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); From 67ea597d82e14a6e9feeeb83d6d34fbda15b215a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 May 2019 12:14:37 +0300 Subject: [PATCH 024/191] Properly pass root object instance to templates, fixes #2147 #2527 --- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 18 +++++-- .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- .../Xaml/XamlIlTests.cs | 53 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f91e221ac0..70b7fe6aec 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Data; +using Portable.Xaml; using Portable.Xaml.Markup; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -17,19 +18,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { var resourceNodes = provider.GetService().Parents .OfType().ToList(); - - return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes)); + var rootObject = provider.GetService().RootObject; + return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject)); } - class DeferredParentServiceProvider : IAvaloniaXamlIlParentStackProvider, IServiceProvider + class DeferredParentServiceProvider : + IAvaloniaXamlIlParentStackProvider, + IServiceProvider, + IRootObjectProvider { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; - public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes) + public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes, + object rootObject) { _parentProvider = parentProvider; _parentResourceNodes = parentResourceNodes; + RootObject = rootObject; } public IEnumerable Parents => GetParents(); @@ -46,8 +52,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; + if (serviceType == typeof(IRootObjectProvider)) + return this; return _parentProvider?.GetService(serviceType); } + + public object RootObject { get; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 3b3c1f93a5..50920ece52 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 3b3c1f93a566080d417b9782f9cc4ea67cd62344 +Subproject commit 50920ece52647b19760f65b417940da125101365 diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index a584027768..175479e3ff 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -5,8 +5,12 @@ using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using JetBrains.Annotations; @@ -117,6 +121,55 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal(Brushes.Red.Color, ((ISolidColorBrush)canvas.Background).Color); } } + + [Fact] + public void Event_Handlers_Should_Work_For_Templates() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var w =new XamlIlBugTestsEventHandlerCodeBehind(); + w.ApplyTemplate(); + w.Show(); + + Dispatcher.UIThread.RunJobs(); + var itemsPresenter = ((ItemsControl)w.Content).GetVisualChildren().FirstOrDefault(); + var item = itemsPresenter + .GetVisualChildren().First() + .GetVisualChildren().First() + .GetVisualChildren().First(); + ((Control)item).RaiseEvent(new PointerPressedEventArgs {ClickCount = 20}); + Assert.Equal(20, w.Args.ClickCount); + } + } + } + + public class XamlIlBugTestsEventHandlerCodeBehind : Window + { + public PointerPressedEventArgs Args; + public void HandlePointerPressed(object sender, PointerPressedEventArgs args) + { + Args = args; + } + + public XamlIlBugTestsEventHandlerCodeBehind() + { + new AvaloniaXamlLoader().Load(@" + + + + +