overte/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js
2017-06-11 05:42:55 -04:00

341 lines
13 KiB
JavaScript

// CustomSettingsApp.js -- manages Settings between a Client script and connected "settings" tablet WebView page
// see browser/BridgedSettings.js for the webView side
// example:
// var button = tablet.addButton({ text: 'My Settings' });
// var mySettingsApp = new CustomSettingsApp({
// namespace: 'mySettingsGroup',
// url: Script.resolvePath('myapp.html'),
// uuid: button.uuid,
// tablet: tablet
// });
//
// // settings are automatically sync'd from the web page back to Interface; to be notified when that happens, use:
// myAppSettings.valueUpdated.connect(function(name, value, oldValue, origin) {
// print('setting updated from web page', name, value, oldValue, origin);
// });
//
// // settings are also automatically sync'd from Interface back to the web page; to manually sync a value, use:
// myAppSettings.syncValue(fullSettingsKey, value);
/* eslint-env commonjs */
"use strict";
CustomSettingsApp.version = '0.0.0';
module.exports = CustomSettingsApp;
var _utils = require('../_utils.js');
Object.assign = Object.assign || _utils.assign;
function assert(truthy, message) {
return _utils.assert.call(this, truthy, 'CustomSettingsApp | ' + message);
}
function _debugPrint() {
print('CustomSettingsApp | ' + [].slice.call(arguments).join(' '));
}
var debugPrint = function() {};
function CustomSettingsApp(options) {
assert('url' in options, 'expected options.url');
if (options.debug) {
debugPrint = _debugPrint;
}
this.url = options.url;
this.namespace = options.namespace || 'BridgedSettings';
this.uuid = options.uuid || Uuid.generate();
this.recheckInterval = options.recheckInterval || 1000;
this.settingsScreenVisible = false;
this.isActive = false;
this.extraParams = Object.assign(options.extraParams || {}, {
customSettingsVersion: CustomSettingsApp.version+'',
protocolVersion: location.protocolVersion && location.protocolVersion()
});
var params = {
namespace: this.namespace,
uuid: this.uuid,
debug: options.debug || undefined
};
// encode PARAMS into '?key=value&...'
var query = Object.keys(params).map(function encodeValue(key) {
var value = encodeURIComponent(params[key] === undefined ? '' : params[key]);
return [ key, value ].join('=');
}).join('&');
this.url += '?&' + query;
this.isActiveChanged = _utils.signal(function(isActive) {});
this.valueUpdated = _utils.signal(function(key, value, oldValue, origin) {});
this.settingsAPI = options.settingsAPI || Settings;
// keep track of accessed settings so they can be kept in sync when changed on this side
this._activeSettings = {
sent: {},
received: {},
remote: {},
get: function(key) {
return {
sent: this.sent[key],
received: this.received[key],
remote: this.remote[key]
};
}
};
if (options.tablet) {
this._initialize(options.tablet);
}
}
CustomSettingsApp.prototype = {
tablet: null,
resolve: function(key) {
if (0 === key.indexOf('.') || ~key.indexOf('/')) {
// key is already qualified under a group; return as-is
return key;
}
// nest under the current namespace
return [ this.namespace, key ].join('/');
},
sendEvent: function(msg) {
assert(this.tablet, '!this.tablet');
msg.ns = msg.ns || this.namespace;
msg.uuid = msg.uuid || this.uuid;
this.tablet.emitScriptEvent(JSON.stringify(msg));
},
getValue: function(key, defaultValue) {
key = this.resolve(key);
return key in this._activeSettings.remote ? this._activeSettings.remote[key] : defaultValue;
},
setValue: function(key, value) {
key = this.resolve(key);
var current = this.getValue(key);
if (current !== value) {
return this.syncValue(key, value, 'CustomSettingsApp.setValue');
}
return false;
},
syncValue: function(key, value, origin) {
key = this.resolve(key);
var oldValue = this._activeSettings.remote[key];
assert(value !== null, 'CustomSettingsApp.syncValue value is null');
this.sendEvent({ id: 'valueUpdated', params: [key, value, oldValue, origin] });
this._activeSettings.sent[key] = value;
this._activeSettings.remote[key] = value;
this.valueUpdated(key, value, oldValue, (origin ? origin+':' : '') + 'CustomSettingsApp.syncValue');
},
onScreenChanged: function onScreenChanged(type, url) {
this.settingsScreenVisible = (url === this.url);
debugPrint('===> onScreenChanged', type, url, 'settingsScreenVisible: ' + this.settingsScreenVisible);
if (this.isActive && !this.settingsScreenVisible) {
this.isActiveChanged(this.isActive = false);
}
},
_setValue: function(key, value, oldValue, origin) {
var current = this.settingsAPI.getValue(key),
lastRemoteValue = this._activeSettings.remote[key];
debugPrint('.setValue(' + JSON.stringify({key: key, value: value, current: current, lastRemoteValue: lastRemoteValue })+')');
this._activeSettings.received[key] = value;
this._activeSettings.remote[key] = value;
var result;
if (lastRemoteValue !== value) {
this.valueUpdated(key, value, lastRemoteValue, 'CustomSettingsApp.tablet');
}
if (current !== value) {
result = this.settingsAPI.setValue(key, value);
}
return result;
},
_handleValidatedMessage: function(obj, msg) {
var tablet = this.tablet;
if (!tablet) {
throw new Error('_handleValidatedMessage called when not connected to tablet...');
}
var params = Array.isArray(obj.params) ? obj.params : [obj.params];
var parts = (obj.method||'').split('.'), api = parts[0], method = parts[1];
if (api === 'valueUpdated') {
obj.result = this._setValue.apply(this, params);
} else if (api === 'Settings' && method && params[0]) {
var key = this.resolve(params[0]), value = params[1];
debugPrint('>>>>', method, key, value);
switch (method) {
case 'getValue': {
obj.result = this.settingsAPI.getValue(key, value);
this._activeSettings.sent[key] = obj.result;
this._activeSettings.remote[key] = obj.result;
break;
}
case 'setValue': {
obj.result = this._setValue(key, value, params[2], params[3]);
break;
}
default: {
obj.error = 'unmapped Settings method: ' + method;
throw new Error(obj.error);
}
}
} else {
if (this.onUnhandledMessage) {
this.onUnhandledMessage(obj, msg);
} else {
obj.error = 'unmapped method call: ' + msg;
}
}
if (obj.id) {
// if message has an id, reply with the same message obj which now has a .result or .error field
// note: a small delay is needed because of an apparent race condition between ScriptEngine and Tablet WebViews
Script.setTimeout(_utils.bind(this, function() {
debugPrint(obj.id, '########', JSON.stringify(obj));
this.sendEvent(obj);
}), 100);
} else if (obj.error) {
throw new Error(obj.error);
}
},
onWebEventReceived: function onWebEventReceived(msg) {
// !/xSettings[.]getValue/.test(msg) &&
_debugPrint('onWebEventReceived', msg);
var tablet = this.tablet;
if (!tablet) {
throw new Error('onWebEventReceived called when not connected to tablet...');
}
if (msg === this.url) {
this.isActiveChanged(this.isActive = true);
// reply to initial HTML page ACK with any extraParams that were specified
this.sendEvent({ id: 'extraParams', extraParams: this.extraParams });
return;
}
try {
var obj = JSON.parse(msg);
var validSender = obj.ns === this.namespace && obj.uuid === this.uuid;
if (validSender) {
this._handleValidatedMessage(obj, msg);
} else {
debugPrint('xskipping', JSON.stringify([obj.ns, obj.uuid]), JSON.stringify(this), msg);
}
} catch (e) {
_debugPrint('rpc error:', e, msg);
}
},
_initialize: function(tablet) {
if (this.tablet) {
throw new Error('CustomSettingsApp._initialize called but this.tablet already has a value');
}
this.tablet = tablet;
tablet.webEventReceived.connect(this, 'onWebEventReceived');
tablet.screenChanged.connect(this, 'onScreenChanged');
this.onAPIValueUpdated = function(key, newValue, oldValue, origin) {
if (this._activeSettings.remote[key] !== newValue) {
_debugPrint(
'[onAPIValueUpdated @ ' + origin + ']',
key + ' = ' + JSON.stringify(newValue), '(was: ' + JSON.stringify(oldValue) +')',
JSON.stringify(this._activeSettings.get(key)));
this.syncValue(key, newValue, (origin ? origin+':' : '') + 'CustomSettingsApp.onAPIValueUpdated');
}
};
this.isActiveChanged.connect(this, function(isActive) {
debugPrint('============= CustomSettingsApp... isActiveChanged', JSON.stringify({
isActive: isActive,
interval: this.interval || 0,
recheckInterval: this.recheckInterval
},0,2));
this._activeSettings.remote = {}; // reset assumptions about remote values
isActive ? this.$startMonitor() : this.$stopMonitor();
});
debugPrint('CustomSettingsApp...initialized', this.namespace);
},
$syncSettings: function() {
for (var p in this._activeSettings.sent) {
var value = this.settingsAPI.getValue(p),
lastValue = this._activeSettings.remote[p];
if (value !== undefined && value !== null && value !== '' && value !== lastValue) {
_debugPrint('CustomSettingsApp... detected external settings change', JSON.stringify({
key: p,
lastValueSent: (this._activeSettings.sent[p]+''),
lastValueAssumed: (this._activeSettings.remote[p]+''),
lastValueReceived: (this._activeSettings.received[p]+''),
newValue: (value+'')
}));
this.syncValue(p, value, 'Settings');
this.valueUpdated(p, value, lastValue, 'CustomSettingsApp.$syncSettings');
}
}
},
$startMonitor: function() {
if (!(this.recheckInterval > 0)) { // expressed this way handles -1, NaN, null, undefined etc.
_debugPrint('$startMonitor -- recheckInterval <= 0; not starting settings monitor thread');
return false;
}
if (this.interval) {
this.$stopMonitor();
}
if (this.settingsAPI.valueUpdated) {
_debugPrint('settingsAPI supports valueUpdated -- binding to detect settings changes', this.settingsAPI);
this.settingsAPI.valueUpdated.connect(this, 'onAPIValueUpdated');
}
this.interval = Script.setInterval(_utils.bind(this, '$syncSettings'), this.recheckInterval);
_debugPrint('STARTED MONITORING THREAD');
},
$stopMonitor: function() {
if (this.interval) {
Script.clearInterval(this.interval);
this.interval = 0;
if (this.settingsAPI.valueUpdated) {
this.settingsAPI.valueUpdated.disconnect(this, 'onAPIValueUpdated');
}
_debugPrint('stopped monitoring thread');
return true;
}
},
cleanup: function() {
if (!this.tablet) {
return _debugPrint('CustomSettingsApp...cleanup called when not initialized');
}
var tablet = this.tablet;
tablet.webEventReceived.disconnect(this, 'onWebEventReceived');
tablet.screenChanged.disconnect(this, 'onScreenChanged');
this.$stopMonitor();
if (this.isActive) {
try {
this.isActiveChanged(this.isActive = false);
} catch (e) {
_debugPrint('ERROR: cleanup error during isActiveChanged(false)', e);
}
}
this.toggle(false);
this.settingsScreenVisible = false;
this.tablet = null;
debugPrint('cleanup completed', this.namespace);
},
toggle: function(show) {
if (!this.tablet) {
return _debugPrint('CustomSettingsApp...cleanup called when not initialized');
}
if (typeof show !== 'boolean') {
show = !this.settingsScreenVisible;
}
if (this.settingsScreenVisible && !show) {
this.tablet.gotoHomeScreen();
} else if (!this.settingsScreenVisible && show) {
Script.setTimeout(_utils.bind(this, function() {
// Interface sometimes crashes if not for adding a small timeout here :(
this.tablet.gotoWebScreen(this.url);
}), 1);
}
}
};