From 1be0f9242966d5c02a3aa5214a70125723773474 Mon Sep 17 00:00:00 2001 From: atanasovg Date: Thu, 5 Jun 2014 13:53:09 +0300 Subject: [PATCH] Initial implementation of Observable + Bindable + Label --- BCL.csproj | 30 ++++++-- Tests/application-tests.android.ts | 4 +- application/application.android.ts | 24 ++++-- application/application.d.ts | 7 +- ui/core/bindable.ts | 115 ++++++++++++++++++++++++++++ ui/core/index.ts | 1 + ui/core/observable.ts | 87 ++++++++++++--------- ui/core/proxy.ts | 10 ++- ui/core/view.android.ts | 11 +++ ui/core/view.d.ts | 5 ++ ui/core/view.ios.ts | 1 + ui/label/index.ts | 2 + ui/label/label.android.ts | 55 +++++++++++++ ui/label/label.d.ts | 5 ++ ui/label/label.ios.ts | 5 ++ ui/text-input/index.ts | 2 + ui/text-input/text-input.android.ts | 51 ++++++++++++ ui/text-input/text-input.d.ts | 1 + ui/text-input/text-input.ios.ts | 1 + 19 files changed, 359 insertions(+), 58 deletions(-) create mode 100644 ui/core/bindable.ts create mode 100644 ui/core/index.ts create mode 100644 ui/core/view.android.ts create mode 100644 ui/core/view.d.ts create mode 100644 ui/core/view.ios.ts create mode 100644 ui/label/index.ts create mode 100644 ui/label/label.android.ts create mode 100644 ui/label/label.d.ts create mode 100644 ui/label/label.ios.ts create mode 100644 ui/text-input/index.ts create mode 100644 ui/text-input/text-input.android.ts create mode 100644 ui/text-input/text-input.d.ts create mode 100644 ui/text-input/text-input.ios.ts diff --git a/BCL.csproj b/BCL.csproj index b5e8216d0..9620dbb86 100644 --- a/BCL.csproj +++ b/BCL.csproj @@ -152,15 +152,31 @@ text.d.ts + + - - text-view.d.ts + + view.d.ts - - - - text-view.d.ts + + view.d.ts + + + + label.d.ts + + + label.d.ts + + + + text-input.d.ts + + + text-input.d.ts + + @@ -223,7 +239,7 @@ - + diff --git a/Tests/application-tests.android.ts b/Tests/application-tests.android.ts index b5c1f1e35..15113d7f9 100644 --- a/Tests/application-tests.android.ts +++ b/Tests/application-tests.android.ts @@ -20,7 +20,7 @@ var dir = context.getFilesDir(); // ``` // Tracking the current Activity // ``` JavaScript -if (androidApp.currentActivity === androidApp.startActivity) { +if (androidApp.foregroundActivity === androidApp.foregroundActivity) { //// We are currently in the main (start) activity of the application } // ``` @@ -29,7 +29,7 @@ if (androidApp.currentActivity === androidApp.startActivity) { export var testAndroidApplicationInitialized = function () { TKUnit.assert(app.android, "Android application not initialized."); TKUnit.assert(app.android.context, "Android context not initialized."); - TKUnit.assert(app.android.currentActivity, "Android currentActivity not initialized."); + TKUnit.assert(app.android.foregroundActivity, "Android currentActivity not initialized."); TKUnit.assert(app.android.startActivity, "Android startActivity not initialized."); TKUnit.assert(app.android.nativeApp, "Android nativeApp not initialized."); TKUnit.assert(app.android.packageName, "Android packageName not initialized."); diff --git a/application/application.android.ts b/application/application.android.ts index b8e80f99c..bdf01d7ee 100644 --- a/application/application.android.ts +++ b/application/application.android.ts @@ -1,4 +1,5 @@ import appModule = require("application/application-common"); +import dts = require("application"); // merge the exports of the application_common file with the exports of this file declare var exports; @@ -21,11 +22,17 @@ var initEvents = function () { androidApp.onActivityCreated(activity, bundle); } } + + androidApp.currentContext = activity; }, onActivityDestroyed: function (activity: any) { // Clear the current activity reference to prevent leak - if (activity === androidApp.currentActivity) { - androidApp.currentActivity = undefined; + if (activity === androidApp.foregroundActivity) { + androidApp.foregroundActivity = undefined; + } + + if (activity === androidApp.currentContext) { + androidApp.currentContext = undefined; } if (activity === androidApp.startActivity) { @@ -42,7 +49,7 @@ var initEvents = function () { gc(); }, onActivityPaused: function (activity: any) { - if (activity === androidApp.currentActivity) { + if (activity === androidApp.foregroundActivity) { if (exports.onSuspend) { exports.onSuspend(); } @@ -53,7 +60,7 @@ var initEvents = function () { } }, onActivityResumed: function (activity: any) { - if (activity === androidApp.currentActivity) { + if (activity === androidApp.foregroundActivity) { if (exports.onResume) { exports.onResume(); } @@ -69,7 +76,7 @@ var initEvents = function () { } }, onActivityStarted: function (activity: any) { - androidApp.currentActivity = activity; + androidApp.foregroundActivity = activity; if (androidApp.onActivityStarted) { androidApp.onActivityStarted(activity); @@ -98,12 +105,14 @@ export var init = function (nativeApp: android.app.Application) { initialized = true; } -class AndroidApplication { +class AndroidApplication implements dts.AndroidApplication { public nativeApp: android.app.Application; public context: android.content.Context; - public currentActivity: android.app.Activity; + public currentContext: android.content.Context; + public foregroundActivity: android.app.Activity; public startActivity: android.app.Activity; public packageName: string; + 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; @@ -119,6 +128,7 @@ class AndroidApplication { this.nativeApp = nativeApp; this.packageName = nativeApp.getPackageName(); this.context = nativeApp.getApplicationContext(); + this.getActivity = undefined; } public init() { diff --git a/application/application.d.ts b/application/application.d.ts index ec5818111..7d301d610 100644 --- a/application/application.d.ts +++ b/application/application.d.ts @@ -64,7 +64,12 @@ declare module "application" { /** * The currently active (loaded) android.app.Activity. This property is automatically updated upon Activity events. */ - currentActivity: android.app.Activity; + foregroundActivity: android.app.Activity; + + /** + * The currently active (loaded) Context. This is typically the top-level Activity that is just created. + */ + currentContext: android.content.Context; /** * The main (start) Activity for the application. diff --git a/ui/core/bindable.ts b/ui/core/bindable.ts new file mode 100644 index 000000000..b26dc39bf --- /dev/null +++ b/ui/core/bindable.ts @@ -0,0 +1,115 @@ +import observable = require("ui/core/observable"); + +export interface BindingOptions { + sourceProperty: string; + targetProperty: string; + twoWay?: boolean; +} + +export class Bindable extends observable.Observable { + private _bindings = {}; + + public bind(source: observable.Observable, options: BindingOptions) { + var binding: Binding = this._bindings[options.targetProperty]; + if (binding) { + binding.unbind(); + } + + binding = new Binding(this, options); + this._bindings[options.targetProperty] = binding; + + binding.bind(source); + } + + public unbind(options: BindingOptions) { + var binding: Binding = this._bindings[options.targetProperty]; + if (binding) { + binding.unbind(); + delete this._bindings[options.targetProperty]; + } + } + + public updateTwoWayBinding(propertyName: string, value: any) { + var binding: Binding = this._bindings[propertyName]; + + if (binding) { + binding.updateTwoWay(value); + } + } + + public getBinding(propertyName: string) { + return this._bindings[propertyName]; + } + + private clearBinding(binding: Binding) { + binding.unbind(); + } +} + +class Binding { + options: BindingOptions; + updating = false; + source: observable.Observable; + target: Bindable; + + constructor(target: Bindable, options: BindingOptions) { + this.target = target; + this.options = options; + } + + public bind(obj: observable.Observable) { + this.source = obj; + this.updateTarget(this.source.getProperty(this.options.sourceProperty)); + + var that = this; + var callback = function (data: observable.PropertyChangeData) { + that.onSourcePropertyChanged(data); + } + + this.source.addObserver(observable.Observable.propertyChangeEvent, callback); + } + + public unbind() { + if (!this.source) { + return; + } + + this.source.removeObserver(observable.Observable.propertyChangeEvent, this.onSourcePropertyChanged); + this.source = undefined; + this.target = undefined; + } + + public updateTwoWay(value: any) { + if (this.options.twoWay) { + this.updateSource(value); + } + } + + public onSourcePropertyChanged(data: observable.PropertyChangeData) { + if (data.propertyName !== this.options.sourceProperty) { + return; + } + + this.updateTarget(data.value); + } + + private updateTarget(value: any) { + if (this.updating) { + return; + } + + this.updating = true; + this.target.setProperty(this.options.targetProperty, value); + this.updating = false; + } + + private updateSource(value: any) { + if (this.updating) { + return; + } + + this.updating = true; + this.source.setProperty(this.options.sourceProperty, value); + this.updating = false; + } +} \ No newline at end of file diff --git a/ui/core/index.ts b/ui/core/index.ts new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/ui/core/index.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/core/observable.ts b/ui/core/observable.ts index 0213f18bb..a862b54cc 100644 --- a/ui/core/observable.ts +++ b/ui/core/observable.ts @@ -11,8 +11,7 @@ export interface ChangeData { export interface PropertyChangeData extends ChangeData { propertyName: string; - oldValue: any; - newValue: any; + value: any; cancel?: boolean; } @@ -23,33 +22,33 @@ export class Observable { // true to track the Changing phase, false otherwise private _trackChanging = false; - public bind(eventName: string, callback: (data: ChangeData) => void) { + public addObserver(eventName: string, callback: (data: ChangeData) => void) { this.verifyCallback(callback); var list = this.getEventList(eventName, true); list.push(callback); } - public setProperty(name: string, value: any) { - // TODO: Parameter validation - - if (!(name in this._observers)) { - // no observers to notify for the PropertyChange event + public removeObserver(eventName: string, callback: any) { + var list = this.getEventList(eventName, false); + if (!list) { return; } - // create data for the change - var data: PropertyChangeData = { - eventName: Observable.propertyChangeEvent, - propertyName: name, - sender: this, - oldValue: this[name], - newValue: value, - cancel: false - }; + var index = list.indexOf(callback); + if (index >= 0) { + list.splice(index, 1); + } + } - if (this._trackChanging) { + public setProperty(name: string, value: any) { + // TODO: Parameter validation + + // create data for the change + var data = this.createPropertyChangeData(name, value); + + if (this.hasObservers(Observable.propertyChangeEvent) && this._trackChanging) { data.phase = ChangePhase.Changing; - this.notifyObservers(data); + this.notify(data); if (data.cancel) { // change is canceled by an observer // TODO: define some priority, e.g. if someone cancels the change should others be able to override this cancelation? @@ -58,9 +57,8 @@ export class Observable { } data.phase = ChangePhase.Changed; - this.notifyObservers(data); - this.setPropertyCore(data); + this.notify(data); } public getProperty(name: string): any { @@ -71,8 +69,36 @@ export class Observable { * This method is intended to be overriden by inheritors to specify additional */ public setPropertyCore(data: PropertyChangeData) { - // get the new value from the data since some observer may have it modified - e.g. validation scenario - this[data.eventName] = data.newValue; + this[data.propertyName] = data.value; + } + + // The method will return true if the change is accepted, false otherwise + public notify(data: ChangeData) { + var observers = this.getEventList(data.eventName); + if (!observers) { + return; + } + + var i, + callback; + for (i = 0; i < observers.length; i++) { + callback = observers[i]; + callback(data); + } + } + + public hasObservers(eventName: string) { + return eventName in this._observers; + } + + public createPropertyChangeData(name: string, value: any): PropertyChangeData { + return { + eventName: Observable.propertyChangeEvent, + propertyName: name, + sender: this, + value: value, + cancel: false + }; } private getEventList(eventName: string, createIfNeeded?: boolean): Array<(data: ChangeData) => void> { @@ -94,19 +120,4 @@ export class Observable { throw new TypeError("Callback must be a valid function."); } } - - // The method will return true if the change is accepted, false otherwise - private notifyObservers(data: ChangeData) { - var observers = this.getEventList(data.eventName); - if (!observers) { - return; - } - - var i, - callback; - for (i = 0; i < observers.length; i++) { - callback = observers[i]; - callback(data); - } - } } \ No newline at end of file diff --git a/ui/core/proxy.ts b/ui/core/proxy.ts index b5f88f92b..281b32aa8 100644 --- a/ui/core/proxy.ts +++ b/ui/core/proxy.ts @@ -1,12 +1,16 @@ import observable = require("ui/core/observable"); +import bindable = require("ui/core/bindable"); -export class ProxyObject extends observable.Observable { +export class ProxyObject extends bindable.Bindable { public setPropertyCore(data: observable.PropertyChangeData) { - super.setPropertyCore(data); this.setNativeProperty(data); } public setNativeProperty(data: observable.PropertyChangeData) { - // TODO: + // inheritors will override this method to provide specific implementation + } + + public onNativePropertyChanged(data: observable.PropertyChangeData) { + } } \ No newline at end of file diff --git a/ui/core/view.android.ts b/ui/core/view.android.ts new file mode 100644 index 000000000..d6666aaa0 --- /dev/null +++ b/ui/core/view.android.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 000000000..1c3f9ea3f --- /dev/null +++ b/ui/core/view.d.ts @@ -0,0 +1,5 @@ +import proxy = require("ui/core/proxy"); + +export declare class View extends proxy.ProxyObject { + addToParent: (parent: any) => void; +} \ No newline at end of file diff --git a/ui/core/view.ios.ts b/ui/core/view.ios.ts new file mode 100644 index 000000000..19a76f130 --- /dev/null +++ b/ui/core/view.ios.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/label/index.ts b/ui/label/index.ts new file mode 100644 index 000000000..909df85b7 --- /dev/null +++ b/ui/label/index.ts @@ -0,0 +1,2 @@ +declare var module, require; +module.exports = require("ui/label/label"); \ No newline at end of file diff --git a/ui/label/label.android.ts b/ui/label/label.android.ts new file mode 100644 index 000000000..54cf62b83 --- /dev/null +++ b/ui/label/label.android.ts @@ -0,0 +1,55 @@ +import observable = require("ui/core/observable"); +import view = require("ui/core/view"); +import application = require("application"); + +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? + } + + 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 { + return this._android; + } + + get text(): string { + return this._android.getText().toString(); + } + set text(value: string) { + this.setProperty(Label.textProperty, 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); + } else if (true) { + } + } +} \ No newline at end of file diff --git a/ui/label/label.d.ts b/ui/label/label.d.ts new file mode 100644 index 000000000..57342e807 --- /dev/null +++ b/ui/label/label.d.ts @@ -0,0 +1,5 @@ +declare module "ui/label" { + class Label { + text: string; + } +} \ No newline at end of file diff --git a/ui/label/label.ios.ts b/ui/label/label.ios.ts new file mode 100644 index 000000000..1f056b0e9 --- /dev/null +++ b/ui/label/label.ios.ts @@ -0,0 +1,5 @@ +import view = require("ui/core/view"); + +export class Label extends view.View { + +} \ No newline at end of file diff --git a/ui/text-input/index.ts b/ui/text-input/index.ts new file mode 100644 index 000000000..5fce8f31b --- /dev/null +++ b/ui/text-input/index.ts @@ -0,0 +1,2 @@ +declare var module, require; +module.exports = require("ui/text-input/text-input"); \ No newline at end of file diff --git a/ui/text-input/text-input.android.ts b/ui/text-input/text-input.android.ts new file mode 100644 index 000000000..5ca064487 --- /dev/null +++ b/ui/text-input/text-input.android.ts @@ -0,0 +1,51 @@ +import observable = require("ui/core/observable"); +import view = require("ui/core/view"); +import application = require("application"); + +export class TextInput extends view.View { + private _android: android.widget.EditText; + private static textProperty = "text"; + + constructor() { + super(); + + var context = application.android.currentContext; + this._android = new android.widget.EditText(context); + + // TODO: This code is same for Label, extract base class or derive from Label + 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.EditText { + return this._android; + } + + get text(): string { + return this._android.getText().toString(); + } + set text(value: string) { + this.setProperty(TextInput.textProperty, value); + } + + public setNativeProperty(data: observable.PropertyChangeData) { + // TODO: Will this be a gigantic if-else switch? + if (data.propertyName === TextInput.textProperty) { + this._android.setText(data.value, android.widget.TextView.BufferType.EDITABLE); + } else if (true) { + } + } +} \ No newline at end of file diff --git a/ui/text-input/text-input.d.ts b/ui/text-input/text-input.d.ts new file mode 100644 index 000000000..19a76f130 --- /dev/null +++ b/ui/text-input/text-input.d.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/text-input/text-input.ios.ts b/ui/text-input/text-input.ios.ts new file mode 100644 index 000000000..19a76f130 --- /dev/null +++ b/ui/text-input/text-input.ios.ts @@ -0,0 +1 @@ + \ No newline at end of file