/* CodeQ: an online programming tutor. Copyright (C) 2015 UL FRI This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ /** * Created by robert on 9/17/15. * * The prolog state of the state machine. When it is entered it'll prepare the console and code editor and load a sub-state machine which represents the 4 different parts fo the screen. */ (function() { "use strict"; var subScreens, //this will be the actual (sub)state machine stateNameTag = 'stateName', //a tag for data which is added to some html elements jqScreen = $('#screen_prolog'), // the screen container element //quadrants jqDescription = jqScreen.find('.block1'), jqCode = jqScreen.find('.block2'), jqConsole = jqScreen.find('.block3'), jqInfo = jqScreen.find('.block4'), jqAllQuadrants = jqDescription.add(jqCode).add(jqConsole).add(jqInfo), // all the quadrants // buttons jqBtnPlan = jqScreen.find('.btn-plan'), jqBtnTest = jqScreen.find('.btn-test').ladda(), jqAllButtons = jqBtnPlan.add(jqBtnTest), // all the buttons // misc currentSubState = null, transitionEventName = 'mousedown',//event name of the event which will trigger the transition between these substates - the most common transition at least (there are some corner cases on the hint and test buttons -> see the code below) substates = { 'description': { 'enter': function () { currentSubState = 'block1'; jqScreen.addClass(currentSubState); }, 'exit': function () { jqScreen.removeClass(currentSubState); currentSubState = null; } }, 'code': { 'enter': function () { currentSubState = 'block2'; jqScreen.addClass(currentSubState); }, 'exit': function () { jqScreen.removeClass(currentSubState); currentSubState = null; } }, 'info': { 'enter': function () { currentSubState = 'block4'; jqScreen.addClass(currentSubState); }, 'exit': function () { jqScreen.removeClass(currentSubState); currentSubState = null; } }, 'console': { 'enter': function () { currentSubState = 'block3'; jqScreen.addClass(currentSubState); }, 'exit': function () { jqScreen.removeClass(currentSubState); currentSubState = null; } } }; var enterFun = function(problemDef, commonDef, currentSolution){ $('#navigation-problem_list').css('display', ''); var navigationProlog = $("#navigation-prolog"); navigationProlog.addClass("active"); navigationProlog.css('display', ''); jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly prologHandler = createPrologHandler(problemDef, commonDef, currentSolution); subScreens = codeq.makeStateMachine(substates); subScreens.transition(jqDescription.data(stateNameTag)); jqAllButtons.on(transitionEventName, function (event) { subScreens.transition('info'); // set focus on the hints quadrant event.stopPropagation(); // don't allow the event to go on and trigger further transition }); jqAllQuadrants.on(transitionEventName, function () { subScreens.transition($(this).data(stateNameTag)); }); }; var prologHandler; //created when we enter the prolog state and destroyed once we leave it codeq.globalStateMachine.register('prolog', { 'jqScreen': jqScreen, 'enter': function (ref, data) { codeq.loadProblemData(ref,data).then(function(problem){ //codeq.log.debug("then:"+JSON.stringify(problem)); enterFun(problem.generalProblemData,data.commonDef,problem.solution); }) .fail(function (reason) { codeq.log.error('Failed to obtain the problem definition: ' + reason, reason); alert('Failed to obtain the problem definition: ' + reason); history.back();//TODO test }) .done(); }, 'exit': function () { jqAllButtons.off(); // unregister all event handlers jqAllQuadrants.off(); jqScreen.css('display', 'none'); prologHandler.destroy(); prologHandler = null; subScreens.destroy(); subScreens = null; jqScreen.addClass('block1'); $('#navigation-problem_list').css('display', 'none'); var navigationProlog = $("#navigation-prolog"); navigationProlog.removeClass("active"); navigationProlog.css('display', 'none'); } }); jqDescription.data(stateNameTag, 'description'); jqCode.data(stateNameTag, 'code'); jqConsole.data(stateNameTag, 'console'); jqInfo.data(stateNameTag, 'info'); // a constant var firstCharacterPos = {'line': 0, 'ch': 0}; var makePrologTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) { var promptMode = true, // default: query composition; alternative: query result browsing manualStop = false,// if the user stopped showing next answers (false) or if there are no more answers (true) terminal = codeq.makeConsole(jqConsole, { 'greeting': 'CodeQ Prolog terminal proxy', 'autoHistory': true }), tcs = function terminalCommandSuccess (data) { var t, lines, i; if (data.code === 0) { t = data.terminal; terminal.append(t.messages.join('\n'), 'output'); promptMode = !t.have_more; } else { terminal.append(data.message, 'error'); promptMode = true; } if (promptMode) { terminal.setLineBuffered(); terminal.append(manualStop ? '?- ' : '.\n?- ', 'output'); } }, tcf = function terminalCommandFailed (error) { promptMode = true; terminal.setLineBuffered(); terminal.append(error + '\n', 'error'); terminal.append('?- ', 'output'); }; terminal.onKeypress = function (c) { if (promptMode) return c; // query composition: return the character unchanged switch (c) { case ' ': case ';': case 'n': case 'r': return ';'; // show next answer on space, semicolon, 'n' or 'r' default: return '.'; // everything else: stop searching for answers } }; terminal.onInput = function (command) { if (promptMode) { if (!command) { terminal.append('?- ', 'output'); return; } promptMode = false; manualStop = false; terminal.setNotBuffered(); return codeq.comms.sendQuery({ 'problem_id': problem_id, 'step': 'run', 'program': editor.getDoc().getValue(), 'query': command, 'trace': activityHandler.addAndPurge({'typ': 'prolog_solve', 'query': command}) }, problem_id).then(tcs, tcf); } else { terminal.append('\n', 'input'); if (command == ';') { // show next answer return codeq.comms.sendQuery({ 'problem_id': problem_id, 'step': 'next', 'trace': activityHandler.addAndPurge({'typ': 'prolog_next'}) }, problem_id).then(tcs, tcf); } else { // stop searching for answers manualStop = true; return codeq.comms.sendQuery({ 'problem_id': problem_id, 'step': 'end', 'trace': activityHandler.addAndPurge({'typ': 'prolog_end'}) }, problem_id).then(tcs, tcf); } } }; terminal.leftmostCol = 3; terminal.append('?- ', 'output'); return terminal; }; codeq.on('init', function (args) { codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active }); var createPrologHandler = function (problemDef, commonDef, currentSolution) { var jqDescriptionContent = jqDescription.find('.description'), jqEditor = jqCode.find('.code_editor'), jqTerminal = jqConsole.find('.console'), jqHints = jqInfo.find('.hints'), editor = codeq.makeEditor(jqEditor[0], { mode: 'prolog', indentUnit: 4, value: currentSolution || '' }, function () { jqTerminal.click(); }), activityHandler = codeq.makeActivityHandler(editor, problemDef.id), terminal = makePrologTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'prolog_hints', problemDef, commonDef, activityHandler), commError = function (error) { alert(error); }; codeq.template.processDictionary(problemDef.translations.description, [problemDef.language, problemDef.group, problemDef.problem]); codeq.tr.registerDictionary('prolog', problemDef.translations); codeq.tr.translateDom(jqScreen); jqBtnPlan.prop('disabled', !hinter.hasNextPlan()); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)), text; text = changeObj.removed.join('\n'); if (text) { activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'txt': text}); } text = changeObj.text.join('\n'); if (text) { activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': text}); } }); jqBtnPlan.on('click', function () { if (!hinter.planNext()) { jqBtnPlan.prop('disabled', true).blur(); } }); jqBtnTest.on('click', function () { // random timeout before returning results to the user, helps // maintain the illusion var timeout = 500 + Math.random() * 1000; editor.setOption('readOnly', true); terminal.inputDisable(); jqBtnTest.ladda('start'); codeq.throttle( codeq.comms.sendTest({ 'program': editor.getDoc().getValue(), 'problem_id': problemDef.id }), timeout) .then(function (data) { if (data.code === 0) { hinter.handle(data.hints); } else { terminal.append(data.message + '\n', 'error'); } }) .fail(commError) .fin(function () { editor.setOption('readOnly', false); terminal.inputEnable(); jqBtnTest.ladda('stop'); }) .done(); }); codeq.comms.loadProblem(problemDef.id).done(); activityHandler.queueTrace({ 'typ': 'open', 'time': Date.now(), 'content': editor.getDoc().getValue() }); return { destroy: function () { codeq.comms.endProblem().done(); $('#screen_prolog .title').text('');//empty the title text jqAllButtons.off(); editor.off('change'); activityHandler.queueTrace({'typ': 'close'}); activityHandler.flush(); hinter.destroy(); terminal.destroy(); jqDescriptionContent.empty(); jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it jqTerminal.empty(); // TODO: the same with the console jqDescriptionContent = null; jqEditor = null; jqTerminal = null; jqHints = null; codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary); } }; }; })();