diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index b9181f6e..51962a03 100644 --- a/core/block_render_svg_horizontal.js +++ b/core/block_render_svg_horizontal.js @@ -300,7 +300,7 @@ Blockly.BlockSvg.metricsAreEquivalent_ = function(first, second) { * Play some UI effects (sound) after a connection has been established. */ Blockly.BlockSvg.prototype.connectionUiEffect = function() { - this.workspace.playAudio('click'); + this.workspace.getAudioManager().play('click'); }; /** diff --git a/core/block_svg.js b/core/block_svg.js index 75a479dc..1a4040aa 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -28,6 +28,7 @@ goog.provide('Blockly.BlockSvg'); goog.require('Blockly.Block'); goog.require('Blockly.ContextMenu'); +goog.require('Blockly.Grid'); goog.require('Blockly.RenderedConnection'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); @@ -482,11 +483,11 @@ Blockly.BlockSvg.prototype.snapToGrid = function() { if (this.isInFlyout) { return; // Don't move blocks around in a flyout. } - if (!this.workspace.options.gridOptions || - !this.workspace.options.gridOptions['snap']) { + var grid = this.workspace.getGrid(); + if (!grid || !grid.shouldSnap()) { return; // Config says no snapping. } - var spacing = this.workspace.options.gridOptions['spacing']; + var spacing = grid.getSpacing(); var half = spacing / 2; var xy = this.getRelativeToSurfaceXY(); var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x; @@ -910,7 +911,7 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { * Play some UI effects (sound, animation) when disposing of a block. */ Blockly.BlockSvg.prototype.disposeUiEffect = function() { - this.workspace.playAudio('delete'); + this.workspace.getAudioManager().play('delete'); var xy = this.workspace.getSvgXY(/** @type {!Element} */ (this.svgGroup_)); // Deeply clone the current block. @@ -930,7 +931,7 @@ Blockly.BlockSvg.prototype.disposeUiEffect = function() { * Play some UI effects (sound) after a connection has been established. */ Blockly.BlockSvg.prototype.connectionUiEffect = function() { - this.workspace.playAudio('click'); + this.workspace.getAudioManager().play('click'); }; /** diff --git a/core/constants.js b/core/constants.js index 97f659f1..323900ee 100644 --- a/core/constants.js +++ b/core/constants.js @@ -310,3 +310,19 @@ Blockly.VARIABLE_CATEGORY_NAME = 'VARIABLE'; * @const {string} */ Blockly.PROCEDURE_CATEGORY_NAME = 'PROCEDURE'; + +/** + * String for use in the dropdown created in field_variable. + * This string indicates that this option in the dropdown is 'Rename + * variable...' and if selected, should trigger the prompt to rename a variable. + * @const {string} + */ +Blockly.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID'; + +/** + * String for use in the dropdown created in field_variable. + * This string indicates that this option in the dropdown is 'Delete the "%1" + * variable' and if selected, should trigger the prompt to delete a variable. + * @const {string} + */ +Blockly.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID'; diff --git a/core/css.js b/core/css.js index 7ec0dc2f..e4e63809 100644 --- a/core/css.js +++ b/core/css.js @@ -767,6 +767,10 @@ Blockly.Css.CONTENT = [ 'vertical-align: middle;', '}', + '.blocklyToolboxDelete .blocklyTreeLabel {', + 'cursor: url("<<<PATH>>>/handdelete.cur"), auto;', + '}', + '.blocklyTreeSelected .blocklyTreeLabel {', 'color: #fff;', '}', diff --git a/core/field_image.js b/core/field_image.js index a13ea4ab..c9fd165c 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -162,6 +162,7 @@ Blockly.FieldImage.prototype.setText = function(alt) { Blockly.FieldImage.prototype.render_ = function() { // NOP }; + /** * Images are fixed width, no need to update. * @private diff --git a/core/field_textinput.js b/core/field_textinput.js index c5452042..f80e5833 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -489,7 +489,6 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { } } thisField.setText(text); - // Rerender the field now that the text has changed. thisField.sourceBlock_.rendered && thisField.sourceBlock_.render(); Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_); Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_); diff --git a/core/field_variable.js b/core/field_variable.js index 2b10a016..880e1580 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -28,6 +28,7 @@ goog.provide('Blockly.FieldVariable'); goog.require('Blockly.FieldDropdown'); goog.require('Blockly.Msg'); +goog.require('Blockly.VariableModel'); goog.require('Blockly.Variables'); goog.require('goog.asserts'); goog.require('goog.string'); @@ -50,21 +51,6 @@ Blockly.FieldVariable = function(varname, opt_validator) { }; goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown); -/** - * The menu item index for the rename variable option. - * @type {number} - * @private - */ -Blockly.FieldVariable.prototype.renameVarItemIndex_ = -1; - -/** - * The menu item index for the delete variable option. - * @type {number} - * @private - */ -Blockly.FieldVariable.prototype.deleteVarItemIndex_ = -1; - - /** * Install this dropdown on a block. */ @@ -135,37 +121,41 @@ Blockly.FieldVariable.prototype.setValue = function(newValue) { * @this {Blockly.FieldVariable} */ Blockly.FieldVariable.dropdownCreate = function() { - var variableNameList = []; - if (this.sourceBlock_ && this.sourceBlock_.workspace) { + var variableModelList = []; + var name = this.getText(); + // Don't create a new variable if there is nothing selected. + var createSelectedVariable = name ? true : false; + var workspace = null; + if (this.sourceBlock_) { + workspace = this.sourceBlock_.workspace; + } + + if (workspace) { // Get a copy of the list, so that adding rename and new variable options // doesn't modify the workspace's list. - - var variableModelList = this.sourceBlock_.workspace.getVariablesOfType(''); - for (var i = 0; i < variableModelList.length; i++) { - variableNameList.push(variableModelList[i].name); + var variableModelList = workspace.getVariablesOfType(''); + for (var i = 0; i < variableModelList.length; i++){ + if (createSelectedVariable && + goog.string.caseInsensitiveEquals(variableModelList[i].name, name)) { + createSelectedVariable = false; + break; + } } } // Ensure that the currently selected variable is an option. - var name = this.getText(); - if (name && variableNameList.indexOf(name) == -1) { - variableNameList.push(name); + if (createSelectedVariable && workspace) { + var newVar = workspace.createVariable(name); + variableModelList.push(newVar); } - variableNameList.sort(goog.string.caseInsensitiveCompare); - - this.renameVarItemIndex_ = variableNameList.length; - variableNameList.push(Blockly.Msg.RENAME_VARIABLE); - - this.deleteVarItemIndex_ = variableNameList.length; - variableNameList.push(Blockly.Msg.DELETE_VARIABLE.replace('%1', name)); - // Variables are not language-specific, use the name as both the user-facing - // text and the internal representation. + variableModelList.sort(Blockly.VariableModel.compareByName); var options = []; - for (var i = 0; i < variableNameList.length; i++) { - // TODO(marisaleung): Set options[i] to [name, uuid]. This requires - // changes where the variable gets set since the initialized value would be - // id. - options[i] = [variableNameList[i], variableNameList[i]]; + for (var i = 0; i < variableModelList.length; i++) { + // Set the uuid as the internal representation of the variable. + options[i] = [variableModelList[i].name, variableModelList[i].getId()]; } + options.push([Blockly.Msg.RENAME_VARIABLE, Blockly.RENAME_VARIABLE_ID]); + options.push([Blockly.Msg.DELETE_VARIABLE.replace('%1', name), + Blockly.DELETE_VARIABLE_ID]); return options; }; @@ -177,11 +167,18 @@ Blockly.FieldVariable.dropdownCreate = function() { * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu. */ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { - var itemText = menuItem.getValue(); - if (this.sourceBlock_) { + var id = menuItem.getValue(); + // TODO(marisaleung): change setValue() to take in an id as the parameter. + // Then remove itemText. + var itemText; + if (this.sourceBlock_ && this.sourceBlock_.workspace) { var workspace = this.sourceBlock_.workspace; - if (this.renameVarItemIndex_ >= 0 && - menu.getChildAt(this.renameVarItemIndex_) === menuItem) { + var variable = workspace.getVariableById(id); + // If the item selected is a variable, set itemText to the variable name. + if (variable) { + itemText = variable.name; + } + else if (id == Blockly.RENAME_VARIABLE_ID) { // Rename variable. var oldName = this.getText(); Blockly.hideChaff(); @@ -193,8 +190,7 @@ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { } }); return; - } else if (this.deleteVarItemIndex_ >= 0 && - menu.getChildAt(this.deleteVarItemIndex_) === menuItem) { + } else if (id == Blockly.DELETE_VARIABLE_ID) { // Delete variable. workspace.deleteVariable(this.getText()); return; diff --git a/core/gesture.js b/core/gesture.js index 06cbd491..f89d2d81 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -77,13 +77,23 @@ Blockly.Gesture = function(e, creatorWorkspace) { this.startField_ = null; /** - * The block that the gesture started on, or null if it did not block on a - * field. + * The block that the gesture started on, or null if it did not start on a + * block. * @type {Blockly.BlockSvg} * @private */ this.startBlock_ = null; + /** + * The block that this gesture targets. If the gesture started on a + * shadow block, this is the first non-shadow parent of the block. If the + * gesture started in the flyout, this is the root block of the block group + * that was clicked or dragged. + * @type {Blockly.BlockSvg} + * @private + */ + this.targetBlock_ = null; + /** * The workspace that the gesture started on. There may be multiple * workspaces on a page; this is more accurate than using @@ -213,6 +223,7 @@ Blockly.Gesture.prototype.dispose = function() { this.startField_ = null; this.startBlock_ = null; + this.targetBlock_ = null; this.startWorkspace_ = null; this.flyout_ = null; @@ -274,13 +285,13 @@ Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) { * This function should be called on a mouse/touch move event the first time the * drag radius is exceeded. It should be called no more than once per gesture. * If a block should be dragged from the flyout this function creates the new - * block on the main workspace and updates startBlock_ and startWorkspace_. + * block on the main workspace and updates targetBlock_ and startWorkspace_. * @return {boolean} True if a block is being dragged from the flyout. * @private */ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { // Disabled blocks may not be dragged from the flyout. - if (this.startBlock_.disabled) { + if (this.targetBlock_.disabled) { return false; } if (!this.flyout_.isScrollable() || @@ -292,8 +303,10 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { if (!Blockly.Events.getGroup()) { Blockly.Events.setGroup(true); } - this.startBlock_ = this.flyout_.createBlock(this.startBlock_); - this.startBlock_.select(); + // The start block is no longer relevant, because this is a drag. + this.startBlock_ = null; + this.targetBlock_ = this.flyout_.createBlock(this.targetBlock_); + this.targetBlock_.select(); return true; } return false; @@ -309,13 +322,13 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { * @private */ Blockly.Gesture.prototype.updateIsDraggingBlock_ = function() { - if (!this.startBlock_) { + if (!this.targetBlock_) { return false; } if (this.flyout_) { this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_(); - } else if (this.startBlock_.isMovable()){ + } else if (this.targetBlock_.isMovable()){ this.isDraggingBlock_ = true; } @@ -377,7 +390,7 @@ Blockly.Gesture.prototype.updateIsDragging_ = function() { * @private */ Blockly.Gesture.prototype.startDraggingBlock_ = function() { - this.blockDragger_ = new Blockly.BlockDragger(this.startBlock_, + this.blockDragger_ = new Blockly.BlockDragger(this.targetBlock_, this.startWorkspace_); this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_); this.blockDragger_.dragBlock(this.mostRecentEvent_, @@ -407,12 +420,12 @@ Blockly.Gesture.prototype.doStart = function(e) { this.startWorkspace_.markFocused(); this.mostRecentEvent_ = e; - // Hide chaff also hides the flyout by default. + // Hide chaff also hides the flyout, so don't do it if the click is in a flyout. Blockly.hideChaff(!!this.flyout_); Blockly.Tooltip.block(); - if (this.startBlock_) { - this.startBlock_.select(); + if (this.targetBlock_) { + this.targetBlock_.select(); } if (Blockly.utils.isRightButton(e)) { @@ -514,10 +527,10 @@ Blockly.Gesture.prototype.cancel = function() { * @package */ Blockly.Gesture.prototype.handleRightClick = function(e) { - if (this.startBlock_) { + if (this.targetBlock_) { this.bringBlockToFront_(); Blockly.hideChaff(this.flyout_); - this.startBlock_.showContextMenu_(e); + this.targetBlock_.showContextMenu_(e); } else if (this.startWorkspace_ && !this.flyout_) { Blockly.hideChaff(); this.startWorkspace_.showContextMenu_(e); @@ -595,7 +608,7 @@ Blockly.Gesture.prototype.doBlockClick_ = function() { if (!Blockly.Events.getGroup()) { Blockly.Events.setGroup(true); } - var newBlock = this.flyout_.createBlock(this.startBlock_); + var newBlock = this.flyout_.createBlock(this.targetBlock_); newBlock.scheduleSnapAndBump(); } else { // A field is being edited if either the WidgetDiv or DropDownDiv is currently open. @@ -635,8 +648,8 @@ Blockly.Gesture.prototype.doWorkspaceClick_ = function() { */ Blockly.Gesture.prototype.bringBlockToFront_ = function() { // Blocks in the flyout don't overlap, so skip the work. - if (this.startBlock_ && !this.flyout_) { - this.startBlock_.bringToFront(); + if (this.targetBlock_ && !this.flyout_) { + this.targetBlock_.bringToFront(); } }; @@ -657,24 +670,37 @@ Blockly.Gesture.prototype.setStartField = function(field) { }; /** - * Record the block that a gesture started on. - * If the block is a shadow, record the parent. If the block is in the flyout, - * use the root block from the block group. + * Record the block that a gesture started on, and set the target block + * appropriately. * @param {Blockly.BlockSvg} block The block the gesture started on. * @package */ Blockly.Gesture.prototype.setStartBlock = function(block) { if (!this.startBlock_) { - if (block.isShadow()) { - this.setStartBlock(block.getParent()); - } else if (block.isInFlyout && block != block.getRootBlock()) { - this.setStartBlock(block.getRootBlock()); + this.startBlock_ = block; + if (block.isInFlyout && block != block.getRootBlock()) { + this.setTargetBlock_(block.getRootBlock()); } else { - this.startBlock_ = block; + this.setTargetBlock_(block); } } }; +/** + * Record the block that a gesture targets, meaning the block that will be + * dragged if this turns into a drag. If this block is a shadow, that will be + * its first non-shadow parent. + * @param {Blockly.BlockSvg} block The block the gesture targets. + * @private + */ +Blockly.Gesture.prototype.setTargetBlock_ = function(block) { + if (block.isShadow()) { + this.setTargetBlock_(block.getParent()); + } else { + this.targetBlock_ = block; + } +}; + /** * Record the workspace that a gesture started on. * @param {Blockly.WorkspaceSvg} ws The workspace the gesture started on. diff --git a/core/grid.js b/core/grid.js new file mode 100644 index 00000000..e87df602 --- /dev/null +++ b/core/grid.js @@ -0,0 +1,222 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2017 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 Object for configuring and updating a workspace grid in + * Blockly. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.Grid'); + +goog.require('Blockly.utils'); + +goog.require('goog.userAgent'); + + +/** + * Class for a workspace's grid. + * @param {!SVGElement} pattern The grid's SVG pattern, created during injection. + * @param {!Object} options A dictionary of normalized options for the grid. + * See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + * @constructor + */ +Blockly.Grid = function(pattern, options) { + /** + * The grid's SVG pattern, created during injection. + * @type {!SVGElement} + * @private + */ + this.gridPattern_ = pattern; + + /** + * The spacing of the grid lines (in px). + * @type {number} + * @private + */ + this.spacing_ = options['spacing']; + + /** + * How long the grid lines should be (in px). + * @type {number} + * @private + */ + this.length_ = options['length']; + + /** + * The horizontal grid line, if it exists. + * @type {SVGElement} + * @private + */ + this.line1_ = pattern.firstChild; + + /** + * The vertical grid line, if it exists. + * @type {SVGElement} + * @private + */ + this.line2_ = this.line1_ && this.line1_.nextSibling; + + /** + * Whether blocks should snap to the grid. + * @type {boolean} + * @private + */ + this.snapToGrid_ = options['snap']; +}; + +/** + * The scale of the grid, used to set stroke width on grid lines. + * This should always be the same as the workspace scale. + * @type {number} + * @private + */ +Blockly.Grid.prototype.scale_ = 1; + +/** + * Dispose of this grid and unlink from the DOM. + * @package + */ +Blockly.Grid.prototype.dispose = function() { + this.gridPattern_ = null; +}; + +/** + * Whether blocks should snap to the grid, based on the initial configuration. + * @return {boolean} True if blocks should snap, false otherwise. + * @package + */ +Blockly.Grid.prototype.shouldSnap = function() { + return this.snapToGrid_; +}; + +/** + * Get the spacing of the grid points (in px). + * @return {number} The spacing of the grid points. + * @package + */ +Blockly.Grid.prototype.getSpacing = function() { + return this.spacing_; +}; + +/** + * Get the id of the pattern element, which should be randomized to avoid + * conflicts with other Blockly instances on the page. + * @return {string} The pattern id. + * @package + */ +Blockly.Grid.prototype.getPatternId = function() { + return this.gridPattern_.id; +}; + +/** + * Update the grid with a new scale. + * @param {number} scale The new workspace scale. + * @package + */ +Blockly.Grid.prototype.update = function(scale) { + this.scale_ = scale; + // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100. + var safeSpacing = (this.spacing_ * scale) || 100; + + this.gridPattern_.setAttribute('width', safeSpacing); + this.gridPattern_.setAttribute('height', safeSpacing); + + var half = Math.floor(this.spacing_ / 2) + 0.5; + var start = half - this.length_ / 2; + var end = half + this.length_ / 2; + + half *= scale; + start *= scale; + end *= scale; + + this.setLineAttributes_(this.line1_, scale, start, end, half, half); + this.setLineAttributes_(this.line2_, scale, half, half, start, end); +}; + +/** + * Set the attributes on one of the lines in the grid. Use this to update the + * length and stroke width of the grid lines. + * @param {!SVGElement} line Which line to update. + * @param {number} width The new stroke size (in px). + * @param {number} x1 The new x start position of the line (in px). + * @param {number} x2 The new x end position of the line (in px). + * @param {number} y1 The new y start position of the line (in px). + * @param {number} y2 The new y end position of the line (in px). + * @private + */ +Blockly.Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) { + if (line) { + line.setAttribute('stroke-width', width); + line.setAttribute('x1', x1); + line.setAttribute('y1', y1); + line.setAttribute('x2', x2); + line.setAttribute('y2', y2); + } +}; + +/** + * Move the grid to a new x and y position, and make sure that change is visible. + * @param {number} x The new x position of the grid (in px). + * @param {number} y The new y position ofthe grid (in px). + * @package + */ +Blockly.Grid.prototype.moveTo = function(x, y) { + this.gridPattern_.setAttribute('x', x); + this.gridPattern_.setAttribute('y', y); + + if (goog.userAgent.IE || goog.userAgent.EDGE) { + // IE/Edge doesn't notice that the x/y offsets have changed. + // Force an update. + this.update(this.scale_); + } +}; + +/** + * Create the DOM for the grid described by options. + * @param {string} rnd A random ID to append to the pattern's ID. + * @param {!Object} gridOptions The object containing grid configuration. + * @param {!SVGElement} defs The root SVG element for this workspace's defs. + * @return {!SVGElement} The SVG element for the grid pattern. + * @package + */ +Blockly.Grid.createDom = function(rnd, gridOptions, defs) { + /* + <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse"> + <rect stroke="#888" /> + <rect stroke="#888" /> + </pattern> + */ + var gridPattern = Blockly.utils.createSvgElement('pattern', + {'id': 'blocklyGridPattern' + rnd, + 'patternUnits': 'userSpaceOnUse'}, defs); + if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) { + Blockly.utils.createSvgElement('line', + {'stroke': gridOptions['colour']}, gridPattern); + if (gridOptions['length'] > 1) { + Blockly.utils.createSvgElement('line', + {'stroke': gridOptions['colour']}, gridPattern); + } + // x1, y1, x1, x2 properties will be set later in update. + } + return gridPattern; +}; diff --git a/core/inject.js b/core/inject.js index 0a2eea8a..db344ad6 100644 --- a/core/inject.js +++ b/core/inject.js @@ -30,6 +30,7 @@ goog.require('Blockly.BlockDragSurfaceSvg'); goog.require('Blockly.Css'); goog.require('Blockly.constants'); goog.require('Blockly.DropDownDiv'); +goog.require('Blockly.Grid'); goog.require('Blockly.Options'); goog.require('Blockly.WorkspaceSvg'); goog.require('Blockly.WorkspaceDragSurfaceSvg'); @@ -183,27 +184,7 @@ Blockly.createDom_ = function(container, options) { {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); options.disabledPatternId = disabledPattern.id; - /* - <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse"> - <rect stroke="#888" /> - <rect stroke="#888" /> - </pattern> - */ - var gridPattern = Blockly.utils.createSvgElement('pattern', - {'id': 'blocklyGridPattern' + rnd, - 'patternUnits': 'userSpaceOnUse'}, defs); - if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) { - Blockly.utils.createSvgElement('line', - {'stroke': options.gridOptions['colour']}, - gridPattern); - if (options.gridOptions['length'] > 1) { - Blockly.utils.createSvgElement('line', - {'stroke': options.gridOptions['colour']}, - gridPattern); - } - // x1, y1, x1, x2 properties will be set later in updateGridPattern_. - } - options.gridPattern = gridPattern; + options.gridPattern = Blockly.Grid.createDom(rnd, options.gridOptions, defs); return svg; }; @@ -389,10 +370,15 @@ Blockly.inject.bindDocumentEvents_ = function() { * @private */ Blockly.inject.loadSounds_ = function(pathToMedia, workspace) { - workspace.loadAudio_( - [pathToMedia + 'click.wav'], 'click'); - workspace.loadAudio_( - [pathToMedia + 'delete.wav'], 'delete'); + var audioMgr = workspace.getAudioManager(); + audioMgr.load( + [pathToMedia + 'click.mp3', + pathToMedia + 'click.wav', + pathToMedia + 'click.ogg'], 'click'); + audioMgr.load( + [pathToMedia + 'delete.mp3', + pathToMedia + 'delete.ogg', + pathToMedia + 'delete.wav'], 'delete'); // Bind temporary hooks that preload the sounds. var soundBinds = []; @@ -400,7 +386,7 @@ Blockly.inject.loadSounds_ = function(pathToMedia, workspace) { while (soundBinds.length) { Blockly.unbindEvent_(soundBinds.pop()); } - workspace.preloadAudio_(); + audioMgr.preload(); }; // opt_noCaptureIdentifier is true because this is an action to take on a diff --git a/core/toolbox.js b/core/toolbox.js index 5794d795..e0236d47 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -116,7 +116,6 @@ Blockly.Toolbox.prototype.init = function() { // Clicking on toolbox closes popups. Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this, function(e) { - Blockly.DropDownDiv.hide(); if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) { // Close flyout. Blockly.hideChaff(false); @@ -257,24 +256,6 @@ Blockly.Toolbox.prototype.removeDeleteStyle = function() { 'blocklyToolboxDelete'); }; -/** - * Adds styles on the toolbox indicating blocks will be deleted. - * @package - */ -Blockly.Toolbox.prototype.addDeleteStyle = function() { - Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv), - 'blocklyToolboxDelete'); -}; - -/** - * Remove styles from the toolbox that indicate blocks will be deleted. - * @package - */ -Blockly.Toolbox.prototype.removeDeleteStyle = function() { - Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv), - 'blocklyToolboxDelete'); -}; - /** * Return the deletion rectangle for this toolbox. * @return {goog.math.Rect} Rectangle in which to delete. diff --git a/core/utils.js b/core/utils.js index 4c1ab572..9b088977 100644 --- a/core/utils.js +++ b/core/utils.js @@ -56,13 +56,6 @@ Blockly.utils.removeAttribute = function(element, attributeName) { } }; -/** - * Cached value for whether 3D is supported - * @type {boolean} - * @private - */ -Blockly.cache3dSupported_ = null; - /** * Add a CSS class to a element. * Similar to Closure's goog.dom.classes.add, except it handles SVG elements. diff --git a/core/variable_map.js b/core/variable_map.js index 2cb16b71..522516db 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -28,7 +28,6 @@ goog.provide('Blockly.VariableMap'); goog.require('Blockly.VariableModel'); - /** * Class for a variable map. This contains a dictionary data structure with * variable types as keys and lists of variables as values. The list of diff --git a/core/variable_model.js b/core/variable_model.js index 1a1f4fbb..b5ec897a 100644 --- a/core/variable_model.js +++ b/core/variable_model.js @@ -26,6 +26,9 @@ goog.provide('Blockly.VariableModel'); +goog.require('goog.string'); + + /** * Class for a variable model. * Holds information for the variable including name, id, and type. @@ -73,3 +76,15 @@ Blockly.VariableModel = function(name, opt_type, opt_id) { Blockly.VariableModel.prototype.getId = function() { return this.id_; }; + +/** + * A custom compare function for the VariableModel objects. + * @param {Blockly.VariableModel} var1 First variable to compare. + * @param {Blockly.VariableModel} var2 Second variable to compare. + * @return {number} -1 if name of var1 is less than name of var2, 0 if equal, + * and 1 if greater. + * @package + */ +Blockly.VariableModel.compareByName = function(var1, var2) { + return goog.string.caseInsensitiveCompare(var1.name, var2.name); +}; diff --git a/core/variables.js b/core/variables.js index 679777a5..a8d89481 100644 --- a/core/variables.js +++ b/core/variables.js @@ -32,6 +32,7 @@ goog.provide('Blockly.Variables'); goog.require('Blockly.Blocks'); goog.require('Blockly.constants'); +goog.require('Blockly.VariableModel'); goog.require('Blockly.Workspace'); goog.require('goog.string'); @@ -109,12 +110,8 @@ Blockly.Variables.allVariables = function(root) { * @return {!Array.<!Element>} Array of XML block elements. */ Blockly.Variables.flyoutCategory = function(workspace) { - var variableNameList = []; var variableModelList = workspace.getVariablesOfType(''); - for (var i = 0; i < variableModelList.length; i++) { - variableNameList.push(variableModelList[i].name); - } - variableNameList.sort(goog.string.caseInsensitiveCompare); + variableModelList.sort(Blockly.VariableModel.compareByName); var xmlList = []; var button = goog.dom.createDom('button'); @@ -127,17 +124,19 @@ Blockly.Variables.flyoutCategory = function(workspace) { xmlList.push(button); - for (var i = 0; i < variableNameList.length; i++) { + for (var i = 0; i < variableModelList.length; i++) { if (Blockly.Blocks['data_variable']) { // <block type="data_variable"> - // <field name="VARIABLE">variablename</field> + // <field name="VARIABLE" variableType="" id="">variablename</field> // </block> var block = goog.dom.createDom('block'); block.setAttribute('type', 'data_variable'); block.setAttribute('gap', 8); - var field = goog.dom.createDom('field', null, variableNameList[i]); + var field = goog.dom.createDom('field', null, variableModelList[i].name); field.setAttribute('name', 'VARIABLE'); + field.setAttribute('variableType', variableModelList[i].type); + field.setAttribute('id', variableModelList[i].getId()); block.appendChild(field); xmlList.push(block); @@ -161,7 +160,7 @@ Blockly.Variables.flyoutCategory = function(workspace) { var block = goog.dom.createDom('block'); block.setAttribute('type', 'data_setvariableto'); block.setAttribute('gap', 8); - block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0])); + block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0])); block.appendChild(Blockly.Variables.createTextDom_()); xmlList.push(block); } @@ -179,7 +178,7 @@ Blockly.Variables.flyoutCategory = function(workspace) { var block = goog.dom.createDom('block'); block.setAttribute('type', 'data_changevariableby'); block.setAttribute('gap', 8); - block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0])); + block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0])); block.appendChild(Blockly.Variables.createMathNumberDom_()); xmlList.push(block); } @@ -192,7 +191,7 @@ Blockly.Variables.flyoutCategory = function(workspace) { var block = goog.dom.createDom('block'); block.setAttribute('type', 'data_showvariable'); block.setAttribute('gap', 8); - block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0])); + block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0])); xmlList.push(block); } if (Blockly.Blocks['data_hidevariable']) { @@ -203,7 +202,7 @@ Blockly.Variables.flyoutCategory = function(workspace) { // </block> var block = goog.dom.createDom('block'); block.setAttribute('type', 'data_hidevariable'); - block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0])); + block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0])); xmlList.push(block); } } @@ -235,10 +234,10 @@ Blockly.Variables.createShadowDom_ = function(type) { /** * Create a dom element for value tag with a shadow variable inside. - * @param {string} name The name of the variable to select. + * @param {Blockly.VariableModel} variableModel The variable to use. * @return {!Element} An XML element. */ -Blockly.Variables.createVariableDom_ = function(name) { +Blockly.Variables.createVariableDom_ = function(variableModel) { // <value name="VARIABLE"> // <shadow type="data_variablemenu"> // <field name="VARIABLE">variablename @@ -247,8 +246,10 @@ Blockly.Variables.createVariableDom_ = function(name) { // </value> var value = Blockly.Variables.createValueDom_('VARIABLE'); var shadow = Blockly.Variables.createShadowDom_('data_variablemenu'); - var field = goog.dom.createDom('field', null, name); + var field = goog.dom.createDom('field', null, variableModel.name); field.setAttribute('name', 'VARIABLE'); + field.setAttribute('variableType', variableModel.type); + field.setAttribute('id', variableModel.getId()); shadow.appendChild(field); value.appendChild(shadow); return value; diff --git a/core/workspace.js b/core/workspace.js index 39b1d7ec..f9403581 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -229,7 +229,7 @@ Blockly.Workspace.prototype.updateVariableStore = function(clear) { var tempVar = this.getVariable(name); if (tempVar) { varList.push({'name': tempVar.name, 'type': tempVar.type, - 'id': tempVar.getId()}); + 'id': tempVar.getId()}); } else { varList.push({'name': name, 'type': null, 'id': null}); @@ -415,6 +415,7 @@ Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable) { uses[i].dispose(true, false); } Blockly.Events.setGroup(false); + this.variableMap_.deleteVariable(variable); }; @@ -564,23 +565,6 @@ Blockly.Workspace.prototype.getBlockById = function(id) { return block || null; }; -/** - * Checks whether all value and statement inputs in the workspace are filled - * with blocks. - * @param {boolean=} opt_shadowBlocksAreFilled An optional argument controlling - * whether shadow blocks are counted as filled. Defaults to true. - * @return {boolean} True if all inputs are filled, false otherwise. - */ -Blockly.Workspace.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) { - var blocks = this.getTopBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { - if (!block.allInputsFilled(opt_shadowBlocksAreFilled)) { - return false; - } - } - return true; -}; - /** * Getter for the flyout associated with this workspace. This is null in a * non-rendered workspace, but may be overriden by subclasses. diff --git a/core/workspace_audio.js b/core/workspace_audio.js new file mode 100644 index 00000000..8b746a36 --- /dev/null +++ b/core/workspace_audio.js @@ -0,0 +1,154 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2017 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 Object in charge of loading, storing, and playing audio for a + * workspace. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.WorkspaceAudio'); + +/** + * Class for loading, storing, and playing audio for a workspace. + * @param {Blockly.WorkspaceSvg} parentWorkspace The parent of the workspace + * this audio object belongs to, or null. + */ +Blockly.WorkspaceAudio = function(parentWorkspace) { + + /** + * The parent of the workspace this object belongs to, or null. May be + * checked for sounds that this object can't find. + * @type {Blockly.WorkspaceSvg} + * @private + */ + this.parentWorkspace_ = parentWorkspace; + + /** + * Database of pre-loaded sounds. + * @private + * @const + */ + this.SOUNDS_ = Object.create(null); +}; + +/** + * Time that the last sound was played. + * @type {Date} + * @private + */ +Blockly.WorkspaceAudio.prototype.lastSound_ = null; + +/** + * Dispose of this audio manager. + * @package + */ +Blockly.WorkspaceAudio.prototype.dispose = function() { + this.parentWorkspace_ = null; + this.SOUNDS_ = null; +}; + +/** + * Load an audio file. Cache it, ready for instantaneous playing. + * @param {!Array.<string>} filenames List of file types in decreasing order of + * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] + * Filenames include path from Blockly's root. File extensions matter. + * @param {string} name Name of sound. + * @package + */ +Blockly.WorkspaceAudio.prototype.load = function(filenames, name) { + if (!filenames.length) { + return; + } + try { + var audioTest = new window['Audio'](); + } catch (e) { + // No browser support for Audio. + // IE can throw an error even if the Audio object exists. + return; + } + var sound; + for (var i = 0; i < filenames.length; i++) { + var filename = filenames[i]; + var ext = filename.match(/\.(\w+)$/); + if (ext && audioTest.canPlayType('audio/' + ext[1])) { + // Found an audio format we can play. + sound = new window['Audio'](filename); + break; + } + } + if (sound && sound.play) { + this.SOUNDS_[name] = sound; + } +}; + +/** + * Preload all the audio files so that they play quickly when asked for. + * @package + */ +Blockly.WorkspaceAudio.prototype.preload = function() { + for (var name in this.SOUNDS_) { + var sound = this.SOUNDS_[name]; + sound.volume = .01; + sound.play(); + sound.pause(); + // iOS can only process one sound at a time. Trying to load more than one + // corrupts the earlier ones. Just load one and leave the others uncached. + if (goog.userAgent.IPAD || goog.userAgent.IPHONE) { + break; + } + } +}; + +/** + * Play a named sound at specified volume. If volume is not specified, + * use full volume (1). + * @param {string} name Name of sound. + * @param {number=} opt_volume Volume of sound (0-1). + */ +Blockly.WorkspaceAudio.prototype.play = function(name, opt_volume) { + var sound = this.SOUNDS_[name]; + if (sound) { + // Don't play one sound on top of another. + var now = new Date; + if (this.lastSound_ != null && + now - this.lastSound_ < Blockly.SOUND_LIMIT) { + return; + } + this.lastSound_ = now; + var mySound; + var ie9 = goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE === 9; + if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) { + // Creating a new audio node causes lag in IE9, Android and iPad. Android + // and IE9 refetch the file from the server, iPad uses a singleton audio + // node which must be deleted and recreated for each new audio tag. + mySound = sound; + } else { + mySound = sound.cloneNode(); + } + mySound.volume = (opt_volume === undefined ? 1 : opt_volume); + mySound.play(); + } else if (this.parentWorkspace_) { + // Maybe a workspace on a lower level knows about this sound. + this.parentWorkspace_.getAudioManager().play(name, opt_volume); + } +}; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index c1cc7a90..6d9e782d 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -33,14 +33,15 @@ goog.require('Blockly.ConnectionDB'); goog.require('Blockly.constants'); goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Events'); -//goog.require('Blockly.HorizontalFlyout'); goog.require('Blockly.Gesture'); +goog.require('Blockly.Grid'); goog.require('Blockly.Options'); goog.require('Blockly.ScrollbarPair'); goog.require('Blockly.Touch'); goog.require('Blockly.Trashcan'); //goog.require('Blockly.VerticalFlyout'); goog.require('Blockly.Workspace'); +goog.require('Blockly.WorkspaceAudio'); goog.require('Blockly.WorkspaceDragSurfaceSvg'); goog.require('Blockly.Xml'); goog.require('Blockly.ZoomControls'); @@ -82,12 +83,6 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface this.useWorkspaceDragSurface_ = this.workspaceDragSurface_ && Blockly.utils.is3dSupported(); - /** - * Database of pre-loaded sounds. - * @private - * @const - */ - this.SOUNDS_ = Object.create(null); /** * List of currently highlighted blocks. Block highlighting is often used to * visually mark blocks currently being executed. @@ -96,6 +91,21 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface */ this.highlightedBlocks_ = []; + /** + * Object in charge of loading, storing, and playing audio for a workspace. + * @type {Blockly.WorkspaceAudio} + * @private + */ + this.audioManager_ = new Blockly.WorkspaceAudio(options.parentWorkspace); + + /** + * This workspace's grid object or null. + * @type {Blockly.Grid} + * @private + */ + this.grid_ = this.options.gridPattern ? + new Blockly.Grid(options.gridPattern, options.gridOptions) : null; + this.registerToolboxCategoryCallback(Blockly.VARIABLE_CATEGORY_NAME, Blockly.Variables.flyoutCategory); this.registerToolboxCategoryCallback(Blockly.PROCEDURE_CATEGORY_NAME, @@ -227,13 +237,6 @@ Blockly.WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false; */ Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false; -/** - * Time that the last sound was played. - * @type {Date} - * @private - */ -Blockly.WorkspaceSvg.prototype.lastSound_ = null; - /** * Last known position of the page scroll. * This is used to determine whether we have recalculated screen coordinate @@ -352,9 +355,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { {'height': '100%', 'width': '100%', 'class': opt_backgroundClass}, this.svgGroup_); - if (opt_backgroundClass == 'blocklyMainBackground') { + if (opt_backgroundClass == 'blocklyMainBackground' && this.grid_) { this.svgBackground_.style.fill = - 'url(#' + this.options.gridPattern.id + ')'; + 'url(#' + this.grid_.getPatternId() + ')'; } } /** @type {SVGElement} */ @@ -390,8 +393,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { */ this.toolbox_ = new Blockly.Toolbox(this); } - this.updateGridPattern_(); - this.updateStackGlowScale_(); + if (this.grid_) { + this.grid_.update(this.scale); + } this.recordDeleteAreas(); return this.svgGroup_; }; @@ -434,6 +438,16 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.zoomControls_ = null; } + if (this.audioManager_) { + this.audioManager_.dispose(); + this.audioManager_ = null; + } + + if (this.grid_) { + this.grid_.dispose(); + this.grid_ = null; + } + if (this.toolboxCategoryCallbacks_) { this.toolboxCategoryCallbacks_ = null; } @@ -966,13 +980,15 @@ Blockly.WorkspaceSvg.prototype.renameVariable = function(oldName, newName) { * variable immediately. * TODO: #468 * @param {string} name The new variable's name. + * @return {?Blockly.VariableModel} The newly created variable. */ Blockly.WorkspaceSvg.prototype.createVariable = function(name) { - Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name); + var newVar = Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name); // Don't refresh the toolbox if there's a drag in progress. if (this.toolbox_ && this.toolbox_.flyout_ && !this.currentGesture_) { this.toolbox_.refreshSelection(); } + return newVar; }; /** @@ -1308,96 +1324,6 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) { Blockly.ContextMenu.show(e, menuOptions, this.RTL); }; -/** - * Load an audio file. Cache it, ready for instantaneous playing. - * @param {!Array.<string>} filenames List of file types in decreasing order of - * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] - * Filenames include path from Blockly's root. File extensions matter. - * @param {string} name Name of sound. - * @private - */ -Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) { - if (!filenames.length) { - return; - } - try { - var audioTest = new window['Audio'](); - } catch(e) { - // No browser support for Audio. - // IE can throw an error even if the Audio object exists. - return; - } - var sound; - for (var i = 0; i < filenames.length; i++) { - var filename = filenames[i]; - var ext = filename.match(/\.(\w+)$/); - if (ext && audioTest.canPlayType('audio/' + ext[1])) { - // Found an audio format we can play. - sound = new window['Audio'](filename); - break; - } - } - if (sound && sound.play) { - this.SOUNDS_[name] = sound; - } -}; - -/** - * Preload all the audio files so that they play quickly when asked for. - * @private - */ -Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() { - for (var name in this.SOUNDS_) { - var sound = this.SOUNDS_[name]; - sound.volume = .01; - sound.play(); - sound.pause(); - // iOS can only process one sound at a time. Trying to load more than one - // corrupts the earlier ones. Just load one and leave the others uncached. - if (goog.userAgent.IPAD || goog.userAgent.IPHONE) { - break; - } - } -}; - -/** - * Play a named sound at specified volume. If volume is not specified, - * use full volume (1). - * @param {string} name Name of sound. - * @param {number=} opt_volume Volume of sound (0-1). - */ -Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) { - // Send a UI event in case we wish to play the sound externally - var event = new Blockly.Events.Ui(null, 'sound', null, name); - event.workspaceId = this.id; - Blockly.Events.fire(event); - var sound = this.SOUNDS_[name]; - if (sound) { - // Don't play one sound on top of another. - var now = new Date; - if (now - this.lastSound_ < Blockly.SOUND_LIMIT) { - return; - } - this.lastSound_ = now; - var mySound; - var ie9 = goog.userAgent.DOCUMENT_MODE && - goog.userAgent.DOCUMENT_MODE === 9; - if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) { - // Creating a new audio node causes lag in IE9, Android and iPad. Android - // and IE9 refetch the file from the server, iPad uses a singleton audio - // node which must be deleted and recreated for each new audio tag. - mySound = sound; - } else { - mySound = sound.cloneNode(); - } - mySound.volume = (opt_volume === undefined ? 1 : opt_volume); - mySound.play(); - } else if (this.options.parentWorkspace) { - // Maybe a workspace on a lower level knows about this sound. - this.options.parentWorkspace.playAudio(name, opt_volume); - } -}; - /** * Modify the block tree on the existing toolbox. * @param {Node|string} tree DOM tree of blocks, or text representation of same. @@ -1588,11 +1514,9 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) { newScale = this.options.zoomOptions.minScale; } this.scale = newScale; - this.updateStackGlowScale_(); - this.updateGridPattern_(); - // Hide the WidgetDiv without animation (zoom makes field out of place with div) - Blockly.WidgetDiv.hide(true); - Blockly.DropDownDiv.hideWithoutAnimation(); + if (this.grid_) { + this.grid_.update(this.scale); + } if (this.scrollbar) { this.scrollbar.resize(); } else { @@ -1628,42 +1552,6 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { -y - metrics.contentTop); }; -/** - * Updates the grid pattern. - * @private - */ -Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() { - if (!this.options.gridPattern) { - return; // No grid. - } - // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100. - var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100; - this.options.gridPattern.setAttribute('width', safeSpacing); - this.options.gridPattern.setAttribute('height', safeSpacing); - var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5; - var start = half - this.options.gridOptions['length'] / 2; - var end = half + this.options.gridOptions['length'] / 2; - var line1 = this.options.gridPattern.firstChild; - var line2 = line1 && line1.nextSibling; - half *= this.scale; - start *= this.scale; - end *= this.scale; - if (line1) { - line1.setAttribute('stroke-width', this.scale); - line1.setAttribute('x1', start); - line1.setAttribute('y1', half); - line1.setAttribute('x2', end); - line1.setAttribute('y2', half); - } - if (line2) { - line2.setAttribute('stroke-width', this.scale); - line2.setAttribute('x1', half); - line2.setAttribute('y1', start); - line2.setAttribute('x2', half); - line2.setAttribute('y2', end); - } -}; - /** * Update the workspace's stack glow radius to be proportional to scale. * Ensures that stack glows always appear to be a fixed size. @@ -1792,14 +1680,8 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) { var x = this.scrollX + metrics.absoluteLeft; var y = this.scrollY + metrics.absoluteTop; this.translate(x, y); - if (this.options.gridPattern) { - this.options.gridPattern.setAttribute('x', x); - this.options.gridPattern.setAttribute('y', y); - if (goog.userAgent.IE || goog.userAgent.EDGE) { - // IE/Edge doesn't notice that the x/y offsets have changed. - // Force an update. - this.updateGridPattern_(); - } + if (this.grid_) { + this.grid_.moveTo(x, y); } }; @@ -1952,6 +1834,24 @@ Blockly.WorkspaceSvg.prototype.cancelCurrentGesture = function() { } }; +/** + * Get the audio manager for this workspace. + * @return {Blockly.WorkspaceAudio} The audio manager for this workspace. + * @package + */ +Blockly.WorkspaceSvg.prototype.getAudioManager = function() { + return this.audioManager_; +}; + +/** + * Get the grid object for this workspace, or null if there is none. + * @return {Blockly.Grid} The grid object for this workspace. + * @package + */ +Blockly.WorkspaceSvg.prototype.getGrid = function() { + return this.grid_; +}; + // Export symbols that would otherwise be renamed by Closure compiler. Blockly.WorkspaceSvg.prototype['setVisible'] = Blockly.WorkspaceSvg.prototype.setVisible; diff --git a/core/xml.js b/core/xml.js index 7bf91628..48225d30 100644 --- a/core/xml.js +++ b/core/xml.js @@ -43,6 +43,7 @@ goog.require('goog.userAgent'); */ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { var xml = goog.dom.createDom('xml'); + xml.appendChild(Blockly.Xml.variablesToDom(workspace.getAllVariables())); var blocks = workspace.getTopBlocks(true); for (var i = 0, block; block = blocks[i]; i++) { xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId)); @@ -50,6 +51,23 @@ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { return xml; }; +/** + * Encode a list of variables as XML. + * @param {!Array.<!Blockly.VariableModel>} variableList List of all variable + * models. + * @return {!Element} List of XML elements. + */ +Blockly.Xml.variablesToDom = function(variableList) { + var variables = goog.dom.createDom('variables'); + for (var i = 0, variable; variable = variableList[i]; i++) { + var element = goog.dom.createDom('variable', null, variable.name); + element.setAttribute('type', variable.type); + element.setAttribute('id', variable.getId()); + variables.appendChild(element); + } + return variables; +}; + /** * Encode a block subtree as XML with XY coordinates. * @param {!Blockly.Block} block The root block to encode. @@ -92,6 +110,13 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { if (field.name && field.EDITABLE) { var container = goog.dom.createDom('field', null, field.getValue()); container.setAttribute('name', field.name); + if (field instanceof Blockly.FieldVariable) { + var variable = block.workspace.getVariable(field.getValue()); + if (variable) { + container.setAttribute('id', variable.getId()); + container.setAttribute('variableType', variable.type); + } + } element.appendChild(container); } } @@ -325,6 +350,14 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { } } else if (name == 'shadow') { goog.asserts.fail('Shadow block cannot be a top-level block.'); + } else if (name == 'variables') { + if (i == 1) { + Blockly.Xml.domToVariables(xmlChild, workspace); + } + else { + throw Error('\'variables\' tag must be the first element in the' + + 'workspace XML, but it was found in another location.'); + } } } if (!existingGroup) { @@ -463,6 +496,25 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { return topBlock; }; +/** + * Decode an XML list of variables and add the variables to the workspace. + * @param {!Element} xmlVariables List of XML variable elements. + * @param {!Blockly.Workspace} workspace The workspace to which the variable + * should be added. + */ +Blockly.Xml.domToVariables = function(xmlVariables, workspace) { + for (var i = 0, xmlChild; xmlChild = xmlVariables.children[i]; i++) { + var type = xmlChild.getAttribute('type'); + var id = xmlChild.getAttribute('id'); + var name = xmlChild.textContent; + + if (typeof(type) === undefined || type === null) { + throw Error('Variable with id, ' + id + ' is without a type'); + } + workspace.createVariable(name, type, id); + } +}; + /** * Decode an XML block tag and create a block (and possibly sub blocks) on the * workspace. @@ -544,12 +596,32 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { // Fall through. case 'field': var field = block.getField(name); + var text = xmlChild.textContent; + if (field instanceof Blockly.FieldVariable) { + // TODO (marisaleung): When we change setValue and getValue to + // interact with id's instead of names, update this so that we get + // the variable based on id instead of textContent. + var type = xmlChild.getAttribute('variabletype') || ''; + var variable = workspace.getVariable(text); + if (!variable) { + variable = workspace.createVariable(text, type, + xmlChild.getAttribute(id)); + } + if (typeof(type) !== undefined && type !== null) { + if (type !== variable.type) { + throw Error('Serialized variable type with id \'' + + variable.getId() + '\' had type ' + variable.type + ', and ' + + 'does not match variable field that references it: ' + + Blockly.Xml.domToText(xmlChild) + '.'); + } + } + } if (!field) { console.warn('Ignoring non-existent field ' + name + ' in block ' + prototypeName); break; } - field.setValue(xmlChild.textContent); + field.setValue(text); break; case 'value': case 'statement': @@ -629,9 +701,6 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { goog.asserts.assert(child.isShadow(), 'Shadow block not allowed non-shadow child.'); } - // Ensure this block doesn't have any variable inputs. - goog.asserts.assert(block.getVars().length == 0, - 'Shadow blocks cannot have variable fields.'); block.setShadow(true); } return block; diff --git a/dart_compressed.js b/dart_compressed.js index 3ab9772e..bd2a74c9 100644 --- a/dart_compressed.js +++ b/dart_compressed.js @@ -6,7 +6,7 @@ Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert,break,case,catch,class,const,continue,default,do,else,enum,extends,false,final,finally,for,if,in,is,new,null,rethrow,return,super,switch,this,throw,true,try,var,void,while,with,print,identityHashCode,identical,BidirectionalIterator,Comparable,double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,ArgumentError,AssertionError,CastError,ConcurrentModificationError,CyclicInitializationError,Error,Exception,FallThroughError,FormatException,IntegerDivisionByZeroException,NoSuchMethodError,NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,StateError,TypeError,UnimplementedError,UnsupportedError"); Blockly.Dart.ORDER_ATOMIC=0;Blockly.Dart.ORDER_UNARY_POSTFIX=1;Blockly.Dart.ORDER_UNARY_PREFIX=2;Blockly.Dart.ORDER_MULTIPLICATIVE=3;Blockly.Dart.ORDER_ADDITIVE=4;Blockly.Dart.ORDER_SHIFT=5;Blockly.Dart.ORDER_BITWISE_AND=6;Blockly.Dart.ORDER_BITWISE_XOR=7;Blockly.Dart.ORDER_BITWISE_OR=8;Blockly.Dart.ORDER_RELATIONAL=9;Blockly.Dart.ORDER_EQUALITY=10;Blockly.Dart.ORDER_LOGICAL_AND=11;Blockly.Dart.ORDER_LOGICAL_OR=12;Blockly.Dart.ORDER_IF_NULL=13;Blockly.Dart.ORDER_CONDITIONAL=14; Blockly.Dart.ORDER_CASCADE=15;Blockly.Dart.ORDER_ASSIGNMENT=16;Blockly.Dart.ORDER_NONE=99; -Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var e=[];a=a.variableList;if(a.length){for(var b=0;b<a.length;b++)e[b]=Blockly.Dart.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE);Blockly.Dart.definitions_.variables="var "+e.join(", ")+";"}}; +Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var e=[];a=a.getAllVariables();if(a.length){for(var b=0;b<a.length;b++)e[b]=Blockly.Dart.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE);Blockly.Dart.definitions_.variables="var "+e.join(", ")+";"}}; Blockly.Dart.finish=function(a){a&&(a=Blockly.Dart.prefixLines(a,Blockly.Dart.INDENT));a="main() {\n"+a+"}";var e=[],b=[],d;for(d in Blockly.Dart.definitions_){var c=Blockly.Dart.definitions_[d];c.match(/^import\s/)?e.push(c):b.push(c)}delete Blockly.Dart.definitions_;delete Blockly.Dart.functionNames_;Blockly.Dart.variableDB_.reset();return(e.join("\n")+"\n\n"+b.join("\n\n")).replace(/\n\n+/g,"\n\n").replace(/\n*$/,"\n\n\n")+a};Blockly.Dart.scrubNakedValue=function(a){return a+";\n"}; Blockly.Dart.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/\$/g,"\\$").replace(/'/g,"\\'");return"'"+a+"'"}; Blockly.Dart.scrub_=function(a,e){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var d=a.getCommentText();(d=Blockly.utils.wrap(d,Blockly.Dart.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+Blockly.Dart.prefixLines(d+"\n","/// "):b+Blockly.Dart.prefixLines(d+"\n","// "));for(var c=0;c<a.inputList.length;c++)a.inputList[c].type==Blockly.INPUT_VALUE&&(d=a.inputList[c].connection.targetBlock())&&(d=Blockly.Dart.allNestedComments(d))&&(b+=Blockly.Dart.prefixLines(d,"// "))}c=a.nextConnection&& diff --git a/generators/dart.js b/generators/dart.js index 295d9002..83ef76cd 100644 --- a/generators/dart.js +++ b/generators/dart.js @@ -106,7 +106,7 @@ Blockly.Dart.init = function(workspace) { var variables = workspace.getAllVariables(); if (variables.length) { for (var i = 0; i < variables.length; i++) { - defvars[i] = Blockly.Dart.variableDB_.getName(variables[i], + defvars[i] = Blockly.Dart.variableDB_.getName(variables[i].name, Blockly.Variables.NAME_TYPE); } Blockly.Dart.definitions_['variables'] = diff --git a/generators/javascript.js b/generators/javascript.js index b641d2dc..00b96bcc 100644 --- a/generators/javascript.js +++ b/generators/javascript.js @@ -156,7 +156,7 @@ Blockly.JavaScript.init = function(workspace) { var variables = workspace.getAllVariables(); if (variables.length) { for (var i = 0; i < variables.length; i++) { - defvars[i] = Blockly.JavaScript.variableDB_.getName(variables[i], + defvars[i] = Blockly.JavaScript.variableDB_.getName(variables[i].name, Blockly.Variables.NAME_TYPE); } Blockly.JavaScript.definitions_['variables'] = diff --git a/generators/python.js b/generators/python.js index 3babbde6..e81a0423 100644 --- a/generators/python.js +++ b/generators/python.js @@ -163,7 +163,7 @@ Blockly.Python.init = function(workspace) { var defvars = []; var variables = workspace.getAllVariables(); for (var i = 0; i < variables.length; i++) { - defvars[i] = Blockly.Python.variableDB_.getName(variables[i], + defvars[i] = Blockly.Python.variableDB_.getName(variables[i].name, Blockly.Variables.NAME_TYPE) + ' = None'; } Blockly.Python.definitions_['variables'] = defvars.join('\n'); diff --git a/javascript_compressed.js b/javascript_compressed.js index 46a840b1..729ff4f8 100644 --- a/javascript_compressed.js +++ b/javascript_compressed.js @@ -9,7 +9,7 @@ Blockly.JavaScript.ORDER_DIVISION=5.1;Blockly.JavaScript.ORDER_MULTIPLICATION=5. Blockly.JavaScript.ORDER_LOGICAL_AND=13;Blockly.JavaScript.ORDER_LOGICAL_OR=14;Blockly.JavaScript.ORDER_CONDITIONAL=15;Blockly.JavaScript.ORDER_ASSIGNMENT=16;Blockly.JavaScript.ORDER_COMMA=17;Blockly.JavaScript.ORDER_NONE=99; Blockly.JavaScript.ORDER_OVERRIDES=[[Blockly.JavaScript.ORDER_FUNCTION_CALL,Blockly.JavaScript.ORDER_MEMBER],[Blockly.JavaScript.ORDER_FUNCTION_CALL,Blockly.JavaScript.ORDER_FUNCTION_CALL],[Blockly.JavaScript.ORDER_MEMBER,Blockly.JavaScript.ORDER_MEMBER],[Blockly.JavaScript.ORDER_MEMBER,Blockly.JavaScript.ORDER_FUNCTION_CALL],[Blockly.JavaScript.ORDER_LOGICAL_NOT,Blockly.JavaScript.ORDER_LOGICAL_NOT],[Blockly.JavaScript.ORDER_MULTIPLICATION,Blockly.JavaScript.ORDER_MULTIPLICATION],[Blockly.JavaScript.ORDER_ADDITION, Blockly.JavaScript.ORDER_ADDITION],[Blockly.JavaScript.ORDER_LOGICAL_AND,Blockly.JavaScript.ORDER_LOGICAL_AND],[Blockly.JavaScript.ORDER_LOGICAL_OR,Blockly.JavaScript.ORDER_LOGICAL_OR]]; -Blockly.JavaScript.init=function(a){Blockly.JavaScript.definitions_=Object.create(null);Blockly.JavaScript.functionNames_=Object.create(null);Blockly.JavaScript.variableDB_?Blockly.JavaScript.variableDB_.reset():Blockly.JavaScript.variableDB_=new Blockly.Names(Blockly.JavaScript.RESERVED_WORDS_);var d=[];a=a.variableList;if(a.length){for(var b=0;b<a.length;b++)d[b]=Blockly.JavaScript.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE);Blockly.JavaScript.definitions_.variables="var "+d.join(", ")+ +Blockly.JavaScript.init=function(a){Blockly.JavaScript.definitions_=Object.create(null);Blockly.JavaScript.functionNames_=Object.create(null);Blockly.JavaScript.variableDB_?Blockly.JavaScript.variableDB_.reset():Blockly.JavaScript.variableDB_=new Blockly.Names(Blockly.JavaScript.RESERVED_WORDS_);var d=[];a=a.getAllVariables();if(a.length){for(var b=0;b<a.length;b++)d[b]=Blockly.JavaScript.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE);Blockly.JavaScript.definitions_.variables="var "+d.join(", ")+ ";"}};Blockly.JavaScript.finish=function(a){var d=[],b;for(b in Blockly.JavaScript.definitions_)d.push(Blockly.JavaScript.definitions_[b]);delete Blockly.JavaScript.definitions_;delete Blockly.JavaScript.functionNames_;Blockly.JavaScript.variableDB_.reset();return d.join("\n\n")+"\n\n\n"+a};Blockly.JavaScript.scrubNakedValue=function(a){return a+";\n"};Blockly.JavaScript.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/'/g,"\\'");return"'"+a+"'"}; Blockly.JavaScript.scrub_=function(a,d){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var e=a.getCommentText();(e=Blockly.utils.wrap(e,Blockly.JavaScript.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+("/**\n"+Blockly.JavaScript.prefixLines(e+"\n"," * ")+" */\n"):b+Blockly.JavaScript.prefixLines(e+"\n","// "));for(var c=0;c<a.inputList.length;c++)a.inputList[c].type==Blockly.INPUT_VALUE&&(e=a.inputList[c].connection.targetBlock())&&(e=Blockly.JavaScript.allNestedComments(e))&&(b+= Blockly.JavaScript.prefixLines(e,"// "))}c=a.nextConnection&&a.nextConnection.targetBlock();c=Blockly.JavaScript.blockToCode(c);return b+d+c}; diff --git a/media/click.mp3 b/media/click.mp3 new file mode 100644 index 00000000..0c1b05cd Binary files /dev/null and b/media/click.mp3 differ diff --git a/media/click.ogg b/media/click.ogg new file mode 100644 index 00000000..37535b86 Binary files /dev/null and b/media/click.ogg differ diff --git a/media/delete.mp3 b/media/delete.mp3 new file mode 100644 index 00000000..a937a381 Binary files /dev/null and b/media/delete.mp3 differ diff --git a/media/delete.ogg b/media/delete.ogg new file mode 100644 index 00000000..e123af6b Binary files /dev/null and b/media/delete.ogg differ diff --git a/msg/js/en.js b/msg/js/en.js index 656f6fd9..f4c4fc20 100644 --- a/msg/js/en.js +++ b/msg/js/en.js @@ -310,6 +310,7 @@ Blockly.Msg.PROCEDURES_MUTATORARG_TITLE = "input name:"; Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP = "Add an input to the function."; Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE = "inputs"; Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP = "Add, remove, or reorder inputs to this function."; +Blockly.Msg.PROCEDURE_ALREADY_EXISTS = "A procedure named '%1' already exists."; Blockly.Msg.REDO = "Redo"; Blockly.Msg.REMOVE_COMMENT = "Remove Comment"; Blockly.Msg.RENAME_VARIABLE = "Rename variable..."; diff --git a/msg/json/constants.json b/msg/json/constants.json new file mode 100644 index 00000000..5b501035 --- /dev/null +++ b/msg/json/constants.json @@ -0,0 +1 @@ +{"MATH_HUE": "230", "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", "VARIABLES_HUE": "330", "TEXTS_HUE": "160", "PROCEDURES_HUE": "290", "COLOUR_HUE": "20"} \ No newline at end of file diff --git a/msg/json/en.json b/msg/json/en.json index 7581b38b..bcb80266 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus <ellen.spertus@gmail.com>", - "lastupdated": "2017-04-04 17:16:42.193032", + "lastupdated": "2017-05-25 07:58:37.810709", "locale": "en", "messagedocumentation" : "qqq" }, @@ -31,6 +31,7 @@ "NEW_VARIABLE": "Create variable...", "NEW_VARIABLE_TITLE": "New variable name:", "VARIABLE_ALREADY_EXISTS": "A variable named '%1' already exists.", + "PROCEDURE_ALREADY_EXISTS": "A procedure named '%1' already exists.", "DELETE_VARIABLE_CONFIRMATION": "Delete %1 uses of the '%2' variable?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Can't delete the variable '%1' because it's part of the definition of the function '%2'", "DELETE_VARIABLE": "Delete the '%1' variable", diff --git a/python_compressed.js b/python_compressed.js index 41bacd54..459dea12 100644 --- a/python_compressed.js +++ b/python_compressed.js @@ -7,7 +7,7 @@ Blockly.Python=new Blockly.Generator("Python");Blockly.Python.addReservedWords(" Blockly.Python.ORDER_ATOMIC=0;Blockly.Python.ORDER_COLLECTION=1;Blockly.Python.ORDER_STRING_CONVERSION=1;Blockly.Python.ORDER_MEMBER=2.1;Blockly.Python.ORDER_FUNCTION_CALL=2.2;Blockly.Python.ORDER_EXPONENTIATION=3;Blockly.Python.ORDER_UNARY_SIGN=4;Blockly.Python.ORDER_BITWISE_NOT=4;Blockly.Python.ORDER_MULTIPLICATIVE=5;Blockly.Python.ORDER_ADDITIVE=6;Blockly.Python.ORDER_BITWISE_SHIFT=7;Blockly.Python.ORDER_BITWISE_AND=8;Blockly.Python.ORDER_BITWISE_XOR=9;Blockly.Python.ORDER_BITWISE_OR=10; Blockly.Python.ORDER_RELATIONAL=11;Blockly.Python.ORDER_LOGICAL_NOT=12;Blockly.Python.ORDER_LOGICAL_AND=13;Blockly.Python.ORDER_LOGICAL_OR=14;Blockly.Python.ORDER_CONDITIONAL=15;Blockly.Python.ORDER_LAMBDA=16;Blockly.Python.ORDER_NONE=99; Blockly.Python.ORDER_OVERRIDES=[[Blockly.Python.ORDER_FUNCTION_CALL,Blockly.Python.ORDER_MEMBER],[Blockly.Python.ORDER_FUNCTION_CALL,Blockly.Python.ORDER_FUNCTION_CALL],[Blockly.Python.ORDER_MEMBER,Blockly.Python.ORDER_MEMBER],[Blockly.Python.ORDER_MEMBER,Blockly.Python.ORDER_FUNCTION_CALL],[Blockly.Python.ORDER_LOGICAL_NOT,Blockly.Python.ORDER_LOGICAL_NOT],[Blockly.Python.ORDER_LOGICAL_AND,Blockly.Python.ORDER_LOGICAL_AND],[Blockly.Python.ORDER_LOGICAL_OR,Blockly.Python.ORDER_LOGICAL_OR]]; -Blockly.Python.init=function(a){Blockly.Python.PASS=this.INDENT+"pass\n";Blockly.Python.definitions_=Object.create(null);Blockly.Python.functionNames_=Object.create(null);Blockly.Python.variableDB_?Blockly.Python.variableDB_.reset():Blockly.Python.variableDB_=new Blockly.Names(Blockly.Python.RESERVED_WORDS_);var e=[];a=a.variableList;for(var b=0;b<a.length;b++)e[b]=Blockly.Python.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE)+" = None";Blockly.Python.definitions_.variables=e.join("\n")}; +Blockly.Python.init=function(a){Blockly.Python.PASS=this.INDENT+"pass\n";Blockly.Python.definitions_=Object.create(null);Blockly.Python.functionNames_=Object.create(null);Blockly.Python.variableDB_?Blockly.Python.variableDB_.reset():Blockly.Python.variableDB_=new Blockly.Names(Blockly.Python.RESERVED_WORDS_);var e=[];a=a.getAllVariables();for(var b=0;b<a.length;b++)e[b]=Blockly.Python.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE)+" = None";Blockly.Python.definitions_.variables=e.join("\n")}; Blockly.Python.finish=function(a){var e=[],b=[],c;for(c in Blockly.Python.definitions_){var d=Blockly.Python.definitions_[c];d.match(/^(from\s+\S+\s+)?import\s+\S+/)?e.push(d):b.push(d)}delete Blockly.Python.definitions_;delete Blockly.Python.functionNames_;Blockly.Python.variableDB_.reset();return(e.join("\n")+"\n\n"+b.join("\n\n")).replace(/\n\n+/g,"\n\n").replace(/\n*$/,"\n\n\n")+a};Blockly.Python.scrubNakedValue=function(a){return a+"\n"}; Blockly.Python.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/\%/g,"\\%");var e="'";-1!==a.indexOf("'")&&(-1===a.indexOf('"')?e='"':a=a.replace(/'/g,"\\'"));return e+a+e}; Blockly.Python.scrub_=function(a,e){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var c=a.getCommentText();(c=Blockly.utils.wrap(c,Blockly.Python.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+('"""'+c+'\n"""\n'):b+Blockly.Python.prefixLines(c+"\n","# "));for(var d=0;d<a.inputList.length;d++)a.inputList[d].type==Blockly.INPUT_VALUE&&(c=a.inputList[d].connection.targetBlock())&&(c=Blockly.Python.allNestedComments(c))&&(b+=Blockly.Python.prefixLines(c,"# "))}d=a.nextConnection&&a.nextConnection.targetBlock(); diff --git a/scripts/get_chromedriver.sh b/scripts/get_chromedriver.sh index a16854dd..a0fbf3b7 100755 --- a/scripts/get_chromedriver.sh +++ b/scripts/get_chromedriver.sh @@ -5,6 +5,8 @@ if [ ! -d $chromedriver_dir ]; then mkdir $chromedriver_dir fi +echo "downloading chromedriver" + if [[ $os_name == 'Linux' && ! -f $chromedriver_dir/chromedriver ]]; then cd chromedriver && curl -L https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip > tmp.zip && unzip -o tmp.zip && rm tmp.zip # wait until download finish diff --git a/tests/jsunit/workspace_test.js b/tests/jsunit/workspace_test.js index ac2d78f2..bf4618a8 100644 --- a/tests/jsunit/workspace_test.js +++ b/tests/jsunit/workspace_test.js @@ -62,12 +62,12 @@ function workspaceTest_tearDownWithMockBlocks() { /** * Create a test get_var_block. - * @param {?string} variable The string to put into the variable field. + * @param {?string} variable_name The string to put into the variable field. * @return {!Blockly.Block} The created block. */ -function createMockBlock(variable) { +function createMockBlock(variable_name) { var block = new Blockly.Block(workspace, 'get_var_block'); - block.inputList[0].fieldRow[0].setValue(variable); + block.inputList[0].fieldRow[0].setValue(variable_name); return block; } diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js index d77ed934..835ffbd4 100644 --- a/tests/jsunit/xml_test.js +++ b/tests/jsunit/xml_test.js @@ -19,6 +19,12 @@ */ 'use strict'; +goog.require('goog.testing'); +goog.require('goog.testing.MockControl'); + +var mockControl_; +var saved_msg = Blockly.Msg.DELETE_VARIABLE; +var workspace; var XML_TEXT = ['<xml xmlns="http://www.w3.org/1999/xhtml">', ' <block type="controls_repeat_ext" inline="true" x="21" y="23">', ' <value name="TIMES">', @@ -46,6 +52,97 @@ var XML_TEXT = ['<xml xmlns="http://www.w3.org/1999/xhtml">', ' </block>', '</xml>'].join('\n'); +function xmlTest_setUp() { + workspace = new Blockly.Workspace(); + mockControl_ = new goog.testing.MockControl(); +} + +function xmlTest_setUpWithMockBlocks() { + xmlTest_setUp(); + Blockly.defineBlocksWithJsonArray([{ + 'type': 'field_variable_test_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': 'item' + } + ], + }]); + // Need to define this because field_variable's dropdownCreate() calls replace + // on undefined value, Blockly.Msg.DELETE_VARIABLE. To fix this, define + // Blockly.Msg.DELETE_VARIABLE as %1 so the replace function finds the %1 it + // expects. + Blockly.Msg.DELETE_VARIABLE = '%1'; +} + +function xmlTest_tearDown() { + mockControl_.$tearDown(); + workspace.dispose(); +} + +function xmlTest_tearDownWithMockBlocks() { + xmlTest_tearDown(); + delete Blockly.Blocks.field_variable_test_block; + Blockly.Msg.DELETE_VARIABLE = saved_msg; +} + +/** + * Check the values of the non variable field dom. + * @param {!Element} fieldDom The xml dom of the non variable field. + * @param {!string} name The expected name of the variable. + * @param {!string} text The expected text of the variable. + */ +function xmlTest_checkNonVariableField(fieldDom, name, text) { + assertEquals(text, fieldDom.textContent); + assertEquals(name, fieldDom.getAttribute('name')); + assertNull(fieldDom.getAttribute('id')); + assertNull(fieldDom.getAttribute('variableType')); +} + +/** + * Check the values of the variable field DOM. + * @param {!Element} fieldDom The xml dom of the variable field. + * @param {!string} name The expected name of the variable. + * @param {!string} type The expected type of the variable. + * @param {!string} id The expected id of the variable. + * @param {!string} text The expected text of the variable. + */ +function xmlTest_checkVariableFieldDomValues(fieldDom, name, type, id, text) { + assertEquals(name, fieldDom.getAttribute('name')); + assertEquals(type, fieldDom.getAttribute('variableType')); + assertEquals(id, fieldDom.getAttribute('id')); + assertEquals(text, fieldDom.textContent); +} + +/** + * Check the values of the variable DOM. + * @param {!Element} variableDom The xml dom of the variable. + * @param {!string} type The expected type of the variable. + * @param {!string} id The expected id of the variable. + * @param {!string} text The expected text of the variable. + */ +function xmlTest_checkVariableDomValues(variableDom, type, id, text) { + assertEquals(type, variableDom.getAttribute('type')); + assertEquals(id, variableDom.getAttribute('id')); + assertEquals(text, variableDom.textContent); +} + +/** + * Check if a variable with the given values exists. + * @param {!string} name The expected name of the variable. + * @param {!string} type The expected type of the variable. + * @param {!string} id The expected id of the variable. + */ +function xmlTest_checkVariableValues(name, type, id) { + var variable = workspace.getVariable(name); + assertNotUndefined(variable); + assertEquals(name, variable.name); + assertEquals(type, variable.type); + assertEquals(id, variable.getId()); +} + function test_textToDom() { var dom = Blockly.Xml.textToDom(XML_TEXT); assertEquals('XML tag', 'xml', dom.nodeName); @@ -59,7 +156,131 @@ function test_domToText() { text.replace(/\s+/g, '')); } -function test_domToWorkspace() { +function test_domToWorkspace_BackwardCompatibility() { + // Expect that workspace still loads without serialized variables. + xmlTest_setUpWithMockBlocks(); + var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); + mockGenUid().$returns('1'); + mockGenUid().$returns('1'); + mockGenUid().$replay(); + try { + var dom = Blockly.Xml.textToDom( + '<xml>' + + ' <block type="field_variable_test_block" id="block_id">' + + ' <field name="VAR">name1</field>' + + ' </block>' + + '</xml>'); + Blockly.Xml.domToWorkspace(dom, workspace); + assertEquals('Block count', 1, workspace.getAllBlocks().length); + xmlTest_checkVariableValues('name1', '', '1'); + } finally { + xmlTest_tearDownWithMockBlocks(); + } +} + +function test_domToWorkspace_VariablesAtTop() { + // Expect that unused variables are preserved. + xmlTest_setUpWithMockBlocks(); + try { + var dom = Blockly.Xml.textToDom( + '<xml>' + + ' <variables>' + + ' <variable type="type1" id="id1">name1</variable>' + + ' <variable type="type2" id="id2">name2</variable>' + + ' <variable type="" id="id3">name3</variable>' + + ' </variables>' + + ' <block type="field_variable_test_block">' + + ' <field name="VAR" id="id3" variabletype="">name3</field>' + + ' </block>' + + '</xml>'); + Blockly.Xml.domToWorkspace(dom, workspace); + assertEquals('Block count', 1, workspace.getAllBlocks().length); + xmlTest_checkVariableValues('name1', 'type1', 'id1'); + xmlTest_checkVariableValues('name2', 'type2', 'id2'); + xmlTest_checkVariableValues('name3', '', 'id3'); + } finally { + xmlTest_tearDownWithMockBlocks(); + } +} + +function test_domToWorkspace_VariablesAtTop_DuplicateVariablesTag() { + // Expect thrown Error because of duplicate 'variables' tag + xmlTest_setUpWithMockBlocks(); + try { + var dom = Blockly.Xml.textToDom( + '<xml>' + + ' <variables>' + + ' </variables>' + + ' <variables>' + + ' </variables>' + + '</xml>'); + Blockly.Xml.domToWorkspace(dom, workspace); + fail(); + } + catch (e) { + // expected + } finally { + xmlTest_tearDownWithMockBlocks(); + } +} + +function test_domToWorkspace_VariablesAtTop_MissingType() { + // Expect thrown error when a variable tag is missing the type attribute. + workspace = new Blockly.Workspace(); + try { + var dom = Blockly.Xml.textToDom( + '<xml>' + + ' <variables>' + + ' <variable id="id1">name1</variable>' + + ' </variables>' + + ' <block type="field_variable_test_block">' + + ' <field name="VAR" id="id1" variabletype="">name3</field>' + + ' </block>' + + '</xml>'); + Blockly.Xml.domToWorkspace(dom, workspace); + fail(); + } catch (e) { + // expected + } finally { + workspace.dispose(); + } +} + +function test_domToWorkspace_VariablesAtTop_MismatchBlockType() { + // Expect thrown error when the serialized type of a variable does not match + // the type of a variable field that references it. + xmlTest_setUpWithMockBlocks(); + try { + var dom = Blockly.Xml.textToDom( + '<xml>' + + ' <variables>' + + ' <variable type="type1" id="id1">name1</variable>' + + ' </variables>' + + ' <block type="field_variable_test_block">' + + ' <field name="VAR" id="id1" variabletype="">name1</field>' + + ' </block>' + + '</xml>'); + Blockly.Xml.domToWorkspace(dom, workspace); + fail(); + } catch (e) { + // expected + } finally { + xmlTest_tearDownWithMockBlocks(); + } +} + +function test_domToPrettyText() { + var dom = Blockly.Xml.textToDom(XML_TEXT); + var text = Blockly.Xml.domToPrettyText(dom); + assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''), + text.replace(/\s+/g, '')); +} + +/** + * Tests the that appendDomToWorkspace works in a headless mode. + * Also see test_appendDomToWorkspace() in workspace_svg_test.js. + */ +function test_appendDomToWorkspace() { Blockly.Blocks.test_block = { init: function() { this.jsonInit({ @@ -75,20 +296,101 @@ function test_domToWorkspace() { ' <block type="test_block" inline="true" x="21" y="23">' + ' </block>' + '</xml>'); - Blockly.Xml.domToWorkspace(dom, workspace); + workspace = new Blockly.Workspace(); + Blockly.Xml.appendDomToWorkspace(dom, workspace); assertEquals('Block count', 1, workspace.getAllBlocks().length); + var newBlockIds = Blockly.Xml.appendDomToWorkspace(dom, workspace); + assertEquals('Block count', 2, workspace.getAllBlocks().length); + assertEquals('Number of new block ids',1,newBlockIds.length); } finally { delete Blockly.Blocks.test_block; - workspace.dispose(); } } -function test_domToPrettyText() { - var dom = Blockly.Xml.textToDom(XML_TEXT); - var text = Blockly.Xml.domToPrettyText(dom); - assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''), - text.replace(/\s+/g, '')); +function test_blockToDom_fieldToDom_trivial() { + xmlTest_setUpWithMockBlocks() + workspace.createVariable('name1', 'type1', 'id1'); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + block.inputList[0].fieldRow[0].setValue('name1'); + var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; + xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', 'type1', 'id1', 'name1') + xmlTest_tearDownWithMockBlocks() +} + +function test_blockToDom_fieldToDom_defaultCase() { + xmlTest_setUpWithMockBlocks() + var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); + mockGenUid().$returns('1'); + mockGenUid().$replay(); + workspace.createVariable('name1'); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + block.inputList[0].fieldRow[0].setValue('name1'); + var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; + // Expect type is '' and id is '1' since we don't specify type and id. + xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', '', '1', 'name1') + xmlTest_tearDownWithMockBlocks() +} + +function test_blockToDom_fieldToDom_notAFieldVariable() { + Blockly.defineBlocksWithJsonArray([{ + "type": "field_angle_test_block", + "message0": "%1", + "args0": [ + { + "type": "field_angle", + "name": "VAR", + "angle": 90 + } + ], + }]); + xmlTest_setUpWithMockBlocks() + var block = new Blockly.Block(workspace, 'field_angle_test_block'); + var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; + xmlTest_checkNonVariableField(resultFieldDom, 'VAR', '90'); + delete Blockly.Blocks.field_angle_block; + xmlTest_tearDownWithMockBlocks() +} + +function test_variablesToDom_oneVariable() { + xmlTest_setUp(); + var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); + mockGenUid().$returns('1'); + mockGenUid().$replay(); + + workspace.createVariable('name1'); + var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); + assertEquals(1, resultDom.children.length); + var resultVariableDom = resultDom.children[0]; + assertEquals('name1', resultVariableDom.textContent); + assertEquals('', resultVariableDom.getAttribute('type')); + assertEquals('1', resultVariableDom.getAttribute('id')); + xmlTest_tearDown(); +} + +function test_variablesToDom_twoVariables_oneBlock() { + xmlTest_setUpWithMockBlocks(); + + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + block.inputList[0].fieldRow[0].setValue('name1'); + + var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); + assertEquals(2, resultDom.children.length); + xmlTest_checkVariableDomValues(resultDom.children[0], 'type1', 'id1', + 'name1'); + xmlTest_checkVariableDomValues(resultDom.children[1], 'type2', 'id2', + 'name2'); + xmlTest_tearDownWithMockBlocks(); +} + +function test_variablesToDom_noVariables() { + xmlTest_setUp(); + workspace.createVariable('name1'); + var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); + assertEquals(1, resultDom.children.length); + xmlTest_tearDown(); } /**