Browse Source

Merge branch 'fixes/TextLineCaretNavigation' of https://github.com/Gillibald/Avalonia into fixes/TextLineCaretNavigation

Benedikt Schroeder 5 years ago
parent
commit
6aedb7cce8

+ 9 - 4
native/Avalonia.Native/src/OSX/window.mm

@@ -1291,10 +1291,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _parent->UpdateCursor();
     
     auto fsize = [self convertSizeToBacking: [self frame].size];
-    _lastPixelSize.Width = (int)fsize.width;
-    _lastPixelSize.Height = (int)fsize.height;
-    [self updateRenderTarget];
-    _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
+    
+    if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height)
+    {
+        _lastPixelSize.Width = (int)fsize.width;
+        _lastPixelSize.Height = (int)fsize.height;
+        [self updateRenderTarget];
+    
+        _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
+    }
 }
 
 - (void)updateLayer

+ 7 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -10,7 +10,13 @@
               HorizontalAlignment="Center"
               Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8">
-        <ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True"  SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
+        <ListBox Items="{Binding Items}"
+                 SelectedItem="{Binding SelectedItem}"
+                 SelectedItems="{Binding SelectedItems}"
+                 AutoScrollToSelectedItem="True"
+                 SelectionMode="{Binding SelectionMode}"
+                 Width="250"
+                 Height="350"/>
 
         <Button Command="{Binding AddItemCommand}">Add</Button>
 

+ 10 - 6
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -88,7 +88,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>(nameof(SelectedDate), 
                 x => x.SelectedDate, (x, v) => x.SelectedDate = v);
 
-        //Template Items
+        // Template Items
         private Button _flyoutButton;
         private TextBlock _dayText;
         private TextBlock _monthText;
@@ -359,10 +359,14 @@ namespace Avalonia.Controls
                 }
             }
 
-            Grid.SetColumn(_spacer1, 1);
-            Grid.SetColumn(_spacer2, 3);
-            _spacer1.IsVisible = columnIndex > 1;
-            _spacer2.IsVisible = columnIndex > 2;
+            var isSpacer1Visible = columnIndex > 1;
+            var isSpacer2Visible = columnIndex > 2;
+            // ternary conditional operator is used to make sure grid cells will be validated
+            Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
+            Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
+
+            _spacer1.IsVisible = isSpacer1Visible;
+            _spacer2.IsVisible = isSpacer2Visible;
         }
 
         private void SetSelectedDateText()
@@ -398,7 +402,7 @@ namespace Avalonia.Controls
 
             var deltaY = _presenter.GetOffsetForPopup();
 
-            //The extra 5 px I think is related to default popup placement behavior
+            // The extra 5 px I think is related to default popup placement behavior
             _popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
                 Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
                  Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);

+ 17 - 14
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Controls
             DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>(x => 
             x.YearVisible, (x, v) => x.YearVisible = v);
 
-        //Template Items
+        // Template Items
         private Grid _pickerContainer;
         private Button _acceptButton;
         private Button _dismissButton;
@@ -107,7 +107,7 @@ namespace Avalonia.Controls
         private bool _yearVisible = true;
         private DateTimeOffset _syncDate;
 
-        private GregorianCalendar _calendar;
+        private readonly GregorianCalendar _calendar;
         private bool _suppressUpdateSelection;
 
         public DatePickerPresenter()
@@ -234,7 +234,7 @@ namespace Avalonia.Controls
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
             base.OnApplyTemplate(e);
-            //These are requirements, so throw if not found
+            // These are requirements, so throw if not found
             _pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
             _monthHost = e.NameScope.Get<Panel>("MonthHost");
             _dayHost = e.NameScope.Get<Panel>("DayHost");
@@ -326,7 +326,7 @@ namespace Avalonia.Controls
         /// </summary>
         private void InitPicker()
         {
-            //OnApplyTemplate must've been called before we can init here...
+            // OnApplyTemplate must've been called before we can init here...
             if (_pickerContainer == null)
                 return;
 
@@ -344,12 +344,11 @@ namespace Avalonia.Controls
 
             SetGrid();
 
-            //Date should've been set when we reach this point
+            // Date should've been set when we reach this point
             var dt = Date;
             if (DayVisible)
             {
-                GregorianCalendar gc = new GregorianCalendar();
-                var maxDays = gc.GetDaysInMonth(dt.Year, dt.Month);
+                var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
                 _daySelector.MaximumValue = maxDays;
                 _daySelector.MinimumValue = 1;
                 _daySelector.SelectedValue = dt.Day;
@@ -407,10 +406,14 @@ namespace Avalonia.Controls
                 }
             }
 
-            Grid.SetColumn(_spacer1, 1);
-            Grid.SetColumn(_spacer2, 3);
-            _spacer1.IsVisible = columnIndex > 1;
-            _spacer2.IsVisible = columnIndex > 2;
+            var isSpacer1Visible = columnIndex > 1;
+            var isSpacer2Visible = columnIndex > 2;
+            // ternary conditional operator is used to make sure grid cells will be validated
+            Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
+            Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
+
+            _spacer1.IsVisible = isSpacer1Visible;
+            _spacer2.IsVisible = isSpacer2Visible;
         }
 
         private void SetInitialFocus()
@@ -433,12 +436,12 @@ namespace Avalonia.Controls
             }
         }
 
-        private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+        private void OnDismissButtonClicked(object sender, RoutedEventArgs e)
         {
             OnDismiss();
         }
 
-        private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+        private void OnAcceptButtonClicked(object sender, RoutedEventArgs e)
         {
             Date = _syncDate;
             OnConfirmed();
@@ -471,7 +474,7 @@ namespace Avalonia.Controls
 
             _syncDate = newDate;
 
-            //We don't need to update the days if not displaying day, not february
+            // We don't need to update the days if not displaying day, not february
             if (!DayVisible || _syncDate.Month != 2)
                 return;
 

+ 0 - 1
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -142,7 +142,6 @@ namespace Avalonia.Controls.Generators
             private readonly IDataTemplate _inner;
             public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
             public IControl Build(object param) => _inner.Build(param);
-            public bool SupportsRecycling => _inner.SupportsRecycling;
             public bool Match(object data) => _inner.Match(data);
             public InstancedBinding ItemsSelector(object item) => null;
         }

+ 10 - 11
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -86,7 +86,7 @@ namespace Avalonia.Controls.Presenters
 
         private IControl _child;
         private bool _createdChild;
-        private IDataTemplate _dataTemplate;
+        private IRecyclingDataTemplate _recyclingDataTemplate;
         private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
         /// <summary>
@@ -281,7 +281,7 @@ namespace Avalonia.Controls.Presenters
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            _dataTemplate = null;
+            _recyclingDataTemplate = null;
             _createdChild = false;
             InvalidateMeasure();
         }
@@ -307,22 +307,21 @@ namespace Avalonia.Controls.Presenters
             {
                 var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
 
-                // We have content and it isn't a control, so if the new data template is the same
-                // as the old data template, try to recycle the existing child control to display
-                // the new data.
-                if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling)
+                if (dataTemplate is IRecyclingDataTemplate rdt)
                 {
-                    newChild = oldChild;
+                    var toRecycle = rdt == _recyclingDataTemplate ? oldChild : null;
+                    newChild = rdt.Build(content, toRecycle);
+                    _recyclingDataTemplate = rdt;
                 }
                 else
                 {
-                    _dataTemplate = dataTemplate;
-                    newChild = _dataTemplate.Build(content);
+                    newChild = dataTemplate.Build(content);
+                    _recyclingDataTemplate = null;
                 }
             }
             else
             {
-                _dataTemplate = null;
+                _recyclingDataTemplate = null;
             }
 
             return newChild;
@@ -422,7 +421,7 @@ namespace Avalonia.Controls.Presenters
                 LogicalChildren.Remove(Child);
                 ((ISetInheritanceParent)Child).SetParent(Child.Parent);
                 Child = null;
-                _dataTemplate = null;
+                _recyclingDataTemplate = null;
             }
 
             InvalidateMeasure();

+ 15 - 5
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -692,14 +692,24 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            foreach (var i in e.SelectedIndices)
+            if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
             {
-                Mark(i.GetAt(0), true);
-            }
+                foreach (var i in e.SelectedIndices)
+                {
+                    Mark(i.GetAt(0), true);
+                }
 
-            foreach (var i in e.DeselectedIndices)
+                foreach (var i in e.DeselectedIndices)
+                {
+                    Mark(i.GetAt(0), false);
+                }
+            }
+            else if (e.DeselectedItems.Count > 0)
             {
-                Mark(i.GetAt(0), false);
+                // (De)selected indices being empty means that a selected item was removed from
+                // the Items (it can't tell us the index of the item because the index is no longer
+                // valid). In this case, we just update the selection state of all containers.
+                UpdateContainerSelection();
             }
 
             var newSelectedIndex = SelectedIndex;

+ 0 - 2
src/Avalonia.Controls/Repeater/ElementFactory.cs

@@ -4,8 +4,6 @@ namespace Avalonia.Controls
 {
     public abstract class ElementFactory : IElementFactory
     {
-        bool IDataTemplate.SupportsRecycling => false;
-
         public IControl Build(object data)
         {
             return GetElementCore(new ElementFactoryGetArgs { Data = data });

+ 0 - 1
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

@@ -13,7 +13,6 @@ namespace Avalonia.Controls
 
         public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
 
-        public bool SupportsRecycling => false;
         public IControl Build(object param) => GetElement(null, param);
         public bool Match(object data) => _dataTemplate.Match(data);
 

+ 21 - 8
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Controls.Templates
     /// <summary>
     /// Builds a control for a piece of data.
     /// </summary>
-    public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
+    public class FuncDataTemplate : FuncTemplate<object, IControl>, IRecyclingDataTemplate
     {
         /// <summary>
         /// The default data template used in the case where no matching data template is found.
@@ -30,10 +30,8 @@ namespace Avalonia.Controls.Templates
                 },
                 true);
 
-        /// <summary>
-        /// The implementation of the <see cref="Match"/> method.
-        /// </summary>
         private readonly Func<object, bool> _match;
+        private readonly bool _supportsRecycling;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
@@ -70,12 +68,9 @@ namespace Avalonia.Controls.Templates
             Contract.Requires<ArgumentNullException>(match != null);
 
             _match = match;
-            SupportsRecycling = supportsRecycling;
+            _supportsRecycling = supportsRecycling;
         }
 
-        /// <inheritdoc/>
-        public bool SupportsRecycling { get; }
-
         /// <summary>
         /// Checks to see if this data template matches the specified data.
         /// </summary>
@@ -88,6 +83,24 @@ namespace Avalonia.Controls.Templates
             return _match(data);
         }
 
+        /// <summary>
+        /// Creates or recycles a control to display the specified data.
+        /// </summary>
+        /// <param name="data">The data to display.</param>
+        /// <param name="existing">An optional control to recycle.</param>
+        /// <returns>
+        /// The <paramref name="existing"/> control if supplied and applicable to
+        /// <paramref name="data"/>, otherwise a new control.
+        /// </returns>
+        /// <remarks>
+        /// The caller should ensure that any control passed to <paramref name="existing"/>
+        /// originated from the same data template.
+        /// </remarks>
+        public IControl Build(object data, IControl existing)
+        {
+            return _supportsRecycling && existing is object ? existing : Build(data);
+        }
+
         /// <summary>
         /// Determines of an object is of the specified type.
         /// </summary>

+ 3 - 3
src/Avalonia.Controls/Templates/FuncTemplate`2.cs

@@ -1,5 +1,7 @@
 using System;
 
+#nullable enable
+
 namespace Avalonia.Controls.Templates
 {
     /// <summary>
@@ -18,9 +20,7 @@ namespace Avalonia.Controls.Templates
         /// <param name="func">The function used to create the control.</param>
         public FuncTemplate(Func<TParam, INameScope, TControl> func)
         {
-            Contract.Requires<ArgumentNullException>(func != null);
-
-            _func = func;
+            _func = func ?? throw new ArgumentNullException(nameof(func));
         }
 
         /// <summary>

+ 5 - 7
src/Avalonia.Controls/Templates/IDataTemplate.cs

@@ -1,3 +1,7 @@
+using System;
+
+#nullable enable
+
 namespace Avalonia.Controls.Templates
 {
     /// <summary>
@@ -5,12 +9,6 @@ namespace Avalonia.Controls.Templates
     /// </summary>
     public interface IDataTemplate : ITemplate<object, IControl>
     {
-        /// <summary>
-        /// Gets a value indicating whether the data template supports recycling of the generated
-        /// control.
-        /// </summary>
-        bool SupportsRecycling { get; }
-
         /// <summary>
         /// Checks to see if this data template matches the specified data.
         /// </summary>
@@ -20,4 +18,4 @@ namespace Avalonia.Controls.Templates
         /// </returns>
         bool Match(object data);
     }
-}
+}

+ 25 - 0
src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs

@@ -0,0 +1,25 @@
+#nullable enable
+
+namespace Avalonia.Controls.Templates
+{
+    /// <summary>
+    /// An <see cref="IDataTemplate"/> that supports recycling existing elements.
+    /// </summary>
+    public interface IRecyclingDataTemplate : IDataTemplate
+    {
+        /// <summary>
+        /// Creates or recycles a control to display the specified data.
+        /// </summary>
+        /// <param name="data">The data to display.</param>
+        /// <param name="existing">An optional control to recycle.</param>
+        /// <returns>
+        /// The <paramref name="existing"/> control if supplied and applicable to
+        /// <paramref name="data"/>, otherwise a new control.
+        /// </returns>
+        /// <remarks>
+        /// The caller should ensure that any control passed to <paramref name="existing"/>
+        /// originated from the same data template.
+        /// </remarks>
+        IControl Build(object data, IControl? existing);
+    }
+}

+ 0 - 2
src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@@ -7,8 +7,6 @@ namespace Avalonia.Diagnostics
 {
     internal class ViewLocator : IDataTemplate
     {
-        public bool SupportsRecycling => false;
-
         public IControl Build(object data)
         {
             var name = data.GetType().FullName.Replace("ViewModel", "View");

+ 7 - 4
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -5,7 +5,7 @@ using Avalonia.Metadata;
 
 namespace Avalonia.Markup.Xaml.Templates
 {
-    public class DataTemplate : IDataTemplate
+    public class DataTemplate : IRecyclingDataTemplate
     {
         public Type DataType { get; set; }
 
@@ -14,8 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public bool SupportsRecycling { get; set; } = true;
-
         public bool Match(object data)
         {
             if (DataType == null)
@@ -28,6 +26,11 @@ namespace Avalonia.Markup.Xaml.Templates
             }
         }
 
-        public IControl Build(object data) => TemplateContent.Load(Content).Control;
+        public IControl Build(object data) => Build(data, null);
+
+        public IControl Build(object data, IControl existing)
+        {
+            return existing ?? TemplateContent.Load(Content).Control;
+        }
     }
 }

+ 0 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -18,8 +18,6 @@ namespace Avalonia.Markup.Xaml.Templates
         [AssignBinding]
         public Binding ItemsSource { get; set; }
 
-        public bool SupportsRecycling { get; set; } = true;
-
         public bool Match(object data)
         {
             if (DataType == null)

+ 2 - 0
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -402,6 +402,8 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_GETMINMAXINFO:
                     {
                         MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
+                        
+                        _maxTrackSize = mmi.ptMaxTrackSize;
 
                         if (_minSize.Width > 0)
                         {

+ 40 - 0
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -531,6 +531,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             };
 
             target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
             target.SelectedIndex = 1;
 
             Assert.Equal(items[1], target.SelectedItem);
@@ -549,6 +550,45 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.NotNull(receivedArgs);
             Assert.Empty(receivedArgs.AddedItems);
             Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
+            Assert.False(items.Single().IsSelected);
+        }
+
+        [Fact]
+        public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit()
+        {
+            var items = new AvaloniaList<Item>
+            {
+                new Item(),
+                new Item(),
+            };
+
+            var target = new SelectingItemsControl();
+            target.BeginInit();
+            target.Items = items;
+            target.Template = Template();
+            target.EndInit();
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+            target.SelectedIndex = 0;
+
+            Assert.Equal(items[0], target.SelectedItem);
+            Assert.Equal(0, target.SelectedIndex);
+
+            SelectionChangedEventArgs receivedArgs = null;
+
+            target.SelectionChanged += (_, args) => receivedArgs = args;
+
+            var removed = items[0];
+
+            items.RemoveAt(0);
+
+            Assert.Null(target.SelectedItem);
+            Assert.Equal(-1, target.SelectedIndex);
+            Assert.NotNull(receivedArgs);
+            Assert.Empty(receivedArgs.AddedItems);
+            Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
+            Assert.False(items.Single().IsSelected);
         }
 
         [Fact]

+ 0 - 2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1273,8 +1273,6 @@ namespace Avalonia.Controls.UnitTests
                 return new TextBlock { Text = node.Value };
             }
 
-            public bool SupportsRecycling => false;
-
             public InstancedBinding ItemsSelector(object item)
             {
                 var obs = ExpressionObserver.Create(item, o => (o as Node).Children);