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
{
///
/// String constant used for the Root Name
///
private const string RootName = "Root";
///
/// Private accessor for empty object instance
///
private static readonly object UseAsItemDirectly = new object();
///
/// Private accessor for the top level GroupDescription
///
private static ListBoxGroupDescription? _topLevelGroupDescription;
///
/// Private accessor for an ObservableCollection containing group descriptions
///
private readonly AvaloniaList _groupBy = new ();
///
/// Indicates whether the list of items (after applying the sort and filters, if any)
/// is already in the correct order for grouping.
///
private bool _isDataInGroupOrder;
///
/// Private accessor for the owning ICollectionView
///
private readonly IListBoxCollectionView _view;
///
/// Raise this event when the (grouped) view changes
///
public event NotifyCollectionChangedEventHandler? CollectionChanged;
///
/// Raise this event when the GroupDescriptions change
///
internal event EventHandler? GroupDescriptionChanged;
///
/// Initializes a new instance of the ListBoxCollectionViewGroupRoot class.
///
/// CollectionView that contains this grouping
/// True if items are already in correct order for grouping
internal ListBoxCollectionViewGroupRoot(IListBoxCollectionView view, bool isDataInGroupOrder)
: base(RootName, null)
{
_view = view;
_isDataInGroupOrder = isDataInGroupOrder;
}
///
/// Gets the description of grouping, indexed by level.
///
public virtual AvaloniaList GroupDescriptions => _groupBy;
///
/// Gets or sets the current IComparer being used
///
internal IComparer? ActiveComparer { get; set; }
///
/// Gets the culture to use during sorting.
///
internal CultureInfo Culture
{
get
{
Debug.Assert(_view != null, "this._view should have been set from the constructor");
return _view.Culture;
}
}
///
/// Gets or sets a value indicating whether the data is in group order
///
internal bool IsDataInGroupOrder
{
get => _isDataInGroupOrder;
set => _isDataInGroupOrder = value;
}
///
/// Finds the index of the specified item
///
/// Item we are looking for
/// Seed of the item we are looking for
/// Comparer used to find the item
/// Low range of item index
/// High range of item index
/// Index of the specified item
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);
}
///
/// Initializes the group descriptions
///
internal void Initialize()
{
if (_topLevelGroupDescription == null)
{
_topLevelGroupDescription = new TopLevelGroupDescription();
}
InitializeGroup(this, 0, null);
}
///
/// Inserts specified item into the collection
///
/// Index to insert into
/// Item to insert
/// Whether we are currently loading
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));
}
}
///
/// Notify listeners that this View has changed
///
///
/// CollectionViews (and sub-classes) should take their filter/sort/grouping
/// into account before calling this method to forward CollectionChanged events.
///
/// The NotifyCollectionChangedEventArgs to be passed to the EventHandler
public void HandleCollectionChanged(NotifyCollectionChangedEventArgs args)
{
Debug.Assert(args != null, "Arguments passed in should not be null");
CollectionChanged?.Invoke(this, args);
}
///
/// Notify host that a group description has changed somewhere in the tree
///
protected override void OnGroupByChanged()
{
GroupDescriptionChanged?.Invoke(this, EventArgs.Empty);
}
///
/// Remove specified item from subgroups
///
/// Item to remove
/// Whether the operation was successful
internal bool RemoveFromSubgroups(object item)
{
return RemoveFromSubgroups(item, this, 0);
}
///
/// Remove specified item from subgroups using an exhaustive search
///
/// Item to remove
internal void RemoveItemFromSubgroupsByExhaustiveSearch(object item)
{
RemoveItemFromSubgroupsByExhaustiveSearch(this, item);
}
///
/// Removes specified item into the collection
///
/// Index to remove from
/// Item to remove
/// Whether we are currently loading
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));
}
}
///
/// Adds specified item to subgroups
///
/// Item to add
/// Whether we are currently loading
internal void AddToSubgroups(object item, bool loading)
{
AddToSubgroups(item, this, 0, loading);
}
///
/// Add an item to the subgroup with the given name
///
/// Item to add
/// Group to add item to
/// The level of grouping.
/// Name of subgroup to add to
/// Whether we are currently loading
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);
}
///
/// Add an item to the desired subgroup(s) of the given group
///
/// Item to add
/// Group to add item to
/// The level of grouping
/// Whether we are currently loading
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? GroupBySelector { get; set; }
///
/// Returns the description of how to divide the given group into subgroups
///
/// CollectionViewGroup to get group description from
/// The level of grouping
/// GroupDescription of how to divide the given group
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;
}
///
/// Get the group name(s) for the given item
///
/// Item to get group name for
/// GroupDescription for the group
/// The level of grouping
/// Group names for the specified item
private object? GetGroupKey(object item, ListBoxGroupDescription? groupDescription, int level)
{
if (groupDescription != null)
{
return groupDescription.GroupKeyFromItem(item, level, Culture);
}
return UseAsItemDirectly;
}
///
/// Initialize the given group
///
/// Group to initialize
/// The level of grouping
/// The seed item to compare with to see where to insert
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;
}
///
/// Remove an item from the direct children of a group.
///
/// Group to remove item from
/// Item to remove
/// True if item could not be removed
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;
}
///
/// Remove an item from the subgroup with the given name.
///
/// Item to remove
/// Group to remove item from
/// The level of grouping
/// Name of item to remove
/// Return true if the item was not in one of the subgroups it was supposed to be.
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;
}
///
/// Remove an item from the desired subgroup(s) of the given group.
///
/// Item to remove
/// Group to remove item from
/// The level of grouping
/// Return true if the item was not in one of the subgroups it was supposed to be.
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;
}
///
/// 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.
///
/// Group to remove item from
/// Item to remove
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);
}
}
}
}
///
/// TopLevelGroupDescription class
///
private class TopLevelGroupDescription : ListBoxGroupDescription
{
///
/// Initializes a new instance of the TopLevelGroupDescription class.
///
public TopLevelGroupDescription()
{
}
///
/// We have to implement this abstract method, but it should never be called
///
/// Item to get group name from
/// The level of grouping
/// Culture used for sorting
/// We do not return a value here
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;
}
}
}