/* * Copyright(c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ using System; using System.Collections.Generic; using System.ComponentModel; using Tizen.NUI.BaseComponents; using Tizen.NUI.Binding; namespace Tizen.NUI.Components { /// /// PoppedEventArgs is a class to record event arguments which will be sent to user. /// /// 9 public class PoppedEventArgs : EventArgs { /// /// Page popped by Navigator. /// /// 9 public Page Page { get; internal set; } } /// /// The Navigator is a class which navigates pages with stack methods such as Push and Pop. /// /// /// With Transition class, Navigator supports smooth transition of View pair between two Pages /// by using and methods. /// If current top Page and next top Page have s those have same TransitionTag, /// Navigator creates smooth transition motion for them. /// Navigator.Transition property can be used to set properties of the Transition such as TimePeriod and AlphaFunction. /// When all transitions are finished, Navigator calls a callback methods those connected on the "TransitionFinished" event. /// /// /// /// Navigator navigator = new Navigator() /// { /// TimePeriod = new TimePeriod(500), /// AlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseInOutSine) /// }; /// /// View view = new View() /// { /// TransitionOptions = new TransitionOptions() /// { /// /* Set properties for the transition of this View */ /// } /// }; /// /// ContentPage newPage = new ContentPage() /// { /// Content = view, /// }; /// /// Navigator.PushWithTransition(newPage); /// /// /// 9 public class Navigator : Control { /// /// TransitionProperty /// [EditorBrowsable(EditorBrowsableState.Never)] public static readonly BindableProperty TransitionProperty = BindableProperty.Create(nameof(Transition), typeof(Transition), typeof(Navigator), null, propertyChanged: (bindable, oldValue, newValue) => { var instance = (Navigator)bindable; if (newValue != null) { instance.InternalTransition = newValue as Transition; } }, defaultValueCreator: (bindable) => { var instance = (Navigator)bindable; return instance.InternalTransition; }); private const int DefaultTransitionDuration = 300; //This will be replaced with view transition class instance. private Animation curAnimation = null; //This will be replaced with view transition class instance. private Animation newAnimation = null; private TransitionSet transitionSet = null; private Transition transition = new Transition() { TimePeriod = new TimePeriod(DefaultTransitionDuration), AlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Default), }; private bool transitionFinished = true; //TODO: Needs to consider how to remove disposed window from dictionary. //Two dictionaries are required to remove disposed navigator from dictionary. private static Dictionary windowNavigator = new Dictionary(); private static Dictionary navigatorWindow = new Dictionary(); private List navigationPages = new List(); /// /// Creates a new instance of a Navigator. /// /// 9 public Navigator() : base() { Layout = new AbsoluteLayout(); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void OnInitialize() { base.OnInitialize(); AccessibilityRole = Role.PageTabList; } /// /// An event fired when Transition has been finished. /// /// 9 public event EventHandler TransitionFinished; /// /// An event fired when Pop of a page has been finished. /// /// /// When you free resources in the Popped event handler, please make sure if the popped page is the page you find. /// /// 9 public event EventHandler Popped; /// /// Returns the count of pages in Navigator. /// /// 9 public int PageCount => navigationPages.Count; /// /// Transition properties for the transition of View pair having same transition tag. /// /// 9 public Transition Transition { get { return GetValue(TransitionProperty) as Transition; } set { SetValue(TransitionProperty, value); NotifyPropertyChanged(); } } private Transition InternalTransition { set { transition = value; } get { return transition; } } /// /// Pushes a page to Navigator. /// If the page is already in Navigator, then it is not pushed. /// /// The page to push to Navigator. /// Thrown when the argument page is null. /// 9 public void PushWithTransition(Page page) { if (!transitionFinished) { Tizen.Log.Error("NUI", "Transition is still not finished.\n"); return; } if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } //Duplicate page is not pushed. if (navigationPages.Contains(page)) return; var topPage = Peek(); if (!topPage) { Insert(0, page); return; } navigationPages.Add(page); Add(page); page.Navigator = this; //Invoke Page events page.InvokeAppearing(); topPage.InvokeDisappearing(); transitionSet = CreateTransitions(topPage, page, true); transitionSet.Finished += (object sender, EventArgs e) => { if (page is DialogPage == false) { topPage.SetVisible(false); } // Need to update Content of the new page ShowContentOfPage(page); //Invoke Page events page.InvokeAppeared(); topPage.InvokeDisappeared(); NotifyAccessibilityStatesChangeOfPages(topPage, page); }; transitionFinished = false; } /// /// Pops the top page from Navigator. /// /// The popped page. /// Thrown when there is no page in Navigator. /// 9 public Page PopWithTransition() { if (!transitionFinished) { Tizen.Log.Error("NUI", "Transition is still not finished.\n"); return null; } if (navigationPages.Count == 0) { throw new InvalidOperationException("There is no page in Navigator."); } var topPage = Peek(); if (navigationPages.Count == 1) { Remove(topPage); //Invoke Popped event Popped?.Invoke(this, new PoppedEventArgs() { Page = topPage }); return topPage; } var newTopPage = navigationPages[navigationPages.Count - 2]; //Invoke Page events newTopPage.InvokeAppearing(); topPage.InvokeDisappearing(); transitionSet = CreateTransitions(topPage, newTopPage, false); transitionSet.Finished += (object sender, EventArgs e) => { Remove(topPage); topPage.SetVisible(true); // Need to update Content of the new page ShowContentOfPage(newTopPage); //Invoke Page events newTopPage.InvokeAppeared(); topPage.InvokeDisappeared(); //Invoke Popped event Popped?.Invoke(this, new PoppedEventArgs() { Page = topPage }); }; transitionFinished = false; return topPage; } /// /// Pushes a page to Navigator. /// If the page is already in Navigator, then it is not pushed. /// /// The page to push to Navigator. /// Thrown when the argument page is null. /// 9 public void Push(Page page) { if (!transitionFinished) { Tizen.Log.Error("NUI", "Transition is still not finished.\n"); return; } if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } //Duplicate page is not pushed. if (navigationPages.Contains(page)) return; var curTop = Peek(); if (!curTop) { Insert(0, page); return; } navigationPages.Add(page); Add(page); page.Navigator = this; //Invoke Page events page.InvokeAppearing(); curTop.InvokeDisappearing(); curTop.SaveKeyFocus(); //TODO: The following transition codes will be replaced with view transition. InitializeAnimation(); if (page is DialogPage == false) { curAnimation = new Animation(DefaultTransitionDuration); curAnimation.AnimateTo(curTop, "PositionX", 0.0f, 0, DefaultTransitionDuration); curAnimation.EndAction = Animation.EndActions.StopFinal; curAnimation.Finished += (object sender, EventArgs args) => { curTop.SetVisible(false); //Invoke Page events curTop.InvokeDisappeared(); }; curAnimation.Play(); page.PositionX = SizeWidth; page.SetVisible(true); // Set Content visible because it was hidden by HideContentOfPage. (page as ContentPage).Content?.SetVisible(true); newAnimation = new Animation(DefaultTransitionDuration); newAnimation.AnimateTo(page, "PositionX", 0.0f, 0, DefaultTransitionDuration); newAnimation.EndAction = Animation.EndActions.StopFinal; newAnimation.Finished += (object sender, EventArgs e) => { // Need to update Content of the new page ShowContentOfPage(page); //Invoke Page events page.InvokeAppeared(); NotifyAccessibilityStatesChangeOfPages(curTop, page); page.RestoreKeyFocus(); }; newAnimation.Play(); } else { ShowContentOfPage(page); page.RestoreKeyFocus(); } } /// /// Pops the top page from Navigator. /// /// The popped page. /// Thrown when there is no page in Navigator. /// 9 public Page Pop() { if (!transitionFinished) { Tizen.Log.Error("NUI", "Transition is still not finished.\n"); return null; } if (navigationPages.Count == 0) { throw new InvalidOperationException("There is no page in Navigator."); } var curTop = Peek(); if (navigationPages.Count == 1) { Remove(curTop); //Invoke Popped event Popped?.Invoke(this, new PoppedEventArgs() { Page = curTop }); return curTop; } var newTop = navigationPages[navigationPages.Count - 2]; //Invoke Page events newTop.InvokeAppearing(); curTop.InvokeDisappearing(); curTop.SaveKeyFocus(); //TODO: The following transition codes will be replaced with view transition. InitializeAnimation(); if (curTop is DialogPage == false) { curAnimation = new Animation(DefaultTransitionDuration); curAnimation.AnimateTo(curTop, "PositionX", SizeWidth, 0, DefaultTransitionDuration); curAnimation.EndAction = Animation.EndActions.StopFinal; curAnimation.Finished += (object sender, EventArgs e) => { //Removes the current top page after transition is finished. Remove(curTop); curTop.PositionX = 0.0f; //Invoke Page events curTop.InvokeDisappeared(); //Invoke Popped event Popped?.Invoke(this, new PoppedEventArgs() { Page = curTop }); }; curAnimation.Play(); newTop.SetVisible(true); // Set Content visible because it was hidden by HideContentOfPage. (newTop as ContentPage).Content?.SetVisible(true); newAnimation = new Animation(DefaultTransitionDuration); newAnimation.AnimateTo(newTop, "PositionX", 0.0f, 0, DefaultTransitionDuration); newAnimation.EndAction = Animation.EndActions.StopFinal; newAnimation.Finished += (object sender, EventArgs e) => { // Need to update Content of the new page ShowContentOfPage(newTop); //Invoke Page events newTop.InvokeAppeared(); newTop.RestoreKeyFocus(); }; newAnimation.Play(); } else { Remove(curTop); } return curTop; } /// /// Returns the page of the given index in Navigator. /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator. /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs. /// /// The index of a page in Navigator. /// The page of the given index in Navigator. /// Thrown when the argument index is less than 0, or greater than the number of pages. public Page GetPage(int index) { if ((index < 0) || (index > navigationPages.Count)) { throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than or equal to the number of pages."); } return navigationPages[index]; } /// /// Returns the current index of the given page in Navigator. /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator. /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs. /// /// The page in Navigator. /// The index of the given page in Navigator. If the given page is not in the Navigator, then -1 is returned. /// Thrown when the argument page is null. /// 9 public int IndexOf(Page page) { if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } for (int i = 0; i < navigationPages.Count; i++) { if (navigationPages[i] == page) { return i; } } return -1; } /// /// Inserts a page at the specified index of Navigator. /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator. /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs. /// To find the current index of a page in Navigator, please use IndexOf(page). /// If the page is already in Navigator, then it is not inserted. /// /// The index of a page in Navigator where the page will be inserted. /// The page to insert to Navigator. /// Thrown when the argument index is less than 0, or greater than the number of pages. /// Thrown when the argument page is null. /// 9 public void Insert(int index, Page page) { if ((index < 0) || (index > navigationPages.Count)) { throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than or equal to the number of pages."); } if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } //Duplicate page is not pushed. if (navigationPages.Contains(page)) return; //TODO: The following transition codes will be replaced with view transition. InitializeAnimation(); ShowContentOfPage(page); if (index == PageCount) { page.SetVisible(true); } else { page.SetVisible(false); } navigationPages.Insert(index, page); Add(page); page.Navigator = this; if (index == PageCount - 1) { if (PageCount > 1) { NotifyAccessibilityStatesChangeOfPages(navigationPages[PageCount - 2], page); } else { NotifyAccessibilityStatesChangeOfPages(null, page); } } } /// /// Inserts a page to Navigator before an existing page. /// If the page is already in Navigator, then it is not inserted. /// /// The existing page, before which a page will be inserted. /// The page to insert to Navigator. /// Thrown when the argument before is null. /// Thrown when the argument page is null. /// Thrown when the argument before does not exist in Navigator. /// 9 public void InsertBefore(Page before, Page page) { if (before == null) { throw new ArgumentNullException(nameof(before), "before should not be null."); } if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } //Find the index of before page. int beforeIndex = navigationPages.FindIndex(x => x == before); //before does not exist in Navigator. if (beforeIndex == -1) { throw new ArgumentException("before does not exist in Navigator.", nameof(before)); } Insert(beforeIndex, page); } /// /// Removes a page from Navigator. /// /// The page to remove from Navigator. /// Thrown when the argument page is null. /// 9 public void Remove(Page page) { if (page == null) { throw new ArgumentNullException(nameof(page), "page should not be null."); } //TODO: The following transition codes will be replaced with view transition. InitializeAnimation(); HideContentOfPage(page); if (page == Peek()) { if (PageCount >= 2) { navigationPages[PageCount - 2].SetVisible(true); NotifyAccessibilityStatesChangeOfPages(page, navigationPages[PageCount - 2]); } else if (PageCount == 1) { NotifyAccessibilityStatesChangeOfPages(page, null); } } page.Navigator = null; navigationPages.Remove(page); base.Remove(page); } /// /// Removes a page at the specified index of Navigator. /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator. /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs. /// To find the current index of a page in Navigator, please use IndexOf(page). /// /// The index of a page in Navigator where the page will be removed. /// Thrown when the index is less than 0, or greater than or equal to the number of pages. /// 9 public void RemoveAt(int index) { if ((index < 0) || (index >= navigationPages.Count)) { throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than the number of pages."); } Remove(navigationPages[index]); } /// /// Returns the page at the top of Navigator. /// /// The page at the top of Navigator. /// 9 public Page Peek() { if (navigationPages.Count == 0) return null; return navigationPages[navigationPages.Count - 1]; } /// /// Disposes Navigator and all children on it. /// /// Dispose type. [EditorBrowsable(EditorBrowsableState.Never)] protected override void Dispose(DisposeTypes type) { if (disposed) { return; } if (type == DisposeTypes.Explicit) { foreach (Page page in navigationPages) { Utility.Dispose(page); } navigationPages.Clear(); Window window; if (navigatorWindow.TryGetValue(this, out window) == true) { navigatorWindow.Remove(this); windowNavigator.Remove(window); } } base.Dispose(type); } /// /// Returns the default navigator of the given window. /// /// The default navigator of the given window. /// Thrown when the argument window is null. /// 9 public static Navigator GetDefaultNavigator(Window window) { if (window == null) { throw new ArgumentNullException(nameof(window), "window should not be null."); } if (windowNavigator.ContainsKey(window) == true) { return windowNavigator[window]; } var defaultNavigator = new Navigator(); defaultNavigator.WidthResizePolicy = ResizePolicyType.FillToParent; defaultNavigator.HeightResizePolicy = ResizePolicyType.FillToParent; window.Add(defaultNavigator); windowNavigator.Add(window, defaultNavigator); navigatorWindow.Add(defaultNavigator, window); return defaultNavigator; } /// /// Create Transitions between currentTopPage and newTopPage /// /// The top page of Navigator. /// The new top page after transition. /// True if this transition is for push new page private TransitionSet CreateTransitions(Page currentTopPage, Page newTopPage, bool pushTransition) { currentTopPage.SetVisible(true); // Set Content visible because it was hidden by HideContentOfPage. (currentTopPage as ContentPage).Content?.SetVisible(true); newTopPage.SetVisible(true); // Set Content visible because it was hidden by HideContentOfPage. (newTopPage as ContentPage).Content?.SetVisible(true); List taggedViewsInNewTopPage = new List(); RetrieveTaggedViews(taggedViewsInNewTopPage, newTopPage, true); List taggedViewsInCurrentTopPage = new List(); RetrieveTaggedViews(taggedViewsInCurrentTopPage, currentTopPage, true); List> sameTaggedViewPair = new List>(); foreach (View currentTopPageView in taggedViewsInCurrentTopPage) { bool findPair = false; foreach (View newTopPageView in taggedViewsInNewTopPage) { if ((currentTopPageView.TransitionOptions != null) && (newTopPageView.TransitionOptions != null) && currentTopPageView.TransitionOptions?.TransitionTag == newTopPageView.TransitionOptions?.TransitionTag) { sameTaggedViewPair.Add(new KeyValuePair(currentTopPageView, newTopPageView)); findPair = true; break; } } if (findPair) { taggedViewsInNewTopPage.Remove(sameTaggedViewPair[sameTaggedViewPair.Count - 1].Value); } } foreach (KeyValuePair pair in sameTaggedViewPair) { taggedViewsInCurrentTopPage.Remove(pair.Key); } TransitionSet newTransitionSet = new TransitionSet(); foreach (KeyValuePair pair in sameTaggedViewPair) { TransitionItem pairTransition = transition.CreateTransition(pair.Key, pair.Value, pushTransition); if (pair.Value.TransitionOptions?.TransitionWithChild ?? false) { pairTransition.TransitionWithChild = true; } newTransitionSet.AddTransition(pairTransition); } newTransitionSet.Finished += (object sender, EventArgs e) => { if (newTopPage.Layout != null) { newTopPage.Layout.RequestLayout(); } if (currentTopPage.Layout != null) { currentTopPage.Layout.RequestLayout(); } transitionFinished = true; InvokeTransitionFinished(); transitionSet.Dispose(); }; if (!pushTransition || newTopPage is DialogPage == false) { View transitionView = (currentTopPage is ContentPage) ? (currentTopPage as ContentPage).Content : (currentTopPage as DialogPage).Content; if (currentTopPage.DisappearingTransition != null && transitionView != null) { TransitionItemBase disappearingTransition = currentTopPage.DisappearingTransition.CreateTransition(transitionView, false); disappearingTransition.TransitionWithChild = true; newTransitionSet.AddTransition(disappearingTransition); } else { currentTopPage.SetVisible(false); } } if (pushTransition || currentTopPage is DialogPage == false) { View transitionView = (newTopPage is ContentPage) ? (newTopPage as ContentPage).Content : (newTopPage as DialogPage).Content; if (newTopPage.AppearingTransition != null && transitionView != null) { TransitionItemBase appearingTransition = newTopPage.AppearingTransition.CreateTransition(transitionView, true); appearingTransition.TransitionWithChild = true; newTransitionSet.AddTransition(appearingTransition); } } newTransitionSet.Play(); return newTransitionSet; } /// /// Retrieve Tagged Views in the view tree. /// /// Returned tagged view list.. /// Root View to get tagged child View. /// Flag to check current View is page or not private void RetrieveTaggedViews(List taggedViews, View view, bool isRoot) { if (!isRoot && view.TransitionOptions != null) { if (!string.IsNullOrEmpty(view.TransitionOptions?.TransitionTag)) { taggedViews.Add((view as View)); if (view.TransitionOptions.TransitionWithChild) { return; } } } foreach (View child in view.Children) { RetrieveTaggedViews(taggedViews, child, false); } } /// /// Notify accessibility states change of pages. /// /// Disappeared page /// Appeared page private void NotifyAccessibilityStatesChangeOfPages(Page disappearedPage, Page appearedPage) { if (disappearedPage != null) { disappearedPage.UnregisterDefaultLabel(); //We can call disappearedPage.NotifyAccessibilityStatesChange //To reduce accessibility events, we are using currently highlighted view instead View curHighlightedView = Accessibility.Accessibility.GetCurrentlyHighlightedView(); if (curHighlightedView != null) { curHighlightedView.NotifyAccessibilityStatesChange(new AccessibilityStates(AccessibilityState.Visible, AccessibilityState.Showing), AccessibilityStatesNotifyMode.Single); } } if (appearedPage != null) { appearedPage.RegisterDefaultLabel(); appearedPage.NotifyAccessibilityStatesChange(new AccessibilityStates(AccessibilityState.Visible, AccessibilityState.Showing), AccessibilityStatesNotifyMode.Single); } } internal void InvokeTransitionFinished() { TransitionFinished?.Invoke(this, new EventArgs()); } //TODO: The following transition codes will be replaced with view transition. private void InitializeAnimation() { if (curAnimation != null) { curAnimation.Stop(); curAnimation.Clear(); curAnimation = null; } if (newAnimation != null) { newAnimation.Stop(); newAnimation.Clear(); newAnimation = null; } } // Show and Register Content of Page to Accessibility bridge private void ShowContentOfPage(Page page) { View content = (page is DialogPage) ? (page as DialogPage)?.Content : (page as ContentPage)?.Content; if (content != null) { content.Show(); // Calls RegisterDefaultLabel() } } // Hide and Remove Content of Page from Accessibility bridge private void HideContentOfPage(Page page) { View content = (page is DialogPage) ? (page as DialogPage)?.Content : (page as ContentPage)?.Content; if (content != null) { content.Hide(); // Calls UnregisterDefaultLabel() } } } }