diff options
-rw-r--r-- | css/codeq.css | 41 | ||||
-rw-r--r-- | index.html | 227 | ||||
-rw-r--r-- | js/codemirror/panel.js | 112 | ||||
-rw-r--r-- | js/codeq/comms.js | 8 | ||||
-rw-r--r-- | js/codeq/core.js | 4 | ||||
-rw-r--r-- | js/codeq/editor.js | 30 | ||||
-rw-r--r-- | js/codeq/hint.js | 23 | ||||
-rw-r--r-- | js/codeq/language.js | 56 | ||||
-rw-r--r-- | js/codeq/login.js | 7 | ||||
-rw-r--r-- | js/codeq/navigation.js | 15 | ||||
-rw-r--r-- | js/codeq/problem.js | 193 | ||||
-rw-r--r-- | js/codeq/prolog.js | 721 | ||||
-rw-r--r-- | js/codeq/python.js | 664 | ||||
-rw-r--r-- | js/codeq/statusbar.js | 2 | ||||
-rw-r--r-- | js/codeq/translation.js | 7 |
15 files changed, 1308 insertions, 802 deletions
diff --git a/css/codeq.css b/css/codeq.css index 153e870..0e4c689 100644 --- a/css/codeq.css +++ b/css/codeq.css @@ -109,18 +109,6 @@ body { min-height: 100%; height: 100%; } - /* focus on 1st block */ - .quadrants.block1 > * > .block { min-width: 22%; width: 22%; } - .quadrants.block1 > * > .block.block1 { min-width: 34%; width: 34%; } - /* focus on 2nd block */ - .quadrants.block2 > * > .block { min-width: 22%; width: 22%; } - .quadrants.block2 > * > .block.block2 { min-width: 34%; width: 34%; } - /* focus on 3rd block */ - .quadrants.block3 > * > .block { min-width: 22%; width: 22%; } - .quadrants.block3 > * > .block.block3 { min-width: 34%; width: 34%; } - /* focus on 4th block */ - .quadrants.block4 > * > .block { min-width: 22%; width: 22%; } - .quadrants.block4 > * > .block.block4 { min-width: 34%; width: 34%; } } /* md */ @@ -135,26 +123,6 @@ body { min-height: 50%; height: 50%; } - /* focus on 1st block */ - .quadrants.block1 > * > .block1 { min-height: 60%; height: 60%; min-width: 60%; width: 60%; } - .quadrants.block1 > * > .block2 { min-height: 60%; height: 60%; min-width: 40%; width: 40%; } - .quadrants.block1 > * > .block3 { min-height: 40%; height: 40%; min-width: 60%; width: 60%; } - .quadrants.block1 > * > .block4 { min-height: 40%; height: 40%; min-width: 40%; width: 40%; } - /* focus on 2nd block */ - .quadrants.block2 > * > .block1 { min-height: 60%; height: 60%; min-width: 40%; width: 40%; } - .quadrants.block2 > * > .block2 { min-height: 60%; height: 60%; min-width: 60%; width: 60%; } - .quadrants.block2 > * > .block3 { min-height: 40%; height: 40%; min-width: 40%; width: 40%; } - .quadrants.block2 > * > .block4 { min-height: 40%; height: 40%; min-width: 60%; width: 60%; } - /* focus on 3rd block */ - .quadrants.block3 > * > .block1 { min-height: 40%; height: 40%; min-width: 60%; width: 60%; } - .quadrants.block3 > * > .block2 { min-height: 40%; height: 40%; min-width: 40%; width: 40%; } - .quadrants.block3 > * > .block3 { min-height: 60%; height: 60%; min-width: 60%; width: 60%; } - .quadrants.block3 > * > .block4 { min-height: 60%; height: 60%; min-width: 40%; width: 40%; } - /* focus on 4th block */ - .quadrants.block4 > * > .block1 { min-height: 40%; height: 40%; min-width: 40%; width: 40%; } - .quadrants.block4 > * > .block2 { min-height: 40%; height: 40%; min-width: 60%; width: 60%; } - .quadrants.block4 > * > .block3 { min-height: 60%; height: 60%; min-width: 40%; width: 40%; } - .quadrants.block4 > * > .block4 { min-height: 60%; height: 60%; min-width: 60%; width: 60%; } } /* sm */ @@ -206,6 +174,13 @@ body { border: 1px solid black; } +.editor-statusbar { + background-color: #F7F7F7; + border-top: 1px solid #DDD; + font-family: monospace; + padding: 1px 4px; + text-align: right; +} /***** helpers *****/ /* webkit-scrollbar */ @@ -240,4 +215,4 @@ ul.dropdown-menu a { /* problem index screen */ #screen_problem .language-problems a { cursor: pointer; -}
\ No newline at end of file +} @@ -36,7 +36,6 @@ <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> - <li style="display: none;" id="navigation-login"><a href="">Login</a></li> <li style="display: none;" id="navigation-language"><a href="">Language</a></li> <li style="display: none;" id="navigation-problem"><a href="">Problem</a></li> <li style="display: none;" id="navigation-python"><a href="">Python</a></li> @@ -55,12 +54,57 @@ <span class="glyphicon glyphicon glyphicon-user"></span> </a> <ul class="dropdown-menu"> - <li><a href="#">Profile</a></li> - <li><a href="#">Login or Signup</a></li> - <li><a href="#">Logout</a></li> + <li id="navigation-logout"><a href="#">Logout</a></li> + <li id="navigation-profile"><a href="#">Profile</a></li> + <li role="separator" class="divider"></li> + <li><a href="#" data-toggle="modal" data-target="#modalChangePassword">Change password</a></li> </ul> </li> - <li><a href="#"><span class="glyphicon glyphicon glyphicon-wrench" aria-hidden="true"></span></a></li> + <li class="dropdown"> + <a href="" class="dropdown-toggle" data-toggle="dropdown" id="settingsTrigger" title="Settings, options..." aria-expanded="true"><span class="glyphicon glyphicon glyphicon-wrench" aria-hidden="true"></span></a> + <div class="dropdown-menu" style="background-color: #fff"> + <form class="form col-sm-12"> + <div class="form-group"> + <span class="small">Language</span> + <a class="text-muted" data-container="body" data-toggle="popover" data-trigger="hover" data-placement="left" data-html="true" data-content="<span class='small'>Select the UI language.</span>" data-original-title="" title=""><i class="glyphicon glyphicon-question-sign"></i></a><br> + <select class="form-control"> + <option value="sl">Slovensko</option> + <option value="en">English</option> + <option value="d">Dolgooooooooooooooooooooooooooooooooooooooooooooooo</option> + </select> + </div> + <div class="form-group"> + <span class="small"> + Language + </span> + <a class="text-muted" data-container="body" data-toggle="popover" data-trigger="hover" data-placement="left" data-html="true" data-content="<span class='small'>Select the UI language.</span>" data-original-title="" title=""><i class="glyphicon glyphicon-question-sign"></i></a><br> + <div> + <div class="radio"> + <label> + <input name="lang" value="en" type="radio">English + </label> + </div> + <div class="radio"> + <label> + <input name="lang" value="sl" type="radio">Slovensko + </label> + </div> + <div class="radio"> + <label> + <input name="lang" value="d" type="radio">Dolgooooooooooooooooooooooooooooooooooooooooooooooo + </label> + </div> + </div> + </div> + <div class="form-group"> + <div class="text-right"> + <button type="button" class="btn btn-default" data-target="settingsDropdown" data-toggle="dropdown">Close</button> + <button type="button" class="btn btn-primary" data-target="settingsDropdown" data-toggle="dropdown">Save</button> + </div> + </div> + </form> + </div> + </li> </ul> </div><!--/.nav-collapse --> @@ -71,14 +115,20 @@ <div class="container" id="screen_login"> <form class="form-signin"> <h2 class="form-signin-heading">Please login</h2> - <label for="username" class="sr-only">Username</label> - <input type="text" id="username" class="form-control" placeholder="Username" required="" autofocus=""> - <label for="password" class="sr-only">Password</label> - <input type="password" id="password" class="form-control" placeholder="Password" required=""> - <button class="btn btn-lg btn-default btn-block" type="button" id="submit">Login</button> + <div class="form-group"> + <label for="username" class="sr-only">Username</label> + <input type="text" id="username" class="form-control" placeholder="Username" required="" autofocus=""> + <label for="password" class="sr-only">Password</label> + <input type="password" id="password" class="form-control" placeholder="Password" required=""> + <button class="btn btn-lg btn-default btn-block" type="button" id="submit">Login</button> + </div> + <div class="form-group"> + <a href="" data-dismiss="modal" data-toggle="modal" role="button" data-target="#modalSignIn">New User? Sign-in..</a> + </div> </form> </div> + <!-- main screen: programming language selection, settings, etc. --> <div class="container" id="screen_language" style="text-align: center; display: none;"> <div class="row"> @@ -92,7 +142,7 @@ <h2>Python</h2> <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.</p> </div><!-- /.col-lg-4 --> - <div class="col-lg-4" id="choose-python"> + <div class="col-lg-4" id="choose-robot"> <img class="img-circle" src=res/eve.png alt="Generic placeholder image" width="140" height="140"> <h2>Robot</h2> <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.</p> @@ -107,8 +157,8 @@ <div class="container-fluid quadrants block1" id="screen_prolog" style="display: none;"> <div class="row"> <div class="col-lg-3 col-md-6 col-sm-12 block block1"> - <h2 class="title"></h2> - <div class="description"></div> + <h2 class="title translatable" data-dict="prolog" data-tkey="slug"></h2> + <div class="description translatable" data-dict="prolog" data-tkey="description"></div> <div class="block-label">Instructions</div> </div> <div class="col-lg-3 col-md-6 col-sm-12 block block2"> @@ -137,8 +187,8 @@ <div class="container-fluid quadrants block1" id="screen_python" style="display: none;"> <div class="row"> <div class="col-lg-3 col-md-6 col-sm-12 block block1"> - <h2 class="title"></h2> - <div class="description"></div> + <h2 class="title translatable" data-dict="python" data-tkey="slug"></h2> + <div class="description translatable" data-dict="python" data-tkey="description"></div> <div class="block-label">Instructions</div> </div> <div class="col-lg-3 col-md-6 col-sm-12 block block2"> @@ -165,6 +215,150 @@ </div><!--/row--> </div><!--container--> + <!-- problem screen: profile --> + <div class="container-fluid" id="screen_profile" style="display: none;"> + <h2> + Profile + <div class="btn-group btn-group-xs hidden-md pull-right"> + <a href="" data-toggle="modal" data-target="#modalChangePassword" class="btn btn-default">Change Password</a> + <a href="#" class="btn btn-default">Go back</a> + </div> + </h2> + <hr> + <div class="row"> + <div class="col-lg-3 col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + Overview + </div> + <ul class="list-group small"> + <li class="list-group-item text-right"><span class="pull-left"><strong>Username</strong></span>markopusnik</li> + <li class="list-group-item text-right"><span class="pull-left"><strong>Real name</strong></span>Marko Pušnik</li> + <li class="list-group-item text-right"><span class="pull-left"><strong>Joined</strong></span>Sep 29, 2015</li> + <li class="list-group-item text-right"><span class="pull-left"><strong>Last seen</strong></span>1 hour ago</li> + </ul> + </div> + + <div class="panel panel-default"> + <div class="panel-heading"> + Statistics + </div> + <ul class="list-group small"> + <li class="list-group-item"><strong>Points</strong> <span class="label label-default pull-right">87</span></li> + <li class="list-group-item"><strong>Prolog</strong> <span class="label label-default pull-right">10</span></li> + <li class="list-group-item"><strong>Python</strong> <span class="label label-default pull-right">10</span></li> + <li class="list-group-item"><strong>Robot</strong> <span class="label label-default pull-right">10</span></li> + </ul> + </div> + </div><!--/col-3--> + + <div class="col-lg-9 col-md-8"> + <ul class="nav nav-tabs" id="profileTab"> + <li class="active"><a href="#" class="text-primary" data-target="#profileSettings" data-toggle="tab">Edit Profile</a></li> + <li><a href="#" class="text-primary" data-target="#profile2" data-toggle="tab">Neki</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="profileSettings"> + <br> + <form class="form"> + <div class="col-lg-6"> + <div class="form-group"> + <label><h4>Username</h4></label> + <input type="text" class="form-control input-lg disabled" disabled="disabled" name="username" value="markopusnik"> + </div> + </div> + <div class="col-lg-6"> + <div class="form-group"> + <label><h4>Real name</h4></label> + <input type="text" class="form-control input-lg" name="realName" placeholder="First & last name" maxlength="50"> + </div> + </div><!--/col--> + <div class="col-lg-12"> + <div class="form-group"> + <label><h4>Email</h4></label> + <input type="email" class="form-control input-lg disabled" disabled="disabled" name="email" placeholder="Email address (private)" required="required"> + </div> + </div><!--/col--> + <div class="col-lg-12"> + <div class="form-group"> + <button class="btn btn-lg btn-primary pull-right" type="submit">Save</button> + </div> + </div><!--/col--> + </form> + </div><!--/tab-pane--> + + <div class="tab-pane" id="profile2"> + <h2 class="text-center">Todo...</h2> + </div><!--/tab-pane--> + + </div><!--/tab-content--> + </div><!--/col-9--> + </div><!--row--> + </div><!--container--> + + <!-- Modals --> + <div id="modalSignIn" class="modal fade in" tabindex="-1" role="dialog" aria-hidden="false" style="display: none;"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h2 class="text-center">Sign-in</h2> + </div> + <div class="modal-body"> + <form class="form" method="post"> + <div class="form-group"> + <label class="control-label">Email</label> + <input class="form-control" name="email" id="inputEmail" type="email" data-ng-model="newuser.email" placeholder="email@you.com (kept private)" required=""> + </div> + <div class="form-group" bs-has-error=""> + <label class="control-label">Username</label> + <input class="form-control" name="username" id="inputUsername" type="text" data-ng-model="newuser.username" placeholder="desired username" pattern="^[a-z,A-Z,0-9,_]{5,15}$" data-valid-min="5" title="Choose a alpha-numeric username of 5-15 characters" required=""> + </div> + <div class="form-group"> + <label class="control-label">Password</label> + <input class="form-control" name="password" id="inputpassword" type="password" data-ng-model="newuser.password" placeholder="password" pattern="^[a-z,A-Z,0-9,_]{6,15}$" data-valid-min="6" title="Choose a alpha-numeric password of a least 6 characters" required=""> + </div> + <div class="form-group"> + <label class="control-label">Verify (repeat password)</label> + <input class="form-control" name="verify" id="inputVerify" type="password" data-ng-model="newuser.verify" placeholder="password (again)" pattern="^[a-z,A-Z,0-9,_]{6,15}$" data-valid-min="6" title="Choose a alpha-numeric password of a least 6 characters" required=""> + </div> + </form><!--/row--> + </div><!--/modal-body--> + <div class="modal-footer"> + <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button> + <button class="btn btn-lg btn-primary" id="btnRegister" type="submit">Sign</button> + </div><!--/modal-footer--> + </div> + </div> + </div> + + <div id="modalChangePassword" class="modal fade in" tabindex="-1" role="dialog" aria-hidden="false" style="display: none;"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h2 class="text-center">Password Change</h2> + </div> + <div class="modal-body"> + <form class="form" method="post"> + <div class="form-group"> + <label>New Password</label> + <input class="form-control input-lg" placeholder="Enter a new password" name="new" type="password"> + </div> + <div class="form-group"> + <label>Verify Password</label> + <input class="form-control input-lg" placeholder="Repeat the password again" name="verify" type="password"> + </div> + </form><!--/row--> + </div><!--/modal-body--> + <div class="modal-footer"> + <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button> + <button class="btn btn-lg btn-primary" id="btnPasswdChange" type="submit">Change</button> + </div><!--/modal-footer--> + </div> + </div> + </div> + <!-- Covers the whole screen with a semi-transparent block, so no input events can be triggered below. Used as a wait screen; must be the last element in the DOM. It is active after load, javascript turns it off after initialization. --> @@ -184,6 +378,7 @@ <!-- CodeMirror stuff --> <script src="js/codemirror/codemirror.js"></script> <script src="js/codemirror/matchbrackets.js"></script> + <script src="js/codemirror/panel.js"></script> <script src="js/codemirror/prolog.js"></script> <script src="js/codemirror/python.js"></script> <script src="js/codemirror/show-hint.js"></script> @@ -194,10 +389,12 @@ <script src="js/codeq/navigation.js"></script> <script src="js/codeq/comms.js"></script> <script src="js/codeq/console.js"></script> + <script src="js/codeq/editor.js"></script> <script src="js/codeq/hint.js"></script> <script src="js/codeq/prolog.js"></script> <script src="js/codeq/python.js"></script> <script src="js/codeq/login.js"></script> + <script src="js/codeq/profile.js"></script> <script src="js/codeq/language.js"></script> <script src="js/codeq/problem.js"></script> </body> diff --git a/js/codemirror/panel.js b/js/codemirror/panel.js new file mode 100644 index 0000000..ba29484 --- /dev/null +++ b/js/codemirror/panel.js @@ -0,0 +1,112 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineExtension("addPanel", function(node, options) { + options = options || {}; + + if (!this.state.panels) initPanels(this); + + var info = this.state.panels; + var wrapper = info.wrapper; + var cmWrapper = this.getWrapperElement(); + + if (options.after instanceof Panel && !options.after.cleared) { + wrapper.insertBefore(node, options.before.node.nextSibling); + } else if (options.before instanceof Panel && !options.before.cleared) { + wrapper.insertBefore(node, options.before.node); + } else if (options.replace instanceof Panel && !options.replace.cleared) { + wrapper.insertBefore(node, options.replace.node); + options.replace.clear(); + } else if (options.position == "bottom") { + wrapper.appendChild(node); + } else if (options.position == "before-bottom") { + wrapper.insertBefore(node, cmWrapper.nextSibling); + } else if (options.position == "after-top") { + wrapper.insertBefore(node, cmWrapper); + } else { + wrapper.insertBefore(node, wrapper.firstChild); + } + + var height = (options && options.height) || node.offsetHeight; + this._setSize(null, info.heightLeft -= height); + info.panels++; + return new Panel(this, node, options, height); + }); + + function Panel(cm, node, options, height) { + this.cm = cm; + this.node = node; + this.options = options; + this.height = height; + this.cleared = false; + } + + Panel.prototype.clear = function() { + if (this.cleared) return; + this.cleared = true; + var info = this.cm.state.panels; + this.cm._setSize(null, info.heightLeft += this.height); + info.wrapper.removeChild(this.node); + if (--info.panels == 0) removePanels(this.cm); + }; + + Panel.prototype.changed = function(height) { + var newHeight = height == null ? this.node.offsetHeight : height; + var info = this.cm.state.panels; + this.cm._setSize(null, info.height += (newHeight - this.height)); + this.height = newHeight; + }; + + function initPanels(cm) { + var wrap = cm.getWrapperElement(); + var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle; + var height = parseInt(style.height); + var info = cm.state.panels = { + setHeight: wrap.style.height, + heightLeft: height, + panels: 0, + wrapper: document.createElement("div") + }; + wrap.parentNode.insertBefore(info.wrapper, wrap); + var hasFocus = cm.hasFocus(); + info.wrapper.appendChild(wrap); + if (hasFocus) cm.focus(); + + cm._setSize = cm.setSize; + if (height != null) cm.setSize = function(width, newHeight) { + if (newHeight == null) return this._setSize(width, newHeight); + info.setHeight = newHeight; + if (typeof newHeight != "number") { + var px = /^(\d+\.?\d*)px$/.exec(newHeight); + if (px) { + newHeight = Number(px[1]); + } else { + info.wrapper.style.height = newHeight; + newHeight = info.wrapper.offsetHeight; + info.wrapper.style.height = ""; + } + } + cm._setSize(width, info.heightLeft += (newHeight - height)); + height = newHeight; + }; + } + + function removePanels(cm) { + var info = cm.state.panels; + cm.state.panels = null; + + var wrap = cm.getWrapperElement(); + info.wrapper.parentNode.replaceChild(wrap, info.wrapper); + wrap.style.height = info.setHeight; + cm.setSize = cm._setSize; + cm.setSize(); + } +}); diff --git a/js/codeq/comms.js b/js/codeq/comms.js index 2d934bd..5ac3d66 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -164,8 +164,8 @@ // AJAX communication support functions (loading of static web resources) // ================================================================================ - var languageCache = {},// language defs, keyed by language identifier - problemCache = {},// problem cache, 3-level, keyed by: language, problem group, and problem identifier + var languageCache = {}, // language defs, keyed by language identifier + problemCache = {}, // problem cache, 3-level, keyed by: language, problem group, and problem identifier ajaxGet = function (url) { return Q.Promise(function (resolve, reject, notify) { $.ajax({ @@ -407,6 +407,10 @@ ); languageCache[identifier] = x; return x; + }, + + 'getProblemDef': function (language, group, problem) { + return ajaxGet(ajaxPrefix + language + '/' + group + '/' + problem + '/problem.json'); } }; })(); diff --git a/js/codeq/core.js b/js/codeq/core.js index ef3648f..01382d1 100644 --- a/js/codeq/core.js +++ b/js/codeq/core.js @@ -391,5 +391,7 @@ codeq.setLang(lang || 'en'); // initial language setting // go to login codeq.globalStateMachine.transition('login'); - }); + + //For performance reasons, the Tooltip and Popover data-apis are opt-in, meaning you must initialize them yourself. + $('[data-toggle="popover"]').popover()}); })(); diff --git a/js/codeq/editor.js b/js/codeq/editor.js new file mode 100644 index 0000000..e7d15f9 --- /dev/null +++ b/js/codeq/editor.js @@ -0,0 +1,30 @@ +codeq.makeEditor = function (elt, options) { + var statusBar = document.createElement("div"), + updateStatusBar = function (pos) { + statusBar.innerHTML = 'line ' + (pos.line+1) + ', column ' + (pos.ch+1); + }, + editor; + + options.cursorHeight = 0.85; + options.lineNumbers = true; + options.matchBrackets = true; + options.extraKeys = { + // replace tabs with spaces + Tab: function (cm) { + var spaces = Array(cm.getOption("indentUnit") + 1).join(" "); + cm.replaceSelection(spaces); + } + }; + editor = CodeMirror(elt, options), + + statusBar.className = 'editor-statusbar'; + updateStatusBar({line: 0, ch: 0}); + + editor.addPanel(statusBar, {position: 'bottom'}); + editor.on('cursorActivity', function (instance) { + var pos = instance.getDoc().getCursor(); + updateStatusBar(pos); + }); + + return editor; +}; diff --git a/js/codeq/hint.js b/js/codeq/hint.js index e1c7a6b..1b97afc 100644 --- a/js/codeq/hint.js +++ b/js/codeq/hint.js @@ -7,10 +7,11 @@ var firstCharacterPos = {'line': 0, 'ch': 0}, sel_no_scroll = {'scroll': false}; - codeq.makeHinter = function (jqHints, jqEditor, editor, hintDefs, planDef) { + codeq.makeHinter = function (jqHints, jqEditor, editor, trNamespace, hintDefsA, commonHintDefsA, planDef) { var hintCounter = 0, // for generating unique class-names hintCleaners = [], planIdx = 0, + dictionary = [], clearHints = function () { var i; @@ -130,6 +131,25 @@ } }; + codeq.tr.registerDictionary(trNamespace, dictionary); + + // TODO: below is a temporary code to bridge the old implementation with the new data format + if (planDef.sl) planDef = planDef.sl; + else planDef = planDef.en || []; + var hintDefs = {}, t1, t2, k; + if (hintDefsA.sl) t1 = hintDefsA.sl; + else t1 = hintDefsA.en || {}; + if (commonHintDefsA.sl) t2 = commonHintDefsA.sl; + else t2 = commonHintDefsA.en || {}; + for (k in t2) { + if (!t2.hasOwnProperty(k)) continue; + hintDefs[k] = t2[k]; + } + for (k in t1) { + if (!t1.hasOwnProperty(k)) continue; + hintDefs[k] = t1[k]; + } + return { /** Display the next "planning" hint and return whether there are * any more available. @@ -195,6 +215,7 @@ 'destroy': function () { clearHints(); + codeq.tr.unregisterDictionary(trNamespace); jqHints.empty(); jqHints = null; jqEditor = null; diff --git a/js/codeq/language.js b/js/codeq/language.js index be2a319..aac4e78 100644 --- a/js/codeq/language.js +++ b/js/codeq/language.js @@ -1,30 +1,28 @@ -/** - * Created by robert on 9/18/15. - */ - -(function(){ - var jqScreen = $('#screen_language'), - jqProlog = $('#choose-prolog'), - jqPython = $('#choose-python'), - chooseProlog = function () {codeq.globalStateMachine.transition('problem', 'prolog');}, - choosePython = function () {codeq.globalStateMachine.transition('problem', 'python');}; - - codeq.globalStateMachine.register('language',{ - 'enter': function(){ - $('#navigation-login').css('display', ''); - $("#navigation-language").addClass("active").css('display', ''); - - jqScreen.css('display', ''); - jqProlog.on('click', chooseProlog); - jqPython.on('click', choosePython); - }, - 'exit' : function(){ - jqProlog.off(); - jqPython.off(); - jqScreen.css('display', 'none'); - - $('#navigation-login').css('display', 'none'); - $('#navigation-language').css('display', 'none').removeClass("active"); - } - }); +/**
+ * Created by robert on 9/18/15.
+ */
+
+(function(){
+ var jqScreen = $('#screen_language'),
+ jqProlog = $('#choose-prolog'),
+ jqPython = $('#choose-python'),
+ chooseProlog = function () {codeq.globalStateMachine.transition('problem', 'prolog');},
+ choosePython = function () {codeq.globalStateMachine.transition('problem', 'python');};
+
+ codeq.globalStateMachine.register('language',{
+ 'enter': function(){
+ $("#navigation-language").addClass("active").css('display', '');
+
+ jqScreen.css('display', '');
+ jqProlog.on('click', chooseProlog);
+ jqPython.on('click', choosePython);
+ },
+ 'exit' : function(){
+ jqProlog.off();
+ jqPython.off();
+ jqScreen.css('display', 'none');
+
+ $('#navigation-language').css('display', 'none').removeClass("active");
+ }
+ });
})();
\ No newline at end of file diff --git a/js/codeq/login.js b/js/codeq/login.js index 02a6517..e3204d3 100644 --- a/js/codeq/login.js +++ b/js/codeq/login.js @@ -37,9 +37,7 @@ codeq.globalStateMachine.register('login',{ 'enter': function(){ $("#submit").on('click', loginFun); - - $("#navigation-login").addClass("active"); - $('#navigation-login').css('display', ''); + //$('#modalLogin').modal(); $("#screen_login").css('display', ''); $('#disabled').css('display', 'none'); @@ -48,9 +46,6 @@ $("#submit").off('click', loginFun); $("#screen_login").css('display', 'none'); $("#password").val(''); - - $('#navigation-login').css('display', 'none'); - $("#navigation-login").removeClass("active"); } }); })();
\ No newline at end of file diff --git a/js/codeq/navigation.js b/js/codeq/navigation.js index 4445a3d..80fd321 100644 --- a/js/codeq/navigation.js +++ b/js/codeq/navigation.js @@ -57,7 +57,7 @@ History.pushState({'state': name, 'params': Array.prototype.slice.apply(arguments, [1])}, null, '?s=' + name); } catch (e) { - codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e); + codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e, e); } }, 'destroy': function () { @@ -81,10 +81,6 @@ //setup all the buttons in the banner - $('#navigation-login').on('click', function(e){ - codeq.globalStateMachine.transition('login'); - e.preventDefault();//prevent this since we'll trigger a page reload otherwise - }); $('#navigation-language').on('click', function(e){ codeq.globalStateMachine.transition('language'); e.preventDefault(); @@ -101,4 +97,13 @@ codeq.globalStateMachine.transition('prolog'); e.preventDefault(); }); + $('#navigation-logout').on('click', function(e){ + codeq.globalStateMachine.transition('login'); + e.preventDefault();//prevent this since we'll trigger a page reload otherwise + }); + $('#navigation-profile').on('click', function(e){ + codeq.globalStateMachine.transition('profile'); + e.preventDefault();//prevent this since we'll trigger a page reload otherwise + }); + })();
\ No newline at end of file diff --git a/js/codeq/problem.js b/js/codeq/problem.js index 3e218d8..7ae54d8 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -2,8 +2,93 @@ var jqScreen = $('#screen_problem'), languageCache = {}, // keyed by language identifier: processed data about languages translationCache = [], // keys are autogenerated in ta(), a value is a dictionary of translations of a translation key for every language + problemCache = {}, // problem data cache, 3-level, keyed by: language, problem group, and problem identifier langs, Nlangs, // constants, set on init + // ================================================================================ + // Hint processing: extract hints from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processHints = function (rawTranslations) { + var defaultHint = {}, // here we put all the hints with their default translations + allHints = {}, // the result + allHintKeys = [], + tr, key, i, lang, hint, h, j; + // find the default hint translations, they will form the basis of default hints + tr = chooseDefaultTranslation(rawTranslations, 'hint') || {}; + for (key in tr) { // copy the hints + if (!tr.hasOwnProperty(key)) continue; + defaultHint[key] = tr[key]; + allHintKeys.push(key); + } + // copy any hints not in the default hints to the default hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + if (!tr || !tr.hint) continue; // skip unavailable translations or translations with no hints + hint = tr.hint; + for (key in hint) { + if (!hint.hasOwnProperty(key) || !hint[key]) continue; + if (!key in defaultHint) { + defaultHint[key] = hint[key]; + allHintKeys.push(key); + } + } + } + // create all translations for hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up hints + if (!tr || !tr.hint) { + // there's no hint in the current language, copy the default in its entirety + allHints[lang] = defaultHint; + } + else { + // make a copy of all hints, using the default hint value where a hint value is missing + hint = {}; + allHints[lang] = hint; + h = tr.hint; + for (j = allHintKeys.length; j >= 0; j--) { + key = allHintKeys[j]; + hint[key] = h[key] || defaultHint[key]; + } + } + } + return allHints; + }, + + // ================================================================================ + // Plan processing: extract plans from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processPlans = function (rawTranslations) { + // find the default plan translation + var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan') || [], + allPlans = {}, // the result + i, lang, tr; + // create all translations for plan + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up plan + if (!tr || !tr.plan) { + // there's no plan in the current language, copy the default plan + allPlans[lang] = defaultPlan; + } + else { + allPlans[lang] = tr.plan; + } + } + return allPlans; + }, + + // ================================================================================ + // Group + problems directory processing + // ================================================================================ + chooseTranslation = function (keyword, lang, currentDict, enDict, translations) { var tr = currentDict[keyword], otherLang; @@ -65,50 +150,56 @@ */ createLanguageData = function (data, languageIdentifier) { // data is the content of language.json var li = languageIdentifier, // a shorthand - groups = data.groups || {}, + rawTranslations = data.translations || {}, + groups = data.groups || [], + Ngroups = groups.length, html = [], problemReferences = [], - groupIdentifier, group, problems, problemIdentifier, problem; - var langDict = convertTranslations(data.translations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict + group, problems, Nproblems, problem, i, j; + var langDict = convertTranslations(rawTranslations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict groupDict, problemDict; // title: HTML structure for "name" and "desc" html.push('<h1 class="language-title translatable" ', ta(langDict.name), '></h1><hr>'); html.push('<div class="language-description translatable" ', ta(langDict.description), '></div>'); // content: problem directory html.push('<ul class="language-problems">'); - for (groupIdentifier in groups) { - if (!groups.hasOwnProperty(groupIdentifier)) continue; - group = groups[groupIdentifier] || {}; + for (i = 0; i < Ngroups; i++) { + group = groups[i] || {}; groupDict = convertTranslations(group.translations, 'name', 'description'); // the group-level translations, added will // group content html.push('<li><div class="group-title translatable" ', ta(groupDict.name), '></div>'); html.push('<div class="group-description translatable" ', ta(groupDict.description), '></div>'); html.push('<ul class="group-problems">'); // problem content - problems = group.problems || {}; - for (problemIdentifier in problems) { - if (!problems.hasOwnProperty(problemIdentifier)) continue; - problem = problems[problemIdentifier] || {}; + problems = group.problems || []; + Nproblems = problems.length; + for (j = 0; j < Nproblems; j++) { + problem = problems[j] || {}; problemDict = convertTranslations(problem.translations, 'name'); html.push('<li><a class="problem-', '' + problemReferences.length, ' translatable" ', ta(problemDict.name), '></a></li>'); - problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier}); + problemReferences.push({'g': group.identifier || 'nogroup', 'p': problem.identifier || 'noproblem', 'id': problem.id}); } html.push('</ul></li>'); } html.push('</ul>'); return { - 'language': languageIdentifier, - 'html': html.join(''), - 'refs': problemReferences, - 'hints': {} // TODO: prepare common hints for the language + 'language': languageIdentifier, // 'prolog', 'python', ... + 'html': html.join(''), // the DOM structure (without textual content), as HTML text + 'refs': problemReferences, // array of problem info {g: group, p: problem, id: problem_id}, referenced from DOM <a> elements + 'hints': processHints(rawTranslations) // hint translations: keyword -> lang -> value }; }, + // ================================================================================ + // DOM instantiation. + // The transition to problem solving takes place here, after the user clicks on a + // problem and all the required data is loaded. + // ================================================================================ /** * Instantiates the screen from the given processed data. */ - createDom = function (data) { + createDom = function (data) { // data is the (cached) result of createLanguageData() var language = data.language; jqScreen.html(data.html); codeq.tr.translateDom(jqScreen); @@ -119,12 +210,15 @@ codeq.log.error('Clicked on a problem link having erroneous index: ' + index); return; } - // transition codeq.wait( - codeq.comms.getProblem(language, ref.g, ref.p) - .then(function (data) { - if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message); - codeq.globalStateMachine.transition(language, data); + Q.all([ + codeq.comms.getProblem(language, ref.g, ref.p), // TODO: use ref.id instead // the current solution + getProblemData(language, ref.g, ref.p) // the (cached) result of processProblemData() + ]) + .spread(function (userProblemData, generalProblemData) { + if (userProblemData.code !== 0) throw new Error('Failed to obtain user problem data, code: ' + userProblemData.code + ', message: ' + userProblemData.message); + if (!generalProblemData) throw new Error('General problem data is not defined'); + codeq.globalStateMachine.transition(language, generalProblemData, data.hints, userProblemData.solution); }) ) .fail(function (reason) { @@ -134,6 +228,61 @@ .done(); }); }, + + // ================================================================================ + // Problem definition processing + // ================================================================================ + + chooseDefaultTranslation = function (rawTranslations, translationKey) { + var tr = rawTranslations.en, // try English as the default + lang; + if (tr && tr[translationKey]) return tr[translationKey]; + for (lang in rawTranslations) { // find a translation with hints + if (!rawTranslations.hasOwnProperty(lang) || rawTranslations[lang]) continue; + tr = rawTranslations[lang]; + if (tr[translationKey]) return tr[translationKey]; + } + return null; // default must be chosen by the caller + }, + + processProblemData = function (rawData, language, group, problem) { + var rawTranslations = rawData.translations || {}; + return { + 'language': language, + 'group': group, + 'problem': problem, + 'id': rawData.id, + 'translations': convertTranslations(rawTranslations, 'title', 'name', 'slug', 'description'), // GUI translations: keyword -> lang -> value + 'hint': processHints(rawTranslations), // hint translations: keyword -> lang -> value + 'plan': processPlans(rawTranslations) // plan translations: keyword -> lang -> value + }; + }, + + getProblemData = function (language, group, problem) { + var langCache = problemCache[language], + groupCache, cachedProblem, promise; + if (langCache) { + groupCache = langCache[group]; + if (!groupCache) { + groupCache = {}; + langCache[group] = groupCache; + } + } + else { + langCache = {}; + problemCache[language] = langCache; + groupCache = {}; + langCache[group] = groupCache; + } + cachedProblem = groupCache[problem]; + if (cachedProblem) return Q(cachedProblem); + return codeq.comms.getProblemDef(language, group, problem).then(function (rawData) { + var data = processProblemData(rawData); + groupCache[problem] = data; + return data; + }); + }, + currentLanguage; // the currently active language // ================================================================================ @@ -154,7 +303,6 @@ 'enter': function(language){ var data = null; // language data - $('#navigation-login').css('display', ''); $('#navigation-language').css('display', ''); $("#navigation-problem").addClass("active").css('display', ''); @@ -180,7 +328,6 @@ }, 'exit' : function(){ jqScreen.css('display', 'none'); - $('#navigation-login').css('display', 'none'); $('#navigation-language').css('display', 'none'); $('#navigation-problem').css('display', 'none').removeClass("active"); } diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js index be6b0c8..87ac353 100644 --- a/js/codeq/prolog.js +++ b/js/codeq/prolog.js @@ -1,357 +1,364 @@ -/**
- * 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() {
- 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'),
- jqBtnHint = jqScreen.find('.btn-hint'),
- jqBtnTest = jqScreen.find('.btn-test'),
- jqAllButtons = jqBtnPlan.add(jqBtnHint).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 prologHandler; //created when we enter the prolog state and destroyed once we leave it
- codeq.globalStateMachine.register('prolog', {
- 'enter': function (data) {
- $('#navigation-login').css('display', '');
- $('#navigation-language').css('display', '');
- $('#navigation-problem').css('display', '');
- $("#navigation-prolog").addClass("active");
- $('#navigation-prolog').css('display', '');
-
- jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly
- prologHandler = createPrologHandler(data.data);
- subScreens = codeq.makeStateMachine(substates);
- subScreens.transition(jqDescription.data(stateNameTag));
-/* Q.delay(100).then(function(){
- jqAllQuadrants.addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading
- }).done();*/
- 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));
- });
- },
- 'exit': function () {
- jqAllButtons.off(); // unregister all event handlers
- jqAllQuadrants.off();
- jqScreen.css('display', 'none');
-// jqAllQuadrants.removeClass('transition');
- prologHandler.destroy();
- prologHandler = null;
- subScreens.destroy();
- subScreens = null;
- jqScreen.addClass('block1');
-
- $('#navigation-login').css('display', 'none');
- $('#navigation-language').css('display', 'none');
- $('#navigation-problem').css('display', 'none');
- $("#navigation-prolog").removeClass("active");
- $('#navigation-prolog').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) {
- 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': 'slv', 'qry': 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': 'nxt'})
- }, 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': 'stp'})
- }, problem_id).then(tcs, tcf);
- }
-
- }
- };
-
- terminal.leftmostCol = 3;
- terminal.append('?- ', 'output');
-
- return terminal;
- };
-
- var makeActivityHandler = function (editor, problem_id) {
- var lastActivityMillis = Date.now(),
- deltaActivityMillis = function deltaActivityMillisFunc () {
- var now = Date.now(),
- dt = now - lastActivityMillis;
- lastActivityMillis = now;
- return dt;
- },
- queue = [],
- ts = null,
- timer = function () {
- var promise;
- ts = null;
- if (queue.length === 0) return Q(true);
- promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
- queue.length = 0;
- return promise;
- },
- flush = function () {
- clearTimeout(ts);
- return timer();
- };
-
- return {
- 'queueTrace': function (trace) {
- trace['dt'] = deltaActivityMillis();
- queue.push(trace);
- if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds
- return this;
- },
- 'flush': flush,
- 'addAndPurge': function (trace) {
- var accumulatedTrace = queue;
- queue = [];
- trace['dt'] = deltaActivityMillis();
- accumulatedTrace.push(trace);
- if (ts !== null) {
- clearTimeout(ts);
- ts = null;
- }
- return accumulatedTrace;
- }
- };
- };
-
- /**
- * Creates a new handler for the given Prolog assignment definition.
- *
- * @param {PrologTaskDef} info
- * @returns {{destroy: Function, processServerHints: Function}}
- */
- createPrologHandler = function (info) {
- var problem = info.problem,
- jqDescriptionContent = jqDescription.find('.description'),
- jqEditor = jqCode.find('.code_editor'),
- jqTerminal = jqConsole.find('.console'),
- jqHints = jqInfo.find('.hints'),
- editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }),
- activityHandler = makeActivityHandler(editor, problem.id),
- terminal = makePrologTerminalHandler(jqTerminal, editor, problem.id, activityHandler),
- hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan),
- commError = function (error) {
- alert(error);
- };
-
- editor.setValue(info.solution);
- $('#screen_prolog .title').text(problem.slug);
- jqDescriptionContent.html(problem.description);
- jqBtnPlan.prop('disabled', (problem.plan || '').length == 0);
-
- editor.on('change', function (instance, changeObj) {
- var doc = editor.getDoc(),
- pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
-
- if (changeObj.removed) {
- activityHandler.queueTrace({'typ': 'r', 'off': pos, 'len': codeq.codePointCount(changeObj.removed.join(''))});
- }
-
- if (changeObj.text) {
- activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
- }
- });
-
- jqBtnPlan.on('click', function () {
- if (!hinter.planNext()) {
- jqBtnPlan.prop('disabled', true).blur();
- }
- });
- jqBtnHint.on('click', function () {
- terminal.append('hint.\n', 'input');
- terminal.inputDisable();
- var doc = editor.getDoc();
- codeq.comms.sendHint({
- 'language': 'prolog',
- 'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
- })
- .then(function (data) {
- if (data.code === 0) {
- hinter.handle(data.hints);
- }
- else {
- terminal.append(data.message + '\n', 'error');
- }
- })
- .fail(commError)
- .fin(function () {
- terminal.inputEnable();
- terminal.append('?- ', 'output');
- })
- .done();
- });
- jqBtnTest.on('click', function () {
- terminal.append('test.\n', 'input');
- terminal.inputDisable();
- var doc = editor.getDoc();
- codeq.comms.sendTest({
- 'language': 'prolog',
- 'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
- })
- .then(function (data) {
- if (data.code === 0) {
- hinter.handle(data.hints);
- }
- else {
- terminal.append(data.message + '\n', 'error');
- }
- })
- .fail(commError)
- .fin(function () {
- terminal.inputEnable();
- terminal.append('?- ', 'output');
- })
- .done();
- });
-
- return {
- destroy: function () {
- $('#screen_prolog .title').text('');//empty the title text
- jqAllButtons.off();
- editor.off('change');
- 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;
- }
- };
- };
-})();
+/** + * 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() { + 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'), + jqBtnHint = jqScreen.find('.btn-hint'), + jqBtnTest = jqScreen.find('.btn-test'), + jqAllButtons = jqBtnPlan.add(jqBtnHint).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 prologHandler; //created when we enter the prolog state and destroyed once we leave it + codeq.globalStateMachine.register('prolog', { + 'enter': function (problemDef, commonHints, currentSolution) { + $('#navigation-language').css('display', ''); + $('#navigation-problem').css('display', ''); + $("#navigation-prolog").addClass("active"); + $('#navigation-prolog').css('display', ''); + + jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly + prologHandler = createPrologHandler(problemDef, commonHints, currentSolution); + subScreens = codeq.makeStateMachine(substates); + subScreens.transition(jqDescription.data(stateNameTag)); +/* Q.delay(100).then(function(){ + jqAllQuadrants.addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading + }).done();*/ + 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)); + }); + }, + 'exit': function () { + jqAllButtons.off(); // unregister all event handlers + jqAllQuadrants.off(); + jqScreen.css('display', 'none'); +// jqAllQuadrants.removeClass('transition'); + prologHandler.destroy(); + prologHandler = null; + subScreens.destroy(); + subScreens = null; + jqScreen.addClass('block1'); + + $('#navigation-language').css('display', 'none'); + $('#navigation-problem').css('display', 'none'); + $("#navigation-prolog").removeClass("active"); + $('#navigation-prolog').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) { + 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': 'slv', 'qry': 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': 'nxt'}) + }, 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': 'stp'}) + }, problem_id).then(tcs, tcf); + } + + } + }; + + terminal.leftmostCol = 3; + terminal.append('?- ', 'output'); + + return terminal; + }; + + var makeActivityHandler = function (editor, problem_id) { + var lastActivityMillis = Date.now(), + deltaActivityMillis = function deltaActivityMillisFunc () { + var now = Date.now(), + dt = now - lastActivityMillis; + lastActivityMillis = now; + return dt; + }, + queue = [], + ts = null, + timer = function () { + var promise; + ts = null; + if (queue.length === 0) return Q(true); + promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id); + queue.length = 0; + return promise; + }, + flush = function () { + clearTimeout(ts); + return timer(); + }; + + return { + 'queueTrace': function (trace) { + trace['dt'] = deltaActivityMillis(); + queue.push(trace); + if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds + return this; + }, + 'flush': flush, + 'addAndPurge': function (trace) { + var accumulatedTrace = queue; + queue = []; + trace['dt'] = deltaActivityMillis(); + accumulatedTrace.push(trace); + if (ts !== null) { + clearTimeout(ts); + ts = null; + } + return accumulatedTrace; + } + }; + }; + + codeq.on('init', function (args) { + codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active + }); + + /** + * Creates a new handler for the given Prolog assignment definition. + * + * @param {PrologTaskDef} info + * @returns {{destroy: Function, processServerHints: Function}} + */ + createPrologHandler = function (problemDef, commonHints, currentSolution) { + var //problem = info.problem, + jqDescriptionContent = jqDescription.find('.description'), + jqEditor = jqCode.find('.code_editor'), + jqTerminal = jqConsole.find('.console'), + jqHints = jqInfo.find('.hints'), + editor = codeq.makeEditor(jqEditor[0], { + mode: 'prolog' + }), + activityHandler = makeActivityHandler(editor, problemDef.id), + terminal = makePrologTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), + hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'prolog_hints', problemDef.hint, commonHints, problemDef.plan), + commError = function (error) { + alert(error); + }; + + codeq.tr.registerDictionary('prolog', problemDef.translations); + codeq.tr.translateDom(jqScreen); + if (currentSolution) editor.setValue(currentSolution); +// $('#screen_prolog .title').text(problem.slug); +// jqDescriptionContent.html(problem.description); + jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.sl) || []).length == 0); + + editor.on('change', function (instance, changeObj) { + var doc = editor.getDoc(), + pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)); + + if (changeObj.removed) { + activityHandler.queueTrace({'typ': 'r', 'off': pos, 'len': codeq.codePointCount(changeObj.removed.join(''))}); + } + + if (changeObj.text) { + activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text}); + } + }); + + jqBtnPlan.on('click', function () { + if (!hinter.planNext()) { + jqBtnPlan.prop('disabled', true).blur(); + } + }); + jqBtnHint.on('click', function () { + terminal.append('hint.\n', 'input'); + terminal.inputDisable(); + var doc = editor.getDoc(); + codeq.comms.sendHint({ + 'language': 'prolog', + 'program': editor.getDoc().getValue(), + 'problem_id': problemDef.id + }) + .then(function (data) { + if (data.code === 0) { + hinter.handle(data.hints); + } + else { + terminal.append(data.message + '\n', 'error'); + } + }) + .fail(commError) + .fin(function () { + terminal.inputEnable(); + terminal.append('?- ', 'output'); + }) + .done(); + }); + jqBtnTest.on('click', function () { + terminal.append('test.\n', 'input'); + terminal.inputDisable(); + var doc = editor.getDoc(); + codeq.comms.sendTest({ + 'language': 'prolog', + 'program': editor.getDoc().getValue(), + 'problem_id': problemDef.id + }) + .then(function (data) { + if (data.code === 0) { + hinter.handle(data.hints); + } + else { + terminal.append(data.message + '\n', 'error'); + } + }) + .fail(commError) + .fin(function () { + terminal.inputEnable(); + terminal.append('?- ', 'output'); + }) + .done(); + }); + + return { + destroy: function () { + $('#screen_prolog .title').text('');//empty the title text + jqAllButtons.off(); + editor.off('change'); + 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); + } + }; + }; +})(); diff --git a/js/codeq/python.js b/js/codeq/python.js index e80bbb7..e20fe06 100644 --- a/js/codeq/python.js +++ b/js/codeq/python.js @@ -1,329 +1,335 @@ -/**
- * Created by robert on 9/17/15.
- *
- * The python 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.
- *
- * Currentyl it is mostly a copy of the prolog state (the exception is of course the makePythonTerminalHandler instead of makePrologTerminalHandler)
- */
-
-(function() {
- 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_python'), // 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'),
- jqBtnHint = jqScreen.find('.btn-hint'),
- jqBtnTest = jqScreen.find('.btn-test'),
- jqBtnRun = jqScreen.find('.btn-run'),
- jqBtnStop = jqScreen.find('.btn-stop'),
- jqInfoButtons = jqBtnPlan.add(jqBtnHint).add(jqBtnTest), // all info-focusing buttons
- jqAllButtons = jqInfoButtons.add(jqBtnRun).add(jqBtnStop), // all 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 pythonHandler; //created when we enter the python state and destroyed once we leave it
- codeq.globalStateMachine.register('python', {
- 'enter': function (data) {
- $('#navigation-login').css('display', '');
- $('#navigation-language').css('display', '');
- $('#navigation-problem').css('display', '');
- $("#navigation-python").addClass("active");
- $('#navigation-python').css('display', '');
-
- jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly
- pythonHandler = createPythonHandler(data.data);
- subScreens = codeq.makeStateMachine(substates);
- subScreens.transition(jqDescription.data(stateNameTag));
-/* Q.delay(100).then(function(){
- jqAllQuadrants.addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading
- }).done();*/
- jqInfoButtons.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
- });
- jqBtnRun.on(transitionEventName, function (event) {
- subScreens.transition('console'); // 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));
- });
- },
- 'exit': function () {
- jqAllButtons.off(); // unregister all event handlers
- jqAllQuadrants.off();
- jqScreen.css('display', 'none');
-// jqAllQuadrants.removeClass('transition');
- pythonHandler.destroy();
- pythonHandler = null;
- subScreens.destroy();
- subScreens = null;
- jqScreen.addClass('block1');
-
- $('#navigation-login').css('display', 'none');
- $('#navigation-language').css('display', 'none');
- $('#navigation-problem').css('display', 'none');
- $("#navigation-python").removeClass("active");
- $('#navigation-python').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 makePythonTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) {
- var terminal = codeq.makeConsole(jqConsole, {
- 'greeting': 'CodeQ Python terminal proxy',
- 'autoHistory': true
- }),
- tcs = function terminalCommandSuccess (data) {
- if (data.code !== 0) {
- terminal.append(data.message, 'error');
- }
- },
- tcf = function terminalCommandFailed (error) {
- terminal.append(error + '\n', 'error');
- };
-
- terminal.onInput = function (text) {
- terminal.leftmostCol = 0;
- return codeq.comms.sendPythonPush({
- 'text': text + '\n'
- }).then(tcs, tcf);
- };
-
- codeq.comms.on('terminal_output', function (data) {
- var text = data.text,
- lines = text.split('\n');
- terminal.append(text, 'output');
- terminal.leftmostCol = lines[lines.length-1].length;
- });
-
- terminal.leftmostCol = 1;
-
- return terminal;
- };
-
- var makeActivityHandler = function (editor, problem_id) {
- var lastActivityMillis = Date.now(),
- deltaActivityMillis = function deltaActivityMillisFunc () {
- var now = Date.now(),
- dt = now - lastActivityMillis;
- lastActivityMillis = now;
- return dt;
- },
- queue = [],
- ts = null,
- timer = function () {
- var promise;
- ts = null;
- if (queue.length === 0) return Q(true);
- promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
- queue.length = 0;
- return promise;
- },
- flush = function () {
- clearTimeout(ts);
- return timer();
- };
-
- return {
- 'queueTrace': function (trace) {
- trace['dt'] = deltaActivityMillis();
- queue.push(trace);
- if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds
- return this;
- },
- 'flush': flush,
- 'addAndPurge': function (trace) {
- var accumulatedTrace = queue;
- queue = [];
- trace['dt'] = deltaActivityMillis();
- accumulatedTrace.push(trace);
- if (ts !== null) {
- clearTimeout(ts);
- ts = null;
- }
- return accumulatedTrace;
- }
- };
- };
-
-
-
- /**
- * Creates a new handler for the given Prolog assignment definition.
- *
- * @param {PrologTaskDef} info
- * @returns {{destroy: Function, processServerHints: Function}}
- */
- var createPythonHandler = function (info) {
- var problem = info.problem,
- jqDescriptionContent = jqDescription.find('.description'),
- jqEditor = jqCode.find('.code_editor'),
- jqTerminal = jqConsole.find('.console'),
- jqHints = jqInfo.find('.hints'),
- editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true, mode: 'python' }),
- activityHandler = makeActivityHandler(editor, problem.id),
- terminal = makePythonTerminalHandler(jqTerminal, editor, problem.id, activityHandler),
- hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan),
- commError = function (error) {
- alert(error);
- };
-
- editor.setValue(info.solution);
- $('#screen_python .title').text(problem.slug);
- jqDescriptionContent.html(problem.description);
- jqBtnPlan.prop('disabled', (problem.plan || '').length == 0);
-
- editor.on('change', function (instance, changeObj) {
- var doc = editor.getDoc(),
- pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
-
- if (changeObj.removed) {
- activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed.join(''))});
- }
-
- if (changeObj.text) {
- activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
- }
- });
-
- jqBtnPlan.on('click', function () {
- if (!hinter.planNext()) {
- jqBtnPlan.prop('disabled', true).blur();
- }
- });
- jqBtnHint.on('click', function () {
- var doc = editor.getDoc();
- codeq.comms.sendHint({
- 'language': 'python',
- 'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
- })
- .then(function (data) {
- if (data.code === 0) {
- hinter.handle(data.hints);
- }
- else {
- terminal.append('error: ' + data.message);
- }
- })
- .fail(commError)
- .done();
- });
- jqBtnTest.on('click', function () {
- var doc = editor.getDoc();
- codeq.comms.sendTest({
- 'language': 'python',
- 'program': editor.getDoc().getValue(),
- 'problem_id': problem.id
- })
- .then(function (data) {
- if (data.code === 0) {
- hinter.handle(data.hints);
- }
- else {
- terminal.append('error: ' + data.message);
- }
- })
- .fail(commError)
- .done();
- });
- jqBtnRun.on('click', function () {
- var program = editor.getDoc().getValue();
- codeq.comms.sendPythonStop({})
- .then(function () {
- codeq.comms.sendPythonExec({
- 'program': program
- });
- })
- .fail(commError)
- .done();
- // focus the terminal
- jqTerminal.click();
- });
- jqBtnStop.on('click', function () {
- codeq.comms.sendPythonStop({})
- .fail(commError)
- .done();
- });
-
- // TODO first line of interpreter output is buffered without this, why?
- codeq.comms.sendPythonPush({
- 'text': ''
- });
-
- return {
- destroy: function () {
- $('#screen_python .title').text('');//empty the title text
- jqAllButtons.off();
- editor.off('change');
- codeq.comms.off('terminal_output'); // stop listening for the terminal events from server
- 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;
- }
- };
- };
-})();
+/** + * Created by robert on 9/17/15. + * + * The python 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. + * + * Currentyl it is mostly a copy of the prolog state (the exception is of course the makePythonTerminalHandler instead of makePrologTerminalHandler) + */ + +(function() { + 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_python'), // 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'), + jqBtnHint = jqScreen.find('.btn-hint'), + jqBtnTest = jqScreen.find('.btn-test'), + jqBtnRun = jqScreen.find('.btn-run'), + jqBtnStop = jqScreen.find('.btn-stop'), + jqInfoButtons = jqBtnPlan.add(jqBtnHint).add(jqBtnTest), // all info-focusing buttons + jqAllButtons = jqInfoButtons.add(jqBtnRun).add(jqBtnStop), // all 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 pythonHandler; //created when we enter the python state and destroyed once we leave it + codeq.globalStateMachine.register('python', { + 'enter': function (problemDef, commonHints, currentSolution) { + $('#navigation-language').css('display', ''); + $('#navigation-problem').css('display', ''); + $("#navigation-python").addClass("active"); + $('#navigation-python').css('display', ''); + + jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly + pythonHandler = createPythonHandler(problemDef, commonHints, currentSolution); + subScreens = codeq.makeStateMachine(substates); + subScreens.transition(jqDescription.data(stateNameTag)); +/* Q.delay(100).then(function(){ + jqAllQuadrants.addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading + }).done();*/ + jqInfoButtons.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 + }); + jqBtnRun.on(transitionEventName, function (event) { + subScreens.transition('console'); // 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)); + }); + }, + 'exit': function () { + jqAllButtons.off(); // unregister all event handlers + jqAllQuadrants.off(); + jqScreen.css('display', 'none'); +// jqAllQuadrants.removeClass('transition'); + pythonHandler.destroy(); + pythonHandler = null; + subScreens.destroy(); + subScreens = null; + jqScreen.addClass('block1'); + + $('#navigation-language').css('display', 'none'); + $('#navigation-problem').css('display', 'none'); + $("#navigation-python").removeClass("active"); + $('#navigation-python').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 makePythonTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) { + var terminal = codeq.makeConsole(jqConsole, { + 'greeting': 'CodeQ Python terminal proxy', + 'autoHistory': true + }), + tcs = function terminalCommandSuccess (data) { + if (data.code !== 0) { + terminal.append(data.message, 'error'); + } + }, + tcf = function terminalCommandFailed (error) { + terminal.append(error + '\n', 'error'); + }; + + terminal.onInput = function (text) { + terminal.leftmostCol = 0; + return codeq.comms.sendPythonPush({ + 'text': text + '\n' + }).then(tcs, tcf); + }; + + codeq.comms.on('terminal_output', function (data) { + var text = data.text, + lines = text.split('\n'); + terminal.append(text, 'output'); + terminal.leftmostCol = lines[lines.length-1].length; + }); + + terminal.leftmostCol = 1; + + return terminal; + }; + + var makeActivityHandler = function (editor, problem_id) { + var lastActivityMillis = Date.now(), + deltaActivityMillis = function deltaActivityMillisFunc () { + var now = Date.now(), + dt = now - lastActivityMillis; + lastActivityMillis = now; + return dt; + }, + queue = [], + ts = null, + timer = function () { + var promise; + ts = null; + if (queue.length === 0) return Q(true); + promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id); + queue.length = 0; + return promise; + }, + flush = function () { + clearTimeout(ts); + return timer(); + }; + + return { + 'queueTrace': function (trace) { + trace['dt'] = deltaActivityMillis(); + queue.push(trace); + if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds + return this; + }, + 'flush': flush, + 'addAndPurge': function (trace) { + var accumulatedTrace = queue; + queue = []; + trace['dt'] = deltaActivityMillis(); + accumulatedTrace.push(trace); + if (ts !== null) { + clearTimeout(ts); + ts = null; + } + return accumulatedTrace; + } + }; + }; + + codeq.on('init', function (args) { + codeq.tr.registerDictionary('python', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active + }); + + /** + * Creates a new handler for the given Prolog assignment definition. + * + * @param {PrologTaskDef} info + * @returns {{destroy: Function, processServerHints: Function}} + */ + var createPythonHandler = function (problemDef, commonHints, currentSolution) { + var //problem = info.problem, + jqDescriptionContent = jqDescription.find('.description'), + jqEditor = jqCode.find('.code_editor'), + jqTerminal = jqConsole.find('.console'), + jqHints = jqInfo.find('.hints'), + editor = codeq.makeEditor(jqEditor[0], { + mode: 'python', + indentUnit: 4 + }), + activityHandler = makeActivityHandler(editor, problemDef.id), + terminal = makePythonTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), + hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'python_hints', problemDef.hint, commonHints, problemDef.plan), + commError = function (error) { + alert(error); + }; + + codeq.tr.registerDictionary('python', problemDef.translations); + codeq.tr.translateDom(jqScreen); + if (currentSolution) editor.setValue(currentSolution); +// $('#screen_python .title').text(problem.slug); +// jqDescriptionContent.html(problem.description); + jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.sl) || []).length == 0); + + editor.on('change', function (instance, changeObj) { + var doc = editor.getDoc(), + pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)); + + if (changeObj.removed) { + activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed.join(''))}); + } + + if (changeObj.text) { + activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text}); + } + }); + + jqBtnPlan.on('click', function () { + if (!hinter.planNext()) { + jqBtnPlan.prop('disabled', true).blur(); + } + }); + jqBtnHint.on('click', function () { + var doc = editor.getDoc(); + codeq.comms.sendHint({ + 'language': 'python', + 'program': editor.getDoc().getValue(), + 'problem_id': problemDef.id + }) + .then(function (data) { + if (data.code === 0) { + hinter.handle(data.hints); + } + else { + terminal.append('error: ' + data.message); + } + }) + .fail(commError) + .done(); + }); + jqBtnTest.on('click', function () { + var doc = editor.getDoc(); + codeq.comms.sendTest({ + 'language': 'python', + 'program': editor.getDoc().getValue(), + 'problem_id': problemDef.id + }) + .then(function (data) { + if (data.code === 0) { + hinter.handle(data.hints); + } + else { + terminal.append('error: ' + data.message); + } + }) + .fail(commError) + .done(); + }); + jqBtnRun.on('click', function () { + var program = editor.getDoc().getValue(); + codeq.comms.sendPythonStop({}) + .then(function () { + codeq.comms.sendPythonExec({ + 'program': program + }); + }) + .fail(commError) + .done(); + // focus the terminal + jqTerminal.click(); + }); + jqBtnStop.on('click', function () { + codeq.comms.sendPythonStop({}) + .fail(commError) + .done(); + }); + + // TODO first line of interpreter output is buffered without this, why? + codeq.comms.sendPythonPush({ + 'text': '' + }); + + return { + destroy: function () { + $('#screen_python .title').text('');//empty the title text + jqAllButtons.off(); + editor.off('change'); + codeq.comms.off('terminal_output'); // stop listening for the terminal events from server + 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('python', codeq.tr.emptyDictionary); + } + }; + }; +})(); diff --git a/js/codeq/statusbar.js b/js/codeq/statusbar.js index 2134b93..56070bd 100644 --- a/js/codeq/statusbar.js +++ b/js/codeq/statusbar.js @@ -6,7 +6,7 @@ (function () { var langs = codeq.supportedLangs, - jqMenu = jqBar.find('.dropdown-menu'), + jqMenu = jqLang.find('.dropdown-menu'), lang, cssClass; for (lang in langs) { if (!langs.hasOwnProperty(lang)) continue; diff --git a/js/codeq/translation.js b/js/codeq/translation.js index af39cdc..d4278ac 100644 --- a/js/codeq/translation.js +++ b/js/codeq/translation.js @@ -6,6 +6,7 @@ translationKey = jqElt.data('tkey'), dict = dicts[dictionaryKey], translations, html, key; + if (dict === codeq.tr.emptyDictionary) return; // silent ignore if (!dict) { codeq.log.error('Cannot find translation dictionary ' + dictionaryKey); return; @@ -61,6 +62,12 @@ dicts[name] = dict; }, + 'unregisterDictionary': function (name) { + delete dicts[name]; + }, + + 'emptyDictionary': {}, // use this with registerDictionary when you don't want any translations + 'translateDom': function (jqTopElt) { var lang = codeq.getLang(); jqTopElt.find('.translatable').each(function () { |