diff --git a/.editorconfig b/.editorconfig index b7a03207a4..5f08d1e940 100644 --- a/.editorconfig +++ b/.editorconfig @@ -132,6 +132,9 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# Wrapping preferences +csharp_wrap_before_ternary_opsigns = false + # Xaml files [*.xaml] indent_size = 4 diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index f8f3cd5848..c03edb8b03 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -9,7 +9,7 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index 24b3fc1f32..0da84008f6 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -34,7 +34,12 @@ namespace Avalonia.Controls.Generators /// /// Gets the currently materialized containers. /// - public IEnumerable Items => _containerToItem.Keys; + public IEnumerable Containers => _containerToItem.Keys; + + /// + /// Gets the items of currently materialized containers. + /// + public IEnumerable Items => _containerToItem.Values; /// /// Adds an entry to the index. diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f73a335de5..fbdf885709 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Presenters Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), TextAlignment = TextAlignment, Constraint = availableSize, - }.Measure(); + }.Bounds.Size; } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index f9bbcff9c2..a64dbe0546 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -10,6 +10,7 @@ using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Styling; using Avalonia.VisualTree; @@ -459,6 +460,23 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (!e.Handled) + { + var keymap = AvaloniaLocator.Current.GetService(); + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + { + SynchronizeItems(SelectedItems, Items?.Cast()); + e.Handled = true; + } + } + } + /// /// Moves the selection in the specified direction relative to the current selection. /// @@ -614,32 +632,12 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Gets a range of items from an IEnumerable. - /// - /// The items. - /// The index of the first item. - /// The index of the last item. - /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) - { - var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; - - for (int i = first; i != last; i += step) - { - yield return list[i]; - } - - yield return list[last]; - } - /// /// Makes a list of objects equal another. /// /// The items collection. /// The desired items. - private static void SynchronizeItems(IList items, IEnumerable desired) + internal static void SynchronizeItems(IList items, IEnumerable desired) { int index = 0; @@ -666,6 +664,26 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets a range of items from an IEnumerable. + /// + /// The items. + /// The index of the first item. + /// The index of the last item. + /// The items. + private static IEnumerable GetRange(IEnumerable items, int first, int last) + { + var list = (items as IList) ?? items.Cast().ToList(); + int step = first > last ? -1 : 1; + + for (int i = first; i != last; i += step) + { + yield return list[i]; + } + + yield return list[last]; + } + /// /// Called when a container raises the . /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index af7b0f835e..6b0c48b97b 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -396,7 +396,7 @@ namespace Avalonia.Controls FormattedText.Constraint = Size.Infinity; } - return FormattedText.Measure(); + return FormattedText.Bounds.Size; } return new Size(); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c574799724..94989254dc 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -2,13 +2,16 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; -using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; @@ -34,14 +37,24 @@ namespace Avalonia.Controls (o, v) => o.SelectedItem = v); /// - /// Defines the event. + /// Defines the property. /// - public static readonly RoutedEvent SelectedItemChangedEvent = - RoutedEvent.Register( - "SelectedItemChanged", - RoutingStrategies.Bubble); + public static readonly DirectProperty SelectedItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItems), + o => o.SelectedItems, + (o, v) => o.SelectedItems = v); + /// + /// Defines the property. + /// + protected static readonly StyledProperty SelectionModeProperty = + AvaloniaProperty.Register( + nameof(SelectionMode)); + + private static readonly IList Empty = new object[0]; private object _selectedItem; + private IList _selectedItems; /// /// Initializes static members of the class. @@ -54,16 +67,16 @@ namespace Avalonia.Controls /// /// Occurs when the control's selection changes. /// - public event EventHandler SelectedItemChanged + public event EventHandler SelectionChanged { - add { AddHandler(SelectedItemChangedEvent, value); } - remove { RemoveHandler(SelectedItemChangedEvent, value); } + add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); + remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); } /// /// Gets the for the tree view. /// - public new ITreeItemContainerGenerator ItemContainerGenerator => + public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// @@ -71,67 +84,258 @@ namespace Avalonia.Controls /// public bool AutoScrollToSelectedItem { - get { return GetValue(AutoScrollToSelectedItemProperty); } - set { SetValue(AutoScrollToSelectedItemProperty, value); } + get => GetValue(AutoScrollToSelectedItemProperty); + set => SetValue(AutoScrollToSelectedItemProperty, value); + } + + private bool _syncingSelectedItems; + + /// + /// Gets or sets the selection mode. + /// + public SelectionMode SelectionMode + { + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); } /// /// Gets or sets the selected item. /// public object SelectedItem + { + get => _selectedItem; + set + { + SetAndRaise(SelectedItemProperty, ref _selectedItem, + (object val, ref object backing, Action notifyWrapper) => + { + var old = backing; + backing = val; + + notifyWrapper(() => + RaisePropertyChanged( + SelectedItemProperty, + old, + val)); + + if (val != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != val) + { + _syncingSelectedItems = true; + SelectSingleItem(val); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } + }, value); + } + } + + /// + /// Gets the selected items. + /// + public IList SelectedItems { get { - return _selectedItem; + if (_selectedItems == null) + { + _selectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } + + return _selectedItems; } set { - if (_selectedItem != null) + if (value?.IsFixedSize == true || value?.IsReadOnly == true) { - var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); - MarkContainerSelected(container, false); + throw new NotSupportedException( + "Cannot use a fixed size or read-only collection as SelectedItems."); } - var oldItem = _selectedItem; - SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + UnsubscribeFromSelectedItems(); + _selectedItems = value ?? new AvaloniaList(); + SubscribeToSelectedItems(); + } + } - if (_selectedItem != null) - { - var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); - MarkContainerSelected(container, true); + /// + /// Subscribes to the CollectionChanged event, if any. + /// + private void SubscribeToSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += SelectedItemsCollectionChanged; + } + + SelectedItemsCollectionChanged( + _selectedItems, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private void SelectSingleItem(object item) + { + SelectedItems.Clear(); + SelectedItems.Add(item); + } + + /// + /// Called when the CollectionChanged event is raised. + /// + /// The event sender. + /// The event args. + private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + IList added = null; + IList removed = null; - if (AutoScrollToSelectedItem && container != null) + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + + SelectedItemsAdded(e.NewItems.Cast().ToArray()); + + if (AutoScrollToSelectedItem) { - container.BringIntoView(); + var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]); + + container?.BringIntoView(); } - } - if (oldItem != _selectedItem) - { - // Fire the SelectionChanged event - List removed = new List(); - if (oldItem != null) + added = e.NewItems; + + break; + case NotifyCollectionChangedAction.Remove: + + if (!_syncingSelectedItems) { - removed.Add(oldItem); + if (SelectedItems.Count == 0) + { + SelectedItem = null; + } + else + { + var selectedIndex = SelectedItems.IndexOf(_selectedItem); + + if (selectedIndex == -1) + { + var old = _selectedItem; + _selectedItem = SelectedItems[0]; + + RaisePropertyChanged(SelectedItemProperty, old, _selectedItem); + } + } } - List added = new List(); - if (_selectedItem != null) + foreach (var item in e.OldItems) { - added.Add(_selectedItem); + MarkItemSelected(item, false); } - var changed = new SelectionChangedEventArgs( - SelectedItemChangedEvent, - added, - removed); - RaiseEvent(changed); - } + removed = e.OldItems; + + break; + case NotifyCollectionChangedAction.Reset: + + foreach (IControl container in ItemContainerGenerator.Index.Containers) + { + MarkContainerSelected(container, false); + } + + if (SelectedItems.Count > 0) + { + SelectedItemsAdded(SelectedItems); + + added = SelectedItems; + } + else if (!_syncingSelectedItems) + { + SelectedItem = null; + } + + break; + case NotifyCollectionChangedAction.Replace: + + foreach (var item in e.OldItems) + { + MarkItemSelected(item, false); + } + + foreach (var item in e.NewItems) + { + MarkItemSelected(item, true); + } + + if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) + { + var oldItem = SelectedItem; + var item = SelectedItems[0]; + _selectedItem = item; + RaisePropertyChanged(SelectedItemProperty, oldItem, item); + } + + added = e.NewItems; + removed = e.OldItems; + + break; + } + + if (added?.Count > 0 || removed?.Count > 0) + { + var changed = new SelectionChangedEventArgs( + SelectingItemsControl.SelectionChangedEvent, + added ?? Empty, + removed ?? Empty); + RaiseEvent(changed); + } + } + + private void MarkItemSelected(object item, bool selected) + { + var container = ItemContainerGenerator.Index.ContainerFromItem(item); + + MarkContainerSelected(container, selected); + } + + private void SelectedItemsAdded(IList items) + { + if (items.Count == 0) + { + return; + } + + foreach (object item in items) + { + MarkItemSelected(item, true); + } + + if (SelectedItem == null && !_syncingSelectedItems) + { + SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); + } + } + + /// + /// Unsubscribes from the CollectionChanged event, if any. + /// + private void UnsubscribeFromSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= SelectedItemsCollectionChanged; } } - (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) + (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, + NavigationDirection direction) { if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { @@ -142,10 +346,8 @@ namespace Avalonia.Controls ItemContainerGenerator.ContainerFromIndex(0); return (true, result); } - else - { - return (true, null); - } + + return (true, null); } return (false, null); @@ -186,7 +388,7 @@ namespace Avalonia.Controls if (SelectedItem != null) { var next = GetContainerInDirection( - GetContainerFromEventSource(e.Source) as TreeViewItem, + GetContainerFromEventSource(e.Source), direction.Value, true); @@ -201,6 +403,18 @@ namespace Avalonia.Controls SelectedItem = ElementAt(Items, 0); } } + + if (!e.Handled) + { + var keymap = AvaloniaLocator.Current.GetService(); + bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + + if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + { + SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + e.Handled = true; + } + } } private TreeViewItem GetContainerInDirection( @@ -208,17 +422,9 @@ namespace Avalonia.Controls NavigationDirection direction, bool intoChildren) { - IItemContainerGenerator parentGenerator; + IItemContainerGenerator parentGenerator = GetParentContainerGenerator(from); - if (from?.Parent is TreeView treeView) - { - parentGenerator = treeView.ItemContainerGenerator; - } - else if (from?.Parent is TreeViewItem item) - { - parentGenerator = item.ItemContainerGenerator; - } - else + if (parentGenerator == null) { return null; } @@ -257,6 +463,7 @@ namespace Avalonia.Controls { return GetContainerInDirection(parentItem, direction, false); } + break; } @@ -293,18 +500,182 @@ namespace Avalonia.Controls { var item = ItemContainerGenerator.Index.ItemFromContainer(container); - if (item != null) + if (item == null) { - if (SelectedItem != null) + return; + } + + IControl selectedContainer = null; + + if (SelectedItem != null) + { + selectedContainer = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); + } + + var mode = SelectionMode; + var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; + var multi = (mode & SelectionMode.Multiple) != 0; + var range = multi && selectedContainer != null && rangeModifier; + + if (!toggle && !range) + { + SelectSingleItem(item); + } + else if (multi && range) + { + SelectingItemsControl.SynchronizeItems( + SelectedItems, + GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); + } + else + { + var i = SelectedItems.IndexOf(item); + + if (i != -1) + { + SelectedItems.Remove(item); + } + else + { + if (multi) + { + SelectedItems.Add(item); + } + else + { + SelectedItem = item; + } + } + } + } + + private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item) + { + if (item == null) + { + return null; + } + + switch (item.Parent) + { + case TreeView treeView: + return treeView.ItemContainerGenerator; + case TreeViewItem treeViewItem: + return treeViewItem.ItemContainerGenerator; + default: + return null; + } + } + + /// + /// Find which node is first in hierarchy. + /// + /// Search root. + /// Nodes to find. + /// Node to find. + /// Found first node. + private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) + { + return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB); + } + + private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator, + TreeViewItem nodeA, + TreeViewItem nodeB) + { + IEnumerable containers = containerGenerator.Containers; + + foreach (ItemContainerInfo container in containers) + { + TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB); + + if (node != null) + { + return node; + } + } + + return null; + } + + private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB) + { + if (node == null) + { + return null; + } + + TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; + + if (match != null) + { + return match; + } + + return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB); + } + + /// + /// Returns all items that belong to containers between and . + /// The range is inclusive. + /// + /// From container. + /// To container. + private List GetItemsInRange(TreeViewItem from, TreeViewItem to) + { + var items = new List(); + + if (from == null || to == null) + { + return items; + } + + TreeViewItem firstItem = FindFirstNode(this, from, to); + + if (firstItem == null) + { + return items; + } + + bool wasReversed = false; + + if (firstItem == to) + { + var temp = from; + + from = to; + to = temp; + + wasReversed = true; + } + + TreeViewItem node = from; + + while (node != to) + { + var item = ItemContainerGenerator.Index.ItemFromContainer(node); + + if (item != null) { - var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); - MarkContainerSelected(old, false); + items.Add(item); } - SelectedItem = item; + node = GetContainerInDirection(node, NavigationDirection.Down, true); + } + + var toItem = ItemContainerGenerator.Index.ItemFromContainer(to); - MarkContainerSelected(container, true); + if (toItem != null) + { + items.Add(toItem); + } + + if (wasReversed) + { + items.Reverse(); } + + return items; } /// @@ -341,7 +712,7 @@ namespace Avalonia.Controls /// /// The control that raised the event. /// The container or null if the event did not originate in a container. - protected IControl GetContainerFromEventSource(IInteractive eventSource) + protected TreeViewItem GetContainerFromEventSource(IInteractive eventSource) { var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() .OfType() @@ -349,7 +720,7 @@ namespace Avalonia.Controls if (item != null) { - if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index) + if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index) { return item; } @@ -367,21 +738,23 @@ namespace Avalonia.Controls { var selectedItem = SelectedItem; - if (selectedItem != null) + if (selectedItem == null) { - foreach (var container in e.Containers) - { - if (container.Item == selectedItem) - { - ((TreeViewItem)container.ContainerControl).IsSelected = true; + return; + } - if (AutoScrollToSelectedItem) - { - Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView); - } + foreach (var container in e.Containers) + { + if (container.Item == selectedItem) + { + ((TreeViewItem)container.ContainerControl).IsSelected = true; - break; + if (AutoScrollToSelectedItem) + { + Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView); } + + break; } } } @@ -393,18 +766,18 @@ namespace Avalonia.Controls /// Whether the control is selected private void MarkContainerSelected(IControl container, bool selected) { - if (container != null) + if (container == null) { - var selectable = container as ISelectable; + return; + } - if (selectable != null) - { - selectable.IsSelected = selected; - } - else - { - ((IPseudoClasses)container.Classes).Set(":selected", selected); - } + if (container is ISelectable selectable) + { + selectable.IsSelected = selected; + } + else + { + container.Classes.Set(":selected", selected); } } } diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index 3f51f613a4..e42ca5e0e6 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -27,9 +27,6 @@ namespace Avalonia.Controls remove { _nameScope.Unregistered -= value; } } - /// - Type IStyleable.StyleKey => typeof(UserControl); - /// void INameScope.Register(string name, object element) { diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 726e086d9c..e364d5de0b 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -163,6 +163,8 @@ namespace Avalonia this.Log().Info($"Ready to show {view} with autowired {viewModel}."); view.ViewModel = viewModel; + if (view is IStyledElement styled) + styled.DataContext = viewModel; UpdateContent(view); } diff --git a/src/Avalonia.Themes.Default/UserControl.xaml b/src/Avalonia.Themes.Default/UserControl.xaml index 2bf5f19698..f4d0c21367 100644 --- a/src/Avalonia.Themes.Default/UserControl.xaml +++ b/src/Avalonia.Themes.Default/UserControl.xaml @@ -1,4 +1,4 @@ -