@qooxdoo/framework
Version:
The JS Framework for Coders
538 lines (446 loc) • 15.3 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)
************************************************************************ */
/**
* Theme classes contain styling information for certain aspects of the
* graphical user interface.
*
* Supported themes are: colors, decorations, fonts, icons, appearances.
* The additional meta theme allows for grouping of the individual
* themes.
*
* For more details, take a look at the
* <a href='http://manual.qooxdoo.org/${qxversion}/pages/desktop/ui_theming.html' target='_blank'>
* documentation of the theme system in the qooxdoo manual.</a>
*/
qx.Bootstrap.define("qx.Theme",
{
statics:
{
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Theme config
*
* Example:
* <pre class='javascript'>
* qx.Theme.define("name",
* {
* aliases : {
* "aliasKey" : "resourceFolderOrUri"
* },
* extend : otherTheme,
* include : [MMixinTheme],
* patch : [MMixinTheme],
* colors : {},
* decorations : {},
* fonts : {},
* widgets : {},
* appearances : {},
* meta : {},
* boot : function(){}
* });
* </pre>
*
* For more details, take a look at the
* <a href='http://manual.qooxdoo.org/${qxversion}/pages/desktop/ui_theming.html' target='_blank'>
* documentation of the theme system in the qooxdoo manual.</a>
*
* @param name {String} name of the mixin
* @param config {Map} config structure
*/
define : function(name, config)
{
if (!config) {
var config = {};
}
config.include = this.__normalizeArray(config.include);
config.patch = this.__normalizeArray(config.patch);
// Validate incoming data
if (qx.core.Environment.get("qx.debug")) {
this.__validateConfig(name, config);
}
// Create alias
var theme =
{
$$type : "Theme",
name : name,
title : config.title,
// Attach toString
toString : this.genericToString
};
// Remember extend
if (config.extend) {
theme.supertheme = config.extend;
}
// Assign to namespace
theme.basename = qx.Bootstrap.createNamespace(name, theme);
// Convert theme entry from Object to Function (for prototype inheritance)
this.__convert(theme, config);
this.__initializeAliases(theme, config);
// Store class reference in global class registry
this.$$registry[name] = theme;
// Include mixin themes
for (var i=0, a=config.include, l=a.length; i<l; i++) {
this.include(theme, a[i]);
}
for (var i=0, a=config.patch, l=a.length; i<l; i++) {
this.patch(theme, a[i]);
}
// Run boot code
if (config.boot) {
config.boot();
}
},
/**
* Normalize an object to an array
*
* @param objectOrArray {Object|Array} Either an object that is to be
* normalized to an array, or an array, which is just passed through
*
* @return {Array} Either an array that has the original object as its
* single item, or the original array itself
*/
__normalizeArray : function(objectOrArray)
{
if (!objectOrArray) {
return [];
}
if (qx.Bootstrap.isArray(objectOrArray)) {
return objectOrArray;
} else {
return [objectOrArray];
}
},
/**
* Initialize alias inheritance
*
* @param theme {Map} The theme
* @param config {Map} config structure
*/
__initializeAliases : function(theme, config)
{
var aliases = config.aliases || {};
if (config.extend && config.extend.aliases) {
qx.Bootstrap.objectMergeWith(aliases, config.extend.aliases, false);
}
theme.aliases = aliases;
},
/**
* Return a map of all known themes
*
* @return {Map} known themes
*/
getAll : function() {
return this.$$registry;
},
/**
* Returns a theme by name
*
* @param name {String} theme name to check
* @return {Object ? void} theme object
*/
getByName : function(name) {
return this.$$registry[name];
},
/**
* Determine if theme exists
*
* @param name {String} theme name to check
* @return {Boolean} true if theme exists
*/
isDefined : function(name) {
return this.getByName(name) !== undefined;
},
/**
* Determine the number of themes which are defined
*
* @return {Number} the number of classes
*/
getTotalNumber : function() {
return qx.Bootstrap.objectGetLength(this.$$registry);
},
/*
---------------------------------------------------------------------------
PRIVATE/INTERNAL API
---------------------------------------------------------------------------
*/
/**
* This method will be attached to all themes to return
* a nice identifier for them.
*
* @internal
* @return {String} The interface identifier
*/
genericToString : function() {
return "[Theme " + this.name + "]";
},
/**
* Extract the inheritable key (could be only one)
*
* @param config {Map} The map from where to extract the key
* @return {String} the key which was found
*/
__extractType : function(config)
{
for (var i=0, keys=this.__inheritableKeys, l=keys.length; i<l; i++)
{
if (config[keys[i]]) {
return keys[i];
}
}
},
/**
* Convert existing entry to a prototype based inheritance function
*
* @param theme {Theme} newly created theme object
* @param config {Map} incoming theme configuration
*/
__convert : function(theme, config)
{
var type = this.__extractType(config);
// Use theme key from extended theme if own one is not available
if (config.extend && !type) {
type = config.extend.type;
}
// Save theme type
theme.type = type || "other";
// Create pseudo class
var clazz = function() {};
// Process extend config
if (config.extend) {
clazz.prototype = new config.extend.$$clazz;
}
var target = clazz.prototype;
var source = config[type];
// Copy entries to prototype
for (var id in source)
{
target[id] = source[id];
// Appearance themes only:
// Convert base flag to class reference (needed for mixin support)
if (target[id].base)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!config.extend) {
throw new Error("Found base flag in entry '" + id + "' of theme '" + config.name + "'. Base flags are not allowed for themes without a valid super theme!");
}
}
target[id].base = config.extend;
}
}
// store pseudo class
theme.$$clazz = clazz;
// and create instance under the old key
theme[type] = new clazz;
},
/** @type {Map} Internal theme registry */
$$registry : {},
/** @type {Array} Keys which support inheritance */
__inheritableKeys : [ "colors", "borders", "decorations", "fonts", "icons", "widgets", "appearances", "meta" ],
/** @type {Map} allowed keys in theme definition */
__allowedKeys : qx.core.Environment.select("qx.debug",
{
"true":
{
"title" : "string", // String
"aliases" : "object", // Map
"type" : "string", // String
"extend" : "object", // Theme
"colors" : "object", // Map
"borders" : "object", // Map
"decorations" : "object", // Map
"fonts" : "object", // Map
"icons" : "object", // Map
"widgets" : "object", // Map
"appearances" : "object", // Map
"meta" : "object", // Map
"include" : "object", // Array
"patch" : "object", // Array
"boot" : "function" // Function
},
"default" : null
}),
/** @type {Map} allowed keys inside a meta theme block */
__metaKeys :qx.core.Environment.select("qx.debug",
{
"true":
{
"color" : "object",
"border" : "object",
"decoration" : "object",
"font" : "object",
"icon" : "object",
"appearance" : "object",
"widget" : "object"
},
"default" : null
}),
/**
* Validates incoming configuration and checks keys and values
*
* @signature function(name, config)
* @param name {String} The name of the class
* @param config {Map} Configuration map
* @throws {Error} if the given config is not valid (e.g. wrong key or wrong key value)
*/
__validateConfig : qx.core.Environment.select("qx.debug",
{
"true": function(name, config)
{
var allowed = this.__allowedKeys;
for (var key in config)
{
if (allowed[key] === undefined) {
throw new Error('The configuration key "' + key + '" in theme "' + name + '" is not allowed!');
}
if (config[key] == null) {
throw new Error('Invalid key "' + key + '" in theme "' + name + '"! The value is undefined/null!');
}
if (allowed[key] !== null && typeof config[key] !== allowed[key]) {
throw new Error('Invalid type of key "' + key + '" in theme "' + name + '"! The type of the key must be "' + allowed[key] + '"!');
}
}
// Validate maps
var maps = [ "colors", "borders", "decorations", "fonts", "icons", "widgets", "appearances", "meta" ];
for (var i=0, l=maps.length; i<l; i++)
{
var key = maps[i];
if (config[key] !== undefined && (config[key] instanceof Array || config[key] instanceof RegExp || config[key] instanceof Date || config[key].classname !== undefined)) {
throw new Error('Invalid key "' + key + '" in theme "' + name + '"! The value needs to be a map!');
}
}
// Check conflicts (detect number ...)
var counter = 0;
for (var i=0, l=maps.length; i<l; i++)
{
var key = maps[i];
if (config[key]) {
counter++;
}
if (counter > 1) {
throw new Error("You can only define one theme category per file! Invalid theme: " + name);
}
}
// Validate meta
if (config.meta)
{
var value;
for (var key in config.meta)
{
value = config.meta[key];
if (this.__metaKeys[key] === undefined) {
throw new Error('The key "' + key + '" is not allowed inside a meta theme block.');
}
if (typeof value !== this.__metaKeys[key]) {
throw new Error('The type of the key "' + key + '" inside the meta block is wrong.');
}
if (!(typeof value === "object" && value !== null && value.$$type === "Theme")) {
throw new Error('The content of a meta theme must reference to other themes. The value for "' + key + '" in theme "' + name + '" is invalid: ' + value);
}
}
}
// Validate extend
if (config.extend && config.extend.$$type !== "Theme") {
throw new Error('Invalid extend in theme "' + name + '": ' + config.extend);
}
// Validate include
if (config.include) {
for (var i=0,l=config.include.length; i<l; i++) {
if (typeof(config.include[i]) == "undefined" || config.include[i].$$type !== "Theme") {
throw new Error('Invalid include in theme "' + name + '": ' + config.include[i]);
}
}
}
// Validate patch
if (config.patch) {
for (var i=0,l=config.patch.length; i<l; i++) {
if (typeof(config.patch[i]) === "undefined" || config.patch[i].$$type !== "Theme") {
throw new Error('Invalid patch in theme "' + name + '": ' + config.patch[i]);
}
}
}
},
"default" : function() {}
}),
/**
* Include all keys of the given mixin theme into the theme. The mixin may
* include keys which are already defined in the target theme. Existing
* features of equal name will be overwritten.
*
* @param theme {Theme} An existing theme which should be modified by including the mixin theme.
* @param mixinTheme {Theme} The theme to be included.
*/
patch : function(theme, mixinTheme)
{
this.__checkForInvalidTheme(mixinTheme);
var type = this.__extractType(mixinTheme);
if (type !== this.__extractType(theme)) {
throw new Error("The mixins '" + theme.name + "' are not compatible '" + mixinTheme.name + "'!");
}
var source = mixinTheme[type];
var target = theme.$$clazz.prototype;
for (var key in source) {
target[key] = source[key];
}
},
/**
* Include all keys of the given mixin theme into the theme. If the
* mixin includes any keys that are already available in the
* class, they will be silently ignored. Use the {@link #patch} method
* if you need to overwrite keys in the current class.
*
* @param theme {Theme} An existing theme which should be modified by including the mixin theme.
* @param mixinTheme {Theme} The theme to be included.
*/
include : function(theme, mixinTheme)
{
this.__checkForInvalidTheme(mixinTheme);
var type = mixinTheme.type;
if (type !== theme.type) {
throw new Error("The mixins '" + theme.name + "' are not compatible '" + mixinTheme.name + "'!");
}
var source = mixinTheme[type];
var target = theme.$$clazz.prototype;
for (var key in source)
{
//Skip keys already present
if (target[key] !== undefined) {
continue;
}
target[key] = source[key];
}
},
/**
* Helper method to check for an invalid theme
*
* @param mixinTheme {qx.Theme?null} theme to check
* @throws {Error} if the theme is not valid
*/
__checkForInvalidTheme: function(mixinTheme)
{
if (typeof mixinTheme === "undefined" || mixinTheme == null)
{
var errorObj = new Error("Mixin theme is not a valid theme!");
if (qx.core.Environment.get("qx.debug")) {
var stackTrace = qx.dev.StackTrace.getStackTraceFromError(errorObj);
qx.Bootstrap.error(this, stackTrace);
}
throw errorObj;
}
}
}
});