diff --git a/application/application-common.ts b/application/application-common.ts index ec4ac8e66..377c8cdf9 100644 --- a/application/application-common.ts +++ b/application/application-common.ts @@ -3,6 +3,7 @@ import definition = require("application"); import fs = require("file-system"); import styleScope = require("ui/styling/style-scope"); import observable = require("data/observable"); +import frame = require("ui/frame"); var events = new observable.Observable(); global.moduleMerge(events, exports); @@ -15,6 +16,9 @@ export var lowMemoryEvent = "lowMemory"; export var uncaughtErrorEvent = "uncaughtError"; export var orientationChangedEvent = "orientationChanged"; +export var mainModule: string; +export var mainEntry: frame.NavigationEntry; + export var cssFile: string = "app.css" export var resources: any = {}; diff --git a/application/application.android.ts b/application/application.android.ts index 213a44bd7..85340b804 100644 --- a/application/application.android.ts +++ b/application/application.android.ts @@ -7,8 +7,6 @@ import enums = require("ui/enums"); global.moduleMerge(appModule, exports); -export var mainModule: string; - // We are using the exports object for the common events since we merge the appModule with this module's exports, which is what users will receive when require("application") is called; // TODO: This is kind of hacky and is "pure JS in TypeScript" @@ -218,10 +216,15 @@ export class AndroidApplication extends observable.Observable implements dts.And var topFrame = frame.topmost(); if (!topFrame) { - // try to navigate to the mainModule (if specified) - if (mainModule) { + // try to navigate to the mainEntry/Module (if specified) + var navParam = dts.mainEntry; + if (!navParam) { + navParam = dts.mainModule; + } + + if (navParam) { topFrame = new frame.Frame(); - topFrame.navigate(mainModule); + topFrame.navigate(navParam); } else { // TODO: Throw an exception? throw new Error("A Frame must be used to navigate to a Page."); diff --git a/application/application.d.ts b/application/application.d.ts index 60c3c70f9..ec75aedcd 100644 --- a/application/application.d.ts +++ b/application/application.d.ts @@ -4,6 +4,7 @@ declare module "application" { import cssSelector = require("ui/styling/css-selector"); import observable = require("data/observable"); + import frame = require("ui/frame"); /** * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. @@ -94,6 +95,11 @@ declare module "application" { */ export var mainModule: string; + /** + * The main navigation entry to be used when loading the main Page. + */ + export var mainEntry: frame.NavigationEntry; + /** * An application level static resources. */ diff --git a/application/application.ios.ts b/application/application.ios.ts index 3ffb911f1..97c84c793 100644 --- a/application/application.ios.ts +++ b/application/application.ios.ts @@ -5,9 +5,8 @@ import types = require("utils/types"); import view = require("ui/core/view"); import definition = require("application"); import enums = require("ui/enums"); -global.moduleMerge(appModule, exports); -export var mainModule: string; +global.moduleMerge(appModule, exports); class Responder extends UIResponder { // @@ -120,13 +119,18 @@ class IOSApplication implements definition.iOSApplication { var topFrame = frame.topmost(); if (!topFrame) { - if (mainModule) { + // try to navigate to the mainEntry/Module (if specified) + var navParam = definition.mainEntry; + if (!navParam) { + navParam = definition.mainModule; + } + + if (navParam) { topFrame = new frame.Frame(); - topFrame.navigate(mainModule); + topFrame.navigate(navParam); } else { // TODO: Throw an exception? - // throw new Error("A Frame must be used to navigate to a Page."); - return; + throw new Error("A Frame must be used to navigate to a Page."); } } diff --git a/ui/border/border.ts b/ui/border/border.ts index 6ef647bc8..441e59a8c 100644 --- a/ui/border/border.ts +++ b/ui/border/border.ts @@ -3,7 +3,7 @@ import contentView = require("ui/content-view"); import viewModule = require("ui/core/view"); import utils = require("utils/utils"); -@Deprecated +//@Deprecated export class Border extends contentView.ContentView implements definition.Border { get cornerRadius(): number { return this.borderRadius; diff --git a/ui/frame/frame-common.ts b/ui/frame/frame-common.ts index f76be238c..4c1f52d7a 100644 --- a/ui/frame/frame-common.ts +++ b/ui/frame/frame-common.ts @@ -196,6 +196,21 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { } } + public _isEntryBackstackVisible(entry: definition.BackstackEntry): boolean { + if (!entry) { + return false; + } + + var backstackVisibleValue = entry.entry.backstackVisible; + var backstackHidden = types.isDefined(backstackVisibleValue) && !backstackVisibleValue; + + return !backstackHidden; + } + + public _updateActionBar(page?: pages.Page) { + trace.write("calling _updateActionBar on Frame", trace.categories.Navigation); + } + private _processNavigationContext(navigationContext: NavigationContext) { if (navigationContext.isBackNavigation) { this.performGoBack(navigationContext); @@ -209,7 +224,7 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { var navContext = navigationContext.entry; this._onNavigatingTo(navContext); - if (this.currentPage) { + if (this._isEntryBackstackVisible(this._currentEntry)) { this._backStack.push(this._currentEntry); } diff --git a/ui/frame/frame.android.ts b/ui/frame/frame.android.ts index 68a9e4339..cf7da7cda 100644 --- a/ui/frame/frame.android.ts +++ b/ui/frame/frame.android.ts @@ -13,6 +13,8 @@ var OWNER = "_owner"; var HIDDEN = "_hidden"; var INTENT_EXTRA = "com.tns.activity"; var ANDROID_FRAME = "android_frame"; +var BACKSTAK_TAG = "_backstackTag"; +var NAV_DEPTH = "_navDepth"; var navDepth = 0; @@ -227,8 +229,10 @@ export class Frame extends frameCommon.Frame { var manager = activity.getFragmentManager(); var fragmentTransaction = manager.beginTransaction(); - var newFragmentTag = "fragment" + this.backStack.length; + var newFragmentTag = "fragment" + navDepth; var newFragment = new PageFragmentBody(this, backstackEntry); + backstackEntry[BACKSTAK_TAG] = newFragmentTag; + backstackEntry[NAV_DEPTH] = navDepth; // remember the fragment tag at page level so that we can retrieve the fragment associated with a Page instance backstackEntry.resolvedPage[TAG] = newFragmentTag; @@ -285,13 +289,15 @@ export class Frame extends frameCommon.Frame { trace.write("fragmentTransaction.commit();", trace.categories.NativeLifecycle); } - public _goBackCore(entry: definition.NavigationEntry) { - navDepth--; + public _goBackCore(backstackEntry: definition.BackstackEntry) { + navDepth = backstackEntry[NAV_DEPTH]; trace.write("Frame<" + this._domId + ">.fragmentTransaction POP depth = " + navDepth, trace.categories.Navigation); var manager = this._android.activity.getFragmentManager(); if (manager.getBackStackEntryCount() > 0) { - manager.popBackStack(); + // pop all other fragments up until the named one + // this handles cases where user may navigate to an inner page without adding it on the backstack + manager.popBackStack(backstackEntry[BACKSTAK_TAG], android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } } diff --git a/ui/frame/frame.d.ts b/ui/frame/frame.d.ts index e22e1d69f..c0aaf8274 100644 --- a/ui/frame/frame.d.ts +++ b/ui/frame/frame.d.ts @@ -89,6 +89,7 @@ declare module "ui/frame" { //@private _processNavigationQueue(page: pages.Page); + _updateActionBar(page?: pages.Page); //@endprivate /** @@ -144,6 +145,12 @@ declare module "ui/frame" { * True to navigate to the new Page using animated transitions, false otherwise. */ animated?: boolean; + + /** + * True to record the navigation in the backstack, false otherwise. + * If the parameter is set to false then the Page will be displayed but once navigated from it will not be able to be navigated back to. + */ + backstackVisible?: boolean; } /** diff --git a/ui/frame/frame.ios.ts b/ui/frame/frame.ios.ts index 6c0180b18..d34e25b53 100644 --- a/ui/frame/frame.ios.ts +++ b/ui/frame/frame.ios.ts @@ -10,14 +10,16 @@ import types = require("utils/types"); global.moduleMerge(frameCommon, exports); var ENTRY = "_entry"; +var PREV_ENTRY = "_prevEntry"; +var NAV_DEPTH = "_navDepth"; -var navDepth = 0; +var navDepth = -1; export class Frame extends frameCommon.Frame { private _ios: iOSFrame; private _paramToNavigate: any; - public _shouldSkipNativePop: boolean = false; public _navigateToEntry: definition.BackstackEntry; + public _goBackScheduled: boolean; constructor() { super(); @@ -48,31 +50,54 @@ export class Frame extends frameCommon.Frame { throw new Error("Required page does have an viewController created."); } + navDepth++; + var animated = false; if (this.currentPage) { animated = this._getIsAnimatedNavigation(backstackEntry.entry); } - this.updateNavigationBar(); - + backstackEntry[NAV_DEPTH] = navDepth; viewController[ENTRY] = backstackEntry; - - navDepth++; + this._navigateToEntry = backstackEntry; + trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); + + this._updateActionBar(backstackEntry.resolvedPage); this._ios.controller.pushViewControllerAnimated(viewController, animated); } - public _goBackCore(entry: definition.NavigationEntry) { - navDepth--; + public _goBackCore(backstackEntry: definition.BackstackEntry) { + navDepth = backstackEntry[NAV_DEPTH]; trace.write("Frame<" + this._domId + ">.popViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); - if (!this._shouldSkipNativePop) { - this._ios.controller.popViewControllerAnimated(this._getIsAnimatedNavigation(entry)); + + var controller = backstackEntry.resolvedPage.ios; + var animated = this._getIsAnimatedNavigation(backstackEntry.entry); + this._navigateToEntry = backstackEntry; + //this._updateActionBar(backstackEntry.resolvedPage); + this._ios.controller.popToViewControllerAnimated(controller, animated); + } + + public _updateActionBar(page?: pages.Page): void { + super._updateActionBar(page); + + var previousValue = !!this._ios.showNavigationBar; + var page = page || this.currentPage; + var newValue = this._getNavBarVisible(page); + + this._ios.showNavigationBar = newValue; + if (previousValue !== newValue) { + this.requestLayout(); } } - public updateNavigationBar(page?: pages.Page): void { - var previousValue = !!this._ios.showNavigationBar; - var newValue: boolean = false; + public _getNavBarVisible(page: pages.Page) { + if (!page) { + return false; + } + + var newValue = false; + switch (this._ios.navBarVisibility) { case enums.NavigationBarVisibility.always: newValue = true; @@ -83,21 +108,17 @@ export class Frame extends frameCommon.Frame { break; case enums.NavigationBarVisibility.auto: - var pageInstance: pages.Page = page || this.currentPage; - if (pageInstance && types.isDefined(pageInstance.actionBarHidden)) { - newValue = !pageInstance.actionBarHidden; + if (page && types.isDefined(page.actionBarHidden)) { + newValue = !page.actionBarHidden; } else { - newValue = this.backStack.length > 0 || (pageInstance && !pageInstance.actionBar._isEmpty()); + newValue = this.backStack.length > 0 || (page && !page.actionBar._isEmpty()); } newValue = !!newValue; // Make sure it is boolean break; } - this._ios.showNavigationBar = newValue; - if (previousValue !== newValue) { - this.requestLayout(); - } + return !!newValue; } public get ios(): definition.iOSFrame { @@ -156,20 +177,132 @@ export class Frame extends frameCommon.Frame { } } +class UINavigationBarImpl extends UINavigationBar { + public static ObjCProtocols = [UINavigationBarDelegate]; + + static new(): UINavigationBarImpl { + return super.new(); + } + + private _originalDelegate: UINavigationBarDelegate; + + get controller(): UINavigationControllerImpl { + return this._originalDelegate; + } + + get ownerFrame(): Frame { + return this.controller.owner; + } + + get delegate() { + return this; + } + set delegate(value: UINavigationBarDelegate) { + this._originalDelegate = value; + (this).super.delegate = this; + } + + public setItemsAnimated(items: NSArray, animated: boolean) { + if (items) { + trace.write("updating navigation bar items stack; original items count: " + items.count, trace.categories.Navigation); + + items = this.controller._getNavBarItems(); + + trace.write("updated navigation bar items stack; new items count: " + items.count, trace.categories.Navigation); + } + + super.setItemsAnimated(items, animated); + } + + public navigationBarShouldPopItem(navBar: UINavigationBar, item: UINavigationItem): boolean { + // should pop will be called only when the NavigationBar is visible and in the following cases: + // 1. The user has pressed the back button - we need to manually call 'goBack' on the Frame + // 2. The user has programmatically called goBack on the Frame - in this case the '_popScheduled' flag is set to true + if (!this.controller._popScheduled) { + this.ownerFrame.goBack(); + } + return true; + } + + public navigationBarShouldPushItem(navBar: UINavigationBar, item: UINavigationItem): boolean { + var entry = this.ownerFrame._navigateToEntry || this.ownerFrame._currentEntry; + if (!entry) { + return true; + } + + var prevEntry: definition.BackstackEntry = entry[PREV_ENTRY]; + if (!prevEntry) { + return true; + } + + return this.ownerFrame._isEntryBackstackVisible(prevEntry); + } + + //private _callBaseDelegateMethod() { + // //var controller = UINavigationController.alloc().init(); + // //var methodSignature = controller.methodSignatureForSelector("navigationBar:shouldPopItem:"); + // //if (methodSignature != null) { + // // var invocation = NSInvocation.invocationWithMethodSignature(methodSignature); + // // invocation.target = controller; + // // invocation.selector = "navigationBar:shouldPopItem:"; + + // // invocation.setArgumentAtIndex(new interop.Reference(UINavigationBar, UINavigationBar.alloc().init()), 2); + // // invocation.setArgumentAtIndex(new interop.Reference(UINavigationItem, UINavigationItem.alloc().init()), 3); + + // // invocation.invoke(); + + // // var result = new interop.Reference(interop.types.bool, false); + // // invocation.getReturnValue(result); + + // // console.log(result.value); + // //} + //} +} + class UINavigationControllerImpl extends UINavigationController implements UINavigationControllerDelegate { public static ObjCProtocols = [UINavigationControllerDelegate]; static new(): UINavigationControllerImpl { - return super.new(); + return new UINavigationControllerImpl(UINavigationBarImpl.class(), null); } private _owner: Frame; + public _popScheduled; public initWithOwner(owner: Frame): UINavigationControllerImpl { this._owner = owner; return this; } + public _getNavBarItems(): NSArray { + var frame = this._owner; + var backstack = frame.backStack; + var length = backstack.length; + var entry: definition.BackstackEntry; + + var navItems = NSMutableArray.alloc().init(); + for (let i = 0; i < length; i++) { + entry = backstack[i]; + var controller: UIViewController = entry.resolvedPage.ios; + navItems.addObject(controller.navigationItem); + } + + // add the top controller + entry = frame._navigateToEntry || frame._currentEntry; + if (entry) { + var topController = entry.resolvedPage.ios; + if (topController) { + navItems.addObject(topController.navigationItem); + } + } + + return navItems; + } + + get owner(): Frame { + return this._owner; + } + public viewDidLoad(): void { this.view.autoresizesSubviews = false; this.view.autoresizingMask = UIViewAutoresizing.UIViewAutoresizingNone; @@ -181,65 +314,91 @@ class UINavigationControllerImpl extends UINavigationController implements UINav this._owner._updateLayout(); } - public navigationControllerWillShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { - // In this method we need to layout the new page otherwise page will be shown empty and update after that which is bad UX. - var frame = this._owner; + public popToViewControllerAnimated(controller: UIViewController, animated: boolean): NSArray { + // our _goBackCore routine uses popToViewController, hence it is granted this method is called through the navbar back button click + this._popScheduled = true; + return super.popToViewControllerAnimated(controller, animated); + } + + navigationControllerWillShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { + //// In this method we need to layout the new page otherwise page will be shown empty and update after that which is bad UX. + //var frame = this._owner; + //var newEntry: definition.BackstackEntry = viewController[ENTRY]; + //var newPage = newEntry.resolvedPage; + //if (!newPage.parent) { + // if (!frame._currentEntry) { + // // First navigation + // frame._currentEntry = newEntry; + // } + // else { + // frame._navigateToEntry = newEntry; + // } + + // frame._addView(newPage); + //} + //else if (newPage.parent !== frame) { + // throw new Error("Page is already shown on another frame."); + //} + + //newPage.actionBar.update(); + var currEntry = this._owner._currentEntry; + if (currEntry) { + this._owner._removeView(currEntry.resolvedPage); + //delete currEntry[PREV_ENTRY]; + } + var newEntry: definition.BackstackEntry = viewController[ENTRY]; var newPage = newEntry.resolvedPage; - if (!newPage.parent) { - if (!frame._currentEntry) { - // First navigation - frame._currentEntry = newEntry; - } - else { - frame._navigateToEntry = newEntry; - } - frame._addView(newPage); - } - else if (newPage.parent !== frame) { - throw new Error("Page is already shown on another frame."); - } + newEntry[PREV_ENTRY] = currEntry; + this._owner._currentEntry = newEntry; + this._owner._navigateToEntry = undefined; + this._owner._addView(newEntry.resolvedPage); + this._owner._updateActionBar(newEntry.resolvedPage); + newPage.onNavigatedTo(); newPage.actionBar.update(); } - public navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { + navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { + this._popScheduled = false; + var entry: definition.BackstackEntry = viewController[ENTRY]; + this._owner._processNavigationQueue(entry.resolvedPage); - var frame: Frame = this._owner; - var backStack = frame.backStack; - var currentEntry = backStack.length > 0 ? backStack[backStack.length - 1] : null; - var newEntry: definition.BackstackEntry = viewController[ENTRY]; + //var frame: Frame = this._owner; + //var backStack = frame.backStack; + //var currentEntry = backStack.length > 0 ? backStack[backStack.length - 1] : null; + //var newEntry: definition.BackstackEntry = viewController[ENTRY]; - // This code check if navigation happened through UI (e.g. back button or swipe gesture). - // When calling goBack on frame isBack will be false. - var isBack: boolean = currentEntry && newEntry === currentEntry; - if (isBack) { - try { - frame._shouldSkipNativePop = true; - frame.goBack(); - } - finally { - frame._shouldSkipNativePop = false; - } - } + //// This code check if navigation happened through UI (e.g. back button or swipe gesture). + //// When calling goBack on frame isBack will be false. + //var isBack: boolean = currentEntry && newEntry === currentEntry; + //if (isBack) { + // try { + // frame._shouldSkipNativePop = true; + // frame.goBack(); + // } + // finally { + // frame._shouldSkipNativePop = false; + // } + //} - var page = frame.currentPage; - if (page && !navigationController.viewControllers.containsObject(page.ios)) { - frame._removeView(page); - } + //var page = frame.currentPage; + //if (page && !navigationController.viewControllers.containsObject(page.ios)) { + // frame._removeView(page); + //} - frame._navigateToEntry = null; - frame._currentEntry = newEntry; - frame.updateNavigationBar(); + //frame._navigateToEntry = null; + //frame._currentEntry = newEntry; + //frame.updateNavigationBar(); - frame.ios.controller.navigationBar.backIndicatorImage + //frame.ios.controller.navigationBar.backIndicatorImage - var newPage = newEntry.resolvedPage; + //var newPage = newEntry.resolvedPage; - // notify the page - newPage.onNavigatedTo(); - frame._processNavigationQueue(newPage); + //// notify the page + //newPage.onNavigatedTo(); + //frame._processNavigationQueue(newPage); } public supportedInterfaceOrientation(): number { @@ -258,7 +417,7 @@ class iOSFrame implements definition.iOSFrame { this._controller = UINavigationControllerImpl.new().initWithOwner(owner); this._controller.delegate = this._controller; this._controller.automaticallyAdjustsScrollViewInsets = false; - this.showNavigationBar = false; + //this.showNavigationBar = false; this._navBarVisibility = enums.NavigationBarVisibility.auto; } diff --git a/ui/page/page.ios.ts b/ui/page/page.ios.ts index f79348e85..bd8e081d0 100644 --- a/ui/page/page.ios.ts +++ b/ui/page/page.ios.ts @@ -3,7 +3,6 @@ import definition = require("ui/page"); import viewModule = require("ui/core/view"); import trace = require("trace"); import utils = require("utils/utils"); -import types = require("utils/types"); global.moduleMerge(pageCommon, exports); @@ -149,9 +148,9 @@ export class Page extends pageCommon.Page { } public _updateActionBar(hidden: boolean) { - if (types.isDefined(hidden) && this.ios.navigationController.navigationBarHidden !== hidden) { - this.ios.navigationController.navigationBarHidden = hidden; - this.requestLayout(); + var frame = this.frame; + if (frame) { + frame._updateActionBar(this); } }