Browse Source

Merge branch 'master' into feature/RichTextBlock

Benedikt Stebner 3 years ago
parent
commit
579468403d

+ 1 - 0
.editorconfig

@@ -21,6 +21,7 @@ csharp_new_line_before_finally = true
 csharp_new_line_before_members_in_object_initializers = true
 csharp_new_line_before_members_in_anonymous_types = true
 csharp_new_line_between_query_expression_clauses = true
+trim_trailing_whitespace = true
 
 # Indentation preferences
 csharp_indent_block_contents = true

+ 1 - 0
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -121,6 +121,7 @@ namespace Avalonia.PropertyStore
 
         public void ClearLocalValue()
         {
+            _localValue = default;
             UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
                 _owner,
                 Property,

+ 20 - 7
src/Avalonia.Base/Styling/PropertySetterInstance.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Styling
         private readonly DirectPropertyBase<T>? _directProperty;
         private readonly T _value;
         private IDisposable? _subscription;
-        private bool _isActive;
+        private State _state;
 
         public PropertySetterInstance(
             IStyleable target,
@@ -40,6 +40,8 @@ namespace Avalonia.Styling
             _value = value;
         }
 
+        private bool IsActive => _state == State.Active;
+
         public void Start(bool hasActivator)
         {
             if (hasActivator)
@@ -70,31 +72,35 @@ namespace Avalonia.Styling
 
         public void Activate()
         {
-            if (!_isActive)
+            if (!IsActive)
             {
-                _isActive = true;
+                _state = State.Active;
                 PublishNext();
             }
         }
 
         public void Deactivate()
         {
-            if (_isActive)
+            if (IsActive)
             {
-                _isActive = false;
+                _state = State.Inactive;
                 PublishNext();
             }
         }
 
         public override void Dispose()
         {
+            if (_state == State.Disposed)
+                return;
+            _state = State.Disposed;
+
             if (_subscription is object)
             {
                 var sub = _subscription;
                 _subscription = null;
                 sub.Dispose();
             }
-            else if (_isActive)
+            else if (IsActive)
             {
                 if (_styledProperty is object)
                 {
@@ -114,7 +120,14 @@ namespace Avalonia.Styling
 
         private void PublishNext()
         {
-            PublishNext(_isActive ? new BindingValue<T>(_value) : default);
+            PublishNext(IsActive ? new BindingValue<T>(_value) : default);
+        }
+
+        private enum State
+        {
+            Inactive,
+            Active,
+            Disposed,
         }
     }
 }

+ 19 - 15
src/Avalonia.Controls/ComboBox.cs

@@ -181,26 +181,13 @@ namespace Avalonia.Controls
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnAttachedToVisualTree(e);
-            this.UpdateSelectionBoxItem(SelectedItem);
+            UpdateSelectionBoxItem(SelectedItem);
         }
 
-        // Because the SelectedItem isn't connected to the visual tree
         public override void InvalidateMirrorTransform()
         {
             base.InvalidateMirrorTransform();
-
-            if (SelectedItem is Control selectedControl)
-            {
-                selectedControl.InvalidateMirrorTransform();
-
-                foreach (var visual in selectedControl.GetVisualDescendants())
-                {
-                    if (visual is Control childControl)
-                    {
-                        childControl.InvalidateMirrorTransform();
-                    }
-                }
-            }
+            UpdateFlowDirection();
         }
 
         /// <inheritdoc/>
@@ -365,6 +352,8 @@ namespace Avalonia.Controls
             {
                 parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
             }
+
+            UpdateFlowDirection();
         }
 
         private void IsVisibleChanged(bool isVisible)
@@ -432,6 +421,8 @@ namespace Avalonia.Controls
                         }
                     };
                 }
+
+                UpdateFlowDirection();
             }
             else
             {
@@ -439,6 +430,19 @@ namespace Avalonia.Controls
             }
         }
 
+        private void UpdateFlowDirection()
+        {
+            if (SelectionBoxItem is Rectangle rectangle)
+            {
+                if ((rectangle.Fill as VisualBrush)?.Visual is Control content)
+                {
+                    var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? 
+                        FlowDirection.LeftToRight;
+                    rectangle.FlowDirection = flowDirection;
+                }
+            }
+        }
+
         private void SelectFocusedItem()
         {
             foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)

+ 1 - 6
src/Avalonia.Controls/Control.cs

@@ -378,17 +378,12 @@ namespace Avalonia.Controls
             bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
             bool parentBypassFlowDirectionPolicies = false;
 
-            var parent = this.FindAncestorOfType<Control>();
+            var parent = ((IVisual)this).VisualParent as Control;
             if (parent != null)
             {
                 parentFlowDirection = parent.FlowDirection;
                 parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
             }
-            else if (Parent is Control logicalParent)
-            {
-                parentFlowDirection = logicalParent.FlowDirection;
-                parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies;
-            }
 
             bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
             bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;

+ 26 - 48
src/Avalonia.Controls/TextBox.cs

@@ -53,7 +53,7 @@ namespace Avalonia.Controls
 
         public static readonly StyledProperty<char> PasswordCharProperty =
             AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar));
-
+            
         public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
             AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush));
 
@@ -196,7 +196,6 @@ namespace Avalonia.Controls
         private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
         private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
         private bool _isUndoingRedoing;
-        private bool _ignoreTextChanges;
         private bool _canCut;
         private bool _canCopy;
         private bool _canPaste;
@@ -276,7 +275,7 @@ namespace Avalonia.Controls
             get => GetValue(IsReadOnlyProperty);
             set => SetValue(IsReadOnlyProperty, value);
         }
-
+        
         public char PasswordChar
         {
             get => GetValue(PasswordCharProperty);
@@ -368,21 +367,17 @@ namespace Avalonia.Controls
             get => _text;
             set
             {
-                if (!_ignoreTextChanges)
-                {
-                    var caretIndex = CaretIndex;
-                    var selectionStart = SelectionStart;
-                    var selectionEnd = SelectionEnd;
+                var caretIndex = CaretIndex;
+                var selectionStart = SelectionStart;
+                var selectionEnd = SelectionEnd;
                     
-                    CaretIndex = CoerceCaretIndex(caretIndex, value);
-                    SelectionStart = CoerceCaretIndex(selectionStart, value);
-                    SelectionEnd = CoerceCaretIndex(selectionEnd, value);
-
-                    if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
-                    {
-                        _undoRedoHelper.Clear();
-                        SnapshotUndoRedo(); // so we always have an initial state
-                    }
+                CaretIndex = CoerceCaretIndex(caretIndex, value);
+                SelectionStart = CoerceCaretIndex(selectionStart, value);
+                SelectionEnd = CoerceCaretIndex(selectionEnd, value);
+                if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
+                {
+                    _undoRedoHelper.Clear();
+                    SnapshotUndoRedo(); // so we always have an initial state
                 }
             }
         }
@@ -736,32 +731,23 @@ namespace Avalonia.Controls
             {
                 var oldText = _text;
 
-                _ignoreTextChanges = true;
-
-                try
-                {
-                    DeleteSelection(false);
-                    var caretIndex = CaretIndex;
-                    text = Text ?? string.Empty;
-                    SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
-                    ClearSelection();
-                    
-                    if (IsUndoEnabled)
-                    {
-                        _undoRedoHelper.DiscardRedo();
-                    }
-
-                    if (_text != oldText)
-                    {
-                        RaisePropertyChanged(TextProperty, oldText, _text);
-                    }
+                DeleteSelection(false);
+                var caretIndex = CaretIndex;
+                text = Text ?? string.Empty;
+                SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
+                ClearSelection();
                     
-                    CaretIndex = caretIndex + input.Length;
+                if (IsUndoEnabled)
+                {
+                    _undoRedoHelper.DiscardRedo();
                 }
-                finally
+
+                if (_text != oldText)
                 {
-                    _ignoreTextChanges = false;
+                    RaisePropertyChanged(TextProperty, oldText, _text);
                 }
+                    
+                CaretIndex = caretIndex + input.Length;
             }
         }
 
@@ -1499,15 +1485,7 @@ namespace Avalonia.Controls
         {
             if (raiseTextChanged)
             {
-                try
-                {
-                    _ignoreTextChanges = true;
-                    SetAndRaise(TextProperty, ref _text, value);
-                }
-                finally
-                {
-                    _ignoreTextChanges = false;
-                }
+                SetAndRaise(TextProperty, ref _text, value);
             }
             else
             {

+ 15 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@@ -17,6 +17,21 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void ClearValue_Resets_Value_To_Style_value()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
+            target.SetValue(Class1.FooProperty, "local");
+
+            Assert.Equal("local", target.GetValue(Class1.FooProperty));
+
+            target.ClearValue(Class1.FooProperty);
+
+            Assert.Equal("style", target.GetValue(Class1.FooProperty));
+        }
+
         [Fact]
         public void ClearValue_Raises_PropertyChanged()
         {

+ 33 - 0
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -349,6 +349,39 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
             }
         }
 
+        [Fact]
+        public void MirrorTransform_For_Control_With_RenderTransform_Should_Be_Correct()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                Border border;
+                var tree = new TestRoot
+                {
+                    Width = 400,
+                    Height = 200,
+                    Child = border = new Border
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Left,
+                        Background = Brushes.Red,
+                        Width = 100,
+                        RenderTransform = new ScaleTransform(0.5, 1),
+                        FlowDirection = FlowDirection.RightToLeft
+                    }
+                };
+
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(tree.DesiredSize));
+
+                var scene = new Scene(tree);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene);
+
+                var expectedTransform = new Matrix(-1, 0, 0, 1, 100, 0) * Matrix.CreateScale(0.5, 1) * Matrix.CreateTranslation(25, 0);
+                var borderNode = scene.FindNode(border);
+                Assert.Equal(expectedTransform, borderNode.Transform);
+            }
+        }
+
         [Fact]
         public void Should_Update_Border_Background_Node()
         {

+ 36 - 6
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@@ -150,13 +150,43 @@ namespace Avalonia.Base.UnitTests.Styling
             Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority);
         }
 
-        private IBinding CreateMockBinding(AvaloniaProperty property)
+        [Fact]
+        public void Disposing_Setter_Should_Preserve_LocalValue()
         {
-            var subject = new Subject<object>();
-            var descriptor = InstancedBinding.OneWay(subject);
-            var binding = Mock.Of<IBinding>(x => 
-                x.Initiate(It.IsAny<IAvaloniaObject>(), property, null, false) == descriptor);
-            return binding;
+            var control = new Canvas();
+            var setter = new Setter(TextBlock.TagProperty, "foo");
+
+            var instance = setter.Instance(control);
+            instance.Start(true);
+            instance.Activate();
+
+            control.Tag = "bar";
+
+            instance.Dispose();
+
+            Assert.Equal("bar", control.Tag);
+        }
+
+        [Fact]
+        public void Disposing_Binding_Setter_Should_Preserve_LocalValue()
+        {
+            var control = new Canvas();
+            var source = new { Foo = "foo" };
+            var setter = new Setter(TextBlock.TagProperty, new Binding
+            {
+                Source = source,
+                Path = nameof(source.Foo),
+            });
+
+            var instance = setter.Instance(control);
+            instance.Start(true);
+            instance.Activate();
+
+            control.Tag = "bar";
+
+            instance.Dispose();
+
+            Assert.Equal("bar", control.Tag);
         }
 
         private class TestConverter : IValueConverter

+ 100 - 1
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -8,7 +8,7 @@ using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
-using Avalonia.Threading;
+using Avalonia.VisualTree;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -336,5 +336,104 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(1, count);
             }
         }
+
+        [Fact]
+        public void FlowDirection_Of_RectangleContent_Shuold_Be_LeftToRight()
+        {
+            var items = new[]
+            {
+                new ComboBoxItem()
+                { 
+                    Content = new Control()
+                }
+            };
+            var target = new ComboBox
+            {
+                FlowDirection = FlowDirection.RightToLeft,
+                Items = items,
+                Template = GetTemplate()
+            };
+
+            var root = new TestRoot(target);
+            target.ApplyTemplate();
+            target.SelectedIndex = 0;
+
+            var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
+
+            Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
+        }
+
+        [Fact]
+        public void FlowDirection_Of_RectangleContent_Updated_After_InvalidateMirrorTransform()
+        {
+            var parentContent = new Decorator()
+            {
+                Child = new Control()
+            };
+            var items = new[]
+            { 
+                new ComboBoxItem()
+                {
+                    Content = parentContent.Child
+                }
+            };
+            var target = new ComboBox
+            {
+                Items = items,
+                Template = GetTemplate()
+            };
+
+            var root = new TestRoot(target);
+            target.ApplyTemplate();
+            target.SelectedIndex = 0;
+
+            var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
+            Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
+
+            parentContent.FlowDirection = FlowDirection.RightToLeft;
+            target.FlowDirection = FlowDirection.RightToLeft;
+            
+            Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection);
+        }
+
+        [Fact]
+        public void FlowDirection_Of_RectangleContent_Updated_After_OpenPopup()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var parentContent = new Decorator()
+                {
+                    Child = new Control()
+                };
+                var items = new[]
+                { 
+                    new ComboBoxItem()
+                    {
+                        Content = parentContent.Child
+                    }
+                };
+                var target = new ComboBox
+                {
+                    FlowDirection = FlowDirection.RightToLeft,
+                    Items = items,
+                    Template = GetTemplate()
+                };
+
+                var root = new TestRoot(target);
+                target.ApplyTemplate();
+                target.SelectedIndex = 0;
+
+                var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
+                Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
+
+                parentContent.FlowDirection = FlowDirection.RightToLeft;
+
+                var popup = target.GetVisualDescendants().OfType<Popup>().First();
+                popup.PlacementTarget = new Window();
+                popup.Open();
+                
+                Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection);
+            }
+        }
     }
 }

+ 57 - 0
tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs

@@ -0,0 +1,57 @@
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class FlowDirectionTests
+    {
+        [Fact]
+        public void HasMirrorTransform_Should_Be_True()
+        {
+            var target = new Control
+            {
+                FlowDirection = FlowDirection.RightToLeft,
+            };
+
+            Assert.True(target.HasMirrorTransform);    
+        }
+
+        [Fact]
+        public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent()
+        {
+            Control child;
+            var target = new Decorator
+            {
+                FlowDirection = FlowDirection.RightToLeft,
+                Child = child = new Control()
+            };
+
+            child.FlowDirection = FlowDirection.LeftToRight;
+
+            Assert.True(target.HasMirrorTransform);
+            Assert.True(child.HasMirrorTransform);  
+        }
+
+        [Fact]
+        public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed()
+        {
+            Control child;
+            var target = new Decorator
+            {
+                FlowDirection = FlowDirection.LeftToRight,
+                Child = child = new Control()
+                {
+                    FlowDirection = FlowDirection.LeftToRight,
+                }
+            };
+
+            Assert.False(target.HasMirrorTransform);
+            Assert.False(child.HasMirrorTransform);
+
+            target.FlowDirection = FlowDirection.RightToLeft;
+
+            Assert.True(target.HasMirrorTransform);
+            Assert.True(child.HasMirrorTransform);
+        }
+    }
+}

+ 0 - 28
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -179,34 +179,6 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        [Fact]
-        public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
-        {
-            using (Start())
-            {
-                var source = new Class1();
-                var target = new MaskedTextBox
-                {
-                    DataContext = source,
-                    Template = CreateTemplate(),
-                };
-
-                target.ApplyTemplate();
-                target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
-
-                Assert.Equal("0", target.Text);
-
-                target.CaretIndex = 1;
-                target.RaiseEvent(new TextInputEventArgs
-                {
-                    RoutedEvent = InputElement.TextInputEvent,
-                    Text = "2",
-                });
-
-                Assert.Equal("02", target.Text);
-            }
-        }
-
         [Fact]
         public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
         {

+ 0 - 28
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -180,34 +180,6 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        [Fact]
-        public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
-        {
-            using (UnitTestApplication.Start(Services))
-            {
-                var source = new Class1();
-                var target = new TextBox
-                {
-                    DataContext = source,
-                    Template = CreateTemplate(),
-                };
-
-                target.ApplyTemplate();
-                target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
-
-                Assert.Equal("0", target.Text);
-
-                target.CaretIndex = 1;
-                target.RaiseEvent(new TextInputEventArgs
-                {
-                    RoutedEvent = InputElement.TextInputEvent,
-                    Text = "2",
-                });
-
-                Assert.Equal("02", target.Text);
-            }
-        }
-
         [Fact]
         public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
         {