/** * @license * Visual Blocks Editor * * Copyright 2013 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 Angle input field. * @author fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.FieldAngle'); goog.require('Blockly.FieldTextInput'); goog.require('goog.math'); goog.require('goog.userAgent'); /** * Class for an editable angle field. * @param {string} text The initial content of the field. * @param {Function=} opt_changeHandler An optional function that is called * to validate any constraints on what the user entered. Takes the new * text as an argument and returns the accepted text or null to abort * the change. * @extends {Blockly.FieldTextInput} * @constructor */ Blockly.FieldAngle = function(text, opt_changeHandler) { // Add degree symbol: "360°" (LTR) or "°360" (RTL) this.symbol_ = Blockly.createSvgElement('tspan', {}, null); this.symbol_.appendChild(document.createTextNode('\u00B0')); Blockly.FieldAngle.superClass_.constructor.call(this, text, null); this.setChangeHandler(opt_changeHandler); }; goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); /** * Sets a new change handler for angle field. * @param {Function} handler New change handler, or null. */ Blockly.FieldAngle.prototype.setChangeHandler = function(handler) { var wrappedHandler; if (handler) { // Wrap the user's change handler together with the angle validator. wrappedHandler = function(value) { var v1 = handler.call(this, value); if (v1 === null) { var v2 = v1; } else { if (v1 === undefined) { v1 = value; } var v2 = Blockly.FieldAngle.angleValidator.call(this, v1); if (v2 !== undefined) { v2 = v1; } } return v2 === value ? undefined : v2; }; } else { wrappedHandler = Blockly.FieldAngle.angleValidator; } Blockly.FieldAngle.superClass_.setChangeHandler(wrappedHandler); }; /** * Round angles to the nearest 15 degrees when using mouse. * Set to 0 to disable rounding. */ Blockly.FieldAngle.ROUND = 15; /** * Half the width of protractor image. */ Blockly.FieldAngle.HALF = 100 / 2; /** * Radius of protractor circle. Slightly smaller than protractor size since * otherwise SVG crops off half the border at the edges. */ Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF - 1; /** * Clean up this FieldAngle, as well as the inherited FieldTextInput. * @return {!Function} Closure to call on destruction of the WidgetDiv. * @private */ Blockly.FieldAngle.prototype.dispose_ = function() { var thisField = this; return function() { Blockly.FieldAngle.superClass_.dispose_.call(thisField)(); thisField.gauge_ = null; if (thisField.clickWrapper_) { Blockly.unbindEvent_(thisField.clickWrapper_); } if (thisField.moveWrapper1_) { Blockly.unbindEvent_(thisField.moveWrapper1_); } if (thisField.moveWrapper2_) { Blockly.unbindEvent_(thisField.moveWrapper2_); } }; }; /** * Show the inline free-text editor on top of the text. * @private */ Blockly.FieldAngle.prototype.showEditor_ = function() { var noFocus = goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD; // Mobile browsers have issues with in-line textareas (focus & keyboards). Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); var div = Blockly.WidgetDiv.DIV; if (!div.firstChild) { // Mobile interface uses window.prompt. return; } // Build the SVG DOM. var svg = Blockly.createSvgElement('svg', { 'xmlns': 'http://www.w3.org/2000/svg', 'xmlns:html': 'http://www.w3.org/1999/xhtml', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'version': '1.1', 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px' }, div); var circle = Blockly.createSvgElement('circle', { 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, 'r': Blockly.FieldAngle.RADIUS, 'class': 'blocklyAngleCircle' }, svg); this.gauge_ = Blockly.createSvgElement('path', {'class': 'blocklyAngleGauge'}, svg); this.line_ = Blockly.createSvgElement('line', {'x1': Blockly.FieldAngle.HALF, 'y1': Blockly.FieldAngle.HALF, 'class': 'blocklyAngleLine'}, svg); // Draw markers around the edge. for (var a = 0; a < 360; a += 15) { Blockly.createSvgElement('line', { 'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS, 'y1': Blockly.FieldAngle.HALF, 'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - (a % 45 == 0 ? 10 : 5), 'y2': Blockly.FieldAngle.HALF, 'class': 'blocklyAngleMarks', 'transform': 'rotate(' + a + ', ' + Blockly.FieldAngle.HALF + ', ' + Blockly.FieldAngle.HALF + ')' }, svg); } svg.style.marginLeft = '-35px'; this.clickWrapper_ = Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide); this.moveWrapper1_ = Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); this.moveWrapper2_ = Blockly.bindEvent_(this.gauge_, 'mousemove', this, this.onMouseMove); this.updateGraph_(); }; /** * Set the angle to match the mouse's position. * @param {!Event} e Mouse move event. */ Blockly.FieldAngle.prototype.onMouseMove = function(e) { var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF; var angle = Math.atan(-dy / dx); if (isNaN(angle)) { // This shouldn't happen, but let's not let this error propogate further. return; } angle = goog.math.toDegrees(angle); // 0: East, 90: North, 180: West, 270: South. if (dx < 0) { angle += 180; } else if (dy > 0) { angle += 360; } if (Blockly.FieldAngle.ROUND) { angle = Math.round(angle / Blockly.FieldAngle.ROUND) * Blockly.FieldAngle.ROUND; } if (angle >= 360) { // Rounding may have rounded up to 360. angle -= 360; } angle = String(angle); Blockly.FieldTextInput.htmlInput_.value = angle; this.setText(angle); }; /** * Insert a degree symbol. * @param {?string} text New text. */ Blockly.FieldAngle.prototype.setText = function(text) { Blockly.FieldAngle.superClass_.setText.call(this, text); if (!this.textElement_) { // Not rendered yet. return; } this.updateGraph_(); // Insert degree symbol. if (this.sourceBlock_.RTL) { this.textElement_.insertBefore(this.symbol_, this.textElement_.firstChild); } else { this.textElement_.appendChild(this.symbol_); } // Cached width is obsolete. Clear it. this.size_.width = 0; }; /** * Redraw the graph with the current angle. * @private */ Blockly.FieldAngle.prototype.updateGraph_ = function() { if (!this.gauge_) { return; } var angleRadians = goog.math.toRadians(Number(this.getText())); if (isNaN(angleRadians)) { this.gauge_.setAttribute('d', 'M ' + Blockly.FieldAngle.HALF + ', ' + Blockly.FieldAngle.HALF); this.line_.setAttribute('x2', Blockly.FieldAngle.HALF); this.line_.setAttribute('y2', Blockly.FieldAngle.HALF); } else { var x = Blockly.FieldAngle.HALF + Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS; var y = Blockly.FieldAngle.HALF + Math.sin(angleRadians) * -Blockly.FieldAngle.RADIUS; var largeFlag = (angleRadians > Math.PI) ? 1 : 0; this.gauge_.setAttribute('d', 'M ' + Blockly.FieldAngle.HALF + ', ' + Blockly.FieldAngle.HALF + ' h ' + Blockly.FieldAngle.RADIUS + ' A ' + Blockly.FieldAngle.RADIUS + ',' + Blockly.FieldAngle.RADIUS + ' 0 ' + largeFlag + ' 0 ' + x + ',' + y + ' z'); this.line_.setAttribute('x2', x); this.line_.setAttribute('y2', y); } }; /** * Ensure that only an angle may be entered. * @param {string} text The user's text. * @return {?string} A string representing a valid angle, or null if invalid. */ Blockly.FieldAngle.angleValidator = function(text) { var n = Blockly.FieldTextInput.numberValidator(text); if (n !== null) { n = n % 360; if (n < 0) { n += 360; } n = String(n); } return n; };