mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-20 23:58:05 +08:00
549 lines
19 KiB
C#
549 lines
19 KiB
C#
using System.Collections;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using Avalonia.Collections;
|
|
|
|
namespace AtomUI.Controls.Data;
|
|
|
|
internal class ListBoxCollectionViewGroupRoot : ListBoxCollectionViewGroupInternal, INotifyCollectionChanged
|
|
{
|
|
/// <summary>
|
|
/// String constant used for the Root Name
|
|
/// </summary>
|
|
private const string RootName = "Root";
|
|
|
|
/// <summary>
|
|
/// Private accessor for empty object instance
|
|
/// </summary>
|
|
private static readonly object UseAsItemDirectly = new object();
|
|
|
|
/// <summary>
|
|
/// Private accessor for the top level GroupDescription
|
|
/// </summary>
|
|
private static ListBoxGroupDescription? _topLevelGroupDescription;
|
|
|
|
/// <summary>
|
|
/// Private accessor for an ObservableCollection containing group descriptions
|
|
/// </summary>
|
|
private readonly AvaloniaList<ListBoxGroupDescription> _groupBy = new ();
|
|
|
|
/// <summary>
|
|
/// Indicates whether the list of items (after applying the sort and filters, if any)
|
|
/// is already in the correct order for grouping.
|
|
/// </summary>
|
|
private bool _isDataInGroupOrder;
|
|
|
|
/// <summary>
|
|
/// Private accessor for the owning ICollectionView
|
|
/// </summary>
|
|
private readonly IListBoxCollectionView _view;
|
|
|
|
/// <summary>
|
|
/// Raise this event when the (grouped) view changes
|
|
/// </summary>
|
|
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
|
|
|
/// <summary>
|
|
/// Raise this event when the GroupDescriptions change
|
|
/// </summary>
|
|
internal event EventHandler? GroupDescriptionChanged;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ListBoxCollectionViewGroupRoot class.
|
|
/// </summary>
|
|
/// <param name="view">CollectionView that contains this grouping</param>
|
|
/// <param name="isDataInGroupOrder">True if items are already in correct order for grouping</param>
|
|
internal ListBoxCollectionViewGroupRoot(IListBoxCollectionView view, bool isDataInGroupOrder)
|
|
: base(RootName, null)
|
|
{
|
|
_view = view;
|
|
_isDataInGroupOrder = isDataInGroupOrder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the description of grouping, indexed by level.
|
|
/// </summary>
|
|
public virtual AvaloniaList<ListBoxGroupDescription> GroupDescriptions => _groupBy;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current IComparer being used
|
|
/// </summary>
|
|
internal IComparer? ActiveComparer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the culture to use during sorting.
|
|
/// </summary>
|
|
internal CultureInfo Culture
|
|
{
|
|
get
|
|
{
|
|
Debug.Assert(_view != null, "this._view should have been set from the constructor");
|
|
return _view.Culture;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the data is in group order
|
|
/// </summary>
|
|
internal bool IsDataInGroupOrder
|
|
{
|
|
get => _isDataInGroupOrder;
|
|
set => _isDataInGroupOrder = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the index of the specified item
|
|
/// </summary>
|
|
/// <param name="item">Item we are looking for</param>
|
|
/// <param name="seed">Seed of the item we are looking for</param>
|
|
/// <param name="comparer">Comparer used to find the item</param>
|
|
/// <param name="low">Low range of item index</param>
|
|
/// <param name="high">High range of item index</param>
|
|
/// <returns>Index of the specified item</returns>
|
|
protected override int FindIndex(object item, object seed, IComparer? comparer, int low, int high)
|
|
{
|
|
// root group needs to adjust the bounds of the search to exclude the new item (if any)
|
|
// if (_view is IDataGridEditableCollectionView iecv && iecv.IsAddingNew)
|
|
// {
|
|
// --high;
|
|
// }
|
|
|
|
return base.FindIndex(item, seed, comparer, low, high);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the group descriptions
|
|
/// </summary>
|
|
internal void Initialize()
|
|
{
|
|
if (_topLevelGroupDescription == null)
|
|
{
|
|
_topLevelGroupDescription = new TopLevelGroupDescription();
|
|
}
|
|
|
|
InitializeGroup(this, 0, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts specified item into the collection
|
|
/// </summary>
|
|
/// <param name="index">Index to insert into</param>
|
|
/// <param name="item">Item to insert</param>
|
|
/// <param name="loading">Whether we are currently loading</param>
|
|
internal void InsertSpecialItem(int index, object item, bool loading)
|
|
{
|
|
ChangeCounts(item, +1);
|
|
ProtectedItems.Insert(index, item);
|
|
|
|
if (!loading)
|
|
{
|
|
int globalIndex = LeafIndexFromItem(item, index);
|
|
HandleCollectionChanged(
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, globalIndex));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify listeners that this View has changed
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// CollectionViews (and sub-classes) should take their filter/sort/grouping
|
|
/// into account before calling this method to forward CollectionChanged events.
|
|
/// </remarks>
|
|
/// <param name="args">The NotifyCollectionChangedEventArgs to be passed to the EventHandler</param>
|
|
public void HandleCollectionChanged(NotifyCollectionChangedEventArgs args)
|
|
{
|
|
Debug.Assert(args != null, "Arguments passed in should not be null");
|
|
CollectionChanged?.Invoke(this, args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify host that a group description has changed somewhere in the tree
|
|
/// </summary>
|
|
protected override void OnGroupByChanged()
|
|
{
|
|
GroupDescriptionChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove specified item from subgroups
|
|
/// </summary>
|
|
/// <param name="item">Item to remove</param>
|
|
/// <returns>Whether the operation was successful</returns>
|
|
internal bool RemoveFromSubgroups(object item)
|
|
{
|
|
return RemoveFromSubgroups(item, this, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove specified item from subgroups using an exhaustive search
|
|
/// </summary>
|
|
/// <param name="item">Item to remove</param>
|
|
internal void RemoveItemFromSubgroupsByExhaustiveSearch(object item)
|
|
{
|
|
RemoveItemFromSubgroupsByExhaustiveSearch(this, item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes specified item into the collection
|
|
/// </summary>
|
|
/// <param name="index">Index to remove from</param>
|
|
/// <param name="item">Item to remove</param>
|
|
/// <param name="loading">Whether we are currently loading</param>
|
|
internal void RemoveSpecialItem(int index, object item, bool loading)
|
|
{
|
|
Debug.Assert(Object.Equals(item, ProtectedItems[index]), "RemoveSpecialItem finds inconsistent data");
|
|
int globalIndex = -1;
|
|
|
|
if (!loading)
|
|
{
|
|
globalIndex = LeafIndexFromItem(item, index);
|
|
}
|
|
|
|
ChangeCounts(item, -1);
|
|
ProtectedItems.RemoveAt(index);
|
|
|
|
if (!loading)
|
|
{
|
|
HandleCollectionChanged(
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, globalIndex));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds specified item to subgroups
|
|
/// </summary>
|
|
/// <param name="item">Item to add</param>
|
|
/// <param name="loading">Whether we are currently loading</param>
|
|
internal void AddToSubgroups(object item, bool loading)
|
|
{
|
|
AddToSubgroups(item, this, 0, loading);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an item to the subgroup with the given name
|
|
/// </summary>
|
|
/// <param name="item">Item to add</param>
|
|
/// <param name="group">Group to add item to</param>
|
|
/// <param name="level">The level of grouping.</param>
|
|
/// <param name="key">Name of subgroup to add to</param>
|
|
/// <param name="loading">Whether we are currently loading</param>
|
|
private void AddToSubgroup(object item, ListBoxCollectionViewGroupInternal group, int level, object key,
|
|
bool loading)
|
|
{
|
|
ListBoxCollectionViewGroupInternal? subgroup;
|
|
int index = (_isDataInGroupOrder) ? group.LastIndex : 0;
|
|
|
|
// find the desired subgroup
|
|
for (int n = group.Items.Count; index < n; ++index)
|
|
{
|
|
subgroup = group.Items[index] as ListBoxCollectionViewGroupInternal;
|
|
if (subgroup == null)
|
|
{
|
|
continue; // skip children that are not groups
|
|
}
|
|
|
|
if (group.GroupBy != null)
|
|
{
|
|
group.LastIndex = index;
|
|
AddToSubgroups(item, subgroup, level + 1, loading);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// the item didn't match any subgroups. Create a new subgroup and add the item.
|
|
subgroup = new ListBoxCollectionViewGroupInternal(key, group);
|
|
InitializeGroup(subgroup, level + 1, item);
|
|
|
|
if (loading)
|
|
{
|
|
group.Add(subgroup);
|
|
group.LastIndex = index;
|
|
}
|
|
else
|
|
{
|
|
// using insert will find the correct sort index to
|
|
// place the subgroup, and will default to the last
|
|
// position if no ActiveComparer is specified
|
|
if (ActiveComparer != null)
|
|
{
|
|
group.Insert(subgroup, item, ActiveComparer);
|
|
}
|
|
}
|
|
|
|
AddToSubgroups(item, subgroup, level + 1, loading);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an item to the desired subgroup(s) of the given group
|
|
/// </summary>
|
|
/// <param name="item">Item to add</param>
|
|
/// <param name="group">Group to add item to</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <param name="loading">Whether we are currently loading</param>
|
|
private void AddToSubgroups(object item, ListBoxCollectionViewGroupInternal group, int level, bool loading)
|
|
{
|
|
object? key = GetGroupKey(item, group.GroupBy, level);
|
|
|
|
if (key == UseAsItemDirectly)
|
|
{
|
|
// the item belongs to the group itself (not to any subgroups)
|
|
if (loading)
|
|
{
|
|
group.Add(item);
|
|
}
|
|
else
|
|
{
|
|
if (ActiveComparer != null)
|
|
{
|
|
int localIndex = group.Insert(item, item, ActiveComparer);
|
|
int index = group.LeafIndexFromItem(item, localIndex);
|
|
HandleCollectionChanged(
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
|
|
}
|
|
}
|
|
}
|
|
else if (key is ICollection keyList)
|
|
{
|
|
// the item belongs to multiple subgroups
|
|
foreach (object o in keyList)
|
|
{
|
|
AddToSubgroup(item, group, level, o, loading);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (key != null)
|
|
{
|
|
// the item belongs to one subgroup
|
|
AddToSubgroup(item, group, level, key, loading);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual Func<ListBoxCollectionViewGroup, int, ListBoxGroupDescription>? GroupBySelector { get; set; }
|
|
|
|
/// <summary>
|
|
/// Returns the description of how to divide the given group into subgroups
|
|
/// </summary>
|
|
/// <param name="group">CollectionViewGroup to get group description from</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <returns>GroupDescription of how to divide the given group</returns>
|
|
private ListBoxGroupDescription? GetGroupDescription(ListBoxCollectionViewGroup group, int level)
|
|
{
|
|
ListBoxGroupDescription? result = null;
|
|
ListBoxCollectionViewGroup? targetGroup = group;
|
|
if (targetGroup == this)
|
|
{
|
|
targetGroup = null;
|
|
}
|
|
|
|
if (result == null && GroupBySelector != null && targetGroup != null)
|
|
{
|
|
result = GroupBySelector?.Invoke(targetGroup, level);
|
|
}
|
|
|
|
if (result == null && level < GroupDescriptions.Count)
|
|
{
|
|
result = GroupDescriptions[level];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the group name(s) for the given item
|
|
/// </summary>
|
|
/// <param name="item">Item to get group name for</param>
|
|
/// <param name="groupDescription">GroupDescription for the group</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <returns>Group names for the specified item</returns>
|
|
private object? GetGroupKey(object item, ListBoxGroupDescription? groupDescription, int level)
|
|
{
|
|
if (groupDescription != null)
|
|
{
|
|
return groupDescription.GroupKeyFromItem(item, level, Culture);
|
|
}
|
|
return UseAsItemDirectly;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize the given group
|
|
/// </summary>
|
|
/// <param name="group">Group to initialize</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <param name="seedItem">The seed item to compare with to see where to insert</param>
|
|
private void InitializeGroup(ListBoxCollectionViewGroupInternal group, int level, object? seedItem)
|
|
{
|
|
// set the group description for dividing the group into subgroups
|
|
ListBoxGroupDescription? groupDescription = GetGroupDescription(group, level);
|
|
group.GroupBy = groupDescription;
|
|
|
|
// create subgroups for each of the explicit names
|
|
var keys = groupDescription?.GroupKeys;
|
|
if (keys != null)
|
|
{
|
|
for (int k = 0, n = keys.Count; k < n; ++k)
|
|
{
|
|
ListBoxCollectionViewGroupInternal subgroup = new ListBoxCollectionViewGroupInternal(keys[k], group);
|
|
InitializeGroup(subgroup, level + 1, seedItem);
|
|
group.Add(subgroup);
|
|
}
|
|
}
|
|
|
|
group.LastIndex = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove an item from the direct children of a group.
|
|
/// </summary>
|
|
/// <param name="group">Group to remove item from</param>
|
|
/// <param name="item">Item to remove</param>
|
|
/// <returns>True if item could not be removed</returns>
|
|
private bool RemoveFromGroupDirectly(ListBoxCollectionViewGroupInternal group, object item)
|
|
{
|
|
int leafIndex = group.Remove(item, true);
|
|
if (leafIndex >= 0)
|
|
{
|
|
HandleCollectionChanged(
|
|
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, leafIndex));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove an item from the subgroup with the given name.
|
|
/// </summary>
|
|
/// <param name="item">Item to remove</param>
|
|
/// <param name="group">Group to remove item from</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <param name="key">Name of item to remove</param>
|
|
/// <returns>Return true if the item was not in one of the subgroups it was supposed to be.</returns>
|
|
private bool RemoveFromSubgroup(object item, ListBoxCollectionViewGroupInternal group, int level, object key)
|
|
{
|
|
bool itemIsMissing = false;
|
|
ListBoxCollectionViewGroupInternal? subGroup;
|
|
|
|
// find the desired subgroup
|
|
for (int index = 0, n = group.Items.Count; index < n; ++index)
|
|
{
|
|
subGroup = group.Items[index] as ListBoxCollectionViewGroupInternal;
|
|
if (subGroup == null)
|
|
{
|
|
continue; // skip children that are not groups
|
|
}
|
|
|
|
if (group.GroupBy != null && group.GroupBy.KeysMatch(subGroup.Key, key))
|
|
{
|
|
if (RemoveFromSubgroups(item, subGroup, level + 1))
|
|
{
|
|
itemIsMissing = true;
|
|
}
|
|
|
|
return itemIsMissing;
|
|
}
|
|
}
|
|
|
|
// the item didn't match any subgroups. It should have.
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove an item from the desired subgroup(s) of the given group.
|
|
/// </summary>
|
|
/// <param name="item">Item to remove</param>
|
|
/// <param name="group">Group to remove item from</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <returns>Return true if the item was not in one of the subgroups it was supposed to be.</returns>
|
|
private bool RemoveFromSubgroups(object item, ListBoxCollectionViewGroupInternal group, int level)
|
|
{
|
|
bool itemIsMissing = false;
|
|
object? key = GetGroupKey(item, group.GroupBy, level);
|
|
|
|
if (key == UseAsItemDirectly)
|
|
{
|
|
// the item belongs to the group itself (not to any subgroups)
|
|
itemIsMissing = RemoveFromGroupDirectly(group, item);
|
|
}
|
|
else if (key is ICollection keyList)
|
|
{
|
|
// the item belongs to multiple subgroups
|
|
foreach (object o in keyList)
|
|
{
|
|
if (RemoveFromSubgroup(item, group, level, o))
|
|
{
|
|
itemIsMissing = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (key != null)
|
|
{
|
|
// the item belongs to one subgroup
|
|
if (RemoveFromSubgroup(item, group, level, key))
|
|
{
|
|
itemIsMissing = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return itemIsMissing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The item did not appear in one or more of the subgroups it
|
|
/// was supposed to. This can happen if the item's properties
|
|
/// change so that the group names we used to insert it are
|
|
/// different from the names used to remove it. If this happens,
|
|
/// remove the item the hard way.
|
|
/// </summary>
|
|
/// <param name="group">Group to remove item from</param>
|
|
/// <param name="item">Item to remove</param>
|
|
private void RemoveItemFromSubgroupsByExhaustiveSearch(ListBoxCollectionViewGroupInternal group, object item)
|
|
{
|
|
// try to remove the item from the direct children
|
|
// this function only returns true if it failed to remove from group directly
|
|
// in which case we will step through and search exhaustively
|
|
if (RemoveFromGroupDirectly(group, item))
|
|
{
|
|
// if that didn't work, recurse into each subgroup
|
|
// (loop runs backwards in case an entire group is deleted)
|
|
for (int k = group.Items.Count - 1; k >= 0; --k)
|
|
{
|
|
if (group.Items[k] is ListBoxCollectionViewGroupInternal subgroup)
|
|
{
|
|
RemoveItemFromSubgroupsByExhaustiveSearch(subgroup, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// TopLevelGroupDescription class
|
|
/// </summary>
|
|
private class TopLevelGroupDescription : ListBoxGroupDescription
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the TopLevelGroupDescription class.
|
|
/// </summary>
|
|
public TopLevelGroupDescription()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// We have to implement this abstract method, but it should never be called
|
|
/// </summary>
|
|
/// <param name="item">Item to get group name from</param>
|
|
/// <param name="level">The level of grouping</param>
|
|
/// <param name="culture">Culture used for sorting</param>
|
|
/// <returns>We do not return a value here</returns>
|
|
public override object? GroupKeyFromItem(object item, int level, CultureInfo? culture)
|
|
{
|
|
Debug.Assert(true, "We have to implement this abstract method, but it should never be called");
|
|
return null;
|
|
}
|
|
}
|
|
} |