|
|
@ -12,6 +12,7 @@ using Avalonia.Data; |
|
|
using Avalonia.Input; |
|
|
using Avalonia.Input; |
|
|
using Avalonia.Input.Platform; |
|
|
using Avalonia.Input.Platform; |
|
|
using Avalonia.Interactivity; |
|
|
using Avalonia.Interactivity; |
|
|
|
|
|
using Avalonia.Logging; |
|
|
using Avalonia.Styling; |
|
|
using Avalonia.Styling; |
|
|
using Avalonia.VisualTree; |
|
|
using Avalonia.VisualTree; |
|
|
|
|
|
|
|
|
@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
|
private static readonly IList Empty = Array.Empty<object>(); |
|
|
private static readonly IList Empty = Array.Empty<object>(); |
|
|
|
|
|
|
|
|
|
|
|
private readonly Selection _selection = new Selection(); |
|
|
private int _selectedIndex = -1; |
|
|
private int _selectedIndex = -1; |
|
|
private object _selectedItem; |
|
|
private object _selectedItem; |
|
|
private IList _selectedItems; |
|
|
private IList _selectedItems; |
|
|
@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives |
|
|
{ |
|
|
{ |
|
|
if (_updateCount == 0) |
|
|
if (_updateCount == 0) |
|
|
{ |
|
|
{ |
|
|
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) => |
|
|
var effective = (value >= 0 && value < ItemCount) ? value : -1; |
|
|
{ |
|
|
UpdateSelectedItem(effective); |
|
|
var old = backing; |
|
|
|
|
|
var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1; |
|
|
|
|
|
|
|
|
|
|
|
if (old != effective) |
|
|
|
|
|
{ |
|
|
|
|
|
backing = effective; |
|
|
|
|
|
notifyWrapper(() => |
|
|
|
|
|
RaisePropertyChanged( |
|
|
|
|
|
SelectedIndexProperty, |
|
|
|
|
|
old, |
|
|
|
|
|
effective, |
|
|
|
|
|
BindingPriority.LocalValue)); |
|
|
|
|
|
SelectedItem = ElementAt(Items, effective); |
|
|
|
|
|
} |
|
|
|
|
|
}, value); |
|
|
|
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives |
|
|
{ |
|
|
{ |
|
|
if (_updateCount == 0) |
|
|
if (_updateCount == 0) |
|
|
{ |
|
|
{ |
|
|
SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) => |
|
|
UpdateSelectedItem(IndexOf(Items, value)); |
|
|
{ |
|
|
|
|
|
var old = backing; |
|
|
|
|
|
var index = IndexOf(Items, val); |
|
|
|
|
|
var effective = index != -1 ? val : null; |
|
|
|
|
|
|
|
|
|
|
|
if (!object.Equals(effective, old)) |
|
|
|
|
|
{ |
|
|
|
|
|
backing = effective; |
|
|
|
|
|
|
|
|
|
|
|
notifyWrapper(() => |
|
|
|
|
|
RaisePropertyChanged( |
|
|
|
|
|
SelectedItemProperty, |
|
|
|
|
|
old, |
|
|
|
|
|
effective, |
|
|
|
|
|
BindingPriority.LocalValue)); |
|
|
|
|
|
|
|
|
|
|
|
SelectedIndex = index; |
|
|
|
|
|
|
|
|
|
|
|
if (effective != null) |
|
|
|
|
|
{ |
|
|
|
|
|
if (SelectedItems.Count != 1 || SelectedItems[0] != effective) |
|
|
|
|
|
{ |
|
|
|
|
|
_syncingSelectedItems = true; |
|
|
|
|
|
SelectedItems.Clear(); |
|
|
|
|
|
SelectedItems.Add(effective); |
|
|
|
|
|
_syncingSelectedItems = false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
else if (SelectedItems.Count > 0) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedItems.Clear(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, value); |
|
|
|
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
@ -354,31 +307,23 @@ namespace Avalonia.Controls.Primitives |
|
|
{ |
|
|
{ |
|
|
SelectedIndex = 0; |
|
|
SelectedIndex = 0; |
|
|
} |
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); |
|
|
|
|
|
UpdateSelectedItem(_selection.First(), false); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Remove: |
|
|
case NotifyCollectionChangedAction.Remove: |
|
|
case NotifyCollectionChangedAction.Replace: |
|
|
_selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); |
|
|
var selectedIndex = SelectedIndex; |
|
|
UpdateSelectedItem(_selection.First(), false); |
|
|
|
|
|
ResetSelectedItems(); |
|
|
if (selectedIndex >= e.OldStartingIndex && |
|
|
break; |
|
|
selectedIndex < e.OldStartingIndex + e.OldItems.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!AlwaysSelected) |
|
|
|
|
|
{ |
|
|
|
|
|
selectedIndex = SelectedIndex = -1; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
LostSelection(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var items = Items?.Cast<object>(); |
|
|
case NotifyCollectionChangedAction.Replace: |
|
|
if (selectedIndex >= items.Count()) |
|
|
UpdateSelectedItem(SelectedIndex, false); |
|
|
{ |
|
|
ResetSelectedItems(); |
|
|
selectedIndex = SelectedIndex = items.Count() - 1; |
|
|
|
|
|
} |
|
|
|
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Move: |
|
|
case NotifyCollectionChangedAction.Move: |
|
|
@ -439,11 +384,7 @@ namespace Avalonia.Controls.Primitives |
|
|
{ |
|
|
{ |
|
|
if (i.ContainerControl != null && i.Item != null) |
|
|
if (i.ContainerControl != null && i.Item != null) |
|
|
{ |
|
|
{ |
|
|
var ms = MemberSelector; |
|
|
bool selected = _selection.Contains(i.Index); |
|
|
bool selected = ms == null ? |
|
|
|
|
|
SelectedItems.Contains(i.Item) : |
|
|
|
|
|
SelectedItems.OfType<object>().Any(v => Equals(ms.Select(v), i.Item)); |
|
|
|
|
|
|
|
|
|
|
|
MarkContainerSelected(i.ContainerControl, selected); |
|
|
MarkContainerSelected(i.ContainerControl, selected); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -476,9 +417,12 @@ namespace Avalonia.Controls.Primitives |
|
|
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
|
|
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
|
|
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); |
|
|
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); |
|
|
|
|
|
|
|
|
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) |
|
|
if (ItemCount > 0 && |
|
|
|
|
|
Match(keymap.SelectAll) && |
|
|
|
|
|
(((SelectionMode & SelectionMode.Multiple) != 0) || |
|
|
|
|
|
(SelectionMode & SelectionMode.Toggle) != 0)) |
|
|
{ |
|
|
{ |
|
|
SynchronizeItems(SelectedItems, Items?.Cast<object>()); |
|
|
SelectAll(); |
|
|
e.Handled = true; |
|
|
e.Handled = true; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -520,6 +464,41 @@ namespace Avalonia.Controls.Primitives |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Selects all items in the control.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected void SelectAll() |
|
|
|
|
|
{ |
|
|
|
|
|
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) |
|
|
|
|
|
{ |
|
|
|
|
|
throw new NotSupportedException("Multiple selection is not enabled on this control."); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
UpdateSelectedItems(() => |
|
|
|
|
|
{ |
|
|
|
|
|
_selection.Clear(); |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < ItemCount; ++i) |
|
|
|
|
|
{ |
|
|
|
|
|
_selection.Add(i); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
UpdateSelectedItem(0, false); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var container in ItemContainerGenerator.Containers) |
|
|
|
|
|
{ |
|
|
|
|
|
MarkItemSelected(container.Index, true); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ResetSelectedItems(); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Deselects all items in the control.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected void UnselectAll() => UpdateSelectedItem(-1); |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
/// Updates the selection for an item based on user interaction.
|
|
|
/// Updates the selection for an item based on user interaction.
|
|
|
/// </summary>
|
|
|
/// </summary>
|
|
|
@ -527,51 +506,83 @@ namespace Avalonia.Controls.Primitives |
|
|
/// <param name="select">Whether the item should be selected or unselected.</param>
|
|
|
/// <param name="select">Whether the item should be selected or unselected.</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
|
|
|
/// <param name="rightButton">Whether the event is a right-click.</param>
|
|
|
protected void UpdateSelection( |
|
|
protected void UpdateSelection( |
|
|
int index, |
|
|
int index, |
|
|
bool select = true, |
|
|
bool select = true, |
|
|
bool rangeModifier = false, |
|
|
bool rangeModifier = false, |
|
|
bool toggleModifier = false) |
|
|
bool toggleModifier = false, |
|
|
|
|
|
bool rightButton = false) |
|
|
{ |
|
|
{ |
|
|
if (index != -1) |
|
|
if (index != -1) |
|
|
{ |
|
|
{ |
|
|
if (select) |
|
|
if (select) |
|
|
{ |
|
|
{ |
|
|
var mode = SelectionMode; |
|
|
var mode = SelectionMode; |
|
|
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; |
|
|
|
|
|
var multi = (mode & SelectionMode.Multiple) != 0; |
|
|
var multi = (mode & SelectionMode.Multiple) != 0; |
|
|
var range = multi && SelectedIndex != -1 && rangeModifier; |
|
|
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); |
|
|
|
|
|
var range = multi && rangeModifier; |
|
|
|
|
|
|
|
|
if (!toggle && !range) |
|
|
if (range) |
|
|
{ |
|
|
{ |
|
|
SelectedIndex = index; |
|
|
UpdateSelectedItems(() => |
|
|
} |
|
|
{ |
|
|
else if (multi && range) |
|
|
var start = SelectedIndex != -1 ? SelectedIndex : 0; |
|
|
{ |
|
|
var step = start < index ? 1 : -1; |
|
|
SynchronizeItems( |
|
|
|
|
|
SelectedItems, |
|
|
_selection.Clear(); |
|
|
GetRange(Items, SelectedIndex, index)); |
|
|
|
|
|
|
|
|
for (var i = start; i != index; i += step) |
|
|
|
|
|
{ |
|
|
|
|
|
_selection.Add(i); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_selection.Add(index); |
|
|
|
|
|
|
|
|
|
|
|
var first = Math.Min(start, index); |
|
|
|
|
|
var last = Math.Max(start, index); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var container in ItemContainerGenerator.Containers) |
|
|
|
|
|
{ |
|
|
|
|
|
MarkItemSelected( |
|
|
|
|
|
container.Index, |
|
|
|
|
|
container.Index >= first && container.Index <= last); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ResetSelectedItems(); |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
else |
|
|
else if (multi && toggle) |
|
|
{ |
|
|
{ |
|
|
var item = ElementAt(Items, index); |
|
|
UpdateSelectedItems(() => |
|
|
var i = SelectedItems.IndexOf(item); |
|
|
|
|
|
|
|
|
|
|
|
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedItems.Remove(item); |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
{ |
|
|
if (multi) |
|
|
if (!_selection.Contains(index)) |
|
|
{ |
|
|
{ |
|
|
SelectedItems.Add(item); |
|
|
_selection.Add(index); |
|
|
|
|
|
MarkItemSelected(index, true); |
|
|
|
|
|
SelectedItems.Add(ElementAt(Items, index)); |
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
SelectedIndex = index; |
|
|
_selection.Remove(index); |
|
|
|
|
|
MarkItemSelected(index, false); |
|
|
|
|
|
|
|
|
|
|
|
if (index == _selectedIndex) |
|
|
|
|
|
{ |
|
|
|
|
|
UpdateSelectedItem(_selection.First(), false); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
SelectedItems.Remove(ElementAt(Items, index)); |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
else if (toggle) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedIndex = (SelectedIndex == index) ? -1 : index; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (Presenter?.Panel != null) |
|
|
if (Presenter?.Panel != null) |
|
|
@ -596,17 +607,19 @@ namespace Avalonia.Controls.Primitives |
|
|
/// <param name="select">Whether the container should be selected or unselected.</param>
|
|
|
/// <param name="select">Whether the container should be selected or unselected.</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
|
|
|
/// <param name="rightButton">Whether the event is a right-click.</param>
|
|
|
protected void UpdateSelection( |
|
|
protected void UpdateSelection( |
|
|
IControl container, |
|
|
IControl container, |
|
|
bool select = true, |
|
|
bool select = true, |
|
|
bool rangeModifier = false, |
|
|
bool rangeModifier = false, |
|
|
bool toggleModifier = false) |
|
|
bool toggleModifier = false, |
|
|
|
|
|
bool rightButton = false) |
|
|
{ |
|
|
{ |
|
|
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; |
|
|
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; |
|
|
|
|
|
|
|
|
if (index != -1) |
|
|
if (index != -1) |
|
|
{ |
|
|
{ |
|
|
UpdateSelection(index, select, rangeModifier, toggleModifier); |
|
|
UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -618,6 +631,7 @@ namespace Avalonia.Controls.Primitives |
|
|
/// <param name="select">Whether the container should be selected or unselected.</param>
|
|
|
/// <param name="select">Whether the container should be selected or unselected.</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
|
|
|
|
|
|
/// <param name="rightButton">Whether the event is a right-click.</param>
|
|
|
/// <returns>
|
|
|
/// <returns>
|
|
|
/// True if the event originated from a container that belongs to the control; otherwise
|
|
|
/// True if the event originated from a container that belongs to the control; otherwise
|
|
|
/// false.
|
|
|
/// false.
|
|
|
@ -626,51 +640,20 @@ namespace Avalonia.Controls.Primitives |
|
|
IInteractive eventSource, |
|
|
IInteractive eventSource, |
|
|
bool select = true, |
|
|
bool select = true, |
|
|
bool rangeModifier = false, |
|
|
bool rangeModifier = false, |
|
|
bool toggleModifier = false) |
|
|
bool toggleModifier = false, |
|
|
|
|
|
bool rightButton = false) |
|
|
{ |
|
|
{ |
|
|
var container = GetContainerFromEventSource(eventSource); |
|
|
var container = GetContainerFromEventSource(eventSource); |
|
|
|
|
|
|
|
|
if (container != null) |
|
|
if (container != null) |
|
|
{ |
|
|
{ |
|
|
UpdateSelection(container, select, rangeModifier, toggleModifier); |
|
|
UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton); |
|
|
return true; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Makes a list of objects equal another.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="items">The items collection.</param>
|
|
|
|
|
|
/// <param name="desired">The desired items.</param>
|
|
|
|
|
|
internal static void SynchronizeItems(IList items, IEnumerable<object> desired) |
|
|
|
|
|
{ |
|
|
|
|
|
var index = 0; |
|
|
|
|
|
|
|
|
|
|
|
foreach (object item in desired) |
|
|
|
|
|
{ |
|
|
|
|
|
int itemIndex = items.IndexOf(item); |
|
|
|
|
|
|
|
|
|
|
|
if (itemIndex == -1) |
|
|
|
|
|
{ |
|
|
|
|
|
items.Insert(index, item); |
|
|
|
|
|
} |
|
|
|
|
|
else if(itemIndex != index) |
|
|
|
|
|
{ |
|
|
|
|
|
items.RemoveAt(itemIndex); |
|
|
|
|
|
items.Insert(index, item); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
++index; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
while (index < items.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
items.RemoveAt(items.Count - 1); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
/// Gets a range of items from an IEnumerable.
|
|
|
/// Gets a range of items from an IEnumerable.
|
|
|
/// </summary>
|
|
|
/// </summary>
|
|
|
@ -678,17 +661,19 @@ namespace Avalonia.Controls.Primitives |
|
|
/// <param name="first">The index of the first item.</param>
|
|
|
/// <param name="first">The index of the first item.</param>
|
|
|
/// <param name="last">The index of the last item.</param>
|
|
|
/// <param name="last">The index of the last item.</param>
|
|
|
/// <returns>The items.</returns>
|
|
|
/// <returns>The items.</returns>
|
|
|
private static IEnumerable<object> GetRange(IEnumerable items, int first, int last) |
|
|
private static List<object> GetRange(IEnumerable items, int first, int last) |
|
|
{ |
|
|
{ |
|
|
var list = (items as IList) ?? items.Cast<object>().ToList(); |
|
|
var list = (items as IList) ?? items.Cast<object>().ToList(); |
|
|
int step = first > last ? -1 : 1; |
|
|
var step = first > last ? -1 : 1; |
|
|
|
|
|
var result = new List<object>(); |
|
|
|
|
|
|
|
|
for (int i = first; i != last; i += step) |
|
|
for (int i = first; i != last; i += step) |
|
|
{ |
|
|
{ |
|
|
yield return list[i]; |
|
|
result.Add(list[i]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
yield return list[last]; |
|
|
result.Add(list[last]); |
|
|
|
|
|
return result; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
@ -724,19 +709,14 @@ namespace Avalonia.Controls.Primitives |
|
|
private void LostSelection() |
|
|
private void LostSelection() |
|
|
{ |
|
|
{ |
|
|
var items = Items?.Cast<object>(); |
|
|
var items = Items?.Cast<object>(); |
|
|
|
|
|
var index = -1; |
|
|
|
|
|
|
|
|
if (items != null && AlwaysSelected) |
|
|
if (items != null && AlwaysSelected) |
|
|
{ |
|
|
{ |
|
|
var index = Math.Min(SelectedIndex, items.Count() - 1); |
|
|
index = Math.Min(SelectedIndex, items.Count() - 1); |
|
|
|
|
|
|
|
|
if (index > -1) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedItem = items.ElementAt(index); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
SelectedIndex = -1; |
|
|
SelectedIndex = index; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
@ -793,7 +773,7 @@ namespace Avalonia.Controls.Primitives |
|
|
/// </summary>
|
|
|
/// </summary>
|
|
|
/// <param name="item">The item.</param>
|
|
|
/// <param name="item">The item.</param>
|
|
|
/// <param name="selected">Whether the item should be selected or deselected.</param>
|
|
|
/// <param name="selected">Whether the item should be selected or deselected.</param>
|
|
|
private void MarkItemSelected(object item, bool selected) |
|
|
private int MarkItemSelected(object item, bool selected) |
|
|
{ |
|
|
{ |
|
|
var index = IndexOf(Items, item); |
|
|
var index = IndexOf(Items, item); |
|
|
|
|
|
|
|
|
@ -801,6 +781,21 @@ namespace Avalonia.Controls.Primitives |
|
|
{ |
|
|
{ |
|
|
MarkItemSelected(index, selected); |
|
|
MarkItemSelected(index, selected); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return index; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void ResetSelectedItems() |
|
|
|
|
|
{ |
|
|
|
|
|
UpdateSelectedItems(() => |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedItems.Clear(); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var i in _selection) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedItems.Add(ElementAt(Items, i)); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
@ -810,95 +805,97 @@ namespace Avalonia.Controls.Primitives |
|
|
/// <param name="e">The event args.</param>
|
|
|
/// <param name="e">The event args.</param>
|
|
|
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|
|
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|
|
{ |
|
|
{ |
|
|
var generator = ItemContainerGenerator; |
|
|
if (_syncingSelectedItems) |
|
|
|
|
|
{ |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Add(IList newItems, IList addedItems = null) |
|
|
|
|
|
{ |
|
|
|
|
|
foreach (var item in newItems) |
|
|
|
|
|
{ |
|
|
|
|
|
var index = MarkItemSelected(item, true); |
|
|
|
|
|
|
|
|
|
|
|
if (index != -1 && _selection.Add(index) && addedItems != null) |
|
|
|
|
|
{ |
|
|
|
|
|
addedItems.Add(item); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void UpdateSelection() |
|
|
|
|
|
{ |
|
|
|
|
|
if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || |
|
|
|
|
|
(SelectedIndex == -1 && _selection.HasItems)) |
|
|
|
|
|
{ |
|
|
|
|
|
_selectedIndex = _selection.First(); |
|
|
|
|
|
_selectedItem = ElementAt(Items, _selectedIndex); |
|
|
|
|
|
RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); |
|
|
|
|
|
RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); |
|
|
|
|
|
|
|
|
|
|
|
if (AutoScrollToSelectedItem) |
|
|
|
|
|
{ |
|
|
|
|
|
ScrollIntoView(_selectedIndex); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
IList added = null; |
|
|
IList added = null; |
|
|
IList removed = null; |
|
|
IList removed = null; |
|
|
|
|
|
|
|
|
switch (e.Action) |
|
|
switch (e.Action) |
|
|
{ |
|
|
{ |
|
|
case NotifyCollectionChangedAction.Add: |
|
|
case NotifyCollectionChangedAction.Add: |
|
|
SelectedItemsAdded(e.NewItems.Cast<object>().ToList()); |
|
|
|
|
|
|
|
|
|
|
|
if (AutoScrollToSelectedItem) |
|
|
|
|
|
{ |
|
|
{ |
|
|
ScrollIntoView(e.NewItems[0]); |
|
|
Add(e.NewItems); |
|
|
|
|
|
UpdateSelection(); |
|
|
|
|
|
added = e.NewItems; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
added = e.NewItems; |
|
|
|
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Remove: |
|
|
case NotifyCollectionChangedAction.Remove: |
|
|
if (SelectedItems.Count == 0) |
|
|
if (SelectedItems.Count == 0) |
|
|
{ |
|
|
{ |
|
|
if (!_syncingSelectedItems) |
|
|
SelectedIndex = -1; |
|
|
{ |
|
|
|
|
|
SelectedIndex = -1; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
foreach (var item in e.OldItems) |
|
|
foreach (var item in e.OldItems) |
|
|
{ |
|
|
{ |
|
|
MarkItemSelected(item, false); |
|
|
var index = MarkItemSelected(item, false); |
|
|
|
|
|
_selection.Remove(index); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
removed = e.OldItems; |
|
|
removed = e.OldItems; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Replace: |
|
|
|
|
|
throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); |
|
|
|
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Move: |
|
|
|
|
|
throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); |
|
|
|
|
|
|
|
|
case NotifyCollectionChangedAction.Reset: |
|
|
case NotifyCollectionChangedAction.Reset: |
|
|
if (generator != null) |
|
|
|
|
|
{ |
|
|
{ |
|
|
removed = new List<object>(); |
|
|
removed = new List<object>(); |
|
|
|
|
|
added = new List<object>(); |
|
|
|
|
|
|
|
|
foreach (var item in generator.Containers) |
|
|
foreach (var index in _selection.ToList()) |
|
|
{ |
|
|
{ |
|
|
if (item?.ContainerControl != null) |
|
|
var item = ElementAt(Items, index); |
|
|
|
|
|
|
|
|
|
|
|
if (!SelectedItems.Contains(item)) |
|
|
{ |
|
|
{ |
|
|
if (MarkContainerSelected(item.ContainerControl, false)) |
|
|
MarkItemSelected(index, false); |
|
|
{ |
|
|
removed.Add(item); |
|
|
removed.Add(item.Item); |
|
|
_selection.Remove(index); |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (SelectedItems.Count > 0) |
|
|
Add(SelectedItems, added); |
|
|
{ |
|
|
UpdateSelection(); |
|
|
_selectedItem = null; |
|
|
|
|
|
SelectedItemsAdded(SelectedItems); |
|
|
|
|
|
added = SelectedItems; |
|
|
|
|
|
} |
|
|
|
|
|
else if (!_syncingSelectedItems) |
|
|
|
|
|
{ |
|
|
|
|
|
SelectedIndex = -1; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 oldIndex = SelectedIndex; |
|
|
|
|
|
var item = SelectedItems[0]; |
|
|
|
|
|
var index = IndexOf(Items, item); |
|
|
|
|
|
_selectedIndex = index; |
|
|
|
|
|
_selectedItem = item; |
|
|
|
|
|
RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue); |
|
|
|
|
|
RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
added = e.NewItems; |
|
|
|
|
|
removed = e.OldItems; |
|
|
|
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -912,34 +909,6 @@ namespace Avalonia.Controls.Primitives |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Called when items are added to the <see cref="SelectedItems"/> collection.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="items">The added items.</param>
|
|
|
|
|
|
private void SelectedItemsAdded(IList items) |
|
|
|
|
|
{ |
|
|
|
|
|
if (items.Count > 0) |
|
|
|
|
|
{ |
|
|
|
|
|
foreach (var item in items) |
|
|
|
|
|
{ |
|
|
|
|
|
MarkItemSelected(item, true); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (SelectedItem == null && !_syncingSelectedItems) |
|
|
|
|
|
{ |
|
|
|
|
|
var index = IndexOf(Items, items[0]); |
|
|
|
|
|
|
|
|
|
|
|
if (index != -1) |
|
|
|
|
|
{ |
|
|
|
|
|
_selectedItem = items[0]; |
|
|
|
|
|
_selectedIndex = index; |
|
|
|
|
|
RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue); |
|
|
|
|
|
RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// <summary>
|
|
|
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
|
|
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
|
|
/// </summary>
|
|
|
/// </summary>
|
|
|
@ -970,6 +939,112 @@ namespace Avalonia.Controls.Primitives |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Updates the selection due to a change to <see cref="SelectedIndex"/> or
|
|
|
|
|
|
/// <see cref="SelectedItem"/>.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="index">The new selected index.</param>
|
|
|
|
|
|
/// <param name="clear">Whether to clear existing selection.</param>
|
|
|
|
|
|
private void UpdateSelectedItem(int index, bool clear = true) |
|
|
|
|
|
{ |
|
|
|
|
|
var oldIndex = _selectedIndex; |
|
|
|
|
|
var oldItem = _selectedItem; |
|
|
|
|
|
|
|
|
|
|
|
if (index == -1 && AlwaysSelected) |
|
|
|
|
|
{ |
|
|
|
|
|
index = Math.Min(SelectedIndex, ItemCount - 1); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var item = ElementAt(Items, index); |
|
|
|
|
|
var added = -1; |
|
|
|
|
|
HashSet<int> removed = null; |
|
|
|
|
|
|
|
|
|
|
|
_selectedIndex = index; |
|
|
|
|
|
_selectedItem = item; |
|
|
|
|
|
|
|
|
|
|
|
if (oldIndex != index || _selection.HasMultiple) |
|
|
|
|
|
{ |
|
|
|
|
|
if (clear) |
|
|
|
|
|
{ |
|
|
|
|
|
removed = _selection.Clear(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (index != -1) |
|
|
|
|
|
{ |
|
|
|
|
|
if (_selection.Add(index)) |
|
|
|
|
|
{ |
|
|
|
|
|
added = index; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (removed?.Contains(index) == true) |
|
|
|
|
|
{ |
|
|
|
|
|
removed.Remove(index); |
|
|
|
|
|
added = -1; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (removed != null) |
|
|
|
|
|
{ |
|
|
|
|
|
foreach (var i in removed) |
|
|
|
|
|
{ |
|
|
|
|
|
MarkItemSelected(i, false); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
MarkItemSelected(index, true); |
|
|
|
|
|
|
|
|
|
|
|
RaisePropertyChanged( |
|
|
|
|
|
SelectedIndexProperty, |
|
|
|
|
|
oldIndex, |
|
|
|
|
|
index); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!Equals(item, oldItem)) |
|
|
|
|
|
{ |
|
|
|
|
|
RaisePropertyChanged( |
|
|
|
|
|
SelectedItemProperty, |
|
|
|
|
|
oldItem, |
|
|
|
|
|
item); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (removed != null && index != -1) |
|
|
|
|
|
{ |
|
|
|
|
|
removed.Remove(index); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (added != -1 || removed?.Count > 0) |
|
|
|
|
|
{ |
|
|
|
|
|
ResetSelectedItems(); |
|
|
|
|
|
|
|
|
|
|
|
var e = new SelectionChangedEventArgs( |
|
|
|
|
|
SelectionChangedEvent, |
|
|
|
|
|
added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(), |
|
|
|
|
|
removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>()); |
|
|
|
|
|
RaiseEvent(e); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void UpdateSelectedItems(Action action) |
|
|
|
|
|
{ |
|
|
|
|
|
try |
|
|
|
|
|
{ |
|
|
|
|
|
_syncingSelectedItems = true; |
|
|
|
|
|
action(); |
|
|
|
|
|
} |
|
|
|
|
|
catch (Exception ex) |
|
|
|
|
|
{ |
|
|
|
|
|
Logger.Error( |
|
|
|
|
|
LogArea.Property, |
|
|
|
|
|
this, |
|
|
|
|
|
"Error thrown updating SelectedItems: {Error}", |
|
|
|
|
|
ex); |
|
|
|
|
|
} |
|
|
|
|
|
finally |
|
|
|
|
|
{ |
|
|
|
|
|
_syncingSelectedItems = false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private void UpdateFinished() |
|
|
private void UpdateFinished() |
|
|
{ |
|
|
{ |
|
|
if (_updateSelectedIndex != int.MinValue) |
|
|
if (_updateSelectedIndex != int.MinValue) |
|
|
@ -981,5 +1056,104 @@ namespace Avalonia.Controls.Primitives |
|
|
SelectedItems = _updateSelectedItems; |
|
|
SelectedItems = _updateSelectedItems; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private class Selection : IEnumerable<int> |
|
|
|
|
|
{ |
|
|
|
|
|
private readonly List<int> _list = new List<int>(); |
|
|
|
|
|
private HashSet<int> _set = new HashSet<int>(); |
|
|
|
|
|
|
|
|
|
|
|
public bool HasItems => _set.Count > 0; |
|
|
|
|
|
public bool HasMultiple => _set.Count > 1; |
|
|
|
|
|
|
|
|
|
|
|
public bool Add(int index) |
|
|
|
|
|
{ |
|
|
|
|
|
if (index == -1) |
|
|
|
|
|
{ |
|
|
|
|
|
throw new ArgumentException("Invalid index", "index"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (_set.Add(index)) |
|
|
|
|
|
{ |
|
|
|
|
|
_list.Add(index); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public bool Remove(int index) |
|
|
|
|
|
{ |
|
|
|
|
|
if (_set.Remove(index)) |
|
|
|
|
|
{ |
|
|
|
|
|
_list.RemoveAll(x => x == index); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public HashSet<int> Clear() |
|
|
|
|
|
{ |
|
|
|
|
|
var result = _set; |
|
|
|
|
|
_list.Clear(); |
|
|
|
|
|
_set = new HashSet<int>(); |
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void ItemsInserted(int index, int count) |
|
|
|
|
|
{ |
|
|
|
|
|
_set = new HashSet<int>(); |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < _list.Count; ++i) |
|
|
|
|
|
{ |
|
|
|
|
|
var ix = _list[i]; |
|
|
|
|
|
|
|
|
|
|
|
if (ix >= index) |
|
|
|
|
|
{ |
|
|
|
|
|
var newIndex = ix + count; |
|
|
|
|
|
_list[i] = newIndex; |
|
|
|
|
|
_set.Add(newIndex); |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
_set.Add(ix); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void ItemsRemoved(int index, int count) |
|
|
|
|
|
{ |
|
|
|
|
|
var last = (index + count) - 1; |
|
|
|
|
|
|
|
|
|
|
|
_set = new HashSet<int>(); |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < _list.Count; ++i) |
|
|
|
|
|
{ |
|
|
|
|
|
var ix = _list[i]; |
|
|
|
|
|
|
|
|
|
|
|
if (ix >= index && ix <= last) |
|
|
|
|
|
{ |
|
|
|
|
|
_list.RemoveAt(i--); |
|
|
|
|
|
} |
|
|
|
|
|
else if (ix > last) |
|
|
|
|
|
{ |
|
|
|
|
|
var newIndex = ix - count; |
|
|
|
|
|
_list[i] = newIndex; |
|
|
|
|
|
_set.Add(newIndex); |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
_set.Add(ix); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public bool Contains(int index) => _set.Contains(index); |
|
|
|
|
|
|
|
|
|
|
|
public int First() => HasItems ? _list[0] : -1; |
|
|
|
|
|
|
|
|
|
|
|
public IEnumerator<int> GetEnumerator() => _set.GetEnumerator(); |
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|