File: FlyoutPage\FlyoutPage.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.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="Type[@FullName='Microsoft.Maui.Controls.FlyoutPage']/Docs/*" />
	[ContentProperty(nameof(Detail))]
	public partial class FlyoutPage : Page, IFlyoutPageController, IElementConfiguration<FlyoutPage>, IFlyoutView
	{
		/// <summary>Bindable property for <see cref="IsGestureEnabled"/>.</summary>
		public static readonly BindableProperty IsGestureEnabledProperty = BindableProperty.Create(nameof(IsGestureEnabled), typeof(bool), typeof(FlyoutPage), true);
 
		/// <summary>Bindable property for <see cref="IsPresented"/>.</summary>
		public static readonly BindableProperty IsPresentedProperty = BindableProperty.Create(nameof(IsPresented), typeof(bool), typeof(FlyoutPage), default(bool),
			propertyChanged: OnIsPresentedPropertyChanged, propertyChanging: OnIsPresentedPropertyChanging, defaultValueCreator: GetDefaultValue);
 
		/// <summary>Bindable property for <see cref="FlyoutLayoutBehavior"/>.</summary>
		public static readonly BindableProperty FlyoutLayoutBehaviorProperty = BindableProperty.Create(nameof(FlyoutLayoutBehavior), typeof(FlyoutLayoutBehavior), typeof(FlyoutPage), default(FlyoutLayoutBehavior),
			propertyChanged: OnFlyoutLayoutBehaviorPropertyChanged);
 
		Page _detail;
 
		Rect _detailBounds;
 
		Page _flyout;
 
		Rect _flyoutBounds;
 
		IFlyoutPageController FlyoutPageController => this;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='Detail']/Docs/*" />
		public Page Detail
		{
			get { return _detail; }
			set
			{
				if (_detail != null && value == null)
					throw new ArgumentNullException(nameof(value), "Detail cannot be set to null once a value is set.");
 
				if (_detail == value)
					return;
 
				if (value.RealParent != null)
					throw new InvalidOperationException("Detail must not already have a parent.");
 
				var previousDetail = _detail;
				// TODO MAUI refine this to fire earlier
				_detail?.SendNavigatingFrom(new NavigatingFromEventArgs());
 
				OnPropertyChanging();
				if (_detail != null)
					InternalChildren.Remove(_detail);
				_detail = value;
				InternalChildren.Add(_detail);
				OnPropertyChanged();
 
				if (this.HasAppeared)
				{
					previousDetail?.SendDisappearing();
					_detail?.SendAppearing();
				}
 
				previousDetail?.SendNavigatedFrom(new NavigatedFromEventArgs(_detail, NavigationType.PageSwap));
				_detail?.SendNavigatedTo(new NavigatedToEventArgs(previousDetail));
			}
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='IsGestureEnabled']/Docs/*" />
		public bool IsGestureEnabled
		{
			get { return (bool)GetValue(IsGestureEnabledProperty); }
			set { SetValue(IsGestureEnabledProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='IsPresented']/Docs/*" />
		public bool IsPresented
		{
			get { return (bool)GetValue(IsPresentedProperty); }
			set { SetValue(IsPresentedProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='Flyout']/Docs/*" />
		public Page Flyout
		{
			get { return _flyout; }
			set
			{
				if (_flyout != null && value == null)
					throw new ArgumentNullException(nameof(value), "Flyout cannot be set to null once a value is set");
 
				if (string.IsNullOrEmpty(value.Title))
					throw new InvalidOperationException("Title property must be set on Flyout page");
 
				if (_flyout == value)
					return;
 
				if (value.RealParent != null)
					throw new InvalidOperationException("Flyout must not already have a parent.");
 
				// TODO MAUI refine this to fire earlier
				var previousFlyout = _flyout;
				// TODO MAUI refine this to fire earlier
				_flyout?.SendNavigatingFrom(new NavigatingFromEventArgs());
 
				OnPropertyChanging();
				if (_flyout != null)
					InternalChildren.Remove(_flyout);
				_flyout = value;
				InternalChildren.Add(_flyout);
				OnPropertyChanged();
 
				if (this.HasAppeared)
				{
					previousFlyout?.SendDisappearing();
					_flyout?.SendAppearing();
				}
 
				previousFlyout?.SendNavigatedFrom(new NavigatedFromEventArgs(_flyout, NavigationType.PageSwap));
				_flyout?.SendNavigatedTo(new NavigatedToEventArgs(previousFlyout));
			}
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='FlyoutLayoutBehavior']/Docs/*" />
		public FlyoutLayoutBehavior FlyoutLayoutBehavior
		{
			get { return (FlyoutLayoutBehavior)GetValue(FlyoutLayoutBehaviorProperty); }
			set { SetValue(FlyoutLayoutBehaviorProperty, value); }
		}
 
		bool IFlyoutPageController.CanChangeIsPresented { get; set; } = true;
 
		Rect IFlyoutPageController.DetailBounds
		{
			get { return _detailBounds; }
			set
			{
				_detailBounds = value;
				if (_detail == null)
					throw new InvalidOperationException("Detail must be set before using a FlyoutPage");
				_detail.Layout(value);
			}
		}
 
		Rect IFlyoutPageController.FlyoutBounds
		{
			get { return _flyoutBounds; }
			set
			{
				_flyoutBounds = value;
				if (_flyout == null)
					throw new InvalidOperationException("Flyout must be set before using a FlyoutPage");
				_flyout.Layout(value);
			}
		}
 
		bool IFlyoutPageController.ShouldShowSplitMode
		{
			get
			{
				if (DeviceInfo.Idiom == DeviceIdiom.Phone)
					return false;
 
				FlyoutLayoutBehavior behavior = FlyoutLayoutBehavior;
				var orientation = Window.GetOrientation();
 
				bool isSplitOnLandscape = (behavior == FlyoutLayoutBehavior.SplitOnLandscape || behavior == FlyoutLayoutBehavior.Default) && orientation.IsLandscape();
				bool isSplitOnPortrait = behavior == FlyoutLayoutBehavior.SplitOnPortrait && orientation.IsPortrait();
				return behavior == FlyoutLayoutBehavior.Split || isSplitOnLandscape || isSplitOnPortrait;
			}
		}
 
		public event EventHandler IsPresentedChanged;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='ShouldShowToolbarButton']/Docs/*" />
		public virtual bool ShouldShowToolbarButton()
		{
			if (DeviceInfo.Idiom == DeviceIdiom.Phone)
				return true;
 
			FlyoutLayoutBehavior behavior = FlyoutLayoutBehavior;
			var orientation = DeviceDisplay.MainDisplayInfo.Orientation;
 
			bool isSplitOnLandscape = (behavior == FlyoutLayoutBehavior.SplitOnLandscape || behavior == FlyoutLayoutBehavior.Default) && orientation.IsLandscape();
			bool isSplitOnPortrait = behavior == FlyoutLayoutBehavior.SplitOnPortrait && orientation.IsPortrait();
			return behavior != FlyoutLayoutBehavior.Split && !isSplitOnLandscape && !isSplitOnPortrait;
		}
 
		[Obsolete("Use ArrangeOverride instead")]
		protected override void LayoutChildren(double x, double y, double width, double height)
		{
			if (Flyout == null || Detail == null)
				throw new InvalidOperationException("Flyout and Detail must be set before using a FlyoutPage");
 
#if !ANDROID
			_flyout.Layout(_flyoutBounds);
			_detail.Layout(_detailBounds);
#endif
		}
 
		protected override void OnAppearing()
		{
			Flyout?.SendAppearing();
			Detail?.SendAppearing();
 
			FlyoutPageController.CanChangeIsPresented = true;
			UpdateFlyoutLayoutBehavior(this);
			base.OnAppearing();
		}
 
		protected override void OnDisappearing()
		{
			Flyout?.SendDisappearing();
			Detail?.SendDisappearing();
 
			base.OnDisappearing();
		}
 
		protected override bool OnBackButtonPressed()
		{
			if (IsPresented)
			{
				if (Flyout.SendBackButtonPressed())
					return true;
			}
 
			EventHandler<BackButtonPressedEventArgs> handler = BackButtonPressed;
			if (handler != null)
			{
				var args = new BackButtonPressedEventArgs();
				handler(this, args);
				if (args.Handled)
					return true;
			}
 
			if (Detail.SendBackButtonPressed())
			{
				return true;
			}
 
			return base.OnBackButtonPressed();
		}
 
		protected override void OnParentSet()
		{
			if (RealParent != null && (Flyout == null || Detail == null))
				throw new InvalidOperationException("Flyout and Detail must be set before adding FlyoutPage to a container");
			base.OnParentSet();
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public event EventHandler<BackButtonPressedEventArgs> BackButtonPressed;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='UpdateFlyoutLayoutBehavior']/Docs/*" />
		[EditorBrowsable(EditorBrowsableState.Never)]
		public void UpdateFlyoutLayoutBehavior()
		{
			UpdateFlyoutLayoutBehavior(this);
		}
 
		internal static void UpdateFlyoutLayoutBehavior(FlyoutPage page)
		{
			if (page is IFlyoutPageController fpc && fpc.ShouldShowSplitMode)
			{
				page.SetValue(IsPresentedProperty, true);
				if (page.FlyoutLayoutBehavior != FlyoutLayoutBehavior.Default)
					fpc.CanChangeIsPresented = false;
			}
		}
 
		static void OnIsPresentedPropertyChanged(BindableObject sender, object oldValue, object newValue)
			=> ((FlyoutPage)sender).IsPresentedChanged?.Invoke(sender, EventArgs.Empty);
 
		static void OnIsPresentedPropertyChanging(BindableObject sender, object oldValue, object newValue)
		{
			if (sender is Maui.IElement element && element.IsShimmed())
			{
				if (sender is FlyoutPage fp && fp is IFlyoutPageController fpc && !fpc.CanChangeIsPresented)
					throw new InvalidOperationException(string.Format("Can't change IsPresented when setting {0}", fp.FlyoutLayoutBehavior));
			}
			else
			{
				if ((!(bool)newValue) && sender is IFlyoutPageController fpc && fpc.ShouldShowSplitMode && sender is FlyoutPage fp)
				{
					throw new InvalidOperationException(string.Format("Can't change IsPresented when setting {0}", fp.FlyoutLayoutBehavior));
				}
			}
		}
 
		static void OnFlyoutLayoutBehaviorPropertyChanged(BindableObject sender, object oldValue, object newValue)
		{
			var page = (FlyoutPage)sender;
			UpdateFlyoutLayoutBehavior(page);
		}
 
		static object GetDefaultValue(BindableObject bindable)
		{
			return DeviceInfo.Platform == DevicePlatform.macOS;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/FlyoutPage.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public FlyoutPage()
		{
			_platformConfigurationRegistry = new Lazy<PlatformConfigurationRegistry<FlyoutPage>>(() => new PlatformConfigurationRegistry<FlyoutPage>(this));
			(this as IControlsVisualElement).WindowChanged += OnWindowChanged;
			this.SizeChanged += OnSizeChanged;
		}
 
		readonly Lazy<PlatformConfigurationRegistry<FlyoutPage>> _platformConfigurationRegistry;
 
		/// <inheritdoc/>
		public new IPlatformElementConfiguration<T, FlyoutPage> On<T>() where T : IConfigPlatform
		{
			return _platformConfigurationRegistry.Value.On<T>();
		}
 
		void OnSizeChanged(object sender, EventArgs e)
		{
			if (Handler is not null)
			{
				Handler?.UpdateValue(nameof(FlyoutBehavior));
				SizeChanged -= OnSizeChanged;
			}
		}
 
		void OnWindowChanged(object sender, EventArgs e)
		{
			if (Window is null)
			{
				SizeChanged -= OnSizeChanged;
				SizeChanged += OnSizeChanged;
				DeviceDisplay.MainDisplayInfoChanged -= OnMainDisplayInfoChanged;
			}
			else
			{
				DeviceDisplay.MainDisplayInfoChanged += OnMainDisplayInfoChanged;
			}
		}
 
		void OnMainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e)
		{
			Handler?.UpdateValue(nameof(FlyoutBehavior));
		}
 
		IView IFlyoutView.Flyout => this.Flyout;
		IView IFlyoutView.Detail => this.Detail;
 
		Maui.FlyoutBehavior IFlyoutView.FlyoutBehavior
		{
			get
			{
				if (((IFlyoutPageController)this).ShouldShowSplitMode)
					return Maui.FlyoutBehavior.Locked;
 
				return Maui.FlyoutBehavior.Flyout;
			}
		}
 
#if ANDROID

		const double DefaultFlyoutSize = 320;
		const double DefaultSmallFlyoutSize = 240;
 
		double IFlyoutView.FlyoutWidth
		{
			get
			{
				if (DeviceInfo.Idiom == DeviceIdiom.Phone)
					return -1;
 
				var scaledScreenSize = DeviceDisplay.MainDisplayInfo.GetScaledScreenSize();
				double w = scaledScreenSize.Width;
				return w < DefaultSmallFlyoutSize ? w : (w < DefaultFlyoutSize ? DefaultSmallFlyoutSize : DefaultFlyoutSize);
			}
		}
#else
		double IFlyoutView.FlyoutWidth => -1;
#endif
	}
}