GUACAMOLE-630: Merge use singleton instance of Pickr for all color input fields.

diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
index 3b010cc..1762c31 100644
--- a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
+++ b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
@@ -18,11 +18,11 @@
  */
 
 /**
- * A directive which implements a color input field, leveraging the "Pickr"
- * color picker. If the "Picker" color picker cannot be used because it relies
- * on JavaScript features not supported by the browser (Internet Explorer), a
- * "guacInputColorUnavailable" event will be emitted up the scope, and this
+ * A directive which implements a color input field. If the underlying color
+ * picker implementation cannot be used due to a lack of browser support, this
  * directive will become read-only, functioning essentially as a color preview.
+ *
+ * @see colorPickerService
  */
 angular.module('form').directive('guacInputColor', [function guacInputColor() {
 
@@ -59,17 +59,12 @@
         function guacInputColorController($scope, $element, $injector) {
 
         // Required services
-        var $q         = $injector.get('$q');
-        var $translate = $injector.get('$translate');
+        var colorPickerService = $injector.get('colorPickerService');
 
         /**
-         * Whether the color picker ("Pickr") cannot be used. In general, all
-         * browsers should support Pickr with the exception of Internet
-         * Explorer.
-         *
-         * @type Boolean
+         * @borrows colorPickerService.isAvailable()
          */
-        $scope.colorPickerUnavailable = false;
+        $scope.isColorPickerAvailable = colorPickerService.isAvailable;
 
         /**
          * Returns whether the color currently selected is "dark" in the sense
@@ -102,98 +97,18 @@
 
         };
 
-        // Init color picker after required translation strings are available
-        $q.all({
-            'save'   : $translate('APP.ACTION_SAVE'),
-            'cancel' : $translate('APP.ACTION_CANCEL')
-        }).then(function stringsRetrieved(strings) {
-
-            try {
-
-                /**
-                 * An instance of the "Pickr" color picker, bound to the underlying
-                 * element of this directive.
-                 *
-                 * @type Pickr
-                 */
-                var pickr = Pickr.create({
-
-                    // Bind color picker to the underlying element of this directive
-                    el : $element[0],
-
-                    // Wrap color picker dialog in Guacamole-specific class for
-                    // sake of additional styling
-                    appClass : 'guac-input-color-picker',
-
-                    // Display color details as hex
-                    defaultRepresentation : 'HEX',
-
-                    // Use "monolith" theme, as a nice balance between "nano" (does
-                    // not work in Internet Explorer) and "classic" (too big)
-                    theme : 'monolith',
-
-                    // Leverage the container element as the button which shows the
-                    // picker, relying on our own styling for that button
-                    useAsButton  : true,
-                    appendToBody : true,
-
-                    // Do not include opacity controls
-                    lockOpacity : true,
-
-                    // Include a selection of palette entries for convenience and
-                    // reference
-                    swatches : $scope.palette || [],
-
-                    components: {
-
-                        // Include hue and color preview controls
-                        preview : true,
-                        hue     : true,
-
-                        // Display only a text color input field and the save and
-                        // cancel buttons (no clear button)
-                        interaction: {
-                            input  : true,
-                            save   : true,
-                            cancel : true
-                        }
-
-                    },
-
-                    // Use translation strings for buttons
-                    strings : strings
-
-                });
-
-                // Hide color picker after user clicks "cancel"
-                pickr.on('cancel', function colorChangeCanceled() {
-                    pickr.hide();
-                });
-
-                // Keep model in sync with changes to the color picker
-                pickr.on('save', function colorChanged(color) {
-                    $scope.$evalAsync(function updateModel() {
-                        $scope.model = color.toHEXA().toString();
-                    });
-                });
-
-                // Keep color picker in sync with changes to the model
-                pickr.on('init', function pickrReady(color) {
-                    $scope.$watch('model', function modelChanged(model) {
-                        pickr.setColor(model);
-                    });
-                });
-
-            }
-
-            // If the "Pickr" color picker cannot be loaded (Internet Explorer),
-            // let the scope above us know
-            catch (e) {
-                $scope.colorPickerUnavailable = true;
-                $scope.$emit('guacInputColorUnavailable', e);
-            }
-
-        }, angular.noop);
+        /**
+         * Prompts the user to choose a color by displaying a color selection
+         * dialog. If the user chooses a color, this directive's model is
+         * automatically updated. If the user cancels the dialog, the model is
+         * left untouched.
+         */
+        $scope.selectColor = function selectColor() {
+            colorPickerService.selectColor($element[0], $scope.model, $scope.palette)
+            .then(function colorSelected(color) {
+                $scope.model = color;
+            }, angular.noop);
+        };
 
     }];
 
diff --git a/guacamole/src/main/webapp/app/form/services/colorPickerService.js b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
new file mode 100644
index 0000000..cb9e63f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * A service for prompting the user to choose a color using the "Pickr" color
+ * picker. As the Pickr color picker might not be available if the JavaScript
+ * features it requires are not supported by the browser (Internet Explorer),
+ * the isAvailable() function should be used to test for usability.
+ */
+angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() {
+
+    /**
+     * A singleton instance of the "Pickr" color picker, shared by all users of
+     * this service. Pickr does not initialize synchronously, nor is it
+     * supported by all browsers. If Pickr is not yet initialized, or is
+     * unsupported, this will be null.
+     *
+     * @type {Pickr}
+     */
+    var pickr = null;
+
+    /**
+     * Whether Pickr has completed initialization.
+     *
+     * @type {Boolean}
+     */
+    var pickrInitComplete = false;
+
+    /**
+     * The HTML element to provide to Pickr as the root element.
+     *
+     * @type {HTMLDivElement}
+     */
+    var pickerContainer = document.createElement('div');
+    pickerContainer.className = 'shared-color-picker';
+
+    /**
+     * An instance of Deferred which represents an active request for the
+     * user to choose a color. The promise associated with the Deferred will
+     * be resolved with the chosen color once a color is chosen, and rejected
+     * if the request is cancelled or Pickr is not available. If no request is
+     * active, this will be null.
+     *
+     * @type {Deferred}
+     */
+    var activeRequest = null;
+
+    /**
+     * Resolves the current active request with the given color value. If no
+     * color value is provided, the active request is rejected. If no request
+     * is active, this function has no effect.
+     *
+     * @param {String} [color]
+     *     The color value to resolve the active request with.
+     */
+    var completeActiveRequest = function completeActiveRequest(color) {
+        if (activeRequest) {
+
+            // Hide color picker, if shown
+            pickr.hide();
+
+            // Resolve/reject active request depending on value provided
+            if (color)
+                activeRequest.resolve(color);
+            else
+                activeRequest.reject();
+
+            // No active request
+            activeRequest = null;
+
+        }
+    };
+
+    try {
+        pickr = Pickr.create({
+
+            // Bind color picker to the container element
+            el : pickerContainer,
+
+            // Wrap color picker dialog in Guacamole-specific class for
+            // sake of additional styling
+            appClass : 'guac-input-color-picker',
+
+            'default' : '#000000',
+
+            // Display color details as hex
+            defaultRepresentation : 'HEX',
+
+            // Use "monolith" theme, as a nice balance between "nano" (does
+            // not work in Internet Explorer) and "classic" (too big)
+            theme : 'monolith',
+
+            // Leverage the container element as the button which shows the
+            // picker, relying on our own styling for that button
+            useAsButton  : true,
+            appendToBody : true,
+
+            // Do not include opacity controls
+            lockOpacity : true,
+
+            // Include a selection of palette entries for convenience and
+            // reference
+            swatches : [],
+
+            components: {
+
+                // Include hue and color preview controls
+                preview : true,
+                hue     : true,
+
+                // Display only a text color input field and the save and
+                // cancel buttons (no clear button)
+                interaction: {
+                    input  : true,
+                    save   : true,
+                    cancel : true
+                }
+
+            }
+
+        });
+
+        // Hide color picker after user clicks "cancel"
+        pickr.on('cancel', function colorChangeCanceled() {
+            completeActiveRequest();
+        });
+
+        // Keep model in sync with changes to the color picker
+        pickr.on('save', function colorChanged(color) {
+            completeActiveRequest(color.toHEXA().toString());
+            activeRequest = null;
+        });
+
+        // Keep color picker in sync with changes to the model
+        pickr.on('init', function pickrReady() {
+            pickrInitComplete = true;
+        });
+    }
+    catch (e) {
+        // If the "Pickr" color picker cannot be loaded (Internet Explorer),
+        // the available flag will remain set to false
+    }
+
+    // Factory method required by provider
+    this.$get = ['$injector', function colorPickerServiceFactory($injector) {
+
+        // Required services
+        var $q         = $injector.get('$q');
+        var $translate = $injector.get('$translate');
+
+        var service = {};
+
+        /**
+         * Promise which is resolved when Pickr initialization has completed
+         * and rejected if Pickr cannot be used.
+         *
+         * @type {Promise}
+         */
+        var pickrPromise = (function getPickr() {
+
+            var deferred = $q.defer();
+
+            // Resolve promise when Pickr has completed initialization
+            if (pickrInitComplete)
+                deferred.resolve();
+            else if (pickr)
+                pickr.on('init', deferred.resolve);
+
+            // Reject promise if Pickr cannot be used at all
+            else
+                deferred.reject();
+
+            return deferred.promise;
+
+        })();
+
+        /**
+         * Returns whether the underlying color picker (Pickr) can be used by
+         * calling selectColor(). If the browser cannot support the color
+         * picker, false is returned.
+         *
+         * @returns {Boolean}
+         *     true if the underlying color picker can be used by calling
+         *     selectColor(), false otherwise.
+         */
+        service.isAvailable = function isAvailable() {
+            return !!pickr;
+        };
+
+        /**
+         * Prompts the user to choose a color, returning the color chosen via a
+         * Promise.
+         *
+         * @param {Element} element
+         *     The element that the user interacted with to indicate their
+         *     desire to choose a color.
+         *
+         * @param {String} current
+         *     The color that should be selected by default, in standard
+         *     6-digit hexadecimal RGB format, including "#" prefix.
+         *
+         * @param {String[]} [palette]
+         *     An array of color choices which should be exposed to the user
+         *     within the color chooser for convenience. Each color must be in
+         *     standard 6-digit hexadecimal RGB format, including "#" prefix.
+         *
+         * @returns {Promise.<String>}
+         *     A Promise which is resolved with the color chosen by the user,
+         *     in standard 6-digit hexadecimal RGB format with "#" prefix, and
+         *     rejected if the selection operation was cancelled or the color
+         *     picker cannot be used.
+         */
+        service.selectColor = function selectColor(element, current, palette) {
+
+            // Show picker once the relevant translation strings have been
+            // retrieved and Pickr is ready for use
+            return $q.all({
+                'saveString'   : $translate('APP.ACTION_SAVE'),
+                'cancelString' : $translate('APP.ACTION_CANCEL'),
+                'pickr'        : pickrPromise
+            }).then(function dependenciesReady(deps) {
+
+                // Cancel any active request
+                completeActiveRequest();
+
+                // Reset state of color picker to provided parameters
+                pickr.setColor(current);
+                element.appendChild(pickerContainer);
+
+                // Assign translated strings to button text
+                var pickrRoot = pickr.getRoot();
+                pickrRoot.interaction.save.value = deps.saveString;
+                pickrRoot.interaction.cancel.value = deps.cancelString;
+
+                // Replace all color swatches with the palette of colors given
+                while (pickr.removeSwatch(0)) {}
+                angular.forEach(palette, pickr.addSwatch.bind(pickr));
+
+                // Show color picker and wait for user to complete selection
+                activeRequest = $q.defer();
+                pickr.show();
+                return activeRequest.promise;
+
+            });
+
+        };
+
+        return service;
+
+    }];
+
+});
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/form/services/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js
index cc0a240..4198c10 100644
--- a/guacamole/src/main/webapp/app/form/services/formService.js
+++ b/guacamole/src/main/webapp/app/form/services/formService.js
@@ -220,11 +220,46 @@
         var $q               = $injector.get('$q');
         var $templateRequest = $injector.get('$templateRequest');
 
+        /**
+         * Map of module name to the injector instance created for that module.
+         *
+         * @type {Object.<String, injector>}
+         */
+        var injectors = {};
+
         var service = {};
 
         service.fieldTypes = provider.fieldTypes;
 
         /**
+         * Given the name of a module, returns an injector instance which
+         * injects dependencies within that module. A new injector may be
+         * created and initialized if no such injector has yet been requested.
+         * If the injector available to formService already includes the
+         * requested module, that injector will simply be returned.
+         *
+         * @param {String} module
+         *     The name of the module to produce an injector for.
+         *
+         * @returns {injector}
+         *     An injector instance which injects dependencies for the given
+         *     module.
+         */
+        var getInjector = function getInjector(module) {
+
+            // Use the formService's injector if possible
+            if ($injector.modules[module])
+                return $injector;
+
+            // If the formService's injector does not include the requested
+            // module, create the necessary injector, reusing that injector for
+            // future calls
+            injectors[module] = injectors[module] || angular.injector(['ng', module]);
+            return injectors[module];
+
+        };
+
+        /**
          * Compiles and links the field associated with the given name to the given
          * scope, producing a distinct and independent DOM Element which functions
          * as an instance of that field. The scope object provided must include at
@@ -300,7 +335,7 @@
 
                 // Populate scope using defined controller
                 if (fieldType.module && fieldType.controller) {
-                    var $controller = angular.injector(['ng', fieldType.module]).get('$controller');
+                    var $controller = getInjector(fieldType.module).get('$controller');
                     $controller(fieldType.controller, {
                         '$scope'   : scope,
                         '$element' : angular.element(fieldContainer.childNodes)
diff --git a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
index fc6e675..eae1f66 100644
--- a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
+++ b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
@@ -1,8 +1,9 @@
 <div class="guac-input-color"
      ng-class="{
          'dark' : isDark(),
-         'read-only' : colorPickerUnavailable
+         'read-only' : !isColorPickerAvailable()
      }"
+     ng-click="selectColor()"
      ng-style="{
         'background-color' : model
      }">