File: Shell\BaseShellItem.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.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.StyleSheets;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="Type[@FullName='Microsoft.Maui.Controls.BaseShellItem']/Docs/*" />
	[DebuggerDisplay("Title = {Title}, Route = {Route}")]
	public class BaseShellItem : NavigableElement, IPropertyPropagationController, IVisualController, IFlowDirectionController, IWindowController
	{
		public event EventHandler Appearing;
		public event EventHandler Disappearing;
 
		bool _hasAppearing;
		const string DefaultFlyoutItemLabelStyle = "Default_FlyoutItemLabelStyle";
		const string DefaultFlyoutItemImageStyle = "Default_FlyoutItemImageStyle";
		const string DefaultFlyoutItemLayoutStyle = "Default_FlyoutItemLayoutStyle";
 
		protected private ObservableCollection<Element> DeclaredChildren { get; } = new ObservableCollection<Element>();
 
		#region PropertyKeys
 
		internal static readonly BindablePropertyKey IsCheckedPropertyKey = BindableProperty.CreateReadOnly(nameof(IsChecked), typeof(bool), typeof(BaseShellItem), false);
 
		#endregion PropertyKeys
 
		/// <summary>Bindable property for <see cref="FlyoutIcon"/>.</summary>
		public static readonly BindableProperty FlyoutIconProperty =
			BindableProperty.Create(nameof(FlyoutIcon), typeof(ImageSource), typeof(BaseShellItem), null, BindingMode.OneTime);
 
		/// <summary>Bindable property for <see cref="Icon"/>.</summary>
		public static readonly BindableProperty IconProperty =
			BindableProperty.Create(nameof(Icon), typeof(ImageSource), typeof(BaseShellItem), null, BindingMode.OneWay,
				propertyChanged: OnIconChanged);
 
		/// <summary>Bindable property for <see cref="IsChecked"/>.</summary>
		public static readonly BindableProperty IsCheckedProperty = IsCheckedPropertyKey.BindableProperty;
 
		/// <summary>Bindable property for <see cref="IsEnabled"/>.</summary>
		public static readonly BindableProperty IsEnabledProperty =
			BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(BaseShellItem), true, BindingMode.OneWay);
 
		/// <summary>Bindable property for <see cref="Title"/>.</summary>
		public static readonly BindableProperty TitleProperty =
			BindableProperty.Create(nameof(Title), typeof(string), typeof(BaseShellItem), null, BindingMode.TwoWay, propertyChanged: OnTitlePropertyChanged);
 
		/// <summary>Bindable property for <see cref="IsVisible"/>.</summary>
		public static readonly BindableProperty IsVisibleProperty =
			BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(BaseShellItem), true);
 
		/// <summary>Bindable property for <see cref="FlyoutItemIsVisible"/>.</summary>
		public static readonly BindableProperty FlyoutItemIsVisibleProperty =
			BindableProperty.Create(nameof(FlyoutItemIsVisible), typeof(bool), typeof(BaseShellItem), true, propertyChanged: OnFlyoutItemIsVisibleChanged);
 
		public BaseShellItem()
		{
			DeclaredChildren.CollectionChanged += (_, args) =>
			{
				if (args.NewItems != null)
					foreach (Element element in args.NewItems)
						AddLogicalChild(element);
 
				if (args.OldItems != null)
					foreach (Element element in args.OldItems)
						RemoveLogicalChild(element);
			};
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='FlyoutIcon']/Docs/*" />
		public ImageSource FlyoutIcon
		{
			get { return (ImageSource)GetValue(FlyoutIconProperty); }
			set { SetValue(FlyoutIconProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='Icon']/Docs/*" />
		public ImageSource Icon
		{
			get { return (ImageSource)GetValue(IconProperty); }
			set { SetValue(IconProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='IsChecked']/Docs/*" />
		public bool IsChecked => (bool)GetValue(IsCheckedProperty);
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='IsEnabled']/Docs/*" />
		public bool IsEnabled
		{
			get { return (bool)GetValue(IsEnabledProperty); }
			set { SetValue(IsEnabledProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='Route']/Docs/*" />
		public string Route
		{
			get { return Routing.GetRoute(this); }
			set { Routing.SetRoute(this, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='Title']/Docs/*" />
		public string Title
		{
			get { return (string)GetValue(TitleProperty); }
			set { SetValue(TitleProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='IsVisible']/Docs/*" />
		public bool IsVisible
		{
			get => (bool)GetValue(IsVisibleProperty);
			set => SetValue(IsVisibleProperty, value);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='FlyoutItemIsVisible']/Docs/*" />
		public bool FlyoutItemIsVisible
		{
			get => (bool)GetValue(FlyoutItemIsVisibleProperty);
			set => SetValue(FlyoutItemIsVisibleProperty, value);
		}
 
 
		internal bool IsPartOfVisibleTree()
		{
			if (Parent is IShellController shell)
				return shell.GetItems().Contains(this);
			else if (Parent is ShellGroupItem sgi)
				return sgi.ShellElementCollection.Contains(this);
 
			return false;
		}
 
		internal virtual void SendAppearing()
		{
			if (_hasAppearing)
				return;
 
			_hasAppearing = true;
			OnAppearing();
			Appearing?.Invoke(this, EventArgs.Empty);
		}
 
		internal virtual void SendDisappearing()
		{
			if (!_hasAppearing)
				return;
 
			_hasAppearing = false;
			OnDisappearing();
			Disappearing?.Invoke(this, EventArgs.Empty);
		}
 
		protected virtual void OnAppearing()
		{
		}
 
		protected virtual void OnDisappearing()
		{
		}
 
		internal void OnAppearing(Action action)
		{
			if (_hasAppearing)
				action();
			else
			{
				if (Navigation.ModalStack.Count > 0)
				{
					Navigation.ModalStack[Navigation.ModalStack.Count - 1]
						.OnAppearing(action);
 
					return;
				}
				else if (Navigation.NavigationStack.Count > 1)
				{
					Navigation.NavigationStack[Navigation.NavigationStack.Count - 1]
						.OnAppearing(action);
 
					return;
				}
 
				EventHandler eventHandler = null;
				eventHandler = (_, __) =>
				{
					this.Appearing -= eventHandler;
					action();
				};
 
				this.Appearing += eventHandler;
			}
		}
 
		IVisual _effectiveVisual = Microsoft.Maui.Controls.VisualMarker.Default;
		IVisual IVisualController.EffectiveVisual
		{
			get { return _effectiveVisual; }
			set
			{
				if (value == _effectiveVisual)
					return;
 
				_effectiveVisual = value;
				OnPropertyChanged(VisualElement.VisualProperty.PropertyName);
			}
		}
		IVisual IVisualController.Visual => Microsoft.Maui.Controls.VisualMarker.MatchParent;
 
 
		static void OnTitlePropertyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			var shellItem = (BaseShellItem)bindable;
			if (shellItem.FindParentOfType<Shell>()?.Toolbar is ShellToolbar st)
				st.UpdateTitle();
		}
 
		static void OnIconChanged(BindableObject bindable, object oldValue, object newValue)
		{
			if (newValue == null || bindable.IsSet(FlyoutIconProperty))
				return;
 
			var shellItem = (BaseShellItem)bindable;
			shellItem.FlyoutIcon = (ImageSource)newValue;
		}
 
		static void OnFlyoutItemIsVisibleChanged(BindableObject bindable, object oldValue, object newValue)
		{
			Shell.SetFlyoutItemIsVisible(bindable, (bool)newValue);
		}
 
		protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			base.OnPropertyChanged(propertyName);
			if (Parent != null)
			{
				if (propertyName == Shell.ItemTemplateProperty.PropertyName || propertyName == nameof(Parent))
					Propagate(Shell.ItemTemplateProperty, this, Parent, true);
			}
		}
 
		internal static void PropagateFromParent(BindableProperty property, Element me)
		{
			if (me == null || me.Parent == null)
				return;
 
			Propagate(property, me.Parent, me, false);
		}
 
		internal static void Propagate(BindableProperty property, BindableObject from, BindableObject to, bool onlyToImplicit)
		{
			if (from == null || to == null)
				return;
 
			if (onlyToImplicit && Routing.IsImplicit(from))
				return;
 
			if (to is Shell)
				return;
 
			if (from.IsSet(property) && !to.IsSet(property))
				to.SetValue(property, from.GetValue(property));
		}
 
		void IPropertyPropagationController.PropagatePropertyChanged(string propertyName)
		{
			PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, ((IVisualTreeElement)this).GetVisualChildren());
		}
 
		EffectiveFlowDirection _effectiveFlowDirection = default(EffectiveFlowDirection);
		EffectiveFlowDirection IFlowDirectionController.EffectiveFlowDirection
		{
			get { return _effectiveFlowDirection; }
			set
			{
				if (value == _effectiveFlowDirection)
					return;
 
				_effectiveFlowDirection = value;
 
				var ve = (Parent as VisualElement);
				ve?.InvalidateMeasureInternal(InvalidationTrigger.Undefined);
				OnPropertyChanged(VisualElement.FlowDirectionProperty.PropertyName);
			}
		}
 
		bool IFlowDirectionController.ApplyEffectiveFlowDirectionToChildContainer => true;
		double IFlowDirectionController.Width => (Parent as VisualElement)?.Width ?? 0;
 
		static readonly BindablePropertyKey WindowPropertyKey = BindableProperty.CreateReadOnly(
			nameof(Window), typeof(Window), typeof(BaseShellItem), null);
 
		/// <summary>Bindable property for <see cref="Window"/>.</summary>
		public static readonly BindableProperty WindowProperty = WindowPropertyKey.BindableProperty;
 
		public Window Window => (Window)GetValue(WindowProperty);
 
		Window IWindowController.Window
		{
			get => (Window)GetValue(WindowProperty);
			set => SetValue(WindowPropertyKey, value);
		}
 
		internal virtual void ApplyQueryAttributes(ShellRouteParameters query)
		{
		}
 
		static void UpdateFlyoutItemStyles(Grid flyoutItemCell, IStyleSelectable source)
		{
			List<string> bindableObjectStyle = new List<string>() {
				DefaultFlyoutItemLabelStyle,
				DefaultFlyoutItemImageStyle,
				DefaultFlyoutItemLayoutStyle,
				FlyoutItem.LabelStyle,
				FlyoutItem.ImageStyle,
				FlyoutItem.LayoutStyle };
 
			if (source?.Classes != null)
				foreach (var styleClass in source.Classes)
					bindableObjectStyle.Add(styleClass);
 
			flyoutItemCell
				.StyleClass = bindableObjectStyle;
			flyoutItemCell.Children.OfType<Label>().First()
				.StyleClass = bindableObjectStyle;
			flyoutItemCell.Children.OfType<Image>().First()
				.StyleClass = bindableObjectStyle;
		}
 
		BindableObject NonImplicitParent
		{
			get
			{
				if (Parent is Shell)
					return Parent;
 
				var parent = (BaseShellItem)Parent;
 
				if (!Routing.IsImplicit(parent))
					return parent;
 
				return parent.NonImplicitParent;
			}
		}
 
		internal static DataTemplate CreateDefaultFlyoutItemCell(BindableObject bo)
		{
			return new DataTemplate(() =>
			{
				var grid = new Grid()
				{
					IgnoreSafeArea = true
				};
 
				if (OperatingSystem.IsWindows())
					grid.ColumnSpacing = grid.RowSpacing = 0;
 
				grid.Resources = new ResourceDictionary();
 
				var defaultLabelClass = new Style(typeof(Label))
				{
					Setters = {
						new Setter { Property = Label.VerticalTextAlignmentProperty, Value = TextAlignment.Center }
					},
					Class = DefaultFlyoutItemLabelStyle,
				};
 
				var defaultImageClass = new Style(typeof(Image))
				{
					Setters = {
						new Setter { Property = Image.VerticalOptionsProperty, Value = LayoutOptions.Center }
					},
					Class = DefaultFlyoutItemImageStyle,
				};
 
				var defaultGridClass = new Style(typeof(Grid))
				{
					Class = DefaultFlyoutItemLayoutStyle,
				};
 
				var groups = new VisualStateGroupList();
 
				var commonGroup = new VisualStateGroup();
				commonGroup.Name = "CommonStates";
				groups.Add(commonGroup);
 
				var normalState = new VisualState();
				normalState.Name = "Normal";
				commonGroup.States.Add(normalState);
 
				var selectedState = new VisualState();
				selectedState.Name = "Selected";
 
				if (!OperatingSystem.IsWindows())
				{
					selectedState.Setters.Add(new Setter
					{
						Property = VisualElement.BackgroundColorProperty,
						Value = new AppThemeBinding() { Light = Colors.Black.MultiplyAlpha(0.1f), Dark = Colors.White.MultiplyAlpha(0.1f) }
					});
				}
 
				normalState.Setters.Add(new Setter
				{
					Property = VisualElement.BackgroundColorProperty,
					Value = Colors.Transparent
				});
 
				commonGroup.States.Add(selectedState);
 
				defaultGridClass.Setters.Add(new Setter { Property = VisualStateManager.VisualStateGroupsProperty, Value = groups });
 
				if (OperatingSystem.IsAndroid())
					defaultGridClass.Setters.Add(new Setter { Property = Grid.HeightRequestProperty, Value = 50 });
				else
					defaultGridClass.Setters.Add(new Setter { Property = Grid.HeightRequestProperty, Value = 44 });
 
 
				ColumnDefinitionCollection columnDefinitions = new ColumnDefinitionCollection();
 
				if (OperatingSystem.IsAndroid())
					columnDefinitions.Add(new ColumnDefinition { Width = 54 });
				else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst())
					columnDefinitions.Add(new ColumnDefinition { Width = 50 });
				else if (OperatingSystem.IsWindows())
					columnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
				else if (DeviceInfo.Platform == DevicePlatform.Tizen)
					columnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
 
				columnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
				defaultGridClass.Setters.Add(new Setter { Property = Grid.ColumnDefinitionsProperty, Value = columnDefinitions });
 
				BindingBase automationIdBinding = Binding.Create(static (Element element) => element.AutomationId);
				defaultGridClass.Setters.Add(new Setter { Property = Element.AutomationIdProperty, Value = automationIdBinding });
 
				BindingBase imageBinding = null;
				BindingBase labelBinding = null;
				if (bo is MenuItem)
				{
					imageBinding = Binding.Create(static (MenuItem item) => item.IconImageSource);
					labelBinding = Binding.Create(static (MenuItem item) => item.Text);
				}
				else
				{
					imageBinding = Binding.Create(static (BaseShellItem item) => item.FlyoutIcon);
					labelBinding = Binding.Create(static (BaseShellItem item) => item.Title);
				}
 
				var image = new Image();
 
				double sizeRequest = -1;
				if (OperatingSystem.IsAndroid())
					sizeRequest = 24;
				else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst())
					sizeRequest = 22;
				else if (OperatingSystem.IsWindows())
					sizeRequest = 16;
				else if (DeviceInfo.Platform == DevicePlatform.Tizen)
					sizeRequest = 25;
 
				if (sizeRequest > 0)
				{
					defaultImageClass.Setters.Add(new Setter() { Property = Image.HeightRequestProperty, Value = sizeRequest });
					defaultImageClass.Setters.Add(new Setter() { Property = Image.WidthRequestProperty, Value = sizeRequest });
				}
 
				if (OperatingSystem.IsWindows())
				{
					defaultImageClass.Setters.Add(new Setter { Property = Image.HorizontalOptionsProperty, Value = LayoutOptions.Start });
					defaultImageClass.Setters.Add(new Setter { Property = Image.MarginProperty, Value = new Thickness(12, 0, 12, 0) });
				}
 
				defaultImageClass.Setters.Add(new Setter { Property = Image.SourceProperty, Value = imageBinding });
 
				grid.Add(image);
 
				var label = new Label();
				defaultLabelClass.Setters.Add(new Setter { Property = Label.TextProperty, Value = labelBinding });
 
				grid.Add(label, 1, 0);
 
				if (OperatingSystem.IsAndroid())
				{
					object textColor;
 
					if (Application.Current == null)
					{
						textColor = Colors.Black.MultiplyAlpha(0.87f);
					}
					else
					{
						textColor = new AppThemeBinding { Light = Colors.Black.MultiplyAlpha(0.87f), Dark = Colors.White };
					}
 
					defaultLabelClass.Setters.Add(new Setter { Property = Label.FontSizeProperty, Value = 14 });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.TextColorProperty, Value = textColor });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.FontFamilyProperty, Value = "sans-serif-medium" });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.MarginProperty, Value = new Thickness(20, 0, 0, 0) });
				}
				else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst())
				{
					defaultLabelClass.Setters.Add(new Setter { Property = Label.FontSizeProperty, Value = 14 });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.FontAttributesProperty, Value = FontAttributes.Bold });
				}
				else if (OperatingSystem.IsWindows())
				{
					defaultLabelClass.Setters.Add(new Setter { Property = Label.HorizontalOptionsProperty, Value = LayoutOptions.Start });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.HorizontalTextAlignmentProperty, Value = TextAlignment.Start });
				}
				else if (DeviceInfo.Platform == DevicePlatform.Tizen)
				{
					defaultLabelClass.Setters.Add(new Setter { Property = Label.HorizontalOptionsProperty, Value = LayoutOptions.Start });
					defaultLabelClass.Setters.Add(new Setter { Property = Label.HorizontalTextAlignmentProperty, Value = TextAlignment.Start });
				}
 
				INameScope nameScope = new NameScope();
				NameScope.SetNameScope(grid, nameScope);
				nameScope.RegisterName("FlyoutItemLayout", grid);
				nameScope.RegisterName("FlyoutItemImage", image);
				nameScope.RegisterName("FlyoutItemLabel", label);
 
 
				ActionDisposable previousBindingContext = null;
				grid.BindingContextChanged += (sender, _) =>
				{
					previousBindingContext?.Dispose();
					previousBindingContext = null;
 
					if (sender is Grid g)
					{
						var bo = g.BindingContext as BindableObject;
						var styleClassSource = Shell.GetBindableObjectWithFlyoutItemTemplate(bo) as IStyleSelectable;
						UpdateFlyoutItemStyles(g, styleClassSource);
 
						// this means they haven't changed the BaseShellItemContext so we are
						// going to propagate the semantic properties to the default template
						if (g.BindingContext is BaseShellItem bsi)
						{
							previousBindingContext = SemanticProperties.FakeBindSemanticProperties(bsi, g);
 
							// If the user hasn't set a semantic property on the flyout item then we'll
							// just bind the semantic description to the title
							if (!g.IsSet(SemanticProperties.DescriptionProperty))
							{
								g.SetBinding(SemanticProperties.DescriptionProperty, static (BaseShellItem item) => item.Title);
							}
						}
					}
				};
 
				grid.Resources = new ResourceDictionary() { defaultGridClass, defaultLabelClass, defaultImageClass };
				return grid;
			});
		}
 
#if NETSTANDARD
		sealed class OperatingSystem
		{
			public static bool IsAndroid() => false;
			public static bool IsIOS() => false;
			public static bool IsMacCatalyst() => false;
			public static bool IsWindows() => false;
		}
#endif
	}
 
	public interface IQueryAttributable
	{
		void ApplyQueryAttributes(IDictionary<string, object> query);
	}
}