From 2c4781db013fe30db1a9de2d224c02cb56bb63e0 Mon Sep 17 00:00:00 2001 From: atanasovg Date: Thu, 12 Jun 2014 17:37:55 +0300 Subject: [PATCH] Initial prototype of Frame + Page + Navigation. --- BCL.csproj | 36 ++++-- application/application.android.ts | 18 ++- application/application.d.ts | 10 +- declarations.android.d.ts | 1 + ui/core/bindable.ts | 5 + ui/core/index.ts | 1 - ui/core/observable.ts | 7 ++ ui/core/proxy.ts | 1 + ui/core/view.android.ts | 11 -- ui/core/view.d.ts | 7 -- ui/core/view.ios.ts | 11 -- ui/core/view.ts | 99 +++++++++++++++ ui/frame/frame-common.ts | 114 ++++++++++++++++++ ui/frame/frame.android.ts | 35 ++++++ ui/frame/frame.d.ts | 24 ++++ .../text-input.d.ts => frame/frame.ios.ts} | 0 ui/frame/index.ts | 2 + ui/label/label.android.ts | 67 ++++++---- ui/pages/index.ts | 2 + ui/pages/page-common.ts | 36 ++++++ ui/pages/page.android.ts | 81 +++++++++++++ ui/pages/page.d.ts | 25 ++++ ui/{text-input => text-field}/index.ts | 0 .../text-field.android.ts} | 6 +- .../text-field.d.ts} | 0 ui/text-field/text-field.ios.ts | 1 + 26 files changed, 520 insertions(+), 80 deletions(-) delete mode 100644 ui/core/index.ts delete mode 100644 ui/core/view.android.ts delete mode 100644 ui/core/view.d.ts delete mode 100644 ui/core/view.ios.ts create mode 100644 ui/core/view.ts create mode 100644 ui/frame/frame-common.ts create mode 100644 ui/frame/frame.android.ts create mode 100644 ui/frame/frame.d.ts rename ui/{text-input/text-input.d.ts => frame/frame.ios.ts} (100%) create mode 100644 ui/frame/index.ts create mode 100644 ui/pages/index.ts create mode 100644 ui/pages/page-common.ts create mode 100644 ui/pages/page.android.ts create mode 100644 ui/pages/page.d.ts rename ui/{text-input => text-field}/index.ts (100%) rename ui/{text-input/text-input.android.ts => text-field/text-field.android.ts} (83%) rename ui/{text-input/text-input.ios.ts => text-field/text-field.d.ts} (100%) create mode 100644 ui/text-field/text-field.ios.ts diff --git a/BCL.csproj b/BCL.csproj index e88f9728c..cfe0edb89 100644 --- a/BCL.csproj +++ b/BCL.csproj @@ -153,14 +153,18 @@ text.d.ts - - - view.d.ts + + frame.d.ts - - view.d.ts + + frame.d.ts + + + frame.d.ts + + image.d.ts @@ -177,14 +181,22 @@ label.d.ts - - - text-input.d.ts + + + page.d.ts - - text-input.d.ts + + page.d.ts + + + + + text-field.d.ts + + + + text-field.d.ts - @@ -247,7 +259,7 @@ - + dialogs.d.ts diff --git a/application/application.android.ts b/application/application.android.ts index bdf01d7ee..45cee78f1 100644 --- a/application/application.android.ts +++ b/application/application.android.ts @@ -1,5 +1,6 @@ import appModule = require("application/application-common"); import dts = require("application"); +import frame = require("ui/frame"); // merge the exports of the application_common file with the exports of this file declare var exports; @@ -112,7 +113,7 @@ class AndroidApplication implements dts.AndroidApplication { public foregroundActivity: android.app.Activity; public startActivity: android.app.Activity; public packageName: string; - public getActivity: (intent: android.content.Intent) => any; + // public getActivity: (intent: android.content.Intent) => any; public onActivityCreated: (activity: android.app.Activity, bundle: android.os.Bundle) => void; public onActivityDestroyed: (activity: android.app.Activity) => void; @@ -128,7 +129,15 @@ class AndroidApplication implements dts.AndroidApplication { this.nativeApp = nativeApp; this.packageName = nativeApp.getPackageName(); this.context = nativeApp.getApplicationContext(); - this.getActivity = undefined; + } + + public getActivity(intent: android.content.Intent): any { + var currentPage = rootFrame.currentPage; + if (!currentPage) { + throw new Error("Root frame not navigated to a page."); + } + + return currentPage.android.getActivityExtends(); } public init() { @@ -136,4 +145,7 @@ class AndroidApplication implements dts.AndroidApplication { this.nativeApp.registerActivityLifecycleCallbacks(this._eventsToken); this.context = this.nativeApp.getApplicationContext(); } -} \ No newline at end of file +} + +// The root frame of the application +export var rootFrame = new frame.Frame(); \ No newline at end of file diff --git a/application/application.d.ts b/application/application.d.ts index 7d301d610..89bf5b0d7 100644 --- a/application/application.d.ts +++ b/application/application.d.ts @@ -1,11 +1,13 @@  declare module "application" { + import frame = require("ui/frame"); + + export var rootFrame: frame.Frame; /** - * The main entry point event. This method is expected to return an instance of the root UI for the application. - * This will be an Activity extends for Android and a RootViewController for iOS. + * The main entry point event. This method is expected to use the root frame to navigate to the main application page. */ - export function onLaunch(): any; + export function onLaunch(): void; /** * This method will be called when the Application is suspended. @@ -85,7 +87,7 @@ declare module "application" { * This method is called by the JavaScript Bridge when navigation to a new activity is triggered. * The return value of this method should be com.tns.NativeScriptActivity.extends implementation. */ - getActivity: (intent: android.content.Intent) => any; + getActivity(intent: android.content.Intent): any; /** * Direct handler of the android.app.Application.ActivityLifecycleCallbacks.onActivityCreated method. diff --git a/declarations.android.d.ts b/declarations.android.d.ts index f3d6acb33..9201d3e57 100644 --- a/declarations.android.d.ts +++ b/declarations.android.d.ts @@ -338,6 +338,7 @@ declare module android { } */ + declare var app; declare var telerik; declare var gc: () => any; diff --git a/ui/core/bindable.ts b/ui/core/bindable.ts index 3928ffd32..081ca39f0 100644 --- a/ui/core/bindable.ts +++ b/ui/core/bindable.ts @@ -37,6 +37,11 @@ export class Bindable extends observable.Observable { } } + public setPropertyCore(data: observable.PropertyChangeData) { + super.setPropertyCore(data); + this.updateTwoWayBinding(data.propertyName, data.value); + } + public getBinding(propertyName: string) { return this._bindings[propertyName]; } diff --git a/ui/core/index.ts b/ui/core/index.ts deleted file mode 100644 index 5f282702b..000000000 --- a/ui/core/index.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/core/observable.ts b/ui/core/observable.ts index cde190fc4..f927b1fd2 100644 --- a/ui/core/observable.ts +++ b/ui/core/observable.ts @@ -31,6 +31,13 @@ export class Observable { private _trackChanging = false; constructor(body?: any) { + if (body) { + for (var key in body) { + // TODO: Is this correct + this[key] = body[key]; + } + } + this.on = this.addEventListener = this.addObserver; this.off = this.removeEventListener = this.removeObserver; } diff --git a/ui/core/proxy.ts b/ui/core/proxy.ts index 281b32aa8..736b17571 100644 --- a/ui/core/proxy.ts +++ b/ui/core/proxy.ts @@ -4,6 +4,7 @@ import bindable = require("ui/core/bindable"); export class ProxyObject extends bindable.Bindable { public setPropertyCore(data: observable.PropertyChangeData) { this.setNativeProperty(data); + this.updateTwoWayBinding(data.propertyName, data.value); } public setNativeProperty(data: observable.PropertyChangeData) { diff --git a/ui/core/view.android.ts b/ui/core/view.android.ts deleted file mode 100644 index d6666aaa0..000000000 --- a/ui/core/view.android.ts +++ /dev/null @@ -1,11 +0,0 @@ -import proxy = require("ui/core/proxy"); - -export class View extends proxy.ProxyObject { - public addToParent(parent: android.view.ViewGroup) { - var nativeInstance: android.view.View = this["android"]; - if (nativeInstance) { - // TODO: Check for existing parent - parent.addView(nativeInstance); - } - } -} \ No newline at end of file diff --git a/ui/core/view.d.ts b/ui/core/view.d.ts deleted file mode 100644 index 5e26b640d..000000000 --- a/ui/core/view.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import proxy = require("ui/core/proxy"); - -export declare class View extends proxy.ProxyObject { - public addToParent(parent: any); - android: any; - ios: any; -} \ No newline at end of file diff --git a/ui/core/view.ios.ts b/ui/core/view.ios.ts deleted file mode 100644 index 75dc1b331..000000000 --- a/ui/core/view.ios.ts +++ /dev/null @@ -1,11 +0,0 @@ -import proxy = require("ui/core/proxy"); - -export class View extends proxy.ProxyObject { - public addToParent(parent: UIKit.UIView) { - var nativeInstance: UIKit.UIView = this["ios"]; - if (nativeInstance) { - // TODO: Check for existing parent - parent.addSubview(nativeInstance); - } - } -} \ No newline at end of file diff --git a/ui/core/view.ts b/ui/core/view.ts new file mode 100644 index 000000000..1e47acfcb --- /dev/null +++ b/ui/core/view.ts @@ -0,0 +1,99 @@ +import proxy = require("ui/core/proxy"); +import application = require("application"); + +export class View extends proxy.ProxyObject { + private _parent: Panel; + + public onInitialized(content: android.content.Context) { + // TODO: This is used by Android, rethink this routine + } + + public addToParent(native: any) { + // TODO: Temporary + if (application.ios && this.ios) { + native.addSubview(this.ios); + } else if (application.android && this.android) { + native.addView(this.android); + } + } + + /** + * Gets the Panel instance that parents this view. This property is read-only. + */ + get parent(): Panel { + return this._parent; + } + + /** + * Gets the android-specific native instance that lies behind this view. Will be available if running on an Android platform. + */ + get android(): any { + return undefined; + } + + /** + * Gets the ios-specific native instance that lies behind this view. Will be available if running on an Android platform. + */ + get ios(): any { + return undefined; + } + + // TODO: Should these be public? + public onAddedToParent(parent: Panel) { + this._parent = parent; + // TODO: Attach to parent - e.g. update data context, bindings, styling, etc. + } + + public onRemovedFromParent() { + this._parent = null; + // TODO: Detach from parent. + } +} + +/** +* The Panel class represents an extended View which can have other views as children. +*/ +export class Panel extends View { + private _children: Array; + + constructor() { + super(); + + this._children = new Array(); + } + + public addChild(child: View) { + // Validate child is not parented + if (child.parent) { + var message; + if (child.parent === this) { + message = "View already added to this panel."; + } else { + message = "View is already a child of another panel."; + } + + throw new Error(message); + } + + this._children.push(child); + child.onAddedToParent(this); + } + + public removeChild(child: View) { + if (!child) { + return; + } + + if (child.parent !== this) { + throw new Error("View is not parented by this panel."); + } + + var index = this._children.indexOf(child); + if (index < 0) { + throw new Error("View not found in children collection."); + } + + this._children.splice(index, 1); + child.onRemovedFromParent(); + } +} \ No newline at end of file diff --git a/ui/frame/frame-common.ts b/ui/frame/frame-common.ts new file mode 100644 index 000000000..c97b362e8 --- /dev/null +++ b/ui/frame/frame-common.ts @@ -0,0 +1,114 @@ +import frame = require("ui/frame"); +import view = require("ui/core/view"); +import pages = require("ui/pages"); + +enum NavigationType { + New, + Back, + Forward +} + +export class Frame extends view.View implements frame.Frame { + private _backStack: Array; + private _forwardStack: Array; + private _currentEntry: frame.PageNavigationEntry; + private _currentPage: pages.Page; + private _navigationType: NavigationType; + + // TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity. + + constructor() { + super(); + + this._backStack = new Array(); + this._forwardStack = new Array(); + this._navigationType = NavigationType.New; + } + + public canGoBack(): boolean { + return this._backStack.length > 0; + } + + public canGoForward(): boolean { + return this._forwardStack.length > 0; + } + + public goBack() { + if (!this.canGoBack()) { + // TODO: Do we need to throw an error? + return; + } + + var entry = this._backStack.pop(); + this._navigationType = NavigationType.Back; + this.navigate(entry); + } + + public goForward() { + if (!this.canGoForward()) { + // TODO: Do we need to throw an error? + return; + } + + var entry = this._forwardStack.pop(); + this._navigationType = NavigationType.Forward; + this.navigate(entry); + } + + public navigate(entry: frame.PageNavigationEntry) { + if (this._currentPage) { + this._backStack.push(this._currentEntry); + } + + // perform the actual navigation, depending on the requested navigation type + switch (this._navigationType) { + case NavigationType.New: + this.navigateCore(entry.context); + break; + case NavigationType.Back: + this.goBackCore(); + if (this._currentPage) { + this._forwardStack.push(this._currentEntry); + } + break; + case NavigationType.Forward: + this.goForwardCore(); + if (this._currentPage) { + this._backStack.push(this._currentEntry); + } + break; + } + + // TODO: We assume here that there is a Page object in the exports of the required module. This should be well documented. + this._currentPage = require(entry.pageModuleName).Page; + this._currentPage.frame = this; + this._currentEntry = entry; + + // notify the page + this._currentPage.onNavigatedTo(entry.context); + + // reset the navigation type back to new + this._navigationType = NavigationType.New; + } + + public goBackCore() { + } + + public goForwardCore() { + } + + public navigateCore(context: any) { + } + + get backStack(): Array { + return this._backStack; + } + + get forwardStack(): Array { + return this._forwardStack; + } + + get currentPage(): pages.Page { + return this._currentPage; + } +} \ No newline at end of file diff --git a/ui/frame/frame.android.ts b/ui/frame/frame.android.ts new file mode 100644 index 000000000..850f1624f --- /dev/null +++ b/ui/frame/frame.android.ts @@ -0,0 +1,35 @@ +import frameCommon = require("ui/frame/frame-common"); +import frame = require("ui/frame"); +import pages = require("ui/pages"); +import application = require("application"); + +export class Frame extends frameCommon.Frame { + public navigateCore(context: any) { + if (this.backStack.length === 0) { + // When navigating for the very first time we do not want to start an activity + // TODO: Revisit/polish this behavior + return; + } + + var activity = this.currentPage.android.activity; + if (!activity) { + throw new Error("Current page does have an activity created."); + } + + var intent = new android.content.Intent(activity, (com).tns.NativeScriptActivity.class); + activity.startActivity(intent); + } + + public goBackCore() { + var activity = this.currentPage.android.activity; + if (!activity) { + throw new Error("Current page does have an activity created."); + } + + // TODO: This is not true in all cases, update once added support for parent activity in Android manifest + activity.finish(); + } + + public goForwardCore() { + } +} \ No newline at end of file diff --git a/ui/frame/frame.d.ts b/ui/frame/frame.d.ts new file mode 100644 index 000000000..b3f60ddc4 --- /dev/null +++ b/ui/frame/frame.d.ts @@ -0,0 +1,24 @@ +declare module "ui/frame" { + import view = require("ui/core/view"); + + // There is a cyclic reference here (pages module requires frame) but it is intented and needed. + import pages = require("ui/pages"); + + export class Frame extends view.View { + goBack(); + canGoBack(): boolean; + goForward(); + canGoForward(): boolean; + navigate(entry: PageNavigationEntry); + + currentPage: pages.Page; + + backStack: Array; + forwardStack: Array; + } + + export interface PageNavigationEntry { + pageModuleName: string; + context?: any; + } +} \ No newline at end of file diff --git a/ui/text-input/text-input.d.ts b/ui/frame/frame.ios.ts similarity index 100% rename from ui/text-input/text-input.d.ts rename to ui/frame/frame.ios.ts diff --git a/ui/frame/index.ts b/ui/frame/index.ts new file mode 100644 index 000000000..02aeb5f98 --- /dev/null +++ b/ui/frame/index.ts @@ -0,0 +1,2 @@ +declare var module, require; +module.exports = require("ui/frame/frame"); \ No newline at end of file diff --git a/ui/label/label.android.ts b/ui/label/label.android.ts index 54cf62b83..8026b28b9 100644 --- a/ui/label/label.android.ts +++ b/ui/label/label.android.ts @@ -2,36 +2,23 @@ import view = require("ui/core/view"); import application = require("application"); +var TEXT = "text"; + +// this is the name of the property to store text locally until attached to a valid Context +var TEXTPRIVATE = "_text"; + export class Label extends view.View { - private static textProperty = "text"; private _android: android.widget.TextView; constructor() { super(); + } - // TODO: Verify that this is always true - var context = application.android.currentContext; - if (!context) { - // TODO: Delayed loading? + public onInitialized(context: android.content.Context) { + if (!this._android) { + // TODO: We need to decide whether we will support context switching and if yes - to implement it. + this.createUI(context); } - - this._android = new android.widget.TextView(context); - - var that = this; - var textWatcher = new android.text.TextWatcher({ - beforeTextChanged: function (text: string, start: number, count: number, after: number) { - }, - onTextChanged: function (text: string, start: number, before: number, count: number) { - }, - afterTextChanged: function (editable: android.text.IEditable) { - //if (that.hasObservers(observable.Observable.propertyChangeEvent)) { - // var data = that.createPropertyChangeData(TextView.textProperty, that.text); - // that.notify(data); - //} - that.updateTwoWayBinding("text", editable.toString()); - } - }); - this._android.addTextChangedListener(textWatcher); } get android(): android.widget.TextView { @@ -39,17 +26,45 @@ export class Label extends view.View { } get text(): string { + if (!this._android) { + return this[TEXTPRIVATE]; + } return this._android.getText().toString(); } set text(value: string) { - this.setProperty(Label.textProperty, value); + this.setProperty(TEXT, value); } public setNativeProperty(data: observable.PropertyChangeData) { // TODO: Will this be a gigantic if-else switch? - if (data.propertyName === Label.textProperty) { - this._android.setText(data.value); + if (data.propertyName === TEXT) { + if (this._android) { + this._android.setText(data.value); + } else { + this[TEXTPRIVATE] = data.value; + } } else if (true) { } } + + private createUI(context: android.content.Context) { + this._android = new android.widget.TextView(context); + if (this[TEXTPRIVATE]) { + this._android.setText(this[TEXTPRIVATE]); + delete this[TEXTPRIVATE]; + } + + // TODO: Do we need to listen for text change here? + //var that = this; + //var textWatcher = new android.text.TextWatcher({ + // beforeTextChanged: function (text: string, start: number, count: number, after: number) { + // }, + // onTextChanged: function (text: string, start: number, before: number, count: number) { + // }, + // afterTextChanged: function (editable: android.text.IEditable) { + // that.updateTwoWayBinding("text", editable.toString()); + // } + //}); + //this._android.addTextChangedListener(textWatcher); + } } \ No newline at end of file diff --git a/ui/pages/index.ts b/ui/pages/index.ts new file mode 100644 index 000000000..c8940c55a --- /dev/null +++ b/ui/pages/index.ts @@ -0,0 +1,2 @@ +declare var module, require; +module.exports = require("ui/pages/page"); \ No newline at end of file diff --git a/ui/pages/page-common.ts b/ui/pages/page-common.ts new file mode 100644 index 000000000..2e854a562 --- /dev/null +++ b/ui/pages/page-common.ts @@ -0,0 +1,36 @@ +import view = require("ui/core/view"); +import dts = require("ui/pages"); +import frame = require("ui/frame"); +import application = require("application"); + +export class Page extends view.View implements dts.Page { + private _contentView: view.View; + private _frame: frame.Frame; + private _navigationContext: any; + + public onLoaded: () => any; + + get contentView(): view.View { + return this._contentView; + } + set contentView(value: view.View) { + this._contentView = value; + + // TODO: Check if page is already loaded and update as needed + } + + get frame(): frame.Frame { + return this._frame; + } + set frame(value: frame.Frame) { + // TODO: This method is called internally, check how to hide the setter from users. + this._frame = value; + } + + public onNavigatedTo(context: any) { + this._navigationContext = context; + } + + public onNavigatedFrom() { + } +} \ No newline at end of file diff --git a/ui/pages/page.android.ts b/ui/pages/page.android.ts new file mode 100644 index 000000000..9f3bdaf90 --- /dev/null +++ b/ui/pages/page.android.ts @@ -0,0 +1,81 @@ +import definition = require("ui/pages"); +import pageCommon = require("ui/pages/page-common"); + +export class Page extends pageCommon.Page { + private _android: AndroidPage; + + constructor() { + super(); + + this._android = new AndroidPage(this); + } + + get android(): definition.AndroidPage { + return this._android; + } +} + +class AndroidPage implements definition.AndroidPage { + private _ownerPage: definition.Page; + private _body: any; + private _activityExtends: any; + private _activity: android.app.Activity; + + constructor(ownerPage: definition.Page) { + this._ownerPage = ownerPage; + } + + get activity(): android.app.Activity { + return this._activity; + } + + get activityBody(): any { + return this._body; + } + set activityBody(value: any) { + if (this._activityExtends) { + throw new Error("Activity already loaded and its body may not be changed."); + } + + this._body = value; + } + + public getActivityExtends(): any { + if (!this._body) { + this.rebuildBody(); + } + + return this._activityExtends; + } + + public resetBody() { + this._body = null; + this._activityExtends = null; + } + + private rebuildBody() { + var that = this; + this._body = { + onCreate: function () { + that._activity = this; + this.super.onCreate(null); + + var view = that._ownerPage.contentView; + if (view) { + // TODO: Notify the entire visual tree for being initialized + view.onInitialized(that._activity); + that._activity.setContentView(view.android); + } + } + } + + if (this._ownerPage.onLoaded) { + this._body.onStart = function () { + this.super.onStart(); + that._ownerPage.onLoaded(); + } + } + + this._activityExtends = (com).tns.NativeScriptActivity.extends(this._body); + } +} \ No newline at end of file diff --git a/ui/pages/page.d.ts b/ui/pages/page.d.ts new file mode 100644 index 000000000..94c3ca1f2 --- /dev/null +++ b/ui/pages/page.d.ts @@ -0,0 +1,25 @@ +declare module "ui/pages" { + import view = require("ui/core/view"); + import frame = require("ui/frame"); + + export class Page extends view.View { + contentView: view.View; + android: AndroidPage; + + /** + * Gets the Frame object controlling this instance. + */ + frame: frame.Frame; + + onNavigatedTo(context: any): void; + onNavigatedFrom(): void; + + onLoaded: () => void; + } + + export interface AndroidPage { + activity: android.app.Activity; + activityBody: any; + getActivityExtends(): any; + } +} \ No newline at end of file diff --git a/ui/text-input/index.ts b/ui/text-field/index.ts similarity index 100% rename from ui/text-input/index.ts rename to ui/text-field/index.ts diff --git a/ui/text-input/text-input.android.ts b/ui/text-field/text-field.android.ts similarity index 83% rename from ui/text-input/text-input.android.ts rename to ui/text-field/text-field.android.ts index 5ca064487..92c307bcb 100644 --- a/ui/text-input/text-input.android.ts +++ b/ui/text-field/text-field.android.ts @@ -20,11 +20,7 @@ export class TextInput extends view.View { onTextChanged: function (text: string, start: number, before: number, count: number) { }, afterTextChanged: function (editable: android.text.IEditable) { - //if (that.hasObservers(observable.Observable.propertyChangeEvent)) { - // var data = that.createPropertyChangeData(TextView.textProperty, that.text); - // that.notify(data); - //} - that.updateTwoWayBinding("text", editable.toString()); + that.updateTwoWayBinding(TextInput.textProperty, editable.toString()); } }); this._android.addTextChangedListener(textWatcher); diff --git a/ui/text-input/text-input.ios.ts b/ui/text-field/text-field.d.ts similarity index 100% rename from ui/text-input/text-input.ios.ts rename to ui/text-field/text-field.d.ts diff --git a/ui/text-field/text-field.ios.ts b/ui/text-field/text-field.ios.ts new file mode 100644 index 000000000..19a76f130 --- /dev/null +++ b/ui/text-field/text-field.ios.ts @@ -0,0 +1 @@ + \ No newline at end of file