Commit e66428d7 authored by Jonathan Wilkes's avatar Jonathan Wilkes

first draft of new pdgui interface using a small callback-based framework

Some reference to patchwin remain...
This refactors pdgui.js to prevent errors for early messages from Pd.
Some references to raw patchwin array still remain, but on the whole this
should make things more sane.

There might also still be some places where a null-dereference error will
still bite us. For example, I think we need a check in the "q" method for
existence. But such changes can now happen in a single location inside
the "gui" definition, rather than scattering them throughout the file as
was done before as a stopgap.
parent 4bd1b3a6
......@@ -637,10 +637,12 @@ function pd_geo_string(w, h, x, y) {
// have it affect an empty canvas' geometry
// requires nw.js API
function gui_canvas_change_geometry(cid, w, h, x, y) {
patchwin[cid].width = w;
patchwin[cid].height = h + 23; // 23 is a kludge to account for menubar
patchwin[cid].x = x;
patchwin[cid].y = y;
gui(cid).get_nw_window(function(nw_win) {
nw_win.width = w;
nw_win.height = h + 23; // 23 is a kludge to account for menubar
nw_win.x = x;
nw_win.y = y;
});
}
// In tcl/tk, this function had some checks to apparently
......@@ -847,11 +849,11 @@ function gui_window_close(cid) {
// for some edge cases like [vis 1, vis 0(--[send subpatch] we
// may not have finished creating the window yet. So we check to
// make sure the canvas cid exists...
if (patchwin[cid]) {
nw_close_window(patchwin[cid]);
}
gui(cid).get_nw_window(function(nw_win) {
nw_close_window(nw_win);
});
// remove reference to the window from patchwin object
patchwin[cid] = null;
set_patchwin(cid, null);
loading[cid] = null;
}
......@@ -974,13 +976,14 @@ function menu_send(name) {
// requires nw.js API (Menuitem)
function canvas_set_editmode(cid, state) {
var patchsvg = patchwin[cid].window.document.querySelector("#patchsvg");
patchwin[cid].window.set_editmode_checkbox(state !== 0 ? true : false);
if (state !== 0) {
patchsvg.classList.add("editmode");
} else {
patchsvg.classList.remove("editmode");
}
gui(cid).get_elem("patchsvg", function(patchsvg, w) {
w.set_editmode_checkbox(state !== 0 ? true : false);
if (state !== 0) {
patchsvg.classList.add("editmode");
} else {
patchsvg.classList.remove("editmode");
}
});
}
exports.canvas_set_editmode = canvas_set_editmode;
......@@ -1258,10 +1261,17 @@ exports.get_patchwin = function(name) {
return patchwin[name];
}
exports.set_patchwin = function(cid, win) {
var set_patchwin = function(cid, win) {
patchwin[cid] = win;
if (win) {
gui.add(cid, win);
} else {
gui.remove(cid, win);
}
}
exports.set_patchwin = set_patchwin;
exports.get_dialogwin = function(name) {
return dialogwin[name];
}
......@@ -1284,52 +1294,53 @@ exports.last_loaded = function () {
// close a canvas window
function gui_canvas_cursor(cid, pd_event_type) {
if (!patchwin[cid]) {
return;
}
var patch = get_item(cid, "patchsvg"),
c;
// A quick mapping of events to pointers-- these can
// be revised later
switch(pd_event_type) {
case "cursor_runmode_nothing":
c = "default";
break;
case "cursor_runmode_clickme":
// The "pointer" icon seems the natural choice for "clickme" here,
// but unfortunately it creates ambiguity with the default editmode
// pointer icon. Not sure what the best solution is, but for now
// we'll just use "default" for clickme. That creates another
// ambiguity, but it's less of an issue since most of the
// clickable runtime items are fairly obvious anyway.
//c = "pointer";
c = "default";
break;
case "cursor_runmode_thicken":
c = "inherit";
break;
case "cursor_runmode_addpoint":
c = "cell";
break;
case "cursor_editmode_nothing":
c = "pointer";
break;
case "cursor_editmode_connect":
c = "-webkit-grabbing";
break;
case "cursor_editmode_disconnect":
c = "no-drop";
break;
case "cursor_editmode_resize":
c = "ew-resize";
break;
case "cursor_editmode_resize_bottom_right": c = "se-resize";
break;
case "cursor_scroll":
c = "all-scroll";
break;
}
patch.style.cursor = c;
gui(cid).get_elem("patchsvg", function(patch) {
// A quick mapping of events to pointers-- these can
// be revised later
var c;
switch(pd_event_type) {
case "cursor_runmode_nothing":
c = "default";
break;
case "cursor_runmode_clickme":
// The "pointer" icon seems the natural choice for "clickme"
// here, but unfortunately it creates ambiguity with the
// default editmode pointer icon. Not sure what the best
// solution is, but for now so we use "default" for clickme.
// That creates another ambiguity, but it's less of an issue
// since most of the clickable runtime items are fairly obvious
// anyway.
//c = "pointer";
c = "default";
break;
case "cursor_runmode_thicken":
c = "inherit";
break;
case "cursor_runmode_addpoint":
c = "cell";
break;
case "cursor_editmode_nothing":
c = "pointer";
break;
case "cursor_editmode_connect":
c = "-webkit-grabbing";
break;
case "cursor_editmode_disconnect":
c = "no-drop";
break;
case "cursor_editmode_resize":
c = "ew-resize";
break;
case "cursor_editmode_resize_bottom_right": c = "se-resize";
break;
case "cursor_scroll":
c = "all-scroll";
break;
}
patch.style.cursor = c;
});
}
// Note: cid can either be a real canvas id, or the string "pd" for the
......@@ -1461,14 +1472,12 @@ function canvas_map(name) {
}
function gui_canvas_erase_all_gobjs(cid) {
var svg_elem,
elem;
if (patchwin[cid]) {
svg_elem = get_item(cid, "patchsvg");
gui(cid).get_elem("patchsvg", function(svg_elem) {
var elem;
while (elem = svg_elem.firstChild) {
svg_elem.removeChild(elem);
}
}
});
}
exports.canvas_map = canvas_map;
......@@ -1808,14 +1817,110 @@ function configure_item(item, attributes) {
// The GUI side probably shouldn't know about "items" on SVG.
function gui_configure_item(cid, tag, attributes) {
var item = get_item(cid, tag);
configure_item(item, attributes);
gui(cid).get_elem(tag, attributes);
}
function add_gobj_to_svg(svg, gobj) {
svg.insertBefore(gobj, svg.querySelector(".cord"));
}
// New interface to incrementally move away from the tcl-like functions
// we're currently using. Basically like a trimmed down jquery, except
// we're dealing with multiple toplevel windows so we need an window id
// to get the correct Pd canvas context. Also, some of Pd's t_text tags
// still have "." in them which unfortunately means we must wrap
// getElementById instead of the more expressive querySelector[All] where
// the "." is interpreted as a CSS class selector.
// Methods:
// get_gobj(id, callbackOrObject) returns a reference to this little canvas
// interface
// get_elem(id, callbackOrObject) returns a reference to this little canvas
// interface
// get_nw_window(callback) returns a reference to the nw.js Window
// wrapper. We keep this separate from the
// others in order to annotate those parts
// of the code which rely on the nw API (and
// abstract them out later if we need to)
// objects are used to set SVG attributes (in the SVG namespace)
// function callbacks have the following args: (DOMElement, window)
// Note about checking for existence:
// Why? Because an iemgui inside a gop canvas will send drawing updates,
// __even__ __if__ that iemgui is outside the bounds of the gop and thus
// not displayed. This would be best fixed in the C code, but I'm not
// exactly sure where or how yet.
// Same problem on Pd Vanilla, except that tk canvas commands on
// non-existent tags don't throw an error.
var gui = (function() {
var c = {}; // object to hold references to all our canvas closures
var last_thing; // last thing we got
var null_fn, null_canvas;
var create_canvas = function(cid, w) {
var get = function(parent, sel, arg, suffix) {
sel = sel + (suffix ? "gobj" : "");
var elem = parent ?
parent.querySelector(sel) :
w.window.document.getElementById(sel);
last_thing = parent ? last_thing : elem;
if (elem) {
if (arg && typeof arg === "object") {
configure_item(elem, arg);
} else if (typeof arg === "function") {
arg(elem, w.window, c[cid]);
}
}
return c[cid];
}
return {
append: !w ? null_fn: function(cb) {
var frag = w.window.document.createDocumentFragment();
frag = cb(frag, w.window);
last_thing.appendChild(frag);
return c[cid];
},
get_gobj: !w ? null_fn : function(sel, arg) {
return get(null, sel, arg, "gobj");
},
get_elem: !w ? null_fn : function(sel, arg) {
return get(null, sel, arg);
},
get_nw_window: !w ? null_fn : function(cb) {
cb(w);
return c[cid];
},
q: !w ? null_fn : function(sel, arg) {
return last_thing ? get(last_thing, sel, arg) : c[cid];
},
debug: function() { return last_thing; }
}
};
// The tcl/tk interface ignores calls to configure non-existent items on
// canvases. Additionally, its interface was synchronous so that the window
// would always be guaranteed to exist. So we create a null canvas to keep
// from erroring out when things don't exist.
null_fn = function() {
return null_canvas;
}
null_canvas = create_canvas(null);
var canvas_container = function(cid) {
last_thing = c[cid] ? c[cid] : null_canvas;
return last_thing;
}
canvas_container.add = function(cid, nw_win) {
c[cid] = create_canvas(cid, nw_win);
}
canvas_container.remove = function(cid, nw_win) {
c[cid] = null;
}
return canvas_container;
}());
// For debugging
exports.gui = gui;
// Most of the following functions map either to pd.tk procs, or in some cases
// tk canvas subcommands
......@@ -1844,34 +1949,28 @@ function add_gobj_to_svg(svg, gobj) {
// creation, in which case a flag to toggle the offset would be appropriate.
function gui_gobj_new(cid, tag, type, xpos, ypos, is_toplevel) {
var svg,
g,
transform_string;
if (patchwin[cid]) {
svg = get_item(cid, "patchsvg"), // id for the svg element
xpos += 0.5;
ypos += 0.5;
transform_string = "matrix(1,0,0,1," + xpos + "," + ypos + ")",
var g;
xpos += 0.5,
ypos += 0.5,
gui(cid).get_elem("patchsvg", function(svg_elem) {
var transform_string = "matrix(1,0,0,1," + xpos + "," + ypos + ")";
g = create_item(cid, "g", {
id: tag + "gobj",
transform: transform_string,
class: type + (is_toplevel !== 0 ? "" : " gop")
});
add_gobj_to_svg(svg, g);
// hm... why returning g and not the return value of appendChild?
return g;
}
add_gobj_to_svg(svg_elem, g);
});
return g;
}
function gui_text_draw_border(cid, tag, bgcolor, isbroken, x1, y1, x2, y2) {
var g,
rect;
if (patchwin[cid]) {
g = get_gobj(cid, tag),
gui(cid).get_gobj(tag)
.append(function(frag) {
// isbroken means either
// a) the object couldn't create or
// b) the box is empty
rect = create_item(cid, "rect", {
var rect = create_item(cid, "rect", {
width: x2 - x1,
height: y2 - y1,
//"shape-rendering": "crispEdges",
......@@ -1880,46 +1979,46 @@ function gui_text_draw_border(cid, tag, bgcolor, isbroken, x1, y1, x2, y2) {
if (isbroken === 1) {
rect.classList.add("broken_border");
}
g.appendChild(rect);
}
frag.appendChild(rect);
return frag;
});
}
function gui_gobj_draw_io(cid, parenttag, tag, x1, y1, x2, y2, basex, basey,
type, i, is_signal, is_iemgui) {
var xlet_class, xlet_id, rect, g;
if (!patchwin[cid]) {
return;
}
g = get_gobj(cid, parenttag);
if (is_iemgui) {
xlet_class = "xlet_iemgui";
// We have an inconsistency here. We're setting the tag using
// string concatenation below, but the "tag" for iemguis arrives
// to us pre-concatenated. We need to remove that formatting in c, and
// in general try to simplify tag creation on the c side as much
// as possible.
xlet_id = tag;
} else if (is_signal) {
xlet_class = "xlet_signal";
xlet_id = tag + type + i;
} else {
xlet_class = "xlet_control";
xlet_id = tag + type + i;
}
rect = create_item(cid, "rect", {
width: x2 - x1,
height: y2 - y1,
x: x1 - basex,
y: y1 - basey,
id: xlet_id,
class: xlet_class,
//"shape-rendering": "crispEdges"
gui(cid).get_gobj(parenttag)
.append(function(frag) {
var xlet_class, xlet_id, rect;
if (is_iemgui) {
xlet_class = "xlet_iemgui";
// We have an inconsistency here. We're setting the tag using
// string concatenation below, but the "tag" for iemguis arrives
// to us pre-concatenated. We need to remove that formatting in c,
// and in general try to simplify tag creation on the c side as
// much as possible.
xlet_id = tag;
} else if (is_signal) {
xlet_class = "xlet_signal";
xlet_id = tag + type + i;
} else {
xlet_class = "xlet_control";
xlet_id = tag + type + i;
}
rect = create_item(cid, "rect", {
width: x2 - x1,
height: y2 - y1,
x: x1 - basex,
y: y1 - basey,
id: xlet_id,
class: xlet_class,
//"shape-rendering": "crispEdges"
});
frag.appendChild(rect);
return frag;
});
g.appendChild(rect);
}
function gui_gobj_redraw_io(cid, parenttag, tag, x, y, type, i, basex, basey) {
var xlet = get_item(cid, tag + type + i);
// We have to check for null. Here's why...
// if you create a gatom:
// canvas_atom -> glist_add -> text_vis -> glist_retext ->
......@@ -1927,45 +2026,40 @@ function gui_gobj_redraw_io(cid, parenttag, tag, x, y, type, i, basex, basey) {
// text_drawborder (firsttime=0) -> glist_drawiofor (firsttime=0)
// This means that a new gatom tries to redraw its inlets before
// it has created them.
if (xlet !== null) {
configure_item(xlet, { x: x - basex, y: y - basey });
}
gui(cid).get_elem(tag + type + i, {
x: x - basex,
y: y - basey
});
}
function gui_gobj_erase_io(cid, tag) {
var xlet = get_item(cid, tag);
xlet.parentNode.removeChild(xlet);
gui(cid).get_elem(tag, function(e) {
e.parentNode.removeChild(e);
});
}
function gui_gobj_configure_io(cid, tag, is_iemgui, is_signal, width) {
var xlet = get_item(cid, tag);
// We have to check for null here. Empty/broken object boxes
// can have "phantom" xlets as placeholders for connections
// to other objects. This may happen due to:
// * autopatching
// * objects which fail to create when loading a patch
if (xlet !== null) {
configure_item(xlet, {
"stroke-width": width,
});
gui(cid).get_elem(tag, {
"stroke-width": width
})
.get_elem(tag, function(e) {
var type;
if (is_iemgui) {
xlet.classList.add("xlet_iemgui");
type = "xlet_iemgui";
} else if (is_signal) {
xlet.classList.add("xlet_signal");
type = "xlet_signal";
} else {
xlet.classList.add("xlet_control");
"xlet_control";
}
// remove xlet_selected tag
xlet.classList.remove("xlet_selected");
}
e.classList.add(type);
e.classList.remove("xlet_selected");
});
}
function gui_gobj_highlight_io(cid, tag) {
var xlet = get_item(cid, tag);
// must check for null (see gui_gobj_configure_io)
if (xlet !== null) {
xlet.classList.add("xlet_selected");
}
gui(cid).get_elem(tag, function(e) {
e.classList.add("xlet_selected");
});
}
function message_border_points(width, height) {
......@@ -1980,42 +2074,35 @@ function message_border_points(width, height) {
}
function gui_message_draw_border(cid, tag, width, height) {
var g,
polygon;
if (!patchwin[cid]) {
return;
}
g = get_gobj(cid, tag);
polygon = create_item(cid, "polygon", {
points: message_border_points(width, height),
fill: "none",
stroke: "black",
class: "border"
//id: tag + "border"
gui(cid).get_gobj(tag)
.append(function(frag) {
var polygon = create_item(cid, "polygon", {
points: message_border_points(width, height),
fill: "none",
stroke: "black",
class: "border"
//id: tag + "border"
});
frag.appendChild(polygon);
return frag;
});
g.appendChild(polygon);
}
function gui_message_flash(cid, tag, state) {
var g = get_gobj(cid, tag);
if (state !== 0) {
g.classList.add("flashed");
} else {
g.classList.remove("flashed");
}
gui(cid).get_gobj(tag, function(e) {
if (state !== 0) {
e.classList.add("flashed");
} else {
e.classList.remove("flashed");
}
});
}
function gui_message_redraw_border(cid, tag, width, height) {
var g, b;
if (patchwin[cid]) {
g = get_gobj(cid, tag);
if (g) {
b = g.querySelector(".border");
configure_item(b, {
points: message_border_points(width, height),
});
}
}
gui(cid).get_gobj(tag)
.q(".border", {
points: message_border_points(width, height)
});
}
function atom_border_points(width, height, is_dropdown) {
......@@ -2040,67 +2127,56 @@ function atom_arrow_points(width, height) {
].join(" ");
}
function gui_atom_draw_border(cid, tag, type, width, height) {
var g, polygon, arrow, m;
if (!patchwin[cid]) {
return;
}
g = get_gobj(cid, tag),
polygon = create_item(cid, "polygon", {
points: atom_border_points(width, height, type !== 0),
fill: "none",
stroke: "gray",
"stroke-width": 1,
class: "border"
//id: tag + "border"
});
g.appendChild(polygon);
if (type !== 0) { // dropdown
// 1 = output index
// 2 = output value
// Let's make the two visually distinct so that the user can still
// reason about the patch functionality merely by reading the diagram
m = height < 20 ? 1 : height / 12;
arrow = create_item(cid, "polygon", {
points: atom_arrow_points(width, height),
"class": type === 1 ? "index_arrow" : "value_arrow"
gui(cid).get_gobj(tag)
.append(function(frag) {
var polygon = create_item(cid, "polygon", {
points: atom_border_points(width, height, type !== 0),
fill: "none",
stroke: "gray",
"stroke-width": 1,
class: "border"
//id: tag + "border"
});
g.appendChild(arrow);
}
frag.appendChild(polygon);
if (type !== 0) { // dropdown
// 1 = output index
// 2 = output value
// Let's make the two visually distinct so that the user can still
// reason about the patch functionality merely by reading the
// diagram
var m = height < 20 ? 1 : height / 12;
var arrow = create_item(cid, "polygon", {
points: atom_arrow_points(width, height),
"class": type === 1 ? "index_arrow" : "value_arrow"
});
frag.appendChild(arrow);
}
return frag;
});