diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..8699508320 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -36,6 +36,7 @@ + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 0ca3567970..f90a0c4658 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -37,10 +37,6 @@ - - + + TabStrip + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 0000000000..f0630cf534 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class TabStripPage : UserControl + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new[] + { + new TabStripItemViewModel + { + Header = "Item 1", + }, + new TabStripItemViewModel + { + Header = "Item 2", + }, + new TabStripItemViewModel + { + Header = "Disabled", + IsEnabled = false, + }, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private class TabStripItemViewModel + { + public string Header { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3047b1e519..3513e94107 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -29,8 +29,7 @@ Name="PART_ItemsPresenter" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" - ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> - \ No newline at end of file + diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 990a4b04f2..0ffd6a9539 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return AvaloniaProperty.UnsetValue; + return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 473c4fe21b..1e2fc9f9d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -345,7 +345,6 @@ namespace Avalonia.Controls /// private IDisposable _collectionChangeSubscription; - private IMemberSelector _valueMemberSelector; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; @@ -541,12 +540,6 @@ namespace Avalonia.Controls o => o.Items, (o, v) => o.Items = v); - public static readonly DirectProperty ValueMemberSelectorProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueMemberSelector), - o => o.ValueMemberSelector, - (o, v) => o.ValueMemberSelector = v); - public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), @@ -958,20 +951,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the MemberSelector that is used to get values for - /// display in the text portion of the - /// control. - /// - /// The MemberSelector that is used to get values for display in - /// the text portion of the - /// control. - public IMemberSelector ValueMemberSelector - { - get { return _valueMemberSelector; } - set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } - } - /// /// Gets or sets the selected item in the drop-down. /// @@ -1841,11 +1820,6 @@ namespace Avalonia.Controls return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; } - if (_valueMemberSelector != null) - { - value = _valueMemberSelector.Select(value); - } - return value == null ? String.Empty : value.ToString(); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 5d427df5a6..f32b8fabc6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -333,8 +333,7 @@ namespace Avalonia.Controls } else { - var selector = MemberSelector; - SelectionBoxItem = selector != null ? selector.Select(item) : item; + SelectionBoxItem = item; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..58b4324a3e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,12 +1,12 @@ using System; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +90,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +112,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +127,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +143,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +171,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 653a4f5dcb..2d6757219d 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators /// The index of the item of data in the control's items. /// /// The item. - /// An optional member selector. /// The created controls. - ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector); + ItemContainerInfo Materialize(int index, object item); /// /// Removes a set of created containers. @@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); - bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector); + bool TryRecycle(int oldIndex, int newIndex, object item); /// /// Clears all created containers and returns the removed controls. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f1a1f94a01..4fd6f4135c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators public virtual Type ContainerType => null; /// - public ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector) + public ItemContainerInfo Materialize(int index, object item) { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainerInfo(CreateContainer(i), item, index); + var container = new ItemContainerInfo(CreateContainer(item), item, index); _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); @@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators } /// - public virtual bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) - { - return false; - } + public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; /// public virtual IEnumerable Clear() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 320d6c8faf..d1d1c2b172 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators } /// - public override bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) { var container = ContainerFromIndex(oldIndex); @@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators throw new IndexOutOfRangeException("Could not recycle container: not materialized."); } - var i = selector != null ? selector.Select(item) : item; - - container.SetValue(ContentProperty, i); + container.SetValue(ContentProperty, item); if (!(item is IControl)) { - container.DataContext = i; + container.DataContext = item; } - var info = MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, item); RaiseRecycled(new ItemContainerEventArgs(info)); return true; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 43d1108fb9..c06a64443c 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -117,10 +117,7 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } - public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) - { - return false; - } + public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; class WrapperTreeDataTemplate : ITreeDataTemplate { diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a292ff7d0a..902e55bde6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,12 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - AvaloniaProperty.Register(nameof(MemberSelector)); - private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -144,15 +138,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the items presenter control. /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3150b6be91..f26cd47bcb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -68,7 +68,13 @@ namespace Avalonia.Controls /// public new IList SelectedItems => base.SelectedItems; - /// + /// + /// Gets or sets the selection mode. + /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// public new SelectionMode SelectionMode { get { return base.SelectionMode; } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..b60a97e1c8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -40,37 +40,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..8eed58bb4d 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -7,7 +7,6 @@ using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -31,13 +30,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a3123cf8c6..dedab3e43e 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters if (container == null && IsVirtualized) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + var materialized = ItemContainerGenerator.Materialize(index, item); Panel.Children.Add(materialized.ContainerControl); container = materialized.ContainerControl; } diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs index 035d404dec..6e72908e6b 100644 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 413855bcc6..56f64779f6 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, Owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d11ce9a7ea..b8b8094582 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) { - var memberSelector = Owner.MemberSelector; var index = NextIndex; var step = 1; @@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters } } - var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + var materialized = generator.Materialize(index, Items.ElementAt(index)); if (step == 1) { @@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; var containers = generator.Containers.ToList(); var itemIndex = FirstIndex; @@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters if (!object.Equals(container.Item, item)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + if (!generator.TryRecycle(itemIndex, itemIndex, item)) { throw new NotImplementedException(); } @@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; //validate delta it should never overflow last index or generate index < 0 delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); @@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters var item = Items.ElementAt(newItemIndex); - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b4b792139d..ea56a0c6fc 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - ItemsControl.MemberSelectorProperty.AddOwner(); - private IEnumerable _items; private IDisposable _itemsSubscription; private bool _createdPanel; @@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the panel used to display the items. /// diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..058658357f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; @@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 91a9fa7e40..188685f796 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the selection mode. /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } @@ -338,24 +342,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var selectedIndex = SelectedIndex; - var selectedContainer = e.Containers - .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + var resetSelectedItems = false; - if (selectedContainer != null) + foreach (var container in e.Containers) { - SelectedIndex = selectedContainer.Index; - } - else if (selectedIndex >= e.StartingIndex && - selectedIndex < e.StartingIndex + e.Containers.Count) - { - var container = e.Containers[selectedIndex - e.StartingIndex]; + if ((container.ContainerControl as ISelectable)?.IsSelected == true) + { + if (SelectedIndex == -1) + { + SelectedIndex = container.Index; + } + else + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } - if (container.ContainerControl != null) + MarkContainerSelected(container.ContainerControl, true); + } + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// @@ -469,11 +485,6 @@ namespace Avalonia.Controls.Primitives /// protected void SelectAll() { - if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) - { - throw new NotSupportedException("Multiple selection is not enabled on this control."); - } - UpdateSelectedItems(() => { _selection.Clear(); @@ -523,7 +534,14 @@ namespace Avalonia.Controls.Primitives var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var range = multi && rangeModifier; - if (range) + if (rightButton) + { + if (!_selection.Contains(index)) + { + UpdateSelectedItem(index); + } + } + else if (range) { UpdateSelectedItems(() => { @@ -582,7 +600,7 @@ namespace Avalonia.Controls.Primitives } else { - UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); + UpdateSelectedItem(index); } if (Presenter?.Panel != null) diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..a61757e628 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -12,11 +12,8 @@ namespace Avalonia.Controls.Primitives private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); - static TabStrip() { - MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -51,11 +48,5 @@ namespace Avalonia.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } - - private static object SelectHeader(object o) - { - var headered = o as IHeadered; - return (headered != null) ? (headered.Header ?? string.Empty) : o; - } } } diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs deleted file mode 100644 index 5ab186261e..0000000000 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object using a . - /// - public class FuncMemberSelector : IMemberSelector - { - private readonly Func _selector; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The selector. - public FuncMemberSelector(Func selector) - { - this._selector = selector; - } - - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - public object Select(object o) - { - return (o is TObject) ? _selector((TObject)o) : default(TMember); - } - } -} diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs deleted file mode 100644 index e1ec42a849..0000000000 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object. - /// - public interface IMemberSelector - { - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - object Select(object o); - } -} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 888f4a2013..4514109e12 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,32 +105,21 @@ namespace Avalonia.Controls get => _selectedItem; set { - SetAndRaise(SelectedItemProperty, ref _selectedItem, - (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - backing = val; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - val)); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); - if (val != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != val) - { - _syncingSelectedItems = true; - SelectSingleItem(val); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - }, value); + if (value != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } } } @@ -164,6 +153,48 @@ namespace Avalonia.Controls } } + /// + /// Expands the specified all descendent s. + /// + /// The item to expand. + public void ExpandSubTree(TreeViewItem item) + { + item.IsExpanded = true; + + var panel = item.Presenter.Panel; + + if (panel != null) + { + foreach (var child in panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + ExpandSubTree(treeViewItem); + } + } + } + } + + /// + /// Selects all items in the . + /// + /// + /// Note that this method only selects nodes currently visible due to their parent nodes + /// being expanded: it does not expand nodes. + /// + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } + + /// + /// Deselects all items in the . + /// + public void UnselectAll() + { + SelectedItems.Clear(); + } + /// /// Subscribes to the CollectionChanged event, if any. /// @@ -409,7 +440,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SelectAll(); e.Handled = true; } } @@ -479,7 +510,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } @@ -490,11 +522,13 @@ namespace Avalonia.Controls /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var item = ItemContainerGenerator.Index.ItemFromContainer(container); @@ -515,7 +549,14 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!toggle && !range) + if (rightButton) + { + if (!SelectedItems.Contains(item)) + { + SelectSingleItem(item); + } + } + else if (!toggle && !range) { SelectSingleItem(item); } @@ -684,6 +725,7 @@ namespace Avalonia.Controls /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -692,13 +734,14 @@ namespace Avalonia.Controls IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 0f55d42e33..a538516c1a 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,23 +1,24 @@ - - - - - - + - - - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - - + + + + + + + + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index e0bacf326b..ccb6151ada 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -1,10 +1,13 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; @@ -82,7 +85,8 @@ namespace Avalonia.Diagnostics DataTemplates = { new ViewLocator(), - } + }, + Title = "Avalonia DevTools" }; devToolsWindow.Closed += devTools.DevToolsClosed; diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index b107338aec..cda511909a 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics return data is TViewModel; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index c6d3f02e8b..bc80ab0550 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; @@ -10,21 +12,23 @@ namespace Avalonia.Diagnostics.ViewModels { internal class DevToolsViewModel : ViewModelBase { - private ViewModelBase _content; - private int _selectedTab; - private TreePageViewModel _logicalTree; - private TreePageViewModel _visualTree; - private EventsViewModel _eventsView; + private IDevToolViewModel _selectedTool; private string _focusedControl; private string _pointerOverElement; public DevToolsViewModel(IControl root) { - _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); - _eventsView = new EventsViewModel(root); + Tools = new ObservableCollection + { + new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), + new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), + new EventsViewModel(root) + }; + + SelectedTool = Tools.First(); UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) @@ -33,58 +37,33 @@ namespace Avalonia.Diagnostics.ViewModels } }; - SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } - public ViewModelBase Content + public IDevToolViewModel SelectedTool { - get { return _content; } - private set { RaiseAndSetIfChanged(ref _content, value); } + get => _selectedTool; + set => RaiseAndSetIfChanged(ref _selectedTool, value); } - public int SelectedTab - { - get { return _selectedTab; } - set - { - _selectedTab = value; - - switch (value) - { - case 0: - Content = _logicalTree; - break; - case 1: - Content = _visualTree; - break; - case 2: - Content = _eventsView; - break; - } - - RaisePropertyChanged(); - } - } + public ObservableCollection Tools { get; } public string FocusedControl { - get { return _focusedControl; } - private set { RaiseAndSetIfChanged(ref _focusedControl, value); } + get => _focusedControl; + private set => RaiseAndSetIfChanged(ref _focusedControl, value); } public string PointerOverElement { - get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + get => _pointerOverElement; + private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); } public void SelectControl(IControl control) { - var tree = Content as TreePageViewModel; - - if (tree != null) + if (SelectedTool is TreePageViewModel tree) { tree.SelectControl(control); } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs index a23677afc8..1c868148ce 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -5,8 +5,6 @@ using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; -using System.Windows.Input; - using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Interactivity; @@ -14,21 +12,24 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { - internal class EventsViewModel : ViewModelBase + internal class EventsViewModel : ViewModelBase, IDevToolViewModel { private readonly IControl _root; private FiredEvent _selectedEvent; public EventsViewModel(IControl root) { - this._root = root; - this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + _root = root; + + Nodes = RoutedEventRegistry.Instance.GetAllRegistered() .GroupBy(e => e.OwnerType) .OrderBy(e => e.Key.Name) .Select(g => new EventOwnerTreeNode(g.Key, g, this)) .ToArray(); } + public string Name => "Events"; + public EventTreeNodeBase[] Nodes { get; } public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); @@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + return (bool)value ? Brushes.Green : Brushes.Transparent; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs new file mode 100644 index 0000000000..0434230a63 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Avalonia.Diagnostics.ViewModels +{ + /// + /// View model interface for tool showing up in DevTools + /// + public interface IDevToolViewModel + { + /// + /// Name of a tool. + /// + string Name { get; } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index dba44c5d0c..6b294c98bd 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,16 +6,19 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase + internal class TreePageViewModel : ViewModelBase, IDevToolViewModel { private TreeNode _selected; private ControlDetailsViewModel _details; - public TreePageViewModel(TreeNode[] nodes) + public TreePageViewModel(TreeNode[] nodes, string name) { Nodes = nodes; + Name = name; } + public string Name { get; } + public TreeNode[] Nodes { get; protected set; } public TreeNode SelectedNode diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 381b2e04b4..868bc774bb 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -7,7 +7,6 @@ using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Media; -using Avalonia.Styling; namespace Avalonia.Diagnostics.Views { @@ -42,16 +41,6 @@ namespace Avalonia.Diagnostics.Views { Content = _grid = new SimpleGrid { - Styles = - { - new Style(x => x.Is()) - { - Setters = new[] - { - new Setter(MarginProperty, new Thickness(2)), - } - }, - }, [GridRepeater.TemplateProperty] = pt, } }; @@ -61,8 +50,11 @@ namespace Avalonia.Diagnostics.Views { var property = (PropertyDetails)i; + var margin = new Thickness(2); + yield return new TextBlock { + Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), @@ -70,6 +62,7 @@ namespace Avalonia.Diagnostics.Views yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) @@ -78,6 +71,7 @@ namespace Avalonia.Diagnostics.Views yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), }; diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs similarity index 85% rename from src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs rename to src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs index 2d833763a6..0bd08929ad 100644 --- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs +++ b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.ComponentModel; using System.Reactive.Linq; using System.Reflection; diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs index d445f1cd70..88cbb03c34 100644 --- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs +++ b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 11d8a344d9..788b60892b 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -27,7 +27,6 @@ Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding ValueMemberSelector}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" /> diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Carousel.xaml index efe12c4333..955a49a974 100644 --- a/src/Avalonia.Themes.Default/Carousel.xaml +++ b/src/Avalonia.Themes.Default/Carousel.xaml @@ -8,10 +8,9 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" SelectedIndex="{TemplateBinding SelectedIndex}" PageTransition="{TemplateBinding PageTransition}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index ca6c2e372e..6227962a48 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -45,7 +45,6 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}" /> diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/DataValidationErrors.xaml index 0c40a7eb25..f4145a51f5 100644 --- a/src/Avalonia.Themes.Default/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/DataValidationErrors.xaml @@ -29,7 +29,7 @@ - + diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index 7b6671b42c..f3def542fc 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -4,8 +4,7 @@ + ItemTemplate="{TemplateBinding ItemTemplate}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 57b0c541b8..59c596bcaa 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -18,10 +18,9 @@ ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index be86e8b14c..a794d15577 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -56,8 +56,7 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - Margin="2" - MemberSelector="{TemplateBinding MemberSelector}"/> + Margin="2"/> + Margin="2"/> + ItemTemplate="{TemplateBinding ItemTemplate}"> @@ -18,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 4e38c6db3a..6ed2fd17b8 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -15,8 +15,7 @@ + Margin="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index b5e0e7a005..5dd082cf7a 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -32,8 +32,7 @@ + ItemsPanel="{TemplateBinding ItemsPanel}"/> diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index 0ff7862ab6..e82934fbad 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -8,8 +8,8 @@ namespace Avalonia.Rendering { public class RenderLayers : IEnumerable { - private List _inner = new List(); - private Dictionary _index = new Dictionary(); + private readonly List _inner = new List(); + private readonly Dictionary _index = new Dictionary(); public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -56,6 +56,7 @@ namespace Avalonia.Rendering } _index.Clear(); + _inner.Clear(); } public bool TryGetValue(IVisual layerRoot, out RenderLayer value) diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6f3dabd568..06c5375520 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -12,7 +12,6 @@ - @@ -33,7 +32,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs deleted file mode 100644 index 8dc052fe63..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Globalization; -using Avalonia.Markup.Xaml.Templates; - -namespace Avalonia.Markup.Xaml.Converters -{ - using System.ComponentModel; - - public class MemberSelectorTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return MemberSelector.Parse((string)value); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs deleted file mode 100644 index fa91ab60ff..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Parsers; -using System; -using System.Reactive.Linq; - -namespace Avalonia.Markup.Xaml.Templates -{ - public class MemberSelector : IMemberSelector - { - private string _memberName; - - public string MemberName - { - get { return _memberName; } - set - { - if (_memberName != value) - { - _memberName = value; - } - } - } - - public static MemberSelector Parse(string s) - { - return new MemberSelector { MemberName = s }; - } - - public object Select(object o) - { - if (string.IsNullOrEmpty(MemberName)) - { - return o; - } - - var expression = ExpressionObserverBuilder.Build(o, MemberName); - object result = AvaloniaProperty.UnsetValue; - - expression.Subscribe(x => result = x); - return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result; - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index ea9eaee73a..63c8b1c074 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -103,8 +103,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); - Add("Avalonia.Controls.Templates.IMemberSelector", - "Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter"); Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter"); Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter"); Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter"); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index b12b2e3c31..428f878945 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7)); Assert.Equal( new[] @@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.ValidatedDirectProperty] = source.ToBinding(), + [!Class1.ValidatedDirectIntProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); @@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests target.Notifications.AsEnumerable()); } + [Fact] + public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + { + var source = new ViewModel + { + StringValue = "foo", + }; + + var target = new Class1 + { + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; + + Assert.Equal("foo", target.ValidatedDirectString); + + source.StringValue = null; + + Assert.Null(target.ValidatedDirectString); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty NonValidatedProperty = @@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests o => o.NonValidatedDirect, (o, v) => o.NonValidatedDirect = v); - public static readonly DirectProperty ValidatedDirectProperty = + public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(ValidatedDirect), - o => o.ValidatedDirect, - (o, v) => o.ValidatedDirect = v, + nameof(ValidatedDirectInt), + o => o.ValidatedDirectInt, + (o, v) => o.ValidatedDirectInt = v, + enableDataValidation: true); + + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValidatedDirectString), + o => o.ValidatedDirectString, + (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); private int _nonValidatedDirect; - private int _direct; + private int _directInt; + private string _directString; public int NonValidated { @@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests public int NonValidatedDirect { - get { return _direct; } + get { return _directInt; } set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } } - public int ValidatedDirect + public int ValidatedDirectInt + { + get { return _directInt; } + set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); } + } + + public string ValidatedDirectString { - get { return _direct; } - set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); } + get { return _directString; } + set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } public IList Notifications { get; } = new List(); @@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests Notifications.Add(notification); } } + + public class ViewModel : NotifyingBase + { + private string _stringValue; + + public string StringValue + { + get { return _stringValue; } + set { _stringValue = value; RaisePropertyChanged(); } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 6482fcb4da..58d205deaa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests private Mock popupImpl; private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] + public void Opening_Raises_Single_Opened_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + int openedCount = 0; + + sut.MenuOpened += (sender, args) => + { + openedCount++; + }; + + sut.Open(null); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Closing_Raises_Single_Closed_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + sut.Open(null); + + int closedCount = 0; + + sut.MenuClosed += (sender, args) => + { + closedCount++; + }; + + sut.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void Clicking_On_Control_Toggles_ContextMenu() { diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 9b4be59647..70410dff0d 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl; + var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl; + var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs index f63c0efbf9..05954cbcd2 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 2599d24354..b2839360ee 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -430,27 +430,6 @@ namespace Avalonia.Controls.UnitTests dataContexts); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsControl - { - Template = GetTemplate(), - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var text = target.Presenter.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - [Fact] public void Control_Item_Should_Not_Be_NameScope() { @@ -563,7 +542,6 @@ namespace Avalonia.Controls.UnitTests Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], }.RegisterInNameScope(scope) }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 7ca11d9bed..274ca335d9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -310,46 +310,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(target.Panel, child); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsPresenter - { - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var text = target.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - - [Fact] - public void MemberSelector_Should_Set_DataContext() - { - var items = new[] { new Item("Foo"), new Item("Bar") }; - var target = new ItemsPresenter - { - Items = items, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var dataContexts = target.Panel.Children - .Cast() - .Do(x => x.UpdateChild()) - .Select(x => x.DataContext) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, dataContexts); - } - private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index e266150901..2e22725125 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Moq; using Avalonia.Controls.Presenters; @@ -185,6 +186,53 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Open_Should_Raise_Single_Opened_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + + int openedCount = 0; + + target.Opened += (sender, args) => + { + openedCount++; + }; + + target.Open(); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Popup_Close_Should_Raise_Single_Closed_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + target.Open(); + + int closedCount = 0; + + target.Closed += (sender, args) => + { + closedCount++; + }; + + target.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void PopupRoot_Should_Have_Template_Applied() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 9d0cc368e0..4bcfeb6d03 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Assigning_SelectedItems_Should_Set_SelectedIndex() + public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() { var target = new TestSelector { @@ -62,9 +62,51 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("bar"); Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } + + [Fact] + public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() + { + // Note that we don't need SelectionMode = Multiple here. Multiple selections can always + // be made in code. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } + + [Fact] + public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() + { + // Issue #2565. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + target.Presenter.ApplyTemplate(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } [Fact] @@ -1026,6 +1068,71 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Adding_Selected_ItemContainers_Should_Update_Selection() + { + var items = new AvaloniaList(new[] + { + new ItemContainer(), + new ItemContainer(), + }); + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.Add(new ItemContainer { IsSelected = true }); + items.Add(new ItemContainer { IsSelected = true }); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(items[2], target.SelectedItem); + Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems); + } + + [Fact] + public void Shift_Right_Click_Should_Not_Select_Multiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Multiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children @@ -1078,5 +1185,11 @@ namespace Avalonia.Controls.UnitTests.Primitives public List Items { get; } public List SelectedItems { get; } } + + private class ItemContainer : Control, ISelectable + { + public string Value { get; set; } + public bool IsSelected { get; set; } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index a7b90afa70..b4570ec229 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -14,60 +14,6 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class TabStripTests { - [Fact] - public void Header_Of_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { -#pragma warning disable CS0252 // Possible unintended reference comparison; left hand side needs cast - Mock.Of(x => x.Header == "foo"), - Mock.Of(x => x.Header == "bar"), -#pragma warning restore CS0252 // Possible unintended reference comparison; left hand side needs cast - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - - [Fact] - public void Data_Of_Non_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { - "foo", - "bar" - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - [Fact] public void First_Tab_Should_Be_Selected_By_Default() { @@ -165,7 +111,6 @@ namespace Avalonia.Controls.UnitTests.Primitives { Name = "itemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }.RegisterInNameScope(scope); } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index f40571bc39..35f0b39210 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -444,6 +444,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Bound_Text_To_Null_Works() + { + using (UnitTestApplication.Start(Services)) + { + var source = new Class1 { Bar = "bar" }; + var target = new TextBox { DataContext = source }; + + target.Bind(TextBox.TextProperty, new Binding("Bar")); + + Assert.Equal("bar", target.Text); + source.Bar = null; + Assert.Null(target.Text); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -492,12 +508,19 @@ namespace Avalonia.Controls.UnitTests private class Class1 : NotifyingBase { private int _foo; + private string _bar; public int Foo { get { return _foo; } set { _foo = value; RaisePropertyChanged(); } } + + public string Bar + { + get { return _bar; } + set { _bar = value; RaisePropertyChanged(); } + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2868928455..5646e86f7a 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -11,6 +11,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Xunit; @@ -719,6 +720,129 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + 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); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + target.SelectAll(); + + AssertChildrenSelected(target, tree[0]); + Assert.Equal(5, target.SelectedItems.Count); + + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + + Assert.Equal(5, target.SelectedItems.Count); + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + 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); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _mouse.Click(thenContainer, MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Shift_Right_Click_Should_Not_Select_Multiple() + { + 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); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Multiple() + { + 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); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -853,7 +977,6 @@ namespace Avalonia.Controls.UnitTests } } - private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.LeakTests/MemberSelectorTests.cs b/tests/Avalonia.LeakTests/MemberSelectorTests.cs deleted file mode 100644 index ffee18ae0a..0000000000 --- a/tests/Avalonia.LeakTests/MemberSelectorTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Avalonia.Markup.Xaml.Templates; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests -{ - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class MemberSelectorTests - { - public MemberSelectorTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Should_Not_Hold_Reference_To_Object() - { - WeakReference dataRef = null; - - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - Action run = () => - { - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - - dataRef = new WeakReference(data); - }; - - run(); - - GC.Collect(); - - Assert.False(dataRef.IsAlive); - } - - private class Item - { - public Item Child { get; set; } - public int IntValue { get; set; } - - public string StringValue { get; set; } - } - } -} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs deleted file mode 100644 index aa1e56f2a5..0000000000 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Markup.Xaml.Templates; -using System; -using Xunit; - -namespace Avalonia.Markup.Xaml.UnitTests.Templates -{ - public class MemberSelectorTests - { - [Fact] - public void Should_Select_Child_Property_Value() - { - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Child_Property_Value_In_Multiple_Items() - { - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - var data = new Item[] - { - new Item() { Child = new Item() { StringValue = "Value1" } }, - new Item() { Child = new Item() { StringValue = "Value2" } }, - new Item() { Child = new Item() { StringValue = "Value3" } } - }; - - Assert.Same("Value1", selector.Select(data[0])); - Assert.Same("Value2", selector.Select(data[1])); - Assert.Same("Value3", selector.Select(data[2])); - } - - [Fact] - public void Should_Select_MoreComplex_Property_Value() - { - var selector = new MemberSelector() { MemberName = "Child.Child.Child.StringValue" }; - - var data = new Item() - { - Child = new Item() - { - Child = new Item() - { - Child = new Item() { StringValue = "Value1" } - } - } - }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Null_Value_On_Null_Object() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - Assert.Null(selector.Select(null)); - } - - [Fact] - public void Should_Select_Null_Value_On_Wrong_MemberName() - { - var selector = new MemberSelector() { MemberName = "WrongProperty" }; - - var data = new Item() { StringValue = "Value1" }; - - Assert.Null(selector.Select(data)); - } - - [Fact] - public void Should_Select_Simple_Property_Value() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() { StringValue = "Value1" }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Simple_Property_Value_In_Multiple_Items() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item[] - { - new Item() { StringValue = "Value1" }, - new Item() { StringValue = "Value2" }, - new Item() { StringValue = "Value3" } - }; - - Assert.Same("Value1", selector.Select(data[0])); - Assert.Same("Value2", selector.Select(data[1])); - Assert.Same("Value3", selector.Select(data[2])); - } - - [Fact] - public void Should_Select_Target_On_Empty_MemberName() - { - var selector = new MemberSelector(); - - var data = new Item() { StringValue = "Value1" }; - - Assert.Same(data, selector.Select(data)); - } - - [Fact] - public void Should_Support_Change_Of_MemberName() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() - { - StringValue = "Value1", - IntValue = 1 - }; - - Assert.Same("Value1", selector.Select(data)); - - selector.MemberName = "IntValue"; - - Assert.Equal(1, selector.Select(data)); - } - - [Fact] - public void Should_Support_Change_Of_Target_Value() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() - { - StringValue = "Value1" - }; - - Assert.Same("Value1", selector.Select(data)); - - data.StringValue = "Value2"; - - Assert.Same("Value2", selector.Select(data)); - } - - private class Item - { - public Item Child { get; set; } - public int IntValue { get; set; } - - public string StringValue { get; set; } - } - } -} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 8769bf2b8b..d300877f0b 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -106,7 +106,6 @@ namespace Avalonia.ReactiveUI.UnitTests Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], }.RegisterInNameScope(scope) };