GUI -- TopoView - re-implemented Quick Help panel.

Change-Id: I92edeb570a97eff87a5f9b08373ff0517849bf24
diff --git a/web/gui/src/main/webapp/app/fw/layer/quickhelp.css b/web/gui/src/main/webapp/app/fw/layer/quickhelp.css
new file mode 100644
index 0000000..bb806d8
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/quickhelp.css
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014,2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Quick Help Service -- CSS file
+ */
+
+#quickhelp {
+    z-index: 1300;
+}
+
+#quickhelp svg {
+    position: absolute;
+    top: 180px;
+    opacity: 1;
+}
+
+#quickhelp svg g.help rect {
+    fill: black;
+    opacity: 0.7;
+}
+
+#quickhelp svg text.title {
+    font-size: 10pt;
+    font-style: italic;
+    text-anchor: middle;
+    fill: #999;
+}
+
+#quickhelp svg g.keyItem {
+    fill: white;
+}
+
+#quickhelp svg g line.qhrowsep {
+    stroke: #888;
+    stroke-dasharray: 2 2;
+}
+
+#quickhelp svg text {
+    font-size: 7pt;
+    alignment-baseline: middle;
+}
+
+#quickhelp svg text.key {
+    fill: #add;
+}
+
+#quickhelp svg text.desc {
+    fill: #ddd;
+}
+
diff --git a/web/gui/src/main/webapp/app/fw/layer/quickhelp.js b/web/gui/src/main/webapp/app/fw/layer/quickhelp.js
new file mode 100644
index 0000000..8d533c7
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/quickhelp.js
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Layer -- Quick Help Service
+
+ Provides a mechanism to display key bindings and mouse gesture notes.
+ */
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, fs, sus;
+
+    // configuration
+    var defaultSettings = {
+            fade: 500
+        },
+        w = '100%',
+        h = '80%',
+        vbox = '-200 0 400 400',
+        pad = 10,
+        offy = 45,
+        sepYDelta = 20,
+        colXDelta = 16,
+        yTextSpc = 12,
+        offDesc = 8;
+
+    // internal state
+    var settings,
+        data = [],
+        yCount;
+
+    // DOM elements
+    var qhdiv, svg, pane, rect, items;
+
+    // key-logical-name to key-display lookup..
+    var keyDisp = {
+        equals: '=',
+        dash: '-',
+        slash: '/',
+        backSlash: '\\',
+        backQuote: '`',
+        leftArrow: 'L-arrow',
+        upArrow: 'U-arrow',
+        rightArrow: 'R-arrow',
+        downArrow: 'D-arrow'
+    };
+
+    // ===========================================
+    // === Function Definitions ===
+
+
+    // TODO: move this to FnService.
+    function cap(s) {
+        return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
+    }
+
+
+    function mkKeyDisp(id) {
+        var v = keyDisp[id] || id;
+        return cap(v);
+    }
+
+    function addSeparator(el, i) {
+        var y = sepYDelta/2 - 5;
+        el.append('line')
+            .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
+    }
+
+    function addContent(el, data, ri) {
+        var xCount = 0,
+            clsPfx = 'qh-r' + ri + '-c';
+
+        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: sus.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;
+            });
+
+            // 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),
+                sep = r.type === 'sep',
+                dy;
+
+            el.attr('transform', sus.translate(0, yCount));
+
+            if (sep) {
+                addSeparator(el, i);
+                yCount += sepYDelta;
+            } else {
+                dy = addContent(el, r.data, i);
+                yCount += dy;
+            }
+        });
+
+        // size the backing rectangle
+        var ibox = items.node().getBBox(),
+            paneW = ibox.width + pad * 2,
+            paneH = ibox.height + offy;
+
+        items.selectAll('.qhrowsep').attr('x2', ibox.width);
+        items.attr('transform', sus.translate(-paneW/2, -pad));
+        rect.attr({
+            width: paneW,
+            height: paneH,
+            transform: sus.translate(-paneW/2-pad, 0)
+        });
+
+    }
+
+    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 = fs.isA(fmt),
+            n = a && a.length,
+            ns = 0,
+            na = 0;
+
+        if (n) {
+            // it is an array which has some content
+            a.forEach(function (d) {
+                fs.isA(d) && na++;
+                fs.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 = fs.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),
+            vgest = bindings.viewGestures,
+            vfmt, vkeys;
+
+        // filter out help format entry
+        vfmt = checkFmt(vmap.get(hf));
+        vmap.remove(hf);
+
+        // if bad (or no) format, fallback to sorted keys
+        if (!vfmt) {
+            vkeys = vmap.keys();
+            vfmt = vkeys.sort();
+        }
+
+        data = [];
+
+        addRow(mkMapRow(gmap, gfmt));
+        addRow();
+        addRow(fs.isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
+        addRow();
+        addRow(mkArrRow(vgest));
+    }
+
+
+    function popBind(bindings) {
+        pane = svg.append('g')
+            .attr({
+                class: 'help',
+                opacity: 0
+            });
+
+        rect = pane.append('rect')
+            .attr('rx', 8);
+
+        pane.append('text')
+            .text('Quick Help')
+            .attr({
+                class: 'title',
+                dy: '1.2em',
+                transform: sus.translate(-pad,0)
+            });
+
+        items = pane.append('g');
+        aggregateData(bindings);
+        updateKeyItems();
+
+        _fade(1);
+    }
+
+    function fadeBindings() {
+        _fade(0);
+    }
+
+    function _fade(o) {
+        svg.selectAll('g.help')
+            .transition()
+            .duration(settings.fade)
+            .attr('opacity', o);
+    }
+
+    function addSvg() {
+        svg = qhdiv.append('svg')
+            .attr({
+                width: w,
+                height: h,
+                viewBox: vbox
+            });
+    }
+
+    function removeSvg() {
+        svg.transition()
+            .delay(settings.fade + 20)
+            .remove();
+    }
+
+
+    // ===========================================
+    // === Module Definition ===
+
+    angular.module('onosLayer')
+    .factory('QuickHelpService',
+        ['$log', 'FnService', 'SvgUtilService',
+
+        function (_$log_, _fs_, _sus_) {
+            $log = _$log_;
+            fs = _fs_;
+            sus = _sus_;
+
+            function initQuickHelp(opts) {
+                settings = angular.extend({}, defaultSettings, opts);
+                qhdiv = d3.select('#quickhelp');
+            }
+
+            function showQuickHelp(bindings) {
+                svg = qhdiv.select('svg');
+                if (svg.empty()) {
+                    addSvg();
+                    popBind(bindings);
+                } else {
+                    hideQuickHelp();
+                }
+            }
+
+            function hideQuickHelp() {
+                svg = qhdiv.select('svg');
+                if (!svg.empty()) {
+                    fadeBindings();
+                    removeSvg();
+                    return true;
+                }
+                return false;
+            }
+
+            return {
+                initQuickHelp: initQuickHelp,
+                showQuickHelp: showQuickHelp,
+                hideQuickHelp: hideQuickHelp
+            };
+        }]);
+
+}());
diff --git a/web/gui/src/main/webapp/app/fw/util/keys.js b/web/gui/src/main/webapp/app/fw/util/keys.js
index d451885..1cdb044 100644
--- a/web/gui/src/main/webapp/app/fw/util/keys.js
+++ b/web/gui/src/main/webapp/app/fw/util/keys.js
@@ -21,7 +21,7 @@
     'use strict';
 
     // references to injected services
-    var $log, fs, ts;
+    var $log, fs, ts, qhs;
 
     // internal state
     var enabled = true,
@@ -115,22 +115,13 @@
     }
 
     function quickHelp(view, key, code, ev) {
-        // TODO: show quick help
-        // delegate to QuickHelp service.
-        //libApi.quickHelp.show(keyHandler);
-        console.log('QUICK-HELP');
+        qhs.showQuickHelp(keyHandler);
         return true;
     }
 
     // returns true if we 'consumed' the ESC keypress, false otherwise
     function escapeKey(view, key, code, ev) {
-        // TODO: plumb in handling of quick help dismissal
-/*
-        if (qh.hide()) {
-            return true;
-        }
-*/
-        return false;
+        return qhs.hideQuickHelp();
     }
 
     function toggleTheme(view, key, code, ev) {
@@ -176,13 +167,18 @@
     }
 
     angular.module('onosUtil')
-        .factory('KeyService', ['$log', 'FnService', 'ThemeService',
+    .factory('KeyService',
+        ['$log', 'FnService', 'ThemeService',
+
         function (_$log_, _fs_, _ts_) {
             $log = _$log_;
             fs = _fs_;
             ts = _ts_;
 
             return {
+                bindQhs: function (_qhs_) {
+                    qhs = _qhs_;
+                },
                 installOn: function (elem) {
                     elem.on('keydown', keyIn);
                     setupGlobalKeys();
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index dc8b51c..79bbd45 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -63,6 +63,7 @@
     <script src="fw/layer/layer.js"></script>
     <script src="fw/layer/panel.js"></script>
     <script src="fw/layer/flash.js"></script>
+    <script src="fw/layer/quickhelp.js"></script>
     <script src="fw/layer/veil.js"></script>
 
     <!-- Framework and library stylesheets included here -->
@@ -74,6 +75,7 @@
     <link rel="stylesheet" href="fw/svg/icon.css">
     <link rel="stylesheet" href="fw/layer/panel.css">
     <link rel="stylesheet" href="fw/layer/flash.css">
+    <link rel="stylesheet" href="fw/layer/quickhelp.css">
     <link rel="stylesheet" href="fw/layer/veil.css">
     <link rel="stylesheet" href="fw/nav/nav.css">
 
diff --git a/web/gui/src/main/webapp/app/onos.js b/web/gui/src/main/webapp/app/onos.js
index 43ce845..55c7b05 100644
--- a/web/gui/src/main/webapp/app/onos.js
+++ b/web/gui/src/main/webapp/app/onos.js
@@ -65,9 +65,10 @@
         .controller('OnosCtrl', [
             '$log', '$route', '$routeParams', '$location',
             'KeyService', 'ThemeService', 'GlyphService', 'PanelService',
-            'FlashService',
+            'FlashService', 'QuickHelpService',
 
-        function ($log, $route, $routeParams, $location, ks, ts, gs, ps, flash) {
+        function ($log, $route, $routeParams, $location,
+                  ks, ts, gs, ps, flash, qhs) {
             var self = this;
 
             self.$route = $route;
@@ -78,9 +79,11 @@
             // initialize services...
             ts.init();
             ks.installOn(d3.select('body'));
+            ks.bindQhs(qhs);
             gs.init();
             ps.init();
             flash.initFlash();
+            qhs.initQuickHelp();
 
             $log.log('OnosCtrl has been created');
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index ca24d8a..44856f3 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -75,14 +75,13 @@
             ]
         });
 
-        // TODO:         // mouse gestures
-        var gestures = [
+        ks.gestureNotes([
             ['click', 'Select the item and show details'],
             ['shift-click', 'Toggle selection state'],
             ['drag', 'Reposition (and pin) device / host'],
             ['cmd-scroll', 'Zoom in / out'],
             ['cmd-drag', 'Pan']
-        ];
+        ]);
     }
 
     // --- Keystroke functions -------------------------------------------
diff --git a/web/gui/src/main/webapp/tests/app/fw/layer/quickhelp-spec.js b/web/gui/src/main/webapp/tests/app/fw/layer/quickhelp-spec.js
new file mode 100644
index 0000000..acbc4a5
--- /dev/null
+++ b/web/gui/src/main/webapp/tests/app/fw/layer/quickhelp-spec.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Layer -- Flash Service - Unit Tests
+ */
+describe('factory: fw/layer/quickhelp.js', function () {
+    var $log, $timeout, fs, qhs, d3Elem;
+
+    beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
+
+    beforeEach(inject(function (_$log_, _$timeout_, FnService, QuickHelpService) {
+        $log = _$log_;
+        //$timeout = _$timeout_;
+        fs = FnService;
+        qhs = QuickHelpService;
+        //jasmine.clock().install();
+        d3Elem = d3.select('body').append('div').attr('id', 'myqhdiv');
+    }));
+
+    afterEach(function () {
+        //jasmine.clock().uninstall();
+        d3.select('#myqhdiv').remove();
+    });
+
+    function helpItemSelection() {
+        return d3Elem.selectAll('.help');
+    }
+
+    it('should define QuickHelpService', function () {
+        expect(qhs).toBeDefined();
+    });
+
+    it('should define api functions', function () {
+        expect(fs.areFunctions(qhs, [
+            'initQuickHelp', 'showQuickHelp', 'hideQuickHelp'
+        ])).toBeTruthy();
+    });
+
+    it('should have no items to start', function () {
+        expect(helpItemSelection().size()).toBe(0);
+    });
+
+    // TODO: check that the help stuff appears
+/*
+    it('should show help items', function () {
+        var item, rect, text;
+        flash.flash('foo');
+        //jasmine.clock().tick(101);
+        setTimeout(function () {
+            item = flashItemSelection();
+            expect(item.size()).toEqual(1);
+            expect(item.classed('flashItem')).toBeTruthy();
+            expect(item.select('rect').size()).toEqual(1);
+            text = item.select('text');
+            expect(text.size()).toEqual(1);
+            expect(text.text()).toEqual('foo');
+        }, 100);
+    });
+*/
+});
+
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/keys-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/keys-spec.js
index 1873e4f..b453964 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/keys-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/keys-spec.js
@@ -18,19 +18,21 @@
  ONOS GUI -- Key Handler Service - Unit Tests
  */
 describe('factory: fw/util/keys.js', function() {
-    var $log, ks, fs,
+    var $log, ks, fs, qhs,
         d3Elem, elem, last;
   
 
-    beforeEach(module('onosUtil'));
+    beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
 
-    beforeEach(inject(function (_$log_, KeyService, FnService) {
+    beforeEach(inject(function (_$log_, KeyService, FnService, QuickHelpService) {
         $log = _$log_;
         ks = KeyService;
         fs = FnService;
+        qhs = QuickHelpService;
         d3Elem = d3.select('body').append('p').attr('id', 'ptest');
         elem = d3Elem.node();
         ks.installOn(d3Elem);
+        ks.bindQhs(qhs);
         last = {
             view: null,
             key: null,
@@ -49,7 +51,7 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(ks, [
-            'installOn', 'keyBindings', 'gestureNotes', 'enableKeys'
+            'bindQhs', 'installOn', 'keyBindings', 'gestureNotes', 'enableKeys'
         ])).toBeTruthy();
     });