Browse Source

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

pull/4339/head
Benedikt Schroeder 6 years ago
parent
commit
6aedb7cce8
  1. 13
      native/Avalonia.Native/src/OSX/window.mm
  2. 8
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  3. 16
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  4. 31
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  5. 1
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  6. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  7. 20
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  8. 2
      src/Avalonia.Controls/Repeater/ElementFactory.cs
  9. 1
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  10. 29
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  11. 6
      src/Avalonia.Controls/Templates/FuncTemplate`2.cs
  12. 12
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  13. 25
      src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs
  14. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  15. 11
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  16. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  17. 2
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  18. 40
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  19. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

13
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

8
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>

16
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);

31
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;

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;
}

21
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();

20
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;

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 });

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);

29
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>

6
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>

12
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
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);
}
}

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");

11
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;
}
}
}

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
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
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]

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);

Loading…
Cancel
Save