GUI - Rebuilt Quick Help layout to allow multi-column key binding section.

Change-Id: Icada50c695ce60c8cbedb38d86434a842d935e77
diff --git a/web/gui/src/main/webapp/onos.js b/web/gui/src/main/webapp/onos.js
index 89768db..803607c 100644
--- a/web/gui/src/main/webapp/onos.js
+++ b/web/gui/src/main/webapp/onos.js
@@ -417,19 +417,23 @@
         }
 
         function setupGlobalKeys() {
-            keyHandler.globalKeys = {
-                slash: [quickHelp, 'Show / hide Quick Help'],
-                backSlash: [quickHelp, 'Show / hide Quick Help'],
-                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.
-            keyHandler.maskedKeys = {
-                slash: true,
-                backSlash: true,
-                T: true
-            };
+            $.extend(keyHandler, {
+                globalKeys: {
+                    backSlash: [quickHelp, 'Show / hide Quick Help'],
+                    slash: [quickHelp, 'Show / hide Quick Help'],
+                    esc: [escapeKey, 'Dismiss dialog or cancel selections'],
+                    T: [toggleTheme, "Toggle theme"]
+                },
+                globalFormat: ['backSlash', 'slash', 'esc', 'T'],
+
+                // Masked keys are global key handlers that always return true.
+                // That is, the view will never see the event for that key.
+                maskedKeys: {
+                    slash: true,
+                    backSlash: true,
+                    T: true
+                }
+            });
         }
 
         function quickHelp(view, key, code, ev) {
diff --git a/web/gui/src/main/webapp/onosQuickHelp.css b/web/gui/src/main/webapp/onosQuickHelp.css
index eb400cf..3f985e0 100644
--- a/web/gui/src/main/webapp/onosQuickHelp.css
+++ b/web/gui/src/main/webapp/onosQuickHelp.css
@@ -26,7 +26,7 @@
 
 #quickhelp svg {
     position: absolute;
-    bottom: 40px;
+    top: 180px;
     opacity: 1;
 }
 
@@ -36,7 +36,7 @@
 }
 
 #quickhelp svg text.title {
-    font-size: 8pt;
+    font-size: 10pt;
     font-style: italic;
     text-anchor: middle;
     fill: #999;
@@ -46,23 +46,21 @@
     fill: white;
 }
 
-#quickhelp svg g.keyItem line {
+#quickhelp svg g line.qhrowsep {
     stroke: #888;
     stroke-dasharray: 2 2;
 }
 
 #quickhelp svg text {
-    font-size: 5pt;
+    font-size: 7pt;
     alignment-baseline: middle;
 }
 
 #quickhelp svg text.key {
-    font-size: 5pt;
     fill: #add;
 }
 
 #quickhelp svg text.desc {
-    font-size: 5pt;
     fill: #ddd;
 }
 
diff --git a/web/gui/src/main/webapp/onosQuickHelp.js b/web/gui/src/main/webapp/onosQuickHelp.js
index 28c488b..a0da6aa 100644
--- a/web/gui/src/main/webapp/onosQuickHelp.js
+++ b/web/gui/src/main/webapp/onosQuickHelp.js
@@ -26,29 +26,35 @@
 (function (onos){
     'use strict';
 
-    // API's
-    var api = onos.api;
-
     // Config variables
     var w = '100%',
         h = '80%',
         fade = 500,
         vb = '-200 0 400 400';
 
+    // layout configuration
+    var pad = 10,
+        offy = 45,
+        sepYDelta = 20,
+        colXDelta = 16,
+        yTextSpc = 12,
+        offDesc = 8;
+
     // State variables
-    var data = [];
+    var data = [],
+        yCount;
 
     // DOM elements and the like
     var qhdiv = d3.select('#quickhelp'),
         svg = qhdiv.select('svg'),
-        pane,
-        rect,
-        items,
-        keyAgg;
+        pane, rect, items;
 
     // General functions
-    function isA(a) {
-        return $.isArray(a) ? a : null;
+    function isA(a) { return $.isArray(a) ? a : null; }
+    function isS(s) { return typeof s === 'string'; }
+
+    function cap(s) {
+        return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
     }
 
     var keyDisp = {
@@ -63,144 +69,212 @@
         downArrow: 'D-arrow'
     };
 
-    function cap(s) {
-        return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
-    }
-
     function mkKeyDisp(id) {
         var v = keyDisp[id] || id;
         return cap(v);
     }
 
-    // layout configuration
-    var pad = 8,
-        offy = 45,
-        dy = 10,
-        offDesc = 8;
+    function addSeparator(el, i) {
+        var y = sepYDelta/2 - 5;
+        el.append('line')
+            .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
+    }
 
-    // D3 magic
-    function updateKeyItems() {
-        var keyItems = items.selectAll('.keyItem')
-            .data(data);
+    function addContent(el, data, ri) {
+        var xCount = 0,
+            clsPfx = 'qh-r' + ri + '-c';
 
-        var entering = keyItems.enter()
-            .append('g')
-            .attr({
-                id: function (d) { return d.id; },
-                class: 'keyItem'
+        function addColumn(el, c, i) {
+            var cls = clsPfx + i,
+                oy = 0,
+                aggKey = el.append('g').attr('visibility', 'hidden'),
+                gcol = el.append('g').attr({
+                   'class': cls,
+                    transform: translate(xCount, 0)
+                });
+
+            c.forEach(function (j) {
+                var k = j[0],
+                    v = j[1];
+
+                if (k !== '-') {
+                    aggKey.append('text').text(k);
+
+                    gcol.append('text').text(k)
+                        .attr({
+                            'class': 'key',
+                            y: oy
+                        });
+                    gcol.append('text').text(v)
+                        .attr({
+                            'class': 'desc',
+                            y: oy
+                        });
+                }
+
+                oy += yTextSpc;
             });
 
-        entering.each(function (d, i) {
+            // adjust position of descriptions, based on widest key
+            var kbox = aggKey.node().getBBox(),
+                ox = kbox.width + offDesc;
+            gcol.selectAll('.desc').attr('x', ox);
+            aggKey.remove();
+
+            // now update x-offset for next column
+            var bbox = gcol.node().getBBox();
+            xCount += bbox.width + colXDelta;
+        }
+
+        data.forEach(function (d, i) {
+            addColumn(el, d, i);
+        });
+
+        // finally, return the height of the row..
+        return el.node().getBBox().height;
+    }
+
+    function updateKeyItems() {
+        var rows = items.selectAll('.qhRow').data(data);
+
+        yCount = offy;
+
+        var entering = rows.enter()
+            .append('g')
+            .attr({
+                'class': 'qhrow'
+            });
+
+        entering.each(function (r, i) {
             var el = d3.select(this),
-                y = offy + dy * i;
+                sep = r.type === 'sep',
+                dy;
 
-            if (d.id[0] === '_') {
-                el.append('line')
-                    .attr({ x1: 0, y1: y, x2: 1, y2: y});
+            el.attr('transform', translate(0, yCount));
+
+            if (sep) {
+                addSeparator(el, i);
+                yCount += sepYDelta;
             } else {
-                el.append('text')
-                    .text(d.key)
-                    .attr({
-                        class: 'key',
-                        x: 0,
-                        y: y
-                    });
-                // NOTE: used for sizing column width...
-                keyAgg.append('text').text(d.key).attr('class', 'key');
-
-                el.append('text')
-                    .text(d.desc)
-                    .attr({
-                        class: 'desc',
-                        x: offDesc,
-                        y: y
-                    });
+                dy = addContent(el, r.data, i);
+                yCount += dy;
             }
         });
 
-        var kbox = keyAgg.node().getBBox();
-        items.selectAll('.desc').attr('x', kbox.width + offDesc);
+        // size the backing rectangle
+        var ibox = items.node().getBBox(),
+            paneW = ibox.width + pad * 2,
+            paneH = ibox.height + offy;
 
-        var box = items.node().getBBox(),
-            paneW = box.width + pad * 2,
-            paneH = box.height + offy;
-
-        items.selectAll('line').attr('x2', box.width);
+        items.selectAll('.qhrowsep').attr('x2', ibox.width);
         items.attr('transform', translate(-paneW/2, -pad));
         rect.attr({
             width: paneW,
             height: paneH,
             transform: translate(-paneW/2-pad, 0)
         });
+
     }
 
     function translate(x, y) {
         return 'translate(' + x + ',' + y + ')';
     }
 
+    function checkFmt(fmt) {
+        // should be a single array of keys,
+        // or array of arrays of keys (one per column).
+        // return null if there is a problem.
+        var a = isA(fmt),
+            n = a && a.length,
+            ns = 0,
+            na = 0;
+
+        if (n) {
+            // it is an array which has some content
+            a.forEach(function (d) {
+                isA(d) && na++;
+                isS(d) && ns++;
+            });
+            if (na === n || ns === n) {
+                // all arrays or all strings...
+                return a;
+            }
+        }
+        return null;
+    }
+
+    function buildBlock(map, fmt) {
+        var b = [];
+        fmt.forEach(function (k) {
+            var v = map.get(k),
+                a = isA(v),
+                d = (a && a[1]);
+
+            // '-' marks a separator; d is the description
+            if (k === '-' || d) {
+                b.push([mkKeyDisp(k), d]);
+            }
+        });
+        return b;
+    }
+
+    function emptyRow() {
+        return { type: 'row', data: []};
+    }
+
+    function mkArrRow(fmt) {
+        var d = emptyRow();
+        d.data.push(fmt);
+        return d;
+    }
+
+    function mkColumnarRow(map, fmt) {
+        var d = emptyRow();
+        fmt.forEach(function (a) {
+            d.data.push(buildBlock(map, a));
+        });
+        return d;
+    }
+
+    function mkMapRow(map, fmt) {
+        var d = emptyRow();
+        d.data.push(buildBlock(map, fmt));
+        return d;
+    }
+
+    function addRow(row) {
+        var d = row || { type: 'sep' };
+        data.push(d);
+    }
+
     function aggregateData(bindings) {
         var hf = '_helpFormat',
             gmap = d3.map(bindings.globalKeys),
+            gfmt = bindings.globalFormat,
             vmap = d3.map(bindings.viewKeys),
-            fmt = vmap.get(hf),
             vgest = bindings.viewGestures,
-            gkeys = gmap.keys(),
-            vkeys,
-            sep = 0;
+            vfmt, vkeys;
 
         // filter out help format entry
+        vfmt = checkFmt(vmap.get(hf));
         vmap.remove(hf);
-        vkeys = vmap.keys(),
 
-        gkeys.sort();
-        vkeys.sort();
+        // if bad (or no) format, fallback to sorted keys
+        if (!vfmt) {
+            vkeys = vmap.keys();
+            vfmt = vkeys.sort();
+        }
 
         data = [];
-        gkeys.forEach(function (k) {
-            addItem('glob', k, gmap.get(k));
-        });
-        addItem('sep');
-        vkeys.forEach(function (k) {
-            addItem('view', k, vmap.get(k));
-        });
-        addItem('sep');
-        vgest.forEach(function (g) {
-            if (g.length === 2) {
-                addItem('gest', g[0], g[1]);
-            }
-        });
 
-
-        function addItem(type, k, d) {
-            var id = type + '-' + k,
-                a = isA(d),
-                desc = a && a[1];
-
-            if (type === 'sep') {
-                data.push({
-                    id: '_' + sep++,
-                    type: type
-                });
-            } else if (type === 'gest') {
-                data.push({
-                    id: id,
-                    type: type,
-                    key: k,
-                    desc: d
-                });
-            } else if (desc) {
-                data.push(
-                    {
-                        id: id,
-                        type: type,
-                        key: mkKeyDisp(k),
-                        desc: desc
-                    }
-                );
-            }
-        }
+        addRow(mkMapRow(gmap, gfmt));
+        addRow();
+        addRow(isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
+        addRow();
+        addRow(mkArrRow(vgest));
     }
 
+
     function popBind(bindings) {
         pane = svg.append('g')
             .attr({
@@ -220,8 +294,6 @@
             });
 
         items = pane.append('g');
-        keyAgg = pane.append('g').style('visibility', 'hidden');
-
         aggregateData(bindings);
         updateKeyItems();