File: Menu\MenuItemTracker.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
 
namespace Microsoft.Maui.Controls
{
	internal abstract class MenuItemTracker<TMenuItem>
		where TMenuItem : BaseMenuItem
	{
		int _flyoutDetails;
		WeakReference<Page> _target;
		List<WeakReference<Page>> _additionalTargets = new();
		public MenuItemTracker()
		{
		}
 
		protected abstract IList<TMenuItem> GetMenuItems(Page page);
 
		protected abstract IComparer<TMenuItem> CreateComparer();
 
		public IEnumerable<Page> AdditionalTargets
		{
			get
			{
				foreach (var target in _additionalTargets)
				{
					if (target.TryGetTarget(out var page))
						yield return page;
				}
			}
			set
			{
				_additionalTargets.Clear();
				if (value is not null)
				{
					foreach (var page in value)
					{
						_additionalTargets.Add(new(page));
					}
				}
			}
		}
 
		public bool HaveFlyoutPage
		{
			get { return _flyoutDetails > 0; }
		}
 
		public bool SeparateFlyoutPage { get; set; }
 
		public Page Target
		{
			get => _target is not null && _target.TryGetTarget(out var target) ? target : null;
			set
			{
				var target = Target;
				if (target == value)
					return;
 
				UntrackTarget(target);
				_target = value is null ? null : new(value);
 
				if (value != null)
					TrackTarget(value);
				EmitCollectionChanged();
			}
		}
 
		public IList<TMenuItem> ToolbarItems
		{
			get
			{
				if (Target == null)
					return Array.Empty<TMenuItem>();
 
				// I realize this is sorting on every single get but we don't have 
				// a mechanism in place currently to invalidate a stored version of this
 
				List<TMenuItem> returnValue = GetCurrentToolbarItems(Target);
 
				if (AdditionalTargets != null)
					foreach (var item in AdditionalTargets)
						foreach (var menuItem in GetMenuItems(item))
							if (!returnValue.Contains(menuItem))
								returnValue.Add(menuItem);
 
				returnValue.Sort(CreateComparer());
				return returnValue;
			}
		}
 
		public event EventHandler CollectionChanged;
 
		void EmitCollectionChanged()
			=> CollectionChanged?.Invoke(this, EventArgs.Empty);
 
		List<TMenuItem> GetCurrentToolbarItems(Page page)
		{
			var result = new List<TMenuItem>();
			result.AddRange(GetMenuItems(page));
 
			if (page is FlyoutPage)
			{
				var flyoutDetail = (FlyoutPage)page;
				if (SeparateFlyoutPage)
				{
					if (flyoutDetail.IsPresented)
					{
						if (flyoutDetail.Flyout != null)
							result.AddRange(GetCurrentToolbarItems(flyoutDetail.Flyout));
					}
					else
					{
						if (flyoutDetail.Detail != null)
							result.AddRange(GetCurrentToolbarItems(flyoutDetail.Detail));
					}
				}
				else
				{
					if (flyoutDetail.Flyout != null)
						result.AddRange(GetCurrentToolbarItems(flyoutDetail.Flyout));
					if (flyoutDetail.Detail != null)
						result.AddRange(GetCurrentToolbarItems(flyoutDetail.Detail));
				}
			}
			else if (page is Shell shell)
			{
				if (shell.GetCurrentShellPage() is Page shellPage && shellPage != shell)
					result.AddRange(GetCurrentToolbarItems(shellPage));
			}
			else if (page is IPageContainer<Page>)
			{
				var container = (IPageContainer<Page>)page;
				if (container.CurrentPage != null && container.CurrentPage != container)
					result.AddRange(GetCurrentToolbarItems(container.CurrentPage));
			}
 
			return result;
		}
 
		void OnChildAdded(object sender, ElementEventArgs eventArgs)
		{
			var page = eventArgs.Element as Page;
			if (page == null)
				return;
 
			RegisterChildPage(page);
		}
 
		void OnChildRemoved(object sender, ElementEventArgs eventArgs)
		{
			var page = eventArgs.Element as Page;
			if (page == null)
				return;
 
			UnregisterChildPage(page);
		}
 
		void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
		{
			EmitCollectionChanged();
		}
 
		void OnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
		{
			if (propertyChangedEventArgs.PropertyName == NavigationPage.CurrentPageProperty.PropertyName ||
				propertyChangedEventArgs.PropertyName == FlyoutPage.IsPresentedProperty.PropertyName ||
				propertyChangedEventArgs.PropertyName == "Detail" ||
				propertyChangedEventArgs.PropertyName == "Flyout")
			{
				EmitCollectionChanged();
			}
 
			PagePropertyChanged?.Invoke(sender, propertyChangedEventArgs);
		}
 
		public event EventHandler<PropertyChangedEventArgs> PagePropertyChanged;
		public event EventHandler<EventArgs> PageAppearing;
 
		void RegisterChildPage(Page page)
		{
			if (page is FlyoutPage)
				_flyoutDetails++;
 
			((ObservableCollection<TMenuItem>)GetMenuItems(page)).CollectionChanged += OnCollectionChanged;
			page.PropertyChanged += OnPropertyChanged;
			page.Appearing += OnPageAppearing;
		}
 
		void OnPageAppearing(object sender, EventArgs e)
		{
			PageAppearing?.Invoke(sender, e);
		}
 
		void TrackTarget(Page page)
		{
			if (page == null)
				return;
 
			if (page is FlyoutPage)
				_flyoutDetails++;
 
			((ObservableCollection<TMenuItem>)GetMenuItems(page)).CollectionChanged += OnCollectionChanged;
 
			if (page is Shell shell)
			{
				shell.Navigated += OnShellNavigated;
				shell.Navigating += OnShellNavigating;
 
				if (shell.GetCurrentShellPage() is Page currentShellPage)
					RegisterChildPage(currentShellPage);
 
				return;
			}
 
			page.Descendants<Page>().ForEach(RegisterChildPage);
 
			page.DescendantAdded += OnChildAdded;
			page.DescendantRemoved += OnChildRemoved;
			page.PropertyChanged += OnPropertyChanged;
			page.Appearing += OnPageAppearing;
		}
 
		void OnShellNavigating(object sender, ShellNavigatingEventArgs e)
		{
			if (((Shell)sender).GetCurrentShellPage() is Page page)
				UnregisterChildPage(page);
		}
 
		void OnShellNavigated(object sender, ShellNavigatedEventArgs e)
		{
			if (((Shell)sender).GetCurrentShellPage() is Page page)
			{
				UnregisterChildPage(page);
				RegisterChildPage(page);
			}
 
			EmitCollectionChanged();
		}
 
		void UnregisterChildPage(Page page)
		{
			if (page is FlyoutPage)
				_flyoutDetails--;
 
			((ObservableCollection<TMenuItem>)GetMenuItems(page)).CollectionChanged -= OnCollectionChanged;
			page.PropertyChanged -= OnPropertyChanged;
			page.Appearing -= OnPageAppearing;
		}
 
		void UntrackTarget(Page page)
		{
			if (page == null)
				return;
 
			if (page is FlyoutPage)
				_flyoutDetails--;
 
			if (page is Shell shell)
			{
				shell.Navigated -= OnShellNavigated;
				shell.Navigating -= OnShellNavigating;
				return;
			}
 
			((ObservableCollection<TMenuItem>)GetMenuItems(page)).CollectionChanged -= OnCollectionChanged;
			page.Descendants().OfType<Page>().ForEach(UnregisterChildPage);
 
			page.DescendantAdded -= OnChildAdded;
			page.DescendantRemoved -= OnChildRemoved;
			page.PropertyChanged -= OnPropertyChanged;
			page.Appearing -= OnPageAppearing;
		}
	}
}