overte/unpublishedScripts/marketplace/camera-move/modules/config-utils.js
humbletim 01436675b1 * recent changes in master broke event bridge; updated to latest scheme
* fix update throttling for rapidly-changed values from the tablet UI
* include default values for several advanced settings (so if user changes the RESET button works better)
* reduce log spam
2017-06-21 15:22:32 -04:00

252 lines
10 KiB
JavaScript

// config-utils.js -- helpers for coordinating Application runtime vs. Settings configuration
//
// * ApplicationConfig -- Menu items and API values.
// * SettingsConfig -- scoped Settings values.
"use strict";
/* eslint-env commonjs */
/* global log */
module.exports = {
version: '0.0.1a',
ApplicationConfig: ApplicationConfig,
SettingsConfig: SettingsConfig
};
var _utils = require('./_utils.js'),
assert = _utils.assert;
Object.assign = Object.assign || _utils.assign;
function _debugPrint() {
print('config-utils | ' + [].slice.call(arguments).join(' '));
}
var debugPrint = function() {};
// ----------------------------------------------------------------------------
// Application-specific configuration values using runtime state / API props
//
// options.config[] supports the following item formats:
// 'settingsName': { menu: 'Menu > MenuItem'}, // assumes MenuItem is a checkbox / checkable value
// 'settingsName': { object: [ MyAvatar, 'property' ] },
// 'settingsName': { object: [ MyAvatar, 'getterMethod', 'setterMethod' ] },
// 'settingsName': { menu: 'Menu > MenuItem', object: [ MyAvatar, 'property' ] },
// 'settingsName': { get: function getter() { ...}, set: function(nv) { ... } },
function ApplicationConfig(options) {
options = options || {};
assert('namespace' in options && 'config' in options);
if (options.debug) {
debugPrint = _debugPrint;
debugPrint('debugPrinting enabled');
}
this.namespace = options.namespace;
this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});
this.config = {};
this.register(options.config);
}
ApplicationConfig.prototype = {
resolve: function resolve(key) {
assert(typeof key === 'string', 'ApplicationConfig.resolve error: key is not a string: ' + key);
if (0 !== key.indexOf('.') && !~key.indexOf('/')) {
key = [ this.namespace, key ].join('/');
}
return (key in this.config) ? key : (debugPrint('ApplicationConfig -- could not resolve key: ' + key),undefined);
},
registerItem: function(settingName, item) {
item._settingName = settingName;
item.settingName = ~settingName.indexOf('/') ? settingName : [ this.namespace, settingName ].join('/');
return this.config[item.settingName] = this.config[settingName] = new ApplicationConfigItem(item);
},
// process items into fully-qualfied ApplicationConfigItem instances
register: function(items) {
for (var p in items) {
var item = items[p];
item && this.registerItem(p, item);
}
},
_getItem: function(key) {
return this.config[this.resolve(key)];
},
getValue: function(key, defaultValue) {
var item = this._getItem(key);
if (!item) {
return defaultValue;
}
return item.get();
},
setValue: function setValue(key, value) {
key = this.resolve(key);
var lastValue = this.getValue(key, value);
var ret = this._getItem(key).set(value);
if (lastValue !== value) {
this.valueUpdated(key, value, lastValue, 'ApplicationConfig.setValue');
}
return ret;
},
// sync dual-source (ie: Menu + API) items
resyncValue: function(key) {
var item = this._getItem(key);
return item && item.resync();
},
// sync Settings values -> Application state
applyValue: function applyValue(key, value, origin) {
if (this.resolve(key)) {
var appValue = this.getValue(key, value);
debugPrint('applyValue', key, value, origin ? '['+origin+']' : '', appValue);
if (appValue !== value) {
this.setValue(key, value);
debugPrint('applied new setting', key, value, '(was:'+appValue+')');
return true;
}
}
}
};
// ApplicationConfigItem represents a single API/Menu item accessor
function ApplicationConfigItem(item) {
Object.assign(this, item);
Object.assign(this, {
_item: item.get && item,
_object: this._parseObjectConfig(this.object),
_menu: this._parseMenuConfig(this.menu)
});
this.authority = this._item ? 'item' : this._object ? 'object' : this._menu ? 'menu' : null;
this._authority = this['_'+this.authority];
debugPrint('_authority', this.authority, this._authority, Object.keys(this._authority));
assert(this._authority, 'expected item.get, .object or .menu definition; ' + this.settingName);
}
ApplicationConfigItem.prototype = {
resync: function resync() {
var authoritativeValue = this._authority.get();
if (this._menu && this._menu.get() !== authoritativeValue) {
_debugPrint(this.settingName, this._menu.menuItem,
'... menu value ('+this._menu.get()+') out of sync;',
'setting to authoritativeValue ('+authoritativeValue+')');
this._menu.set(authoritativeValue);
}
if (this._object && this._object.get() !== authoritativeValue) {
_debugPrint(this.settingName, this._object.getter || this._object.property,
'... object value ('+this._object.get()+') out of sync;',
'setting to authoritativeValue ('+authoritativeValue+')');
this._object.set(authoritativeValue);
}
},
toString: function() {
return '[ApplicationConfigItem ' + [
'setting:' + JSON.stringify(this.settingName),
'authority:' + JSON.stringify(this.authority),
this._object && 'object:' + JSON.stringify(this._object.property || this._object.getter),
this._menu && 'menu:' + JSON.stringify(this._menu.menu)
].filter(Boolean).join(' ') + ']';
},
get: function get() {
return this._authority.get();
},
set: function set(nv) {
this._object && this._object.set(nv);
this._menu && this._menu.set(nv);
return nv;
},
_raiseError: function(errorMessage) {
if (this.debug) {
throw new Error(errorMessage);
} else {
_debugPrint('ERROR: ' + errorMessage);
}
},
_parseObjectConfig: function(parts) {
if (!Array.isArray(parts) || parts.length < 2) {
return null;
}
var object = parts[0], getter = parts[1], setter = parts[2];
if (typeof object[getter] === 'function' && typeof object[setter] === 'function') {
// [ API, 'getter', 'setter' ]
return {
object: object, getter: getter, setter: setter,
get: function getObjectValue() {
return this.object[this.getter]();
},
set: function setObjectValue(nv) {
return this.object[this.setter](nv), nv;
}
};
} else if (getter in object) {
// [ API, 'property' ]
return {
object: object, property: getter,
get: function() {
return this.object[this.property];
},
set: function(nv) {
return this.object[this.property] = nv;
}
};
}
this._raiseError('{ object: [ Object, getterOrPropertyName, setterName ] } -- invalid params or does not exist: ' +
[ this.settingName, this.object, getter, setter ].join(' | '));
},
_parseMenuConfig: function(menu) {
if (!menu || typeof menu !== 'string') {
return null;
}
var parts = menu.split(/\s*>\s*/), menuItemName = parts.pop(), menuName = parts.join(' > ');
if (menuItemName && Menu.menuItemExists(menuName, menuItemName)) {
return {
menu: menu, menuName: menuName, menuItemName: menuItemName,
get: function() {
return Menu.isOptionChecked(this.menuItemName);
},
set: function(nv) {
return Menu.setIsOptionChecked(this.menuItemName, nv), nv;
}
};
}
this._raiseError('{ menu: "Menu > Item" } structure -- invalid params or does not exist: ' +
[ this.settingName, this.menu, menuName, menuItemName ].join(' | '));
}
}; // ApplicationConfigItem.prototype
// ----------------------------------------------------------------------------
// grouped configuration using the Settings.* API
function SettingsConfig(options) {
options = options || {};
assert('namespace' in options);
this.namespace = options.namespace;
this.defaultValues = {};
this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});
if (options.defaultValues) {
Object.keys(options.defaultValues)
.forEach(_utils.bind(this, function(key) {
var fullSettingsKey = this.resolve(key);
this.defaultValues[fullSettingsKey] = options.defaultValues[key];
}));
}
}
SettingsConfig.prototype = {
resolve: function(key) {
assert(typeof key === 'string', 'SettingsConfig.resolve error: key is not a string: ' + key);
return (0 !== key.indexOf('.') && !~key.indexOf('/')) ?
[ this.namespace, key ].join('/') : key;
},
getValue: function(key, defaultValue) {
key = this.resolve(key);
defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
return Settings.getValue(key, defaultValue);
},
setValue: function setValue(key, value) {
key = this.resolve(key);
var lastValue = this.getValue(key);
var ret = Settings.setValue(key, value);
if (lastValue !== value) {
this.valueUpdated(key, value, lastValue, 'SettingsConfig.setValue');
}
return ret;
},
getFloat: function getFloat(key, defaultValue) {
key = this.resolve(key);
defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
var value = parseFloat(this.getValue(key, defaultValue));
return isFinite(value) ? value : isFinite(defaultValue) ? defaultValue : 0.0;
}
};