]> granicus.if.org Git - graphviz/commitdiff
add lefty, dotty, lneato to graphviz2 tree
authorellson <devnull@localhost>
Thu, 6 Jan 2005 15:01:42 +0000 (15:01 +0000)
committerellson <devnull@localhost>
Thu, 6 Jan 2005 15:01:42 +0000 (15:01 +0000)
cmd/dotty/dotty.bsh [new file with mode: 0755]
cmd/dotty/dotty.ksh [new file with mode: 0755]
cmd/dotty/dotty.lefty [new file with mode: 0644]
cmd/dotty/dotty.sh [new file with mode: 0755]
cmd/dotty/dotty_draw.lefty [new file with mode: 0644]
cmd/dotty/dotty_edit.lefty [new file with mode: 0644]
cmd/dotty/dotty_layout.lefty [new file with mode: 0644]
cmd/dotty/dotty_ui.lefty [new file with mode: 0644]

diff --git a/cmd/dotty/dotty.bsh b/cmd/dotty/dotty.bsh
new file mode 100755 (executable)
index 0000000..e4a256e
--- /dev/null
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+FILES=""
+MLEVEL="0"
+LMODE="async"
+
+usage='echo "usage: dotty [-V] [-lm (sync|async)] [-el (0|1)] <filename>"'
+
+if [ "x$DOTTYOPTIONS" != "x" ]; then
+    set -- $DOTTYOPTIONS $*
+fi
+
+while [ "x$1" != 'x' ]; do
+    case $1 in
+    -V)
+        echo "dotty version 96c (09-24-96)"
+        shift
+        ;;
+    -f)
+        shift
+        loadfile=$1
+        shift
+        ;;
+    -lm)
+        shift
+        LMODE=$1
+        if [ "x$LMODE" != 'xsync' -a "x$LMODE" != 'xasync' ]; then
+            $usage
+            exit 1
+        fi
+        shift
+        ;;
+    -el)
+        shift
+        MLEVEL=$1
+        if [ "x$MLEVEL" != 'x0' -a "x$MLEVEL" != 'x1' ]; then
+            $usage
+            exit 1
+        fi
+        shift
+        ;;
+    -)
+        FILES=`echo $FILES \"$1\"`
+        shift
+        ;;
+    -*)
+        $usage
+        exit 1
+        ;;
+    *)
+        FILES=`echo $FILES \"$1\"`
+        shift
+        ;;
+    esac
+done
+
+if [ "x$DOTTYPATH" != 'x' ]; then
+    LEFTYPATH="$DOTTYPATH:$LEFTYPATH"
+fi
+
+CMDS=""
+
+CMDS="dotty.protogt.layoutmode = '$LMODE';"
+
+CMDS=`echo $CMDS dotty.mlevel = $MLEVEL";"`
+
+if [ "x$loadfile" != 'x' ]; then
+    CMDS=`echo $CMDS load \("'"$loadfile"'"\)";"`
+fi
+
+if [ "x$FILES" = 'x' ]; then
+    FILES=null
+fi
+FUNC="dotty.createviewandgraph"
+for i in $FILES; do
+    CMDS=`echo $CMDS $FUNC \($i, "'"file"'", null, null\)";"`
+done
+
+leftypath=`which lefty`
+if [ ! -f "$leftypath" ]; then
+    echo "dotty: cannot locate the lefty program"
+    echo "       make sure that your path includes"
+    echo "       the directory containing dotty and lefty"
+    exit 1
+fi
+
+exec $leftypath -e "
+load ('dotty.lefty');
+checkpath = function () {
+    if (tablesize (dotty) > 0)
+        remove ('checkpath');
+    else {
+        echo ('dotty: cannot locate the dotty scripts');
+        echo ('       make sure that the environment variable LEFTYPATH');
+        echo ('       is set to the directory containing dotty.lefty');
+        exit ();
+    }
+};
+checkpath ();
+dotty.init ();
+monitorfile = dotty.monitorfile;
+$CMDS
+txtview ('off');
+"
diff --git a/cmd/dotty/dotty.ksh b/cmd/dotty/dotty.ksh
new file mode 100755 (executable)
index 0000000..7ad10dd
--- /dev/null
@@ -0,0 +1,109 @@
+#!/bin/ksh
+
+FILES=""
+MLEVEL="0"
+LMODE="async"
+
+function usage {
+    print "usage: dotty [-V] [-lm (sync|async)] [-el (0|1)] <filename>"
+}
+
+function processoptions {
+    while [[ $# > 0 ]] do
+    case $1 in
+    -V)
+            print "dotty version 96c (09-24-96)"
+        shift
+        ;;
+    -f)
+        shift
+        loadfile=$1
+        shift
+        ;;
+    -lm)
+        shift
+        LMODE=$1
+            if [[ $LMODE != 'sync' && $LMODE != 'async' ]] then
+                usage
+            exit 1
+        fi
+        shift
+        ;;
+    -el)
+        shift
+        MLEVEL=$1
+            if [[ $MLEVEL != '0' && $MLEVEL != '1' ]] then
+                usage
+            exit 1
+        fi
+        shift
+        ;;
+    -)
+            FILES=$(print $FILES "'"$1"'")
+        shift
+        ;;
+    -*)
+            usage
+        exit 1
+        ;;
+    *)
+            FILES=$(print $FILES "'"$1"'")
+        shift
+        ;;
+    esac
+    done
+}
+
+if [[ $DOTTYOPTIONS != '' ]] then
+    processoptions $DOTTYOPTIONS
+fi
+processoptions "$@"
+
+if [[ $DOTTYPATH != '' ]] then
+    export LEFTYPATH="$DOTTYPATH:$LEFTYPATH"
+fi
+
+CMDS=""
+
+CMDS="dotty.protogt.layoutmode = '$LMODE';"
+
+CMDS=$(print $CMDS dotty.mlevel = $MLEVEL";")
+
+if [[ $loadfile != '' ]] then
+    CMDS=$(print $CMDS load \("'"$loadfile"'"\)";")
+fi
+
+if [[ $FILES = '' ]] then
+    FILES=null
+fi
+FUNC="dotty.createviewandgraph"
+for i in $FILES; do
+    CMDS=$(print $CMDS $FUNC \($i, "'"file"'", null, null\)";")
+done
+
+leftypath=$(whence -p lefty)
+if [[ $leftypath == '' ]] then
+    print -u2 "dotty: cannot locate the lefty program"
+    print -u2 "       make sure that your path includes"
+    print -u2 "       the directory containing dotty and lefty"
+    exit 1
+fi
+
+$leftypath -e "
+load ('dotty.lefty');
+checkpath = function () {
+    if (tablesize (dotty) > 0)
+        remove ('checkpath');
+    else {
+        echo ('dotty: cannot locate the dotty scripts');
+        echo ('       make sure that the environment variable LEFTYPATH');
+        echo ('       is set to the directory containing dotty.lefty');
+        exit ();
+    }
+};
+checkpath ();
+dotty.init ();
+monitorfile = dotty.monitorfile;
+$CMDS
+txtview ('off');
+"
diff --git a/cmd/dotty/dotty.lefty b/cmd/dotty/dotty.lefty
new file mode 100644 (file)
index 0000000..3d7b65a
--- /dev/null
@@ -0,0 +1,772 @@
+#
+# DOTTY
+#
+dotty = [
+    'keys' = [
+        'nid'    = 'nid';
+        'eid'    = 'eid';
+        'gid'    = 'gid';
+        'name'   = 'name';
+        'attr'   = 'attr';
+        'gattr'  = 'graphattr';
+        'eattr'  = 'edgeattr';
+        'nattr'  = 'nodeattr';
+        'edges'  = 'edges';
+        'tail'   = 'tail';
+        'tport'  = 'tport';
+        'head'   = 'head';
+        'hport'  = 'hport';
+        'pos'    = 'pos';
+        'size'   = 'size';
+        'points' = 'points';
+        'fname'  = 'fontname';
+        'fsize'  = 'fontsize';
+        'fcolor' = 'fontcolor';
+        'color'  = 'color';
+    ];
+    'maps' = [
+        'X11' = [
+            'fontmap' = [
+                'Times-Roman' =
+                        '-*-times-medium-r-*--%d-*-*-*-*-*-*-1';
+                'Times-Italic' =
+                        '-*-times-medium-i-*--%d-*-*-*-*-*-*-1';
+                'Times-Bold' =
+                        '-*-times-bold-r-*--%d-*-*-*-*-*-*-1';
+                'Courier' =
+                        '-*-courier-bold-r-*--%d-*-*-*-*-*-*-1';
+                'Courier-Bold' =
+                        '-*-courier-bold-r-*--%d-*-*-*-*-*-*-1';
+                'Helvetica' =
+                        '-*-helvetica-medium-r-normal--%d-*-*-*-p-*-iso8859-1';
+                'Helvetica-Bold' =
+                        '-*-helvetica-bold-r-normal--%d-*-*-*-p-*-iso8859-1';
+            ];
+            'psfontmap' = [
+                'Times-Roman'    = 'Times-Roman';
+                'Times-Italic'   = 'Times-Italic';
+                'Times-Bold'     = 'Times-Bold';
+                'Courier'        = 'Courier';
+                'Courier-Bold'   = 'Courier-Bold';
+                'Helvetica'      = 'Helvetica';
+                'Helvetica-Bold' = 'Helvetica-Bold';
+            ];
+        ];
+        'mswin' = [
+            'fontmap' = [
+                'Times-Roman'    = 'Times New Roman';
+                'Times-Italic'   = 'Times New Roman Italic';
+                'Times-Bold'     = 'Times New Roman Bold';
+                'Courier'        = 'Courier New';
+                'Courier-Bold'   = 'Courier New Bold';
+                'Helvetica'      = 'Arial';
+                'Helvetica-Bold' = 'Arial Bold';
+            ];
+            'psfontmap' = [
+                'Times-Roman'    = 'Times New Roman';
+                'Times-Italic'   = 'Times New Roman Italic';
+                'Times-Bold'     = 'Times New Roman Bold';
+                'Courier'        = 'Courier New';
+                'Courier-Bold'   = 'Courier New Bold';
+                'Helvetica'      = 'Arial';
+                'Helvetica-Bold' = 'Arial Bold';
+            ];
+        ];
+    ];
+    'protogt' = [
+        'graph' = [
+            'graphattr' = [
+                'fontsize' = '14';
+                'fontname' = 'Times-Roman';
+                'fontcolor' = 'black';
+                'color' = 'black';
+            ];
+            'nodeattr' = [
+                'shape' = 'ellipse';
+                'fontsize' = '14';
+                'fontname' = 'Times-Roman';
+                'fontcolor' = 'black';
+                'color' = 'black';
+            ];
+            'edgeattr' = [
+                'fontsize' = '14';
+                'fontname' = 'Times-Roman';
+                'fontcolor' = 'black';
+                'color' = 'black';
+            ];
+            'graphdict' = [];
+            'nodedict' = [];
+            'graphs' = [];
+            'nodes' = [];
+            'edges' = [];
+            'maxgid' = 0;
+            'maxnid' = 0;
+            'maxeid' = 0;
+            'type' = 'digraph';
+        ];
+        'layoutmode' = 'sync';
+        'lserver' = 'dot';
+        'edgehandles' = 1;
+        'noundo' = 0;
+    ];
+    'lservers' = [];
+    'mlevel' = 0;
+    'graphs' = [];
+    'views' = [];
+    'protovt' = [
+        'normal' = [
+            'name' = 'DOTTY';
+            'orig' = ['x' = 1; 'y' = 1;];
+            'size' = ['x' = 420; 'y' = 520;];
+            'wrect' = [
+                0 = ['x' = 0; 'y' = 0;];
+                1 = ['x' = 400; 'y' = 500;];
+            ];
+            'vsize' = ['x' = 400; 'y' = 500;];
+            'w2v' = 1;
+        ];
+        'birdseye' = [
+            'type' = 'birdseye';
+            'name' = 'DOTTY birdseye view';
+            'orig' = ['x' = 1; 'y' = 1;];
+            'size' = ['x' = 220; 'y' = 260;];
+            'wrect' = [
+                0 = ['x' = 0; 'y' = 0;];
+                1 = ['x' = 200; 'y' = 250;];
+            ];
+            'vsize' = ['x' = 200; 'y' = 250;];
+            'w2v' = 1;
+        ];
+    ];
+    'pagesizes' = [
+        '8.5x11' = ['x' =    8; 'y' = 10.5;];
+        '11x17'  = ['x' = 10.5; 'y' = 16.5;];
+        '36x50'  = ['x' = 35.5; 'y' = 49.5;];
+    ];
+];
+load ('dotty_draw.lefty');
+load ('dotty_edit.lefty');
+load ('dotty_layout.lefty');
+load ('dotty_ui.lefty');
+#
+# initialization functions
+#
+dotty.init = function () {
+    dotty.outlinecolor = 1;
+    dotty.fontmap = dotty.maps[getenv ('LEFTYWINSYS')].fontmap;
+    dotty.clipgt = dotty.protogt.creategraph (['noundo' = 1;]);
+    dotty.inited = 1;
+};
+dotty.simple = function (file) {
+    if (dotty.inited ~= 1)
+        dotty.init ();
+    dotty.createviewandgraph (file, 'file', null, null);
+    txtview ('off');
+};
+#
+# main operations
+#
+dotty.protogt.creategraph = function (protogt) {
+    local gt, id, gtid;
+
+    if (~protogt)
+        protogt = dotty.protogt;
+    for (gtid = 0; dotty.graphs[gtid]; gtid = gtid + 1)
+        ;
+    gt = (dotty.graphs[gtid] = []);
+    if (protogt.mode ~= 'replace') {
+        for (id in dotty.protogt)
+            gt[id] = copy (dotty.protogt[id]);
+    }
+    for (id in protogt)
+        gt[id] = copy (protogt[id]);
+    gt.gtid = gtid;
+    gt.views = [];
+    gt.undoarray = ['level' = 0; 'entries' = [];];
+    gt.busy = 0;
+    return gt;
+};
+dotty.protogt.copygraph = function (ogt) {
+    local gt, gtid, id;
+
+    for (gtid = 0; dotty.graphs[gtid]; gtid = gtid + 1)
+        ;
+    gt = (dotty.graphs[gtid] = []);
+    for (id in ogt)
+        gt[id] = copy (ogt[id]);
+    gt.gtid = gtid;
+    gt.views = [];
+    gt.undoarray = ['level' = 0; 'entries' = [];];
+    gt.busy = 0;
+    return gt;
+};
+dotty.protogt.destroygraph = function (gt) {
+    local vid, vlist;
+
+    if (gt.layoutpending > 0)
+        gt.cancellayout (gt);
+    for (vid in gt.views)
+        vlist[vid] = gt.views[vid];
+    for (vid in gt.views)
+        gt.destroyview (gt, vlist[vid]);
+    remove (gt.gtid, dotty.graphs);
+};
+dotty.protogt.loadgraph = function (gt, name, type, protograph, layoutflag) {
+    local vid, vt, fd, graph, nid, eid, gid;
+
+    if (gt.layoutpending > 0)
+        gt.cancellayout (gt);
+    if (~name)
+        if (~(name = ask ('file name:', 'file', '')))
+            return;
+    dotty.pushbusy (gt, gt.views);
+    dotty.message (1, 'loading');
+    if (~protograph)
+        protograph = dotty.protogt.graph;
+    if (~((fd = dotty.openio (name, type, 'r')) >= 0) |
+            ~(graph = readgraph (fd, protograph))) {
+        dotty.message (0, 'cannot load graph');
+        dotty.popbusy (gt, gt.views);
+        return;
+    }
+    for (vid in gt.views) {
+        vt = gt.views[vid];
+        vt.colors = [];
+        vt.colorn = 2;
+    }
+    gt.graph = graph;
+    gt.name = name;
+    gt.type = type;
+    gt.undoarray = ['level' = 0; 'entries' = [];];
+    if (~(type == 'file' & name == '-'))
+        closeio (fd);
+    graph.maxgid = tablesize (graph.graphs);
+    graph.maxnid = tablesize (graph.nodes);
+    graph.maxeid = tablesize (graph.edges);
+    for (nid in graph.nodes)
+        graph.nodes[nid][dotty.keys.nid] = nid;
+    for (eid in graph.edges)
+        graph.edges[eid][dotty.keys.eid] = eid;
+    for (gid in graph.graphs)
+        graph.graphs[gid][dotty.keys.gid] = gid;
+    gt.unpackattr (gt);
+    if (layoutflag) {
+        dotty.message (1, 'generating layout');
+        gt.layoutgraph (gt);
+    }
+    dotty.popbusy (gt, gt.views);
+    return gt.graph;
+};
+dotty.protogt.savegraph = function (gt, name, type, savecoord) {
+    local fd, graph, gid, sgraph, nid, node, eid, edge, pointi, attr;
+
+    if (~name)
+        if (~(name = ask ('file name:', 'file', '')))
+            return;
+    if (savecoord) {
+        graph = copy (gt.graph);
+        for (gid in graph.graphs) {
+            sgraph = graph.graphs[gid];
+            if (sgraph.rect)
+                sgraph.graphattr.bb = concat (sgraph.rect[0].x, ',',
+                        sgraph.rect[0].y, ',', sgraph.rect[1].x, ',',
+                        sgraph.rect[1].y);
+            if (sgraph.lp & tablesize (sgraph.lp) > 0)
+                sgraph.graphattr.lp = concat (sgraph.lp.x, ',', sgraph.lp.y);
+            else if (sgraph.lp)
+                sgraph.graphattr.lp = "";
+        }
+        for (nid in graph.nodes) {
+            node = graph.nodes[nid];
+            if (~node.attr.label)
+                node.attr.label = '\N';
+            if (node.pos)
+                node.attr.pos = concat (node.pos.x, ',', node.pos.y);
+            if (node.size) {
+                node.attr.width = node.size.x / 72;
+                node.attr.height = node.size.y / 72;
+            }
+            if (node.fields)
+                node.attr.rects = gt.packrecordfields (gt, node.fields);
+        }
+        for (eid in graph.edges) {
+            edge = graph.edges[eid];
+            if (edge.points) {
+                attr = '';
+                if (edge.sp)
+                    attr = concat ('s,', edge.sp.x, ',', edge.sp.y, ' ');
+                if (edge.ep)
+                    attr = concat (attr, 'e,', edge.ep.x, ',', edge.ep.y, ' ');
+                for (pointi = 0; edge.points[pointi]; pointi = pointi + 1)
+                    attr = concat (attr, ' ', edge.points[pointi].x, ',',
+                        edge.points[pointi].y);
+                edge.attr.pos = attr;
+            }
+            if (edge.lp)
+                edge.attr.lp = concat (edge.lp.x, ',', edge.lp.y);
+        }
+        if (~graph.rect) {
+            graph.rect = [
+                0 = ['x' = 0; 'y' = 0; ]; 1 = ['x' = 1; 'y' = 1; ];
+            ];
+            for (nid in graph.nodes) {
+                node = graph.nodes[nid];
+                if (graph.rect[1].x < node.pos.x + node.size.x / 2)
+                    graph.rect[1].x = node.pos.x + node.size.x / 2;
+                if (graph.rect[1].y < node.pos.y + node.size.y / 2)
+                    graph.rect[1].y = node.pos.y + node.size.y / 2;
+            }
+        }
+            graph.graphattr.bb = concat (graph.rect[0].x, ',',
+                    graph.rect[0].y, ',', graph.rect[1].x, ',',
+                    graph.rect[1].y);
+        if (graph.lp & tablesize (graph.lp) > 0)
+            graph.graphattr.lp = concat (graph.lp.x, ',', graph.lp.y);
+    } else
+        graph = gt.graph;
+    if (~((fd = dotty.openio (name, type, 'w')) >= 0) |
+            ~writegraph (fd, graph, 0)) {
+        dotty.message (0, 'cannot save graph');
+        return;
+    }
+    if (~(type == 'file' & name == '-'))
+        closeio (fd);
+};
+dotty.protogt.packrecordfields = function (gt, fields) {
+    local attrs, attr, fid, field;
+
+    for (fid = 0; fields[fid]; fid = fid + 1) {
+        field = fields[fid];
+        if (field.fields)
+            attr = gt.packrecordfields (gt, field.fields);
+        else
+            attr = concat (field.rect[0].x, ',', field.rect[0].y,
+                    ',', field.rect[1].x, ',', field.rect[1].y);
+        if (attrs)
+            attrs = concat (attrs, ' ', attr);
+        else
+            attrs = attr;
+    }
+    return attrs;
+};
+dotty.protogt.setgraph = function (gt, graph) {
+    local vid, vt, nid, eid, gid;
+
+    if (gt.layoutpending > 0)
+        gt.cancellayout (gt);
+    for (vid in gt.views) {
+        vt = gt.views[vid];
+        vt.colors = [];
+        vt.colorn = 2;
+    }
+    gt.graph = copy (graph);
+    gt.undoarray = ['level' = 0; 'entries' = [];];
+    gt.unpackattr (gt);
+    gt.graph.maxgid = tablesize (graph.graphs);
+    gt.graph.maxnid = tablesize (graph.nodes);
+    gt.graph.maxeid = tablesize (graph.edges);
+    for (nid in gt.graph.nodes)
+        gt.graph.nodes[nid][dotty.keys.nid] = nid;
+    for (eid in gt.graph.edges)
+        gt.graph.edges[eid][dotty.keys.eid] = eid;
+    for (gid in gt.graph.graphs)
+        gt.graph.graphs[gid][dotty.keys.gid] = gid;
+    gt.unpackattr (gt);
+    dotty.message (1, 'generating layout');
+    gt.layoutgraph (gt);
+    return gt.graph;
+};
+dotty.protogt.erasegraph = function (gt, protogt, protovt) {
+    local vid, vt;
+
+    if (gt.layoutpending > 0)
+        gt.cancellayout (gt);
+    for (vid in gt.views) {
+        vt = gt.views[vid];
+        vt.colors = [];
+        vt.colorn = 2;
+        clear (vt.canvas);
+    }
+    if (~protogt)
+        protogt = dotty.protogt;
+    gt.graph = copy (protogt.graph);
+    gt.undoarray = ['level' = 0; 'entries' = [];];
+};
+dotty.protogt.layoutgraph = function (gt) {
+    if (gt.graph.graphattr.bb) {
+        gt.unpacklayout (gt, gt.graph);
+        gt.setviewsize (gt.views, gt.graph.rect);
+        gt.redrawgraph (gt, gt.views);
+        return;
+    }
+    if (gt.layoutmode == 'async') {
+        if (~gt.haveinput) {
+            gt.startlayout (gt);
+            return;
+        }
+        if (~gt.finishlayout (gt))
+            return;
+        gt.setviewsize (gt.views, gt.graph.rect);
+        gt.redrawgraph (gt, gt.views);
+    } else {
+        if (~gt.startlayout (gt))
+            return;
+        else
+            while (~gt.finishlayout (gt))
+                ;
+        gt.setviewsize (gt.views, gt.graph.rect);
+        gt.redrawgraph (gt, gt.views);
+    }
+};
+dotty.protogt.createview = function (gt, protovt) {
+    local vt, ovt, id, t;
+
+    vt = [];
+    vt.colors = [];
+    vt.colorn = 2;
+    if (~protovt)
+        protovt = dotty.protovt.normal;
+    if (protovt.mode ~= 'replace') {
+        for (id in dotty.protovt[protovt.type])
+            vt[id] = copy (dotty.protovt[protovt.type][id]);
+    }
+    for (id in protovt)
+        vt[id] = copy (protovt[id]);
+    if (~(vt.parent >= 0)) {
+        vt.view = createwidget (-1, [
+            'type'   = 'view';
+            'name'   = vt.name;
+            'origin' = vt.orig;
+            'size'   = vt.size;
+        ]);
+        vt.scroll = createwidget (vt.view, ['type' = 'scroll';]);
+    } else {
+        vt.view = -1;
+        vt.scroll = createwidget (vt.parent, [
+            'type' = 'scroll';
+            'size' = vt.size;
+        ]);
+    }
+    vt.canvas = createwidget (vt.scroll, [
+        'type' = 'canvas';
+        'color' = [0 = protovt.bgcolor; 1 = protovt.fgcolor;];
+    ]);
+    setwidgetattr (vt.canvas, [
+        'window' = vt.wrect;
+        'viewport' = vt.vsize;
+    ]);
+    clear (vt.canvas);
+    dotty.views[vt.canvas] = vt;
+    vt.vtid = vt.canvas;
+    vt.gtid = gt.gtid;
+    gt.views[vt.vtid] = vt;
+    dotty.views[vt.scroll] = vt;
+    if (vt.view ~= -1)
+        dotty.views[vt.view] = vt;
+    if (protovt.colors & tablesize (protovt.colors) > 0) {
+        for (id in protovt.colors)
+            if (setwidgetattr (vt.canvas, ['color' = [
+                protovt.colors[id] = id;
+            ];]) ~= 1) {
+                t = split (id, ' ');
+                if (tablesize (t) ~= 3 | setwidgetattr (vt.canvas, [
+                    'color' = [protovt.colors[id] = [
+                        'h' = ston (t[0]); 's' = ston (t[1]); 'v' = ston (t[2]);
+                    ];];
+                ]) ~= 1) {
+                    dotty.message (0,
+                            concat ('unknown color ', id, ' using #1'));
+                }
+            }
+        vt.colors = copy (protovt.colors);
+        vt.colorn = protovt.colorn;
+    } else if (tablesize (gt.views) > 1) {
+        for (id in gt.views)
+            if (gt.views[id] ~= vt)
+                break;
+        ovt = gt.views[id];
+        for (id in ovt.colors)
+            if (setwidgetattr (vt.canvas, ['color' = [
+                ovt.colors[id] = id;
+            ];]) ~= 1) {
+                t = split (id, ' ');
+                if (tablesize (t) ~= 3 | setwidgetattr (vt.canvas, [
+                    'color' = [ovt.colors[id] = [
+                        'h' = ston (t[0]); 's' = ston (t[1]); 'v' = ston (t[2]);
+                    ];];
+                ]) ~= 1) {
+                    dotty.message (0,
+                            concat ('unknown color ', id, ' using #1'));
+                }
+            }
+        vt.colors = copy (ovt.colors);
+        vt.colorn = ovt.colorn;
+    }
+    if (gt.graph.rect)
+        gt.setviewsize ([vt.vtid = vt;], gt.graph.rect);
+    gt.drawgraph (gt, [vt.vtid = vt;]);
+    for (id in vt.uifuncs)
+        if (id == 'closeview')
+            widgets[vt.view][id] = vt.uifuncs[id];
+        else
+            widgets[vt.canvas][id] = vt.uifuncs[id];
+    return vt;
+};
+dotty.protogt.destroyview = function (gt, vt) {
+    destroywidget (vt.canvas);
+    destroywidget (vt.scroll);
+    if (vt.view ~= -1) {
+        destroywidget (vt.view);
+        remove (vt.view, dotty.views);
+    }
+    remove (vt.scroll, dotty.views);
+    remove (vt.canvas, dotty.views);
+    if (vt.gtid >= 0)
+        remove (vt.vtid, gt.views);
+    if (tablesize (dotty.views) == 0)
+        exit ();
+};
+dotty.protogt.zoom = function (gt, vt, factor, pos) {
+    gt.setviewscale ([vt.vtid = vt;], factor);
+    if (pos)
+        gt.setviewcenter ([vt.vtid = vt;], pos);
+    gt.redrawgraph (gt, [vt.vtid = vt;]);
+};
+dotty.protogt.findnode = function (gt, vt) {
+    local key, node, node1, nid;
+
+    if (~(key = ask ('give node name or label')))
+        return;
+    if (gt.graph.nodedict[key] >= 0)
+        node = gt.graph.nodes[gt.graph.nodedict[key]];
+    else if (gt.graph.nodedict[ston (key)] >= 0)
+        node = gt.graph.nodes[gt.graph.nodedict[ston (key)]];
+    else {
+        for (nid in gt.graph.nodes) {
+            node1 = gt.graph.nodes[nid];
+            if (node1.attr.label == key | node1.attr.label == ston (key)) {
+                node = node1;
+                break;
+            }
+        }
+    }
+    if (~node) {
+        dotty.message (0, concat ('cannot find node: ', key));
+        return;
+    }
+    gt.setviewcenter ([vt.vtid = vt;], node.pos);
+};
+dotty.protogt.setattr = function (gt, obj) {
+    local kv, t, attr, value;
+
+    if (~(kv = ask ('give attr/value, eg. color=blue')))
+        return;
+    t = split (kv, '=');
+    attr = t[0];
+    value = t[1];
+    if (
+        obj.attr == gt.graph.graphattr |
+        obj.attr == gt.graph.edgeattr |
+        obj.attr == gt.graph.nodeattr
+    ) {
+        obj.attr[attr] = value;
+        return;
+    }
+    if (obj.nid >= 0) {
+        gt.undrawnode (gt, gt.views, obj);
+        obj.attr[attr] = value;
+        gt.unpacknodeattr (gt, obj);
+        gt.drawnode (gt, gt.views, obj);
+    } else if (obj.eid >= 0) {
+        gt.undrawedge (gt, gt.views, obj);
+        obj.attr[attr] = value;
+        gt.unpackedgeattr (gt, obj);
+        gt.drawedge (gt, gt.views, obj);
+    }
+};
+dotty.protogt.getattr = function (gt, node) {
+    local kv;
+
+    if (~(kv.key = ask ('give attr name')))
+        return null;
+    if ((kv.val = node.attr[kv.key]))
+        return kv;
+    return null;
+};
+#
+# utilities
+#
+dotty.createviewandgraph = function (name, type, protogt, protovt) {
+    local vt, gt;
+
+    if (~protogt)
+        protogt = dotty.protogt;
+    if (protogt.creategraph)
+        gt = protogt.creategraph (protogt);
+    else
+        gt = dotty.protogt.creategraph (protogt);
+    vt = gt.createview (gt, protovt);
+    if (~protogt.graph)
+        protogt.graph = copy (dotty.protogt.graph);
+    if (name)
+        gt.loadgraph (gt, name, type, protogt.graph, 1);
+    return ['gt' = gt; 'vt' = vt;];
+};
+dotty.openio = function (name, type, mode) {
+    local fd;
+
+    if (~name)
+        return null;
+    if (type == 'file') {
+        if (name == '-') {
+            if (mode == 'r' | mode == 'r+')
+                fd = 0;
+            else
+                fd = 1;
+        } else if (~((fd = openio ('file', name, mode)) >= 0)) {
+            dotty.message (0, concat ('cannot open file: ', name));
+            return null;
+        }
+    } else if (type == 'pipe') {
+        if (~((fd = openio ('pipe', 'ksh', mode,
+                concat ("%e ", name))) >= 0)) {
+            dotty.message (0, concat ('cannot run command: ', name));
+            return null;
+        }
+    } else
+        return null;
+    return fd;
+};
+dotty.pushbusy = function (gt, views) {
+    local vid;
+
+    if (gt.busy == 0)
+        for (vid in gt.views)
+            setwidgetattr (vid, ['cursor' = 'watch';]);
+    gt.busy = gt.busy + 1;
+};
+dotty.popbusy = function (gt, views) {
+    local vid;
+
+    gt.busy = gt.busy - 1;
+    if (gt.busy == 0)
+        for (vid in gt.views)
+            setwidgetattr (vid, ['cursor' = 'default';]);
+};
+dotty.message = function (level, text) {
+    if (level <= dotty.mlevel)
+        echo ('dotty.lefty: ', text);
+};
+#
+# printing or saving to file
+#
+dotty.protogt.printorsave = function (gt, vt, otype, name, mode, ptype) {
+    local pr, wrect, vsize, xy, psize, canvas, pscanvas, cid, cname, t;
+    local graph, edgehandles, fontmap, eid, edge, nid, node, gid, sgraph;
+
+    if (~otype)
+        if (~(otype = ask ('print to', 'choice', 'file|printer')))
+            return;
+    if (otype == 'printer') {
+        name = '/tmp/dottyout.ps';
+        if (getenv ('LEFTYWINSYS') ~= 'mswin' & ~pr)
+            if (~(pr = ask ('printer command', 'string', 'lpr')))
+                return;
+    }
+    if (~name)
+        if (~(name = ask ('postscript file', 'file', 'out.ps')))
+            return;
+    if (~ptype)
+        if (~(ptype = ask ('page size', 'choice', '8.5x11|11x17|36x50')))
+            return;
+    if (~mode)
+        if (~(mode = ask ('mode', 'choice', 'portrait|landscape|best fit')))
+            return;
+    wrect = copy (vt.wrect);
+    wrect[0].x = wrect[0].x - 1;
+    wrect[1].x = wrect[1].x + 1;
+    wrect[0].y = wrect[0].y - 1;
+    wrect[1].y = wrect[1].y + 1;
+    vsize = copy (vt.vsize);
+    if (vsize.x == 0)
+        vsize.x = 1;
+    if (vsize.y == 0)
+        vsize.y = 1;
+    xy = vsize.x / vsize.y;
+    if (mode == 'best fit') {
+        if (xy < 1)
+            mode = 'portrait';
+        else
+            mode = 'landscape';
+    }
+    psize = dotty.pagesizes[ptype];
+    if (mode == 'portrait') {
+        if (xy < psize.x / psize.y) {
+            vsize.y = psize.y * 300;
+            vsize.x = vsize.y * xy;
+        } else {
+            vsize.x = psize.x * 300;
+            vsize.y = vsize.x / xy;
+        }
+    } else {
+        if (xy < psize.y / psize.x) {
+            vsize.y = psize.x * 300;
+            vsize.x = vsize.y * xy;
+        } else {
+            vsize.x = psize.y * 300;
+            vsize.y = vsize.x / xy;
+        }
+    }
+    if (~((pscanvas = createwidget (-1, [
+        'type'   = 'ps';
+        'origin' = ['x' = 0; 'y' = 0;];
+        'size'   = vsize;
+        'mode'   = mode;
+        'name'   = name;
+    ])) >= 0)) {
+        dotty.message (0, 'cannot open printer device');
+        return;
+    }
+    for (cname in vt.colors) {
+        cid = vt.colors[cname];
+        if (setwidgetattr (pscanvas, ['color' = [cid = cname;];]) ~= 1) {
+            t = split (cname, ' ');
+            if (tablesize (t) ~= 3 |
+                    setwidgetattr (pscanvas, ['color' = [cid = [
+                        'h' = ston (t[0]); 's' = ston (t[1]); 'v' = ston (t[2]);
+                    ];];]) ~= 1) {
+                dotty.message (0, concat ('unknown color ',
+                        cname, ' using #1'));
+            }
+        }
+    }
+    setwidgetattr (pscanvas, ['window' = wrect;]);
+    graph = copy (gt.graph);
+    canvas = vt.canvas;
+    vt.canvas = pscanvas;
+    edgehandles = gt.edgehandles;
+    gt.edgehandles = 0;
+    fontmap = dotty.maps[getenv ('LEFTYWINSYS')].psfontmap;
+    for (eid in graph.edges) {
+        edge = graph.edges[eid];
+        edge.fontname = fontmap[edge.attr.fontname];
+        gt.drawedge (gt, [0 = vt;], edge);
+    }
+    for (nid in graph.nodes) {
+        node = graph.nodes[nid];
+        node.fontname = fontmap[node.attr.fontname];
+        gt.drawnode (gt, [0 = vt;], node);
+    }
+    for (gid in graph.graphs) {
+        sgraph = graph.graphs[gid];
+        sgraph.fontname = fontmap[sgraph.graphattr.fontname];
+        gt.drawsgraph (gt, [0 = vt;], sgraph);
+    }
+       graph.fontname = fontmap[graph.graphattr.fontname];
+       gt.drawsgraph (gt, [0 = vt;], graph);
+    gt.edgehandles = edgehandles;
+    vt.canvas = canvas;
+    destroywidget (pscanvas);
+    if (otype == 'printer' & getenv ('LEFTYWINSYS') ~= 'mswin')
+        system (concat (pr, ' /tmp/dottyout.ps; rm /tmp/dottyout.ps'));
+};
diff --git a/cmd/dotty/dotty.sh b/cmd/dotty/dotty.sh
new file mode 100755 (executable)
index 0000000..e4a256e
--- /dev/null
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+FILES=""
+MLEVEL="0"
+LMODE="async"
+
+usage='echo "usage: dotty [-V] [-lm (sync|async)] [-el (0|1)] <filename>"'
+
+if [ "x$DOTTYOPTIONS" != "x" ]; then
+    set -- $DOTTYOPTIONS $*
+fi
+
+while [ "x$1" != 'x' ]; do
+    case $1 in
+    -V)
+        echo "dotty version 96c (09-24-96)"
+        shift
+        ;;
+    -f)
+        shift
+        loadfile=$1
+        shift
+        ;;
+    -lm)
+        shift
+        LMODE=$1
+        if [ "x$LMODE" != 'xsync' -a "x$LMODE" != 'xasync' ]; then
+            $usage
+            exit 1
+        fi
+        shift
+        ;;
+    -el)
+        shift
+        MLEVEL=$1
+        if [ "x$MLEVEL" != 'x0' -a "x$MLEVEL" != 'x1' ]; then
+            $usage
+            exit 1
+        fi
+        shift
+        ;;
+    -)
+        FILES=`echo $FILES \"$1\"`
+        shift
+        ;;
+    -*)
+        $usage
+        exit 1
+        ;;
+    *)
+        FILES=`echo $FILES \"$1\"`
+        shift
+        ;;
+    esac
+done
+
+if [ "x$DOTTYPATH" != 'x' ]; then
+    LEFTYPATH="$DOTTYPATH:$LEFTYPATH"
+fi
+
+CMDS=""
+
+CMDS="dotty.protogt.layoutmode = '$LMODE';"
+
+CMDS=`echo $CMDS dotty.mlevel = $MLEVEL";"`
+
+if [ "x$loadfile" != 'x' ]; then
+    CMDS=`echo $CMDS load \("'"$loadfile"'"\)";"`
+fi
+
+if [ "x$FILES" = 'x' ]; then
+    FILES=null
+fi
+FUNC="dotty.createviewandgraph"
+for i in $FILES; do
+    CMDS=`echo $CMDS $FUNC \($i, "'"file"'", null, null\)";"`
+done
+
+leftypath=`which lefty`
+if [ ! -f "$leftypath" ]; then
+    echo "dotty: cannot locate the lefty program"
+    echo "       make sure that your path includes"
+    echo "       the directory containing dotty and lefty"
+    exit 1
+fi
+
+exec $leftypath -e "
+load ('dotty.lefty');
+checkpath = function () {
+    if (tablesize (dotty) > 0)
+        remove ('checkpath');
+    else {
+        echo ('dotty: cannot locate the dotty scripts');
+        echo ('       make sure that the environment variable LEFTYPATH');
+        echo ('       is set to the directory containing dotty.lefty');
+        exit ();
+    }
+};
+checkpath ();
+dotty.init ();
+monitorfile = dotty.monitorfile;
+$CMDS
+txtview ('off');
+"
diff --git a/cmd/dotty/dotty_draw.lefty b/cmd/dotty/dotty_draw.lefty
new file mode 100644 (file)
index 0000000..5e0276c
--- /dev/null
@@ -0,0 +1,763 @@
+#
+# dotty_draw: drawing functions and data structures
+#
+dotty.protogt.drawgraph = function (gt, views) {
+    local gid, eid, nid, graph;
+
+    graph = gt.graph;
+    gt.drawsgraph (gt, views, graph);
+    for (gid in graph.graphs)
+        gt.drawsgraph (gt, views, graph.graphs[gid]);
+    for (eid in graph.edges)
+        gt.drawedge (gt, views, graph.edges[eid]);
+    for (nid in graph.nodes)
+        gt.drawnode (gt, views, graph.nodes[nid]);
+};
+dotty.protogt.redrawgraph = function (gt, views) {
+    local vid;
+
+    for (vid in views)
+        clear (views[vid].canvas);
+    gt.drawgraph (gt, views);
+};
+dotty.protogt.setviewsize = function (views, r) {
+    local vid, vt, w2v, scale, attr;
+
+    for (vid in views) {
+        vt = views[vid];
+        vt.wrect = copy (r);
+        if (r[1].x == 0 | r[1].y == 0) {
+            attr = getwidgetattr (vt.scroll, [0 = 'size';]);
+            vt.wrect[1] = copy (attr.size);
+        }
+        if (vt.type == 'birdseye') {
+            attr = getwidgetattr (vt.scroll, [0 = 'size';]);
+            scale.x = (vt.wrect[1].x - vt.wrect[0].x) / attr.size.x;
+            scale.y = (vt.wrect[1].y - vt.wrect[0].y) / attr.size.y;
+            if (scale.x > 1 & scale.x > scale.y)
+                vt.w2v = scale.x;
+            else if (scale.y > 1)
+                vt.w2v = scale.y;
+            else
+                vt.w2v = 1;
+        }
+        w2v = vt.w2v;
+        vt.vsize = [
+            'x' = (vt.wrect[1].x - vt.wrect[0].x) / w2v;
+            'y' = (vt.wrect[1].y - vt.wrect[0].y) / w2v;
+        ];
+        setwidgetattr (vt.canvas, [
+            'window' = vt.wrect;
+            'viewport' = vt.vsize;
+        ]);
+    }
+};
+dotty.protogt.setviewscale = function (views, factor) {
+    local vid, vt, w2v;
+
+    for (vid in views) {
+        vt = views[vid];
+        if ((w2v = vt.w2v * factor) < 0.01) {
+            dotty.message (0, 'cannot zoom any closer');
+            return;
+        }
+        vt.w2v = w2v;
+        vt.vsize = [
+            'x' = (vt.wrect[1].x - vt.wrect[0].x) / w2v;
+            'y' = (vt.wrect[1].y - vt.wrect[0].y) / w2v;
+        ];
+        setwidgetattr (vt.canvas, ['viewport' = vt.vsize;]);
+    }
+};
+dotty.protogt.setviewcenter = function (views, center) {
+    local vid, vt, pos;
+
+    for (vid in views) {
+        vt = views[vid];
+        pos = [
+            'x' = center.x * vt.vsize.x / (vt.wrect[1].x - vt.wrect[0].x);
+            'y' = (vt.wrect[1].y - center.y) * vt.vsize.y /
+                (vt.wrect[1].y - vt.wrect[0].y);
+        ];
+        setwidgetattr (vt.scroll, ['childcenter' = pos;]);
+    }
+};
+dotty.protogt.drawsgraph = function (gt, views, sgraph) {
+    local vid, canvas, pos;
+
+    sgraph.draw = 1;
+    if (~sgraph.rect[0] | sgraph.graphattr.style == 'invis')
+        return;
+    for (vid in views) {
+        canvas = views[vid].canvas;
+        if (~sgraph.type) # 'type' only exists on top level
+            box (canvas, null, sgraph.rect, ['color' = sgraph.color;]);
+        if (sgraph.graphattr.label) {
+            if (sgraph.lp.x >= 0) {
+                pos = sgraph.lp;
+                text (canvas, null, pos, sgraph.graphattr.label,
+                        sgraph.fontname, sgraph.fontsize, 'cc',
+                        ['color' = sgraph.fontcolor;]);
+            } else {
+                pos = ['x' = sgraph.rect[0].x; 'y' = sgraph.rect[1].y;];
+                text (canvas, null, pos, sgraph.graphattr.label,
+                        sgraph.fontname, sgraph.fontsize, 'ld',
+                        ['color' = sgraph.fontcolor;]);
+            }
+        }
+    }
+};
+dotty.protogt.undrawsgraph = function (gt, views, sgraph) {
+    local vid, canvas, pos;
+
+    if (~sgraph.drawn)
+        return;
+    sgraph.drawn = 0;
+    if (~sgraph.rect[0] | sgraph.graphattr.style == 'invis')
+        return;
+    for (vid in views) {
+        canvas = views[vid].canvas;
+        if (~sgraph.type) # 'type' only exists on top level
+            box (canvas, null, sgraph.rect, ['color' = 0;]);
+        if (sgraph.graphattr.label) {
+            if (sgraph.lp.x >= 0) {
+                pos = sgraph.lp;
+                text (canvas, null, pos, sgraph.graphattr.label,
+                        sgraph.fontname, sgraph.fontsize, 'cc',
+                        ['color' = 0;]);
+            } else {
+                pos = ['x' = sgraph.rect[0].x; 'y' = sgraph.rect[1].y;];
+                text (canvas, null, pos, sgraph.graphattr.label,
+                        sgraph.fontname, sgraph.fontsize, 'ld',
+                        ['color' = 0;]);
+            }
+        }
+        clearpick (canvas, sgraph);
+    }
+};
+dotty.protogt.drawnode = function (gt, views, node) {
+    local vid, func, pos, size, rect;
+
+    node.drawn = 1;
+    if (~node.pos)
+        return;
+    if (node.attr.style == 'invis') {
+        pos = node.pos;
+        size = node.size;
+        rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+        rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+        for (vid in views)
+            setpick (views[vid].canvas, node, rect);
+        return;
+    }
+    if (~(func = gt.shapefunc[node.attr.shape]))
+        func = gt.shapefunc['box'];
+    for (vid in views)
+        func (gt, views[vid].canvas, node);
+};
+dotty.protogt.undrawnode = function (gt, views, node) {
+    local vid, func, pos, size, rect, color, fontcolor, outlinecolor;
+
+    if (~node.drawn)
+        return;
+    node.drawn = 0;
+    if (~node.pos)
+        return;
+    if (node.attr.style == 'invis') {
+        pos = node.pos;
+        size = node.size;
+        rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+        rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+        for (vid in views)
+            clearpick (views[vid].canvas, node);
+        return;
+    }
+    color = node.color;
+    node.color = 0;
+    fontcolor = node.fontcolor;
+    node.fontcolor = 0;
+    outlinecolor = dotty.outlinecolor;
+    dotty.outlinecolor = 0;
+    if (~(func = gt.shapefunc[node.attr.shape]))
+        func = gt.shapefunc['box'];
+    for (vid in views) {
+        func (gt, views[vid].canvas, node);
+        clearpick (views[vid].canvas, node);
+    }
+    node.color = color;
+    node.fontcolor = fontcolor;
+    dotty.outlinecolor = outlinecolor;
+};
+dotty.protogt.shapefunc.record = function (gt, canvas, node) {
+    local rect, pos, size;
+
+    pos = node.pos;
+    size = node.size;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    if (node.attr.style == 'filled') {
+        box (canvas, node, rect, ['color' = node.color; 'fill' = 'on';]);
+        box (canvas, node, rect, ['color' = dotty.outlinecolor;]);
+    }
+    gt.shapefunc.rfields (gt, canvas, node, node.fields);
+    setpick (canvas, node, rect);
+};
+dotty.protogt.shapefunc.rfields = function (gt, canvas, node, fields) {
+    local fid, field, pos, label;
+
+    for (fid in fields) {
+        field = fields[fid];
+        if (field.fields)
+            gt.shapefunc.rfields (gt, canvas, node, field.fields);
+        else {
+            if (node.attr.style == 'filled')
+                box (canvas, null, field.rect, ['color' = dotty.outlinecolor;]);
+            else
+                box (canvas, null, field.rect, ['color' = node.color;]);
+            pos.x = (field.rect[1].x + field.rect[0].x) / 2;
+            pos.y = (field.rect[1].y + field.rect[0].y) / 2;
+            if (~(label = field.text) | label == '\N')
+                label = node.name;
+            text (canvas, null, pos, label, node.fontname, node.fontsize,
+                    'cc', ['color' = node.fontcolor;]);
+        }
+    }
+};
+dotty.protogt.shapefunc.plaintext = function (gt, canvas, node) {
+    local pos, size, label, rect;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    setpick (canvas, node, rect);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.box = function (gt, canvas, node) {
+    local pos, size, label, rect;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    if (node.attr.style == 'filled') {
+        box (canvas, node, rect, ['color' = node.color; 'fill' = 'on';]);
+        box (canvas, node, rect, ['color' = dotty.outlinecolor;]);
+    } else
+        box (canvas, node, rect, ['color' = node.color;]);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.Msquare = function (gt, canvas, node) {
+    local pos, size, label, rect, color;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    if (node.attr.style == 'filled') {
+        box (canvas, node, rect, ['color' = node.color; 'fill' = 'on';]);
+        color = dotty.outlinecolor;
+        box (canvas, node, rect, ['color' = color;]);
+        line (canvas, null, ['x' = rect[0].x; 'y' = rect[0].y + 10;],
+                ['x' = rect[0].x + 10; 'y' = rect[0].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[0].x; 'y' = rect[1].y - 10;],
+                ['x' = rect[0].x + 10; 'y' = rect[1].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[1].x; 'y' = rect[0].y + 10;],
+                ['x' = rect[1].x - 10; 'y' = rect[0].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[1].x; 'y' = rect[1].y - 10;],
+                ['x' = rect[1].x - 10; 'y' = rect[1].y;], ['color' = color;]);
+    } else {
+        color = node.color;
+        box (canvas, node, rect, ['color' = color;]);
+        line (canvas, null, ['x' = rect[0].x; 'y' = rect[0].y + 10;],
+                ['x' = rect[0].x + 10; 'y' = rect[0].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[0].x; 'y' = rect[1].y - 10;],
+                ['x' = rect[0].x + 10; 'y' = rect[1].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[1].x; 'y' = rect[0].y + 10;],
+                ['x' = rect[1].x - 10; 'y' = rect[0].y;], ['color' = color;]);
+        line (canvas, null, ['x' = rect[1].x; 'y' = rect[1].y - 10;],
+                ['x' = rect[1].x - 10; 'y' = rect[1].y;], ['color' = color;]);
+    }
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.ellipse = function (gt, canvas, node) {
+    local pos, size, label;
+
+    pos = node.pos;
+    size.x = node.size.x / 2;
+    size.y = node.size.y / 2;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    if (node.attr.style == 'filled') {
+        if (node.attr.shape == 'doublecircle') {
+            arc (canvas, node, pos, size, ['color' = dotty.outlinecolor;]);
+            size.x = size.x - 4;
+            size.y = size.y - 4;
+        }
+        arc (canvas, node, pos, size, ['color' = node.color; 'fill' = 'on';]);
+        arc (canvas, node, pos, size, ['color' = dotty.outlinecolor;]);
+    } else {
+        if (node.attr.shape == 'doublecircle') {
+            arc (canvas, node, pos, size, ['color' = node.color;]);
+            size.x = size.x - 4;
+            size.y = size.y - 4;
+        }
+        arc (canvas, node, pos, size, ['color' = node.color;]);
+    }
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.circle = dotty.protogt.shapefunc.ellipse;
+dotty.protogt.shapefunc.doublecircle = dotty.protogt.shapefunc.ellipse;
+dotty.protogt.shapefunc.diamond = function (gt, canvas, node) {
+    local pos, size, label, p, rect;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    p[0] = ['x' = pos.x; 'y' = pos.y + size.y / 2;];
+    p[1] = ['x' = pos.x + size.x / 2; 'y' = pos.y;];
+    p[2] = ['x' = pos.x; 'y' = pos.y - size.y / 2;];
+    p[3] = ['x' = pos.x - size.x / 2; 'y' = pos.y;];
+    p[4] = p[0];
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    if (node.attr.style == 'filled') {
+        polygon (canvas, node, p, ['color' = node.color; 'fill' = 'on';]);
+        polygon (canvas, node, p, ['color' = dotty.outlinecolor;]);
+    } else
+        polygon (canvas, node, p, ['color' = node.color;]);
+    setpick (canvas, node, rect);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.parallelogram = function (gt, canvas, node) {
+    local pos, size, label, rect, color, dx, p;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    dx = (rect[1].x - rect[0].x) / 5;
+    p[0] = ['x' = rect[0].x; 'y' = rect[0].y;];
+    p[1] = ['x' = rect[1].x - dx; 'y' = rect[0].y;];
+    p[2] = ['x' = rect[1].x; 'y' = rect[1].y;];
+    p[3] = ['x' = rect[0].x + dx; 'y' = rect[1].y;];
+    p[4] = ['x' = rect[0].x; 'y' = rect[0].y;];
+    if (node.attr.style == 'filled') {
+        polygon (canvas, node, p, ['color' = node.color; 'fill' = 'on';]);
+        polygon (canvas, node, p, ['color' = dotty.outlinecolor;]);
+    } else
+        polygon (canvas, node, p, ['color' = node.color;]);
+    setpick (canvas, node, rect);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.trapezium = function (gt, canvas, node) {
+    local pos, size, label, rect, color, dx, p;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    dx = (rect[1].x - rect[0].x) / 5;
+    p[0] = ['x' = rect[0].x; 'y' = rect[0].y;];
+    p[1] = ['x' = rect[1].x; 'y' = rect[0].y;];
+    p[2] = ['x' = rect[1].x - dx; 'y' = rect[1].y;];
+    p[3] = ['x' = rect[0].x + dx; 'y' = rect[1].y;];
+    p[4] = ['x' = rect[0].x; 'y' = rect[0].y;];
+    if (node.attr.style == 'filled') {
+        polygon (canvas, node, p, ['color' = node.color; 'fill' = 'on';]);
+        polygon (canvas, node, p, ['color' = dotty.outlinecolor;]);
+    } else
+        polygon (canvas, node, p, ['color' = node.color;]);
+    setpick (canvas, node, rect);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.shapefunc.triangle = function (gt, canvas, node) {
+    local pos, size, label, rect, color, dx, dy, p;
+
+    pos = node.pos;
+    size = node.size;
+    if (~(label = node.attr.label) | label == '\N')
+        label = node.name;
+    rect[0] = ['x' = pos.x - size.x / 2; 'y' = pos.y - size.y / 2;];
+    rect[1] = ['x' = rect[0].x + size.x; 'y' = rect[0].y + size.y;];
+    if (node.attr.orientation ~= -90) {
+        dx = size.x / 2;
+        dy = size.y / 4;
+        p[0] = ['x' = pos.x - dx; 'y' = pos.y - dy;];
+        p[1] = ['x' = pos.x + dx; 'y' = pos.y - dy;];
+        p[2] = ['x' = pos.x;      'y' = rect[1].y;];
+        p[3] = ['x' = pos.x - dx; 'y' = pos.y - dy;];
+    } else {
+        dx = size.x / 4;
+        dy = size.y / 2;
+        p[0] = ['x' = pos.x - dx; 'y' = pos.y - dy;];
+        p[1] = ['x' = pos.x - dx; 'y' = pos.y + dy;];
+        p[2] = ['x' = pos.x + dx * 2; 'y' = pos.y;];
+        p[3] = ['x' = pos.x - dx; 'y' = pos.y - dy;];
+    }
+    if (node.attr.style == 'filled') {
+        polygon (canvas, node, p, ['color' = node.color; 'fill' = 'on';]);
+        polygon (canvas, node, p, ['color' = dotty.outlinecolor;]);
+    } else
+        polygon (canvas, node, p, ['color' = node.color;]);
+    setpick (canvas, node, rect);
+    text (canvas, null, pos, label, node.fontname, node.fontsize,
+            'cc', ['color' = node.fontcolor;]);
+};
+dotty.protogt.movenode = function (gt, node, pos) {
+    local ppos, eid, edge, p, fp, lp;
+
+    ppos = copy (node.pos);
+    gt.undrawnode (gt, gt.views, node);
+    node.pos.x = pos.x;
+    node.pos.y = pos.y;
+    if (node.attr.shape == 'record')
+        gt.moverecordfields (gt, node.fields, pos.x - ppos.x, pos.y - ppos.y);
+    for (eid in node.edges) {
+        edge = node.edges[eid];
+        if (~edge.dir & edge.head ~= edge.tail) {
+            p = edge.tail.pos;
+            fp = edge.points[0];
+            lp = edge.points[tablesize (edge.points)  - 1];
+            if (((p.x - fp.x) * (p.x - fp.x) + (p.y - fp.y) * (p.y - fp.y)) <
+                    ((p.x - lp.x) * (p.x - lp.x) + (p.y - lp.y) * (p.y - lp.y)))
+                edge.dir = 1;
+            else
+                edge.dir = -1;
+        }
+        gt.moveedge (gt, edge, node, ppos, pos);
+    }
+    gt.drawnode (gt, gt.views, node);
+};
+dotty.protogt.moverecordfields = function (gt, fields, dx, dy) {
+    local fid, field;
+
+    for (fid in fields) {
+        field = fields[fid];
+        if (field.fields)
+            gt.moverecordfields (gt, field.fields, dx, dy);
+        else {
+            field.rect[0].x = field.rect[0].x + dx;
+            field.rect[0].y = field.rect[0].y + dy;
+            field.rect[1].x = field.rect[1].x + dx;
+            field.rect[1].y = field.rect[1].y + dy;
+        }
+    }
+};
+dotty.protogt.drawedge = function (gt, views, edge) {
+    local vid, canvas;
+
+    edge.drawn = 1;
+    if (~edge.points)
+        return;
+    if (edge.attr.style == 'invis') {
+        if (gt.edgehandles == 0)
+            return;
+        for (vid in views) {
+            arc (views[vid].canvas, edge, [
+                'x' = (edge.points[1].x + edge.points[2].x) / 2;
+                'y' = (edge.points[1].y + edge.points[2].y) / 2;
+            ], ['x' = 5; 'y' = 5;], ['color' = 1;]);
+        }
+        return;
+    }
+    for (vid in views) {
+        canvas = views[vid].canvas;
+        if (edge.attr.style == 'bold')
+            setgfxattr (canvas, ['width' = 3;]);
+        splinegon (canvas, null, edge.points,
+                ['color' = edge.color; 'style' = edge.attr.style;]);
+        if (edge.sp)
+            arrow (canvas, null, edge.points[0],
+                    edge.sp, ['color' = edge.color;]);
+        if (edge.ep)
+            arrow (canvas, null, edge.points[tablesize (edge.points) - 1],
+                    edge.ep, ['color' = edge.color;]);
+        if (edge.attr.style == 'bold')
+            setgfxattr (canvas, ['width' = 0;]);
+        if (edge.lp)
+            text (canvas, null, edge.lp, edge.attr.label, edge.fontname,
+                    edge.fontsize, 'cc', ['color' = edge.fontcolor;]);
+        if (gt.edgehandles == 0)
+            continue;
+        arc (canvas, edge, [
+            'x' = (edge.points[1].x + edge.points[2].x) / 2;
+            'y' = (edge.points[1].y + edge.points[2].y) / 2;
+        ], ['x' = 5; 'y' = 5;], ['color' = 1;]);
+    }
+};
+dotty.protogt.undrawedge = function (gt, views, edge) {
+    local vid, canvas;
+
+    if (~edge.drawn)
+        return;
+    edge.drawn = 0;
+    if (~edge.points)
+        return;
+    if (edge.attr.style == 'invis') {
+        if (gt.edgehandles == 0)
+            return;
+        for (vid in views) {
+            arc (views[vid].canvas, edge, [
+                'x' = (edge.points[1].x + edge.points[2].x) / 2;
+                'y' = (edge.points[1].y + edge.points[2].y) / 2;
+            ], ['x' = 5; 'y' = 5;], ['color' = 0;]);
+            clearpick (views[vid].canvas, edge);
+        }
+        return;
+    }
+    for (vid in views) {
+        canvas = views[vid].canvas;
+        if (edge.attr.style == 'bold')
+            setgfxattr (canvas, ['width' = 3;]);
+        splinegon (canvas, null, edge.points, ['color' = 0;]);
+        if (edge.sp)
+            arrow (canvas, null, edge.points[0],
+                    edge.sp, ['color' = 0;]);
+        if (edge.ep)
+            arrow (canvas, null, edge.points[tablesize (edge.points) - 1],
+                    edge.ep, ['color' = 0;]);
+        if (edge.attr.style == 'bold')
+            setgfxattr (canvas, ['width' = 0;]);
+        if (edge.lp)
+            text (canvas, null, edge.lp, edge.attr.label, edge.fontname,
+                    edge.fontsize, 'cc', ['color' = 0;]);
+        if (gt.edgehandles == 0)
+            continue;
+        arc (canvas, edge, [
+            'x' = (edge.points[1].x + edge.points[2].x) / 2;
+            'y' = (edge.points[1].y + edge.points[2].y) / 2;
+        ], ['x' = 5; 'y' = 5;], ['color' = 0;]);
+        clearpick (canvas, edge);
+    }
+};
+dotty.protogt.moveedge = function (gt, edge, node, pb, pc) {
+    local dx, dy, tp, hp, pid, p, pa, da, lab, lac, s, ce, se, n, x, y, dir;
+
+    gt.undrawedge (gt, gt.views, edge);
+    dx = pc.x - pb.x; dy = pc.y - pb.y;
+    tp = edge.sp;
+    hp = edge.ep;
+    if (edge.tail == node) {
+        if (edge.head == node) {
+            for (pid in edge.points) {
+                p = edge.points[pid];
+                p.x = p.x + dx; p.y = p.y + dy;
+            }
+            if (tp) {
+                tp.x = tp.x + dx; tp.y = tp.y + dy;
+            }
+            if (hp) {
+                hp.x = hp.x + dx; hp.y = hp.y + dy;
+            }
+            if (edge.lp) {
+                edge.lp.x = edge.lp.x + dx;
+                edge.lp.y = edge.lp.y + dy;
+            }
+            gt.drawedge (gt, gt.views, edge);
+            return;
+        }
+        pa = edge.head.pos;
+        dir = 1;
+    } else {
+        pa = edge.tail.pos;
+        dir = -1;
+    }
+    dir = edge.dir * dir;
+    da = atan (pc.y - pa.y, pc.x - pa.x) - atan (pb.y - pa.y, pb.x - pa.x);
+    lab = sqrt ((pb.y - pa.y) * (pb.y - pa.y) +
+            (pb.x - pa.x) * (pb.x - pa.x));
+    lac = sqrt ((pc.y - pa.y) * (pc.y - pa.y) +
+            (pc.x - pa.x) * (pc.x - pa.x));
+    s = lac / lab;
+    ce = cos (da);
+    se = sin (da);
+    n = tablesize (edge.points);
+    for (pid = 1; pid < n - 1; pid = pid + 1) {
+        p = edge.points[pid];
+        x = p.x - pa.x;
+        y = p.y - pa.y;
+        p.x = pa.x + (ce * x - se * y) * s;
+        p.y = pa.y + (se * x + ce * y) * s;
+    }
+    if (dir == 1) {
+        p = edge.points[0];
+        p.x = p.x + dx; p.y = p.y + dy;
+        if (tp) {
+            tp.x = tp.x + dx; tp.y = tp.y + dy;
+        }
+    } else {
+        p = edge.points[n - 1];
+        p.x = p.x + dx; p.y = p.y + dy;
+        if (hp) {
+            hp.x = hp.x + dx; hp.y = hp.y + dy;
+        }
+    }
+    if (edge.lp) {
+        x = edge.lp.x - pa.x;
+        y = edge.lp.y - pa.y;
+        edge.lp.x = pa.x + (ce * x - se * y) * s;
+        edge.lp.y = pa.y + (se * x + ce * y) * s;
+    }
+    gt.drawedge (gt, gt.views, edge);
+};
+dotty.protogt.getcolor = function (views, name) {
+    local vid, vt, color, t;
+
+    for (vid in views) {
+        vt = views[vid];
+        if (~(color >= 0)) {
+            if (~(vt.colors[name] >= 0))
+                color = (vt.colors[name] = vt.colorn);
+            else {
+                color = vt.colors[name];
+                break;
+            }
+        } else if (~(vt.colors[name] >= 0))
+            vt.colors[name] = color;
+        else if (vt.colors[name] ~= color)
+            dotty.message (0, concat ('inconsistent color ids for ', name));
+        if (setwidgetattr (vt.canvas, ['color' = [color = name;];]) ~= 1) {
+            t = split (name, ' ');
+            if (tablesize (t) ~= 3 |
+                    setwidgetattr (vt.canvas, ['color' = [color = [
+                        'h' = ston (t[0]); 's' = ston (t[1]); 'v' = ston (t[2]);
+                    ];];]) ~= 1) {
+                dotty.message (0, concat ('unknown color ', name, ' using #1'));
+                return 1;
+            }
+        }
+        vt.colorn = color + 1;
+    }
+    return color;
+};
+dotty.protogt.unpacksgraphattr = function (gt, sgraph) {
+    local attr;
+
+    attr = sgraph.graphattr;
+    if (dotty.fontmap[attr.fontname])
+        sgraph[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+    else
+        sgraph[dotty.keys.fname] = attr.fontname;
+    sgraph[dotty.keys.fsize] = ston (attr.fontsize);
+    sgraph[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+    if (attr.style == 'filled' & attr.color == 'black')
+        sgraph[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+    else
+        sgraph[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+};
+dotty.protogt.unpacknodeattr = function (gt, node) {
+    local attr;
+
+    attr = node.attr;
+    if (dotty.fontmap[attr.fontname])
+        node[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+    else
+        node[dotty.keys.fname] = attr.fontname;
+    node[dotty.keys.fsize] = ston (attr.fontsize);
+    node[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+    if (attr.style == 'filled' & attr.color == 'black')
+        node[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+    else
+        node[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+};
+dotty.protogt.unpackedgeattr = function (gt, edge) {
+    local attr, n;
+
+    attr = edge.attr;
+    if (dotty.fontmap[attr.fontname])
+        edge[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+    else
+        edge[dotty.keys.fname] = attr.fontname;
+    edge[dotty.keys.fsize] = ston (attr.fontsize);
+    edge[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+    if (attr.style == 'filled' & attr.color == 'black')
+        edge[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+    else
+        edge[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+    if (attr.label & attr.label ~= '' & ~edge.lp & edge.points) {
+        if ((n = tablesize (edge.points)) > 4)
+            edge.lp = [
+                'x' = edge.points[toint (n / 2)].x + 5;
+                'y' = edge.points[toint (n / 2)].y + 5;
+            ];
+        else
+            edge.lp = [
+                'x' = (edge.points[1].x + edge.points[2].x) / 2 + 5;
+                'y' = (edge.points[1].y + edge.points[2].y) / 2 + 5;
+            ];
+    }
+};
+dotty.protogt.unpackattr = function (gt) {
+    local gid, sgraph, nid, node, eid, edge, graph, attr;
+
+    graph = gt.graph;
+    attr = graph.graphattr;
+    if (dotty.fontmap[attr.fontname])
+        graph[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+    else
+        graph[dotty.keys.fname] = attr.fontname;
+    graph[dotty.keys.fsize] = ston (attr.fontsize);
+    graph[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+    if (attr.style == 'filled' & attr.color == 'black')
+        graph[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+    else
+        graph[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+    for (gid in graph.graphdict) {
+        sgraph = graph.graphs[graph.graphdict[gid]];
+        attr = sgraph.graphattr;
+        if (dotty.fontmap[attr.fontname])
+            sgraph[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+        else
+            sgraph[dotty.keys.fname] = attr.fontname;
+        sgraph[dotty.keys.fsize] = ston (attr.fontsize);
+        sgraph[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+        if (attr.style == 'filled' & attr.color == 'black')
+            sgraph[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+        else
+            sgraph[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+    }
+    for (nid in graph.nodedict) {
+        node = graph.nodes[graph.nodedict[nid]];
+        attr = node.attr;
+        if (dotty.fontmap[attr.fontname])
+            node[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+        else
+            node[dotty.keys.fname] = attr.fontname;
+        node[dotty.keys.fsize] = ston (attr.fontsize);
+        node[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+        if (attr.style == 'filled' & attr.color == 'black')
+            node[dotty.keys.color] = gt.getcolor (gt.views, 'grey');
+        else
+            node[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+    }
+    for (eid in graph.edges) {
+        edge = graph.edges[eid];
+        attr = edge.attr;
+        if (dotty.fontmap[attr.fontname])
+            edge[dotty.keys.fname] = dotty.fontmap[attr.fontname];
+        else
+            edge[dotty.keys.fname] = attr.fontname;
+        edge[dotty.keys.fsize] = ston (attr.fontsize);
+        edge[dotty.keys.fcolor] = gt.getcolor (gt.views, attr.fontcolor);
+        edge[dotty.keys.color] = gt.getcolor (gt.views, attr.color);
+    }
+};
diff --git a/cmd/dotty/dotty_edit.lefty b/cmd/dotty/dotty_edit.lefty
new file mode 100644 (file)
index 0000000..d49fbe0
--- /dev/null
@@ -0,0 +1,583 @@
+#
+# dotty_edit: editing functions and data structures
+#
+dotty.protogt.getnodesbyattr = function (gt, key, val) {
+    local nid, node, nlist;
+
+    nlist = [];
+    for (nid in gt.graph.nodes) {
+        node = gt.graph.nodes[nid];
+        if (node.attr[key] == val)
+            nlist[nid] = node;
+    }
+    return nlist;
+};
+dotty.protogt.reachablenodes = function (gt, node) {
+    local nlist, stack, eid, edge, i;
+
+    stack[0] = node;
+    i = 1;
+    while (i > 0) {
+        node = stack[i - 1];
+        i = i - 1;
+        nlist[node.nid] = node;
+        for (eid in node.edges) {
+            edge = node.edges[eid];
+            if (~nlist[edge.head.nid]) {
+                nlist[edge.head.nid] = edge.head;
+                stack[i] = edge.head;
+                i = i + 1;
+            }
+        }
+    }
+    return nlist;
+};
+dotty.protogt.mergegraph = function (gt, graph, show) {
+    local nameid, onode, pos, size, eid, eid2, tnode, hnode, oedge;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    for (nameid in graph.nodedict) {
+        pos = null;
+        size = null;
+        onode = graph.nodes[graph.nodedict[nameid]];
+        if (onode.pos)
+            pos = node.pos;
+        if (onode.size)
+            size = node.size;
+        if (~(gt.graph.nodedict[nameid] >= 0)) {
+            pos = null;
+            size = null;
+            if (onode.pos)
+                pos = node.pos;
+            if (onode.size)
+                size = node.size;
+            gt.insertnode (gt, pos, size, nameid, onode.attr, show);
+        }
+    }
+    for (eid in graph.edges) {
+        oedge = graph.edges[eid];
+        tnode = gt.graph.nodes[gt.graph.nodedict[oedge.tail.name]];
+        hnode = gt.graph.nodes[gt.graph.nodedict[oedge.head.name]];
+        for (eid2 in tnode.edges)
+            if (tnode.edges[eid2].tail == tnode &
+                    tnode.edges[eid2].head == hnode) {
+                oedge = null;
+                break;
+            }
+        if (oedge)
+            gt.insertedge (gt, tnode, null, hnode, null, oedge.attr, show);
+    }
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.insertsgraph = function (gt, name, attr, show) {
+    local gid, sgraph, aid;
+
+    if (~gt)
+        return null;
+    gid = gt.graph.maxgid;
+    if (~name) {
+        while (gt.graph.graphdict[(name = concat ('g', gid))] >= 0)
+            gid = gid + 1;
+    } else if (gt.graph.graphdict[name]) {
+        dotty.message (0, concat ('graph: ', name, ' exists'));
+        return null;
+    }
+    gt.graph.graphdict[name] = gid;
+    gt.graph.maxgid = gid + 1;
+    gt.graph.graphs[gid] = [
+        dotty.keys.gid   = gid;
+        dotty.keys.name  = name;
+        dotty.keys.gattr = copy (gt.graph.graphattr);
+        dotty.keys.nattr = copy (gt.graph.nodeattr);
+        dotty.keys.eattr = copy (gt.graph.edgeattr);
+    ];
+    sgraph = gt.graph.graphs[gid];
+    if (~attr)
+        attr = [];
+    if (~attr.label)
+        attr.label = '\N';
+    for (aid in attr)
+        sgraph.graphattr[aid] = attr[aid];
+    gt.unpacksgraphattr (gt, sgraph);
+    if (show)
+        gt.drawsgraph (gt, gt.views, sgraph);
+    return sgraph;
+};
+dotty.protogt.removesgraph = function (gt, sgraph) {
+    gt.undrawsgraph (gt, gt.views, sgraph);
+    remove (sgraph.name, gt.graph.graphdict);
+    remove (sgraph.gid, gt.graph.graphs);
+};
+dotty.protogt.insertnode = function (gt, pos, size, name, attr, show) {
+    local nid, node, aid;
+
+    nid = gt.graph.maxnid;
+    if (~name) {
+        while (gt.graph.nodedict[(name = concat ('n', nid))] >= 0)
+            nid = nid + 1;
+    } else if (gt.graph.nodedict[name] >= 0) {
+        dotty.message (0, concat ('node: ', name, ' exists'));
+        return null;
+    }
+    gt.graph.nodedict[name] = nid;
+    gt.graph.maxnid = nid + 1;
+    gt.graph.nodes[nid] = [
+        dotty.keys.nid   = nid;
+        dotty.keys.name  = name;
+        dotty.keys.attr  = copy (gt.graph.nodeattr);
+        dotty.keys.edges = [];
+    ];
+    node = gt.graph.nodes[nid];
+    if (~attr)
+        attr = [];
+    if (~attr.label)
+        attr.label = '\N';
+    for (aid in attr)
+        node.attr[aid] = attr[aid];
+    gt.unpacknodeattr (gt, node);
+    if (~pos)
+        pos = ['x' = 10; 'y' = 10;];
+    node[dotty.keys.pos] = copy (pos);
+    if (~size)
+        size = ['x' = strlen (attr.label) * 30; 'y' = 30;];
+    if (size.x == 0)
+        size.x = 30;
+    node[dotty.keys.size] = copy (size);
+    if (show)
+        gt.drawnode (gt, gt.views, node);
+    if (~gt.noundo) {
+        gt.startadd2undo (gt);
+        gt.currundo.inserted.nodes[nid] = node;
+        gt.endadd2undo (gt);
+    }
+    return node;
+};
+dotty.protogt.removenode = function (gt, node) {
+    local eid, list, edge, gid;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    for (eid in node.edges)
+        list[eid] = node.edges[eid];
+    for (eid in list)
+        gt.removeedge (gt, list[eid]);
+    gt.undrawnode (gt, gt.views, node);
+    for (gid in gt.graph.graphs)
+        remove (node.nid, gt.graph.graphs[gid].nodes);
+    remove (node.name, gt.graph.nodedict);
+    remove (node.nid, gt.graph.nodes);
+    if (~gt.noundo) {
+        gt.currundo.deleted.nodes[node.nid] = node;
+        gt.endadd2undo (gt);
+    }
+};
+dotty.protogt.insertedge =
+        function (gt, nodea, porta, nodeb, portb, attr, show) {
+    local eid, edge, aid, tport, hport;
+
+    if (~nodea | ~nodeb)
+        return null;
+    if (porta)
+        tport = porta;
+    if (portb)
+        hport = portb;
+    eid = gt.graph.maxeid;
+    while (gt.graph.edges[eid])
+        eid = eid + 1;
+    gt.graph.maxeid = eid + 1;
+    gt.graph.edges[eid] = [
+        dotty.keys.eid   = eid;
+        dotty.keys.tail  = nodea;
+        dotty.keys.tport = porta;
+        dotty.keys.head  = nodeb;
+        dotty.keys.hport = portb;
+        dotty.keys.attr  = copy (gt.graph.edgeattr);
+    ];
+    edge = gt.graph.edges[eid];
+    if (~attr)
+        attr = [];
+    for (aid in attr)
+        edge.attr[aid] = attr[aid];
+    nodea.edges[eid] = edge;
+    nodeb.edges[eid] = edge;
+    edge[dotty.keys.points] = [
+        0 = copy (nodea.pos);
+        1 = copy (nodea.pos);
+        2 = copy (nodeb.pos);
+        3 = copy (nodeb.pos);
+    ];
+    gt.unpackedgeattr (gt, edge);
+    if (show)
+        gt.drawedge (gt, gt.views, edge);
+    if (~gt.noundo) {
+        gt.startadd2undo (gt);
+        gt.currundo.inserted.edges[eid] = edge;
+        gt.endadd2undo (gt);
+    }
+    return edge;
+};
+dotty.protogt.removeedge = function (gt, edge) {
+    local head, tail;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    if (edge.head.attr.support == 1)
+        head = edge.head;
+    if (edge.tail.attr.support == 1)
+        if (head ~= edge.tail)
+            tail = edge.tail;
+    gt.undrawedge (gt, gt.views, edge);
+    remove (edge.eid, edge.head.edges);
+    remove (edge.eid, edge.tail.edges);
+    remove (edge.eid, gt.graph.edges);
+    if (head & tablesize (head.edges) == 0)
+        gt.removenode (gt, head);
+    if (tail & tablesize (tail.edges) == 0)
+        gt.removenode (gt, tail);
+    if (~gt.noundo) {
+        gt.currundo.deleted.edges[edge.eid] = edge;
+        gt.endadd2undo (gt);
+    }
+};
+dotty.protogt.swapedgeids = function (gt, edge1, edge2) {
+    local eid1, eid2;
+
+    if (edge1.eid == edge2.eid)
+        return;
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    eid1 = edge1.eid;
+    eid2 = edge2.eid;
+    gt.graph.edges[eid1] = edge2;
+    gt.graph.edges[eid2] = edge1;
+    remove (eid1, edge1.tail.edges);
+    remove (eid1, edge1.head.edges);
+    remove (eid2, edge2.tail.edges);
+    remove (eid2, edge2.head.edges);
+    edge1.tail.edges[eid2] = edge1;
+    edge1.head.edges[eid2] = edge1;
+    edge2.tail.edges[eid1] = edge2;
+    edge2.head.edges[eid1] = edge2;
+    edge1.eid = eid2;
+    edge2.eid = eid1;
+    if (~gt.noundo) {
+        gt.currundo.swapped.edges[eid1] = edge1;
+        gt.currundo.swapped.edges[eid2] = edge2;
+        gt.endadd2undo (gt);
+    }
+};
+dotty.protogt.removesubtree = function (gt, obj) {
+    local nlist, node, head, nid, edge, eid;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    if (obj.nid >= 0)
+        node = obj;
+    else if (obj.eid >= 0) {
+        node = obj.head;
+        gt.removeedge (gt, obj);
+        if (~gt.graph.nodes[node.nid]) {
+            if (~gt.noundo)
+                gt.endadd2undo (gt);
+            return;
+        }
+        for (eid in node.edges) {
+            edge = node.edges[eid];
+            if (edge.head == node & edge.tail ~= node) {
+                if (~gt.noundo)
+                    gt.endadd2undo (gt);
+                return;
+            }
+        }
+    } else {
+        dotty.message (0, 'bad object type in gt.removesubtree');
+        return;
+    }
+    nlist = [node.nid = node;];
+    while (node) {
+        for (eid in node.edges) {
+            head = node.edges[eid].head;
+            if (head ~= node)
+                nlist[head.nid] = head;
+        }
+        gt.removenode (gt, node);
+        remove (node.nid, nlist);
+        node = null;
+        for (nid in nlist) {
+            node = nlist[nid];
+            for (eid in node.edges) {
+                edge = node.edges[eid];
+                if (edge.head == node & edge.tail ~= node) {
+                    node = null;
+                    break;
+                }
+            }
+            if (node)
+                break;
+        }
+    }
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.removenodesbyattr = function (gt, key, val) {
+    local nlist, nid;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    nlist = gt.getnodesbyattr (gt, key, val);
+    for (nid in nlist)
+        gt.removenode (gt, nlist[nid]);
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.removesubtreesbyattr = function (gt, key, val) {
+    local nlist, nid;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    nlist = gt.getnodesbyattr (gt, key, val);
+    for (nid in nlist)
+        if (gt.graph.nodes[nid])
+            gt.removesubtree (gt, nlist[nid]);
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.groupnodes = function (gt, nlist, gnode, pos, size, attr,
+        keepmulti, show) {
+    local nid, node, elist, eid, edge, nodea, nodeb, inlist, outlist;
+
+    if (~nlist | tablesize (nlist) == 0)
+        return;
+    if (gnode.attr.support) {
+        dotty.message (0, 'cannot group nodes in a support node');
+        return;
+    }
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    if (~gnode)
+        gnode = gt.insertnode (gt, pos, size, null, attr, show);
+    inlist = [];
+    outlist = [];
+    for (nid in nlist) {
+        if ((node = nlist[nid]) == gnode)
+            continue;
+        elist = [];
+        for (eid in node.edges)
+            elist[eid] = node.edges[eid];
+        for (eid in elist) {
+            edge = elist[eid];
+            if (edge.head == node) {
+                nodea = edge.tail;
+                nodeb = gnode;
+                if (~keepmulti) {
+                    if (inlist[nodea.nid])
+                        continue;
+                    inlist[nodea.nid] = nodea;
+                    if (nodea == gnode)
+                        outlist[nodea.nid] = nodea;
+                }
+            } else {
+                nodea = gnode;
+                nodeb = edge.head;
+                if (~keepmulti) {
+                    if (outlist[nodeb.nid])
+                        continue;
+                    outlist[nodeb.nid] = nodeb;
+                    if (nodeb == gnode)
+                        inlist[nodeb.nid] = nodeb;
+                }
+            }
+            gt.insertedge (gt, nodea, null, nodeb, null, edge.attr, show);
+        }
+        gt.removenode (gt, node);
+    }
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+    return gnode;
+};
+dotty.protogt.groupnodesbyattr =
+        function (gt, key, val, attr, keepmulti, show) {
+    local nlist, nid, pos, size;
+
+    pos = null;
+    size = null;
+    nlist = gt.getnodesbyattr (gt, key, val);
+    if (show)
+        for (nid in nlist) {
+            pos = nlist[nid].pos;
+            size = nlist[nid].size;
+            break;
+        }
+    return gt.groupnodes (gt, nlist, null, pos, size, attr, keepmulti, show);
+};
+dotty.protogt.cut = function (gt, obj, set, mode, op) {
+    local clipgt, list, node, nid, edge, eid, clipnode;
+
+    clipgt = dotty.clipgt;
+    clipgt.graph = copy (dotty.protogt.graph);
+    if (obj.eid >= 0) { # it's an edge
+        list.edges[obj.eid] = obj;
+        node = obj.head;
+    } else if (obj.nid >= 0) {
+        list.nodes[obj.nid] = obj;
+        node = obj;
+        for (eid in node.edges)
+            list.edges[eid] = node.edges[eid];
+    } else {
+        dotty.message (0, 'unknown object type in gt.cut');
+        return;
+    }
+    if (set == 'reachable') {
+        list.nodes = gt.reachablenodes (gt, node);
+        for (nid in list.nodes) {
+            node = list.nodes[nid];
+            for (eid in node.edges) {
+                edge = node.edges[eid];
+                list.edges[edge.eid] = edge;
+            }
+        }
+    }
+    if (mode == 'support') {
+        for (eid in list.edges) {
+            edge = list.edges[eid];
+            if (~list.nodes[edge.tail.nid]) {
+                list.support[edge.tail.nid] = edge.tail;
+                list.nodes[edge.tail.nid] = edge.tail;
+            }
+            if (~list.nodes[edge.head.nid]) {
+                list.support[edge.head.nid] = edge.head;
+                list.nodes[edge.head.nid] = edge.head;
+            }
+        }
+    }
+    for (nid = 0; nid < gt.graph.maxnid; nid = nid + 1) {
+        if (~list.nodes[nid])
+            continue;
+        node = list.nodes[nid];
+        clipnode = gt.insertnode (clipgt, null, null, node.name, node.attr, 0);
+        if (list.support[nid])
+            clipnode.support = 1;
+        list.clipnodes[nid] = clipnode;
+    }
+    for (eid = 0; eid < gt.graph.maxeid; eid = eid + 1) {
+        if (~list.edges[eid])
+            continue;
+        edge = list.edges[eid];
+        if (~list.nodes[edge.tail.nid] | ~list.nodes[edge.head.nid])
+            continue;
+        gt.insertedge (clipgt, list.clipnodes[edge.tail.nid], null,
+                list.clipnodes[edge.head.nid], null, edge.attr, 0);
+    }
+    if (op ~= 'cut')
+        return;
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    for (eid in list.edges)
+        gt.removeedge (gt, list.edges[eid]);
+    for (nid in list.nodes)
+        if (~list.support[nid] & gt.graph.nodes[nid])
+            gt.removenode (gt, list.nodes[nid]);
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.paste = function (gt, pos, show) {
+    local clipgt, offset, center, nid, node, eid, edge, nodes;
+
+    if (~gt.noundo)
+        gt.startadd2undo (gt);
+    clipgt = dotty.clipgt;
+    if (clipgt.graph.rect)
+        center = [
+            'x' = (clipgt.graph.rect[1].x + clipgt.graph.rect[0].x) / 2;
+            'y' = (clipgt.graph.rect[1].y + clipgt.graph.rect[0].y) / 2;
+        ];
+    else
+        center = pos;
+    offset = [
+        'x' = center.x - pos.x;
+        'y' = center.y - pos.y;
+    ];
+    for (nid = 0; clipgt.graph.nodes[nid]; nid = nid + 1) {
+        node = clipgt.graph.nodes[nid];
+        if (node.attr.label == '\N' | ~node.attr.label)
+            node.attr.label = node.name;
+        if (node.support == 1)
+            nodes[nid] = gt.insertnode (gt, [
+                'x' = node.pos.x - offset.x;
+                'y' = node.pos.y - offset.y;
+            ], null, null, [
+                'support' = 1; 'shape' = 'circle';
+                'label' = ''; 'width' = 0.2;
+            ], show);
+        else
+            nodes[nid] = gt.insertnode (gt, [
+                'x' = node.pos.x - offset.x;
+                'y' = node.pos.y - offset.y;
+            ], node.size, null, node.attr, show);
+    }
+    for (eid = 0; clipgt.graph.edges[eid]; eid = eid + 1) {
+        edge = clipgt.graph.edges[eid];
+        gt.insertedge (gt, nodes[edge.tail.nid], null,
+                nodes[edge.head.nid], null, edge.attr, show);
+    }
+    if (~gt.noundo)
+        gt.endadd2undo (gt);
+};
+dotty.protogt.startadd2undo = function (gt) {
+    if (~gt.undoarray.level)
+        gt.currundo =
+                (gt.undoarray.entries[tablesize (gt.undoarray.entries)] = []);
+    gt.undoarray.level = gt.undoarray.level + 1;
+};
+dotty.protogt.endadd2undo = function (gt) {
+    gt.undoarray.level = gt.undoarray.level - 1;
+};
+dotty.protogt.undo = function (gt, show) {
+    local entry, n, eid, edge, nid, node, edges;
+
+    if ((n = tablesize (gt.undoarray.entries)) < 1)
+        return;
+    entry = gt.undoarray.entries[n - 1];
+    remove (n - 1, gt.undoarray.entries);
+    remove ('currundo', gt);
+    gt.noundo = 1;
+    # hardwire nodes and edges back with the same id's as the originals
+    for (nid in entry.deleted.nodes) {
+        node = entry.deleted.nodes[nid];
+        gt.graph.nodedict[node.name] = node.nid;
+        gt.graph.nodes[node.nid] = node;
+        node.edges = [];
+        if (show)
+            gt.drawnode (gt, gt.views, node);
+    }
+    for (eid in entry.deleted.edges) {
+        edge = entry.deleted.edges[eid];
+        gt.graph.edges[edge.eid] = edge;
+        edge.head.edges[edge.eid] = edge;
+        edge.tail.edges[edge.eid] = edge;
+        if (show)
+            gt.drawedge (gt, gt.views, edge);
+    }
+    if (entry.swapped.edges) {
+        if (tablesize (entry.swapped.edges) == 2) {
+            n = 0;
+            for (eid in entry.swapped.edges) {
+                edges[n] = entry.swapped.edges[eid];
+                n = n + 1;
+            }
+            gt.swapedgeids (gt, edges[0], edges[1]);
+        } else
+            dotty.message (0, 'cannot handle undoing swap of > 2 edges');
+    }
+    for (eid in entry.inserted.edges) {
+        edge = entry.inserted.edges[eid];
+        gt.removeedge (gt, edge);
+    }
+    for (nid in entry.inserted.nodes) {
+        node = entry.inserted.nodes[nid];
+        gt.removenode (gt, node);
+    }
+    gt.noundo = 0;
+};
diff --git a/cmd/dotty/dotty_layout.lefty b/cmd/dotty/dotty_layout.lefty
new file mode 100644 (file)
index 0000000..12c0ef9
--- /dev/null
@@ -0,0 +1,272 @@
+#
+# dotty_layout: layout functions and data structures
+#
+dotty.grablserver = function (lserver) {
+    local fd;
+
+    if (~dotty.lservers[lserver] | tablesize (dotty.lservers[lserver]) == 0) {
+        if (~((fd = openio ('pipe', lserver, 'r+')) >= 0)) {
+            dotty.message (0, concat ('cannot start ', lserver));
+            return null;
+        }
+        dotty.lservers[lserver][fd] = [
+            'fd' = fd;
+            'count' = 0;
+        ];
+    }
+    for (fd in dotty.lservers[lserver]) {
+        dotty.lservers[lserver][fd].count =
+                dotty.lservers[lserver][fd].count + 1;
+        dotty.lservers.inuse[fd] = dotty.lservers[lserver][fd];
+        remove (fd, dotty.lservers[lserver]);
+        return fd;
+    }
+};
+dotty.releaselserver = function (lserver, fd, state) {
+    if (state == 'bad' | dotty.lservers.inuse[fd].count > 40) {
+        closeio (fd, 'kill');
+        remove (fd, dotty.lservers.inuse);
+        return;
+    }
+    dotty.lservers[lserver][fd] = dotty.lservers.inuse[fd];
+    remove (fd, dotty.lservers.inuse);
+};
+dotty.protogt.startlayout = function (gt) {
+    local lpt, fd;
+
+    if (gt.layoutpending >= 1) {
+        lpt = dotty.layoutpending[gt.gtid];
+        if (gt.layoutmode == 'async')
+            monitor ('off', lpt.fd);
+        dotty.releaselserver (gt.lserver, lpt.fd, 'bad');
+        remove (gt.gtid, dotty.layoutpending);
+        gt.layoutpending = 0;
+        gt.haveinput = 0;
+        dotty.popbusy (gt, gt.views);
+    }
+    if (~((fd = dotty.grablserver (gt.lserver)) >= 0))
+        return null;
+    dotty.pushbusy (gt, gt.views);
+    writegraph (fd, gt.graph, 1);
+    gt.layoutpending = 1;
+    dotty.layoutpending[gt.gtid] = [
+        'fd' = fd;
+        'gtid' = gt.gtid;
+    ];
+    if (gt.layoutmode == 'async')
+        monitor ('on', fd);
+    return 1;
+};
+dotty.protogt.finishlayout = function (gt) {
+    local graph, lpt, fd;
+
+    if (~(gt.layoutpending >= 1)) {
+        dotty.message (0, concat ('no layout pending for graph ', gt.gtid));
+        return null;
+    }
+    lpt = dotty.layoutpending[gt.gtid];
+    if (~(graph = readgraph (lpt.fd))) {
+        if (gt.layoutmode == 'async')
+            monitor ('off', lpt.fd);
+        dotty.releaselserver (gt.lserver, lpt.fd, 'bad');
+        if (gt.layoutpending == 2) {
+            dotty.message (0, concat ('giving up on ', gt.lserver));
+            if ((fd = openio ('file', 'dottybug.dot', 'w+')) >= 0) {
+                writegraph (fd, gt.graph, 0);
+                closeio (fd);
+                dotty.message (0,
+                        concat ('graph that causes ', gt.lserver));
+                dotty.message (0,
+                        'to fail has been saved in file dottybug.dot');
+                dotty.message (0,
+                        'please fill out a bug report at http://www.research.att.com/~erg/graphviz/bugform.html');
+            }
+            dotty.popbusy (gt, gt.views);
+            gt.layoutpending = 0;
+            gt.haveinput = 0;
+            return 1;
+        }
+        dotty.message (1,
+                concat ('lost connection to ', gt.lserver, ', restarting...'));
+        lpt.fd = dotty.grablserver (gt.lserver);
+        writegraph (lpt.fd, gt.graph, 1);
+        if (gt.layoutmode == 'async')
+            monitor ('on', lpt.fd);
+        gt.layoutpending = 2;
+        gt.haveinput = 0;
+        return null;
+    }
+    if (gt.layoutmode == 'async')
+        monitor ('off', lpt.fd);
+    dotty.releaselserver (gt.lserver, lpt.fd, null);
+    remove (gt.gtid, dotty.layoutpending);
+    gt.layoutpending = 0;
+    gt.haveinput = 0;
+    gt.unpacklayout (gt, graph);
+    dotty.popbusy (gt, gt.views);
+    return 1;
+};
+dotty.protogt.cancellayout = function (gt) {
+    local lpt, vid;
+
+    if (gt.layoutpending >= 1) {
+        lpt = dotty.layoutpending[gt.gtid];
+        if (gt.layoutmode == 'async')
+            monitor ('off', lpt.fd);
+        dotty.releaselserver (gt.lserver, lpt.fd, 'bad');
+        remove (gt.gtid, dotty.layoutpending);
+        gt.layoutpending = 0;
+        gt.haveinput = 0;
+        dotty.popbusy (gt, gt.views);
+    }
+};
+dotty.protogt.unpacklayout = function (gt, graph2) {
+    local graph, gid, sgraph1, sgraph2, nid, node1, node2;
+    local t1, t2, t3, n2, i, j, k, l, m, eid, edge1, edge2, points;
+    local pa1, pa2, pb1, pb2, la, lb;
+
+    graph = gt.graph;
+    for (gid in graph2.graphdict) {
+        if (~(sgraph1 = graph.graphs[graph.graphdict[gid]]))
+            continue;
+        sgraph2 = graph2.graphs[graph2.graphdict[gid]];
+        if (sgraph2.graphattr.bb & sgraph2.graphattr.bb ~= '') {
+            t1 = split (sgraph2.graphattr.bb, ',');
+            sgraph1.rect = [
+                0 = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+                1 = ['x' = ston (t1[2]); 'y' = ston (t1[3]);];
+            ];
+        } else
+            sgraph1.rect = [];
+        if (sgraph2.graphattr.lp & sgraph2.graphattr.lp ~= '') {
+            t1 = split (sgraph2.graphattr.lp, ',');
+            sgraph1.lp = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+        } else
+            sgraph1.lp = [];
+    }
+    for (nid in graph2.nodedict) {
+        if (~(node1 = graph.nodes[graph.nodedict[nid]]))
+            continue;
+        node2 = graph2.nodes[graph2.nodedict[nid]];
+        t1 = split (node2.attr.pos, ',');
+        node1.pos = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+        node1.size.x = ston (node2.attr.width) * 72;
+        node1.size.y = ston (node2.attr.height) * 72;
+        if (node2.attr.rects)
+            node1.fields = parsegraphlabel (node2.attr.label, node2.attr.rects);
+    }
+    for (eid in graph2.edges) {
+        edge2 = graph2.edges[eid];
+        if (edge2.attr.id) {
+            if (~(edge1 = graph.edges[ston (edge2.attr.id)]))
+                continue;
+        } else if (graph == graph2)
+            edge1 = edge2;
+        if (edge2.attr.pos) {
+            points = [];
+            remove ('sp', edge1);
+            remove ('ep', edge1);
+            t2 = split (edge2.attr.pos, ';');
+            for (k = 0; t2[k]; k = k + 1) {
+                t3 = split (t2[k], ' ');
+                n2 = tablesize (t3);
+                j = 0;
+            i = 0;
+                t1 = split (t3[0], ',');
+            while (t1[0] == 's' | t1[0] == 'e') {
+                if (t1[0] == 's')
+                    edge1.sp = ['x' = ston (t1[1]); 'y' = ston (t1[2]);];
+                else # (t1[0] == 'e')
+                    edge1.ep = ['x' = ston (t1[1]); 'y' = ston (t1[2]);];
+                i = i + 1;
+                    t1 = split (t3[i], ',');
+            }
+                points[k][j] = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+            i = i + 1;
+                j = j + 1;
+            while (i < n2) {
+                    t1 = split (t3[i], ',');
+                    points[k][j] = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+                j = j + 1;
+                i = i + 1;
+            }
+        }
+            if (k > 1) { # concentrators
+                l = k;
+                while (l > 1) {
+                    la = tablesize (points[0]);
+                    pa1 = points[0][0];
+                    pa2 = points[0][la - 1];
+                    for (k = 1; points[k]; k = k + 1) {
+                        lb = tablesize (points[k]);
+                        pb1 = points[k][0];
+                        pb2 = points[k][lb - 1];
+                        if (pa1.x == pb2.x & pa1.y == pb2.y) {
+                            for (m = 1; m < la; m = m + 1) {
+                                points[k][lb] = points[0][m];
+                                lb = lb + 1;
+                            }
+                            points[0] = points[l - 1];
+                            remove (l - 1, points);
+                            break;
+                        } else if (pa2.x == pb1.x & pa2.y == pb1.y) {
+                            for (m = 1; m < lb; m = m + 1) {
+                                points[0][la] = points[k][m];
+                                la = la + 1;
+                            }
+                            points[k] = points[l - 1];
+                            remove (l - 1, points);
+                            break;
+                        }
+                    }
+                    if (points[l - 1]) {
+                        dotty.message (1, 'failed to match edge points');
+                        break;
+                    }
+                    l = l - 1;
+                }
+            }
+            edge1.points = points[0];
+        }
+        if (edge2.attr.lp) {
+            t1 = split (edge2.attr.lp, ',');
+            edge1.lp = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+        }
+    }
+    t1 = split (graph2.graphattr.bb, ',');
+    graph.rect[0].x = ston (t1[0]);
+    graph.rect[0].y = ston (t1[1]);
+    graph.rect[1].x = ston (t1[2]);
+    graph.rect[1].y = ston (t1[3]);
+    if (graph2.graphattr.lp & graph2.graphattr.lp ~= '') {
+        t1 = split (graph2.graphattr.lp, ',');
+        graph.lp = ['x' = ston (t1[0]); 'y' = ston (t1[1]);];
+    } else
+        graph.lp = [];
+    if (gt.graph ~= graph2)
+        return;
+    # strip position and size info from the attributes
+    for (gid in graph2.graphdict) {
+        sgraph2 = graph2.graphs[graph2.graphdict[gid]];
+        if (sgraph2.graphattr.bb)
+            remove ('bb', sgraph2.graphattr);
+    }
+    for (nid in graph2.nodedict) {
+        node2 = graph2.nodes[graph2.nodedict[nid]];
+        if (node2.attr.rects)
+            remove ('rects', node2.attr);
+        remove ('pos', node2.attr);
+        remove ('width', node2.attr);
+        remove ('height', node2.attr);
+    }
+    for (eid in graph2.edges) {
+        edge2 = graph2.edges[eid];
+        if (edge2.attr.pos)
+            remove ('pos', edge2.attr);
+        if (edge2.attr.lp)
+            remove ('lp', edge2.attr);
+    }
+    remove ('bb', graph2.graphattr);
+    if (graph2.graphattr.lp)
+        remove ('lp', graph2.graphattr);
+};
diff --git a/cmd/dotty/dotty_ui.lefty b/cmd/dotty/dotty_ui.lefty
new file mode 100644 (file)
index 0000000..7f6f9da
--- /dev/null
@@ -0,0 +1,434 @@
+#
+# dotty_ui: user interface functions and data structures
+#
+dotty.protogt.doaction = function (data, s) {
+    local vt, gt;
+
+    vt = dotty.views[data.widget];
+    gt = dotty.graphs[vt.gtid];
+    if (data.obj.nid >= 0) {
+        if (gt.actions.node[s]) {
+            gt.actions.node[s] (gt, vt, data.obj, data);
+            return;
+        }
+    } else if (data.obj.eid >= 0) {
+        if (gt.actions.edge[s]) {
+            gt.actions.edge[s] (gt, vt, data.obj, data);
+            return;
+        }
+    }
+    if (gt.actions.general[s])
+        gt.actions.general[s] (gt, vt, data);
+};
+dotty.protogt.actions.general = [
+    "undo" = function (gt, vt, data) {
+        gt.undo (gt, 1);
+    };
+    "paste" = function (gt, vt, data) {
+        gt.paste (gt, data.pos, 1);
+    };
+    "do layout" = function (gt, vt, data) {
+        gt.layoutgraph (gt);
+    };
+    "cancel layout" = function (gt, vt, data) {
+        gt.cancellayout (gt);
+    };
+    "redraw" = function (gt, vt, data) {
+        gt.redrawgraph (gt, [vt.vtid = vt;]);
+    };
+    "new graph" = function (gt, vt, data) {
+        gt.erasegraph (gt, null, null);
+    };
+    "load graph" = function (gt, vt, data) {
+        gt.loadgraph (gt, null, 'file', dotty.protogt.graph, 1);
+    };
+    "reload graph" = function (gt, vt, data) {
+        gt.loadgraph (gt, gt.name, gt.type, gt.graph, 1);
+    };
+    "save graph" = function (gt, vt, data) {
+        gt.savegraph (gt, gt.name, gt.type, 1);
+    };
+    "save graph as" = function (gt, vt, data) {
+        gt.savegraph (gt, null, 'file', 1);
+    };
+    "open view" = function (gt, vt, data) {
+        gt = dotty.protogt.creategraph (null);
+        gt.createview (gt, null);
+    };
+    "copy view" = function (gt, vt, data) {
+        gt = gt.copygraph (gt);
+        gt.createview (gt, vt);
+    };
+    "birdseye view" = function (gt, vt, data) {
+        gt.createview (gt, dotty.protovt.birdseye);
+    };
+    "clone view" = function (gt, vt, data) {
+        gt.createview (gt, vt);
+    };
+    "close view" = function (gt, vt, data) {
+        gt.destroyview (gt, vt);
+        if (tablesize (gt.views) == 0)
+            gt.destroygraph (gt);
+    };
+    "set graph attr" = function (gt, vt, data) {
+        gt.setattr (gt, ['attr' = gt.graph.graphattr;]);
+    };
+    "set node attr" = function (gt, vt, data) {
+        gt.setattr (gt, ['attr' = gt.graph.nodeattr;]);
+    };
+    "set edge attr" = function (gt, vt, data) {
+        gt.setattr (gt, ['attr' = gt.graph.edgeattr;]);
+    };
+    "zoom in" = function (gt, vt, data) {
+        gt.zoom (gt, vt, 0.5, data.pos);
+    };
+    "zoom out" = function (gt, vt, data) {
+        gt.zoom (gt, vt, 2, data.pos);
+    };
+    "zoom in slowly" = function (gt, vt, data) {
+        gt.zoom (gt, vt, 0.9, data.pos);
+    };
+    "zoom out slowly" = function (gt, vt, data) {
+        gt.zoom (gt, vt, 1.1, data.pos);
+    };
+    "find node" = function (gt, vt, data) {
+        gt.findnode (gt, vt);
+    };
+    "print graph" = function (gt, vt, data) {
+        gt.printorsave (gt, vt, null, null, null, null);
+    };
+    "text view" = function (gt, vt, data) {
+        if (dotty.txtview == 'on')
+            dotty.txtview = 'off';
+        else
+            dotty.txtview = 'on';
+        txtview (dotty.txtview);
+    };
+    "quit" = function (gt, vt, data) {
+        exit ();
+    };
+];
+dotty.protogt.actions.node = [
+    "cut" = function (gt, vt, obj, data) {
+        gt.cut (gt, obj, 'one', 'support', 'cut');
+        dotty.clipgt.layoutgraph (dotty.clipgt);
+    };
+    "Cut" = function (gt, vt, obj, data) {
+        gt.cut (gt, obj, 'reachable', 'support', 'cut');
+        dotty.clipgt.layoutgraph (dotty.clipgt);
+    };
+    "copy" = function (gt, vt, obj, data) {
+        gt.cut (gt, obj, 'one', 'support', 'copy');
+        dotty.clipgt.layoutgraph (dotty.clipgt);
+    };
+    "Copy" = function (gt, vt, obj, data) {
+        gt.cut (gt, obj, 'reachable', 'support', 'copy');
+        dotty.clipgt.layoutgraph (dotty.clipgt);
+    };
+    "group" = function (gt, vt, obj, data) {
+        local kv;
+
+        if ((kv = gt.getattr (gt, obj)))
+            gt.groupnodesbyattr (gt, kv.key, kv.val, [
+                'label' = kv.val; kv.key = kv.val;
+            ], 1, 1);
+    };
+    "Group" = function (gt, vt, obj, data) {
+        local kv;
+
+        if ((kv = gt.getattr (gt, obj)))
+            gt.groupnodesbyattr (gt, kv.key, kv.val, [
+                'label' = kv.val; kv.key = kv.val;
+            ], 0, 1);
+    };
+    "delete" = function (gt, vt, obj, data) {
+        if (obj.eid >= 0)
+            gt.removeedge (gt, obj);
+        else
+            gt.removenode (gt, obj);
+    };
+    "Delete" = function (gt, vt, obj, data) {
+        gt.removesubtree (gt, obj);
+    };
+    "remove" = function (gt, vt, obj, data) {
+        if (obj.nid >= 0)
+            if ((kv = gt.getattr (gt, obj)))
+                gt.removenodesbyattr (gt, kv.key, kv.val);
+    };
+    "Remove" = function (gt, vt, obj, data) {
+        if (obj.nid >= 0)
+            if ((kv = gt.getattr (gt, obj)))
+                gt.removesubtreesbyattr (gt, kv.key, kv.val);
+    };
+    "set attr" = function (gt, vt, obj, data) {
+        gt.setattr (gt, obj);
+    };
+    "print attr" = function (gt, vt, obj, data) {
+        if (obj.nid >= 0)
+            echo ('node: ', obj.name);
+        dump (obj.attr);
+    };
+];
+dotty.protogt.actions.edge = dotty.protogt.actions.node;
+dotty.protovt.normal.menus = [
+    'general' = [
+        0 = "undo";
+        1 = "paste";
+        2 = "do layout";
+        3 = "cancel layout";
+        4 = "redraw";
+        5 = "new graph";
+        6 = "load graph";
+        7 = "reload graph";
+        8 = "save graph";
+        9 = "save graph as";
+        10 = "open view";
+        11 = "copy view";
+        12 = "clone view";
+        13 = "birdseye view";
+        14 = "close view";
+        15 = "set graph attr";
+        16 = "set node attr";
+        17 = "set edge attr";
+        18 = "zoom in";
+        19 = "zoom out";
+        20 = "find node";
+        21 = "print graph";
+        22 = "text view";
+        23 = "quit";
+    ];
+    'node' = [
+        0 = "cut";
+        1 = "Cut";
+        2 = "copy";
+        3 = "Copy";
+        4 = "group";
+        5 = "Group";
+        6 = "delete";
+        7 = "Delete";
+        8 = "remove";
+        9 = "Remove";
+        10 = "set attr";
+        11 = "print attr";
+    ];
+    'edge' = [
+        0 = "cut";
+        1 = "Cut";
+        2 = "copy";
+        3 = "Copy";
+        4 = "delete";
+        5 = "Delete";
+        6 = "set attr";
+        7 = "print attr";
+    ];
+];
+dotty.protovt.normal.keys = [
+    'general' = [
+        'u' = "undo";
+        'p' = "paste";
+        'l' = "do layout";
+        'k' = "cancel layout";
+        ' ' = "redraw";
+        'L' = "reload graph";
+        's' = "save graph";
+        'Z' = "zoom in slowly";
+        'z' = "zoom out slowly";
+    ];
+    'node' = [
+        'c' = "copy";
+        'C' = "Copy";
+        'g' = "group";
+        'G' = "Group";
+        'd' = "delete";
+        'D' = "Delete";
+        'r' = "remove";
+        'R' = "Remove";
+        'a' = "set attr";
+    ];
+    'edge' = [
+        'c' = "copy";
+        'C' = "Copy";
+        'd' = "delete";
+        'D' = "Delete";
+        'a' = "set attr";
+    ];
+];
+dotty.protovt.birdseye.menus = dotty.protovt.normal.menus;
+dotty.protovt.birdseye.keys = dotty.protovt.normal.keys;
+dotty.protovt.normal.uifuncs = [
+    'leftdown' = function (data) {
+        local gt;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        if (data.obj.nid >= 0) {
+            dotty.node2move = data.obj;
+            dotty.movewidget = data.widget;
+            dotty.rp2 = data.pos;
+        }
+    };
+    'leftmove' = function (data) {
+        local gt;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        if (dotty.node2move &
+                (dotty.rp2.x ~= data.pos.x | dotty.rp2.y ~= data.pos.y)) {
+            gt.movenode (gt, dotty.node2move, data.pos);
+            dotty.rp2 = data.pos;
+        }
+    };
+    'leftup' = function (data) {
+        local gt;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        if (dotty.node2move) {
+            if (dotty.movewidget == data.widget)
+                gt.movenode (gt, dotty.node2move, data.pos);
+            dotty.node2move = 0;
+        } else if (~data.obj)
+            gt.insertnode (gt, data.pos, null, null, null, 1);
+    };
+    'middledown' = function (data) {
+        if (~(data.obj.nid >= 0))
+            return;
+        dotty.rubberband = 1;
+        dotty.movewidget = data.widget;
+        setgfxattr (data.widget, ['mode' = 'xor';]);
+        dotty.rp1 = data.pos;
+        dotty.rp2 = data.pos;
+        line (data.widget, null, dotty.rp1, dotty.rp2, ['color' = 1;]);
+    };
+    'middlemove' = function (data) {
+        if (dotty.rubberband ~= 1 |
+                (dotty.rp2.x == data.pos.x & dotty.rp2.y == data.pos.y))
+            return;
+        line (data.widget, null, dotty.rp1, dotty.rp2, ['color' = 1;]);
+        dotty.rp2 = data.pos;
+        line (data.widget, null, dotty.rp1, dotty.rp2, ['color' = 1;]);
+    };
+    'middleup' = function (data) {
+        local gt;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        if (dotty.rubberband ~= 1)
+            return;
+        dotty.rubberband = 0;
+        line (dotty.movewidget, null, dotty.rp1, dotty.rp2, ['color' = 1;]);
+        setgfxattr (dotty.movewidget, ['mode' = 'src';]);
+        if (dotty.movewidget ~= data.widget |
+                ~(data.pobj.nid >= 0) | ~(data.obj.nid >= 0))
+            return;
+        if (data.pobj.attr.support)
+            gt.groupnodes (gt, [
+                data.obj.nid = data.obj;
+                data.pobj.nid = data.pobj;
+            ], data.obj, null, null, null, 1, 1);
+        else if (data.obj.attr.support)
+            gt.groupnodes (gt, [
+                data.obj.nid = data.obj;
+                data.pobj.nid = data.pobj;
+            ], data.pobj, null, null, null, 1, 1);
+        else
+            gt.insertedge (gt, data.pobj, null, data.obj, null, null, 1);
+    };
+    'rightdown' = function (data) {
+        local vt, gt, menu, i;
+
+        vt = dotty.views[data.widget];
+        gt = dotty.graphs[vt.gtid];
+        if (~data.obj)
+            menu = vt.menus.general;
+        else if (data.obj.nid >= 0)
+            menu = vt.menus.node;
+        else if (data.obj.eid >= 0)
+            menu = vt.menus.edge;
+        if ((i = displaymenu (data.widget, menu)) >= 0)
+            gt.doaction (data, menu[i]);
+    };
+    'keyup' = function (data) {
+        local vt, gt, action;
+
+        vt = dotty.views[data.widget];
+        gt = dotty.graphs[vt.gtid];
+        if (data.obj.nid >= 0) {
+            if (vt.keys.node[data.key])
+                action = vt.keys.node[data.key];
+        } else if (data.obj.eid >= 0) {
+            if (vt.keys.edge[data.key])
+                action = vt.keys.edge[data.key];
+        }
+        if (~action)
+            if (vt.keys.general[data.key])
+                action = vt.keys.general[data.key];
+        if (action)
+            gt.doaction (data, action);
+    };
+    'redraw' = function (data) {
+        local vt, gt;
+
+        vt = dotty.views[data.widget];
+        gt = dotty.graphs[vt.gtid];
+        gt.drawgraph (gt, [vt.vtid = vt;]);
+    };
+    'closeview' = function (data) {
+        local vt, gt;
+
+        vt = dotty.views[data.widget];
+        gt = dotty.graphs[vt.gtid];
+        gt.destroyview (gt, vt);
+        if (tablesize (gt.views) == 0)
+            gt.destroygraph (gt);
+    };
+];
+dotty.protovt.birdseye.uifuncs = [
+    'leftdown' = function (data) {
+        local gt, vid;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        for (vid in gt.views) {
+            vt = gt.views[vid];
+            if (vt.type ~= 'birdseye')
+                gt.setviewcenter ([vid = vt;], data.pos);
+        }
+    };
+    'leftmove' = function (data) {
+        local gt, vid;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        for (vid in gt.views) {
+            vt = gt.views[vid];
+            if (vt.type ~= 'birdseye')
+                gt.setviewcenter ([vid = vt;], data.pos);
+        }
+    };
+    'leftup' = function (data) {
+        local gt, vid;
+
+        gt = dotty.graphs[dotty.views[data.widget].gtid];
+        for (vid in gt.views) {
+            vt = gt.views[vid];
+            if (vt.type ~= 'birdseye')
+                gt.setviewcenter ([vid = vt;], data.pos);
+        }
+    };
+    'middledown' = dotty.protovt.normal.uifuncs.middledown;
+    'middlemove' = dotty.protovt.normal.uifuncs.middlemove;
+    'middleup' = dotty.protovt.normal.uifuncs.middleup;
+    'rightdown' = dotty.protovt.normal.uifuncs.rightdown;
+    'keyup' = dotty.protovt.normal.uifuncs.keyup;
+    'redraw' = dotty.protovt.normal.uifuncs.redraw;
+    'closeview' = dotty.protovt.normal.uifuncs.closeview;
+];
+dotty.monitorfile = function (data) {
+    local gtid, gt, lpt;
+
+    for (gtid in dotty.layoutpending) {
+        lpt = dotty.layoutpending[gtid];
+        if (lpt.fd == data.fd) {
+            gt = dotty.graphs[lpt.gtid];
+            gt.haveinput = 1;
+            gt.layoutgraph (gt);
+            return 1;
+        }
+    }
+    return 0;
+};