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; } } }