/** * @license * Visual Blocks Editor * * Copyright 2012 Google Inc. * https://developers.google.com/blockly/ * * 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. */ /** * @fileoverview Colour input field. * @author fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.FieldColourSlider'); goog.require('Blockly.Field'); goog.require('Blockly.DropDownDiv'); goog.require('goog.dom'); goog.require('goog.events'); goog.require('goog.style'); goog.require('goog.color'); goog.require('goog.ui.Slider'); /** * Class for a slider-based colour input field. * @param {string} colour The initial colour in '#rrggbb' format. * @param {Function=} opt_validator A function that is executed when a new * colour is selected. Its sole argument is the new colour value. Its * return value becomes the selected colour, unless it is undefined, in * which case the new colour stands, or it is null, in which case the change * is aborted. * @extends {Blockly.Field} * @constructor */ Blockly.FieldColourSlider = function(colour, opt_validator) { Blockly.FieldColourSlider.superClass_.constructor.call(this, colour, opt_validator); this.addArgType('colour'); // Flag to track whether or not the slider callbacks should execute this.sliderCallbacksEnabled_ = false; }; goog.inherits(Blockly.FieldColourSlider, Blockly.Field); /** * Construct a FieldColourSlider from a JSON arg object. * @param {!Object} options A JSON object with options (colour). * @returns {!Blockly.FieldColourSlider} The new field instance. * @package * @nocollapse */ Blockly.FieldColourSlider.fromJson = function(options) { return new Blockly.FieldColourSlider(options['colour']); }; /** * Function to be called if eyedropper can be activated. * If defined, an eyedropper button will be added to the color picker. * The button calls this function with a callback to update the field value. * BEWARE: This is not a stable API, so it is being marked as private. It may change. * @private */ Blockly.FieldColourSlider.activateEyedropper_ = null; /** * Path to the eyedropper svg icon. */ Blockly.FieldColourSlider.EYEDROPPER_PATH = 'eyedropper.svg'; /** * Install this field on a block. * @param {!Blockly.Block} block The block containing this field. */ Blockly.FieldColourSlider.prototype.init = function(block) { if (this.fieldGroup_) { // Colour slider has already been initialized once. return; } Blockly.FieldColourSlider.superClass_.init.call(this, block); this.setValue(this.getValue()); }; /** * Return the current colour. * @return {string} Current colour in '#rrggbb' format. */ Blockly.FieldColourSlider.prototype.getValue = function() { return this.colour_; }; /** * Set the colour. * @param {string} colour The new colour in '#rrggbb' format. */ Blockly.FieldColourSlider.prototype.setValue = function(colour) { if (this.sourceBlock_ && Blockly.Events.isEnabled() && this.colour_ != colour) { Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.colour_, colour)); } this.colour_ = colour; if (this.sourceBlock_) { // Set the colours to this value. // The renderer expects to be able to use the secondary colour as the fill for a shadow. this.sourceBlock_.setColour(colour, colour, this.sourceBlock_.getColourTertiary(), this.sourceBlock_.getColourQuaternary()); } this.updateSliderHandles_(); this.updateDom_(); }; /** * Create the hue, saturation or value CSS gradient for the slide backgrounds. * @param {string} channel – Either "hue", "saturation" or "value". * @return {string} Array colour hex colour stops for the given channel * @private */ Blockly.FieldColourSlider.prototype.createColourStops_ = function(channel) { var stops = []; for(var n = 0; n <= 360; n += 20) { switch (channel) { case 'hue': stops.push(goog.color.hsvToHex(n, this.saturation_, this.brightness_)); break; case 'saturation': stops.push(goog.color.hsvToHex(this.hue_, n / 360, this.brightness_)); break; case 'brightness': stops.push(goog.color.hsvToHex(this.hue_, this.saturation_, 255 * n / 360)); break; default: throw new Error("Unknown channel for colour sliders: " + channel); } } return stops; }; /** * Set the gradient CSS properties for the given node and channel * @param {Node} node - The DOM node the gradient will be set on. * @param {string} channel – Either "hue", "saturation" or "value". * @private */ Blockly.FieldColourSlider.prototype.setGradient_ = function(node, channel) { var gradient = this.createColourStops_(channel).join(','); goog.style.setStyle(node, 'background', '-moz-linear-gradient(left, ' + gradient + ')'); goog.style.setStyle(node, 'background', '-webkit-linear-gradient(left, ' + gradient + ')'); goog.style.setStyle(node, 'background', '-o-linear-gradient(left, ' + gradient + ')'); goog.style.setStyle(node, 'background', '-ms-linear-gradient(left, ' + gradient + ')'); goog.style.setStyle(node, 'background', 'linear-gradient(left, ' + gradient + ')'); }; /** * Update the readouts and slider backgrounds after value has changed. * @private */ Blockly.FieldColourSlider.prototype.updateDom_ = function() { if (this.hueSlider_) { // Update the slider backgrounds this.setGradient_(this.hueSlider_.getElement(), 'hue'); this.setGradient_(this.saturationSlider_.getElement(), 'saturation'); this.setGradient_(this.brightnessSlider_.getElement(), 'brightness'); // Update the readouts this.hueReadout_.textContent = Math.floor(100 * this.hue_ / 360).toFixed(0); this.saturationReadout_.textContent = Math.floor(100 * this.saturation_).toFixed(0); this.brightnessReadout_.textContent = Math.floor(100 * this.brightness_ / 255).toFixed(0); } }; /** * Update the slider handle positions from the current field value. * @private */ Blockly.FieldColourSlider.prototype.updateSliderHandles_ = function() { if (this.hueSlider_) { // Don't let the following calls to setValue for each of the sliders // trigger the slider callbacks (which then call setValue on this field again // unnecessarily) this.sliderCallbacksEnabled_ = false; this.hueSlider_.setValue(this.hue_); this.saturationSlider_.setValue(this.saturation_); this.brightnessSlider_.setValue(this.brightness_); this.sliderCallbacksEnabled_ = true; } }; /** * Get the text from this field. Used when the block is collapsed. * @return {string} Current text. */ Blockly.FieldColourSlider.prototype.getText = function() { var colour = this.colour_; // Try to use #rgb format if possible, rather than #rrggbb. var m = colour.match(/^#(.)\1(.)\2(.)\3$/); if (m) { colour = '#' + m[1] + m[2] + m[3]; } return colour; }; /** * Create label and readout DOM elements, returning the readout * @param {string} labelText - Text for the label * @return {Array} The container node and the readout node. * @private */ Blockly.FieldColourSlider.prototype.createLabelDom_ = function(labelText) { var labelContainer = document.createElement('div'); labelContainer.setAttribute('class', 'scratchColourPickerLabel'); var readout = document.createElement('span'); readout.setAttribute('class', 'scratchColourPickerReadout'); var label = document.createElement('span'); label.setAttribute('class', 'scratchColourPickerLabelText'); label.textContent = labelText; labelContainer.appendChild(label); labelContainer.appendChild(readout); return [labelContainer, readout]; }; /** * Factory for creating the different slider callbacks * @param {string} channel - One of "hue", "saturation" or "brightness" * @return {function} the callback for slider update * @private */ Blockly.FieldColourSlider.prototype.sliderCallbackFactory_ = function(channel) { var thisField = this; return function(event) { if (!thisField.sliderCallbacksEnabled_) return; var channelValue = event.target.getValue(); switch (channel) { case 'hue': thisField.hue_ = channelValue; break; case 'saturation': thisField.saturation_ = channelValue; break; case 'brightness': thisField.brightness_ = channelValue; break; } var colour = goog.color.hsvToHex(thisField.hue_, thisField.saturation_, thisField.brightness_); if (thisField.sourceBlock_) { // Call any validation function, and allow it to override. colour = thisField.callValidator(colour); } if (colour !== null) { thisField.setValue(colour, true); } }; }; /** * Activate the eyedropper, passing in a callback for setting the field value. * @private */ Blockly.FieldColourSlider.prototype.activateEyedropperInternal_ = function() { var thisField = this; Blockly.FieldColourSlider.activateEyedropper_(function(value) { // Update the internal hue/saturation/brightness values so sliders update. var hsv = goog.color.hexToHsv(value); thisField.hue_ = hsv[0]; thisField.saturation_ = hsv[1]; thisField.brightness_ = hsv[2]; thisField.setValue(value); }); }; /** * Create hue, saturation and brightness sliders under the colour field. * @private */ Blockly.FieldColourSlider.prototype.showEditor_ = function() { Blockly.DropDownDiv.hideWithoutAnimation(); Blockly.DropDownDiv.clearContent(); var div = Blockly.DropDownDiv.getContentDiv(); // Init color component values that are used while the editor is open // in order to keep the slider values stable. var hsv = goog.color.hexToHsv(this.getValue()); this.hue_ = hsv[0]; this.saturation_ = hsv[1]; this.brightness_ = hsv[2]; var hueElements = this.createLabelDom_(Blockly.Msg.COLOUR_HUE_LABEL); div.appendChild(hueElements[0]); this.hueReadout_ = hueElements[1]; this.hueSlider_ = new goog.ui.Slider(); this.hueSlider_.setUnitIncrement(5); this.hueSlider_.setMinimum(0); this.hueSlider_.setMaximum(360); this.hueSlider_.setMoveToPointEnabled(true); this.hueSlider_.render(div); var saturationElements = this.createLabelDom_(Blockly.Msg.COLOUR_SATURATION_LABEL); div.appendChild(saturationElements[0]); this.saturationReadout_ = saturationElements[1]; this.saturationSlider_ = new goog.ui.Slider(); this.saturationSlider_.setMoveToPointEnabled(true); this.saturationSlider_.setUnitIncrement(0.01); this.saturationSlider_.setStep(0.001); this.saturationSlider_.setMinimum(0); this.saturationSlider_.setMaximum(1.0); this.saturationSlider_.render(div); var brightnessElements = this.createLabelDom_(Blockly.Msg.COLOUR_BRIGHTNESS_LABEL); div.appendChild(brightnessElements[0]); this.brightnessReadout_ = brightnessElements[1]; this.brightnessSlider_ = new goog.ui.Slider(); this.brightnessSlider_.setUnitIncrement(2); this.brightnessSlider_.setMinimum(0); this.brightnessSlider_.setMaximum(255); this.brightnessSlider_.setMoveToPointEnabled(true); this.brightnessSlider_.render(div); if (Blockly.FieldColourSlider.activateEyedropper_) { var button = document.createElement('button'); button.setAttribute('class', 'scratchEyedropper'); var image = document.createElement('img'); image.src = Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldColourSlider.EYEDROPPER_PATH; button.appendChild(image); div.appendChild(button); Blockly.FieldColourSlider.eyedropperEventData_ = Blockly.bindEventWithChecks_(button, 'click', this, this.activateEyedropperInternal_); } Blockly.DropDownDiv.setColour('#ffffff', '#dddddd'); Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory()); Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_); // Set value updates the slider positions // Do this before attaching callbacks to avoid extra events from initial set this.setValue(this.getValue()); // Enable callbacks for the sliders this.sliderCallbacksEnabled_ = true; Blockly.FieldColourSlider.hueChangeEventKey_ = goog.events.listen(this.hueSlider_, goog.ui.Component.EventType.CHANGE, this.sliderCallbackFactory_('hue')); Blockly.FieldColourSlider.saturationChangeEventKey_ = goog.events.listen(this.saturationSlider_, goog.ui.Component.EventType.CHANGE, this.sliderCallbackFactory_('saturation')); Blockly.FieldColourSlider.brightnessChangeEventKey_ = goog.events.listen(this.brightnessSlider_, goog.ui.Component.EventType.CHANGE, this.sliderCallbackFactory_('brightness')); }; Blockly.FieldColourSlider.prototype.dispose = function() { if (Blockly.FieldColourSlider.hueChangeEventKey_) { goog.events.unlistenByKey(Blockly.FieldColourSlider.hueChangeEventKey_); } if (Blockly.FieldColourSlider.saturationChangeEventKey_) { goog.events.unlistenByKey(Blockly.FieldColourSlider.saturationChangeEventKey_); } if (Blockly.FieldColourSlider.brightnessChangeEventKey_) { goog.events.unlistenByKey(Blockly.FieldColourSlider.brightnessChangeEventKey_); } if (Blockly.FieldColourSlider.eyedropperEventData_) { Blockly.unbindEvent_(Blockly.FieldColourSlider.eyedropperEventData_); } Blockly.Events.setGroup(false); Blockly.FieldColourSlider.superClass_.dispose.call(this); }; Blockly.Field.register('field_colour_slider', Blockly.FieldColourSlider);