GUI -- Augmented pan/zoom & select/drag integration by having a toggle button for whether meta needs to be pressed for panning (default) or selecting.
 - multi-select requires the shift key to be held down.
 - Also re-wired deselectAll() to the ESC key, instead of click on background.

Change-Id: I63502839368c6ca10c64ee583a58f836576c4546
diff --git a/web/gui/src/main/webapp/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
index e647a37..90b3032 100644
--- a/web/gui/src/main/webapp/d3Utils.js
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -37,6 +37,9 @@
         if (!$.isFunction(atDragEnd)) {
             alert('d3util.createDragBehavior(): atDragEnd is not a function')
         }
+        if (!$.isFunction(requireMeta)) {
+            alert('d3util.createDragBehavior(): requireMeta is not a function')
+        }
 
         function dragged(d) {
             var threshold = draggedThreshold(force.alpha()),
@@ -51,7 +54,7 @@
         drag = d3.behavior.drag()
             .origin(function(d) { return d; })
             .on('dragstart', function(d) {
-                if (requireMeta ^ !d3.event.sourceEvent.metaKey) {
+                if (requireMeta() ^ !d3.event.sourceEvent.metaKey) {
                     d3.event.sourceEvent.stopPropagation();
 
                     d.oldX = d.x;
@@ -62,7 +65,7 @@
                 }
             })
             .on('drag', function(d) {
-                if (requireMeta ^ !d3.event.sourceEvent.metaKey) {
+                if (requireMeta() ^ !d3.event.sourceEvent.metaKey) {
                     d.px = d3.event.x;
                     d.py = d3.event.y;
                     if (dragged(d)) {
diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css
index 9cf1783..fa23835 100644
--- a/web/gui/src/main/webapp/mast2.css
+++ b/web/gui/src/main/webapp/mast2.css
@@ -100,6 +100,7 @@
 }
 
 #bb .btn {
+    margin: 0 4px;
     padding: 2px 6px;
     font-size: 9pt;
     cursor: pointer;
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 41fcd6b..9fd92d7 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -120,15 +120,16 @@
 
     // key bindings
     var keyDispatch = {
-        M: testMe,                  // TODO: remove (testing only)
-        S: injectStartupEvents,     // TODO: remove (testing only)
-        space: injectTestEvent,     // TODO: remove (testing only)
+        //M: testMe,                  // TODO: remove (testing only)
+        //S: injectStartupEvents,     // TODO: remove (testing only)
+        //space: injectTestEvent,     // TODO: remove (testing only)
 
-        B: toggleBg,                // TODO: do we really need this?
+        B: toggleBg,
         L: cycleLabels,
         P: togglePorts,
         U: unpin,
         R: resetZoomPan,
+        esc: deselectAll,
 
         W: requestTraffic,  // bag of selections
         X: cancelTraffic,
@@ -1040,7 +1041,6 @@
             node.append('circle')
                 .attr('r', 8);     // TODO: define host circle radius
 
-            // TODO: are we attaching labels to hosts?
             node.append('text')
                 .text(hostLabel)
                 .attr('dy', '1.3em')
@@ -1231,7 +1231,13 @@
 
     function selectObject(obj, el) {
         var n,
-            meta = d3.event.sourceEvent.metaKey;
+            srcEv = d3.event.sourceEvent,
+            meta = srcEv.metaKey,
+            shift = srcEv.shiftKey;
+
+        if ((metaSelect() && !meta) || (!metaSelect() && meta)) {
+            return;
+        }
 
         if (el) {
             n = d3.select(el);
@@ -1244,13 +1250,13 @@
         }
         if (!n) return;
 
-        if (meta && n.classed('selected')) {
+        if (shift && n.classed('selected')) {
             deselectObject(obj.id);
             updateDetailPane();
             return;
         }
 
-        if (!meta) {
+        if (!shift) {
             deselectAll();
         }
 
@@ -1282,15 +1288,6 @@
         updateDetailPane();
     }
 
-    // FIXME: this click handler does not get unloaded when the view does
-    $('#view').on('click', function(e) {
-        if (!$(e.target).closest('.node').length) {
-            if (!e.metaKey) {
-                deselectAll();
-            }
-        }
-    });
-
     // update the state of the detail pane, based on current selections
     function updateDetailPane() {
         var nSel = selectOrder.length;
@@ -1376,7 +1373,7 @@
 
     function setupZoomPan() {
         function zoomed() {
-            if (!d3.event.sourceEvent.metaKey) {
+            if (!metaSelect() ^ !d3.event.sourceEvent.metaKey) {
                 zoomPan(d3.event.scale, d3.event.translate);
             }
         }
@@ -1425,26 +1422,31 @@
 
     }
 
-    function para(sel, text) {
-        sel.append('p').text(text);
-    }
+    // ==============================
+    // Toggle Buttons in masthead
 
     // TODO: toggle button (and other widgets in the masthead) should be provided
     //  by the framework; not generated by the view.
 
-    var showTrafficOnHover;
+    var showTrafficOnHover,
+        metaToSelect;
 
     function addButtonBar(view) {
         var bb = d3.select('#mast')
             .append('span').classed('right', true).attr('id', 'bb');
 
-        showTrafficOnHover = bb.append('div')
+        metaToSelect = bb.append('span')
+            .classed('btn', true)
+            .text('Meta to select')
+            .on('click', toggleMetaSelect);
+
+        showTrafficOnHover = bb.append('span')
             .classed('btn', true)
             .text('Show traffic on hover')
-            .on('click', toggleShowTraffic);
+            .on('click', toggleTrafficHover);
     }
 
-    function toggleShowTraffic() {
+    function toggleTrafficHover() {
         showTrafficOnHover.classed('active', !trafficHover());
     }
 
@@ -1452,6 +1454,14 @@
         return showTrafficOnHover.classed('active');
     }
 
+    function toggleMetaSelect() {
+        metaToSelect.classed('active', !metaSelect());
+    }
+
+    function metaSelect() {
+        return metaToSelect.classed('active');
+    }
+
     // ==============================
     // View life-cycle callbacks
 
@@ -1519,8 +1529,8 @@
                 id: d.id,
                 'class': d.class,
                 'memento': {
-                    x: Math.floor(d.x),
-                    y: Math.floor(d.y)
+                    x: d.x,
+                    y: d.y
                 }
             });
         }
@@ -1537,7 +1547,8 @@
             .linkStrength(lstrg)
             .on('tick', tick);
 
-        network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd, true); // true=require meta
+        network.drag = d3u.createDragBehavior(network.force,
+            selectCb, atDragEnd, metaSelect);
 
         // create mask layer for when we lose connection to server.
         mask = view.$div.append('div').attr('id','topo-mask');
@@ -1546,6 +1557,11 @@
         para(mask, 'Try refreshing the page.');
     }
 
+    function para(sel, text) {
+        sel.append('p').text(text);
+    }
+
+
     function load(view, ctx, flags) {
         // resize, in case the window was resized while we were not loaded
         resize(view, ctx, flags);
@@ -1647,9 +1663,6 @@
 
     function resize(view, ctx, flags) {
         setSize(svg, view);
-
-        // TODO: hook to recompute layout, perhaps? work with zoom/pan code
-        // adjust force layout size
     }