mirror of
https://github.com/overte-org/overte.git
synced 2025-04-27 11:36:09 +02:00
324 lines
13 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
};
|
|
|