File: Shell\ShellFlyoutItemsManager.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
 
namespace Microsoft.Maui.Controls
{
	internal class ShellFlyoutItemsManager
	{
		readonly Shell _shell;
		List<List<Element>> _lastGeneratedFlyoutItems;
		public event EventHandler FlyoutItemsChanged;
		IShellController ShellController => _shell;
		ReadOnlyObservableCollectionWithSource<IReadOnlyList<Element>> _flyoutItemsReadonly;
 
		public IEnumerable FlyoutItems => _flyoutItemsReadonly;
		public ShellFlyoutItemsManager(Shell shell)
		{
			_shell = shell;
			_flyoutItemsReadonly = new ReadOnlyObservableCollectionWithSource<IReadOnlyList<Element>>();
		}
 
 
		void SyncFlyoutItemsToReadOnlyCollection()
		{
			var flyoutItems = _flyoutItemsReadonly.List;
 
			// sync the number of groups
			for (var i = flyoutItems.Count; i < _lastGeneratedFlyoutItems.Count; i++)
				flyoutItems.Add(new ReadOnlyObservableCollectionWithSource<Element>());
 
			for (var i = _lastGeneratedFlyoutItems.Count; i < flyoutItems.Count; i++)
				flyoutItems.RemoveAt(i);
 
			for (var i = 0; i < _lastGeneratedFlyoutItems.Count; i++)
			{
				var source = _lastGeneratedFlyoutItems[i];
				var dest = ((ReadOnlyObservableCollectionWithSource<Element>)flyoutItems[i]).List;
 
				for (var j = dest.Count - 1; j >= 0; j--)
				{
					var item = dest[j];
					if (!source.Contains(item))
						dest.RemoveAt(j);
				}
 
				for (var j = 0; j < source.Count; j++)
				{
					var item = source[j];
					var destIndex = dest.IndexOf(item);
 
					if (destIndex == -1)
					{
						if (j < dest.Count)
							dest.Insert(j, item);
						else
							dest.Add(item);
					}
					else
					{
						if (j < dest.Count)
						{
							if (destIndex != j)
								dest.Move(destIndex, j);
						}
						else
							dest.Add(item);
					}
				}
			}
		}
 
		public void CheckIfFlyoutItemsChanged()
		{
			if (UpdateFlyoutGroupings())
			{
				FlyoutItemsChanged?.Invoke(this, EventArgs.Empty);
				SyncFlyoutItemsToReadOnlyCollection();
			}
		}
 
		public List<List<Element>> GenerateFlyoutGrouping()
		{
			if (_lastGeneratedFlyoutItems == null)
				UpdateFlyoutGroupings();
 
			return _lastGeneratedFlyoutItems;
		}
 
		bool UpdateFlyoutGroupings()
		{
			// The idea here is to create grouping such that the Flyout would
			// render correctly if it renderered each item in the groups in order
			// but put a line between the groups. This is needed because our grouping can
			// actually go 3 layers deep.
 
			// Ideally this lets us control where lines are drawn in the core code
			// just by changing how we generate these groupings
 
			var result = new List<List<Element>>();
 
			var currentGroup = new List<Element>();
 
			foreach (var shellItem in ShellController.GetItems())
			{
				if (!ShowInFlyoutMenu(shellItem))
					continue;
 
				if (Routing.IsImplicit(shellItem) || shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
				{
					if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
						IncrementGroup();
 
					foreach (var shellSection in (shellItem as IShellItemController).GetItems())
					{
						if (!ShowInFlyoutMenu(shellSection))
							continue;
 
						var shellContents = ((IShellSectionController)shellSection).GetItems();
						if (Routing.IsImplicit(shellSection) || shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
						{
							foreach (var shellContent in shellContents)
							{
								if (!ShowInFlyoutMenu(shellContent))
									continue;
 
								currentGroup.Add(shellContent);
								if (shellContent == shellSection.CurrentItem)
								{
									AddMenuItems(shellContent.MenuItems);
								}
							}
 
							if (shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
								IncrementGroup();
						}
						else
						{
							if (!(shellSection.Parent is TabBar))
							{
								if (Routing.IsImplicit(shellSection) && shellContents.Count == 1)
								{
									if (!ShowInFlyoutMenu(shellContents[0]))
										continue;
 
									currentGroup.Add(shellContents[0]);
								}
								else
									currentGroup.Add(shellSection);
							}
 
							// If we have only a single child we will also show the items menu items
							if (shellContents.Count == 1 && shellSection == shellItem.CurrentItem && shellSection.CurrentItem.MenuItems.Count > 0)
							{
								AddMenuItems(shellSection.CurrentItem.MenuItems);
							}
						}
					}
 
					if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
						IncrementGroup();
				}
				else
				{
					if (!(shellItem is TabBar))
						currentGroup.Add(shellItem);
				}
			}
 
			IncrementGroup();
 
			// If the flyout groupings haven't changed just return
			// the same instance so the caller knows it hasn't changed
			// at a later point this will all get converted to an observable collection
			if (_lastGeneratedFlyoutItems?.Count == result.Count)
			{
				bool hasChanged = false;
				for (var i = 0; i < result.Count && !hasChanged; i++)
				{
					var topLevelNew = result[i];
					var topLevelPrevious = _lastGeneratedFlyoutItems[i];
 
					if (topLevelNew.Count != topLevelPrevious.Count)
					{
						hasChanged = true;
						break;
					}
 
					for (var j = 0; j > topLevelNew.Count; j++)
					{
						if (topLevelNew[j] != topLevelPrevious[j])
						{
							hasChanged = true;
							break;
						}
					}
 
				}
 
				if (!hasChanged)
					return false;
			}
 
			_lastGeneratedFlyoutItems = result;
			return true;
 
			bool ShowInFlyoutMenu(BindableObject bo)
			{
				if (bo is MenuShellItem msi)
					return Shell.GetFlyoutItemIsVisible(msi.MenuItem);
 
				return Shell.GetFlyoutItemIsVisible(bo);
			}
 
			void AddMenuItems(MenuItemCollection menuItems)
			{
				foreach (var item in menuItems)
				{
					if (ShowInFlyoutMenu(item))
						currentGroup.Add(item);
				}
			}
 
			void IncrementGroup()
			{
				if (currentGroup.Count > 0)
				{
					result.Add(currentGroup);
					currentGroup = new List<Element>();
				}
			}
		}
 
		class ReadOnlyObservableCollectionWithSource<T> : ReadOnlyObservableCollection<T>
		{
			public ReadOnlyObservableCollectionWithSource() : this(new ObservableCollection<T>())
			{
			}
 
			public ReadOnlyObservableCollectionWithSource(ObservableCollection<T> list) : base(list)
			{
				List = list;
			}
 
			public ObservableCollection<T> List { get; }
		}
	}
}