/** * @file Graphic User Interface * * @author Alain Pitiot * @version 3.0.0b10 * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com}) * @license Distributed under the terms of the GNU General Public License (GPL). */ import { PsychoJS } from './PsychoJS'; import { ServerManager } from './ServerManager'; import { Scheduler } from '../util/Scheduler'; import { Clock } from '../util/Clock'; import * as util from '../util/Util'; /** * @class * Graphic User Interface * * @name module:core.GUI * @class * @param {PsychoJS} psychoJS the PsychoJS instance */ export class GUI { get dialogComponent() { return this._dialogComponent; } constructor(psychoJS) { this._psychoJS = psychoJS; // gui listens to RESOURCE events from the server manager: psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => { this._onResourceEvents(signal); }); } /** *

Create a dialog box that (a) enables the participant to set some * experimental values (e.g. the session name), (b) shows progress of resource * download, and (c) enables the partipant to cancel the experiment.

* * Setting experiment values *

DlgFromDict displays an input field for all values in the dictionary. * It is possible to specify default values e.g.:

* let expName = 'stroop';
* let expInfo = {'participant':'', 'session':'01'};
* psychoJS.schedule(psychoJS.gui.DlgFromDict({dictionary: expInfo, title: expName}));
*

If the participant cancels (by pressing Cancel or by closing the dialog box), then * the dictionary remains unchanged.

* * @name module:core.GUI#DlgFromDict * @function * @public * @param {Object} options * @param {Object} options.dictionary - associative array of values for the participant to set * @param {String} options.title - name of the project */ DlgFromDict({ dictionary, title }) { // get info from URL: const infoFromUrl = util.getUrlParameters(); this._progressMsg = ' '; this._progressBarMax = 0; this._OkButtonDisabled = true; // prepare PsychoJS component: this._dialogComponent = {}; this._dialogComponent.status = PsychoJS.Status.NOT_STARTED; const dialogClock = new Clock(); let self = this; let loop = () => { const t = dialogClock.getTime(); if (t >= 0.0 && self._dialogComponent.status === PsychoJS.Status.NOT_STARTED) { self._dialogComponent.tStart = t; self._dialogComponent.status = PsychoJS.Status.STARTED; // prepare jquery UI dialog box: let htmlCode = '
' + '

Fields marked with an asterisk (*) are required.

'; for (const key in dictionary) { // only create an input if the key is not in the URL: let inUrl = false; const cleanedDictKey = key.trim().toLowerCase(); for (const [urlKey, urlValue] of infoFromUrl) { const cleanedUrlKey = urlKey.trim().toLowerCase(); if (cleanedUrlKey == cleanedDictKey) { inUrl = true; break; } } if (!inUrl) { htmlCode = htmlCode + '' + ''; } } htmlCode = htmlCode + '
' + self._progressMsg + '
'; htmlCode = htmlCode + '
'; let dialogElement = document.getElementById('root'); dialogElement.innerHTML = htmlCode; // init and open dialog box: self._dialogComponent.button = 'Cancel'; $("#expDialog").dialog({ width: 400, modal: true, closeOnEscape: false, buttons: [ { id: "buttonOk", text: "Ok", disabled: self._OkButtonDisabled, click: function() { // update dictionary: for (const key in dictionary) { const input = document.getElementById(key + "_id"); if (input) dictionary[key] = input.value; } self._dialogComponent.button = 'OK'; $("#expDialog").dialog( "close" ); // switch to full screen if requested: self._psychoJS.window.adjustScreenSize(); } }, { id: "buttonCancel", text: "Cancel", click: function() { self._dialogComponent.button = 'Cancel'; $("#expDialog").dialog( "close" ); } } ], // close is called by both buttons and when the user clicks on the cross: close : function() { //$.unblockUI(); self._dialogComponent.status = PsychoJS.Status.FINISHED; } }) // change colour of title bar .prev(".ui-dialog-titlebar").css("background", "green"); // block UI until user has pressed dialog button: // note: block UI does not allow for text to be entered in the dialog form boxes, alas! //$.blockUI({ message: "", baseZ: 1}); // show dialog box: $("#expDialog").dialog("open"); $("#progressbar").progressbar({value: self._progressBarCurrentIncrement}); $("#progressbar").progressbar("option", "max", self._progressBarMax); } if (self._dialogComponent.status === PsychoJS.Status.FINISHED) return Scheduler.Event.NEXT; else return Scheduler.Event.FLIP_REPEAT; } return loop; } /** * Listener for resource event from the [Server Manager]{@link ServerManager}. * * @name module:core.GUI#_onResourceEvents * @function * @private * @param {Object.} signal the signal */ _onResourceEvents(signal) { let response = { origin: 'GUI._onResourceEvents', context: 'when handling a resource event' }; this._psychoJS.logger.debug('signal: ' + util.toString(signal)); // all resources have been registered: if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) { // for each resource, we have a 'downloading resource' and a 'resource downloaded' message: this._progressBarMax = signal.count * 2; $("#progressbar").progressbar("option", "max", this._progressBarMax); this._progressBarCurrentIncrement = 0; $("#progressMsg").text('all resources registered.'); } // all the resources have been downloaded: show the ok button else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED) { this._OkButtonDisabled = false; $("#buttonOk").button({ disabled: this._OkButtonDisabled }); $("#progressMsg").text('all resources downloaded.'); } // update progress bar: else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) { if (typeof this._progressBarCurrentIncrement === 'undefined') this._progressBarCurrentIncrement = 0; if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE) $("#progressMsg").text(signal.resource + ': downloading...'); else $("#progressMsg").text(signal.resource + ': downloaded.'); ++ this._progressBarCurrentIncrement; $("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement); } // unknown message: we just display it else $("#progressMsg").text(signal.message); } /** * @callback GUI.onOK */ /** * Show a message to the participant in a dialog box. * *

This function can be used to display both warning and error messages.

* * @name module:core.GUI#dialog * @function * @public * @param {Object} options * @param {string} options.message - the message to be displayed * @param {Object.} options.error - an exception * @param {string} options.warning - a warning message * @param {boolean} [options.showOK=true] - specifies whether to show the OK button * @param {GUI.onOK} [options.onOK] - function called when the participant presses the OK button */ dialog({ message, warning, error, showOK = true, onOK } = {}) { // destroy previous dialog box: this.destroyDialog(); // we are displaying an error: if (typeof error !== 'undefined') { this._psychoJS.logger.fatal(util.toString(error)); var htmlCode = '
'; htmlCode += '

Unfortunately we encountered an error:

'; // go through the error stack: htmlCode += '
    '; while (true) { if (typeof error === 'object' && 'context' in error) { htmlCode += '
  • ' + error.context + '
  • '; error = error.error; } else { htmlCode += '
  • ' + error + '
  • '; break; } } htmlCode += '
'; htmlCode += '

Try to run the experiment again. If the error persists, contact the experimenter.

'; var titleColour = 'red'; } // we are displaying a message: else if (typeof message !== 'undefined') { htmlCode = '
' + '

' + message + '

'; titleColour = 'green'; } // we are displaying a warning: else if (typeof warning !== 'undefined') { htmlCode = '
' + '

' + warning + '

'; titleColour = 'orange'; } htmlCode = htmlCode + '
'; var dialogElement = document.getElementById('root'); dialogElement.innerHTML = htmlCode; // init dialog box: $("#msgDialog").dialog({dialogClass: 'no-close', width: '80%', modal: true, closeOnEscape: false}) // change colour of title bar .prev(".ui-dialog-titlebar").css("background", titleColour); // add OK button if need be: if (showOK) { $("#msgDialog").dialog("option", "buttons", [ { id: "buttonOk", text: "Ok", click: function() { $(this).dialog("close"); // execute callback function: if (typeof onOK !== 'undefined') onOK(); } }]); } // show dialog box: $("#msgDialog").dialog("open"); } /** * Destroy the currently opened dialog box. * * @name module:core.GUI#dialog * @function * @public */ destroyDialog() { if ($("#expDialog").length) { $("#expDialog").dialog("destroy"); } if ($("#msgDialog").length) { $("#msgDialog").dialog("destroy"); } } }