overte/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js
2017-06-16 16:07:50 -04:00

324 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 if changed externally
this._activeSettings = { sent: {}, received: {}, remote: {} };
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);
}
},
_apiGetValue: function(key, defaultValue) {
// trim rooted keys like "/desktopTabletBecomesToolbar" => "desktopTabletBecomesToolbar"
key = key.replace(/^\//,'');
return this.settingsAPI.getValue(key, defaultValue);
},
_apiSetValue: function(key, value) {
key = key.replace(/^\//,'');
return this.settingsAPI.setValue(key, value);
},
_setValue: function(key, value, oldValue, origin) {
var current = this._apiGetValue(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._apiSetValue(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];
switch(api) {
case 'valueUpdated': obj.result = this._setValue.apply(this, params); break;
case 'Settings':
if (method && params[0]) {
var key = this.resolve(params[0]), value = params[1];
debugPrint('>>>>', method, key, value);
switch (method) {
case 'getValue':
obj.result = this._apiGetValue(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);
}
break;
}
default: 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() {
this.sendEvent(obj);
}), 100);
} else if (obj.error) {
throw new Error(obj.error);
}
},
onWebEventReceived: function onWebEventReceived(msg) {
debugPrint('onWebEventReceived', msg);
var tablet = this.tablet;
if (!tablet) {
throw new Error('onWebEventReceived called when not connected to tablet...');
}
if (msg === this.url) {
if (this.isActive) {
// user (or page) refreshed the web view; trigger !isActive so client script can perform cleanup
this.isActiveChanged(this.isActive = false);
}
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 = assert(JSON.parse(msg));
} catch (e) {
return;
}
if (obj.ns === this.namespace && obj.uuid === this.uuid) {
debugPrint('valid onWebEventReceived', msg);
this._handleValidatedMessage(obj, 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: ' + key + ' = ' + JSON.stringify(newValue),
'(was: ' + JSON.stringify(oldValue) +')');
this.syncValue(key, newValue, (origin ? origin+':' : '') + 'CustomSettingsApp.onAPIValueUpdated');
}
};
this.isActiveChanged.connect(this, function(isActive) {
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._apiGetValue(p),
lastValue = this._activeSettings.remote[p];
if (value !== undefined && value !== null && value !== '' && value !== lastValue) {
_debugPrint('CustomSettingsApp... detected external settings change', p, value);
this.syncValue(p, value, 'Settings');
this.valueUpdated(p, value, lastValue, 'CustomSettingsApp.$syncSettings');
}
}
},
$startMonitor: function() {
if (!(this.recheckInterval > 0)) {
_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);
}
}
};