GUI -- Key Bindings pop-up (new branch)

Change-Id: I544c80b1ce78c231985d7104f60b59bba3b7911e
diff --git a/web/gui/src/main/webapp/keymap.css b/web/gui/src/main/webapp/keymap.css
index bf19036..7e6ade0 100644
--- a/web/gui/src/main/webapp/keymap.css
+++ b/web/gui/src/main/webapp/keymap.css
@@ -23,13 +23,41 @@
 #keymap svg {
     position: absolute;
     bottom: 40px;
-    opacity: 0.5;
+    opacity: 0.7;
+}
+
+#keymap svg g.pane {
+    fill: black;
 }
 
 #keymap svg text.title {
     font-size: 12pt;
     font-style: italic;
-    fill: #666;
     text-anchor: middle;
+    fill: #444;
+}
+
+#keymap svg g.keyItem {
+    fill: white;
+}
+
+#keymap svg g.keyItem line {
+    stroke: #444;
+    stroke-dasharray: 4 4;
+}
+
+#keymap svg text {
+    font-size: 10pt;
+    alignment-baseline: middle;
+}
+
+#keymap svg text.key {
+    font-size: 10pt;
+    fill: #8aa;
+}
+
+#keymap svg text.desc {
+    font-size: 10pt;
+    fill: #888;
 }
 
diff --git a/web/gui/src/main/webapp/keymap.js b/web/gui/src/main/webapp/keymap.js
index 292f7fe..0c75d4c 100644
--- a/web/gui/src/main/webapp/keymap.js
+++ b/web/gui/src/main/webapp/keymap.js
@@ -33,9 +33,14 @@
     var w = '100%',
         h = '80%',
         fade = 750,
-        vb = '-200 -200 400 400',
-        xpad = 20,
-        ypad = 10;
+        vb = '-220 -220 440 440',
+        paneW = 400,
+        paneH = 340,
+        offy = 65,
+        dy = 20,
+        offKey = 40,
+        offDesc = offKey + 50,
+        lineW = paneW - (2*offKey);
 
     // State variables
     var data = [];
@@ -43,104 +48,121 @@
     // DOM elements and the like
     var kmdiv = d3.select('#keymap');
 
-    function computeBox(el) {
-        var text = el.select('text'),
-            box = text.node().getBBox();
-
-        // center
-        box.x = -box.width / 2;
-        box.y = -box.height / 2;
-
-        // add some padding
-        box.x -= xpad;
-        box.width += xpad * 2;
-        box.y -= ypad;
-        box.height += ypad * 2;
-
-        return box;
+    function isA(a) {
+        return $.isArray(a) ? a : null;
     }
 
-    function updateKeymap() {
-        var items = svg.selectAll('.bindingItem')
+
+    var svg = kmdiv.select('svg'),
+        pane;
+
+    function updateKeyItems() {
+        var items = pane.selectAll('.keyItem')
             .data(data);
 
         var entering = items.enter()
             .append('g')
             .attr({
-                class: 'bindingItem',
-                opacity: 0
-            })
-            .transition()
-            .duration(fade)
-            .attr('opacity', 1);
+                id: function (d) { return d.id; },
+                class: 'keyItem'
+            });
 
-        entering.each(function (d) {
+        entering.each(function (d, i) {
             var el = d3.select(this),
-                box;
+                y = offy + dy * i;
 
-            d.el = el;
-            el.append('rect').attr({ rx: 10, ry: 10});
-            el.append('text').text(d.label);
-            box = computeBox(el);
-            el.select('rect').attr(box);
+            if (d.id === '_') {
+                el.append('line')
+                    .attr({
+                        class: 'sep',
+                        x1: offKey,
+                        y1: y,
+                        x2: offKey + lineW,
+                        y2: y
+                    });
+            } else {
+                el.append('text')
+                    .text(d.key)
+                    .attr({
+                        class: 'key',
+                        x: offKey,
+                        y: y
+                    });
+
+                el.append('text')
+                    .text(d.desc)
+                    .attr({
+                        class: 'desc',
+                        x: offDesc,
+                        y: y
+                    });
+            }
+        });
+    }
+
+    function aggregateData(bindings) {
+        var gmap = d3.map(bindings.globalKeys),
+            vmap = d3.map(bindings.viewKeys),
+            gkeys = gmap.keys(),
+            vkeys = vmap.keys();
+
+        gkeys.sort();
+        vkeys.sort();
+
+        data = [];
+        gkeys.forEach(function (k) {
+            addItem('global', k, gmap.get(k));
+        });
+        addItem('separator');
+        vkeys.forEach(function (k) {
+            addItem('view', k, vmap.get(k));
         });
 
-        items.exit()
-            .transition()
-            .duration(fade)
-            .attr({ opacity: 0})
-            .remove();
-    }
-
-    function clearFlash() {
-        if (timer) {
-            clearInterval(timer);
+        function addItem(type, k, d) {
+            var id = type + '-' + k,
+                a = isA(d),
+                desc = a && a[1];
+            if (desc) {
+                data.push(
+                    {
+                        id: id,
+                        type: type,
+                        key: k,
+                        desc: desc
+                    }
+                );
+            } else if (type === 'separator') {
+                data.push({
+                    id: '_',
+                    type: type
+                });
+            }
         }
-        data = [];
-        updateFeedback();
+
     }
 
-    // for now, simply display some text feedback
-    function flash(text) {
-        // cancel old scheduled event if there was one
-        if (timer) {
-            clearInterval(timer);
-        }
-        timer = setInterval(function () {
-            clearFlash();
-        }, showFor);
-
-        data = [{
-            label: text
-        }];
-        updateFeedback();
-    }
-
-    // =====================================
-    var svg = kmdiv.select('svg');
-
     function populateBindings(bindings) {
         svg.append('g')
             .attr({
                 class: 'keyBindings',
-                transform: 'translate(-200,-120)',
+                transform: 'translate(-200,-200)',
                 opacity: 0
             })
             .transition()
             .duration(fade)
             .attr('opacity', 1);
 
-        var g = svg.select('g');
+        pane = svg.select('g');
 
-        g.append('rect')
+        pane.append('rect')
             .attr({
-                width: 400,
-                height: 240,
+                width: paneW,
+                height: paneH,
                 rx: 8
             });
 
-        g.append('text')
-            .text('Key Bindings')
+        pane.append('text')
+            .text('Keyboard Shortcuts')
             .attr({
                 x: 200,
                 y: 0,
@@ -148,11 +170,12 @@
                 class: 'title'
             });
 
-        // TODO: append .keyItems to rectangle
+        aggregateData(bindings);
+        updateKeyItems();
     }
 
     function fadeBindings() {
-        svg.selectAll('g')
+        svg.selectAll('g.keyBindings')
             .transition()
             .duration(fade)
             .attr('opacity', 0);
@@ -178,7 +201,8 @@
         if (svg.empty()) {
             addSvg();
             populateBindings(bindings);
-            console.log("SHOW KEY MAP");
+        } else {
+            hideKeyMap();
         }
     }
 
@@ -187,7 +211,6 @@
         if (!svg.empty()) {
             fadeBindings();
             removeSvg();
-            console.log("HIDE KEY MAP");
             return true;
         }
         return false;
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index ae7119e..d2a7baa 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -413,9 +413,9 @@
 
         function setupGlobalKeys() {
             keyHandler.globalKeys = {
-                slash: keyMap,
-                esc: escapeKey,
-                T: toggleTheme
+                slash: [keyMap, 'Show / hide keyboard shortcuts'],
+                esc: [escapeKey, 'Dismiss dialog or cancel selections'],
+                T: [toggleTheme, "Toggle theme"]
             };
             // Masked keys are global key handlers that always return true.
             // That is, the view will never see the event for that key.
@@ -476,7 +476,8 @@
             var event = d3.event,
                 keyCode = event.keyCode,
                 key = whatKey(keyCode),
-                gcb = isF(keyHandler.globalKeys[key]),
+                gk = keyHandler.globalKeys[key],
+                gcb = isF(gk) || (isA(gk) && isF(gk[0])),
                 vk = keyHandler.viewKeys[key],
                 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(keyHandler.viewFn);
 
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 696c92a..c32d508 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -144,12 +144,12 @@
         B: [toggleBg, 'Toggle background image'],
         L: [cycleLabels, 'Cycle Device labels'],
         P: togglePorts,
-        U: unpin,
-        R: resetZoomPan,
-        H: toggleHover,
-        V: showTrafficAction,
-        A: showAllTrafficAction,
-        F: showDeviceLinkFlowsAction,
+        U: [unpin, 'Unpin node'],
+        R: [resetZoomPan, 'Reset zoom/pan'],
+        H: [cycleHoverMode, 'Cycle hover mode'],
+        V: [showTrafficAction, 'Show traffic'],
+        A: [showAllTrafficAction, 'Show all traffic'],
+        F: [showDeviceLinkFlowsAction, 'Show device link flows'],
         esc: handleEscape
     };
 
@@ -322,7 +322,7 @@
         });
     }
 
-    function toggleHover(view) {
+    function cycleHoverMode(view) {
         hoverMode++;
         if (hoverMode === hoverModes.length) {
             hoverMode = 0;