/*
* 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()
}
}
}
}