File: Shell\ShellItem.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;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="Type[@FullName='Microsoft.Maui.Controls.FlyoutItem']/Docs/*" />
	[EditorBrowsable(EditorBrowsableState.Always)]
	public class FlyoutItem : ShellItem
	{
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='LabelStyle']/Docs/*" />
		public const string LabelStyle = "FlyoutItemLabelStyle";
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='ImageStyle']/Docs/*" />
		public const string ImageStyle = "FlyoutItemImageStyle";
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='LayoutStyle']/Docs/*" />
		public const string LayoutStyle = "FlyoutItemLayoutStyle";
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public FlyoutItem()
		{
 
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='IsVisibleProperty']/Docs/*" />
		public static readonly new BindableProperty IsVisibleProperty = BaseShellItem.IsVisibleProperty;
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='GetIsVisible']/Docs/*" />
		public static bool GetIsVisible(BindableObject obj) => (bool)obj.GetValue(IsVisibleProperty);
		/// <include file="../../../docs/Microsoft.Maui.Controls/FlyoutItem.xml" path="//Member[@MemberName='SetIsVisible']/Docs/*" />
		public static void SetIsVisible(BindableObject obj, bool isVisible) => obj.SetValue(IsVisibleProperty, isVisible);
	}
 
	/// <include file="../../../docs/Microsoft.Maui.Controls/TabBar.xml" path="Type[@FullName='Microsoft.Maui.Controls.TabBar']/Docs/*" />
	[EditorBrowsable(EditorBrowsableState.Always)]
	public class TabBar : ShellItem
	{
		/// <include file="../../../docs/Microsoft.Maui.Controls/TabBar.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public TabBar()
		{
		}
	}
 
 
	/// <include file="../../../docs/Microsoft.Maui.Controls/ShellItem.xml" path="Type[@FullName='Microsoft.Maui.Controls.ShellItem']/Docs/*" />
	[ContentProperty(nameof(Items))]
	[EditorBrowsable(EditorBrowsableState.Never)]
	[TypeConverter(typeof(ShellItemConverter))]
	public class ShellItem : ShellGroupItem, IShellItemController, IElementConfiguration<ShellItem>, IPropertyPropagationController, IVisualTreeElement
	{
		#region PropertyKeys
 
		static readonly BindablePropertyKey ItemsPropertyKey = BindableProperty.CreateReadOnly(nameof(Items), typeof(ShellSectionCollection), typeof(ShellItem), null,
				defaultValueCreator: bo => new ShellSectionCollection { Inner = new ElementCollection<ShellSection>(((ShellItem)bo).DeclaredChildren) });
 
		#endregion PropertyKeys
 
		#region IShellItemController
 
		IShellItemController ShellItemController => this;
 
		bool IShellItemController.ProposeSection(ShellSection shellSection, bool setValue)
		{
			var controller = (IShellController)Parent;
 
			if (controller == null)
				return false;
 
			bool accept = controller.ProposeNavigation(ShellNavigationSource.ShellSectionChanged,
				this,
				shellSection,
				shellSection?.CurrentItem,
				shellSection?.Stack,
				true
			);
 
			if (accept && setValue)
				SetValueFromRenderer(CurrentItemProperty, shellSection);
 
			return accept;
		}
 
		// we want the list returned from here to remain point in time accurate
		ReadOnlyCollection<ShellSection> IShellItemController.GetItems() => ((ShellSectionCollection)Items).VisibleItemsReadOnly;
 
		event NotifyCollectionChangedEventHandler IShellItemController.ItemsCollectionChanged
		{
			add { ((ShellSectionCollection)Items).VisibleItemsChanged += value; }
			remove { ((ShellSectionCollection)Items).VisibleItemsChanged -= value; }
		}
 
		bool IShellItemController.ShowTabs
		{
			get
			{
				Shell shell = Parent as Shell;
				if (shell == null)
					return true;
 
				var displayedPage = shell.GetCurrentShellPage();
 
				bool defaultShowTabs = true;
 
#if WINDOWS
				// Windows supports nested tabs so we want the tabs to display
				// if the current shell section has multiple contents
				if (ShellItemController.GetItems().Count > 1 ||
					(CurrentItem as IShellSectionController)?.GetItems()?.Count > 1)
				{
					defaultShowTabs = true;
				}
				else
				{
					defaultShowTabs = false;
				}
#else
 
				if (ShellItemController.GetItems().Count <= 1)
					defaultShowTabs = false;
#endif
 
				return shell.GetEffectiveValue<bool>(Shell.TabBarIsVisibleProperty, () => defaultShowTabs, null, displayedPage);
			}
		}
 
		#endregion IShellItemController
 
		#region IPropertyPropagationController
		void IPropertyPropagationController.PropagatePropertyChanged(string propertyName)
		{
			PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, ((IVisualTreeElement)this).GetVisualChildren());
		}
		#endregion
 
		/// <summary>Bindable property for <see cref="CurrentItem"/>.</summary>
		public static readonly BindableProperty CurrentItemProperty =
			BindableProperty.Create(nameof(CurrentItem), typeof(ShellSection), typeof(ShellItem), null, BindingMode.TwoWay,
				propertyChanged: OnCurrentItemChanged);
 
 
		/// <summary>Bindable property for <see cref="Items"/>.</summary>
 
		public static readonly BindableProperty ItemsProperty = ItemsPropertyKey.BindableProperty;
		Lazy<PlatformConfigurationRegistry<ShellItem>> _platformConfigurationRegistry;
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellItem.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public ShellItem()
		{
			((ShellElementCollection)Items).VisibleItemsChangedInternal += (_, args) =>
			{
				if (args.OldItems != null)
				{
					foreach (Element item in args.OldItems)
					{
						OnVisibleChildRemoved(item);
					}
				}
 
				if (args.NewItems != null)
				{
					foreach (Element item in args.NewItems)
					{
						OnVisibleChildAdded(item);
					}
				}
 
				SendStructureChanged();
			};
 
			(Items as INotifyCollectionChanged).CollectionChanged += ItemsCollectionChanged;
 
			_platformConfigurationRegistry = new Lazy<PlatformConfigurationRegistry<ShellItem>>(() => new PlatformConfigurationRegistry<ShellItem>(this));
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellItem.xml" path="//Member[@MemberName='CurrentItem']/Docs/*" />
		public ShellSection CurrentItem
		{
			get { return (ShellSection)GetValue(CurrentItemProperty); }
			set { SetValue(CurrentItemProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellItem.xml" path="//Member[@MemberName='Items']/Docs/*" />
		public IList<ShellSection> Items => (IList<ShellSection>)GetValue(ItemsProperty);
		internal override ShellElementCollection ShellElementCollection => (ShellElementCollection)Items;
 
		internal bool IsVisibleItem => Parent is Shell shell && shell?.CurrentItem == this;
 
		internal void SendStructureChanged()
		{
			if (Parent is Shell shell)
			{
				if (IsVisibleItem)
					shell.SendStructureChanged();
 
				shell.SendFlyoutItemsChanged();
			}
		}
 
		internal static ShellItem CreateFromShellSection(ShellSection shellSection)
		{
			if (shellSection.Parent != null)
			{
				var current = (ShellItem)shellSection.Parent;
 
				if (current.Items.Contains(shellSection))
					current.CurrentItem = shellSection;
 
				return current;
			}
 
			ShellItem result = null;
 
			if (shellSection is Tab)
				result = new TabBar();
			else
				result = new ShellItem();
 
			result.Route = Routing.GenerateImplicitRoute(shellSection.Route);
 
			result.Items.Add(shellSection);
			result.SetBinding(TitleProperty, static (ShellSection section) => section.Title, BindingMode.OneWay, source: shellSection);
			result.SetBinding(IconProperty, static (ShellSection section) => section.Icon, BindingMode.OneWay, source: shellSection);
			result.SetBinding(FlyoutDisplayOptionsProperty, static (ShellSection section) => section.FlyoutDisplayOptions, BindingMode.OneTime, source: shellSection);
			result.SetBinding(FlyoutIconProperty, static (ShellSection section) => section.FlyoutIcon, BindingMode.OneWay, source: shellSection);
 
			return result;
		}
 
		public static implicit operator ShellItem(ShellSection shellSection)
		{
			return CreateFromShellSection(shellSection);
		}
 
		public static implicit operator ShellItem(ShellContent shellContent) => (ShellSection)shellContent;
 
		public static implicit operator ShellItem(TemplatedPage page) => (ShellSection)(ShellContent)page;
 
		public static implicit operator ShellItem(MenuItem menuItem) => new MenuShellItem(menuItem);
 
		/// <inheritdoc/>
		public IPlatformElementConfiguration<T, ShellItem> On<T>() where T : IConfigPlatform
		{
			return _platformConfigurationRegistry.Value.On<T>();
		}
 
		protected override void OnChildAdded(Element child)
		{
			base.OnChildAdded(child);
			OnVisibleChildAdded(child);
		}
 
		protected override void OnChildRemoved(Element child, int oldLogicalIndex)
		{
			base.OnChildRemoved(child, oldLogicalIndex);
			OnVisibleChildRemoved(child);
		}
 
		void OnVisibleChildAdded(Element child)
		{
			if (CurrentItem == null && ((IShellItemController)this).GetItems().Contains(child))
				SetValueFromRenderer(CurrentItemProperty, child);
		}
 
		void OnVisibleChildRemoved(Element child)
		{
			if (CurrentItem == child)
			{
				if (ShellItemController.GetItems().Count == 0)
					ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromHandler);
				else
					SetValueFromRenderer(CurrentItemProperty, ShellItemController.GetItems()[0]);
			}
		}
 
		static void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
		{
			if (oldValue is BaseShellItem oldShellItem)
				oldShellItem.SendDisappearing();
 
			var shellItem = (ShellItem)bindable;
 
			if (newValue == null)
				return;
 
			if (shellItem.Parent is Shell)
			{
				if (newValue is BaseShellItem newShellItem)
					newShellItem.SendAppearing();
			}
 
			if (shellItem.Parent is IShellController shell && shellItem.IsVisibleItem)
			{
				shell.UpdateCurrentState(ShellNavigationSource.ShellSectionChanged);
			}
 
			shellItem.SendStructureChanged();
 
			if (shellItem.IsVisibleItem)
				((IShellController)shellItem?.Parent)?.AppearanceChanged(shellItem, false);
		}
 
		void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (e.NewItems != null)
			{
				foreach (Element element in e.NewItems)
					OnChildAdded(element);
			}
 
			if (e.OldItems != null)
			{
				for (var i = 0; i < e.OldItems.Count; i++)
				{
					var element = (Element)e.OldItems[i];
					OnChildRemoved(element, e.OldStartingIndex + i);
				}
			}
		}
 
		internal override void SendAppearing()
		{
			base.SendAppearing();
			if (CurrentItem != null && Parent is Shell shell && shell.CurrentItem == this)
			{
				CurrentItem.SendAppearing();
			}
		}
 
		internal override void SendDisappearing()
		{
			base.SendDisappearing();
			CurrentItem?.SendDisappearing();
		}
 
		protected override void OnParentSet()
		{
			base.OnParentSet();
			if (this.IsVisibleItem && CurrentItem != null)
				((IShellController)Parent)?.AppearanceChanged(CurrentItem, false);
		}
 
		private sealed class ShellItemConverter : TypeConverter
		{
			public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
				=> sourceType == typeof(ShellSection)
					|| sourceType == typeof(ShellContent)
					|| sourceType == typeof(TemplatedPage)
					|| sourceType == typeof(MenuItem);
 
			public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
				=> false;
 
			public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
				=> value switch
				{
					ShellSection shellSection => (ShellItem)shellSection,
					ShellContent shellContent => (ShellItem)shellContent,
					TemplatedPage page => (ShellItem)page,
					MenuItem menuItem => (ShellItem)menuItem,
					_ => throw new NotSupportedException(),
				};
 
			public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
				=> throw new NotSupportedException();
		}
	}
}