GUI -- implemented Theme Service.
 - moved body class handling into Theme Service, (out of Key Service).

Change-Id: Ied0d22523fec36cadef8b9669194089585f73959
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 5cae514..ad8ede5 100644
--- a/web/gui/src/main/webapp/app/fw/util/keys.js
+++ b/web/gui/src/main/webapp/app/fw/util/keys.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // references to injected services
-    var $log, f;
+    var $log, fs, ts;
 
     // internal state
     var keyHandler = {
@@ -32,8 +32,7 @@
             viewKeys: {},
             viewFn: null,
             viewGestures: []
-        },
-        theme = 'light';
+        };
 
     // TODO: we need to have the concept of view token here..
     function getViewToken() {
@@ -76,9 +75,9 @@
             key = whatKey(keyCode),
             kh = keyHandler,
             gk = kh.globalKeys[key],
-            gcb = f.isF(gk) || (f.isA(gk) && f.isF(gk[0])),
+            gcb = fs.isF(gk) || (fs.isA(gk) && fs.isF(gk[0])),
             vk = kh.viewKeys[key],
-            vcb = f.isF(vk) || (f.isA(vk) && f.isF(vk[0])) || f.isF(kh.viewFn),
+            vcb = fs.isF(vk) || (fs.isA(vk) && fs.isF(vk[0])) || fs.isF(kh.viewFn),
             token = getViewToken();
 
         d3.event.stopPropagation();
@@ -137,12 +136,7 @@
     }
 
     function toggleTheme(view, key, code, ev) {
-        var body = d3.select('body');
-        theme = (theme === 'light') ? 'dark' : 'light';
-        body.classed('light dark', false);
-        body.classed(theme, true);
-        // TODO: emit theme-change event to current view...
-        //theme(view);
+        ts.toggleTheme();
         return true;
     }
 
@@ -150,7 +144,7 @@
         var viewKeys,
             masked = [];
 
-        if (f.isF(keyArg)) {
+        if (fs.isF(keyArg)) {
             // set general key handler callback
             keyHandler.viewFn = keyArg;
         } else {
@@ -173,7 +167,7 @@
         var gkeys = d3.map(keyHandler.globalKeys).keys(),
             masked = d3.map(keyHandler.maskedKeys).keys(),
             vkeys = d3.map(keyHandler.viewKeys).keys(),
-            vfn = !!f.isF(keyHandler.viewFn);
+            vfn = !!fs.isF(keyHandler.viewFn);
 
         return {
             globalKeys: gkeys,
@@ -184,17 +178,17 @@
     }
 
     angular.module('onosUtil')
-        .factory('KeyService', ['$log', 'FnService', function ($l, fs) {
-            $log = $l;
-            f = fs;
+        .factory('KeyService', ['$log', 'FnService', 'ThemeService',
+        function (_$log_, _fs_, _ts_) {
+            $log = _$log_;
+            fs = _fs_;
+            ts = _ts_;
+
             return {
                 installOn: function (elem) {
                     elem.on('keydown', keyIn);
                     setupGlobalKeys();
                 },
-                theme: function () {
-                    return theme;
-                },
                 keyBindings: function (x) {
                     if (x === undefined) {
                         return getKeyBindings();
@@ -206,7 +200,7 @@
                     if (g === undefined) {
                         return keyHandler.viewGestures;
                     } else {
-                        keyHandler.viewGestures = f.isA(g) || [];
+                        keyHandler.viewGestures = fs.isA(g) || [];
                     }
                 }
             };
diff --git a/web/gui/src/main/webapp/app/fw/util/theme.js b/web/gui/src/main/webapp/app/fw/util/theme.js
new file mode 100644
index 0000000..029d463
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/util/theme.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2014 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 -- Util -- Theme Service
+
+ @author Simon Hunt
+ */
+(function () {
+    'use strict';
+
+    var $log;
+
+    var themes = ['light', 'dark'],
+        themeStr = themes.join(' '),
+        thidx;
+
+    function init() {
+        thidx = 0;
+        updateBodyClass();
+    }
+
+    function getTheme() {
+        return themes[thidx];
+    }
+
+    function setTheme(t) {
+        var idx = themes.indexOf(t);
+        if (idx > -1 && idx !== thidx) {
+            thidx = idx;
+            updateBodyClass();
+            themeEvent('set');
+        }
+    }
+
+    function toggleTheme() {
+        var i = thidx + 1;
+        thidx = (i===themes.length) ? 0 : i;
+        updateBodyClass();
+        themeEvent('toggle');
+        return getTheme();
+    }
+
+    function updateBodyClass() {
+        var body = d3.select('body');
+        body.classed(themeStr, false);
+        body.classed(getTheme(), true);
+    }
+
+    function themeEvent(w) {
+        // TODO: emit a theme-changed event
+        var m = 'Theme-Change-('+w+'): ' + getTheme();
+        $log.debug(m);
+    }
+
+    // TODO: add hook for theme-change listener
+
+    angular.module('onosUtil')
+        .factory('ThemeService', ['$log', function (_$log_) {
+            $log = _$log_;
+            thidx = 0;
+
+            return {
+                init: init,
+                theme: function (x) {
+                    if (x === undefined) {
+                        return getTheme();
+                    } else {
+                        setTheme(x);
+                    }
+                },
+                toggleTheme: toggleTheme
+            };
+    }]);
+
+}());
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index 14a029f..7699630 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -35,6 +35,7 @@
 
     <script src="fw/util/util.js"></script>
     <script src="fw/util/fn.js"></script>
+    <script src="fw/util/theme.js"></script>
     <script src="fw/util/keys.js"></script>
 
     <script src="fw/mast/mast.js"></script>
diff --git a/web/gui/src/main/webapp/app/onos.js b/web/gui/src/main/webapp/app/onos.js
index 1fece59..73af667 100644
--- a/web/gui/src/main/webapp/app/onos.js
+++ b/web/gui/src/main/webapp/app/onos.js
@@ -24,13 +24,15 @@
     'use strict';
 
     angular.module('onosApp', ['onosUtil', 'onosMast'])
-        .controller('OnosCtrl', ['$log', 'KeyService', function (_$log_, ks) {
+        .controller('OnosCtrl', ['$log', 'KeyService', 'ThemeService',
+        function (_$log_, ks, ts) {
             var $log = _$log_,
                 self = this;
 
             self.version = '1.1.0';
 
             // initialize onos (main app) controller here...
+            ts.init();
             ks.installOn(d3.select('body'));
 
             $log.log('OnosCtrl has been created');
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 6cf41a6..b2bc134 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -19,7 +19,7 @@
 
  @author Simon Hunt
  */
-describe('factory: fw/lib/fn.js', function() {
+describe('factory: fw/util/fn.js', function() {
     var fs,
         someFunction = function () {},
         someArray = [1, 2, 3],
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 a37a84b..79d0caa 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
@@ -19,7 +19,7 @@
 
  @author Simon Hunt
  */
-describe('factory: fw/lib/keys.js', function() {
+describe('factory: fw/util/keys.js', function() {
     var $log, ks, fs,
         d3Elem, elem, last;
   
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js
new file mode 100644
index 0000000..e88807c
--- /dev/null
+++ b/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2014 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 -- Util -- Theme Service - Unit Tests
+
+ @author Simon Hunt
+ */
+describe('factory: fw/util/theme.js', function() {
+    var ts, $log;
+
+    beforeEach(module('onosUtil'));
+
+    beforeEach(inject(function (ThemeService, _$log_) {
+        ts = ThemeService;
+        $log = _$log_;
+        ts.init();
+    }));
+
+    function verifyBodyClass(yes, no) {
+        function bodyHasClass(c) {
+            return d3.select('body').classed(c);
+        }
+        expect(bodyHasClass(yes)).toBeTruthy();
+        expect(bodyHasClass(no)).toBeFalsy();
+    }
+
+    it('should default to light theme', function () {
+        expect(ts.theme()).toEqual('light');
+    });
+
+    it('should toggle to dark, then to light again', function () {
+        // Note: re-work this once theme-change listeners are implemented
+        spyOn($log, 'debug');
+
+        expect(ts.toggleTheme()).toEqual('dark');
+        expect(ts.theme()).toEqual('dark');
+        expect($log.debug).toHaveBeenCalledWith('Theme-Change-(toggle): dark');
+        verifyBodyClass('dark', 'light');
+
+        expect(ts.toggleTheme()).toEqual('light');
+        expect(ts.theme()).toEqual('light');
+        expect($log.debug).toHaveBeenCalledWith('Theme-Change-(toggle): light');
+        verifyBodyClass('light', 'dark');
+    });
+
+    it('should let us set the theme by name', function () {
+        // Note: re-work this once theme-change listeners are implemented
+        spyOn($log, 'debug');
+
+        expect(ts.theme()).toEqual('light');
+        ts.theme('dark');
+        expect(ts.theme()).toEqual('dark');
+        expect($log.debug).toHaveBeenCalledWith('Theme-Change-(set): dark');
+        verifyBodyClass('dark', 'light');
+    });
+
+    it('should ignore unknown theme names', function () {
+        // Note: re-work this once theme-change listeners are implemented
+        spyOn($log, 'debug');
+
+        expect(ts.theme()).toEqual('light');
+        verifyBodyClass('light', 'dark');
+
+        ts.theme('turquoise');
+        expect(ts.theme()).toEqual('light');
+        expect($log.debug).not.toHaveBeenCalled();
+        verifyBodyClass('light', 'dark');
+    });
+});