GUI -- Added global key bindings mechanism
 - 'T' now toggles theme (light/dark) -- mast CSS done, other CSS to do
 - 'ESC' now closes alerts box if it is open
 - Minor cleanup to topo2.js

Change-Id: I506a6add4299a6c03dcb717c33394ad94be26997
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 235c1d7..6168271 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -72,9 +72,9 @@
     <!-- Initialize the UI...-->
     <script type="text/javascript">
         var ONOS = $.onos({
-            comment: "configuration options",
+            comment: 'configuration options',
+            theme: 'light',
             startVid: 'topo',
-//            startVid: 'sampleKeys',
             trace: false
         });
     </script>
diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css
index 7f094b3..57fede4 100644
--- a/web/gui/src/main/webapp/mast2.css
+++ b/web/gui/src/main/webapp/mast2.css
@@ -23,8 +23,15 @@
 #mast {
     height: 36px;
     padding: 4px;
-    background-color: #bbb;
     vertical-align: baseline;
+}
+
+.light #mast {
+    background-color: #bbb;
+    box-shadow: 0px 2px 8px #777;
+}
+.dark #mast {
+    background-color: #444;
     box-shadow: 0px 2px 8px #777;
 }
 
@@ -35,12 +42,18 @@
 }
 
 #mast span.title {
-    color: #369;
     font-size: 14pt;
     font-style: italic;
     vertical-align: 12px;
 }
 
+.light #mast span.title {
+    color: #369;
+}
+.dark #mast span.title {
+    color: #78a;
+}
+
 #mast span.right {
     padding-top: 8px;
     padding-right: 16px;
@@ -50,16 +63,31 @@
 #mast span.radio {
     font-size: 10pt;
     margin: 4px 2px;
-    border: 1px dotted #222;
     padding: 1px 6px;
-    color: #eee;
     cursor: pointer;
 }
 
+.light #mast span.radio {
+    border: 1px dotted #222;
+    color: #eee;
+}
+.dark #mast span.radio {
+    border: 1px dotted #bbb;
+    color: #888;
+}
+
 #mast span.radio.active {
+    padding: 1px 6px;
+    font-weight: bold;
+}
+
+.light #mast span.radio.active {
     background-color: #bbb;
     border: 1px solid #eee;
-    padding: 1px 6px;
     color: #666;
-    font-weight: bold;
+}
+.dark #mast span.radio.active {
+    background-color: #222;
+    border: 1px solid #eee;
+    color: #aaf;
 }
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
index 748cc97..2073acf 100644
--- a/web/gui/src/main/webapp/onos2.css
+++ b/web/gui/src/main/webapp/onos2.css
@@ -32,7 +32,7 @@
     display: block;
 }
 
-div#alerts {
+#alerts {
     display: none;
     position: absolute;
     z-index: 2000;
@@ -45,21 +45,28 @@
     box-shadow: 4px 6px 12px #777;
 }
 
-div#alerts pre {
+#alerts pre {
     margin: 0.2em 6px;
 }
 
-div#alerts span.close {
+#alerts span.close {
     color: #6af;
     float: right;
     right: 2px;
     cursor: pointer;
 }
 
-div#alerts span.close:hover {
+#alerts span.close:hover {
     color: #fff;
 }
 
+#alerts p.footnote {
+    text-align: right;
+    font-size: 8pt;
+    margin: 8px 0 0 0;
+    color: #66d;
+}
+
 /*
  * ==============================================================
  * END OF NEW ONOS.JS file
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 31d89fa..4aeb23e 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -37,23 +37,34 @@
 
         var defaultOptions = {
             trace: false,
+            theme: 'light',
             startVid: defaultVid
         };
 
         // compute runtime settings
         var settings = $.extend({}, defaultOptions, options);
 
+        // set the selected theme
+        d3.select('body').classed(settings.theme, true);
+
         // internal state
         var views = {},
             current = {
                 view: null,
-                ctx: ''
+                ctx: '',
+                theme: settings.theme
             },
             built = false,
             errorCount = 0,
             keyHandler = {
-                fn: null,
-                map: {}
+                globalKeys: {},
+                maskedKeys: {},
+                viewKeys: {},
+                viewFn: null
+            },
+            alerts = {
+                open: false,
+                count: 0
             };
 
         // DOM elements etc.
@@ -240,8 +251,8 @@
 
                 // detach radio buttons, key handlers, etc.
                 $('#mastRadio').children().detach();
-                keyHandler.fn = null;
-                keyHandler.map = {};
+                keyHandler.viewKeys = {};
+                keyHandler.viewFn = null;
             }
 
             // cache new view and context
@@ -322,20 +333,74 @@
             $mastRadio.node().appendChild(btnG.node());
         }
 
+        function setupGlobalKeys() {
+            keyHandler.globalKeys = {
+                esc: escapeKey,
+                T: toggleTheme
+            };
+            // Masked keys are global key handlers that always return true.
+            // That is, the view will never see the event for that key.
+            keyHandler.maskedKeys = {
+                T: true
+            };
+        }
+
+        function escapeKey(view, key, code, ev) {
+            if (alerts.open) {
+                closeAlerts();
+                return true;
+            }
+            return false;
+        }
+
+        function toggleTheme(view, key, code, ev) {
+            var body = d3.select('body');
+            current.theme = (current.theme === 'light') ? 'dark' : 'light';
+            body.classed('light dark', false);
+            body.classed(current.theme, true);
+            return true;
+        }
+
         function setKeyBindings(keyArg) {
+            var viewKeys,
+                masked = [];
+
             if ($.isFunction(keyArg)) {
                 // set general key handler callback
-                keyHandler.fn = keyArg;
+                keyHandler.viewFn = keyArg;
             } else {
                 // set specific key filter map
-                keyHandler.map = keyArg;
+                viewKeys = d3.map(keyArg).keys();
+                viewKeys.forEach(function (key) {
+                    if (keyHandler.maskedKeys[key]) {
+                        masked.push('  Key "' + key + '" is reserved');
+                    }
+                });
+
+                if (masked.length) {
+                    doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
+                }
+                keyHandler.viewKeys = keyArg;
             }
         }
 
-        var alerts = {
-            open: false,
-            count: 0
-        };
+        function keyIn() {
+            var event = d3.event,
+                keyCode = event.keyCode,
+                key = whatKey(keyCode),
+                gcb = isF(keyHandler.globalKeys[key]),
+                vcb = isF(keyHandler.viewKeys[key]) || isF(keyHandler.viewFn);
+
+            // global callback?
+            if (gcb && gcb(current.view.token(), key, keyCode, event)) {
+                // if the event was 'handled', we are done
+                return;
+            }
+            // otherwise, let the view callback have a shot
+            if (vcb) {
+                vcb(current.view.token(), key, keyCode, event);
+            }
+        }
 
         function createAlerts() {
             var al = d3.select('#alerts')
@@ -345,15 +410,16 @@
                 .text('X')
                 .on('click', closeAlerts);
             al.append('pre');
+            al.append('p').attr('class', 'footnote')
+                .text('Press ESCAPE to close');
             alerts.open = true;
             alerts.count = 0;
         }
 
         function closeAlerts() {
             d3.select('#alerts')
-                .style('display', 'none');
-            d3.select('#alerts span').remove();
-            d3.select('#alerts pre').remove();
+                .style('display', 'none')
+                .html('');
             alerts.open = false;
         }
 
@@ -384,17 +450,6 @@
             addAlert(msg);
         }
 
-        function keyIn() {
-            var event = d3.event,
-                keyCode = event.keyCode,
-                key = whatKey(keyCode),
-                cb = isF(keyHandler.map[key]) || isF(keyHandler.fn);
-
-            if (cb) {
-                cb(current.view.token(), key, keyCode, event);
-            }
-        }
-
         function resize(e) {
             d3.selectAll('.onosView').call(setViewDimensions);
             // allow current view to react to resize event...
@@ -683,6 +738,7 @@
             $(window).on('resize', resize);
 
             d3.select('body').on('keydown', keyIn);
+            setupGlobalKeys();
 
             // Invoke hashchange callback to navigate to content
             // indicated by the window location hash.
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index fc7c35a..fff829f 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -117,7 +117,7 @@
         S: injectStartupEvents,     // TODO: remove (testing only)
         space: injectTestEvent,     // TODO: remove (testing only)
 
-        B: toggleBg,
+        B: toggleBg,                // TODO: do we really need this?
         L: cycleLabels,
         P: togglePorts,
         U: unpin,
@@ -135,7 +135,6 @@
         webSock,
         labelIdx = 0,
 
-        //selected = {},
         selectOrder = [],
         selections = {},
 
@@ -788,8 +787,9 @@
             if (d.class === 'device') {
                 d.fixed = true;
                 d3.select(self).classed('fixed', true);
-                tellServerCoords(d);
-                // TODO: send new [x,y] back to server, via websocket.
+                if (config.useLiveData) {
+                    tellServerCoords(d);
+                }
             }
         }