Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cortex/tests/test_webgl_tour.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Tests for the optional guided-tour feature (cortex.webgl.make_static(tour=True))."""
import cortex.webgl # noqa: F401 (ensures cortex.webgl is importable)
from cortex.webgl import serve
from cortex.webgl.FallbackLoader import FallbackLoader


def _render_base_template(tour):
"""Render the base webgl template with the given `tour` flag (no subject needed)."""
loader = FallbackLoader([serve.cwd])
tpl = loader.load("template.html")
html = tpl.generate(
title="test",
leapmotion=False,
python_interface=False,
tour=tour,
colormaps=[("RdBu_r", "colormaps/RdBu_r.png")],
default_cmap="RdBu_r",
)
return html.decode("utf-8") if isinstance(html, bytes) else html


def test_tour_include_present_when_enabled():
html = _render_base_template(True)
assert "resources/js/tour.js" in html
assert "resources/css/tour.css" in html


def test_tour_include_absent_by_default():
html = _render_base_template(False)
assert "resources/js/tour.js" not in html
assert "resources/css/tour.css" not in html
35 changes: 35 additions & 0 deletions cortex/webgl/resources/css/tour.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* tour.css — styles for the optional guided-tour box (see resources/js/tour.js).
Adapted from the gallantlab semantic viewers (after the NYT visualizer). */
div#tourbox {
font-family: Helvetica, Arial, sans-serif;
color: white;
width: 25%;
left: 0;
min-width: 600px;
z-index: 100;
visibility: visible;
position: absolute;
top: 0;
opacity: 0.95;
background-color: rgba(0,0,0,0.7);
padding: 0 10px 10px 10px;
}
div.tour-stepper { margin-bottom: 10px; }
div.tour-steps { margin-bottom: 5px; display: inline-block; }
div.tour-step {
display: inline-block; width: 6px; height: 6px; margin-right: 10px;
border-radius: 4px; border: 1px solid white; cursor: pointer;
background: none; transition: background 0.5s;
}
div.tour-steps .active { background: white; }
div.tour-hideshow { display: inline-block; font-size: 8pt; cursor: pointer; }
div.tour-button {
border: 1px solid white; color: #cccccc; width: 100px; padding: 10px 0;
display: inline-block; background: none; text-align: center; opacity: 0.6;
transition: background 0.5s;
}
div.tour-button.active { opacity: 1.0; cursor: pointer; }
div.tour-back { border-top-left-radius: 5px; border-bottom-left-radius: 5px; }
div.tour-next { border-top-right-radius: 5px; border-bottom-right-radius: 5px; }
div.tour-title { font-size: 18pt; margin-bottom: 10px; }
div.tour-content { font-size: 12pt; font-family: Helvetica, Arial, sans-serif; }
8 changes: 8 additions & 0 deletions cortex/webgl/resources/js/mriview.js
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,14 @@ var mriview = (function(module) {
hide_labels: {action:hidelabels, key:'l', hidden:true, help:'Toggle labels'},
});

// optional guided tour (cortex.webgl.make_static(tour=True))
if (typeof viewopts !== "undefined" && viewopts.tour && typeof Tour !== "undefined") {
this.tour = new Tour(this);
this.ui.add({
toggle_tour: {action: this.tour.toggle.bind(this.tour), key: 'o', hidden: true, help: 'Toggle tour'},
});
}

//add sliceplane gui
var sliceplane_ui = this.ui.addFolder("sliceplanes", true)
sliceplane_ui.add({
Expand Down
113 changes: 113 additions & 0 deletions cortex/webgl/resources/js/tour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* tour.js — optional guided-tour stepper for pycortex webgl viewers.
*
* Enabled by cortex.webgl.make_static(..., tour=True). Ships a generic stub
* tour (no dataset-specific text); mriview.js constructs it on boot when
* viewopts.tour is set and binds the 'o' key to Tour.toggle().
*
* Adapted from the tour used in the gallantlab semantic viewers (itself after
* the NYT visualizer by Gregor Aisch & Amanda Cox). Cleaned for reuse: it
* self-injects its markup, per-step `view`/`call` hooks are optional, and its
* keyboard show/hide acts on the whole box (the old showhide left a stray strip).
*/
(function () {
"use strict";

// self-contained tourbox markup (no template HTML needed)
var TOURBOX_HTML =
'<div class="tour-stepper">' +
'<div class="tour-steps"></div>' +
'<div class="tour-hideshow">(hide tour)</div><br/>' +
'<div class="tour-button tour-back">Back</div>' +
'<div class="tour-button tour-next active">Next</div>' +
'</div>' +
'<div class="tour-title"></div>' +
'<div class="tour-content"></div>';

// generic stub steps — nothing dataset-specific (f/i/r/h/o are real core keys)
var STEPS = [
{title: "Welcome",
content: "<p>This is a short guided tour of the viewer. Use <b>Back</b> / <b>Next</b> or the dots above to step through it. Press <b>o</b> to hide or show this box.</p>"},
{title: "Navigating",
content: "<p>Drag to rotate the brain, scroll to zoom, and <b>Shift</b>+drag to pan.</p>"},
{title: "The surface",
content: "<p>Press <b>f</b> to flatten the cortex, <b>i</b> to inflate it, and <b>r</b> to reset the view.</p>"},
{title: "Explore",
content: "<p>Press <b>h</b> at any time to see every keyboard shortcut. That's it — go explore!</p>"}
];

// Tour(viewer, content?) — content defaults to the generic stub STEPS.
var Tour = function (viewer, content) {
this.viewer = viewer;
this.content = content || STEPS;
this.hidden = false;
this.object = document.createElement("div");
this.object.id = "tourbox";
this.object.innerHTML = TOURBOX_HTML;
document.body.appendChild(this.object);
this.setup();
};

Tour.prototype.setup = function () {
var self = this;
$(this.object).find(".tour-hideshow").click(this.showhide.bind(this));
this.content.forEach(function (step, i) {
var el = document.createElement("div");
$(el).addClass("tour-step").attr("title", step.title)
.click(function () { self.goto_step(i); });
$(self.object).find(".tour-steps").append(el);
});
this.goto_step(0);
};

Tour.prototype.goto_step = function (idx) {
this.current_step = parseInt(idx, 10);
this.update_buttons();
var steps = $(this.object).find(".tour-step");
steps.removeClass("active");
$(steps[this.current_step]).addClass("active");
var cont = this.content[this.current_step];
$(this.object).find(".tour-title").html(cont.title);
$(this.object).find(".tour-content").html(cont.content);
// per-step camera move / callback are optional (the stub has neither)
if (cont.view && this.viewer && this.viewer.animate) this.viewer.animate(cont.view);
if (typeof cont.call === "function") cont.call(this.viewer);
};

Tour.prototype.update_buttons = function () {
var self = this;
var last_idx = this.current_step - 1;
var next_idx = this.current_step + 1;
var back = $(this.object).find(".tour-back");
var next = $(this.object).find(".tour-next");
back.unbind("click");
if (this.current_step === 0) {
back.removeClass("active");
} else {
back.addClass("active").click(function () { self.goto_step(last_idx); });
}
if (this.current_step === this.content.length - 1) next_idx = 0;
next.unbind("click").click(function () { self.goto_step(next_idx); });
};

// on-screen link: collapse to / expand from the stepper (keeps the affordance)
Tour.prototype.showhide = function () {
var box = $(this.object);
if (this.hidden) {
box.find(".tour-button, .tour-title, .tour-content").show();
box.find(".tour-hideshow").html("(hide tour)");
this.hidden = false;
} else {
box.find(".tour-button, .tour-title, .tour-content").hide();
box.find(".tour-hideshow").html("(show tour)");
this.hidden = true;
}
};

// keyboard 'o': fully show/hide the whole box (nothing left behind — the fix)
Tour.prototype.toggle = function () {
var el = this.object;
el.style.display = (el.style.display === "none" ? "" : "none");
};

window.Tour = Tour;
})();
5 changes: 5 additions & 0 deletions cortex/webgl/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
{% if python_interface %}
<script type='text/javascript' src='resources/js/python_interface.js'></script>
{% end %}

{% if tour %}
<script type='text/javascript' src="resources/js/tour.js"></script>
<link rel="stylesheet" href="resources/css/tour.css" type='text/css' />
{% end %}
Comment on lines +50 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Rendering this template via cortex.webgl.show() (which uses mixer.html extending template.html) will crash with a NameError: name 'tour' is not defined because tour is not passed to html.generate() in show()'s MixerHandler.

To prevent this crash and ensure backward compatibility with other viewer entry points, wrap the tour conditional block in a try/except NameError block. This is a clean, template-level solution that avoids breaking cortex.webgl.show().

{% try %}
{% if tour %}
<script type='text/javascript' src="resources/js/tour.js"></script>
<link rel="stylesheet" href="resources/css/tour.css" type='text/css' />
{% end %}
{% except NameError %}
{% end %}

{% block javascripts %}
{% end %}

Expand Down
7 changes: 7 additions & 0 deletions cortex/webgl/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def make_static(
html_embed=True,
copy_ctmfiles=True,
title="Brain",
tour=False,
layout=None,
overlay_file=None,
curvature_brightness=None,
Expand Down Expand Up @@ -113,6 +114,10 @@ def make_static(
title : str, optional
The title that is displayed on the viewer website when it is loaded in
a browser.
tour : bool, optional
If True, bake in pycortex's built-in generic guided tour: a small,
dataset-agnostic stepper the user can page through, collapse with its
on-screen link, or fully toggle with the 'o' key. Default False.
layout : None or list of (int, int)
The layout of the viewer subwindows for showing multiple subjects, passed to
the template generator.
Expand Down Expand Up @@ -262,13 +267,15 @@ def make_static(
for sec in options.config.sections():
if "paths" in sec or "labels" in sec:
my_viewopts[sec] = dict(options.config.items(sec))
my_viewopts["tour"] = bool(tour)

html = tpl.generate(
data=json.dumps(metadata),
colormaps=colormaps,
default_cmap="RdBu_r",
python_interface=False,
leapmotion=True,
tour=bool(tour),
layout=layout,
subjects=json.dumps(ctms),
viewopts=json.dumps(my_viewopts),
Expand Down
Loading