@qooxdoo/framework
Version:
The JS Framework for Coders
1,390 lines (1,217 loc) • 67.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Martin Wittemann (martinwittemann)
************************************************************************ */
/**
* Internal class for handling of dynamic properties. Should only be used
* through the methods provided by {@link qx.Class}.
*
* For a complete documentation of properties take a look at
* http://manual.qooxdoo.org/${qxversion}/pages/core.html#properties.
*
*
* *Normal properties*
*
* The <code>properties</code> key in the class definition map of {@link qx.Class#define}
* is used to generate the properties.
*
* Valid keys of a property definition are:
*
* <table>
* <tr><th>Name</th><th>Type</th><th>Description</th></tr>
* <tr><th>check</th><td>Array, String, Function</td><td>
* The check is used to check the type the incoming value of a property. This will only
* be executed in the source version. The build version will not contain the checks.
* The check can be:
* <ul>
* <li>a custom check function. The function takes the incoming value as a parameter and must
* return a boolean value to indicate whether the values is valid.
* </li>
* <li>inline check code as a string e.g. <code>"value > 0 && value < 100"</code></li>
* <li>a class name e.g. <code>qx.ui.form.Button</code></li>
* <li>a name of an interface the value must implement</li>
* <li>an array of all valid values</li>
* <li>one of the predefined checks: Boolean, String, Number, Integer, Float, Double,
* Object, Array, Map, Class, Mixin, Interface, Theme, Error, RegExp, Function,
* Date, Node, Element, Document, Window, Event
* </li>
* <ul>
* </td></tr>
* <tr><th>init</th><td>var</td><td>
* Sets the default/initial value of the property. If no property value is set or the property
* gets reset, the getter will return the <code>init</code> value.
* </td></tr>
* <tr><th>apply</th><td>String</td><td>
* On change of the property value the method of the specified name will be called. The signature of
* the method is <code>function(newValue, oldValue, propertyName)</code>. It is conventional to name
* the callback <code>_apply</code> + <i>PropertyName</i>, with the property name camel-cased (e.g.
* "<i>_applyFooBar</i>" for a property <i>fooBar</i>).
* </td></tr>
* <tr><th>event</th><td>String</td><td>
* On change of the property value an event with the given name will be dispatched. The event type is
* {@link qx.event.type.Data}.
* </td></tr>
* <tr><th>themeable</th><td>Boolean</td><td>
* Whether this property can be set using themes.
* </td></tr>
* <tr><th>inheritable</th><td>Boolean</td><td>
* Whether the property value should be inheritable. If the property does not have an user defined or an
* init value, the property will try to get the value from the parent of the current object.
* </td></tr>
* <tr><th>nullable</th><td>Boolean</td><td>
* Whether <code>null</code> is an allowed value of the property. This is complementary to the check
* defined using the <code>check</code> key.
* </td></tr>
* <tr><th>refine</th><td>Boolean</td><td>
* Whether the property definition is a refinement of a property in one of the super classes of the class.
* Only the <code>init</code> value can be changed using refine.
* </td></tr>
* <tr><th>transform</th><td>String</td><td>
* On setting of the property value the method of the specified name will
* be called. The signature of the method is <code>function(value, oldValue)</code>.
* The parameter <code>value</code> is the value passed to the setter, the
* parameter <code>oldValue</code> is the current value, or undefined if no value
* has been set previously.
* The function must return the modified or unmodified value.
* Transformation occurs before the check function, so both may be
* specified if desired. Alternatively, the transform function may throw
* an error if the value passed to it is invalid.
* </td></tr>
* <tr><th>validate</th><td>Function, String</td><td>
* On setting of the property value the method of the specified name will
* be called. The signature of the method is <code>function(value)</code>.
* The parameter <code>value</code> is the value passed to the setter.
* If the validation fails, an <code>qx.core.ValidationError</code> should
* be thrown by the validation function. Otherwise, just do nothing in the
* function.<br>
* If a string is given, the string should hold a reference to a member
* method.<br>
* <code>"<i>methodname</i>"</code> for example
* <code>"__validateProperty"</code><br>
* There are some default validators in the {@link qx.util.Validate} class.
* See this documentation for usage examples.
* </td></tr>
* <tr><th>dereference</th><td>Boolean</td><td>
* By default, the references to the values (current, init, ...) of the
* property will be stored as references on the object. When disposing
* this object, the references will not be deleted. Setting the
* dereference key to true tells the property system to delete all
* connections made by this property on dispose. This can be necessary for
* disconnecting DOM objects to allow the garbage collector to work
* properly.
* </td></tr>
* <tr><th>deferredInit</th><td>Boolean</td><td>
* Allow for a deferred initialization for reference types. Defaults to false.
* </td></tr>
* <tr><th>isEqual</th><td>Function, String</td><td>
* On setting of the property value the method of the specified name will
* be called to test if two values are equal. These checks for equality are
* performed by the Property-System to decide whether further actions (like
* e.g. calling applier methods or firing of events) are needed.
* The signature of the method is <code>function(valueA, valueB)</code>.
* <br/>
* The <i>isEqual</i>-value can be:
* <ul>
* <li>a custom check function.
* The function takes two values as parameter and must return a
* boolean value to indicate whether the values are considered
* equal e.g. <code>function (a, b) { return Object.is(a, b); }</code>.</li>
* <li>inline check code as a string
* which will be invoked with two parameters <code>a</code> and
* <code>b</code> and results in a boolean value to indicate whether
* the values are equal e.g. <code>"a.length() == b.length()"</code>.</li>
* <li>reference to a member method as string
* <code>"<i>methodname</i>"</code> which will be invoked with two
* parameters and returns a boolean value indicating whether the two
* values are considered equal for example <code>"__areTheSame"</code>.</li>
* </ul>
* The default implementation (if this key is undefined) will check the
* equality by using the <i>identity</i> operator (===) as if defined like
* <code>"a===b"</code>.
* </td></tr>
* </table>
*
* *Property groups*
*
* Property groups are defined in a similar way but support a different set of keys:
*
* <table>
* <tr><th>Name</th><th>Type</th><th>Description</th></tr>
* <tr><th>group</th><td>String[]</td><td>
* A list of property names which should be set using the property group.
* </td></tr>
* <tr><th>mode</th><td>String</td><td>
* If mode is set to <code>"shorthand"</code>, the properties can be set using a CSS like shorthand mode.
* </td></tr>
* <tr><th>themeable</th><td>Boolean</td><td>
* Whether this property can be set using themes.
* </td></tr>
* </table>
*
* @internal
* @ignore(qx.Interface)
*/
qx.Bootstrap.define("qx.core.Property",
{
statics :
{
/**
* This is a method which does nothing than gathering dependencies for the
* module system. Calling this method is useless because it does nothing.
*/
__gatherDependency : function() {
if (qx.core.Environment.get("module.events")) {
qx.event.type.Data;
qx.event.dispatch.Direct;
}
if (qx.core.Environment.get("qx.promise")) {
qx.Promise;
}
},
/**
* Built-in checks
* The keys could be used in the check of the properties
*/
__checks :
{
"Boolean" : 'qx.core.Assert.assertBoolean(value, msg) || true',
"String" : 'qx.core.Assert.assertString(value, msg) || true',
"Number" : 'qx.core.Assert.assertNumber(value, msg) || true',
"Integer" : 'qx.core.Assert.assertInteger(value, msg) || true',
"PositiveNumber" : 'qx.core.Assert.assertPositiveNumber(value, msg) || true',
"PositiveInteger" : 'qx.core.Assert.assertPositiveInteger(value, msg) || true',
"Error" : 'qx.core.Assert.assertInstance(value, Error, msg) || true',
"RegExp" : 'qx.core.Assert.assertInstance(value, RegExp, msg) || true',
"Object" : 'qx.core.Assert.assertObject(value, msg) || true',
"Array" : 'qx.core.Assert.assertArray(value, msg) || true',
"Map" : 'qx.core.Assert.assertMap(value, msg) || true',
"Function" : 'qx.core.Assert.assertFunction(value, msg) || true',
"Date" : 'qx.core.Assert.assertInstance(value, Date, msg) || true',
"Node" : 'value !== null && value.nodeType !== undefined',
"Element" : 'value !== null && value.nodeType === 1 && value.attributes',
"Document" : 'value !== null && value.nodeType === 9 && value.documentElement',
"Window" : 'value !== null && value.document',
"Event" : 'value !== null && value.type !== undefined',
"Class" : 'value !== null && value.$$type === "Class"',
"Mixin" : 'value !== null && value.$$type === "Mixin"',
"Interface" : 'value !== null && value.$$type === "Interface"',
"Theme" : 'value !== null && value.$$type === "Theme"',
"Color" : 'qx.lang.Type.isString(value) && qx.util.ColorUtil.isValidPropertyValue(value)',
"Decorator" : 'value !== null && qx.theme.manager.Decoration.getInstance().isValidPropertyValue(value)',
"Font" : 'value !== null && qx.theme.manager.Font.getInstance().isDynamic(value)'
},
/**
* Contains types from {@link #__checks} list which need to be dereferenced
*/
__dereference :
{
"Node" : true,
"Element" : true,
"Document" : true,
"Window" : true,
"Event" : true
},
/**
* Inherit value, used to override defaults etc. to force inheritance
* even if property value is not undefined (through multi-values)
*
* @internal
*/
$$inherit : "inherit",
/**
* Caching field names for each property created
*
* @internal
*/
$$store :
{
runtime : {},
user : {},
theme : {},
inherit : {},
init : {},
useinit : {}
},
/**
* Caching function names for each property created
*
* @internal
*/
$$method :
{
get : {},
getAsync : {},
set : {},
setImpl : {},
setAsync : {},
reset : {},
init : {},
refresh : {},
setRuntime : {},
resetRuntime : {},
setThemed : {},
resetThemed : {}
},
/**
* Supported keys for property definitions
*
* @internal
*/
$$allowedKeys :
{
"@" : "object", // Anything
name : "string", // String
dereference : "boolean", // Boolean
inheritable : "boolean", // Boolean
nullable : "boolean", // Boolean
themeable : "boolean", // Boolean
refine : "boolean", // Boolean
init : null, // var
apply : "string", // String
event : "string", // String
check : null, // Array, String, Function
transform : "string", // String
async : "boolean", // Boolean
deferredInit : "boolean", // Boolean
validate : null, // String, Function
isEqual : null // String, Function
},
/**
* Supported keys for property group definitions
*
* @internal
*/
$$allowedGroupKeys :
{
"@" : "object", // Anything
name : "string", // String
group : "object", // Array
mode : "string", // String
themeable : "boolean" // Boolean
},
/** Contains names of inheritable properties, filled by {@link qx.Class.define} */
$$inheritable : {},
/**
* Generate optimized refresh method and attach it to the class' prototype
*
* @param clazz {Class} clazz to which the refresher should be added
*/
__executeOptimizedRefresh : function(clazz)
{
var inheritables = this.__getInheritablesOfClass(clazz);
if (!inheritables.length) {
var refresher = function () {};
} else {
refresher = this.__createRefresher(inheritables);
}
clazz.prototype.$$refreshInheritables = refresher;
},
/**
* Get the names of all inheritable properties of the given class
*
* @param clazz {Class} class to get the inheritable properties of
* @return {String[]} List of property names
*/
__getInheritablesOfClass : function(clazz)
{
var inheritable = [];
while(clazz)
{
var properties = clazz.$$properties;
if (properties)
{
for (var name in this.$$inheritable)
{
// Whether the property is available in this class
// and whether it is inheritable in this class as well
if (properties[name] && properties[name].inheritable)
{
inheritable.push(name);
}
}
}
clazz = clazz.superclass;
}
return inheritable;
},
/**
* Assemble the refresher code and return the generated function
*
* @param inheritables {String[]} list of inheritable properties
* @return {Function} refresher function
*/
__createRefresher : function(inheritables)
{
var inherit = this.$$store.inherit;
var init = this.$$store.init;
var refresh = this.$$method.refresh;
var code = [
"var parent = this.getLayoutParent();",
"if (!parent) return;"
];
for (var i=0, l=inheritables.length; i<l; i++)
{
var name = inheritables[i];
code.push(
"var value = parent.", inherit[name],";",
"if (value===undefined) value = parent.", init[name], ";",
"this.", refresh[name], "(value);"
);
}
return new Function(code.join(""));
},
/**
* Attach $$refreshInheritables method stub to the given class
*
* @param clazz {Class} clazz to which the refresher should be added
*/
attachRefreshInheritables : function(clazz)
{
clazz.prototype.$$refreshInheritables = function()
{
qx.core.Property.__executeOptimizedRefresh(clazz);
return this.$$refreshInheritables();
};
},
/**
* Attach one property to class
*
* @param clazz {Class} Class to attach properties to
* @param name {String} Name of property
* @param config {Map} Configuration map of property
*/
attachMethods : function(clazz, name, config)
{
// Divide groups from "normal" properties
config.group ?
this.__attachGroupMethods(clazz, config, name) :
this.__attachPropertyMethods(clazz, config, name);
},
/**
* Attach group methods
*
* @param clazz {Class} Class to attach properties to
* @param config {Map} Property configuration
* @param name {String} Name of the property
*/
__attachGroupMethods : function(clazz, config, name)
{
var upname = qx.Bootstrap.firstUp(name);
var members = clazz.prototype;
var themeable = config.themeable === true;
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.property.level") > 1) {
qx.Bootstrap.debug("Generating property group: " + name);
}
}
var setter = [];
var resetter = [];
if (themeable)
{
var styler = [];
var unstyler = [];
}
var argHandler = "var a=arguments[0] instanceof Array?arguments[0]:arguments;";
setter.push(argHandler);
if (themeable) {
styler.push(argHandler);
}
if (config.mode == "shorthand")
{
var shorthand = "a=qx.lang.Array.fromShortHand(qx.lang.Array.fromArguments(a));";
setter.push(shorthand);
if (themeable) {
styler.push(shorthand);
}
}
for (var i=0, a=config.group, l=a.length; i<l; i++)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!this.$$method.set[a[i]]||!this.$$method.reset[a[i]]) {
throw new Error("Cannot create property group '" + name + "' including non-existing property '" + a[i] + "'!");
}
}
setter.push("this.", this.$$method.set[a[i]], "(a[", i, "]);");
resetter.push("this.", this.$$method.reset[a[i]], "();");
if (themeable)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!this.$$method.setThemed[a[i]]) {
throw new Error("Cannot add the non themable property '" + a[i] + "' to the themable property group '"+ name +"'");
}
}
styler.push("this.", this.$$method.setThemed[a[i]], "(a[", i, "]);");
unstyler.push("this.", this.$$method.resetThemed[a[i]], "();");
}
}
// Attach setter
this.$$method.set[name] = "set" + upname;
members[this.$$method.set[name]] = new Function(setter.join(""));
// Attach resetter
this.$$method.reset[name] = "reset" + upname;
members[this.$$method.reset[name]] = new Function(resetter.join(""));
if (themeable)
{
// Attach styler
this.$$method.setThemed[name] = "setThemed" + upname;
members[this.$$method.setThemed[name]] = new Function(styler.join(""));
// Attach unstyler
this.$$method.resetThemed[name] = "resetThemed" + upname;
members[this.$$method.resetThemed[name]] = new Function(unstyler.join(""));
}
},
/**
* Attach property methods
*
* @param clazz {Class} Class to attach properties to
* @param config {Map} Property configuration
* @param name {String} Name of the property
*/
__attachPropertyMethods : function(clazz, config, name)
{
var upname = qx.Bootstrap.firstUp(name);
var members = clazz.prototype;
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.property.level") > 1) {
qx.Bootstrap.debug("Generating property wrappers: " + name);
}
}
// Fill dispose value
if (config.dereference === undefined && typeof config.check === "string") {
config.dereference = this.__shouldBeDereferenced(config.check);
}
if (!qx.core.Environment.get("qx.promise")) {
if (config.async) {
this.warn("Asynchronous property " + clazz.classname +"." + name + " is switched to synchronous because qx.promise==false");
config.async = false;
}
if (config.check == "qx.Promise") {
this.error("Cannot implement check for property " + clazz.classname +"." + name + " because qx.promise==false");
delete config.check;
}
}
// Check for method name conflicts
if (qx.core.Environment.get("qx.debug")) {
// Exclude qx.data.model.* because that's from marshalling and will cause conflicts to be reported
if (clazz.classname && !clazz.classname.match(/^qx.data.model/)) {
var allNames = [ "get" + upname, "set" + upname, "reset" + upname, "setRuntime" + upname, "resetRuntime" + upname ];
if (config.async) {
allNames.push("get" + upname + "Async");
allNames.push("set" + upname + "Async");
}
if (config.inheritable || config.apply || config.event || config.deferredInit) {
allNames.push("init" + upname);
}
if (config.inheritable) {
allNames.push("refresh" + upname);
}
if (config.themeable) {
allNames.push("getThemed" + upname);
allNames.push("setThemed" + upname);
allNames.push("resetThemed" + upname);
}
if (config.check === "Boolean") {
allNames.push("is" + upname);
allNames.push("toggle" + upname);
}
allNames.forEach(function(name) {
if (clazz.superclass.prototype[name] !== undefined) {
var conflictingClass = null;
for (var tmp = clazz.superclass; tmp && tmp != qx.core.Object; tmp = tmp.superclass) {
if (tmp.superclass.prototype[name] === undefined) {
conflictingClass = tmp;
break;
}
}
if (conflictingClass) {
qx.log.Logger.warn("Conflicting property method " + clazz.classname + "." + name + " with " + conflictingClass.classname);
}
}
});
}
}
var method = this.$$method;
var store = this.$$store;
store.runtime[name] = "$$runtime_" + name;
store.user[name] = "$$user_" + name;
store.theme[name] = "$$theme_" + name;
store.init[name] = "$$init_" + name;
store.inherit[name] = "$$inherit_" + name;
store.useinit[name] = "$$useinit_" + name;
var getName = method.get[name] = "get" + upname;
members[method.get[name]] = new Function(
"this." + getName + ".$$install && this." + getName + ".$$install();" +
"return this." + getName + ".apply(this, arguments);");
if (config.async) {
if (qx.core.Environment.get("qx.debug")) {
if (members.hasOwnProperty(getName + "Async")) {
this.error("Asynchronous property " + clazz.classname +"." + name + " is replacing " + getName + "Async() method in same class");
} else if (members[getName + "Async"] !== undefined) {
this.warn("Asynchronous property " + clazz.classname +"." + name + " is overriding " + getName + "Async() method");
}
}
method.getAsync[name] = getName + "Async";
members[method.getAsync[name]] = new Function(
"this." + getName + ".$$install && this." + getName + ".$$install.call(this);" +
"return this." + getName + "Async.apply(this, arguments);");
}
members[method.get[name]].$$install = function() {
qx.core.Property.__installOptimizedGetter(clazz, name, "get", arguments);
if (config.async) {
qx.core.Property.__installOptimizedGetter(clazz, name, "getAsync", arguments);
}
};
var setName = method.set[name] = "set" + upname;
members[setName] = new Function(
"this." + setName + ".$$install && this." + setName + ".$$install.call(this);" +
"return this." + setName + ".apply(this, arguments);");
method.setAsync[name] = "set" + upname + "Async";
if (config.async) {
if (qx.core.Environment.get("qx.debug")) {
if (members.hasOwnProperty(setName + "Async")) {
this.error("Asynchronous property " + clazz.classname +"." + name + " is replacing " + setName + "Async() method in same class");
} else if (members[setName + "Async"] !== undefined) {
this.warn("Asynchronous property " + clazz.classname +"." + name + " is overriding " + setName + "Async() method");
}
}
members[setName + "Async"] = new Function(
"this." + setName + ".$$install && this." + setName + ".$$install.call(this);" +
"return this." + setName + "Async.apply(this, arguments);");
}
method.setImpl[name] = "$$set" + upname + "Impl";
members[setName].$$install = function() {
qx.core.Property.__installOptimizedSetter(clazz, name, "set");
qx.core.Property.__installOptimizedSetter(clazz, name, "setImpl");
if (config.async) {
qx.core.Property.__installOptimizedSetter(clazz, name, "setAsync");
}
};
method.reset[name] = "reset" + upname;
members[method.reset[name]] = function() {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "reset");
};
members[method.reset[name]].$$install = function() {
qx.core.Property.__installOptimizedSetter(clazz, name, "reset");
};
if (config.inheritable || config.apply || config.event || config.deferredInit)
{
method.init[name] = "init" + upname;
members[method.init[name]] = function(value) {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "init", arguments);
};
if (qx.core.Environment.get("qx.debug")) {
members[method.init[name]].$$propertyMethod = true;
}
}
if (config.inheritable)
{
method.refresh[name] = "refresh" + upname;
members[method.refresh[name]] = function(value) {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "refresh", arguments);
};
if (qx.core.Environment.get("qx.debug")) {
members[method.refresh[name]].$$propertyMethod = true;
}
}
method.setRuntime[name] = "setRuntime" + upname;
members[method.setRuntime[name]] = function(value) {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "setRuntime", arguments);
};
method.resetRuntime[name] = "resetRuntime" + upname;
members[method.resetRuntime[name]] = function() {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "resetRuntime");
};
if (config.themeable)
{
method.setThemed[name] = "setThemed" + upname;
members[method.setThemed[name]] = function(value) {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "setThemed", arguments);
};
method.resetThemed[name] = "resetThemed" + upname;
members[method.resetThemed[name]] = function() {
return qx.core.Property.executeOptimizedSetter(this, clazz, name, "resetThemed");
};
if (qx.core.Environment.get("qx.debug")) {
members[method.setThemed[name]].$$propertyMethod = true;
members[method.resetThemed[name]].$$propertyMethod = true;
}
}
if (config.check === "Boolean")
{
members["toggle" + upname] = new Function("return this." + method.set[name] + "(!this." + method.get[name] + "())");
members["is" + upname] = new Function("return this." + method.get[name] + "()");
if (qx.core.Environment.get("qx.debug")) {
members["toggle" + upname].$$propertyMethod = true;
members["is" + upname].$$propertyMethod = true;
}
}
// attach a flag to make generated property methods
if (qx.core.Environment.get("qx.debug")) {
members[method.get[name]].$$propertyMethod = true;
members[method.set[name]].$$propertyMethod = true;
members[method.reset[name]].$$propertyMethod = true;
members[method.setRuntime[name]].$$propertyMethod = true;
members[method.resetRuntime[name]].$$propertyMethod = true;
}
},
/**
* Returns if the reference for the given property check should be removed
* on dispose.
*
* @param check {var} The check of the property definition.
* @return {Boolean} If the dereference key should be set.
*/
__shouldBeDereferenced : function(check) {
return !!this.__dereference[check];
},
/** @type {Map} Internal data field for error messages used by {@link #error} */
__errors :
{
0 : 'Could not change or apply init value after constructing phase!',
1 : 'Requires exactly one argument!',
2 : 'Undefined value is not allowed!',
3 : 'Does not allow any arguments!',
4 : 'Null value is not allowed!',
5 : 'Is invalid!'
},
/**
* Error method used by the property system to report errors.
*
* @param obj {qx.core.Object} Any qooxdoo object
* @param id {Integer} Numeric error identifier
* @param property {String} Name of the property
* @param variant {String} Name of the method variant e.g. "set", "reset", ...
* @param value {var} Incoming value
*/
error : function(obj, id, property, variant, value)
{
var classname = obj.constructor.classname;
var msg = "Error in property " + property + " of class " + classname +
" in method " + this.$$method[variant][property] + " with incoming value '" + value + "': ";
throw new Error(msg + (this.__errors[id] || "Unknown reason: " + id));
},
/**
* Compiles a string builder object to a function, executes the function and
* returns the return value.
*
* @param instance {Object} Instance which have called the original method
* @param members {Object} Prototype members map where the new function should be stored
* @param name {String} Name of the property
* @param variant {String} Function variant e.g. get, set, reset, ...
* @param code {Array} Array which contains the code
* @param args {arguments} Incoming arguments of wrapper method
* @return {var} Return value of the generated function
*/
__unwrapFunctionFromCode : function(instance, members, name, variant, code, args)
{
var fn = this.__installFunctionFromCode(instance.constructor, name, variant, code, args);
// Executing new function
if (args === undefined) {
return fn.call(instance);
} else if (qx.core.Environment.get("qx.debug")) {
return fn.apply(instance, args);
} else {
return fn.call(instance, args[0]);
}
},
/**
* Takes a string builder object, converts it into a function, and installs it as
* a property accessor
*
* @param clazz {Class} Class to install the method into
* @param name {String} Name of the property
* @param variant {String} Function variant e.g. get, set, reset, ...
* @param code {Array} Array which contains the code
* @param args {arguments} Incoming arguments of wrapper method
* @return {var} Return value of the generated function
*/
__installFunctionFromCode : function(clazz, name, variant, code, args)
{
var store = this.$$method[variant][name];
// Output generate code
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.property.level") > 1) {
qx.Bootstrap.debug("Code[" + this.$$method[variant][name] + "]: " + code.join(""));
}
// Overriding temporary wrapper
try{
clazz.prototype[store] = new Function("value", code.join(""));
} catch(ex) {
throw new Error("Malformed generated code to unwrap method: " + this.$$method[variant][name] + "\n" + code.join(""));
}
}
else
{
clazz.prototype[store] = new Function("value", code.join(""));
}
// Enable profiling code
if (qx.core.Environment.get("qx.aspects")) {
clazz.prototype[store] = qx.core.Aspect.wrap(clazz.classname + "." + store, clazz.prototype[store], "property");
}
qx.Bootstrap.setDisplayName(clazz.prototype[store], clazz.classname + ".prototype", store);
return clazz.prototype[store];
},
/**
* Generates the optimized getter, installs it into the class prototype, and executes it
* Supported variants: get
*
* @param instance {Object} the instance which calls the method
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
* @return {var} Execute return value of apply generated function, generally the incoming value
*/
executeOptimizedGetter : function(instance, clazz, name, variant)
{
var code = this.__compileGetter(clazz, name, variant);
var members = clazz.prototype;
return this.__unwrapFunctionFromCode(instance, members, name, variant, code);
},
/**
* Installs a getter into the class prototype, without executing it
* Supported variants: get
*
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
*/
__installOptimizedGetter : function(clazz, name, variant)
{
var code = this.__compileGetter(clazz, name, variant);
this.__installFunctionFromCode(clazz, name, variant, code);
},
/**
* Compiles a getter into a string builder array
* Supported variants: get
*
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
* @return {String[]} the string builder array
*/
__compileGetter: function(clazz, name, variant)
{
var config = clazz.$$properties[name];
var code = [];
var store = this.$$store;
if (variant == "getAsync") {
code.push("return qx.Promise.resolve(this." + this.$$method.get[name] + "());");
return code;
}
code.push('if(this.', store.runtime[name], '!==undefined)');
code.push('return this.', store.runtime[name], ';');
if (config.inheritable)
{
code.push('else if(this.', store.inherit[name], '!==undefined)');
code.push('return this.', store.inherit[name], ';');
code.push('else ');
}
code.push('if(this.', store.user[name], '!==undefined)');
code.push('return this.', store.user[name], ';');
if (config.themeable)
{
code.push('else if(this.', store.theme[name], '!==undefined)');
code.push('return this.', store.theme[name], ';');
}
if (config.deferredInit && config.init === undefined)
{
code.push('else if(this.', store.init[name], '!==undefined)');
code.push('return this.', store.init[name], ';');
}
code.push('else ');
if (config.init !== undefined)
{
if (config.inheritable)
{
code.push('var init=this.', store.init[name], ';');
if (config.nullable) {
code.push('if(init==qx.core.Property.$$inherit)init=null;');
}
code.push('return init;');
}
else
{
code.push('return this.', store.init[name], ';');
}
}
else if (config.inheritable || config.nullable) {
code.push('return null;');
} else {
code.push('throw new Error("Property ', name, ' of an instance of ', clazz.classname, ' is not (yet) ready!");');
}
return code;
},
/**
* Generates the optimized setter
* Supported variants: set, reset, init, refresh, style, unstyle
*
* @param instance {Object} the instance which calls the method
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
* @param args {arguments} Incoming arguments of wrapper method
* @return {var} Execute return value of apply generated function, generally the incoming value
*/
executeOptimizedSetter : function(instance, clazz, name, variant, args)
{
var code = this.__compileSetter(clazz, name, variant);
var members = clazz.prototype;
return this.__unwrapFunctionFromCode(instance, members, name, variant, code, args);
},
/**
* Installs a setter into the class prototype, without executing it
* Supported variants: set
*
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
* @return {var} Return value of the generated function
*/
__installOptimizedSetter : function(clazz, name, variant)
{
var code = this.__compileSetter(clazz, name, variant);
return this.__installFunctionFromCode(clazz, name, variant, code);
},
/**
* Compiles a setter into a string builder array
* Supported variants: set, setThemed, setRuntime, init
*
* @param instance {Object} the instance which calls the method
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
* @return {String[]} the string builder array
*/
__compileSetter: function(clazz, name, variant) {
var config = clazz.$$properties[name];
var members = clazz.prototype;
var code = [];
var upname = qx.lang.String.firstUp(name);
if (variant == "setAsync") {
code.push('return qx.Promise.resolve(this.$$set' + upname + "Impl.apply(this, arguments));");
return code;
} else if (variant == "set") {
code.push(
'this.$$set' + upname + "Impl.apply(this, arguments);",
'return value;');
return code;
}
var incomingValue =
variant === "setImpl" ||
variant === "setThemed" ||
variant === "setRuntime" ||
(variant === "init" && config.init === undefined);
var hasCallback = config.apply || config.event || config.inheritable;
var store = this.__getStore(variant, name);
this.__emitIsEqualFunction(code, clazz, config, name);
this.__emitSetterPreConditions(code, config, name, variant, incomingValue);
if (incomingValue || hasCallback) {
this.__emitOldValue(code, config, name);
}
if (incomingValue) {
this.__emitIncomingValueTransformation(code, clazz, config, name);
}
if (hasCallback) {
this.__emitOldNewComparison(code, incomingValue, store, variant);
}
if (config.inheritable) {
code.push('var inherit=prop.$$inherit;');
}
if (qx.core.Environment.get("qx.debug"))
{
if (incomingValue) {
this.__emitIncomingValueValidation(code, config, clazz, name, variant);
}
}
if (!hasCallback) {
this.__emitStoreValue(code, name, variant, incomingValue);
} else {
this.__emitStoreComputedValue(code, config, name, variant, incomingValue);
}
if (config.inheritable) {
this.__emitStoreInheritedPropertyValue(code, config, name, variant);
} else if (hasCallback) {
this.__emitNormalizeUndefinedValues(code, config, name, variant);
}
if (hasCallback)
{
// Emit callback and event firing; Refreshing children (5th parameter) requires the parent/children interface
this.__emitCallCallback(code, config, name, variant, !!(config.inheritable && members._getChildren));
}
// Return value
if (incomingValue) {
code.unshift('function set(value){');
code.push('}');
if (qx.core.Environment.get("qx.promise") && (!config.check || config.check != "qx.Promise")) {
code.push(
'var promise;',
'if (value instanceof qx.Promise) ',
'promise = value.then(set.bind(this));',
'else ',
'promise = set.apply(this, arguments);');
if (variant == "setImpl") {
code.push("return promise;");
} else {
code.push('return value;');
}
} else {
code.push(
'set.apply(this, arguments);',
'return value;');
}
}
return code;
},
/**
* Get the object to store the value for the given variant
*
* @param variant {String} Method variant.
* @param name {String} name of the property
*
* @return {Object} the value store
*/
__getStore : function(variant, name)
{
if (variant === "setRuntime" || variant === "resetRuntime") {
var store = this.$$store.runtime[name];
} else if (variant === "setThemed" || variant === "resetThemed") {
store = this.$$store.theme[name];
} else if (variant === "init") {
store = this.$$store.init[name];
} else {
store = this.$$store.user[name];
}
return store;
},
/**
* Emit code for the equality check evaluation
*
* @param code {String[]} String array to append the code to
* @param clazz {Class} the class which originally defined the property
* @param config {Object} The property configuration map
* @param name {String} name of the property
*/
__emitIsEqualFunction : function (code, clazz, config, name)
{
code.push('var equ=');
if (typeof config.isEqual === "function")
{
code.push('function(a,b){return !!', clazz.classname, '.$$properties.',
name, '.isEqual.call(this,a,b);};');
}
else if (typeof config.isEqual === "string")
{
var members = clazz.prototype;
// Name of member?
if (members[config.isEqual]!==undefined)
{
code.push('this.', config.isEqual, ';');
}
else // 'inline' code
{
code.push('function(a,b){return !!(', config.isEqual, ');};');
}
}
else if (typeof config.isEqual === "undefined")
{
code.push('function(a,b){return a===b;};');
}
else
{
throw new Error( "Invalid type for 'isEqual' attribute " +
"of property '" + name + "' in class '" + clazz.classname + "'" );
}
},
/**
* Emit code to check the arguments preconditions
*
* @param code {String[]} String array to append the code to
* @param config {Object} The property configuration map
* @param name {String} name of the property
* @param variant {String} Method variant.
* @param incomingValue {Boolean} Whether the setter has an incoming value
*/
__emitSetterPreConditions : function(code, config, name, variant, incomingValue)
{
if (qx.core.Environment.get("qx.debug"))
{
code.push('var prop=qx.core.Property;');
if (variant === "init") {
code.push('if(this.$$initialized)prop.error(this,0,"', name, '","', variant, '",value);');
}
if (variant === "refresh")
{
// do nothing
// refresh() is internal => no arguments test
// also note that refresh() supports "undefined" values
}
else if (incomingValue)
{
// Check argument length
code.push('if(arguments.length!==1)prop.error(this,1,"', name, '","', variant, '",value);');
// Undefined check
code.push('if(value===undefined)prop.error(this,2,"', name, '","', variant, '",value);');
}
else
{
// Check argument length
code.push('if(arguments.length!==0)prop.error(this,3,"', name, '","', variant, '",value);');
}
}
else
{
if (!config.nullable || config.check || config.inheritable) {
code.push('var prop=qx.core.Property;');
}
// Undefined check
if (variant === "setImpl") {
code.push('if(value===undefined)prop.error(this,2,"', name, '","', variant, '",value);');
}
}
},
/**
* Emit code to apply the "validate" and "transform" config keys.
*
* @param code {String[]} String array to append the code to
* @param clazz {Class} the class which originally defined the property
* @param config {Object} The property configuration map
* @param name {String} name of the property
*/
__emitIncomingValueTransformation : function(code, clazz, config, name)
{
// Call user-provided transform method, if one is provided. Transform
// method should either throw an error or return the new value.
if (config.transform) {
code.push('value=this.', config.transform, '(value, old);');
}
// Call user-provided validate method, if one is provided. Validate
// method should either throw an error or do nothing.
if (config.validate) {
// if it is a string
if (typeof config.validate === "string") {
code.push('this.', config.validate, '(value);');
// if its a function otherwise
} else if (config.validate instanceof Function) {
code.push(clazz.classname, '.$$properties.', name);
code.push('.validate.call(this, value);');
}
}
},
/**
* Emit code, which returns if the incoming value equals the current value.
*
* @param code {String[]} String array to append the code to
* @param incomingValue {Boolean} Whether the setter has an incoming value
* @param store {Object} The data store to use for the incoming value
* @param variant {String} Method variant.
*/
__emitOldNewComparison : function(code, incomingValue, store, variant)
{
var resetValue = (
variant === "reset" ||
variant === "resetThemed" ||
variant === "resetRuntime"
);
if (incomingValue) {
code.push('if(equ.call(this,this.',store,',value))return value;');
} else if (resetValue) {
code.push('if(this.', store, '===undefined)return;');
}
},
/**
* Emit code, which performs validation of the incoming value according to
* the "nullable", "check" and "inheritable" config keys.
*
* @signature function(code, config, clazz, name, variant)
* @param code {String[]} String array to append the code to
* @param config {Object} The property configuration map
* @param clazz {Class} the class which originally defined the property
* @param name {String} name of the property
* @param variant {String} Method variant.
*/
__emitIncomingValueValidation : qx.core.Environment.select("qx.debug",
{
"true" : function(code, config, clazz, name, variant)
{
// Null check
if (!config.nullable) {
code.push('if(value===null)prop.error(this,4,"', name, '","', variant, '",value);');
}
// Processing check definition
if (config.check !== undefined)
{
code.push('var msg = "Invalid incoming value for property \''+name+'\' of class \'' + clazz.classname + '\'";');
// Accept "null"
if (config.nullable) {
code.push('if(value!==null)');
}
// Inheritable properties always accept "inherit" as value
if (config.inheritable) {
code.push('if(value!==inherit)');
}
code.push('if(');
if (this.__checks[config.check] !== undefined)
{
code.push('!(', this.__checks[config.check], ')');
}
else if (qx.Class.isDefined(config.check))
{
code.push('qx.core.Assert.assertInstance(value, qx.Class.getByName("', config.check, '"), msg)');
}
else if (qx.Interface && qx.Interface.isDefined(config.check))
{
code.push('qx.core.Assert.assertInterface(value, qx.Interface.getByName("', config.check, '"), msg)');
}
else if (typeof config.check === "function")
{
code.push('!', clazz.classname, '.$$properties.', name);
code.push('.check.call(this, value)');
}
else if (typeof config.check === "string")
{
code.push('!(', config.check, ')');
}
else if (config.check instanceof Array)
{
code.push('qx.core.Assert.assertInArray(value, ', clazz.classname, '.$$properties.', name, '.check, msg)');
}
else
{
throw new Error("Could not add check to property " + name + " of class " + clazz.classname);
}
code.push(')prop.error(this,5,"', name, '","', variant, '",value);');
}
},
"false" : undefined
}),
/**
* Emit code to store the incoming value
*
* @param code {String[]} String array to append the code to
* @param name {String} name of the property
* @param variant {String} Method variant.
* @param incomingValue {Boolean} Whether the setter has an incoming value
*/
__emitStoreValue : function(code, name, variant, incomingValue)
{
if (variant === "setRuntime")
{
code.push('this.', this.$$store.runtime[name], '=value;');
}
else if (variant === "resetRuntime")
{
code.push('if(this.', this.$$store.runtime[name], '!==unde