diff --git a/core/block.js b/core/block.js index 2021d3f81..872081246 100644 --- a/core/block.js +++ b/core/block.js @@ -160,6 +160,11 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { this.onchangeWrapper_ = this.onchange.bind(this); this.workspace.addChangeListener(this.onchangeWrapper_); } + // Bind an onchange function, if it exists. + if ((!this.isInFlyout) && goog.isFunction(this.onchange)) { + Blockly.bindEvent_(workspace.getCanvas(), 'blocklyWorkspaceChange', this, + this.onchange); + } }; /** @@ -366,6 +371,8 @@ Blockly.Block.prototype.bumpNeighbours_ = function() { } } } + // Remove any associated errors or warnings. + Blockly.WarningHandler.checkDisposedBlock.call(this); }; /** @@ -460,6 +467,11 @@ Blockly.Block.prototype.setParent = function(newParent) { children.splice(x, 1); break; } + if (descendant.errorIcon) { + var data = descendant.errorIcon.getIconLocation(); + data.bubble = descendant.errorIcon; + this.draggedBubbles_.push(data); + } } // Disconnect from superior blocks. @@ -476,6 +488,9 @@ Blockly.Block.prototype.setParent = function(newParent) { // Remove this block from the workspace's list of top-most blocks. this.workspace.removeTopBlock(this); } + if (this.errorIcon) { + this.errorIcon.computeIconLocation(); + } this.parentBlock_ = newParent; if (newParent) { @@ -969,6 +984,17 @@ Blockly.Block.prototype.appendStatementInput = function(name) { return this.appendInput_(Blockly.NEXT_STATEMENT, name); }; +/** + * Shortcut for appending an inline value input row. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ + +Blockly.Block.prototype.appendIndentedValueInput = function(name) { + return this.appendInput_(Blockly.INDENTED_VALUE, name); +}; + /** * Shortcut for appending a dummy input row. * @param {string=} opt_name Language-neutral identifier which may used to find @@ -1165,8 +1191,8 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { /** * Add a value input, statement input or local variable to this block. - * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or - * Blockly.DUMMY_INPUT. + * @param {number} type Either Blockly.INPUT_VALUE, Blockly.NEXT_STATEMENT, Blockly.DUMMY_INPUT, + * or subtypes Blockly.INDENTED_VALUE. * @param {string} name Language-neutral identifier which may used to find this * input again. Should be unique to this block. * @return {!Blockly.Input} The input object created. diff --git a/core/block.js.orig b/core/block.js.orig new file mode 100644 index 000000000..2021d3f81 --- /dev/null +++ b/core/block.js.orig @@ -0,0 +1,1364 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 The class representing one block. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Block'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Comment'); +goog.require('Blockly.Connection'); +goog.require('Blockly.Input'); +goog.require('Blockly.Mutator'); +goog.require('Blockly.Warning'); +goog.require('Blockly.Workspace'); +goog.require('Blockly.Xml'); +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.math.Coordinate'); +goog.require('goog.string'); + + +/** + * Class for one block. + * Not normally called directly, workspace.newBlock() is preferred. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @constructor + */ +Blockly.Block = function(workspace, prototypeName, opt_id) { + /** @type {string} */ + this.id = (opt_id && !workspace.getBlockById(opt_id)) ? + opt_id : Blockly.genUid(); + workspace.blockDB_[this.id] = this; + /** @type {Blockly.Connection} */ + this.outputConnection = null; + /** @type {Blockly.Connection} */ + this.nextConnection = null; + /** @type {Blockly.Connection} */ + this.previousConnection = null; + /** @type {!Array.} */ + this.inputList = []; + /** @type {boolean|undefined} */ + this.inputsInline = undefined; + /** @type {boolean} */ + this.disabled = false; + /** @type {string|!Function} */ + this.tooltip = ''; + /** @type {boolean} */ + this.contextMenu = true; + + /** + * @type {Blockly.Block} + * @private + */ + this.parentBlock_ = null; + + /** + * @type {!Array.} + * @private + */ + this.childBlocks_ = []; + + /** + * @type {boolean} + * @private + */ + this.deletable_ = true; + + /** + * @type {boolean} + * @private + */ + this.movable_ = true; + + /** + * @type {boolean} + * @private + */ + this.editable_ = true; + + /** + * @type {boolean} + * @private + */ + this.isShadow_ = false; + + /** + * @type {boolean} + * @private + */ + this.collapsed_ = false; + + /** @type {string|Blockly.Comment} */ + this.comment = null; + + /** + * @type {!goog.math.Coordinate} + * @private + */ + this.xy_ = new goog.math.Coordinate(0, 0); + + /** @type {!Blockly.Workspace} */ + this.workspace = workspace; + /** @type {boolean} */ + this.isInFlyout = workspace.isFlyout; + /** @type {boolean} */ + this.isInMutator = workspace.isMutator; + + /** @type {boolean} */ + this.RTL = workspace.RTL; + + // Copy the type-specific functions and data from the prototype. + if (prototypeName) { + /** @type {string} */ + this.type = prototypeName; + var prototype = Blockly.Blocks[prototypeName]; + goog.asserts.assertObject(prototype, + 'Error: "%s" is an unknown language block.', prototypeName); + goog.mixin(this, prototype); + } + + workspace.addTopBlock(this); + + // Call an initialization function, if it exists. + if (goog.isFunction(this.init)) { + this.init(); + } + // Record initial inline state. + /** @type {boolean|undefined} */ + this.inputsInlineDefault = this.inputsInline; + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Create(this)); + } + // Bind an onchange function, if it exists. + if (goog.isFunction(this.onchange)) { + this.onchangeWrapper_ = this.onchange.bind(this); + this.workspace.addChangeListener(this.onchangeWrapper_); + } +}; + +/** + * Obtain a newly created block. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @return {!Blockly.Block} The created block. + * @deprecated December 2015 + */ +Blockly.Block.obtain = function(workspace, prototypeName) { + console.warn('Deprecated call to Blockly.Block.obtain, ' + + 'use workspace.newBlock instead.'); + return workspace.newBlock(prototypeName); +}; + +/** + * Optional text data that round-trips beween blocks and XML. + * Has no effect. May be used by 3rd parties for meta information. + * @type {?string} + */ +Blockly.Block.prototype.data = null; + +/** + * Colour of the block in '#RRGGBB' format. + * @type {string} + * @private + */ +Blockly.Block.prototype.colour_ = '#000000'; + +/** + * Dispose of this block. + * @param {boolean} healStack If true, then try to heal any gap by connecting + * the next statement with the previous statement. Otherwise, dispose of + * all children of this block. + */ +Blockly.Block.prototype.dispose = function(healStack) { + if (!this.workspace) { + // Already deleted. + return; + } + // Terminate onchange event calls. + if (this.onchangeWrapper_) { + this.workspace.removeChangeListener(this.onchangeWrapper_); + } + this.unplug(healStack); + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Delete(this)); + } + Blockly.Events.disable(); + + try { + // This block is now at the top of the workspace. + // Remove this block from the workspace's list of top-most blocks. + if (this.workspace) { + this.workspace.removeTopBlock(this); + // Remove from block database. + delete this.workspace.blockDB_[this.id]; + this.workspace = null; + } + + // Just deleting this block from the DOM would result in a memory leak as + // well as corruption of the connection database. Therefore we must + // methodically step through the blocks and carefully disassemble them. + + // First, dispose of all my children. + for (var i = this.childBlocks_.length - 1; i >= 0; i--) { + this.childBlocks_[i].dispose(false); + } + // Then dispose of myself. + // Dispose of all inputs and their fields. + for (var i = 0, input; input = this.inputList[i]; i++) { + input.dispose(); + } + this.inputList.length = 0; + // Dispose of any remaining connections (next/previous/output). + var connections = this.getConnections_(true); + for (var i = 0; i < connections.length; i++) { + var connection = connections[i]; + if (connection.isConnected()) { + connection.disconnect(); + } + connections[i].dispose(); + } + } finally { + Blockly.Events.enable(); + } +}; + +/** + * Unplug this block from its superior block. If this block is a statement, + * optionally reconnect the block underneath with the block on top. + * @param {boolean} opt_healStack Disconnect child statement and reconnect + * stack. Defaults to false. + */ +Blockly.Block.prototype.unplug = function(opt_healStack) { + if (this.outputConnection) { + if (this.outputConnection.isConnected()) { + // Disconnect from any superior block. + this.outputConnection.disconnect(); + } + } else if (this.previousConnection) { + var previousTarget = null; + if (this.previousConnection.isConnected()) { + // Remember the connection that any next statements need to connect to. + previousTarget = this.previousConnection.targetConnection; + // Detach this block from the parent's tree. + this.previousConnection.disconnect(); + } + var nextBlock = this.getNextBlock(); + if (opt_healStack && nextBlock) { + // Disconnect the next statement. + var nextTarget = this.nextConnection.targetConnection; + nextTarget.disconnect(); + if (previousTarget && previousTarget.checkType_(nextTarget)) { + // Attach the next statement to the previous statement. + previousTarget.connect(nextTarget); + } + } + } +}; + +/** + * Returns all connections originating from this block. + * @return {!Array.} Array of connections. + * @private + */ +Blockly.Block.prototype.getConnections_ = function() { + var myConnections = []; + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection) { + myConnections.push(input.connection); + } + } + return myConnections; +}; + +/** + * Walks down a stack of blocks and finds the last next connection on the stack. + * @return {Blockly.Connection} The last next connection on the stack, or null. + * @private + */ +Blockly.Block.prototype.lastConnectionInStack_ = function() { + var nextConnection = this.nextConnection; + while (nextConnection) { + var nextBlock = nextConnection.targetBlock(); + if (!nextBlock) { + // Found a next connection with nothing on the other side. + return nextConnection; + } + nextConnection = nextBlock.nextConnection; + } + // Ran out of next connections. + return null; +}; + +/** + * Bump unconnected blocks out of alignment. Two blocks which aren't actually + * connected should not coincidentally line up on screen. + * @private + */ +Blockly.Block.prototype.bumpNeighbours_ = function() { + if (!this.workspace) { + return; // Deleted block. + } + if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + return; // Don't bump blocks during a drag. + } + var rootBlock = this.getRootBlock(); + if (rootBlock.isInFlyout) { + return; // Don't move blocks around in a flyout. + } + // Loop though every connection on this block. + var myConnections = this.getConnections_(false); + for (var i = 0, connection; connection = myConnections[i]; i++) { + // Spider down from this block bumping all sub-blocks. + if (connection.isConnected() && connection.isSuperior()) { + connection.targetBlock().bumpNeighbours_(); + } + + var neighbours = connection.neighbours_(Blockly.SNAP_RADIUS); + for (var j = 0, otherConnection; otherConnection = neighbours[j]; j++) { + // If both connections are connected, that's probably fine. But if + // either one of them is unconnected, then there could be confusion. + if (!connection.isConnected() || !otherConnection.isConnected()) { + // Only bump blocks if they are from different tree structures. + if (otherConnection.getSourceBlock().getRootBlock() != rootBlock) { + // Always bump the inferior block. + if (connection.isSuperior()) { + otherConnection.bumpAwayFrom_(connection); + } else { + connection.bumpAwayFrom_(otherConnection); + } + } + } + } + } +}; + +/** + * Return the parent block or null if this block is at the top level. + * @return {Blockly.Block} The block that holds the current block. + */ +Blockly.Block.prototype.getParent = function() { + // Look at the DOM to see if we are nested in another block. + return this.parentBlock_; +}; + +/** + * Return the input that connects to the specified block. + * @param {!Blockly.Block} block A block connected to an input on this block. + * @return {Blockly.Input} The input that connects to the specified block. + */ +Blockly.Block.prototype.getInputWithBlock = function(block) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection && input.connection.targetBlock() == block) { + return input; + } + } + return null; +}; + +/** + * Return the parent block that surrounds the current block, or null if this + * block has no surrounding block. A parent block might just be the previous + * statement, whereas the surrounding block is an if statement, while loop, etc. + * @return {Blockly.Block} The block that surrounds the current block. + */ +Blockly.Block.prototype.getSurroundParent = function() { + var block = this; + do { + var prevBlock = block; + block = block.getParent(); + if (!block) { + // Ran off the top. + return null; + } + } while (block.getNextBlock() == prevBlock); + // This block is an enclosing parent, not just a statement in a stack. + return block; +}; + +/** + * Return the next statement block directly connected to this block. + * @return {Blockly.Block} The next statement block or null. + */ +Blockly.Block.prototype.getNextBlock = function() { + return this.nextConnection && this.nextConnection.targetBlock(); +}; + +/** + * Return the top-most block in this block's tree. + * This will return itself if this block is at the top level. + * @return {!Blockly.Block} The root block. + */ +Blockly.Block.prototype.getRootBlock = function() { + var rootBlock; + var block = this; + do { + rootBlock = block; + block = rootBlock.parentBlock_; + } while (block); + return rootBlock; +}; + +/** + * Find all the blocks that are directly nested inside this one. + * Includes value and block inputs, as well as any following statement. + * Excludes any connection on an output tab or any preceding statement. + * @return {!Array.} Array of blocks. + */ +Blockly.Block.prototype.getChildren = function() { + return this.childBlocks_; +}; + +/** + * Set parent of this block to be a new block or null. + * @param {Blockly.Block} newParent New parent block. + */ +Blockly.Block.prototype.setParent = function(newParent) { + if (newParent == this.parentBlock_) { + return; + } + if (this.parentBlock_) { + // Remove this block from the old parent's child list. + var children = this.parentBlock_.childBlocks_; + for (var child, x = 0; child = children[x]; x++) { + if (child == this) { + children.splice(x, 1); + break; + } + } + + // Disconnect from superior blocks. + if (this.previousConnection && this.previousConnection.isConnected()) { + throw 'Still connected to previous block.'; + } + if (this.outputConnection && this.outputConnection.isConnected()) { + throw 'Still connected to parent block.'; + } + this.parentBlock_ = null; + // This block hasn't actually moved on-screen, so there's no need to update + // its connection locations. + } else { + // Remove this block from the workspace's list of top-most blocks. + this.workspace.removeTopBlock(this); + } + + this.parentBlock_ = newParent; + if (newParent) { + // Add this block to the new parent's child list. + newParent.childBlocks_.push(this); + } else { + this.workspace.addTopBlock(this); + } +}; + +/** + * Find all the blocks that are directly or indirectly nested inside this one. + * Includes this block in the list. + * Includes value and block inputs, as well as any following statements. + * Excludes any connection on an output tab or any preceding statements. + * @return {!Array.} Flattened array of blocks. + */ +Blockly.Block.prototype.getDescendants = function() { + var blocks = [this]; + for (var child, x = 0; child = this.childBlocks_[x]; x++) { + blocks.push.apply(blocks, child.getDescendants()); + } + return blocks; +}; + +/** + * Get whether this block is deletable or not. + * @return {boolean} True if deletable. + */ +Blockly.Block.prototype.isDeletable = function() { + return this.deletable_ && !this.isShadow_ && + !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is deletable or not. + * @param {boolean} deletable True if deletable. + */ +Blockly.Block.prototype.setDeletable = function(deletable) { + this.deletable_ = deletable; +}; + +/** + * Get whether this block is movable or not. + * @return {boolean} True if movable. + */ +Blockly.Block.prototype.isMovable = function() { + return this.movable_ && !this.isShadow_ && + !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is movable or not. + * @param {boolean} movable True if movable. + */ +Blockly.Block.prototype.setMovable = function(movable) { + this.movable_ = movable; +}; + +/** + * Get whether this block is a shadow block or not. + * @return {boolean} True if a shadow. + */ +Blockly.Block.prototype.isShadow = function() { + return this.isShadow_; +}; + +/** + * Set whether this block is a shadow block or not. + * @param {boolean} shadow True if a shadow. + */ +Blockly.Block.prototype.setShadow = function(shadow) { + this.isShadow_ = shadow; +}; + +/** + * Get whether this block is editable or not. + * @return {boolean} True if editable. + */ +Blockly.Block.prototype.isEditable = function() { + return this.editable_ && !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this block is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.Block.prototype.setEditable = function(editable) { + this.editable_ = editable; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + field.updateEditable(); + } + } +}; + +/** + * Set whether the connections are hidden (not tracked in a database) or not. + * Recursively walk down all child blocks (except collapsed blocks). + * @param {boolean} hidden True if connections are hidden. + */ +Blockly.Block.prototype.setConnectionsHidden = function(hidden) { + if (!hidden && this.isCollapsed()) { + if (this.outputConnection) { + this.outputConnection.setHidden(hidden); + } + if (this.previousConnection) { + this.previousConnection.setHidden(hidden); + } + if (this.nextConnection) { + this.nextConnection.setHidden(hidden); + var child = this.nextConnection.targetBlock(); + if (child) { + child.setConnectionsHidden(hidden); + } + } + } else { + var myConnections = this.getConnections_(true); + for (var i = 0, connection; connection = myConnections[i]; i++) { + connection.setHidden(hidden); + if (connection.isSuperior()) { + var child = connection.targetBlock(); + if (child) { + child.setConnectionsHidden(hidden); + } + } + } + } +}; + +/** + * Set the URL of this block's help page. + * @param {string|Function} url URL string for block help, or function that + * returns a URL. Null for no help. + */ +Blockly.Block.prototype.setHelpUrl = function(url) { + this.helpUrl = url; +}; + +/** + * Change the tooltip text for a block. + * @param {string|!Function} newTip Text for tooltip or a parent element to + * link to for its tooltip. May be a function that returns a string. + */ +Blockly.Block.prototype.setTooltip = function(newTip) { + this.tooltip = newTip; +}; + +/** + * Get the colour of a block. + * @return {string} #RRGGBB string. + */ +Blockly.Block.prototype.getColour = function() { + return this.colour_; +}; + +/** + * Change the colour of a block. + * @param {number|string} colour HSV hue value, or #RRGGBB string. + */ +Blockly.Block.prototype.setColour = function(colour) { + var hue = parseFloat(colour); + if (!isNaN(hue)) { + this.colour_ = Blockly.hueToRgb(hue); + } else if (goog.isString(colour) && colour.match(/^#[0-9a-fA-F]{6}$/)) { + this.colour_ = colour; + } else { + throw 'Invalid colour: ' + colour; + } +}; + +/** + * Returns the named field from a block. + * @param {string} name The name of the field. + * @return {Blockly.Field} Named field, or null if field does not exist. + */ +Blockly.Block.prototype.getField = function(name) { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field.name === name) { + return field; + } + } + } + return null; +}; + +/** + * Return all variables referenced by this block. + * @return {!Array.} List of variable names. + */ +Blockly.Block.prototype.getVars = function() { + var vars = []; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldVariable) { + vars.push(field.getValue()); + } + } + } + return vars; +}; + +/** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + */ +Blockly.Block.prototype.renameVar = function(oldName, newName) { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldVariable && + Blockly.Names.equals(oldName, field.getValue())) { + field.setValue(newName); + } + } + } +}; + +/** + * Returns the language-neutral value from the field of a block. + * @param {string} name The name of the field. + * @return {?string} Value from the field or null if field does not exist. + */ +Blockly.Block.prototype.getFieldValue = function(name) { + var field = this.getField(name); + if (field) { + return field.getValue(); + } + return null; +}; + +/** + * Returns the language-neutral value from the field of a block. + * @param {string} name The name of the field. + * @return {?string} Value from the field or null if field does not exist. + * @deprecated December 2013 + */ +Blockly.Block.prototype.getTitleValue = function(name) { + console.warn('Deprecated call to getTitleValue, use getFieldValue instead.'); + return this.getFieldValue(name); +}; + +/** + * Change the field value for a block (e.g. 'CHOOSE' or 'REMOVE'). + * @param {string} newValue Value to be the new field. + * @param {string} name The name of the field. + */ +Blockly.Block.prototype.setFieldValue = function(newValue, name) { + var field = this.getField(name); + goog.asserts.assertObject(field, 'Field "%s" not found.', name); + field.setValue(newValue); +}; + +/** + * Change the field value for a block (e.g. 'CHOOSE' or 'REMOVE'). + * @param {string} newValue Value to be the new field. + * @param {string} name The name of the field. + * @deprecated December 2013 + */ +Blockly.Block.prototype.setTitleValue = function(newValue, name) { + console.warn('Deprecated call to setTitleValue, use setFieldValue instead.'); + this.setFieldValue(newValue, name); +}; + +/** + * Set whether this block can chain onto the bottom of another block. + * @param {boolean} newBoolean True if there can be a previous statement. + * @param {string|Array.|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.Block.prototype.setPreviousStatement = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.previousConnection) { + goog.asserts.assert(!this.outputConnection, + 'Remove output connection prior to adding previous connection.'); + this.previousConnection = + this.makeConnection_(Blockly.PREVIOUS_STATEMENT); + } + this.previousConnection.setCheck(opt_check); + } else { + if (this.previousConnection) { + goog.asserts.assert(!this.previousConnection.isConnected(), + 'Must disconnect previous statement before removing connection.'); + this.previousConnection.dispose(); + this.previousConnection = null; + } + } +}; + +/** + * Set whether another block can chain onto the bottom of this block. + * @param {boolean} newBoolean True if there can be a next statement. + * @param {string|Array.|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.Block.prototype.setNextStatement = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.nextConnection) { + this.nextConnection = this.makeConnection_(Blockly.NEXT_STATEMENT); + } + this.nextConnection.setCheck(opt_check); + } else { + if (this.nextConnection) { + goog.asserts.assert(!this.nextConnection.isConnected(), + 'Must disconnect next statement before removing connection.'); + this.nextConnection.dispose(); + this.nextConnection = null; + } + } +}; + +/** + * Set whether this block returns a value. + * @param {boolean} newBoolean True if there is an output. + * @param {string|Array.|null|undefined} opt_check Returned type or list + * of returned types. Null or undefined if any type could be returned + * (e.g. variable get). + */ +Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { + if (newBoolean) { + if (opt_check === undefined) { + opt_check = null; + } + if (!this.outputConnection) { + goog.asserts.assert(!this.previousConnection, + 'Remove previous connection prior to adding output connection.'); + this.outputConnection = this.makeConnection_(Blockly.OUTPUT_VALUE); + } + this.outputConnection.setCheck(opt_check); + } else { + if (this.outputConnection) { + goog.asserts.assert(!this.outputConnection.isConnected(), + 'Must disconnect output value before removing connection.'); + this.outputConnection.dispose(); + this.outputConnection = null; + } + } +}; + +/** + * Set whether value inputs are arranged horizontally or vertically. + * @param {boolean} newBoolean True if inputs are horizontal. + */ +Blockly.Block.prototype.setInputsInline = function(newBoolean) { + if (this.inputsInline != newBoolean) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'inline', null, this.inputsInline, newBoolean)); + this.inputsInline = newBoolean; + } +}; + +/** + * Get whether value inputs are arranged horizontally or vertically. + * @return {boolean} True if inputs are horizontal. + */ +Blockly.Block.prototype.getInputsInline = function() { + if (this.inputsInline != undefined) { + // Set explicitly. + return this.inputsInline; + } + // Not defined explicitly. Figure out what would look best. + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type == Blockly.DUMMY_INPUT && + this.inputList[i].type == Blockly.DUMMY_INPUT) { + // Two dummy inputs in a row. Don't inline them. + return false; + } + } + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type == Blockly.INPUT_VALUE && + this.inputList[i].type == Blockly.DUMMY_INPUT) { + // Dummy input after a value input. Inline them. + return true; + } + } + return false; +}; + +/** + * Set whether the block is disabled or not. + * @param {boolean} disabled True if disabled. + */ +Blockly.Block.prototype.setDisabled = function(disabled) { + if (this.disabled != disabled) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'disabled', null, this.disabled, disabled)); + this.disabled = disabled; + } +}; + +/** + * Get whether the block is disabled or not due to parents. + * The block's own disabled property is not considered. + * @return {boolean} True if disabled. + */ +Blockly.Block.prototype.getInheritedDisabled = function() { + var block = this; + while (true) { + block = block.getSurroundParent(); + if (!block) { + // Ran off the top. + return false; + } else if (block.disabled) { + return true; + } + } +}; + +/** + * Get whether the block is collapsed or not. + * @return {boolean} True if collapsed. + */ +Blockly.Block.prototype.isCollapsed = function() { + return this.collapsed_; +}; + +/** + * Set whether the block is collapsed or not. + * @param {boolean} collapsed True if collapsed. + */ +Blockly.Block.prototype.setCollapsed = function(collapsed) { + if (this.collapsed_ != collapsed) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'collapsed', null, this.collapsed_, collapsed)); + this.collapsed_ = collapsed; + } +}; + +/** + * Create a human-readable text representation of this block and any children. + * @param {number=} opt_maxLength Truncate the string to this length. + * @param {string=} opt_emptyToken The placeholder string used to denote an + * empty field. If not specified, '?' is used. + * @return {string} Text of block. + */ +Blockly.Block.prototype.toString = function(opt_maxLength, opt_emptyToken) { + var text = []; + var emptyFieldPlaceholder = opt_emptyToken || '?'; + if (this.collapsed_) { + text.push(this.getInput('_TEMP_COLLAPSED_INPUT').fieldRow[0].text_); + } else { + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + text.push(field.getText()); + } + if (input.connection) { + var child = input.connection.targetBlock(); + if (child) { + text.push(child.toString(undefined, opt_emptyToken)); + } else { + text.push(emptyFieldPlaceholder); + } + } + } + } + text = goog.string.trim(text.join(' ')) || '???'; + if (opt_maxLength) { + // TODO: Improve truncation so that text from this block is given priority. + // E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not "1+2+3+4+5...". + // E.g. "1+2+3+4+5=6+7+8+9+0" should be "...4+5=6+7...". + text = goog.string.truncate(text, opt_maxLength); + } + return text; +}; + +/** + * Shortcut for appending a value input row. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendValueInput = function(name) { + return this.appendInput_(Blockly.INPUT_VALUE, name); +}; + +/** + * Shortcut for appending a statement input row. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendStatementInput = function(name) { + return this.appendInput_(Blockly.NEXT_STATEMENT, name); +}; + +/** + * Shortcut for appending a dummy input row. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + */ +Blockly.Block.prototype.appendDummyInput = function(opt_name) { + return this.appendInput_(Blockly.DUMMY_INPUT, opt_name || ''); +}; + +/** + * Initialize this block using a cross-platform, internationalization-friendly + * JSON description. + * @param {!Object} json Structured data describing the block. + */ +Blockly.Block.prototype.jsonInit = function(json) { + // Validate inputs. + goog.asserts.assert(json['output'] == undefined || + json['previousStatement'] == undefined, + 'Must not have both an output and a previousStatement.'); + + // Set basic properties of block. + if (json['colour'] !== undefined) { + this.setColour(json['colour']); + } + + // Interpolate the message blocks. + var i = 0; + while (json['message' + i] !== undefined) { + this.interpolate_(json['message' + i], json['args' + i] || [], + json['lastDummyAlign' + i]); + i++; + } + + if (json['inputsInline'] !== undefined) { + this.setInputsInline(json['inputsInline']); + } + // Set output and previous/next connections. + if (json['output'] !== undefined) { + this.setOutput(true, json['output']); + } + if (json['previousStatement'] !== undefined) { + this.setPreviousStatement(true, json['previousStatement']); + } + if (json['nextStatement'] !== undefined) { + this.setNextStatement(true, json['nextStatement']); + } + if (json['tooltip'] !== undefined) { + this.setTooltip(json['tooltip']); + } + if (json['helpUrl'] !== undefined) { + this.setHelpUrl(json['helpUrl']); + } +}; + +/** + * Interpolate a message description onto the block. + * @param {string} message Text contains interpolation tokens (%1, %2, ...) + * that match with fields or inputs defined in the args array. + * @param {!Array} args Array of arguments to be interpolated. + * @param {=string} lastDummyAlign If a dummy input is added at the end, + * how should it be aligned? + * @private + */ +Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { + var tokens = Blockly.utils.tokenizeInterpolation(message); + // Interpolate the arguments. Build a list of elements. + var indexDup = []; + var indexCount = 0; + var elements = []; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token == 'number') { + goog.asserts.assert(token > 0 && token <= args.length, + 'Message index "%s" out of range.', token); + goog.asserts.assert(!indexDup[token], + 'Message index "%s" duplicated.', token); + indexDup[token] = true; + indexCount++; + elements.push(args[token - 1]); + } else { + token = token.trim(); + if (token) { + elements.push(token); + } + } + } + goog.asserts.assert(indexCount == args.length, + 'Message does not reference all %s arg(s).', args.length); + // Add last dummy input if needed. + if (elements.length && (typeof elements[elements.length - 1] == 'string' || + elements[elements.length - 1]['type'].indexOf('field_') == 0)) { + var dummyInput = {type: 'input_dummy'}; + if (lastDummyAlign) { + dummyInput['align'] = lastDummyAlign; + } + elements.push(dummyInput); + } + // Lookup of alignment constants. + var alignmentLookup = { + 'LEFT': Blockly.ALIGN_LEFT, + 'RIGHT': Blockly.ALIGN_RIGHT, + 'CENTRE': Blockly.ALIGN_CENTRE + }; + // Populate block with inputs and fields. + var fieldStack = []; + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (typeof element == 'string') { + fieldStack.push([element, undefined]); + } else { + var field = null; + var input = null; + do { + var altRepeat = false; + if (typeof element == 'string') { + field = new Blockly.FieldLabel(element); + } else { + switch (element['type']) { + case 'input_value': + input = this.appendValueInput(element['name']); + break; + case 'input_statement': + input = this.appendStatementInput(element['name']); + break; + case 'input_dummy': + input = this.appendDummyInput(element['name']); + break; + case 'field_label': + field = new Blockly.FieldLabel(element['text'], element['class']); + break; + case 'field_input': + field = new Blockly.FieldTextInput(element['text']); + if (typeof element['spellcheck'] == 'boolean') { + field.setSpellcheck(element['spellcheck']); + } + break; + case 'field_angle': + field = new Blockly.FieldAngle(element['angle']); + break; + case 'field_checkbox': + field = new Blockly.FieldCheckbox( + element['checked'] ? 'TRUE' : 'FALSE'); + break; + case 'field_colour': + field = new Blockly.FieldColour(element['colour']); + break; + case 'field_variable': + field = new Blockly.FieldVariable(element['variable']); + break; + case 'field_dropdown': + field = new Blockly.FieldDropdown(element['options']); + break; + case 'field_image': + field = new Blockly.FieldImage(element['src'], + element['width'], element['height'], element['alt']); + break; + case 'field_number': + field = new Blockly.FieldNumber(element['value'], + element['min'], element['max'], element['precision']); + break; + case 'field_date': + if (Blockly.FieldDate) { + field = new Blockly.FieldDate(element['date']); + break; + } + // Fall through if FieldDate is not compiled in. + default: + // Unknown field. + if (element['alt']) { + element = element['alt']; + altRepeat = true; + } + } + } + } while (altRepeat); + if (field) { + fieldStack.push([field, element['name']]); + } else if (input) { + if (element['check']) { + input.setCheck(element['check']); + } + if (element['align']) { + input.setAlign(alignmentLookup[element['align']]); + } + for (var j = 0; j < fieldStack.length; j++) { + input.appendField(fieldStack[j][0], fieldStack[j][1]); + } + fieldStack.length = 0; + } + } + } +}; + +/** + * Add a value input, statement input or local variable to this block. + * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or + * Blockly.DUMMY_INPUT. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + * @private + */ +Blockly.Block.prototype.appendInput_ = function(type, name) { + var connection = null; + if (type == Blockly.INPUT_VALUE || type == Blockly.NEXT_STATEMENT) { + connection = this.makeConnection_(type); + } + var input = new Blockly.Input(type, name, this, connection); + // Append input to list. + this.inputList.push(input); + return input; +}; + +/** + * Move a named input to a different location on this block. + * @param {string} name The name of the input to move. + * @param {?string} refName Name of input that should be after the moved input, + * or null to be the input at the end. + */ +Blockly.Block.prototype.moveInputBefore = function(name, refName) { + if (name == refName) { + return; + } + // Find both inputs. + var inputIndex = -1; + var refIndex = refName ? -1 : this.inputList.length; + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + inputIndex = i; + if (refIndex != -1) { + break; + } + } else if (refName && input.name == refName) { + refIndex = i; + if (inputIndex != -1) { + break; + } + } + } + goog.asserts.assert(inputIndex != -1, 'Named input "%s" not found.', name); + goog.asserts.assert(refIndex != -1, 'Reference input "%s" not found.', + refName); + this.moveNumberedInputBefore(inputIndex, refIndex); +}; + +/** + * Move a numbered input to a different location on this block. + * @param {number} inputIndex Index of the input to move. + * @param {number} refIndex Index of input that should be after the moved input. + */ +Blockly.Block.prototype.moveNumberedInputBefore = function( + inputIndex, refIndex) { + // Validate arguments. + goog.asserts.assert(inputIndex != refIndex, 'Can\'t move input to itself.'); + goog.asserts.assert(inputIndex < this.inputList.length, + 'Input index ' + inputIndex + ' out of bounds.'); + goog.asserts.assert(refIndex <= this.inputList.length, + 'Reference input ' + refIndex + ' out of bounds.'); + // Remove input. + var input = this.inputList[inputIndex]; + this.inputList.splice(inputIndex, 1); + if (inputIndex < refIndex) { + refIndex--; + } + // Reinsert input. + this.inputList.splice(refIndex, 0, input); +}; + +/** + * Remove an input from this block. + * @param {string} name The name of the input. + * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @throws {goog.asserts.AssertionError} if the input is not present and + * opt_quiet is not true. + */ +Blockly.Block.prototype.removeInput = function(name, opt_quiet) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + if (input.connection && input.connection.isConnected()) { + input.connection.setShadowDom(null); + var block = input.connection.targetBlock(); + if (block.isShadow()) { + // Destroy any attached shadow block. + block.dispose(); + } else { + // Disconnect any attached normal block. + block.unplug(); + } + } + input.dispose(); + this.inputList.splice(i, 1); + return; + } + } + if (!opt_quiet) { + goog.asserts.fail('Input "%s" not found.', name); + } +}; + +/** + * Fetches the named input object. + * @param {string} name The name of the input. + * @return {Blockly.Input} The input object, or null if input does not exist. + */ +Blockly.Block.prototype.getInput = function(name) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.name == name) { + return input; + } + } + // This input does not exist. + return null; +}; + +/** + * Fetches the block attached to the named input. + * @param {string} name The name of the input. + * @return {Blockly.Block} The attached value block, or null if the input is + * either disconnected or if the input does not exist. + */ +Blockly.Block.prototype.getInputTargetBlock = function(name) { + var input = this.getInput(name); + return input && input.connection && input.connection.targetBlock(); +}; + +/** + * Returns the comment on this block (or '' if none). + * @return {string} Block's comment. + */ +Blockly.Block.prototype.getCommentText = function() { + return this.comment || ''; +}; + +/** + * Set this block's comment text. + * @param {?string} text The text, or null to delete. + */ +Blockly.Block.prototype.setCommentText = function(text) { + if (this.comment != text) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'comment', null, this.comment, text || '')); + this.comment = text; + } +}; + +/** + * Set this block's warning text. + * @param {?string} text The text, or null to delete. + */ +Blockly.Block.prototype.setWarningText = function(text) { + // NOP. +}; + +/** + * Give this block a mutator dialog. + * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. + */ +Blockly.Block.prototype.setMutator = function(mutator) { + // NOP. +}; + +/** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0). + * @return {!goog.math.Coordinate} Object with .x and .y properties. + */ +Blockly.Block.prototype.getRelativeToSurfaceXY = function() { + return this.xy_; +}; + +/** + * Move a block by a relative offset. + * @param {number} dx Horizontal offset. + * @param {number} dy Vertical offset. + */ +Blockly.Block.prototype.moveBy = function(dx, dy) { + goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); + var event = new Blockly.Events.Move(this); + this.xy_.translate(dx, dy); + event.recordNew(); + Blockly.Events.fire(event); +}; + +/** + * Create a connection of the specified type. + * @param {number} type The type of the connection to create. + * @return {!Blockly.Connection} A new connection of the specified type. + * @private + */ +Blockly.Block.prototype.makeConnection_ = function(type) { + return new Blockly.Connection(this, type); +}; diff --git a/core/block.js.rej b/core/block.js.rej new file mode 100644 index 000000000..41e4c27df --- /dev/null +++ b/core/block.js.rej @@ -0,0 +1,510 @@ +*************** +*** 26,40 **** + + goog.provide('Blockly.Block'); + + goog.require('Blockly.BlockSvg'); + goog.require('Blockly.Blocks'); + goog.require('Blockly.Comment'); + goog.require('Blockly.Connection'); + goog.require('Blockly.ContextMenu'); + goog.require('Blockly.Input'); + goog.require('Blockly.Msg'); + goog.require('Blockly.Mutator'); + goog.require('Blockly.Warning'); + goog.require('Blockly.Workspace'); + goog.require('Blockly.Xml'); + goog.require('goog.Timer'); +--- 26,43 ---- + + goog.provide('Blockly.Block'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code + goog.require('Blockly.BlockSvg'); + goog.require('Blockly.Blocks'); + goog.require('Blockly.Comment'); + goog.require('Blockly.Connection'); + goog.require('Blockly.ContextMenu'); ++ goog.require('Blockly.ErrorIcon'); + goog.require('Blockly.Input'); + goog.require('Blockly.Msg'); + goog.require('Blockly.Mutator'); + goog.require('Blockly.Warning'); ++ goog.require('Blockly.WarningHandler'); + goog.require('Blockly.Workspace'); + goog.require('Blockly.Xml'); + goog.require('goog.Timer'); +*************** +*** 149,154 **** + + this.workspace = workspace; + this.isInFlyout = workspace.isFlyout; + + // Copy the type-specific functions and data from the prototype. + if (prototypeName) { +--- 152,159 ---- + + this.workspace = workspace; + this.isInFlyout = workspace.isFlyout; ++ // This is missing from our latest version ++ //workspace.addTopBlock(this); + + // Copy the type-specific functions and data from the prototype. + if (prototypeName) { +*************** +*** 202,207 **** + Blockly.Block.prototype.warning = null; + + /** + * Returns a list of mutator, comment, and warning icons. + * @return {!Array} List of icons. + */ +--- 212,223 ---- + Blockly.Block.prototype.warning = null; + + /** ++ * Block's error icon (if any). ++ * @type {Blockly.ErrorIcon} ++ */ ++ Blockly.Block.prototype.errorIcon = null; ++ ++ /** + * Returns a list of mutator, comment, and warning icons. + * @return {!Array} List of icons. + */ +*************** +*** 216,221 **** + if (this.warning) { + icons.push(this.warning); + } + return icons; + }; + +--- 232,240 ---- + if (this.warning) { + icons.push(this.warning); + } ++ if (this.errorIcon) { ++ icons.push(this.errorIcon); ++ } + return icons; + }; + +*************** +*** 374,379 **** + for (var x = 0; x < icons.length; x++) { + icons[x].dispose(); + } + // Dispose of all inputs and their fields. + for (var x = 0, input; input = this.inputList[x]; x++) { + input.dispose(); +--- 393,402 ---- + for (var x = 0; x < icons.length; x++) { + icons[x].dispose(); + } ++ if (this.errorIcon) { ++ this.errorIcon.dispose(); ++ } ++ + // Dispose of all inputs and their fields. + for (var x = 0, input; input = this.inputList[x]; x++) { + input.dispose(); +*************** +*** 448,464 **** + * @return {!Object} Object with height and width properties. + */ + Blockly.Block.prototype.getHeightWidth = function() { + var height = this.svg_.height; + var width = this.svg_.width; + // Recursively add size of subsequent blocks. + var nextBlock = this.getNextBlock(); + if (nextBlock) { +- var nextHeightWidth = nextBlock.getHeightWidth(); + height += nextHeightWidth.height - 4; // Height of tab. + width = Math.max(width, nextHeightWidth.width); + } + return {height: height, width: width}; +- }; + + /** + * Handle a mouse-down on an SVG block. +--- 473,534 ---- + * @return {!Object} Object with height and width properties. + */ + Blockly.Block.prototype.getHeightWidth = function() { ++ var start = new Date().getTime(); ++ var result; ++ if (Blockly.Instrument.useNeilGetHeightWidthFix) { ++ result = this.getHeightWidthNeil(); ++ } else { ++ // Old inexplicably quadratic version of getHeightWidth ++ // The quadratic nature has something to do with getBBox that Lyn ++ // and others never figured out. ++ try { ++ // var bBox = this.getSvgRoot().getBBox(); ++ var root = this.getSvgRoot(); //***lyn ++ var bBox = root.getBBox(); //***lyn ++ var height = bBox.height; ++ ++ } catch (e) { ++ // Firefox has trouble with hidden elements (Bug 528969). ++ return {height: 0, width: 0}; ++ } ++ if (Blockly.BROKEN_CONTROL_POINTS) { ++ /* HACK: ++ WebKit bug 67298 causes control points to be included in the reported ++ bounding box. The render functions (below) add two 5px spacer control ++ points that we need to subtract. ++ */ ++ height -= 10; ++ if (this.nextConnection) { ++ // Bottom control point partially masked by lower tab. ++ height += 4; ++ } ++ } ++ // Subtract one from the height due to the shadow. ++ height -= 1; ++ result = {height: height, width: bBox.width}; //Why is width handled differently here ++ } ++ var stop = new Date().getTime(); ++ var timeDiff = stop - start; ++ Blockly.Instrument.stats.getHeightWidthCalls++; ++ Blockly.Instrument.stats.getHeightWidthTime += timeDiff; ++ return result; ++ }; ++ ++ /** ++ * Neil's getHeightWidth ++ */ ++ Blockly.Block.prototype.getHeightWidthNeil = function() { + var height = this.svg_.height; + var width = this.svg_.width; + // Recursively add size of subsequent blocks. + var nextBlock = this.getNextBlock(); + if (nextBlock) { ++ var nextHeightWidth = nextBlock.getHeightWidthNeil(); + height += nextHeightWidth.height - 4; // Height of tab. + width = Math.max(width, nextHeightWidth.width); + } + return {height: height, width: width}; ++ } + + /** + * Handle a mouse-down on an SVG block. +*************** +*** 474,480 **** + * @private + */ + Blockly.Block.prototype.onMouseUp_ = function(e) { + var this_ = this; + Blockly.doCommand(function() { + Blockly.terminateDrag_(); + if (Blockly.selected && Blockly.highlightedConnection_) { +--- 549,558 ---- + * @private + */ + Blockly.Block.prototype.onMouseUp_ = function(e) { ++ var start = new Date().getTime(); ++ Blockly.Instrument.initializeStats("onMouseUp"); + var this_ = this; ++ Blockly.resetWorkspaceArrangements(); + Blockly.doCommand(function() { + Blockly.terminateDrag_(); + if (Blockly.selected && Blockly.highlightedConnection_) { +*************** +*** 509,514 **** + Blockly.highlightedConnection_ = null; + } + }); + }; + + /** +--- 587,602 ---- + Blockly.highlightedConnection_ = null; + } + }); ++ if (! Blockly.Instrument.avoidRenderWorkspaceInMouseUp) { ++ // [lyn, 04/01/14] rendering a workspace takes a *long* time and is *not* necessary! ++ // This is the key source of the laggy drag problem. Remove it! ++ Blockly.mainWorkspace.render(); ++ } ++ Blockly.WarningHandler.checkAllBlocksForWarningsAndErrors(); ++ var stop = new Date().getTime(); ++ var timeDiff = stop - start; ++ Blockly.Instrument.stats.totalTime = timeDiff; ++ Blockly.Instrument.displayStats("onMouseUp"); + }; + + /** +*************** +*** 833,850 **** + + /** + * Get the colour of a block. +- * @return {number} HSV hue value. + */ + Blockly.Block.prototype.getColour = function() { +- return this.colourHue_; + }; + + /** + * Change the colour of a block. +- * @param {number} colourHue HSV hue value. + */ +- Blockly.Block.prototype.setColour = function(colourHue) { +- this.colourHue_ = colourHue; + if (this.svg_) { + this.svg_.updateColour(); + } +--- 924,954 ---- + + /** + * Get the colour of a block. ++ * @return {number|Array} HSV hue value or RGB Array. + */ + Blockly.Block.prototype.getColour = function() { ++ return (this.rgbArray_ == null ? this.colourHue_ : this.rgbArray_); + }; + + /** + * Change the colour of a block. ++ * @param {number|Array} hueOrRGBArray HSV hue value or array of RGB values. + */ ++ Blockly.Block.prototype.setColour = function(hueOrRGBArray) { ++ if(Array.isArray(hueOrRGBArray)) { ++ this.rgbArray_ = hueOrRGBArray; ++ this.colourHue_ = null; ++ } else { ++ this.colourHue_ = hueOrRGBArray; ++ this.rgbArray_ = null; ++ } ++ this.updateColour(); ++ }; ++ ++ /** ++ * Update the colour of a block. ++ */ ++ Blockly.Block.prototype.updateColour = function() { + if (this.svg_) { + this.svg_.updateColour(); + } +*************** +*** 852,857 **** + for (var x = 0; x < icons.length; x++) { + icons[x].updateColour(); + } + if (this.rendered) { + // Bump every dropdown to change its colour. + for (var x = 0, input; input = this.inputList[x]; x++) { +--- 956,964 ---- + for (var x = 0; x < icons.length; x++) { + icons[x].updateColour(); + } ++ if (this.errorIcon) { ++ this.errorIcon.updateColour(); ++ } + if (this.rendered) { + // Bump every dropdown to change its colour. + for (var x = 0, input; input = this.inputList[x]; x++) { +*************** +*** 1095,1105 **** + if (this.collapsed_ == collapsed) { + return; + } + this.collapsed_ = collapsed; + var renderList = []; + // Show/hide the inputs. +- for (var x = 0, input; input = this.inputList[x]; x++) { + renderList.push.apply(renderList, input.setVisible(!collapsed)); + } + + var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; +--- 1202,1225 ---- + if (this.collapsed_ == collapsed) { + return; + } ++ var start = new Date().getTime(); + this.collapsed_ = collapsed; + var renderList = []; ++ // //Prepare the string for collapsing if needed ++ // if (collapsed){ ++ // if (this.prepareCollapsedText && goog.isFunction(this.prepareCollapsedText)) ++ // this.prepareCollapsedText(); ++ // } + // Show/hide the inputs. ++ if (Blockly.Instrument.useRenderDown) { ++ for (var x = 0, input; input = this.inputList[x]; x++) { ++ // No need to collect renderList if rendering down. ++ input.setVisible(!collapsed); ++ } ++ } else { ++ for (var x = 0, input; input = this.inputList[x]; x++) { + renderList.push.apply(renderList, input.setVisible(!collapsed)); ++ } + } + + var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; +*************** +*** 1108,1113 **** + for (var x = 0; x < icons.length; x++) { + icons[x].setVisible(false); + } + var text = this.toString(Blockly.COLLAPSE_CHARS); + this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text); + } else { +--- 1228,1236 ---- + for (var x = 0; x < icons.length; x++) { + icons[x].setVisible(false); + } ++ if (this.errorIcon) { ++ this.errorIcon.setVisible(false); ++ } + var text = this.toString(Blockly.COLLAPSE_CHARS); + this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text); + } else { +*************** +*** 1118,1129 **** + // No child blocks, just render this block. + renderList[0] = this; + } + if (this.rendered) { +- for (var x = 0, block; block = renderList[x]; x++) { +- block.render(); + } + this.bumpNeighbours_(); + } + }; + + /** +--- 1241,1264 ---- + // No child blocks, just render this block. + renderList[0] = this; + } ++ + if (this.rendered) { ++ if (Blockly.Instrument.useRenderDown) { ++ this.renderDown(); ++ } else { ++ for (var x = 0, block; block = renderList[x]; x++) { ++ block.render(); ++ } + } + this.bumpNeighbours_(); + } ++ ++ var stop = new Date().getTime(); ++ var timeDiff = stop - start; ++ if (! collapsed) { ++ Blockly.Instrument.stats.expandCollapsedCalls++; ++ Blockly.Instrument.stats.expandCollapsedTime += timeDiff; ++ } + }; + + /** +*************** +*** 1174,1180 **** + */ + Blockly.Block.prototype.appendInput_ = function(type, name) { + var connection = null; +- if (type == Blockly.INPUT_VALUE || type == Blockly.NEXT_STATEMENT) { + connection = new Blockly.Connection(this, type); + } + var input = new Blockly.Input(type, name, this, connection); +--- 1320,1326 ---- + */ + Blockly.Block.prototype.appendInput_ = function(type, name) { + var connection = null; ++ if (type == Blockly.INPUT_VALUE || type == Blockly.NEXT_STATEMENT || type == Blockly.INDENTED_VALUE) { + connection = new Blockly.Connection(this, type); + } + var input = new Blockly.Input(type, name, this, connection); +*************** +*** 1389,1400 **** + }; + + /** + * Render the block. + * Lays out and reflows a block based on its contents and settings. + */ + Blockly.Block.prototype.render = function() { +- goog.asserts.assertObject(this.svg_, +- 'Uninitialized block cannot be rendered. Call block.initSvg()'); +- this.svg_.render(); +- Blockly.Realtime.blockChanged(this); + }; +--- 1535,1609 ---- + }; + + /** ++ * Set this block's warning text. ++ * @param {?string} text The text, or null to delete. ++ */ ++ Blockly.Block.prototype.setErrorIconText = function(text) { ++ if (!Blockly.ErrorIcon) { ++ throw 'Warnings not supported.'; ++ } ++ var changedState = false; ++ if (goog.isString(text)) { ++ if (!this.errorIcon) { ++ this.errorIcon = new Blockly.ErrorIcon(this); ++ changedState = true; ++ } ++ this.errorIcon.setText(/** @type {string} */ (text)); ++ } else { ++ if (this.errorIcon) { ++ this.errorIcon.dispose(); ++ changedState = true; ++ } ++ } ++ if (this.rendered) { ++ this.render(); ++ if (changedState) { ++ // Adding or removing a warning icon will cause the block to change shape. ++ this.bumpNeighbours_(); ++ } ++ } ++ }; ++ ++ /** ++ * [lyn, 04/01/14] Global flag to control whether rendering is done. ++ * There is no need to render blocks in Blocky.SaveFile.load. ++ * We only need to render them when a Screen is loaded in the Blockly editor. ++ * This flag is used to turn off rendering for first case and turn it on for the second. ++ * @type {boolean} ++ */ ++ Blockly.Block.isRenderingOn = true; ++ ++ /** + * Render the block. + * Lays out and reflows a block based on its contents and settings. + */ + Blockly.Block.prototype.render = function() { ++ if (Blockly.Block.isRenderingOn) { ++ goog.asserts.assertObject(this.svg_, ++ 'Uninitialized block cannot be rendered. Call block.initSvg()'); ++ this.svg_.render(); ++ if (Blockly.Realtime.isEnabled() && !Blockly.Realtime.withinSync) { ++ Blockly.Realtime.blockChanged(this); ++ } ++ Blockly.Instrument.stats.renderCalls++; ++ // [lyn, 04/08/14] Because render is recursive, doesn't make sense to track its time here. ++ } ++ }; ++ ++ /** ++ * [lyn, 04/01/14] Render a tree of blocks from top down rather than bottom up. ++ * This is in contrast to render(), which renders a block and all its antecedents. ++ */ ++ Blockly.Block.prototype.renderDown = function() { ++ if (Blockly.Block.isRenderingOn) { ++ goog.asserts.assertObject(this.svg_, ++ ' Uninitialized block cannot be renderedDown. Call block.initSvg()'); ++ this.svg_.renderDown(); ++ Blockly.Instrument.stats.renderDownCalls++; //***lyn ++ if (Blockly.Realtime.isEnabled() && !Blockly.Realtime.withinSync) { ++ Blockly.Realtime.blockChanged(this); ++ } ++ } ++ // [lyn, 04/08/14] Because renderDown is recursive, doesn't make sense to track its time here. + }; ++ diff --git a/core/block_svg.js.orig b/core/block_svg.js.orig new file mode 100644 index 000000000..4d4a7299b --- /dev/null +++ b/core/block_svg.js.orig @@ -0,0 +1,1629 @@ +/** + * @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 Methods for graphically rendering a block as SVG. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.BlockSvg'); + +goog.require('Blockly.Block'); +goog.require('Blockly.ContextMenu'); +goog.require('Blockly.RenderedConnection'); +goog.require('goog.Timer'); +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Class for a block's SVG representation. + * Not normally called directly, workspace.newBlock() is preferred. + * @param {!Blockly.Workspace} workspace The block's workspace. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @extends {Blockly.Block} + * @constructor + */ +Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { + // Create core elements for the block. + /** + * @type {SVGElement} + * @private + */ + this.svgGroup_ = Blockly.createSvgElement('g', {}, null); + + /** + * @type {SVGElement} + * @private + */ + this.svgPathDark_ = Blockly.createSvgElement('path', + {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'}, + this.svgGroup_); + + /** + * @type {SVGElement} + * @private + */ + this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'}, + this.svgGroup_); + + /** + * @type {SVGElement} + * @private + */ + this.svgPathLight_ = Blockly.createSvgElement('path', + {'class': 'blocklyPathLight'}, this.svgGroup_); + this.svgPath_.tooltip = this; + + /** @type {boolean} */ + this.rendered = false; + + Blockly.Tooltip.bindMouseEvents(this.svgPath_); + Blockly.BlockSvg.superClass_.constructor.call(this, + workspace, prototypeName, opt_id); +}; +goog.inherits(Blockly.BlockSvg, Blockly.Block); + +/** + * Height of this block, not including any statement blocks above or below. + */ +Blockly.BlockSvg.prototype.height = 0; +/** + * Width of this block, including any connected value blocks. + */ +Blockly.BlockSvg.prototype.width = 0; + +/** + * Original location of block being dragged. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.BlockSvg.prototype.dragStartXY_ = null; + +/** + * Constant for identifying rows that are to be rendered inline. + * Don't collide with Blockly.INPUT_VALUE and friends. + * @const + */ +Blockly.BlockSvg.INLINE = -1; + +/** + * Create and initialize the SVG representation of the block. + * May be called more than once. + */ +Blockly.BlockSvg.prototype.initSvg = function() { + goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.'); + for (var i = 0, input; input = this.inputList[i]; i++) { + input.init(); + } + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].createIcon(); + } + this.updateColour(); + this.updateMovable(); + if (!this.workspace.options.readOnly && !this.eventsInit_) { + Blockly.bindEvent_(this.getSvgRoot(), 'mousedown', this, + this.onMouseDown_); + var thisBlock = this; + Blockly.bindEvent_(this.getSvgRoot(), 'touchstart', null, + function(e) {Blockly.longStart_(e, thisBlock);}); + } + this.eventsInit_ = true; + + if (!this.getSvgRoot().parentNode) { + this.workspace.getCanvas().appendChild(this.getSvgRoot()); + } +}; + +/** + * Select this block. Highlight it visually. + */ +Blockly.BlockSvg.prototype.select = function() { + if (this.isShadow() && this.getParent()) { + // Shadow blocks should not be selected. + this.getParent().select(); + return; + } + if (Blockly.selected == this) { + return; + } + var oldId = null; + if (Blockly.selected) { + oldId = Blockly.selected.id; + // Unselect any previously selected block. + Blockly.Events.disable(); + try { + Blockly.selected.unselect(); + } finally { + Blockly.Events.enable(); + } + } + var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id); + event.workspaceId = this.workspace.id; + Blockly.Events.fire(event); + Blockly.selected = this; + this.addSelect(); +}; + +/** + * Unselect this block. Remove its highlighting. + */ +Blockly.BlockSvg.prototype.unselect = function() { + if (Blockly.selected != this) { + return; + } + var event = new Blockly.Events.Ui(null, 'selected', this.id, null); + event.workspaceId = this.workspace.id; + Blockly.Events.fire(event); + Blockly.selected = null; + this.removeSelect(); +}; + +/** + * Block's mutator icon (if any). + * @type {Blockly.Mutator} + */ +Blockly.BlockSvg.prototype.mutator = null; + +/** + * Block's comment icon (if any). + * @type {Blockly.Comment} + */ +Blockly.BlockSvg.prototype.comment = null; + +/** + * Block's warning icon (if any). + * @type {Blockly.Warning} + */ +Blockly.BlockSvg.prototype.warning = null; + +/** + * Returns a list of mutator, comment, and warning icons. + * @return {!Array} List of icons. + */ +Blockly.BlockSvg.prototype.getIcons = function() { + var icons = []; + if (this.mutator) { + icons.push(this.mutator); + } + if (this.comment) { + icons.push(this.comment); + } + if (this.warning) { + icons.push(this.warning); + } + return icons; +}; + +/** + * Wrapper function called when a mouseUp occurs during a drag operation. + * @type {Array.} + * @private + */ +Blockly.BlockSvg.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mouseMove occurs during a drag operation. + * @type {Array.} + * @private + */ +Blockly.BlockSvg.onMouseMoveWrapper_ = null; + +/** + * Stop binding to the global mouseup and mousemove events. + * @package + */ +Blockly.BlockSvg.terminateDrag = function() { + Blockly.BlockSvg.disconnectUiStop_(); + if (Blockly.BlockSvg.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_); + Blockly.BlockSvg.onMouseUpWrapper_ = null; + } + if (Blockly.BlockSvg.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.BlockSvg.onMouseMoveWrapper_); + Blockly.BlockSvg.onMouseMoveWrapper_ = null; + } + var selected = Blockly.selected; + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Terminate a drag operation. + if (selected) { + // Update the connection locations. + var xy = selected.getRelativeToSurfaceXY(); + var dxy = goog.math.Coordinate.difference(xy, selected.dragStartXY_); + var event = new Blockly.Events.Move(selected); + event.oldCoordinate = selected.dragStartXY_; + event.recordNew(); + Blockly.Events.fire(event); + + selected.moveConnections_(dxy.x, dxy.y); + delete selected.draggedBubbles_; + selected.setDragging_(false); + selected.render(); + // Ensure that any stap and bump are part of this move's event group. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + Blockly.Events.setGroup(group); + selected.snapToGrid(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY / 2); + setTimeout(function() { + Blockly.Events.setGroup(group); + selected.bumpNeighbours_(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY); + // Fire an event to allow scrollbars to resize. + selected.workspace.resizeContents(); + } + } + Blockly.dragMode_ = Blockly.DRAG_NONE; + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); +}; + +/** + * Set parent of this block to be a new block or null. + * @param {Blockly.BlockSvg} newParent New parent block. + */ +Blockly.BlockSvg.prototype.setParent = function(newParent) { + if (newParent == this.parentBlock_) { + return; + } + var svgRoot = this.getSvgRoot(); + if (this.parentBlock_ && svgRoot) { + // Move this block up the DOM. Keep track of x/y translations. + var xy = this.getRelativeToSurfaceXY(); + this.workspace.getCanvas().appendChild(svgRoot); + svgRoot.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')'); + } + + Blockly.Field.startCache(); + Blockly.BlockSvg.superClass_.setParent.call(this, newParent); + Blockly.Field.stopCache(); + + if (newParent) { + var oldXY = this.getRelativeToSurfaceXY(); + newParent.getSvgRoot().appendChild(svgRoot); + var newXY = this.getRelativeToSurfaceXY(); + // Move the connections to match the child's new position. + this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y); + } +}; + +/** + * Return the coordinates of the top-left corner of this block relative to the + * drawing surface's origin (0,0). + * @return {!goog.math.Coordinate} Object with .x and .y properties. + */ +Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() { + var x = 0; + var y = 0; + var element = this.getSvgRoot(); + if (element) { + do { + // Loop through this block and every parent. + var xy = Blockly.getRelativeXY_(element); + x += xy.x; + y += xy.y; + element = element.parentNode; + } while (element && element != this.workspace.getCanvas()); + } + return new goog.math.Coordinate(x, y); +}; + +/** + * Move a block by a relative offset. + * @param {number} dx Horizontal offset. + * @param {number} dy Vertical offset. + */ +Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { + goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); + var event = new Blockly.Events.Move(this); + var xy = this.getRelativeToSurfaceXY(); + this.getSvgRoot().setAttribute('transform', + 'translate(' + (xy.x + dx) + ',' + (xy.y + dy) + ')'); + this.moveConnections_(dx, dy); + event.recordNew(); + this.workspace.resizeContents(); + Blockly.Events.fire(event); +}; + +/** + * Snap this block to the nearest grid point. + */ +Blockly.BlockSvg.prototype.snapToGrid = function() { + if (!this.workspace) { + return; // Deleted block. + } + if (Blockly.dragMode_ != Blockly.DRAG_NONE) { + return; // Don't bump blocks during a drag. + } + if (this.getParent()) { + return; // Only snap top-level blocks. + } + if (this.isInFlyout) { + return; // Don't move blocks around in a flyout. + } + if (!this.workspace.options.gridOptions || + !this.workspace.options.gridOptions['snap']) { + return; // Config says no snapping. + } + var spacing = this.workspace.options.gridOptions['spacing']; + var half = spacing / 2; + var xy = this.getRelativeToSurfaceXY(); + var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x; + var dy = Math.round((xy.y - half) / spacing) * spacing + half - xy.y; + dx = Math.round(dx); + dy = Math.round(dy); + if (dx != 0 || dy != 0) { + this.moveBy(dx, dy); + } +}; + +/** + * Returns a bounding box describing the dimensions of this block + * and any blocks stacked below it. + * @return {!{height: number, width: number}} Object with height and width + * properties. + */ +Blockly.BlockSvg.prototype.getHeightWidth = function() { + var height = this.height; + var width = this.width; + // Recursively add size of subsequent blocks. + var nextBlock = this.getNextBlock(); + if (nextBlock) { + var nextHeightWidth = nextBlock.getHeightWidth(); + height += nextHeightWidth.height - 4; // Height of tab. + width = Math.max(width, nextHeightWidth.width); + } else if (!this.nextConnection && !this.outputConnection) { + // Add a bit of margin under blocks with no bottom tab. + height += 2; + } + return {height: height, width: width}; +}; + +/** + * Returns the coordinates of a bounding box describing the dimensions of this + * block and any blocks stacked below it. + * @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}} + * Object with top left and bottom right coordinates of the bounding box. + */ +Blockly.BlockSvg.prototype.getBoundingRectangle = function() { + var blockXY = this.getRelativeToSurfaceXY(this); + var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockBounds = this.getHeightWidth(); + var topLeft; + var bottomRight; + if (this.RTL) { + // Width has the tab built into it already so subtract it here. + topLeft = new goog.math.Coordinate(blockXY.x - (blockBounds.width - tab), + blockXY.y); + // Add the width of the tab/puzzle piece knob to the x coordinate + // since X is the corner of the rectangle, not the whole puzzle piece. + bottomRight = new goog.math.Coordinate(blockXY.x + tab, + blockXY.y + blockBounds.height); + } else { + // Subtract the width of the tab/puzzle piece knob to the x coordinate + // since X is the corner of the rectangle, not the whole puzzle piece. + topLeft = new goog.math.Coordinate(blockXY.x - tab, blockXY.y); + // Width has the tab built into it already so subtract it here. + bottomRight = new goog.math.Coordinate(blockXY.x + blockBounds.width - tab, + blockXY.y + blockBounds.height); + } + return {topLeft: topLeft, bottomRight: bottomRight}; +}; + +/** + * Set whether the block is collapsed or not. + * @param {boolean} collapsed True if collapsed. + */ +Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { + if (this.collapsed_ == collapsed) { + return; + } + var renderList = []; + // Show/hide the inputs. + for (var i = 0, input; input = this.inputList[i]; i++) { + renderList.push.apply(renderList, input.setVisible(!collapsed)); + } + + var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; + if (collapsed) { + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].setVisible(false); + } + var text = this.toString(Blockly.COLLAPSE_CHARS); + this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init(); + } else { + this.removeInput(COLLAPSED_INPUT_NAME); + // Clear any warnings inherited from enclosed blocks. + this.setWarningText(null); + } + Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed); + + if (!renderList.length) { + // No child blocks, just render this block. + renderList[0] = this; + } + if (this.rendered) { + for (var i = 0, block; block = renderList[i]; i++) { + block.render(); + } + // Don't bump neighbours. + // Although bumping neighbours would make sense, users often collapse + // all their functions and store them next to each other. Expanding and + // bumping causes all their definitions to go out of alignment. + } +}; + +/** + * Open the next (or previous) FieldTextInput. + * @param {Blockly.Field|Blockly.Block} start Current location. + * @param {boolean} forward If true go forward, otherwise backward. + */ +Blockly.BlockSvg.prototype.tab = function(start, forward) { + // This function need not be efficient since it runs once on a keypress. + // Create an ordered list of all text fields and connected inputs. + var list = []; + for (var i = 0, input; input = this.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + if (field instanceof Blockly.FieldTextInput) { + // TODO: Also support dropdown fields. + list.push(field); + } + } + if (input.connection) { + var block = input.connection.targetBlock(); + if (block) { + list.push(block); + } + } + } + var i = list.indexOf(start); + if (i == -1) { + // No start location, start at the beginning or end. + i = forward ? -1 : list.length; + } + var target = list[forward ? i + 1 : i - 1]; + if (!target) { + // Ran off of list. + var parent = this.getParent(); + if (parent) { + parent.tab(this, forward); + } + } else if (target instanceof Blockly.Field) { + target.showEditor_(); + } else { + target.tab(null, forward); + } +}; + +/** + * Handle a mouse-down on an SVG block. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseDown_ = function(e) { + if (this.workspace.options.readOnly) { + return; + } + if (this.isInFlyout) { + return; + } + if (this.isInMutator) { + // Mutator's coordinate system could be out of date because the bubble was + // dragged, the block was moved, the parent workspace zoomed, etc. + this.workspace.resize(); + } + + this.workspace.updateScreenCalculationsIfScrolled(); + this.workspace.markFocused(); + Blockly.terminateDrag_(); + this.select(); + Blockly.hideChaff(); + if (Blockly.isRightButton(e)) { + // Right-click. + this.showContextMenu_(e); + } else if (!this.isMovable()) { + // Allow immovable blocks to be selected and context menued, but not + // dragged. Let this event bubble up to document, so the workspace may be + // dragged instead. + return; + } else { + if (!Blockly.Events.getGroup()) { + Blockly.Events.setGroup(true); + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.dragStartXY_ = this.getRelativeToSurfaceXY(); + this.workspace.startDrag(e, this.dragStartXY_); + + Blockly.dragMode_ = Blockly.DRAG_STICKY; + Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, this.onMouseUp_); + Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.onMouseMove_); + // Build a list of bubbles that need to be moved and where they started. + this.draggedBubbles_ = []; + var descendants = this.getDescendants(); + for (var i = 0, descendant; descendant = descendants[i]; i++) { + var icons = descendant.getIcons(); + for (var j = 0; j < icons.length; j++) { + var data = icons[j].getIconLocation(); + data.bubble = icons[j]; + this.draggedBubbles_.push(data); + } + } + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Handle a mouse-up anywhere in the SVG pane. Is only registered when a + * block is clicked. We can't use mouseUp on the block since a fast-moving + * cursor can briefly escape the block before it catches up. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseUp_ = function(e) { + if (Blockly.dragMode_ != Blockly.DRAG_FREE && + !Blockly.WidgetDiv.isVisible()) { + Blockly.Events.fire( + new Blockly.Events.Ui(this, 'click', undefined, undefined)); + } + Blockly.terminateDrag_(); + if (Blockly.selected && Blockly.highlightedConnection_) { + // Connect two blocks together. + Blockly.localConnection_.connect(Blockly.highlightedConnection_); + if (this.rendered) { + // Trigger a connection animation. + // Determine which connection is inferior (lower in the source stack). + var inferiorConnection = Blockly.localConnection_.isSuperior() ? + Blockly.highlightedConnection_ : Blockly.localConnection_; + inferiorConnection.getSourceBlock().connectionUiEffect(); + } + if (this.workspace.trashcan) { + // Don't throw an object in the trash can if it just got connected. + this.workspace.trashcan.close(); + } + } else if (!this.getParent() && Blockly.selected.isDeletable() && + this.workspace.isDeleteArea(e)) { + var trashcan = this.workspace.trashcan; + if (trashcan) { + goog.Timer.callOnce(trashcan.close, 100, trashcan); + } + Blockly.selected.dispose(false, true); + } + if (Blockly.highlightedConnection_) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + } + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); + if (!Blockly.WidgetDiv.isVisible()) { + Blockly.Events.setGroup(false); + } +}; + +/** + * Load the block's help page in a new window. + * @private + */ +Blockly.BlockSvg.prototype.showHelp_ = function() { + var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; + if (url) { + window.open(url); + } +}; + +/** + * Show the context menu for this block. + * @param {!Event} e Mouse event. + * @private + */ +Blockly.BlockSvg.prototype.showContextMenu_ = function(e) { + if (this.workspace.options.readOnly || !this.contextMenu) { + return; + } + // Save the current block in a variable for use in closures. + var block = this; + var menuOptions = []; + + if (this.isDeletable() && this.isMovable() && !block.isInFlyout) { + // Option to duplicate this block. + var duplicateOption = { + text: Blockly.Msg.DUPLICATE_BLOCK, + enabled: true, + callback: function() { + Blockly.duplicate_(block); + } + }; + if (this.getDescendants().length > this.workspace.remainingCapacity()) { + duplicateOption.enabled = false; + } + menuOptions.push(duplicateOption); + + if (this.isEditable() && !this.collapsed_ && + this.workspace.options.comments) { + // Option to add/remove a comment. + var commentOption = {enabled: !goog.userAgent.IE}; + if (this.comment) { + commentOption.text = Blockly.Msg.REMOVE_COMMENT; + commentOption.callback = function() { + block.setCommentText(null); + }; + } else { + commentOption.text = Blockly.Msg.ADD_COMMENT; + commentOption.callback = function() { + block.setCommentText(''); + }; + } + menuOptions.push(commentOption); + } + + // Option to make block inline. + if (!this.collapsed_) { + for (var i = 1; i < this.inputList.length; i++) { + if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT && + this.inputList[i].type != Blockly.NEXT_STATEMENT) { + // Only display this option if there are two value or dummy inputs + // next to each other. + var inlineOption = {enabled: true}; + var isInline = this.getInputsInline(); + inlineOption.text = isInline ? + Blockly.Msg.EXTERNAL_INPUTS : Blockly.Msg.INLINE_INPUTS; + inlineOption.callback = function() { + block.setInputsInline(!isInline); + }; + menuOptions.push(inlineOption); + break; + } + } + } + + if (this.workspace.options.collapse) { + // Option to collapse/expand block. + if (this.collapsed_) { + var expandOption = {enabled: true}; + expandOption.text = Blockly.Msg.EXPAND_BLOCK; + expandOption.callback = function() { + block.setCollapsed(false); + }; + menuOptions.push(expandOption); + } else { + var collapseOption = {enabled: true}; + collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK; + collapseOption.callback = function() { + block.setCollapsed(true); + }; + menuOptions.push(collapseOption); + } + } + + if (this.workspace.options.disable) { + // Option to disable/enable block. + var disableOption = { + text: this.disabled ? + Blockly.Msg.ENABLE_BLOCK : Blockly.Msg.DISABLE_BLOCK, + enabled: !this.getInheritedDisabled(), + callback: function() { + block.setDisabled(!block.disabled); + } + }; + menuOptions.push(disableOption); + } + + // Option to delete this block. + // Count the number of blocks that are nested in this block. + var descendantCount = this.getDescendants().length; + var nextBlock = this.getNextBlock(); + if (nextBlock) { + // Blocks in the current stack would survive this block's deletion. + descendantCount -= nextBlock.getDescendants().length; + } + var deleteOption = { + text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK : + Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)), + enabled: true, + callback: function() { + Blockly.Events.setGroup(true); + block.dispose(true, true); + Blockly.Events.setGroup(false); + } + }; + menuOptions.push(deleteOption); + } + + // Option to get help. + var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl; + var helpOption = {enabled: !!url}; + helpOption.text = Blockly.Msg.HELP; + helpOption.callback = function() { + block.showHelp_(); + }; + menuOptions.push(helpOption); + + // Allow the block to add or modify menuOptions. + if (this.customContextMenu && !block.isInFlyout) { + this.customContextMenu(menuOptions); + } + + Blockly.ContextMenu.show(e, menuOptions, this.RTL); + Blockly.ContextMenu.currentBlock = this; +}; + +/** + * Move the connections for this block and all blocks attached under it. + * Also update any attached bubbles. + * @param {number} dx Horizontal offset from current location. + * @param {number} dy Vertical offset from current location. + * @private + */ +Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) { + if (!this.rendered) { + // Rendering is required to lay out the blocks. + // This is probably an invisible block attached to a collapsed block. + return; + } + var myConnections = this.getConnections_(false); + for (var i = 0; i < myConnections.length; i++) { + myConnections[i].moveBy(dx, dy); + } + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].computeIconLocation(); + } + + // Recurse through all blocks attached under this one. + for (var i = 0; i < this.childBlocks_.length; i++) { + this.childBlocks_[i].moveConnections_(dx, dy); + } +}; + +/** + * Recursively adds or removes the dragging class to this node and its children. + * @param {boolean} adding True if adding, false if removing. + * @private + */ +Blockly.BlockSvg.prototype.setDragging_ = function(adding) { + if (adding) { + var group = this.getSvgRoot(); + group.translate_ = ''; + group.skew_ = ''; + this.addDragging(); + Blockly.draggingConnections_ = + Blockly.draggingConnections_.concat(this.getConnections_(true)); + } else { + this.removeDragging(); + Blockly.draggingConnections_ = []; + } + // Recurse through all blocks attached under this one. + for (var i = 0; i < this.childBlocks_.length; i++) { + this.childBlocks_[i].setDragging_(adding); + } +}; + +/** + * Drag this block to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.BlockSvg.prototype.onMouseMove_ = function(e) { + if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 && + e.button == 0) { + /* HACK: + Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove + events on certain touch actions. Ignore events with these signatures. + This may result in a one-pixel blind spot in other browsers, + but this shouldn't be noticeable. */ + e.stopPropagation(); + return; + } + + var oldXY = this.getRelativeToSurfaceXY(); + var newXY = this.workspace.moveDrag(e); + + if (Blockly.dragMode_ == Blockly.DRAG_STICKY) { + // Still dragging within the sticky DRAG_RADIUS. + var dr = goog.math.Coordinate.distance(oldXY, newXY) * this.workspace.scale; + if (dr > Blockly.DRAG_RADIUS) { + // Switch to unrestricted dragging. + Blockly.dragMode_ = Blockly.DRAG_FREE; + Blockly.longStop_(); + if (this.parentBlock_) { + // Push this block to the very top of the stack. + this.unplug(); + var group = this.getSvgRoot(); + group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')'; + this.disconnectUiEffect(); + } + this.setDragging_(true); + } + } + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Unrestricted dragging. + var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_); + var group = this.getSvgRoot(); + group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')'; + group.setAttribute('transform', group.translate_ + group.skew_); + // Drag all the nested bubbles. + for (var i = 0; i < this.draggedBubbles_.length; i++) { + var commentData = this.draggedBubbles_[i]; + commentData.bubble.setIconLocation( + goog.math.Coordinate.sum(commentData, dxy)); + } + + // Check to see if any of this block's connections are within range of + // another block's connection. + var myConnections = this.getConnections_(false); + // Also check the last connection on this stack + var lastOnStack = this.lastConnectionInStack_(); + if (lastOnStack && lastOnStack != this.nextConnection) { + myConnections.push(lastOnStack); + } + var closestConnection = null; + var localConnection = null; + var radiusConnection = Blockly.SNAP_RADIUS; + for (var i = 0; i < myConnections.length; i++) { + var myConnection = myConnections[i]; + var neighbour = myConnection.closest(radiusConnection, dxy); + if (neighbour.connection) { + closestConnection = neighbour.connection; + localConnection = myConnection; + radiusConnection = neighbour.radius; + } + } + + // Remove connection highlighting if needed. + if (Blockly.highlightedConnection_ && + Blockly.highlightedConnection_ != closestConnection) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + Blockly.localConnection_ = null; + } + // Add connection highlighting if needed. + if (closestConnection && + closestConnection != Blockly.highlightedConnection_) { + closestConnection.highlight(); + Blockly.highlightedConnection_ = closestConnection; + Blockly.localConnection_ = localConnection; + } + // Provide visual indication of whether the block will be deleted if + // dropped here. + if (this.isDeletable()) { + this.workspace.isDeleteArea(e); + } + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Add or remove the UI indicating if this block is movable or not. + */ +Blockly.BlockSvg.prototype.updateMovable = function() { + if (this.isMovable()) { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDraggable'); + } else { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDraggable'); + } +}; + +/** + * Set whether this block is movable or not. + * @param {boolean} movable True if movable. + */ +Blockly.BlockSvg.prototype.setMovable = function(movable) { + Blockly.BlockSvg.superClass_.setMovable.call(this, movable); + this.updateMovable(); +}; + +/** + * Set whether this block is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.BlockSvg.prototype.setEditable = function(editable) { + Blockly.BlockSvg.superClass_.setEditable.call(this, editable); + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].updateEditable(); + } +}; + +/** + * Set whether this block is a shadow block or not. + * @param {boolean} shadow True if a shadow. + */ +Blockly.BlockSvg.prototype.setShadow = function(shadow) { + Blockly.BlockSvg.superClass_.setShadow.call(this, shadow); + this.updateColour(); +}; + +/** + * Return the root node of the SVG or null if none exists. + * @return {Element} The root SVG node (probably a group). + */ +Blockly.BlockSvg.prototype.getSvgRoot = function() { + return this.svgGroup_; +}; + +/** + * Dispose of this block. + * @param {boolean} healStack If true, then try to heal any gap by connecting + * the next statement with the previous statement. Otherwise, dispose of + * all children of this block. + * @param {boolean} animate If true, show a disposal animation and sound. + */ +Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { + if (!this.workspace) { + // The block has already been deleted. + return; + } + Blockly.Tooltip.hide(); + Blockly.Field.startCache(); + // Save the block's workspace temporarily so we can resize the + // contents once the block is disposed. + var blockWorkspace = this.workspace; + // If this block is being dragged, unlink the mouse events. + if (Blockly.selected == this) { + this.unselect(); + Blockly.terminateDrag_(); + } + // If this block has a context menu open, close it. + if (Blockly.ContextMenu.currentBlock == this) { + Blockly.ContextMenu.hide(); + } + + if (animate && this.rendered) { + this.unplug(healStack); + this.disposeUiEffect(); + } + // Stop rerendering. + this.rendered = false; + + Blockly.Events.disable(); + try { + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].dispose(); + } + } finally { + Blockly.Events.enable(); + } + Blockly.BlockSvg.superClass_.dispose.call(this, healStack); + + goog.dom.removeNode(this.svgGroup_); + blockWorkspace.resizeContents(); + // Sever JavaScript to DOM connections. + this.svgGroup_ = null; + this.svgPath_ = null; + this.svgPathLight_ = null; + this.svgPathDark_ = null; + Blockly.Field.stopCache(); +}; + +/** + * Play some UI effects (sound, animation) when disposing of a block. + */ +Blockly.BlockSvg.prototype.disposeUiEffect = function() { + this.workspace.playAudio('delete'); + + var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_), + this.workspace); + // Deeply clone the current block. + var clone = this.svgGroup_.cloneNode(true); + clone.translateX_ = xy.x; + clone.translateY_ = xy.y; + clone.setAttribute('transform', + 'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')'); + this.workspace.getParentSvg().appendChild(clone); + clone.bBox_ = clone.getBBox(); + // Start the animation. + Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date, + this.workspace.scale); +}; + +/** + * Animate a cloned block and eventually dispose of it. + * This is a class method, not an instace method since the original block has + * been destroyed and is no longer accessible. + * @param {!Element} clone SVG element to animate and dispose of. + * @param {boolean} rtl True if RTL, false if LTR. + * @param {!Date} start Date of animation's start. + * @param {number} workspaceScale Scale of workspace. + * @private + */ +Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) { + var ms = new Date - start; + var percent = ms / 150; + if (percent > 1) { + goog.dom.removeNode(clone); + } else { + var x = clone.translateX_ + + (rtl ? -1 : 1) * clone.bBox_.width * workspaceScale / 2 * percent; + var y = clone.translateY_ + clone.bBox_.height * workspaceScale * percent; + var scale = (1 - percent) * workspaceScale; + clone.setAttribute('transform', 'translate(' + x + ',' + y + ')' + + ' scale(' + scale + ')'); + var closure = function() { + Blockly.BlockSvg.disposeUiStep_(clone, rtl, start, workspaceScale); + }; + setTimeout(closure, 10); + } +}; + +/** + * Play some UI effects (sound, ripple) after a connection has been established. + */ +Blockly.BlockSvg.prototype.connectionUiEffect = function() { + this.workspace.playAudio('click'); + if (this.workspace.scale < 1) { + return; // Too small to care about visual effects. + } + // Determine the absolute coordinates of the inferior block. + var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_), + this.workspace); + // Offset the coordinates based on the two connection types, fix scale. + if (this.outputConnection) { + xy.x += (this.RTL ? 3 : -3) * this.workspace.scale; + xy.y += 13 * this.workspace.scale; + } else if (this.previousConnection) { + xy.x += (this.RTL ? -23 : 23) * this.workspace.scale; + xy.y += 3 * this.workspace.scale; + } + var ripple = Blockly.createSvgElement('circle', + {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none', + 'stroke': '#888', 'stroke-width': 10}, + this.workspace.getParentSvg()); + // Start the animation. + Blockly.BlockSvg.connectionUiStep_(ripple, new Date, this.workspace.scale); +}; + +/** + * Expand a ripple around a connection. + * @param {!Element} ripple Element to animate. + * @param {!Date} start Date of animation's start. + * @param {number} workspaceScale Scale of workspace. + * @private + */ +Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) { + var ms = new Date - start; + var percent = ms / 150; + if (percent > 1) { + goog.dom.removeNode(ripple); + } else { + ripple.setAttribute('r', percent * 25 * workspaceScale); + ripple.style.opacity = 1 - percent; + var closure = function() { + Blockly.BlockSvg.connectionUiStep_(ripple, start, workspaceScale); + }; + Blockly.BlockSvg.disconnectUiStop_.pid_ = setTimeout(closure, 10); + } +}; + +/** + * Play some UI effects (sound, animation) when disconnecting a block. + */ +Blockly.BlockSvg.prototype.disconnectUiEffect = function() { + this.workspace.playAudio('disconnect'); + if (this.workspace.scale < 1) { + return; // Too small to care about visual effects. + } + // Horizontal distance for bottom of block to wiggle. + var DISPLACEMENT = 10; + // Scale magnitude of skew to height of block. + var height = this.getHeightWidth().height; + var magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180; + if (!this.RTL) { + magnitude *= -1; + } + // Start the animation. + Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date); +}; + +/** + * Animate a brief wiggle of a disconnected block. + * @param {!Element} group SVG element to animate. + * @param {number} magnitude Maximum degrees skew (reversed for RTL). + * @param {!Date} start Date of animation's start. + * @private + */ +Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) { + var DURATION = 200; // Milliseconds. + var WIGGLES = 3; // Half oscillations. + + var ms = new Date - start; + var percent = ms / DURATION; + + if (percent > 1) { + group.skew_ = ''; + } else { + var skew = Math.round(Math.sin(percent * Math.PI * WIGGLES) * + (1 - percent) * magnitude); + group.skew_ = 'skewX(' + skew + ')'; + var closure = function() { + Blockly.BlockSvg.disconnectUiStep_(group, magnitude, start); + }; + Blockly.BlockSvg.disconnectUiStop_.group = group; + Blockly.BlockSvg.disconnectUiStop_.pid = setTimeout(closure, 10); + } + group.setAttribute('transform', group.translate_ + group.skew_); +}; + +/** + * Stop the disconnect UI animation immediately. + * @private + */ +Blockly.BlockSvg.disconnectUiStop_ = function() { + if (Blockly.BlockSvg.disconnectUiStop_.group) { + clearTimeout(Blockly.BlockSvg.disconnectUiStop_.pid); + var group = Blockly.BlockSvg.disconnectUiStop_.group; + group.skew_ = ''; + group.setAttribute('transform', group.translate_); + Blockly.BlockSvg.disconnectUiStop_.group = null; + } +}; + +/** + * PID of disconnect UI animation. There can only be one at a time. + * @type {number} + */ +Blockly.BlockSvg.disconnectUiStop_.pid = 0; + +/** + * SVG group of wobbling block. There can only be one at a time. + * @type {Element} + */ +Blockly.BlockSvg.disconnectUiStop_.group = null; + +/** + * Change the colour of a block. + */ +Blockly.BlockSvg.prototype.updateColour = function() { + if (this.disabled) { + // Disabled blocks don't have colour. + return; + } + var hexColour = this.getColour(); + var rgb = goog.color.hexToRgb(hexColour); + if (this.isShadow()) { + rgb = goog.color.lighten(rgb, 0.6); + hexColour = goog.color.rgbArrayToHex(rgb); + this.svgPathLight_.style.display = 'none'; + this.svgPathDark_.setAttribute('fill', hexColour); + } else { + this.svgPathLight_.style.display = ''; + var hexLight = goog.color.rgbArrayToHex(goog.color.lighten(rgb, 0.3)); + var hexDark = goog.color.rgbArrayToHex(goog.color.darken(rgb, 0.2)); + this.svgPathLight_.setAttribute('stroke', hexLight); + this.svgPathDark_.setAttribute('fill', hexDark); + } + this.svgPath_.setAttribute('fill', hexColour); + + var icons = this.getIcons(); + for (var i = 0; i < icons.length; i++) { + icons[i].updateColour(); + } + + // Bump every dropdown to change its colour. + for (var x = 0, input; input = this.inputList[x]; x++) { + for (var y = 0, field; field = input.fieldRow[y]; y++) { + field.setText(null); + } + } +}; + +/** + * Enable or disable a block. + */ +Blockly.BlockSvg.prototype.updateDisabled = function() { + var hasClass = Blockly.hasClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + if (this.disabled || this.getInheritedDisabled()) { + if (!hasClass) { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + this.svgPath_.setAttribute('fill', + 'url(#' + this.workspace.options.disabledPatternId + ')'); + } + } else { + if (hasClass) { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDisabled'); + this.updateColour(); + } + } + var children = this.getChildren(); + for (var i = 0, child; child = children[i]; i++) { + child.updateDisabled(); + } +}; + +/** + * Returns the comment on this block (or '' if none). + * @return {string} Block's comment. + */ +Blockly.BlockSvg.prototype.getCommentText = function() { + if (this.comment) { + var comment = this.comment.getText(); + // Trim off trailing whitespace. + return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n'); + } + return ''; +}; + +/** + * Set this block's comment text. + * @param {?string} text The text, or null to delete. + */ +Blockly.BlockSvg.prototype.setCommentText = function(text) { + var changedState = false; + if (goog.isString(text)) { + if (!this.comment) { + this.comment = new Blockly.Comment(this); + changedState = true; + } + this.comment.setText(/** @type {string} */ (text)); + } else { + if (this.comment) { + this.comment.dispose(); + changedState = true; + } + } + if (changedState && this.rendered) { + this.render(); + // Adding or removing a comment icon will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Set this block's warning text. + * @param {?string} text The text, or null to delete. + * @param {string=} opt_id An optional ID for the warning text to be able to + * maintain multiple warnings. + */ +Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) { + if (!this.setWarningText.pid_) { + // Create a database of warning PIDs. + // Only runs once per block (and only those with warnings). + this.setWarningText.pid_ = Object.create(null); + } + var id = opt_id || ''; + if (!id) { + // Kill all previous pending processes, this edit supercedes them all. + for (var n in this.setWarningText.pid_) { + clearTimeout(this.setWarningText.pid_[n]); + delete this.setWarningText.pid_[n]; + } + } else if (this.setWarningText.pid_[id]) { + // Only queue up the latest change. Kill any earlier pending process. + clearTimeout(this.setWarningText.pid_[id]); + delete this.setWarningText.pid_[id]; + } + if (Blockly.dragMode_ == Blockly.DRAG_FREE) { + // Don't change the warning text during a drag. + // Wait until the drag finishes. + var thisBlock = this; + this.setWarningText.pid_[id] = setTimeout(function() { + if (thisBlock.workspace) { // Check block wasn't deleted. + delete thisBlock.setWarningText.pid_[id]; + thisBlock.setWarningText(text, id); + } + }, 100); + return; + } + if (this.isInFlyout) { + text = null; + } + + // Bubble up to add a warning on top-most collapsed block. + var parent = this.getSurroundParent(); + var collapsedParent = null; + while (parent) { + if (parent.isCollapsed()) { + collapsedParent = parent; + } + parent = parent.getSurroundParent(); + } + if (collapsedParent) { + collapsedParent.setWarningText(text, 'collapsed ' + this.id + ' ' + id); + } + + var changedState = false; + if (goog.isString(text)) { + if (!this.warning) { + this.warning = new Blockly.Warning(this); + changedState = true; + } + this.warning.setText(/** @type {string} */ (text), id); + } else { + // Dispose all warnings if no id is given. + if (this.warning && !id) { + this.warning.dispose(); + changedState = true; + } else if (this.warning) { + var oldText = this.warning.getText(); + this.warning.setText('', id); + var newText = this.warning.getText(); + if (!newText) { + this.warning.dispose(); + } + changedState = oldText == newText; + } + } + if (changedState && this.rendered) { + this.render(); + // Adding or removing a warning icon will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Give this block a mutator dialog. + * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove. + */ +Blockly.BlockSvg.prototype.setMutator = function(mutator) { + if (this.mutator && this.mutator !== mutator) { + this.mutator.dispose(); + } + if (mutator) { + mutator.block_ = this; + this.mutator = mutator; + mutator.createIcon(); + } +}; + +/** + * Set whether the block is disabled or not. + * @param {boolean} disabled True if disabled. + */ +Blockly.BlockSvg.prototype.setDisabled = function(disabled) { + if (this.disabled != disabled) { + Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled); + if (this.rendered) { + this.updateDisabled(); + } + } +}; + +/** + * Select this block. Highlight it visually. + */ +Blockly.BlockSvg.prototype.addSelect = function() { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklySelected'); + // Move the selected block to the top of the stack. + var block = this; + do { + var root = block.getSvgRoot(); + root.parentNode.appendChild(root); + block = block.getParent(); + } while (block); +}; + +/** + * Unselect this block. Remove its highlighting. + */ +Blockly.BlockSvg.prototype.removeSelect = function() { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklySelected'); +}; + +/** + * Adds the dragging class to this block. + * Also disables the highlights/shadows to improve performance. + */ +Blockly.BlockSvg.prototype.addDragging = function() { + Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDragging'); +}; + +/** + * Removes the dragging class from this block. + */ +Blockly.BlockSvg.prototype.removeDragging = function() { + Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), + 'blocklyDragging'); +}; + +// Overrides of functions on Blockly.Block that take into account whether the +// block has been rendered. + +/** + * Change the colour of a block. + * @param {number|string} colour HSV hue value, or #RRGGBB string. + */ +Blockly.BlockSvg.prototype.setColour = function(colour) { + Blockly.BlockSvg.superClass_.setColour.call(this, colour); + + if (this.rendered) { + this.updateColour(); + } +}; + +/** + * Set whether this block can chain onto the bottom of another block. + * @param {boolean} newBoolean True if there can be a previous statement. + * @param {string|Array.|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.BlockSvg.prototype.setPreviousStatement = + function(newBoolean, opt_check) { + /* eslint-disable indent */ + Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean, + opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; /* eslint-enable indent */ + +/** + * Set whether another block can chain onto the bottom of this block. + * @param {boolean} newBoolean True if there can be a next statement. + * @param {string|Array.|null|undefined} opt_check Statement type or + * list of statement types. Null/undefined if any type could be connected. + */ +Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) { + Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean, + opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Set whether this block returns a value. + * @param {boolean} newBoolean True if there is an output. + * @param {string|Array.|null|undefined} opt_check Returned type or list + * of returned types. Null or undefined if any type could be returned + * (e.g. variable get). + */ +Blockly.BlockSvg.prototype.setOutput = function(newBoolean, opt_check) { + Blockly.BlockSvg.superClass_.setOutput.call(this, newBoolean, opt_check); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Set whether value inputs are arranged horizontally or vertically. + * @param {boolean} newBoolean True if inputs are horizontal. + */ +Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) { + Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean); + + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } +}; + +/** + * Remove an input from this block. + * @param {string} name The name of the input. + * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @throws {goog.asserts.AssertionError} if the input is not present and + * opt_quiet is not true. + */ +Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) { + Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet); + + if (this.rendered) { + this.render(); + // Removing an input will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Move a numbered input to a different location on this block. + * @param {number} inputIndex Index of the input to move. + * @param {number} refIndex Index of input that should be after the moved input. + */ +Blockly.BlockSvg.prototype.moveNumberedInputBefore = function( + inputIndex, refIndex) { + Blockly.BlockSvg.superClass_.moveNumberedInputBefore.call(this, inputIndex, + refIndex); + + if (this.rendered) { + this.render(); + // Moving an input will cause the block to change shape. + this.bumpNeighbours_(); + } +}; + +/** + * Add a value input, statement input or local variable to this block. + * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or + * Blockly.DUMMY_INPUT. + * @param {string} name Language-neutral identifier which may used to find this + * input again. Should be unique to this block. + * @return {!Blockly.Input} The input object created. + * @private + */ +Blockly.BlockSvg.prototype.appendInput_ = function(type, name) { + var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name); + + if (this.rendered) { + this.render(); + // Adding an input will cause the block to change shape. + this.bumpNeighbours_(); + } + return input; +}; + +/** + * Returns connections originating from this block. + * @param {boolean} all If true, return all connections even hidden ones. + * Otherwise, for a non-rendered block return an empty list, and for a + * collapsed block don't return inputs connections. + * @return {!Array.} Array of connections. + * @private + */ +Blockly.BlockSvg.prototype.getConnections_ = function(all) { + var myConnections = []; + if (all || this.rendered) { + if (this.outputConnection) { + myConnections.push(this.outputConnection); + } + if (this.previousConnection) { + myConnections.push(this.previousConnection); + } + if (this.nextConnection) { + myConnections.push(this.nextConnection); + } + if (all || !this.collapsed_) { + for (var i = 0, input; input = this.inputList[i]; i++) { + if (input.connection) { + myConnections.push(input.connection); + } + } + } + } + return myConnections; +}; + +/** + * Create a connection of the specified type. + * @param {number} type The type of the connection to create. + * @return {!Blockly.RenderedConnection} A new connection of the specified type. + * @private + */ +Blockly.BlockSvg.prototype.makeConnection_ = function(type) { + return new Blockly.RenderedConnection(this, type); +}; diff --git a/core/block_svg.js.rej b/core/block_svg.js.rej new file mode 100644 index 000000000..59aca1875 --- /dev/null +++ b/core/block_svg.js.rej @@ -0,0 +1,231 @@ +*************** +*** 26,31 **** + + goog.provide('Blockly.BlockSvg'); + + goog.require('goog.userAgent'); + + +--- 26,32 ---- + + goog.provide('Blockly.BlockSvg'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code + goog.require('goog.userAgent'); + + +*************** +*** 154,167 **** + * @const + */ + Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * +- (Blockly.BlockSvg.CORNER_RADIUS - 1) + 1; + /** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the outside of a curve. + * @const + */ + Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * +- (Blockly.BlockSvg.CORNER_RADIUS + 1) - 1; + /** + * SVG path for drawing next/previous notch from left to right. + * @const +--- 155,168 ---- + * @const + */ + Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * ++ (Blockly.BlockSvg.CORNER_RADIUS - 1) + 1; + /** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the outside of a curve. + * @const + */ + Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * ++ (Blockly.BlockSvg.CORNER_RADIUS + 1) - 1; + /** + * SVG path for drawing next/previous notch from left to right. + * @const +*************** +*** 483,488 **** + Blockly.BlockSvg.prototype.render = function() { + this.block_.rendered = true; + + var cursorX = Blockly.BlockSvg.SEP_SPACE_X; + if (Blockly.RTL) { + cursorX = -cursorX; +--- 484,530 ---- + Blockly.BlockSvg.prototype.render = function() { + this.block_.rendered = true; + ++ this.renderHere(); ++ ++ // Render all blocks above this one (propagate a reflow). ++ var parentBlock = this.block_.getParent(); ++ if (parentBlock) { ++ parentBlock.render(); ++ } ++ }; ++ ++ /** ++ * [lyn, 04/01/14] Render a tree of blocks. ++ * In general, this is more efficient than calling render() on all the leaves of the tree, ++ * because that will: ++ * (1) repeat the rendering of all internal nodes; and ++ * (2) will unnecessarily call Blockly.fireUiEvent(window, 'resize') in the ++ * case where the parentPointer hasn't been set yet (particularly for ++ * value, statement, and next connections in Xml.domToBlock). ++ * These two factors account for much of the slow project loading times in Blockly ++ * and previous versions of AI2. ++ */ ++ Blockly.BlockSvg.prototype.renderDown = function() { ++ this.block_.rendered = true; ++ ++ // Recursively renderDown all my children (as long as I'm not collapsed) ++ if (! (Blockly.Instrument.avoidRenderDownOnCollapsedSubblocks && this.block_.isCollapsed())) { ++ var childBlocks = this.block_.childBlocks_; ++ for (var c = 0, childBlock; childBlock = childBlocks[c]; c++) { ++ childBlock.renderDown(); ++ } ++ } ++ ++ // Render me after all my children have been rendered. ++ this.renderHere(); ++ }; ++ ++ /** ++ * Render this block. Assumes descendants have already been rendered. ++ */ ++ Blockly.BlockSvg.prototype.renderHere = function() { ++ var start = new Date().getTime(); ++ // Now render me (even if I am collapsed, since still need to show collapsed block) + var cursorX = Blockly.BlockSvg.SEP_SPACE_X; + if (Blockly.RTL) { + cursorX = -cursorX; +*************** +*** 500,514 **** + var inputRows = this.renderCompute_(cursorX); + this.renderDraw_(cursorX, inputRows); + +- // Render all blocks above this one (propagate a reflow). + var parentBlock = this.block_.getParent(); +- if (parentBlock) { +- parentBlock.render(); +- } else { + // Top-most block. Fire an event to allow scrollbars to resize. + Blockly.fireUiEvent(window, 'resize'); + } +- }; + + /** + * Render a list of fields starting at the specified location. +--- 542,557 ---- + var inputRows = this.renderCompute_(cursorX); + this.renderDraw_(cursorX, inputRows); + + var parentBlock = this.block_.getParent(); ++ if (!parentBlock) { + // Top-most block. Fire an event to allow scrollbars to resize. + Blockly.fireUiEvent(window, 'resize'); + } ++ var stop = new Date().getTime(); ++ var timeDiff = stop-start; ++ Blockly.Instrument.stats.renderHereCalls++; ++ Blockly.Instrument.stats.renderHereTime += timeDiff; ++ } + + /** + * Render a list of fields starting at the specified location. +*************** +*** 578,583 **** + row = []; + if (isInline && input.type != Blockly.NEXT_STATEMENT) { + row.type = Blockly.BlockSvg.INLINE; + } else { + row.type = input.type; + } +--- 621,629 ---- + row = []; + if (isInline && input.type != Blockly.NEXT_STATEMENT) { + row.type = Blockly.BlockSvg.INLINE; ++ } else if (input.subtype) { ++ row.type = input.type; ++ row.subtype = input.subtype; + } else { + row.type = input.type; + } +*************** +*** 924,932 **** + } + } + this.renderFields_(input.fieldRow, fieldX, fieldY); +- steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); +- var v = row.height - Blockly.BlockSvg.TAB_HEIGHT +- steps.push('v', v); + if (Blockly.RTL) { + // Highlight around back of tab. + highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); +--- 970,1006 ---- + } + } + this.renderFields_(input.fieldRow, fieldX, fieldY); ++ if (row.subtype == Blockly.INDENTED_VALUE){ ++ cursorX = inputRows.statementEdge; ++ steps.push('H', cursorX+input.fieldWidth+8); ++ steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); ++ steps.push('V',cursorY+row.height); ++ steps.push('H', inputRows.rightEdge); ++ steps.push('v 8'); ++ if (Blockly.RTL) { ++ highlightSteps.push('M', ++ (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + ++ Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ++ ',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); ++ highlightSteps.push( ++ Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL); ++ highlightSteps.push('v', ++ row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); ++ highlightSteps.push( ++ Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL); ++ highlightSteps.push('H', inputRows.rightEdge - 1); ++ } else { ++ highlightSteps.push('M', ++ (cursorX + 9 + input.fieldWidth) + ',' + ++ (cursorY + row.height)); ++ highlightSteps.push('H', inputRows.rightEdge); ++ } ++ cursorY+=8; ++ } else { ++ steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); ++ var v = row.height - Blockly.BlockSvg.TAB_HEIGHT ++ steps.push('v', v); ++ } + if (Blockly.RTL) { + // Highlight around back of tab. + highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); +*************** +*** 939,947 **** + ',-1.8'); + } + // Create external input connection. +- connectionX = connectionsXY.x + +- (Blockly.RTL ? -inputRows.rightEdge - 1 : inputRows.rightEdge + 1); +- connectionY = connectionsXY.y + cursorY; + input.connection.moveTo(connectionX, connectionY); + if (input.connection.targetConnection) { + input.connection.tighten_(); +--- 1013,1027 ---- + ',-1.8'); + } + // Create external input connection. ++ if (row.subtype == Blockly.INDENTED_VALUE){ ++ connectionX = connectionsXY.x + ++ (Blockly.RTL ? -inputRows.statementEdge - 1: inputRows.statementEdge + 9 + input.fieldWidth); ++ connectionY = connectionsXY.y + cursorY-8; ++ } else { ++ connectionX = connectionsXY.x + ++ (Blockly.RTL ? -inputRows.rightEdge - 1 : inputRows.rightEdge + 1); ++ connectionY = connectionsXY.y + cursorY; ++ } + input.connection.moveTo(connectionX, connectionY); + if (input.connection.targetConnection) { + input.connection.tighten_(); diff --git a/core/blockly.js b/core/blockly.js index fb6562ed2..2d0715b68 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -63,6 +63,21 @@ var CLOSURE_DEFINES = {'goog.DEBUG': false}; */ Blockly.mainWorkspace = null; +/** + * Workspace blocks arrangements + */ +Blockly.BLKS_HORIZONTAL = 0; +Blockly.BLKS_VERTICAL = 1; +Blockly.BLKS_CATEGORY = 2; + +/** + * Current Workspace arrangement state for position (horizontal or vertical), + * and for type (category) + */ +Blockly.workspace_arranged_position = null; +Blockly.workspace_arranged_latest_position = null; //used to default to (previous is used for menus) +Blockly.workspace_arranged_type = null; + /** * Currently selected block. * @type {Blockly.Block} diff --git a/core/blockly.js.orig b/core/blockly.js.orig new file mode 100644 index 000000000..fb6562ed2 --- /dev/null +++ b/core/blockly.js.orig @@ -0,0 +1,453 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 Core JavaScript library for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +// Top level object for Blockly. +goog.provide('Blockly'); + +goog.require('Blockly.BlockSvg.render'); +goog.require('Blockly.Events'); +goog.require('Blockly.FieldAngle'); +goog.require('Blockly.FieldCheckbox'); +goog.require('Blockly.FieldColour'); +// Date picker commented out since it increases footprint by 60%. +// Add it only if you need it. +//goog.require('Blockly.FieldDate'); +goog.require('Blockly.FieldDropdown'); +goog.require('Blockly.FieldImage'); +goog.require('Blockly.FieldTextInput'); +goog.require('Blockly.FieldNumber'); +goog.require('Blockly.FieldVariable'); +goog.require('Blockly.Generator'); +goog.require('Blockly.Msg'); +goog.require('Blockly.Procedures'); +goog.require('Blockly.Toolbox'); +goog.require('Blockly.WidgetDiv'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('Blockly.constants'); +goog.require('Blockly.inject'); +goog.require('Blockly.utils'); +goog.require('goog.color'); +goog.require('goog.userAgent'); + + +// Turn off debugging when compiled. +var CLOSURE_DEFINES = {'goog.DEBUG': false}; + +/** + * The main workspace most recently used. + * Set by Blockly.WorkspaceSvg.prototype.markFocused + * @type {Blockly.Workspace} + */ +Blockly.mainWorkspace = null; + +/** + * Currently selected block. + * @type {Blockly.Block} + */ +Blockly.selected = null; + +/** + * Currently highlighted connection (during a drag). + * @type {Blockly.Connection} + * @private + */ +Blockly.highlightedConnection_ = null; + +/** + * Connection on dragged block that matches the highlighted connection. + * @type {Blockly.Connection} + * @private + */ +Blockly.localConnection_ = null; + +/** + * All of the connections on blocks that are currently being dragged. + * @type {!Array.} + * @private + */ +Blockly.draggingConnections_ = []; + +/** + * Contents of the local clipboard. + * @type {Element} + * @private + */ +Blockly.clipboardXml_ = null; + +/** + * Source of the local clipboard. + * @type {Blockly.WorkspaceSvg} + * @private + */ +Blockly.clipboardSource_ = null; + +/** + * Is the mouse dragging a block? + * DRAG_NONE - No drag operation. + * DRAG_STICKY - Still inside the sticky DRAG_RADIUS. + * DRAG_FREE - Freely draggable. + * @private + */ +Blockly.dragMode_ = Blockly.DRAG_NONE; + +/** + * Wrapper function called when a touch mouseUp occurs during a drag operation. + * @type {Array.} + * @private + */ +Blockly.onTouchUpWrapper_ = null; + +/** + * Convert a hue (HSV model) into an RGB hex triplet. + * @param {number} hue Hue on a colour wheel (0-360). + * @return {string} RGB code, e.g. '#5ba65b'. + */ +Blockly.hueToRgb = function(hue) { + return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION, + Blockly.HSV_VALUE * 255); +}; + +/** + * Returns the dimensions of the specified SVG image. + * @param {!Element} svg SVG image. + * @return {!Object} Contains width and height properties. + */ +Blockly.svgSize = function(svg) { + return {width: svg.cachedWidth_, + height: svg.cachedHeight_}; +}; + +/** + * Size the workspace when the contents change. This also updates + * scrollbars accordingly. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to resize. + */ +Blockly.resizeSvgContents = function(workspace) { + workspace.resizeContents(); +}; + +/** + * Size the SVG image to completely fill its container. Call this when the view + * actually changes sizes (e.g. on a window resize/device orientation change). + * See Blockly.resizeSvgContents to resize the workspace when the contents + * change (e.g. when a block is added or removed). + * Record the height/width of the SVG image. + * @param {!Blockly.WorkspaceSvg} workspace Any workspace in the SVG. + */ +Blockly.svgResize = function(workspace) { + var mainWorkspace = workspace; + while (mainWorkspace.options.parentWorkspace) { + mainWorkspace = mainWorkspace.options.parentWorkspace; + } + var svg = mainWorkspace.getParentSvg(); + var div = svg.parentNode; + if (!div) { + // Workspace deleted, or something. + return; + } + var width = div.offsetWidth; + var height = div.offsetHeight; + if (svg.cachedWidth_ != width) { + svg.setAttribute('width', width + 'px'); + svg.cachedWidth_ = width; + } + if (svg.cachedHeight_ != height) { + svg.setAttribute('height', height + 'px'); + svg.cachedHeight_ = height; + } + mainWorkspace.resize(); +}; + +/** + * Handle a mouse-up anywhere on the page. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.onMouseUp_ = function(e) { + var workspace = Blockly.getMainWorkspace(); + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); + workspace.dragMode_ = Blockly.DRAG_NONE; + // Unbind the touch event if it exists. + if (Blockly.onTouchUpWrapper_) { + Blockly.unbindEvent_(Blockly.onTouchUpWrapper_); + Blockly.onTouchUpWrapper_ = null; + } + if (Blockly.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.onMouseMoveWrapper_); + Blockly.onMouseMoveWrapper_ = null; + } +}; + +/** + * Handle a mouse-move on SVG drawing surface. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.onMouseMove_ = function(e) { + if (e.touches && e.touches.length >= 2) { + return; // Multi-touch gestures won't have e.clientX. + } + var workspace = Blockly.getMainWorkspace(); + if (workspace.dragMode_ != Blockly.DRAG_NONE) { + var dx = e.clientX - workspace.startDragMouseX; + var dy = e.clientY - workspace.startDragMouseY; + var metrics = workspace.startDragMetrics; + var x = workspace.startScrollX + dx; + var y = workspace.startScrollY + dy; + x = Math.min(x, -metrics.contentLeft); + y = Math.min(y, -metrics.contentTop); + x = Math.max(x, metrics.viewWidth - metrics.contentLeft - + metrics.contentWidth); + y = Math.max(y, metrics.viewHeight - metrics.contentTop - + metrics.contentHeight); + + // Move the scrollbars and the page will scroll automatically. + workspace.scrollbar.set(-x - metrics.contentLeft, + -y - metrics.contentTop); + // Cancel the long-press if the drag has moved too far. + if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) { + Blockly.longStop_(); + workspace.dragMode_ = Blockly.DRAG_FREE; + } + e.stopPropagation(); + e.preventDefault(); + } +}; + +/** + * Handle a key-down on SVG drawing surface. + * @param {!Event} e Key down event. + * @private + */ +Blockly.onKeyDown_ = function(e) { + if (Blockly.mainWorkspace.options.readOnly || Blockly.isTargetInput_(e)) { + // No key actions on readonly workspaces. + // When focused on an HTML text input widget, don't trap any keys. + return; + } + var deleteBlock = false; + if (e.keyCode == 27) { + // Pressing esc closes the context menu. + Blockly.hideChaff(); + } else if (e.keyCode == 8 || e.keyCode == 46) { + // Delete or backspace. + // Stop the browser from going back to the previous page. + // Do this first to prevent an error in the delete code from resulting in + // data loss. + e.preventDefault(); + if (Blockly.selected && Blockly.selected.isDeletable()) { + deleteBlock = true; + } + } else if (e.altKey || e.ctrlKey || e.metaKey) { + if (Blockly.selected && + Blockly.selected.isDeletable() && Blockly.selected.isMovable()) { + if (e.keyCode == 67) { + // 'c' for copy. + Blockly.hideChaff(); + Blockly.copy_(Blockly.selected); + } else if (e.keyCode == 88) { + // 'x' for cut. + Blockly.copy_(Blockly.selected); + deleteBlock = true; + } + } + if (e.keyCode == 86) { + // 'v' for paste. + if (Blockly.clipboardXml_) { + Blockly.Events.setGroup(true); + Blockly.clipboardSource_.paste(Blockly.clipboardXml_); + Blockly.Events.setGroup(false); + } + } else if (e.keyCode == 90) { + // 'z' for undo 'Z' is for redo. + Blockly.hideChaff(); + Blockly.mainWorkspace.undo(e.shiftKey); + } + } + if (deleteBlock) { + // Common code for delete and cut. + Blockly.Events.setGroup(true); + Blockly.hideChaff(); + var heal = Blockly.dragMode_ != Blockly.DRAG_FREE; + Blockly.selected.dispose(heal, true); + if (Blockly.highlightedConnection_) { + Blockly.highlightedConnection_.unhighlight(); + Blockly.highlightedConnection_ = null; + } + Blockly.Events.setGroup(false); + } +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.terminateDrag_ = function() { + Blockly.BlockSvg.terminateDrag(); + Blockly.Flyout.terminateDrag_(); +}; + +/** + * PID of queued long-press task. + * @private + */ +Blockly.longPid_ = 0; + +/** + * Context menus on touch devices are activated using a long-press. + * Unfortunately the contextmenu touch event is currently (2015) only suported + * by Chrome. This function is fired on any touchstart event, queues a task, + * which after about a second opens the context menu. The tasks is killed + * if the touch event terminates early. + * @param {!Event} e Touch start event. + * @param {!Blockly.Block|!Blockly.WorkspaceSvg} uiObject The block or workspace + * under the touchstart event. + * @private + */ +Blockly.longStart_ = function(e, uiObject) { + Blockly.longStop_(); + Blockly.longPid_ = setTimeout(function() { + e.button = 2; // Simulate a right button click. + uiObject.onMouseDown_(e); + }, Blockly.LONGPRESS); +}; + +/** + * Nope, that's not a long-press. Either touchend or touchcancel was fired, + * or a drag hath begun. Kill the queued long-press task. + * @private + */ +Blockly.longStop_ = function() { + if (Blockly.longPid_) { + clearTimeout(Blockly.longPid_); + Blockly.longPid_ = 0; + } +}; + +/** + * Copy a block onto the local clipboard. + * @param {!Blockly.Block} block Block to be copied. + * @private + */ +Blockly.copy_ = function(block) { + var xmlBlock = Blockly.Xml.blockToDom(block); + if (Blockly.dragMode_ != Blockly.DRAG_FREE) { + Blockly.Xml.deleteNext(xmlBlock); + } + // Encode start position in XML. + var xy = block.getRelativeToSurfaceXY(); + xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x); + xmlBlock.setAttribute('y', xy.y); + Blockly.clipboardXml_ = xmlBlock; + Blockly.clipboardSource_ = block.workspace; +}; + +/** + * Duplicate this block and its children. + * @param {!Blockly.Block} block Block to be copied. + * @private + */ +Blockly.duplicate_ = function(block) { + // Save the clipboard. + var clipboardXml = Blockly.clipboardXml_; + var clipboardSource = Blockly.clipboardSource_; + + // Create a duplicate via a copy/paste operation. + Blockly.copy_(block); + block.workspace.paste(Blockly.clipboardXml_); + + // Restore the clipboard. + Blockly.clipboardXml_ = clipboardXml; + Blockly.clipboardSource_ = clipboardSource; +}; + +/** + * Cancel the native context menu, unless the focus is on an HTML input widget. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.onContextMenu_ = function(e) { + if (!Blockly.isTargetInput_(e)) { + // When focused on an HTML text input widget, don't cancel the context menu. + e.preventDefault(); + } +}; + +/** + * Close tooltips, context menus, dropdown selections, etc. + * @param {boolean=} opt_allowToolbox If true, don't close the toolbox. + */ +Blockly.hideChaff = function(opt_allowToolbox) { + Blockly.Tooltip.hide(); + Blockly.WidgetDiv.hide(); + if (!opt_allowToolbox) { + var workspace = Blockly.getMainWorkspace(); + if (workspace.toolbox_ && + workspace.toolbox_.flyout_ && + workspace.toolbox_.flyout_.autoClose) { + workspace.toolbox_.clearSelection(); + } + } +}; + +/** + * When something in Blockly's workspace changes, call a function. + * @param {!Function} func Function to call. + * @return {!Array.} Opaque data that can be passed to + * removeChangeListener. + * @deprecated April 2015 + */ +Blockly.addChangeListener = function(func) { + // Backwards compatability from before there could be multiple workspaces. + console.warn('Deprecated call to Blockly.addChangeListener, ' + + 'use workspace.addChangeListener instead.'); + return Blockly.getMainWorkspace().addChangeListener(func); +}; + +/** + * Returns the main workspace. Returns the last used main workspace (based on + * focus). Try not to use this function, particularly if there are multiple + * Blockly instances on a page. + * @return {!Blockly.Workspace} The main workspace. + */ +Blockly.getMainWorkspace = function() { + return Blockly.mainWorkspace; +}; + +// IE9 does not have a console. Create a stub to stop errors. +if (!goog.global['console']) { + goog.global['console'] = { + 'log': function() {}, + 'warn': function() {} + }; +} + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +goog.global['Blockly']['getMainWorkspace'] = Blockly.getMainWorkspace; +goog.global['Blockly']['addChangeListener'] = Blockly.addChangeListener; diff --git a/core/blockly.js.rej b/core/blockly.js.rej new file mode 100644 index 000000000..351f98928 --- /dev/null +++ b/core/blockly.js.rej @@ -0,0 +1,461 @@ +*************** +*** 24,32 **** + */ + 'use strict'; + + // Top level object for Blockly. + goog.provide('Blockly'); + + // Blockly core dependencies. + goog.require('Blockly.Block'); + goog.require('Blockly.Connection'); +--- 24,38 ---- + */ + 'use strict'; + ++ /** ++ * [lyn, 10/10/13] Modified Blockly.hideChaff() method to hide single instance of Blockly.FieldFlydown. ++ */ ++ + // Top level object for Blockly. + goog.provide('Blockly'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code ++ + // Blockly core dependencies. + goog.require('Blockly.Block'); + goog.require('Blockly.Connection'); +*************** +*** 41,47 **** + goog.require('Blockly.Msg'); + goog.require('Blockly.Procedures'); + goog.require('Blockly.Realtime'); +- goog.require('Blockly.Toolbox'); + goog.require('Blockly.WidgetDiv'); + goog.require('Blockly.Workspace'); + goog.require('Blockly.inject'); +--- 47,54 ---- + goog.require('Blockly.Msg'); + goog.require('Blockly.Procedures'); + goog.require('Blockly.Realtime'); ++ //goog.require('Blockly.Toolbox'); ++ goog.require('Blockly.TypeBlock'); + goog.require('Blockly.WidgetDiv'); + goog.require('Blockly.Workspace'); + goog.require('Blockly.inject'); +*************** +*** 99,107 **** + * @param {number} hue Hue on a colour wheel (0-360). + * @return {string} RGB code, e.g. '#5ba65b'. + */ +- Blockly.makeColour = function(hue) { +- return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION, + Blockly.HSV_VALUE * 256); + }; + + /** +--- 106,118 ---- + * @param {number} hue Hue on a colour wheel (0-360). + * @return {string} RGB code, e.g. '#5ba65b'. + */ ++ Blockly.makeColour = function(hueOrRGBArray) { ++ if(Array.isArray(hueOrRGBArray)){ ++ return goog.color.rgbArrayToHex(hueOrRGBArray); ++ } else { ++ return goog.color.hsvToHex(hueOrRGBArray, Blockly.HSV_SATURATION, + Blockly.HSV_VALUE * 256); ++ } + }; + + /** +*************** +*** 131,136 **** + Blockly.DUMMY_INPUT = 5; + + /** + * ENUM for left alignment. + * @const + */ +--- 142,154 ---- + Blockly.DUMMY_INPUT = 5; + + /** ++ * ENUM for an indented value input. Similar to next_statement but with value ++ * input shape. ++ * @const ++ */ ++ Blockly.INDENTED_VALUE = 6; ++ ++ /** + * ENUM for left alignment. + * @const + */ +*************** +*** 163,176 **** + }; + + /** + * Handle a mouse-down on SVG drawing surface. + * @param {!Event} e Mouse down event. + * @private + */ + Blockly.onMouseDown_ = function(e) { + Blockly.svgResize(); + Blockly.terminateDrag_(); // In case mouse-up event was lost. + Blockly.hideChaff(); + var isTargetSvg = e.target && e.target.nodeName && + e.target.nodeName.toLowerCase() == 'svg'; + if (!Blockly.readOnly && Blockly.selected && isTargetSvg) { +--- 196,237 ---- + }; + + /** ++ * latest clicked position is used to open the type blocking suggestions window ++ * Initial position is 0,0 ++ * @type {{x: number, y:number}} ++ */ ++ Blockly.latestClick = { x: 0, y: 0 }; ++ ++ /** + * Handle a mouse-down on SVG drawing surface. + * @param {!Event} e Mouse down event. + * @private + */ + Blockly.onMouseDown_ = function(e) { ++ Blockly.latestClick = { x: e.clientX, y: e.clientY }; // Might be needed? + Blockly.svgResize(); + Blockly.terminateDrag_(); // In case mouse-up event was lost. + Blockly.hideChaff(); ++ //if drawer exists and supposed to close ++ if(Blockly.Drawer && Blockly.Drawer.flyout_.autoClose) { ++ Blockly.Drawer.hide(); ++ } ++ ++ //Closes mutators ++ var blocks = Blockly.mainWorkspace.getAllBlocks(); ++ var numBlocks = blocks.length; ++ var temp_block = null; ++ for(var i =0; i= 3) { ++ if (confirm("Are you sure you want to delete all " + descendantCount + " of these blocks?")) { ++ Blockly.hideChaff(); ++ Blockly.selected.dispose(true, true); ++ } ++ } ++ else { ++ Blockly.hideChaff(); ++ Blockly.selected.dispose(true, true); ++ } + } + } finally { + // Stop the browser from going back to the previous page. +*************** +*** 363,368 **** + ms += COLLAPSE_DELAY; + } + } + }; + options.push(collapseOption); + +--- 438,444 ---- + ms += COLLAPSE_DELAY; + } + } ++ Blockly.resetWorkspaceArrangements(); + }; + options.push(collapseOption); + +*************** +*** 370,390 **** + var expandOption = {enabled: hasCollapsedBlocks}; + expandOption.text = Blockly.Msg.EXPAND_ALL; + expandOption.callback = function() { +- var ms = 0; +- for (var i = 0; i < topBlocks.length; i++) { +- var block = topBlocks[i]; +- while (block) { +- setTimeout(block.setCollapsed.bind(block, false), ms); +- block = block.getNextBlock(); +- ms += COLLAPSE_DELAY; +- } +- } + }; + options.push(expandOption); + } + + Blockly.ContextMenu.show(e, options); + }; + + /** + * Cancel the native context menu, unless the focus is on an HTML input widget. +--- 446,621 ---- + var expandOption = {enabled: hasCollapsedBlocks}; + expandOption.text = Blockly.Msg.EXPAND_ALL; + expandOption.callback = function() { ++ Blockly.Instrument.initializeStats("expandAllCollapsedBlocks"); ++ Blockly.Instrument.timer( ++ function () { ++ var ms = 0; ++ for (var i = 0; i < topBlocks.length; i++) { ++ var block = topBlocks[i]; ++ while (block) { ++ setTimeout(block.setCollapsed.bind(block, false), ms); ++ block = block.getNextBlock(); ++ ms += COLLAPSE_DELAY; ++ } ++ } ++ Blockly.resetWorkspaceArrangements(); ++ }, ++ function (result, timeDiff) { ++ Blockly.Instrument.stats.totalTime = timeDiff; ++ Blockly.Instrument.displayStats("expandAllCollapsedBlocks"); ++ } ++ ); + }; + options.push(expandOption); + } + ++ // Arrange blocks in row order. ++ var arrangeOptionH = {enabled: (Blockly.workspace_arranged_position !== Blockly.BLKS_HORIZONTAL)}; ++ arrangeOptionH.text = Blockly.Msg.ARRANGE_H; ++ arrangeOptionH.callback = function() { ++ Blockly.workspace_arranged_position = Blockly.BLKS_HORIZONTAL; ++ Blockly.workspace_arranged_latest_position= Blockly.BLKS_HORIZONTAL; ++ arrangeBlocks(Blockly.BLKS_HORIZONTAL); ++ }; ++ options.push(arrangeOptionH); ++ ++ // Arrange blocks in column order. ++ var arrangeOptionV = {enabled: (Blockly.workspace_arranged_position !== Blockly.BLKS_VERTICAL)}; ++ arrangeOptionV.text = Blockly.Msg.ARRANGE_V; ++ arrangeOptionV.callback = function() { ++ Blockly.workspace_arranged_position = Blockly.BLKS_VERTICAL; ++ Blockly.workspace_arranged_latest_position = Blockly.BLKS_VERTICAL; ++ arrangeBlocks(Blockly.BLKS_VERTICAL); ++ }; ++ options.push(arrangeOptionV); ++ ++ /** ++ * Function that returns a name to be used to sort blocks. ++ * The general comparator is the block.category attribute. ++ * In the case of 'Components' the comparator is the instanceName of the component if it exists ++ * (it does not exist for generic components). ++ * In the case of Procedures the comparator is the NAME(for definitions) or PROCNAME (for calls) ++ * @param {!Blockly.Block} the block that will be compared in the sortByCategory function ++ * @returns {string} text to be used in the comparison ++ */ ++ function comparisonName(block){ ++ if (block.category === 'Component' && block.instanceName) ++ return block.instanceName; ++ if (block.category === 'Procedures') ++ return (block.getFieldValue('NAME') || block.getFieldValue('PROCNAME')); ++ return block.category; ++ } ++ ++ /** ++ * Function used to sort blocks by Category. ++ * @param {!Blockly.Block} a first block to be compared ++ * @param {!Blockly.Block} b second block to be compared ++ * @returns {number} returns 0 if the blocks are equal, and -1 or 1 if they are not ++ */ ++ function sortByCategory(a,b) { ++ var comparatorA = comparisonName(a).toLowerCase(); ++ var comparatorB = comparisonName(b).toLowerCase(); ++ ++ if (comparatorA < comparatorB) return -1; ++ else if (comparatorA > comparatorB) return +1; ++ else return 0; ++ } ++ ++ // Arranges block in layout (Horizontal or Vertical). ++ function arrangeBlocks(layout) { ++ var SPACER = 25; ++ var topblocks = Blockly.mainWorkspace.getTopBlocks(false); ++ // If the blocks are arranged by Category, sort the array ++ if (Blockly.workspace_arranged_type === Blockly.BLKS_CATEGORY){ ++ topblocks.sort(sortByCategory); ++ } ++ var metrics = Blockly.mainWorkspace.getMetrics(); ++ var viewLeft = metrics.viewLeft + 5; ++ var viewTop = metrics.viewTop + 5; ++ var x = viewLeft; ++ var y = viewTop; ++ var wsRight = viewLeft + metrics.viewWidth; ++ var wsBottom = viewTop + metrics.viewHeight; ++ var maxHgt = 0; ++ var maxWidth = 0; ++ for (var i = 0, len = topblocks.length; i < len; i++) { ++ var blk = topblocks[i]; ++ var blkXY = blk.getRelativeToSurfaceXY(); ++ var blockHW = blk.getHeightWidth(); ++ var blkHgt = blockHW.height; ++ var blkWidth = blockHW.width; ++ switch (layout) { ++ case Blockly.BLKS_HORIZONTAL: ++ if (x < wsRight) { ++ blk.moveBy(x - blkXY.x, y - blkXY.y); ++ blk.select(); ++ x += blkWidth + SPACER; ++ if (blkHgt > maxHgt) // Remember highest block ++ maxHgt = blkHgt; ++ } else { ++ y += maxHgt + SPACER; ++ maxHgt = blkHgt; ++ x = viewLeft; ++ blk.moveBy(x - blkXY.x, y - blkXY.y); ++ blk.select(); ++ x += blkWidth + SPACER; ++ } ++ break; ++ case Blockly.BLKS_VERTICAL: ++ if (y < wsBottom) { ++ blk.moveBy(x - blkXY.x, y - blkXY.y); ++ blk.select(); ++ y += blkHgt + SPACER; ++ if (blkWidth > maxWidth) // Remember widest block ++ maxWidth = blkWidth; ++ } else { ++ x += maxWidth + SPACER; ++ maxWidth = blkWidth; ++ y = viewTop; ++ blk.moveBy(x - blkXY.x, y - blkXY.y); ++ blk.select(); ++ y += blkHgt + SPACER; ++ } ++ break; ++ } ++ } ++ } ++ ++ // Sort by Category. ++ var sortOptionCat = {enabled: (Blockly.workspace_arranged_type !== Blockly.BLKS_CATEGORY)}; ++ sortOptionCat.text = Blockly.Msg.SORT_C; ++ sortOptionCat.callback = function() { ++ Blockly.workspace_arranged_type = Blockly.BLKS_CATEGORY; ++ rearrangeWorkspace(); ++ }; ++ options.push(sortOptionCat); ++ ++ // Called after a sort or collapse/expand to redisplay blocks. ++ function rearrangeWorkspace() { ++ //default arrangement position set to Horizontal if it hasn't been set yet (is null) ++ if (Blockly.workspace_arranged_latest_position === null || Blockly.workspace_arranged_latest_position === Blockly.BLKS_HORIZONTAL) ++ arrangeOptionH.callback(); ++ else if (Blockly.workspace_arranged_latest_position === Blockly.BLKS_VERTICAL) ++ arrangeOptionV.callback(); ++ } ++ ++ // Option to get help. ++ var helpOption = {enabled: false}; ++ helpOption.text = Blockly.Msg.HELP; ++ helpOption.callback = function() {}; ++ options.push(helpOption); ++ + Blockly.ContextMenu.show(e, options); + }; ++ /** ++ * reset arrangement state; to be called when blocks in the workspace change ++ */ ++ Blockly.resetWorkspaceArrangements = function(){ ++ // reset the variables used for menus, but keep the latest position, so the current horizontal or ++ // vertical state can be kept ++ Blockly.workspace_arranged_type = null; ++ Blockly.workspace_arranged_position = null; ++ }; + + /** + * Cancel the native context menu, unless the focus is on an HTML input widget. +*************** +*** 404,414 **** + */ + Blockly.hideChaff = function(opt_allowToolbox) { + Blockly.Tooltip.hide(); + Blockly.WidgetDiv.hide(); +- if (!opt_allowToolbox && +- Blockly.Toolbox.flyout_ && Blockly.Toolbox.flyout_.autoClose) { +- Blockly.Toolbox.clearSelection(); +- } + }; + + /** +--- 635,643 ---- + */ + Blockly.hideChaff = function(opt_allowToolbox) { + Blockly.Tooltip.hide(); ++ Blockly.FieldFlydown && Blockly.FieldFlydown.hide(); // [lyn, 10/06/13] for handling parameter & procedure flydowns + Blockly.WidgetDiv.hide(); ++ Blockly.TypeBlock && Blockly.TypeBlock.hide(); + }; + + /** +*************** +*** 556,562 **** + */ + Blockly.getMainWorkspaceMetrics_ = function() { + var svgSize = Blockly.svgSize(); +- svgSize.width -= Blockly.Toolbox.width; // Zero if no Toolbox. + var viewWidth = svgSize.width - Blockly.Scrollbar.scrollbarThickness; + var viewHeight = svgSize.height - Blockly.Scrollbar.scrollbarThickness; + try { +--- 785,793 ---- + */ + Blockly.getMainWorkspaceMetrics_ = function() { + var svgSize = Blockly.svgSize(); ++ //We don't use Blockly.Toolbox in our version of Blockly instead we use drawer.js ++ //svgSize.width -= Blockly.Toolbox.width; // Zero if no Toolbox. ++ svgSize.width -= 0; // Zero if no Toolbox. + var viewWidth = svgSize.width - Blockly.Scrollbar.scrollbarThickness; + var viewHeight = svgSize.height - Blockly.Scrollbar.scrollbarThickness; + try { +*************** +*** 582,588 **** + var topEdge = blockBox.y; + var bottomEdge = topEdge + blockBox.height; + } +- var absoluteLeft = Blockly.RTL ? 0 : Blockly.Toolbox.width; + var metrics = { + viewHeight: svgSize.height, + viewWidth: svgSize.width, +--- 813,821 ---- + var topEdge = blockBox.y; + var bottomEdge = topEdge + blockBox.height; + } ++ //We don't use Blockly.Toolbox in our version of Blockly instead we use drawer.js ++ //var absoluteLeft = Blockly.RTL ? 0 : Blockly.Toolbox.width; ++ var absoluteLeft = Blockly.RTL ? 0 : 0; + var metrics = { + viewHeight: svgSize.height, + viewWidth: svgSize.width, diff --git a/core/blocks.js.orig b/core/blocks.js.orig new file mode 100644 index 000000000..d6932ceb9 --- /dev/null +++ b/core/blocks.js.orig @@ -0,0 +1,33 @@ +/** + * @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 Empty name space for the Blocks singleton. + * @author spertus@google.com (Ellen Spertus) + */ +'use strict'; + +goog.provide('Blockly.Blocks'); + +/** + * Allow for switching between one and zero based indexing for lists and text, + * one based by default. + */ +Blockly.Blocks.ONE_BASED_INDEXING = true; diff --git a/core/blocks.js.rej b/core/blocks.js.rej new file mode 100644 index 000000000..cc620ee74 --- /dev/null +++ b/core/blocks.js.rej @@ -0,0 +1,19 @@ +*************** +*** 168,175 **** + * @this Blockly.Block + */ + block.mutationToDom = function() { +- var container = details.mutationToDomFunc ? +- details.mutatationToDomFunc() : document.createElement('mutation'); + container.setAttribute('is_statement', this['isStatement'] || false); + return container; + }; +--- 168,175 ---- + * @this Blockly.Block + */ + block.mutationToDom = function() { ++ var container = details.mutationToDomFunc ? details.mutatationToDomFunc() ++ : document.createElement('mutation'); + container.setAttribute('is_statement', this['isStatement'] || false); + return container; + }; diff --git a/core/bubble.js b/core/bubble.js index d4c1e2719..75e3f512e 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -62,7 +62,12 @@ Blockly.Bubble = function(workspace, content, shape, anchorXY, this.setAnchorLocation(anchorXY); if (!bubbleWidth || !bubbleHeight) { - var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox(); + try { + var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox(); + } catch (e) { + // Firefox has trouble with hidden elements (Bug 528969). + var bBox = {height: 0, width: 0}; + } bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH; bubbleHeight = bBox.height + 2 * Blockly.Bubble.BORDER_WIDTH; } diff --git a/core/bubble.js.orig b/core/bubble.js.orig new file mode 100644 index 000000000..d4c1e2719 --- /dev/null +++ b/core/bubble.js.orig @@ -0,0 +1,579 @@ +/** + * @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 Object representing a UI bubble. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Bubble'); + +goog.require('Blockly.Workspace'); +goog.require('goog.dom'); +goog.require('goog.math'); +goog.require('goog.math.Coordinate'); +goog.require('goog.userAgent'); + + +/** + * Class for UI bubble. + * @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the + * bubble. + * @param {!Element} content SVG content for the bubble. + * @param {Element} shape SVG element to avoid eclipsing. + * @param {!goog.math.Coodinate} anchorXY Absolute position of bubble's anchor + * point. + * @param {?number} bubbleWidth Width of bubble, or null if not resizable. + * @param {?number} bubbleHeight Height of bubble, or null if not resizable. + * @constructor + */ +Blockly.Bubble = function(workspace, content, shape, anchorXY, + bubbleWidth, bubbleHeight) { + this.workspace_ = workspace; + this.content_ = content; + this.shape_ = shape; + + var angle = Blockly.Bubble.ARROW_ANGLE; + if (this.workspace_.RTL) { + angle = -angle; + } + this.arrow_radians_ = goog.math.toRadians(angle); + + var canvas = workspace.getBubbleCanvas(); + canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight))); + + this.setAnchorLocation(anchorXY); + if (!bubbleWidth || !bubbleHeight) { + var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox(); + bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH; + bubbleHeight = bBox.height + 2 * Blockly.Bubble.BORDER_WIDTH; + } + this.setBubbleSize(bubbleWidth, bubbleHeight); + + // Render the bubble. + this.positionBubble_(); + this.renderArrow_(); + this.rendered_ = true; + + if (!workspace.options.readOnly) { + Blockly.bindEvent_(this.bubbleBack_, 'mousedown', this, + this.bubbleMouseDown_); + if (this.resizeGroup_) { + Blockly.bindEvent_(this.resizeGroup_, 'mousedown', this, + this.resizeMouseDown_); + } + } +}; + +/** + * Width of the border around the bubble. + */ +Blockly.Bubble.BORDER_WIDTH = 6; + +/** + * Determines the thickness of the base of the arrow in relation to the size + * of the bubble. Higher numbers result in thinner arrows. + */ +Blockly.Bubble.ARROW_THICKNESS = 10; + +/** + * The number of degrees that the arrow bends counter-clockwise. + */ +Blockly.Bubble.ARROW_ANGLE = 20; + +/** + * The sharpness of the arrow's bend. Higher numbers result in smoother arrows. + */ +Blockly.Bubble.ARROW_BEND = 4; + +/** + * Distance between arrow point and anchor point. + */ +Blockly.Bubble.ANCHOR_RADIUS = 8; + +/** + * Wrapper function called when a mouseUp occurs during a drag operation. + * @type {Array.} + * @private + */ +Blockly.Bubble.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mouseMove occurs during a drag operation. + * @type {Array.} + * @private + */ +Blockly.Bubble.onMouseMoveWrapper_ = null; + +/** + * Function to call on resize of bubble. + * @type {Function} + */ +Blockly.Bubble.prototype.resizeCallback_ = null; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Bubble.unbindDragEvents_ = function() { + if (Blockly.Bubble.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_); + Blockly.Bubble.onMouseUpWrapper_ = null; + } + if (Blockly.Bubble.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_); + Blockly.Bubble.onMouseMoveWrapper_ = null; + } +}; + +/** + * Flag to stop incremental rendering during construction. + * @private + */ +Blockly.Bubble.prototype.rendered_ = false; + +/** + * Absolute coordinate of anchor point. + * @type {goog.math.Coordinate} + * @private + */ +Blockly.Bubble.prototype.anchorXY_ = null; + +/** + * Relative X coordinate of bubble with respect to the anchor's centre. + * In RTL mode the initial value is negated. + * @private + */ +Blockly.Bubble.prototype.relativeLeft_ = 0; + +/** + * Relative Y coordinate of bubble with respect to the anchor's centre. + * @private + */ +Blockly.Bubble.prototype.relativeTop_ = 0; + +/** + * Width of bubble. + * @private + */ +Blockly.Bubble.prototype.width_ = 0; + +/** + * Height of bubble. + * @private + */ +Blockly.Bubble.prototype.height_ = 0; + +/** + * Automatically position and reposition the bubble. + * @private + */ +Blockly.Bubble.prototype.autoLayout_ = true; + +/** + * Create the bubble's DOM. + * @param {!Element} content SVG content for the bubble. + * @param {boolean} hasResize Add diagonal resize gripper if true. + * @return {!Element} The bubble's SVG group. + * @private + */ +Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { + /* Create the bubble. Here's the markup that will be generated: + + + + + + + + + + + [...content goes here...] + + */ + this.bubbleGroup_ = Blockly.createSvgElement('g', {}, null); + var filter = + {'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'}; + if (goog.userAgent.getUserAgentString().indexOf('JavaFX') != -1) { + // Multiple reports that JavaFX can't handle filters. UserAgent: + // Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.44 + // (KHTML, like Gecko) JavaFX/8.0 Safari/537.44 + // https://github.com/google/blockly/issues/99 + filter = {}; + } + var bubbleEmboss = Blockly.createSvgElement('g', + filter, this.bubbleGroup_); + this.bubbleArrow_ = Blockly.createSvgElement('path', {}, bubbleEmboss); + this.bubbleBack_ = Blockly.createSvgElement('rect', + {'class': 'blocklyDraggable', 'x': 0, 'y': 0, + 'rx': Blockly.Bubble.BORDER_WIDTH, 'ry': Blockly.Bubble.BORDER_WIDTH}, + bubbleEmboss); + if (hasResize) { + this.resizeGroup_ = Blockly.createSvgElement('g', + {'class': this.workspace_.RTL ? + 'blocklyResizeSW' : 'blocklyResizeSE'}, + this.bubbleGroup_); + var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; + Blockly.createSvgElement('polygon', + {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())}, + this.resizeGroup_); + Blockly.createSvgElement('line', + {'class': 'blocklyResizeLine', + 'x1': resizeSize / 3, 'y1': resizeSize - 1, + 'x2': resizeSize - 1, 'y2': resizeSize / 3}, this.resizeGroup_); + Blockly.createSvgElement('line', + {'class': 'blocklyResizeLine', + 'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1, + 'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3}, this.resizeGroup_); + } else { + this.resizeGroup_ = null; + } + this.bubbleGroup_.appendChild(content); + return this.bubbleGroup_; +}; + +/** + * Handle a mouse-down on bubble's border. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) { + this.promote_(); + Blockly.Bubble.unbindDragEvents_(); + if (Blockly.isRightButton(e)) { + // No right-click. + e.stopPropagation(); + return; + } else if (Blockly.isTargetInput_(e)) { + // When focused on an HTML text input widget, don't trap any events. + return; + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.workspace_.startDrag(e, new goog.math.Coordinate( + this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_, + this.relativeTop_)); + + Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, Blockly.Bubble.unbindDragEvents_); + Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.bubbleMouseMove_); + Blockly.hideChaff(); + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); +}; + +/** + * Drag this bubble to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Bubble.prototype.bubbleMouseMove_ = function(e) { + this.autoLayout_ = false; + var newXY = this.workspace_.moveDrag(e); + this.relativeLeft_ = this.workspace_.RTL ? -newXY.x : newXY.x; + this.relativeTop_ = newXY.y; + this.positionBubble_(); + this.renderArrow_(); +}; + +/** + * Handle a mouse-down on bubble's resize corner. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Bubble.prototype.resizeMouseDown_ = function(e) { + this.promote_(); + Blockly.Bubble.unbindDragEvents_(); + if (Blockly.isRightButton(e)) { + // No right-click. + e.stopPropagation(); + return; + } + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + + this.workspace_.startDrag(e, new goog.math.Coordinate( + this.workspace_.RTL ? -this.width_ : this.width_, this.height_)); + + Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, Blockly.Bubble.unbindDragEvents_); + Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.resizeMouseMove_); + Blockly.hideChaff(); + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); +}; + +/** + * Resize this bubble to follow the mouse. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Bubble.prototype.resizeMouseMove_ = function(e) { + this.autoLayout_ = false; + var newXY = this.workspace_.moveDrag(e); + this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y); + if (this.workspace_.RTL) { + // RTL requires the bubble to move its left edge. + this.positionBubble_(); + } +}; + +/** + * Register a function as a callback event for when the bubble is resized. + * @param {!Function} callback The function to call on resize. + */ +Blockly.Bubble.prototype.registerResizeEvent = function(callback) { + this.resizeCallback_ = callback; +}; + +/** + * Move this bubble to the top of the stack. + * @private + */ +Blockly.Bubble.prototype.promote_ = function() { + var svgGroup = this.bubbleGroup_.parentNode; + svgGroup.appendChild(this.bubbleGroup_); +}; + +/** + * Notification that the anchor has moved. + * Update the arrow and bubble accordingly. + * @param {!goog.math.Coordinate} xy Absolute location. + */ +Blockly.Bubble.prototype.setAnchorLocation = function(xy) { + this.anchorXY_ = xy; + if (this.rendered_) { + this.positionBubble_(); + } +}; + +/** + * Position the bubble so that it does not fall off-screen. + * @private + */ +Blockly.Bubble.prototype.layoutBubble_ = function() { + // Compute the preferred bubble location. + var relativeLeft = -this.width_ / 4; + var relativeTop = -this.height_ - Blockly.BlockSvg.MIN_BLOCK_Y; + // Prevent the bubble from being off-screen. + var metrics = this.workspace_.getMetrics(); + metrics.viewWidth /= this.workspace_.scale; + metrics.viewLeft /= this.workspace_.scale; + var anchorX = this.anchorXY_.x; + if (this.workspace_.RTL) { + if (anchorX - metrics.viewLeft - relativeLeft - this.width_ < + Blockly.Scrollbar.scrollbarThickness) { + // Slide the bubble right until it is onscreen. + relativeLeft = anchorX - metrics.viewLeft - this.width_ - + Blockly.Scrollbar.scrollbarThickness; + } else if (anchorX - metrics.viewLeft - relativeLeft > + metrics.viewWidth) { + // Slide the bubble left until it is onscreen. + relativeLeft = anchorX - metrics.viewLeft - metrics.viewWidth; + } + } else { + if (anchorX + relativeLeft < metrics.viewLeft) { + // Slide the bubble right until it is onscreen. + relativeLeft = metrics.viewLeft - anchorX; + } else if (metrics.viewLeft + metrics.viewWidth < + anchorX + relativeLeft + this.width_ + + Blockly.BlockSvg.SEP_SPACE_X + + Blockly.Scrollbar.scrollbarThickness) { + // Slide the bubble left until it is onscreen. + relativeLeft = metrics.viewLeft + metrics.viewWidth - anchorX - + this.width_ - Blockly.Scrollbar.scrollbarThickness; + } + } + if (this.anchorXY_.y + relativeTop < metrics.viewTop) { + // Slide the bubble below the block. + var bBox = /** @type {SVGLocatable} */ (this.shape_).getBBox(); + relativeTop = bBox.height; + } + this.relativeLeft_ = relativeLeft; + this.relativeTop_ = relativeTop; +}; + +/** + * Move the bubble to a location relative to the anchor's centre. + * @private + */ +Blockly.Bubble.prototype.positionBubble_ = function() { + var left = this.anchorXY_.x; + if (this.workspace_.RTL) { + left -= this.relativeLeft_ + this.width_; + } else { + left += this.relativeLeft_; + } + var top = this.relativeTop_ + this.anchorXY_.y; + this.bubbleGroup_.setAttribute('transform', + 'translate(' + left + ',' + top + ')'); +}; + +/** + * Get the dimensions of this bubble. + * @return {!Object} Object with width and height properties. + */ +Blockly.Bubble.prototype.getBubbleSize = function() { + return {width: this.width_, height: this.height_}; +}; + +/** + * Size this bubble. + * @param {number} width Width of the bubble. + * @param {number} height Height of the bubble. + */ +Blockly.Bubble.prototype.setBubbleSize = function(width, height) { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; + // Minimum size of a bubble. + width = Math.max(width, doubleBorderWidth + 45); + height = Math.max(height, doubleBorderWidth + 20); + this.width_ = width; + this.height_ = height; + this.bubbleBack_.setAttribute('width', width); + this.bubbleBack_.setAttribute('height', height); + if (this.resizeGroup_) { + if (this.workspace_.RTL) { + // Mirror the resize group. + var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH; + this.resizeGroup_.setAttribute('transform', 'translate(' + + resizeSize + ',' + (height - doubleBorderWidth) + ') scale(-1 1)'); + } else { + this.resizeGroup_.setAttribute('transform', 'translate(' + + (width - doubleBorderWidth) + ',' + + (height - doubleBorderWidth) + ')'); + } + } + if (this.rendered_) { + if (this.autoLayout_) { + this.layoutBubble_(); + } + this.positionBubble_(); + this.renderArrow_(); + } + // Allow the contents to resize. + if (this.resizeCallback_) { + this.resizeCallback_(); + } +}; + +/** + * Draw the arrow between the bubble and the origin. + * @private + */ +Blockly.Bubble.prototype.renderArrow_ = function() { + var steps = []; + // Find the relative coordinates of the center of the bubble. + var relBubbleX = this.width_ / 2; + var relBubbleY = this.height_ / 2; + // Find the relative coordinates of the center of the anchor. + var relAnchorX = -this.relativeLeft_; + var relAnchorY = -this.relativeTop_; + if (relBubbleX == relAnchorX && relBubbleY == relAnchorY) { + // Null case. Bubble is directly on top of the anchor. + // Short circuit this rather than wade through divide by zeros. + steps.push('M ' + relBubbleX + ',' + relBubbleY); + } else { + // Compute the angle of the arrow's line. + var rise = relAnchorY - relBubbleY; + var run = relAnchorX - relBubbleX; + if (this.workspace_.RTL) { + run *= -1; + } + var hypotenuse = Math.sqrt(rise * rise + run * run); + var angle = Math.acos(run / hypotenuse); + if (rise < 0) { + angle = 2 * Math.PI - angle; + } + // Compute a line perpendicular to the arrow. + var rightAngle = angle + Math.PI / 2; + if (rightAngle > Math.PI * 2) { + rightAngle -= Math.PI * 2; + } + var rightRise = Math.sin(rightAngle); + var rightRun = Math.cos(rightAngle); + + // Calculate the thickness of the base of the arrow. + var bubbleSize = this.getBubbleSize(); + var thickness = (bubbleSize.width + bubbleSize.height) / + Blockly.Bubble.ARROW_THICKNESS; + thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 2; + + // Back the tip of the arrow off of the anchor. + var backoffRatio = 1 - Blockly.Bubble.ANCHOR_RADIUS / hypotenuse; + relAnchorX = relBubbleX + backoffRatio * run; + relAnchorY = relBubbleY + backoffRatio * rise; + + // Coordinates for the base of the arrow. + var baseX1 = relBubbleX + thickness * rightRun; + var baseY1 = relBubbleY + thickness * rightRise; + var baseX2 = relBubbleX - thickness * rightRun; + var baseY2 = relBubbleY - thickness * rightRise; + + // Distortion to curve the arrow. + var swirlAngle = angle + this.arrow_radians_; + if (swirlAngle > Math.PI * 2) { + swirlAngle -= Math.PI * 2; + } + var swirlRise = Math.sin(swirlAngle) * + hypotenuse / Blockly.Bubble.ARROW_BEND; + var swirlRun = Math.cos(swirlAngle) * + hypotenuse / Blockly.Bubble.ARROW_BEND; + + steps.push('M' + baseX1 + ',' + baseY1); + steps.push('C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + + ' ' + relAnchorX + ',' + relAnchorY + + ' ' + relAnchorX + ',' + relAnchorY); + steps.push('C' + relAnchorX + ',' + relAnchorY + + ' ' + (baseX2 + swirlRun) + ',' + (baseY2 + swirlRise) + + ' ' + baseX2 + ',' + baseY2); + } + steps.push('z'); + this.bubbleArrow_.setAttribute('d', steps.join(' ')); +}; + +/** + * Change the colour of a bubble. + * @param {string} hexColour Hex code of colour. + */ +Blockly.Bubble.prototype.setColour = function(hexColour) { + this.bubbleBack_.setAttribute('fill', hexColour); + this.bubbleArrow_.setAttribute('fill', hexColour); +}; + +/** + * Dispose of this bubble. + */ +Blockly.Bubble.prototype.dispose = function() { + Blockly.Bubble.unbindDragEvents_(); + // Dispose of and unlink the bubble. + goog.dom.removeNode(this.bubbleGroup_); + this.bubbleGroup_ = null; + this.bubbleArrow_ = null; + this.bubbleBack_ = null; + this.resizeGroup_ = null; + this.workspace_ = null; + this.content_ = null; + this.shape_ = null; +}; diff --git a/core/connection.js b/core/connection.js index 3b8c82af8..0e3fc0df0 100644 --- a/core/connection.js +++ b/core/connection.js @@ -212,6 +212,9 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { } }, Blockly.BUMP_DELAY); } + if (block.errorIcon) { + block.errorIcon.setVisible(false); + } } // Restore the shadow DOM. parentConnection.setShadowDom(shadowDom); diff --git a/core/connection.js.orig b/core/connection.js.orig new file mode 100644 index 000000000..3b8c82af8 --- /dev/null +++ b/core/connection.js.orig @@ -0,0 +1,615 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 Components for creating connections between blocks. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Connection'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); + + +/** + * Class for a connection between blocks. + * @param {!Blockly.Block} source The block establishing this connection. + * @param {number} type The type of the connection. + * @constructor + */ +Blockly.Connection = function(source, type) { + /** + * @type {!Blockly.Block} + * @private + */ + this.sourceBlock_ = source; + /** @type {number} */ + this.type = type; + // Shortcut for the databases for this connection's workspace. + if (source.workspace.connectionDBList) { + this.db_ = source.workspace.connectionDBList[type]; + this.dbOpposite_ = + source.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[type]]; + this.hidden_ = !this.db_; + } +}; + +/** + * Constants for checking whether two connections are compatible. + */ +Blockly.Connection.CAN_CONNECT = 0; +Blockly.Connection.REASON_SELF_CONNECTION = 1; +Blockly.Connection.REASON_WRONG_TYPE = 2; +Blockly.Connection.REASON_TARGET_NULL = 3; +Blockly.Connection.REASON_CHECKS_FAILED = 4; +Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5; +Blockly.Connection.REASON_SHADOW_PARENT = 6; + +/** + * Connection this connection connects to. Null if not connected. + * @type {Blockly.Connection} + */ +Blockly.Connection.prototype.targetConnection = null; + +/** + * List of compatible value types. Null if all types are compatible. + * @type {Array} + * @private + */ +Blockly.Connection.prototype.check_ = null; + +/** + * DOM representation of a shadow block, or null if none. + * @type {Element} + * @private + */ +Blockly.Connection.prototype.shadowDom_ = null; + +/** + * Horizontal location of this connection. + * @type {number} + * @private + */ +Blockly.Connection.prototype.x_ = 0; + +/** + * Vertical location of this connection. + * @type {number} + * @private + */ +Blockly.Connection.prototype.y_ = 0; + +/** + * Has this connection been added to the connection database? + * @type {boolean} + * @private + */ +Blockly.Connection.prototype.inDB_ = false; + +/** + * Connection database for connections of this type on the current workspace. + * @type {Blockly.ConnectionDB} + * @private + */ +Blockly.Connection.prototype.db_ = null; + +/** + * Connection database for connections compatible with this type on the + * current workspace. + * @type {Blockly.ConnectionDB} + * @private + */ +Blockly.Connection.prototype.dbOpposite_ = null; + +/** + * Whether this connections is hidden (not tracked in a database) or not. + * @type {boolean} + * @private + */ +Blockly.Connection.prototype.hidden_ = null; + +/** + * Connect two connections together. This is the connection on the superior + * block. + * @param {!Blockly.Connection} childConnection Connection on inferior block. + * @private + */ +Blockly.Connection.prototype.connect_ = function(childConnection) { + var parentConnection = this; + var parentBlock = parentConnection.getSourceBlock(); + var childBlock = childConnection.getSourceBlock(); + // Disconnect any existing parent on the child connection. + if (childConnection.isConnected()) { + childConnection.disconnect(); + } + if (parentConnection.isConnected()) { + // Other connection is already connected to something. + // Disconnect it and reattach it or bump it as needed. + var orphanBlock = parentConnection.targetBlock(); + var shadowDom = parentConnection.getShadowDom(); + // Temporarily set the shadow DOM to null so it does not respawn. + parentConnection.setShadowDom(null); + // Displaced shadow blocks dissolve rather than reattaching or bumping. + if (orphanBlock.isShadow()) { + // Save the shadow block so that field values are preserved. + shadowDom = Blockly.Xml.blockToDom(orphanBlock); + orphanBlock.dispose(); + orphanBlock = null; + } else if (parentConnection.type == Blockly.INPUT_VALUE) { + // Value connections. + // If female block is already connected, disconnect and bump the male. + if (!orphanBlock.outputConnection) { + throw 'Orphan block does not have an output connection.'; + } + // Attempt to reattach the orphan at the end of the newly inserted + // block. Since this block may be a row, walk down to the end + // or to the first (and only) shadow block. + var connection = Blockly.Connection.lastConnectionInRow_( + childBlock, orphanBlock); + if (connection) { + orphanBlock.outputConnection.connect(connection); + orphanBlock = null; + } + } else if (parentConnection.type == Blockly.NEXT_STATEMENT) { + // Statement connections. + // Statement blocks may be inserted into the middle of a stack. + // Split the stack. + if (!orphanBlock.previousConnection) { + throw 'Orphan block does not have a previous connection.'; + } + // Attempt to reattach the orphan at the bottom of the newly inserted + // block. Since this block may be a stack, walk down to the end. + var newBlock = childBlock; + while (newBlock.nextConnection) { + var nextBlock = newBlock.getNextBlock(); + if (nextBlock && !nextBlock.isShadow()) { + newBlock = nextBlock; + } else { + if (orphanBlock.previousConnection.checkType_( + newBlock.nextConnection)) { + newBlock.nextConnection.connect(orphanBlock.previousConnection); + orphanBlock = null; + } + break; + } + } + } + if (orphanBlock) { + // Unable to reattach orphan. + parentConnection.disconnect(); + if (Blockly.Events.recordUndo) { + // Bump it off to the side after a moment. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + // Verify orphan hasn't been deleted or reconnected (user on meth). + if (orphanBlock.workspace && !orphanBlock.getParent()) { + Blockly.Events.setGroup(group); + if (orphanBlock.outputConnection) { + orphanBlock.outputConnection.bumpAwayFrom_(parentConnection); + } else if (orphanBlock.previousConnection) { + orphanBlock.previousConnection.bumpAwayFrom_(parentConnection); + } + Blockly.Events.setGroup(false); + } + }, Blockly.BUMP_DELAY); + } + } + // Restore the shadow DOM. + parentConnection.setShadowDom(shadowDom); + } + + var event; + if (Blockly.Events.isEnabled()) { + event = new Blockly.Events.Move(childBlock); + } + // Establish the connections. + Blockly.Connection.connectReciprocally_(parentConnection, childConnection); + // Demote the inferior block so that one is a child of the superior one. + childBlock.setParent(parentBlock); + if (event) { + event.recordNew(); + Blockly.Events.fire(event); + } +}; + +/** + * Sever all links to this connection (not including from the source object). + */ +Blockly.Connection.prototype.dispose = function() { + if (this.isConnected()) { + throw 'Disconnect connection before disposing of it.'; + } + if (this.inDB_) { + this.db_.removeConnection_(this); + } + if (Blockly.highlightedConnection_ == this) { + Blockly.highlightedConnection_ = null; + } + if (Blockly.localConnection_ == this) { + Blockly.localConnection_ = null; + } + this.db_ = null; + this.dbOpposite_ = null; +}; + +/** + * Get the source block for this connection. + * @return {Blockly.Block} The source block, or null if there is none. + */ +Blockly.Connection.prototype.getSourceBlock = function() { + return this.sourceBlock_; +}; + +/** + * Does the connection belong to a superior block (higher in the source stack)? + * @return {boolean} True if connection faces down or right. + */ +Blockly.Connection.prototype.isSuperior = function() { + return this.type == Blockly.INPUT_VALUE || + this.type == Blockly.NEXT_STATEMENT; +}; + +/** + * Is the connection connected? + * @return {boolean} True if connection is connected to another connection. + */ +Blockly.Connection.prototype.isConnected = function() { + return !!this.targetConnection; +}; + +/** + * Checks whether the current connection can connect with the target + * connection. + * @param {Blockly.Connection} target Connection to check compatibility with. + * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, + * an error code otherwise. + * @private + */ +Blockly.Connection.prototype.canConnectWithReason_ = function(target) { + if (!target) { + return Blockly.Connection.REASON_TARGET_NULL; + } + if (this.isSuperior()) { + var blockA = this.sourceBlock_; + var blockB = target.getSourceBlock(); + } else { + var blockB = this.sourceBlock_; + var blockA = target.getSourceBlock(); + } + if (blockA && blockA == blockB) { + return Blockly.Connection.REASON_SELF_CONNECTION; + } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) { + return Blockly.Connection.REASON_WRONG_TYPE; + } else if (blockA && blockB && blockA.workspace !== blockB.workspace) { + return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; + } else if (!this.checkType_(target)) { + return Blockly.Connection.REASON_CHECKS_FAILED; + } else if (blockA.isShadow() && !blockB.isShadow()) { + return Blockly.Connection.REASON_SHADOW_PARENT; + } + return Blockly.Connection.CAN_CONNECT; +}; + +/** + * Checks whether the current connection and target connection are compatible + * and throws an exception if they are not. + * @param {Blockly.Connection} target The connection to check compatibility + * with. + * @private + */ +Blockly.Connection.prototype.checkConnection_ = function(target) { + switch (this.canConnectWithReason_(target)) { + case Blockly.Connection.CAN_CONNECT: + break; + case Blockly.Connection.REASON_SELF_CONNECTION: + throw 'Attempted to connect a block to itself.'; + case Blockly.Connection.REASON_DIFFERENT_WORKSPACES: + // Usually this means one block has been deleted. + throw 'Blocks not on same workspace.'; + case Blockly.Connection.REASON_WRONG_TYPE: + throw 'Attempt to connect incompatible types.'; + case Blockly.Connection.REASON_TARGET_NULL: + throw 'Target connection is null.'; + case Blockly.Connection.REASON_CHECKS_FAILED: + throw 'Connection checks failed.'; + case Blockly.Connection.REASON_SHADOW_PARENT: + throw 'Connecting non-shadow to shadow block.'; + default: + throw 'Unknown connection failure: this should never happen!'; + } +}; + +/** + * Check if the two connections can be dragged to connect to each other. + * @param {!Blockly.Connection} candidate A nearby connection to check. + * @return {boolean} True if the connection is allowed, false otherwise. + */ +Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { + // Type checking. + var canConnect = this.canConnectWithReason_(candidate); + if (canConnect != Blockly.Connection.CAN_CONNECT) { + return false; + } + + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. Don't offer to connect the + // bottom of a statement block to one that's already connected. + if (candidate.type == Blockly.OUTPUT_VALUE || + candidate.type == Blockly.PREVIOUS_STATEMENT) { + if (candidate.isConnected() || this.isConnected()) { + return false; + } + } + + // Offering to connect the left (male) of a value block to an already + // connected value pair is ok, we'll splice it in. + // However, don't offer to splice into an immovable block. + if (candidate.type == Blockly.INPUT_VALUE && candidate.isConnected() && + !candidate.targetBlock().isMovable() && + !candidate.targetBlock().isShadow()) { + return false; + } + + // Don't let a block with no next connection bump other blocks out of the + // stack. But covering up a shadow block or stack of shadow blocks is fine. + // Similarly, replacing a terminal statement with another terminal statement + // is allowed. + if (this.type == Blockly.PREVIOUS_STATEMENT && + candidate.isConnected() && + !this.sourceBlock_.nextConnection && + !candidate.targetBlock().isShadow() && + candidate.targetBlock().nextConnection) { + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (Blockly.draggingConnections_.indexOf(candidate) != -1) { + return false; + } + + return true; +}; + +/** + * Connect this connection to another connection. + * @param {!Blockly.Connection} otherConnection Connection to connect to. + */ +Blockly.Connection.prototype.connect = function(otherConnection) { + if (this.targetConnection == otherConnection) { + // Already connected together. NOP. + return; + } + this.checkConnection_(otherConnection); + // Determine which block is superior (higher in the source stack). + if (this.isSuperior()) { + // Superior block. + this.connect_(otherConnection); + } else { + // Inferior block. + otherConnection.connect_(this); + } +}; + +/** + * Update two connections to target each other. + * @param {Blockly.Connection} first The first connection to update. + * @param {Blockly.Connection} second The second conneciton to update. + * @private + */ +Blockly.Connection.connectReciprocally_ = function(first, second) { + goog.asserts.assert(first && second, 'Cannot connect null connections.'); + first.targetConnection = second; + second.targetConnection = first; +}; + +/** + * Does the given block have one and only one connection point that will accept + * an orphaned block? + * @param {!Blockly.Block} block The superior block. + * @param {!Blockly.Block} orphanBlock The inferior block. + * @return {Blockly.Connection} The suitable connection point on 'block', + * or null. + * @private + */ +Blockly.Connection.singleConnection_ = function(block, orphanBlock) { + var connection = false; + for (var i = 0; i < block.inputList.length; i++) { + var thisConnection = block.inputList[i].connection; + if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE && + orphanBlock.outputConnection.checkType_(thisConnection)) { + if (connection) { + return null; // More than one connection. + } + connection = thisConnection; + } + } + return connection; +}; + +/** + * Walks down a row a blocks, at each stage checking if there are any + * connections that will accept the orphaned block. If at any point there + * are zero or multiple eligible connections, returns null. Otherwise + * returns the only input on the last block in the chain. + * Terminates early for shadow blocks. + * @param {!Blockly.Block} startBlock The block on which to start the search. + * @param {!Blockly.Block} orphanBlock The block that is looking for a home. + * @return {Blockly.Connection} The suitable connection point on the chain + * of blocks, or null. + * @private + */ +Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) { + var newBlock = startBlock; + var connection; + while (connection = Blockly.Connection.singleConnection_( + /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) { + // '=' is intentional in line above. + newBlock = connection.targetBlock(); + if (!newBlock || newBlock.isShadow()) { + return connection; + } + } + return null; +}; + +/** + * Disconnect this connection. + */ +Blockly.Connection.prototype.disconnect = function() { + var otherConnection = this.targetConnection; + goog.asserts.assert(otherConnection, 'Source connection not connected.'); + goog.asserts.assert(otherConnection.targetConnection == this, + 'Target connection not connected to source connection.'); + + var parentBlock, childBlock, parentConnection; + if (this.isSuperior()) { + // Superior block. + parentBlock = this.sourceBlock_; + childBlock = otherConnection.getSourceBlock(); + parentConnection = this; + } else { + // Inferior block. + parentBlock = otherConnection.getSourceBlock(); + childBlock = this.sourceBlock_; + parentConnection = otherConnection; + } + this.disconnectInternal_(parentBlock, childBlock); + parentConnection.respawnShadow_(); +}; + +/** + * Disconnect two blocks that are connected by this connection. + * @param {!Blockly.Block} parentBlock The superior block. + * @param {!Blockly.Block} childBlock The inferior block. + * @private + */ +Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock, + childBlock) { + var event; + if (Blockly.Events.isEnabled()) { + event = new Blockly.Events.Move(childBlock); + } + var otherConnection = this.targetConnection; + otherConnection.targetConnection = null; + this.targetConnection = null; + childBlock.setParent(null); + if (event) { + event.recordNew(); + Blockly.Events.fire(event); + } +}; + +/** + * Respawn the shadow block if there was one connected to the this connection. + * @private + */ +Blockly.Connection.prototype.respawnShadow_ = function() { + var parentBlock = this.getSourceBlock(); + var shadow = this.getShadowDom(); + if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { + var blockShadow = + Blockly.Xml.domToBlock(shadow, parentBlock.workspace); + if (blockShadow.outputConnection) { + this.connect(blockShadow.outputConnection); + } else if (blockShadow.previousConnection) { + this.connect(blockShadow.previousConnection); + } else { + throw 'Child block does not have output or previous statement.'; + } + } +}; + +/** + * Returns the block that this connection connects to. + * @return {Blockly.Block} The connected block or null if none is connected. + */ +Blockly.Connection.prototype.targetBlock = function() { + if (this.isConnected()) { + return this.targetConnection.getSourceBlock(); + } + return null; +}; + +/** + * Is this connection compatible with another connection with respect to the + * value type system. E.g. square_root("Hello") is not compatible. + * @param {!Blockly.Connection} otherConnection Connection to compare against. + * @return {boolean} True if the connections share a type. + * @private + */ +Blockly.Connection.prototype.checkType_ = function(otherConnection) { + if (!this.check_ || !otherConnection.check_) { + // One or both sides are promiscuous enough that anything will fit. + return true; + } + // Find any intersection in the check lists. + for (var i = 0; i < this.check_.length; i++) { + if (otherConnection.check_.indexOf(this.check_[i]) != -1) { + return true; + } + } + // No intersection. + return false; +}; + +/** + * Change a connection's compatibility. + * @param {*} check Compatible value type or list of value types. + * Null if all types are compatible. + * @return {!Blockly.Connection} The connection being modified + * (to allow chaining). + */ +Blockly.Connection.prototype.setCheck = function(check) { + if (check) { + // Ensure that check is in an array. + if (!goog.isArray(check)) { + check = [check]; + } + this.check_ = check; + // The new value type may not be compatible with the existing connection. + if (this.isConnected() && !this.checkType_(this.targetConnection)) { + var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; + child.unplug(); + // Bump away. + this.sourceBlock_.bumpNeighbours_(); + } + } else { + this.check_ = null; + } + return this; +}; + +/** + * Change a connection's shadow block. + * @param {Element} shadow DOM representation of a block or null. + */ +Blockly.Connection.prototype.setShadowDom = function(shadow) { + this.shadowDom_ = shadow; +}; + +/** + * Return a connection's shadow block. + * @return {Element} shadow DOM representation of a block or null. + */ +Blockly.Connection.prototype.getShadowDom = function() { + return this.shadowDom_; +}; diff --git a/core/connection.js.rej b/core/connection.js.rej new file mode 100644 index 000000000..95cb2a6d2 --- /dev/null +++ b/core/connection.js.rej @@ -0,0 +1,71 @@ +*************** +*** 39,45 **** + Blockly.Connection = function(source, type) { + this.sourceBlock_ = source; + this.targetConnection = null; +- this.type = type; + this.x_ = 0; + this.y_ = 0; + this.inDB_ = false; +--- 39,50 ---- + Blockly.Connection = function(source, type) { + this.sourceBlock_ = source; + this.targetConnection = null; ++ if (type == Blockly.INDENTED_VALUE) { ++ this.type = Blockly.INPUT_VALUE; ++ this.subtype = Blockly.INDENTED_VALUE; ++ } else { ++ this.type = type; ++ } + this.x_ = 0; + this.y_ = 0; + this.inDB_ = false; +*************** +*** 176,181 **** + + // Demote the inferior block so that one is a child of the superior one. + childBlock.setParent(parentBlock); + + if (parentBlock.rendered) { + parentBlock.svg_.updateDisabled(); +--- 181,188 ---- + + // Demote the inferior block so that one is a child of the superior one. + childBlock.setParent(parentBlock); ++ // Rendering the child node will trigger a rendering of its parent. ++ // Rendering the parent node will move its connected children into position. + + if (parentBlock.rendered) { + parentBlock.svg_.updateDisabled(); +*************** +*** 510,518 **** + // One or both sides are promiscuous enough that anything will fit. + return true; + } +- // Find any intersection in the check lists. + for (var x = 0; x < this.check_.length; x++) { +- if (otherConnection.check_.indexOf(this.check_[x]) != -1) { + return true; + } + } +--- 517,536 ---- + // One or both sides are promiscuous enough that anything will fit. + return true; + } ++ // Find any intersection in the check lists, ++ // or if the check is a function, evaluate the function. + for (var x = 0; x < this.check_.length; x++) { ++ if ((otherConnection.check_.indexOf(this.check_[x]) != -1) || ++ (typeof this.check_[x] == "function" && this.check_[x](this,otherConnection))) { ++ return true; ++ } ++ } ++ ++ // If the check is a function on the other connection, ++ // evaluate the function to see if it evaluates to true. ++ for (var x = 0; x < otherConnection.check_.length; x++) { ++ if (typeof otherConnection.check_[x] == "function" && ++ otherConnection.check_[x](otherConnection,this)) { + return true; + } + } diff --git a/core/css.js b/core/css.js index 0871a4fae..53a71f9d8 100644 --- a/core/css.js +++ b/core/css.js @@ -24,6 +24,14 @@ */ 'use strict'; +/** + * [lyn, 10/10/13] + * + Added CSS tags blocklyFieldParameter and blocklyFieldParameterFlydown + * to control parameter flydowns. + * + Added CSS tags blocklyFieldProcedure and blocklyFieldProcedureFlydown + * to control procedure flydowns. + */ + goog.provide('Blockly.Css'); @@ -148,6 +156,74 @@ Blockly.Css.CONTENT = [ 'height: 100%;', 'position: relative;', '}', + '/*', + ' * [lyn, 10/08/13] Control parameter fields with flydown getter/setter blocks.', + ' * Brightening factors for variable color rgb(208,95,45):', + ' * 10%: rgb(212, 111, 66)', + ' * 20%: rgb(217, 127, 87)', + ' * 30%: rgb(222, 143, 108)', + ' * 40%: rgb(226, 159, 129)', + ' * 50%: rgb(231, 175, 150)', + ' * 60%: rgb(236, 191, 171)', + ' * 70%: rgb(240, 207, 192)', + ' * 80%: rgb(245, 223, 213)', + ' * 90%: rgb(250, 239, 234)', + ' */', + '.blocklyFieldParameter>rect {', + ' /* fill: rgb(231,175,150);*/ /* This looks too much like getter/setter var */', + ' fill: rgb(222, 143, 108);', + ' fill-opacity: 1.0;', + ' stroke-width: 2;', + ' stroke: rgb(231, 175, 150);', + '}', + '.blocklyFieldParameter>text {', + ' /* fill: #000; */ /* Use white rather than black on dark orange */', + ' stroke-width: 1;', + ' fill: #000;', + '}', + '.blocklyFieldParameter:hover>rect {', + ' stroke-width: 2;', + ' stroke: rgb(231,175,150);', + ' fill: rgb(231,175,150);', + ' fill-opacity: 1.0;', + '}', + '/*', + ' * [lyn, 10/08/13] Control flydown with the getter/setter blocks.', + ' */', + '.blocklyFieldParameterFlydown {', + ' fill: rgb(231,175,150);', + ' fill-opacity: 0.8;', + '}', + '/*', + ' * [lyn, 10/08/13] Control parameter fields with flydown procedure caller block.', + ' */', + '.blocklyFieldProcedure>rect {', + ' /* rgb(231,175,150) is procedure color rgb(124,83,133) brightened by 70% */', + ' fill: rgb(215,203,218);', + ' fill-opacity: 1.0;', + ' stroke-width: 0;', + ' stroke: #000;', + '}', + '.blocklyFieldProcedure>text {', + ' fill: #000;', + '}', + '.blocklyFieldProcedure:hover>rect {', + ' stroke-width: 2;', + ' stroke: #fff;', + ' fill: rgb(215,203,218);', + ' fill-opacity: 1.0;', + '}', + '/*', + ' * [lyn, 10/08/13] Control flydown with the procedure caller block.', + ' */', + '.blocklyFieldProcedureFlydown {', + ' fill: rgb(215,203,218);', + ' fill-opacity: 0.8;', + '}', + '/*', + ' * Don\'t allow users to select text. It gets annoying when trying to', + ' * drag a block and selected text moves instead.', + ' */', '.blocklyNonSelectable {', 'user-select: none;', @@ -280,7 +356,14 @@ Blockly.Css.CONTENT = [ '-webkit-user-select: none;', 'cursor: inherit;', '}', - + '/*', + ' * Selecting text for Errors and Warnings is allowed though.', + ' */', + '.blocklySvg text.blocklyErrorWarningText {', + ' -moz-user-select: text;', + ' -webkit-user-select: text;', + ' user-select: text;', + '}', '.blocklyHidden {', 'display: none;', '}', diff --git a/core/css.js.orig b/core/css.js.orig new file mode 100644 index 000000000..0871a4fae --- /dev/null +++ b/core/css.js.orig @@ -0,0 +1,786 @@ +/** + * @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 Inject Blockly's CSS synchronously. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Css'); + + +/** + * List of cursors. + * @enum {string} + */ +Blockly.Css.Cursor = { + OPEN: 'handopen', + CLOSED: 'handclosed', + DELETE: 'handdelete' +}; + +/** + * Current cursor (cached value). + * @type {string} + * @private + */ +Blockly.Css.currentCursor_ = ''; + +/** + * Large stylesheet added by Blockly.Css.inject. + * @type {Element} + * @private + */ +Blockly.Css.styleSheet_ = null; + +/** + * Path to media directory, with any trailing slash removed. + * @type {string} + * @private + */ +Blockly.Css.mediaPath_ = ''; + +/** + * Inject the CSS into the DOM. This is preferable over using a regular CSS + * file since: + * a) It loads synchronously and doesn't force a redraw later. + * b) It speeds up loading by not blocking on a separate HTTP transfer. + * c) The CSS content may be made dynamic depending on init options. + * @param {boolean} hasCss If false, don't inject CSS + * (providing CSS becomes the document's responsibility). + * @param {string} pathToMedia Path from page to the Blockly media directory. + */ +Blockly.Css.inject = function(hasCss, pathToMedia) { + // Only inject the CSS once. + if (Blockly.Css.styleSheet_) { + return; + } + // Placeholder for cursor rule. Must be first rule (index 0). + var text = '.blocklyDraggable {}\n'; + if (hasCss) { + text += Blockly.Css.CONTENT.join('\n'); + if (Blockly.FieldDate) { + text += Blockly.FieldDate.CSS.join('\n'); + } + } + // Strip off any trailing slash (either Unix or Windows). + Blockly.Css.mediaPath_ = pathToMedia.replace(/[\\\/]$/, ''); + text = text.replace(/<<>>/g, Blockly.Css.mediaPath_); + // Inject CSS tag. + var cssNode = document.createElement('style'); + document.head.appendChild(cssNode); + var cssTextNode = document.createTextNode(text); + cssNode.appendChild(cssTextNode); + Blockly.Css.styleSheet_ = cssNode.sheet; + Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN); +}; + +/** + * Set the cursor to be displayed when over something draggable. + * @param {Blockly.Css.Cursor} cursor Enum. + */ +Blockly.Css.setCursor = function(cursor) { + if (Blockly.Css.currentCursor_ == cursor) { + return; + } + Blockly.Css.currentCursor_ = cursor; + var url = 'url(' + Blockly.Css.mediaPath_ + '/' + cursor + '.cur), auto'; + // There are potentially hundreds of draggable objects. Changing their style + // properties individually is too slow, so change the CSS rule instead. + var rule = '.blocklyDraggable {\n cursor: ' + url + ';\n}\n'; + Blockly.Css.styleSheet_.deleteRule(0); + Blockly.Css.styleSheet_.insertRule(rule, 0); + // There is probably only one toolbox, so just change its style property. + var toolboxen = document.getElementsByClassName('blocklyToolboxDiv'); + for (var i = 0, toolbox; toolbox = toolboxen[i]; i++) { + if (cursor == Blockly.Css.Cursor.DELETE) { + toolbox.style.cursor = url; + } else { + toolbox.style.cursor = ''; + } + } + // Set cursor on the whole document, so that rapid movements + // don't result in cursor changing to an arrow momentarily. + var html = document.body.parentNode; + if (cursor == Blockly.Css.Cursor.OPEN) { + html.style.cursor = ''; + } else { + html.style.cursor = url; + } +}; + +/** + * Array making up the CSS content for Blockly. + */ +Blockly.Css.CONTENT = [ + '.blocklySvg {', + 'background-color: #fff;', + 'outline: none;', + 'overflow: hidden;', /* IE overflows by default. */ + 'display: block;', + '}', + + '.blocklyWidgetDiv {', + 'display: none;', + 'position: absolute;', + 'z-index: 99999;', /* big value for bootstrap3 compatibility */ + '}', + + '.injectionDiv {', + 'height: 100%;', + 'position: relative;', + '}', + + '.blocklyNonSelectable {', + 'user-select: none;', + '-moz-user-select: none;', + '-webkit-user-select: none;', + '-ms-user-select: none;', + '}', + + '.blocklyTooltipDiv {', + 'background-color: #ffffc7;', + 'border: 1px solid #ddc;', + 'box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);', + 'color: #000;', + 'display: none;', + 'font-family: sans-serif;', + 'font-size: 9pt;', + 'opacity: 0.9;', + 'padding: 2px;', + 'position: absolute;', + 'z-index: 100000;', /* big value for bootstrap3 compatibility */ + '}', + + '.blocklyResizeSE {', + 'cursor: se-resize;', + 'fill: #aaa;', + '}', + + '.blocklyResizeSW {', + 'cursor: sw-resize;', + 'fill: #aaa;', + '}', + + '.blocklyResizeLine {', + 'stroke: #888;', + 'stroke-width: 1;', + '}', + + '.blocklyHighlightedConnectionPath {', + 'fill: none;', + 'stroke: #fc3;', + 'stroke-width: 4px;', + '}', + + '.blocklyPathLight {', + 'fill: none;', + 'stroke-linecap: round;', + 'stroke-width: 1;', + '}', + + '.blocklySelected>.blocklyPath {', + 'stroke: #fc3;', + 'stroke-width: 3px;', + '}', + + '.blocklySelected>.blocklyPathLight {', + 'display: none;', + '}', + + '.blocklyDragging>.blocklyPath,', + '.blocklyDragging>.blocklyPathLight {', + 'fill-opacity: .8;', + 'stroke-opacity: .8;', + '}', + + '.blocklyDragging>.blocklyPathDark {', + 'display: none;', + '}', + + '.blocklyDisabled>.blocklyPath {', + 'fill-opacity: .5;', + 'stroke-opacity: .5;', + '}', + + '.blocklyDisabled>.blocklyPathLight,', + '.blocklyDisabled>.blocklyPathDark {', + 'display: none;', + '}', + + '.blocklyText {', + 'cursor: default;', + 'fill: #fff;', + 'font-family: sans-serif;', + 'font-size: 11pt;', + '}', + + '.blocklyNonEditableText>text {', + 'pointer-events: none;', + '}', + + '.blocklyNonEditableText>rect,', + '.blocklyEditableText>rect {', + 'fill: #fff;', + 'fill-opacity: .6;', + '}', + + '.blocklyNonEditableText>text,', + '.blocklyEditableText>text {', + 'fill: #000;', + '}', + + '.blocklyEditableText:hover>rect {', + 'stroke: #fff;', + 'stroke-width: 2;', + '}', + + '.blocklyBubbleText {', + 'fill: #000;', + '}', + + '.blocklyFlyoutButton {', + 'fill: #888;', + 'cursor: default;', + '}', + + '.blocklyFlyoutButtonShadow {', + 'fill: #444;', + '}', + + '.blocklyFlyoutButton:hover {', + 'fill: #aaa;', + '}', + + /* + Don't allow users to select text. It gets annoying when trying to + drag a block and selected text moves instead. + */ + '.blocklySvg text {', + 'user-select: none;', + '-moz-user-select: none;', + '-webkit-user-select: none;', + 'cursor: inherit;', + '}', + + '.blocklyHidden {', + 'display: none;', + '}', + + '.blocklyFieldDropdown:not(.blocklyHidden) {', + 'display: block;', + '}', + + '.blocklyIconGroup {', + 'cursor: default;', + '}', + + '.blocklyIconGroup:not(:hover),', + '.blocklyIconGroupReadonly {', + 'opacity: .6;', + '}', + + '.blocklyIconShape {', + 'fill: #00f;', + 'stroke: #fff;', + 'stroke-width: 1px;', + '}', + + '.blocklyIconSymbol {', + 'fill: #fff;', + '}', + + '.blocklyMinimalBody {', + 'margin: 0;', + 'padding: 0;', + '}', + + '.blocklyCommentTextarea {', + 'background-color: #ffc;', + 'border: 0;', + 'margin: 0;', + 'padding: 2px;', + 'resize: none;', + '}', + + '.blocklyHtmlInput {', + 'border: none;', + 'border-radius: 4px;', + 'font-family: sans-serif;', + 'height: 100%;', + 'margin: 0;', + 'outline: none;', + 'padding: 0 1px;', + 'width: 100%', + '}', + + '.blocklyMainBackground {', + 'stroke-width: 1;', + 'stroke: #c6c6c6;', /* Equates to #ddd due to border being off-pixel. */ + '}', + + '.blocklyMutatorBackground {', + 'fill: #fff;', + 'stroke: #ddd;', + 'stroke-width: 1;', + '}', + + '.blocklyFlyoutBackground {', + 'fill: #ddd;', + 'fill-opacity: .8;', + '}', + + '.blocklyScrollbarBackground {', + 'opacity: 0;', + '}', + + '.blocklyScrollbarHandle {', + 'fill: #ccc;', + '}', + + '.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,', + '.blocklyScrollbarHandle:hover {', + 'fill: #bbb;', + '}', + + '.blocklyZoom>image {', + 'opacity: .4;', + '}', + + '.blocklyZoom>image:hover {', + 'opacity: .6;', + '}', + + '.blocklyZoom>image:active {', + 'opacity: .8;', + '}', + + /* Darken flyout scrollbars due to being on a grey background. */ + /* By contrast, workspace scrollbars are on a white background. */ + '.blocklyFlyout .blocklyScrollbarHandle {', + 'fill: #bbb;', + '}', + + '.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,', + '.blocklyFlyout .blocklyScrollbarHandle:hover {', + 'fill: #aaa;', + '}', + + '.blocklyInvalidInput {', + 'background: #faa;', + '}', + + '.blocklyAngleCircle {', + 'stroke: #444;', + 'stroke-width: 1;', + 'fill: #ddd;', + 'fill-opacity: .8;', + '}', + + '.blocklyAngleMarks {', + 'stroke: #444;', + 'stroke-width: 1;', + '}', + + '.blocklyAngleGauge {', + 'fill: #f88;', + 'fill-opacity: .8;', + '}', + + '.blocklyAngleLine {', + 'stroke: #f00;', + 'stroke-width: 2;', + 'stroke-linecap: round;', + '}', + + '.blocklyContextMenu {', + 'border-radius: 4px;', + '}', + + '.blocklyDropdownMenu {', + 'padding: 0 !important;', + '}', + + /* Override the default Closure URL. */ + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', + 'background: url(<<>>/sprites.png) no-repeat -48px -16px !important;', + '}', + + /* Category tree in Toolbox. */ + '.blocklyToolboxDiv {', + 'background-color: #ddd;', + 'overflow-x: visible;', + 'overflow-y: auto;', + 'position: absolute;', + '}', + + '.blocklyTreeRoot {', + 'padding: 4px 0;', + '}', + + '.blocklyTreeRoot:focus {', + 'outline: none;', + '}', + + '.blocklyTreeRow {', + 'height: 22px;', + 'line-height: 22px;', + 'margin-bottom: 3px;', + 'padding-right: 8px;', + 'white-space: nowrap;', + '}', + + '.blocklyHorizontalTree {', + 'float: left;', + 'margin: 1px 5px 8px 0;', + '}', + + '.blocklyHorizontalTreeRtl {', + 'float: right;', + 'margin: 1px 0 8px 5px;', + '}', + + '.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {', + 'margin-left: 8px;', + '}', + + '.blocklyTreeRow:not(.blocklyTreeSelected):hover {', + 'background-color: #e4e4e4;', + '}', + + '.blocklyTreeSeparator {', + 'border-bottom: solid #e5e5e5 1px;', + 'height: 0;', + 'margin: 5px 0;', + '}', + + '.blocklyTreeSeparatorHorizontal {', + 'border-right: solid #e5e5e5 1px;', + 'width: 0;', + 'padding: 5px 0;', + 'margin: 0 5px;', + '}', + + + '.blocklyTreeIcon {', + 'background-image: url(<<>>/sprites.png);', + 'height: 16px;', + 'vertical-align: middle;', + 'width: 16px;', + '}', + + '.blocklyTreeIconClosedLtr {', + 'background-position: -32px -1px;', + '}', + + '.blocklyTreeIconClosedRtl {', + 'background-position: 0px -1px;', + '}', + + '.blocklyTreeIconOpen {', + 'background-position: -16px -1px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconClosedLtr {', + 'background-position: -32px -17px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconClosedRtl {', + 'background-position: 0px -17px;', + '}', + + '.blocklyTreeSelected>.blocklyTreeIconOpen {', + 'background-position: -16px -17px;', + '}', + + '.blocklyTreeIconNone,', + '.blocklyTreeSelected>.blocklyTreeIconNone {', + 'background-position: -48px -1px;', + '}', + + '.blocklyTreeLabel {', + 'cursor: default;', + 'font-family: sans-serif;', + 'font-size: 16px;', + 'padding: 0 3px;', + 'vertical-align: middle;', + '}', + + '.blocklyTreeSelected .blocklyTreeLabel {', + 'color: #fff;', + '}', + + /* Copied from: goog/css/colorpicker-simplegrid.css */ + /* + * Copyright 2007 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /* Author: pupius@google.com (Daniel Pupius) */ + + /* + Styles to make the colorpicker look like the old gmail color picker + NOTE: without CSS scoping this will override styles defined in palette.css + */ + '.blocklyWidgetDiv .goog-palette {', + 'outline: none;', + 'cursor: default;', + '}', + + '.blocklyWidgetDiv .goog-palette-table {', + 'border: 1px solid #666;', + 'border-collapse: collapse;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell {', + 'height: 13px;', + 'width: 15px;', + 'margin: 0;', + 'border: 0;', + 'text-align: center;', + 'vertical-align: middle;', + 'border-right: 1px solid #666;', + 'font-size: 1px;', + '}', + + '.blocklyWidgetDiv .goog-palette-colorswatch {', + 'position: relative;', + 'height: 13px;', + 'width: 15px;', + 'border: 1px solid #666;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell-hover .goog-palette-colorswatch {', + 'border: 1px solid #FFF;', + '}', + + '.blocklyWidgetDiv .goog-palette-cell-selected .goog-palette-colorswatch {', + 'border: 1px solid #000;', + 'color: #fff;', + '}', + + /* Copied from: goog/css/menu.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + '.blocklyWidgetDiv .goog-menu {', + 'background: #fff;', + 'border-color: #ccc #666 #666 #ccc;', + 'border-style: solid;', + 'border-width: 1px;', + 'cursor: default;', + 'font: normal 13px Arial, sans-serif;', + 'margin: 0;', + 'outline: none;', + 'padding: 4px 0;', + 'position: absolute;', + 'overflow-y: auto;', + 'overflow-x: hidden;', + 'max-height: 100%;', + 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ + '}', + + /* Copied from: goog/css/menuitem.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuItemRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + /** + * State: resting. + * + * NOTE(mleibman,chrishenry): + * The RTL support in Closure is provided via two mechanisms -- "rtl" CSS + * classes and BiDi flipping done by the CSS compiler. Closure supports RTL + * with or without the use of the CSS compiler. In order for them not + * to conflict with each other, the "rtl" CSS classes need to have the #noflip + * annotation. The non-rtl counterparts should ideally have them as well, but, + * since .goog-menuitem existed without .goog-menuitem-rtl for so long before + * being added, there is a risk of people having templates where they are not + * rendering the .goog-menuitem-rtl class when in RTL and instead rely solely + * on the BiDi flipping by the CSS compiler. That's why we're not adding the + * #noflip to .goog-menuitem. + */ + '.blocklyWidgetDiv .goog-menuitem {', + 'color: #000;', + 'font: normal 13px Arial, sans-serif;', + 'list-style: none;', + 'margin: 0;', + /* 28px on the left for icon or checkbox; 7em on the right for shortcut. */ + 'padding: 4px 7em 4px 28px;', + 'white-space: nowrap;', + '}', + + /* BiDi override for the resting state. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem.goog-menuitem-rtl {', + /* Flip left/right padding for BiDi. */ + 'padding-left: 7em;', + 'padding-right: 28px;', + '}', + + /* If a menu doesn't have checkable items or items with icons, remove padding. */ + '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,', + '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem {', + 'padding-left: 12px;', + '}', + + /* + * If a menu doesn't have items with shortcuts, leave just enough room for + * submenu arrows, if they are rendered. + */ + '.blocklyWidgetDiv .goog-menu-noaccel .goog-menuitem {', + 'padding-right: 20px;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-content {', + 'color: #000;', + 'font: normal 13px Arial, sans-serif;', + '}', + + /* State: disabled. */ + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-accel,', + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content {', + 'color: #ccc !important;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon {', + 'opacity: 0.3;', + '-moz-opacity: 0.3;', + 'filter: alpha(opacity=30);', + '}', + + /* State: hover. */ + '.blocklyWidgetDiv .goog-menuitem-highlight,', + '.blocklyWidgetDiv .goog-menuitem-hover {', + 'background-color: #d6e9f8;', + /* Use an explicit top and bottom border so that the selection is visible', + * in high contrast mode. */ + 'border-color: #d6e9f8;', + 'border-style: dotted;', + 'border-width: 1px 0;', + 'padding-bottom: 3px;', + 'padding-top: 3px;', + '}', + + /* State: selected/checked. */ + '.blocklyWidgetDiv .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-menuitem-icon {', + 'background-repeat: no-repeat;', + 'height: 16px;', + 'left: 6px;', + 'position: absolute;', + 'right: auto;', + 'vertical-align: middle;', + 'width: 16px;', + '}', + + /* BiDi override for the selected/checked state. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon {', + /* Flip left/right positioning. */ + 'left: auto;', + 'right: 6px;', + '}', + + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', + /* Client apps may override the URL at which they serve the sprite. */ + 'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', + '}', + + /* Keyboard shortcut ("accelerator") style. */ + '.blocklyWidgetDiv .goog-menuitem-accel {', + 'color: #999;', + /* Keyboard shortcuts are untranslated; always left-to-right. */ + /* #noflip */ + 'direction: ltr;', + 'left: auto;', + 'padding: 0 6px;', + 'position: absolute;', + 'right: 0;', + 'text-align: right;', + '}', + + /* BiDi override for shortcut style. */ + /* #noflip */ + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-accel {', + /* Flip left/right positioning and text alignment. */ + 'left: 0;', + 'right: auto;', + 'text-align: left;', + '}', + + /* Mnemonic styles. */ + '.blocklyWidgetDiv .goog-menuitem-mnemonic-hint {', + 'text-decoration: underline;', + '}', + + '.blocklyWidgetDiv .goog-menuitem-mnemonic-separator {', + 'color: #999;', + 'font-size: 12px;', + 'padding-left: 4px;', + '}', + + /* Copied from: goog/css/menuseparator.css */ + /* + * Copyright 2009 The Closure Library Authors. All Rights Reserved. + * + * Use of this source code is governed by the Apache License, Version 2.0. + * See the COPYING file for details. + */ + + /** + * Standard styling for menus created by goog.ui.MenuSeparatorRenderer. + * + * @author attila@google.com (Attila Bodis) + */ + + '.blocklyWidgetDiv .goog-menuseparator {', + 'border-top: 1px solid #ccc;', + 'margin: 4px 0;', + 'padding: 0;', + '}', + + '' +]; diff --git a/core/css.js.rej b/core/css.js.rej new file mode 100644 index 000000000..21e26c1a6 --- /dev/null +++ b/core/css.js.rej @@ -0,0 +1,56 @@ +*************** +*** 355,367 **** + ' outline: none;', + ' width: 100%', + '}', +- + '.blocklyMutatorBackground {', + ' fill: #fff;', + ' stroke-width: 1;', + ' stroke: #ddd;', + '}', +- + '.blocklyFlyoutBackground {', + ' fill: #ddd;', + ' fill-opacity: .8;', +--- 438,477 ---- + ' outline: none;', + ' width: 100%', + '}', ++ '.blocklyContextMenuBackground,', + '.blocklyMutatorBackground {', + ' fill: #fff;', + ' stroke-width: 1;', + ' stroke: #ddd;', + '}', ++ '.blocklyContextMenuOptions>.blocklyMenuDiv,', ++ '.blocklyContextMenuOptions>.blocklyMenuDivDisabled,', ++ '.blocklyDropdownMenuOptions>.blocklyMenuDiv {', ++ ' fill: #fff;', ++ '}', ++ '.blocklyContextMenuOptions>.blocklyMenuDiv:hover>rect,', ++ '.blocklyDropdownMenuOptions>.blocklyMenuDiv:hover>rect {', ++ ' fill: #57e;', ++ '}', ++ '.blocklyMenuSelected>rect {', ++ ' fill: #57e;', ++ '}', ++ '.blocklyMenuText {', ++ ' cursor: default !important;', ++ ' font-family: sans-serif;', ++ ' font-size: 15px; /* All context menu sizes are based on pixels. */', ++ ' fill: #000;', ++ '}', ++ '.blocklyContextMenuOptions>.blocklyMenuDiv:hover>.blocklyMenuText,', ++ '.blocklyDropdownMenuOptions>.blocklyMenuDiv:hover>.blocklyMenuText {', ++ ' fill: #fff;', ++ '}', ++ '.blocklyMenuSelected>.blocklyMenuText {', ++ ' fill: #fff;', ++ '}', ++ '.blocklyMenuDivDisabled>.blocklyMenuText {', ++ ' fill: #ccc;', ++ '}', + '.blocklyFlyoutBackground {', + ' fill: #ddd;', + ' fill-opacity: .8;', diff --git a/core/field.js.orig b/core/field.js.orig new file mode 100644 index 000000000..131a62679 --- /dev/null +++ b/core/field.js.orig @@ -0,0 +1,495 @@ +/** + * @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 Field. Used for editable titles, variables, etc. + * This is an abstract class that defines the UI on the block. Actual + * instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Field'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.math.Size'); +goog.require('goog.style'); +goog.require('goog.userAgent'); + + +/** + * Abstract class for an editable field. + * @param {string} text The initial content of the field. + * @param {Function=} opt_validator An optional function that is called + * to validate any constraints on what the user entered. Takes the new + * text as an argument and returns either the accepted text, a replacement + * text, or null to abort the change. + * @constructor + */ +Blockly.Field = function(text, opt_validator) { + this.size_ = new goog.math.Size(0, 25); + this.setValue(text); + this.setValidator(opt_validator); +}; + +/** + * Temporary cache of text widths. + * @type {Object} + * @private + */ +Blockly.Field.cacheWidths_ = null; + +/** + * Number of current references to cache. + * @type {number} + * @private + */ +Blockly.Field.cacheReference_ = 0; + + +/** + * Name of field. Unique within each block. + * Static labels are usually unnamed. + * @type {string=} + */ +Blockly.Field.prototype.name = undefined; + +/** + * Maximum characters of text to display before adding an ellipsis. + * @type {number} + */ +Blockly.Field.prototype.maxDisplayLength = 50; + +/** + * Visible text to display. + * @type {string} + * @private + */ +Blockly.Field.prototype.text_ = ''; + +/** + * Block this field is attached to. Starts as null, then in set in init. + * @type {Blockly.Block} + * @private + */ +Blockly.Field.prototype.sourceBlock_ = null; + +/** + * Is the field visible, or hidden due to the block being collapsed? + * @type {boolean} + * @private + */ +Blockly.Field.prototype.visible_ = true; + +/** + * Validation function called when user edits an editable field. + * @type {Function} + * @private + */ +Blockly.Field.prototype.validator_ = null; + +/** + * Non-breaking space. + * @const + */ +Blockly.Field.NBSP = '\u00A0'; + +/** + * Editable fields are saved by the XML renderer, non-editable fields are not. + */ +Blockly.Field.prototype.EDITABLE = true; + +/** + * Attach this field to a block. + * @param {!Blockly.Block} block The block containing this field. + */ +Blockly.Field.prototype.setSourceBlock = function(block) { + goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.'); + this.sourceBlock_ = block; +}; + +/** + * Install this field on a block. + */ +Blockly.Field.prototype.init = function() { + if (this.fieldGroup_) { + // Field has already been initialized once. + return; + } + // Build the DOM. + this.fieldGroup_ = Blockly.createSvgElement('g', {}, null); + if (!this.visible_) { + this.fieldGroup_.style.display = 'none'; + } + this.borderRect_ = Blockly.createSvgElement('rect', + {'rx': 4, + 'ry': 4, + 'x': -Blockly.BlockSvg.SEP_SPACE_X / 2, + 'y': 0, + 'height': 16}, this.fieldGroup_, this.sourceBlock_.workspace); + /** @type {!Element} */ + this.textElement_ = Blockly.createSvgElement('text', + {'class': 'blocklyText', 'y': this.size_.height - 12.5}, + this.fieldGroup_); + + this.updateEditable(); + this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); + this.mouseUpWrapper_ = + Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_); + // Force a render. + this.updateTextNode_(); +}; + +/** + * Dispose of all DOM objects belonging to this editable field. + */ +Blockly.Field.prototype.dispose = function() { + if (this.mouseUpWrapper_) { + Blockly.unbindEvent_(this.mouseUpWrapper_); + this.mouseUpWrapper_ = null; + } + this.sourceBlock_ = null; + goog.dom.removeNode(this.fieldGroup_); + this.fieldGroup_ = null; + this.textElement_ = null; + this.borderRect_ = null; + this.validator_ = null; +}; + +/** + * Add or remove the UI indicating if this field is editable or not. + */ +Blockly.Field.prototype.updateEditable = function() { + var group = this.fieldGroup_; + if (!this.EDITABLE || !group) { + return; + } + if (this.sourceBlock_.isEditable()) { + Blockly.addClass_(group, 'blocklyEditableText'); + Blockly.removeClass_(group, 'blocklyNonEditableText'); + this.fieldGroup_.style.cursor = this.CURSOR; + } else { + Blockly.addClass_(group, 'blocklyNonEditableText'); + Blockly.removeClass_(group, 'blocklyEditableText'); + this.fieldGroup_.style.cursor = ''; + } +}; + +/** + * Gets whether this editable field is visible or not. + * @return {boolean} True if visible. + */ +Blockly.Field.prototype.isVisible = function() { + return this.visible_; +}; + +/** + * Sets whether this editable field is visible or not. + * @param {boolean} visible True if visible. + */ +Blockly.Field.prototype.setVisible = function(visible) { + if (this.visible_ == visible) { + return; + } + this.visible_ = visible; + var root = this.getSvgRoot(); + if (root) { + root.style.display = visible ? 'block' : 'none'; + this.render_(); + } +}; + +/** + * Sets a new validation function for editable fields. + * @param {Function} handler New validation function, or null. + */ +Blockly.Field.prototype.setValidator = function(handler) { + this.validator_ = handler; +}; + +/** + * Gets the validation function for editable fields. + * @return {Function} Validation function, or null. + */ +Blockly.Field.prototype.getValidator = function() { + return this.validator_; +}; + +/** + * Validates a change. Does nothing. Subclasses may override this. + * @param {string} text The user's text. + * @return {string} No change needed. + */ +Blockly.Field.prototype.classValidator = function(text) { + return text; +}; + +/** + * Calls the validation function for this field, as well as all the validation + * function for the field's class and its parents. + * @param {string} text Proposed text. + * @return {?string} Revised text, or null if invalid. + */ +Blockly.Field.prototype.callValidator = function(text) { + var classResult = this.classValidator(text); + if (classResult === null) { + // Class validator rejects value. Game over. + return null; + } else if (classResult !== undefined) { + text = classResult; + } + var userValidator = this.getValidator(); + if (userValidator) { + var userResult = userValidator.call(this, text); + if (userResult === null) { + // User validator rejects value. Game over. + return null; + } else if (userResult !== undefined) { + text = userResult; + } + } + return text; +}; + +/** + * Gets the group element for this editable field. + * Used for measuring the size and for positioning. + * @return {!Element} The group element. + */ +Blockly.Field.prototype.getSvgRoot = function() { + return /** @type {!Element} */ (this.fieldGroup_); +}; + +/** + * Draws the border with the correct width. + * Saves the computed width in a property. + * @private + */ +Blockly.Field.prototype.render_ = function() { + if (this.visible_ && this.textElement_) { + var key = this.textElement_.textContent + '\n' + + this.textElement_.className.baseVal; + if (Blockly.Field.cacheWidths_ && Blockly.Field.cacheWidths_[key]) { + var width = Blockly.Field.cacheWidths_[key]; + } else { + try { + var width = this.textElement_.getComputedTextLength(); + } catch (e) { + // MSIE 11 is known to throw "Unexpected call to method or property + // access." if Blockly is hidden. + var width = this.textElement_.textContent.length * 8; + } + if (Blockly.Field.cacheWidths_) { + Blockly.Field.cacheWidths_[key] = width; + } + } + if (this.borderRect_) { + this.borderRect_.setAttribute('width', + width + Blockly.BlockSvg.SEP_SPACE_X); + } + } else { + var width = 0; + } + this.size_.width = width; +}; + +/** + * Start caching field widths. Every call to this function MUST also call + * stopCache. Caches must not survive between execution threads. + */ +Blockly.Field.startCache = function() { + Blockly.Field.cacheReference_++; + if (!Blockly.Field.cacheWidths_) { + Blockly.Field.cacheWidths_ = {}; + } +}; + +/** + * Stop caching field widths. Unless caching was already on when the + * corresponding call to startCache was made. + */ +Blockly.Field.stopCache = function() { + Blockly.Field.cacheReference_--; + if (!Blockly.Field.cacheReference_) { + Blockly.Field.cacheWidths_ = null; + } +}; + +/** + * Returns the height and width of the field. + * @return {!goog.math.Size} Height and width. + */ +Blockly.Field.prototype.getSize = function() { + if (!this.size_.width) { + this.render_(); + } + return this.size_; +}; + +/** + * Returns the height and width of the field, + * accounting for the workspace scaling. + * @return {!goog.math.Size} Height and width. + * @private + */ +Blockly.Field.prototype.getScaledBBox_ = function() { + var bBox = this.borderRect_.getBBox(); + // Create new object, as getBBox can return an uneditable SVGRect in IE. + return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale, + bBox.height * this.sourceBlock_.workspace.scale); +}; + +/** + * Get the text from this field. + * @return {string} Current text. + */ +Blockly.Field.prototype.getText = function() { + return this.text_; +}; + +/** + * Set the text in this field. Trigger a rerender of the source block. + * @param {*} text New text. + */ +Blockly.Field.prototype.setText = function(text) { + if (text === null) { + // No change if null. + return; + } + text = String(text); + if (text === this.text_) { + // No change. + return; + } + this.text_ = text; + this.updateTextNode_(); + + if (this.sourceBlock_ && this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + this.sourceBlock_.bumpNeighbours_(); + } +}; + +/** + * Update the text node of this field to display the current text. + * @private + */ +Blockly.Field.prototype.updateTextNode_ = function() { + if (!this.textElement_) { + // Not rendered yet. + return; + } + var text = this.text_; + if (text.length > this.maxDisplayLength) { + // Truncate displayed string and add an ellipsis ('...'). + text = text.substring(0, this.maxDisplayLength - 2) + '\u2026'; + } + // Empty the text element. + goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_)); + // Replace whitespace with non-breaking spaces so the text doesn't collapse. + text = text.replace(/\s/g, Blockly.Field.NBSP); + if (this.sourceBlock_.RTL && text) { + // The SVG is LTR, force text to be RTL. + text += '\u200F'; + } + if (!text) { + // Prevent the field from disappearing if empty. + text = Blockly.Field.NBSP; + } + var textNode = document.createTextNode(text); + this.textElement_.appendChild(textNode); + + // Cached width is obsolete. Clear it. + this.size_.width = 0; +}; + +/** + * By default there is no difference between the human-readable text and + * the language-neutral values. Subclasses (such as dropdown) may define this. + * @return {string} Current text. + */ +Blockly.Field.prototype.getValue = function() { + return this.getText(); +}; + +/** + * By default there is no difference between the human-readable text and + * the language-neutral values. Subclasses (such as dropdown) may define this. + * @param {string} newText New text. + */ +Blockly.Field.prototype.setValue = function(newText) { + if (newText === null) { + // No change if null. + return; + } + var oldText = this.getValue(); + if (oldText == newText) { + return; + } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, oldText, newText)); + } + this.setText(newText); +}; + +/** + * Handle a mouse up event on an editable field. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Field.prototype.onMouseUp_ = function(e) { + if ((goog.userAgent.IPHONE || goog.userAgent.IPAD) && + !goog.userAgent.isVersionOrHigher('537.51.2') && + e.layerX !== 0 && e.layerY !== 0) { + // Old iOS spawns a bogus event on the next touch after a 'prompt()' edit. + // Unlike the real events, these have a layerX and layerY set. + return; + } else if (Blockly.isRightButton(e)) { + // Right-click. + return; + } else if (this.sourceBlock_.workspace.isDragging()) { + // Drag operation is concluding. Don't open the editor. + return; + } else if (this.sourceBlock_.isEditable()) { + // Non-abstract sub-classes must define a showEditor_ method. + this.showEditor_(); + } +}; + +/** + * Change the tooltip text for this field. + * @param {string|!Element} newTip Text for tooltip or a parent element to + * link to for its tooltip. + */ +Blockly.Field.prototype.setTooltip = function(newTip) { + // Non-abstract sub-classes may wish to implement this. See FieldLabel. +}; + +/** + * Return the absolute coordinates of the top-left corner of this field. + * The origin (0,0) is the top-left corner of the page body. + * @return {!goog.math.Coordinate} Object with .x and .y properties. + * @private + */ +Blockly.Field.prototype.getAbsoluteXY_ = function() { + return goog.style.getPageOffset(this.borderRect_); +}; diff --git a/core/field.js.rej b/core/field.js.rej new file mode 100644 index 000000000..9371818a1 --- /dev/null +++ b/core/field.js.rej @@ -0,0 +1,37 @@ +*************** +*** 87,93 **** + } + this.sourceBlock_ = block; + this.updateEditable(); + block.getSvgRoot().appendChild(this.fieldGroup_); + this.mouseUpWrapper_ = + Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_); + // Bump to set the colours for dropdown arrows. +--- 87,113 ---- + } + this.sourceBlock_ = block; + this.updateEditable(); ++ ++ // [lyn, 10/25/13] Handle the special case where adding a title to a collapsed block. ++ // This can happen if a mutator is forced open on a procedure declaration, which happens ++ // in the current AI implementation in Blockly.FieldProcedure.onChange and ++ // in Blockly.Language.procedures_callnoreturn.setProcedureParameters. ++ // Then when the mutator is closed, the compose method of a procedure declaration ++ // is called, and this can invoke the declarations updateParams_ method, which ++ // makes new title elements for the declaration (even a collapsed one). ++ // ++ // Note: even with this "fix", can still see a few white pixels added to top of ++ // collapsed procedure decl when svg is added. I can't explain why. ++ // So it's even better to avoid adding titles to collapsed blocks in the first place! ++ // E.g., I modified proc decl compose method to avoid calling updateParams_ ++ // if arg names haven't changed from before. ++ // if (block.collapsed) { ++ // // this.fieldGroup_.style.display = 'none'; ++ // this.getRootElement().style.display = 'none'; ++ // } ++ + block.getSvgRoot().appendChild(this.fieldGroup_); ++ + this.mouseUpWrapper_ = + Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_); + // Bump to set the colours for dropdown arrows. diff --git a/core/field_dropdown.js b/core/field_dropdown.js index ec3dd4f5a..dbf78cf85 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -318,3 +318,19 @@ Blockly.FieldDropdown.prototype.dispose = function() { Blockly.WidgetDiv.hideIfOwner(this); Blockly.FieldDropdown.superClass_.dispose.call(this); }; + +/** + * Lookup function to traverse OPERATORS structures (Array of Arrays) to return a human readable + * representation of a particular Operator + * @param {Array} a nested array with Operators and their 'readable' form + * @param {String} the value to look up + * @return {String} the value of the Operator to display to users + */ +Blockly.FieldDropdown.lookupOperator = function(operatorsArray, value){ + var arrLength = operatorsArray.length; + for (var i = 0; i < arrLength; i++){ + if (operatorsArray[i][1] === value) + return operatorsArray[i][0]; + } + throw new Error('value: ' + value + ' not found in OPERATOR: ' + operatorsArray); +}; diff --git a/core/field_dropdown.js.orig b/core/field_dropdown.js.orig new file mode 100644 index 000000000..ec3dd4f5a --- /dev/null +++ b/core/field_dropdown.js.orig @@ -0,0 +1,320 @@ +/** + * @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 Dropdown input field. Used for editable titles and variables. + * In the interests of a consistent UI, the toolbox shares some functions and + * properties with the context menu. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.FieldDropdown'); + +goog.require('Blockly.Field'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.style'); +goog.require('goog.ui.Menu'); +goog.require('goog.ui.MenuItem'); +goog.require('goog.userAgent'); + + +/** + * Class for an editable dropdown field. + * @param {(!Array.>|!Function)} menuGenerator An array of + * options for a dropdown list, or a function which generates these options. + * @param {Function=} opt_validator A function that is executed when a new + * option is selected, with the newly selected value as its sole argument. + * If it returns a value, that value (which must be one of the options) will + * become selected in place of the newly selected option, unless the return + * value is null, in which case the change is aborted. + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldDropdown = function(menuGenerator, opt_validator) { + this.menuGenerator_ = menuGenerator; + this.trimOptions_(); + var firstTuple = this.getOptions_()[0]; + + // Call parent's constructor. + Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1], + opt_validator); +}; +goog.inherits(Blockly.FieldDropdown, Blockly.Field); + +/** + * Horizontal distance that a checkmark ovehangs the dropdown. + */ +Blockly.FieldDropdown.CHECKMARK_OVERHANG = 25; + +/** + * Android can't (in 2014) display "▾", so use "▼" instead. + */ +Blockly.FieldDropdown.ARROW_CHAR = goog.userAgent.ANDROID ? '\u25BC' : '\u25BE'; + +/** + * Mouse cursor style when over the hotspot that initiates the editor. + */ +Blockly.FieldDropdown.prototype.CURSOR = 'default'; + +/** + * Install this dropdown on a block. + */ +Blockly.FieldDropdown.prototype.init = function() { + if (this.fieldGroup_) { + // Dropdown has already been initialized once. + return; + } + // Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL) + this.arrow_ = Blockly.createSvgElement('tspan', {}, null); + this.arrow_.appendChild(document.createTextNode( + this.sourceBlock_.RTL ? Blockly.FieldDropdown.ARROW_CHAR + ' ' : + ' ' + Blockly.FieldDropdown.ARROW_CHAR)); + + Blockly.FieldDropdown.superClass_.init.call(this); + // Force a reset of the text to add the arrow. + var text = this.text_; + this.text_ = null; + this.setText(text); +}; + +/** + * Create a dropdown menu under the text. + * @private + */ +Blockly.FieldDropdown.prototype.showEditor_ = function() { + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); + var thisField = this; + + function callback(e) { + var menuItem = e.target; + if (menuItem) { + var value = menuItem.getValue(); + if (thisField.sourceBlock_) { + // Call any validation function, and allow it to override. + value = thisField.callValidator(value); + } + if (value !== null) { + thisField.setValue(value); + } + } + Blockly.WidgetDiv.hideIfOwner(thisField); + } + + var menu = new goog.ui.Menu(); + menu.setRightToLeft(this.sourceBlock_.RTL); + var options = this.getOptions_(); + for (var i = 0; i < options.length; i++) { + var text = options[i][0]; // Human-readable text. + var value = options[i][1]; // Language-neutral value. + var menuItem = new goog.ui.MenuItem(text); + menuItem.setRightToLeft(this.sourceBlock_.RTL); + menuItem.setValue(value); + menuItem.setCheckable(true); + menu.addChild(menuItem, true); + menuItem.setChecked(value == this.value_); + } + // Listen for mouse/keyboard events. + goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback); + // Listen for touch events (why doesn't Closure handle this already?). + function callbackTouchStart(e) { + var control = this.getOwnerControl(/** @type {Node} */ (e.target)); + // Highlight the menu item. + control.handleMouseDown(e); + } + function callbackTouchEnd(e) { + var control = this.getOwnerControl(/** @type {Node} */ (e.target)); + // Activate the menu item. + control.performActionInternal(e); + } + menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHSTART, + callbackTouchStart); + menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHEND, + callbackTouchEnd); + + // Record windowSize and scrollOffset before adding menu. + var windowSize = goog.dom.getViewportSize(); + var scrollOffset = goog.style.getViewportPageOffset(document); + var xy = this.getAbsoluteXY_(); + var borderBBox = this.getScaledBBox_(); + var div = Blockly.WidgetDiv.DIV; + menu.render(div); + var menuDom = menu.getElement(); + Blockly.addClass_(menuDom, 'blocklyDropdownMenu'); + // Record menuSize after adding menu. + var menuSize = goog.style.getSize(menuDom); + // Recalculate height for the total content, not only box height. + menuSize.height = menuDom.scrollHeight; + + // Position the menu. + // Flip menu vertically if off the bottom. + if (xy.y + menuSize.height + borderBBox.height >= + windowSize.height + scrollOffset.y) { + xy.y -= menuSize.height + 2; + } else { + xy.y += borderBBox.height; + } + if (this.sourceBlock_.RTL) { + xy.x += borderBBox.width; + xy.x += Blockly.FieldDropdown.CHECKMARK_OVERHANG; + // Don't go offscreen left. + if (xy.x < scrollOffset.x + menuSize.width) { + xy.x = scrollOffset.x + menuSize.width; + } + } else { + xy.x -= Blockly.FieldDropdown.CHECKMARK_OVERHANG; + // Don't go offscreen right. + if (xy.x > windowSize.width + scrollOffset.x - menuSize.width) { + xy.x = windowSize.width + scrollOffset.x - menuSize.width; + } + } + Blockly.WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, + this.sourceBlock_.RTL); + menu.setAllowAutoFocus(true); + menuDom.focus(); +}; + +/** + * Factor out common words in statically defined options. + * Create prefix and/or suffix labels. + * @private + */ +Blockly.FieldDropdown.prototype.trimOptions_ = function() { + this.prefixField = null; + this.suffixField = null; + var options = this.menuGenerator_; + if (!goog.isArray(options) || options.length < 2) { + return; + } + var strings = options.map(function(t) {return t[0];}); + var shortest = Blockly.shortestStringLength(strings); + var prefixLength = Blockly.commonWordPrefix(strings, shortest); + var suffixLength = Blockly.commonWordSuffix(strings, shortest); + if (!prefixLength && !suffixLength) { + return; + } + if (shortest <= prefixLength + suffixLength) { + // One or more strings will entirely vanish if we proceed. Abort. + return; + } + if (prefixLength) { + this.prefixField = strings[0].substring(0, prefixLength - 1); + } + if (suffixLength) { + this.suffixField = strings[0].substr(1 - suffixLength); + } + // Remove the prefix and suffix from the options. + var newOptions = []; + for (var i = 0; i < options.length; i++) { + var text = options[i][0]; + var value = options[i][1]; + text = text.substring(prefixLength, text.length - suffixLength); + newOptions[i] = [text, value]; + } + this.menuGenerator_ = newOptions; +}; + +/** + * Return a list of the options for this dropdown. + * @return {!Array.>} Array of option tuples: + * (human-readable text, language-neutral name). + * @private + */ +Blockly.FieldDropdown.prototype.getOptions_ = function() { + if (goog.isFunction(this.menuGenerator_)) { + return this.menuGenerator_.call(this); + } + return /** @type {!Array.>} */ (this.menuGenerator_); +}; + +/** + * Get the language-neutral value from this dropdown menu. + * @return {string} Current text. + */ +Blockly.FieldDropdown.prototype.getValue = function() { + return this.value_; +}; + +/** + * Set the language-neutral value for this dropdown menu. + * @param {string} newValue New value to set. + */ +Blockly.FieldDropdown.prototype.setValue = function(newValue) { + if (newValue === null || newValue === this.value_) { + return; // No change if null. + } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, newValue)); + } + this.value_ = newValue; + // Look up and display the human-readable text. + var options = this.getOptions_(); + for (var i = 0; i < options.length; i++) { + // Options are tuples of human-readable text and language-neutral values. + if (options[i][1] == newValue) { + this.setText(options[i][0]); + return; + } + } + // Value not found. Add it, maybe it will become valid once set + // (like variable names). + this.setText(newValue); +}; + +/** + * Set the text in this field. Trigger a rerender of the source block. + * @param {?string} text New text. + */ +Blockly.FieldDropdown.prototype.setText = function(text) { + if (this.sourceBlock_ && this.arrow_) { + // Update arrow's colour. + this.arrow_.style.fill = this.sourceBlock_.getColour(); + } + if (text === null || text === this.text_) { + // No change if null. + return; + } + this.text_ = text; + this.updateTextNode_(); + + if (this.textElement_) { + // Insert dropdown arrow. + if (this.sourceBlock_.RTL) { + this.textElement_.insertBefore(this.arrow_, this.textElement_.firstChild); + } else { + this.textElement_.appendChild(this.arrow_); + } + } + + if (this.sourceBlock_ && this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + this.sourceBlock_.bumpNeighbours_(); + } +}; + +/** + * Close the dropdown menu if this input is being deleted. + */ +Blockly.FieldDropdown.prototype.dispose = function() { + Blockly.WidgetDiv.hideIfOwner(this); + Blockly.FieldDropdown.superClass_.dispose.call(this); +}; diff --git a/core/flyout.js b/core/flyout.js index 03ea15059..c11726562 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -36,6 +36,20 @@ goog.require('goog.events'); goog.require('goog.math.Rect'); goog.require('goog.userAgent'); +/** + * Factor by which margin is multiplied to vertically separate blocks in flyout + * [lyn, 10/06/13] introduced so can change in flydown subclass.) + * @type {number} + * @const + */ +Blockly.Flyout.prototype.VERTICAL_SEPARATION_FACTOR = 2; + +/* + * Wrapper function called when a resize occurs. + * @type {Array.} + * @private + */ +Blockly.Flyout.prototype.onResizeWrapper_ = null; /** * Class for a flyout. diff --git a/core/flyout.js.orig b/core/flyout.js.orig new file mode 100644 index 000000000..03ea15059 --- /dev/null +++ b/core/flyout.js.orig @@ -0,0 +1,1364 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 Flyout tray containing blocks which may be created. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Flyout'); + +goog.require('Blockly.Block'); +goog.require('Blockly.Comment'); +goog.require('Blockly.Events'); +goog.require('Blockly.FlyoutButton'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.math.Rect'); +goog.require('goog.userAgent'); + + +/** + * Class for a flyout. + * @param {!Object} workspaceOptions Dictionary of options for the workspace. + * @constructor + */ +Blockly.Flyout = function(workspaceOptions) { + workspaceOptions.getMetrics = this.getMetrics_.bind(this); + workspaceOptions.setMetrics = this.setMetrics_.bind(this); + /** + * @type {!Blockly.Workspace} + * @private + */ + this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); + this.workspace_.isFlyout = true; + + /** + * Is RTL vs LTR. + * @type {boolean} + */ + this.RTL = !!workspaceOptions.RTL; + + /** + * Flyout should be laid out horizontally vs vertically. + * @type {boolean} + * @private + */ + this.horizontalLayout_ = workspaceOptions.horizontalLayout; + + /** + * Position of the toolbox and flyout relative to the workspace. + * @type {number} + * @private + */ + this.toolboxPosition_ = workspaceOptions.toolboxPosition; + + /** + * Opaque data that can be passed to Blockly.unbindEvent_. + * @type {!Array.} + * @private + */ + this.eventWrappers_ = []; + + /** + * List of background buttons that lurk behind each block to catch clicks + * landing in the blocks' lakes and bays. + * @type {!Array.} + * @private + */ + this.backgroundButtons_ = []; + + /** + * List of visible buttons. + * @type {!Array.} + * @private + */ + this.buttons_ = []; + + /** + * List of event listeners. + * @type {!Array.} + * @private + */ + this.listeners_ = []; + + /** + * List of blocks that should always be disabled. + * @type {!Array.} + * @private + */ + this.permanentlyDisabled_ = []; + + /** + * y coordinate of mousedown - used to calculate scroll distances. + * @private {number} + */ + this.startDragMouseY_ = 0; + + /** + * x coordinate of mousedown - used to calculate scroll distances. + * @private {number} + */ + this.startDragMouseX_ = 0; +}; + +/** + * When a flyout drag is in progress, this is a reference to the flyout being + * dragged. This is used by Flyout.terminateDrag_ to reset dragMode_. + * @private {Blockly.Flyout} + */ +Blockly.Flyout.startFlyout_ = null; + +/** + * Event that started a drag. Used to determine the drag distance/direction and + * also passed to BlockSvg.onMouseDown_() after creating a new block. + * @private {Event} + */ +Blockly.Flyout.startDownEvent_ = null; + +/** + * Flyout block where the drag/click was initiated. Used to fire click events or + * create a new block. + * @private {Event} + */ +Blockly.Flyout.startBlock_ = null; + +/** + * Wrapper function called when a mouseup occurs during a background or block + * drag operation. + * @private {Array.} + */ +Blockly.Flyout.onMouseUpWrapper_ = null; + +/** + * Wrapper function called when a mousemove occurs during a background drag. + * @private {Array.} + */ +Blockly.Flyout.onMouseMoveWrapper_ = null; + +/** + * Wrapper function called when a mousemove occurs during a block drag. + * @private {Array.} + */ +Blockly.Flyout.onMouseMoveBlockWrapper_ = null; + +/** + * Does the flyout automatically close when a block is created? + * @type {boolean} + */ +Blockly.Flyout.prototype.autoClose = true; + +/** + * Corner radius of the flyout background. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.CORNER_RADIUS = 8; + +/** + * Number of pixels the mouse must move before a drag/scroll starts. Because the + * drag-intention is determined when this is reached, it is larger than + * Blockly.DRAG_RADIUS so that the drag-direction is clearer. + */ +Blockly.Flyout.prototype.DRAG_RADIUS = 10; + +/** + * Margin around the edges of the blocks in the flyout. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS; + +/** + * Gap between items in horizontal flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ +Blockly.Flyout.prototype.GAP_X = Blockly.Flyout.prototype.MARGIN * 3; + +/** + * Gap between items in vertical flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ +Blockly.Flyout.prototype.GAP_Y = Blockly.Flyout.prototype.MARGIN * 3; + +/** + * Top/bottom padding between scrollbar and edge of flyout background. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.SCROLLBAR_PADDING = 2; + +/** + * Width of flyout. + * @type {number} + * @private + */ +Blockly.Flyout.prototype.width_ = 0; + +/** + * Height of flyout. + * @type {number} + * @private + */ +Blockly.Flyout.prototype.height_ = 0; + +/** + * Is the flyout dragging (scrolling)? + * DRAG_NONE - no drag is ongoing or state is undetermined. + * DRAG_STICKY - still within the sticky drag radius. + * DRAG_FREE - in scroll mode (never create a new block). + * @private + */ +Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE; + +/** + * Range of a drag angle from a flyout considered "dragging toward workspace". + * Drags that are within the bounds of this many degrees from the orthogonal + * line to the flyout edge are considered to be "drags toward the workspace". + * Example: + * Flyout Edge Workspace + * [block] / <-within this angle, drags "toward workspace" | + * [block] ---- orthogonal to flyout boundary ---- | + * [block] \ | + * The angle is given in degrees from the orthogonal. + * + * This is used to know when to create a new block and when to scroll the + * flyout. Setting it to 360 means that all drags create a new block. + * @type {number} + * @private +*/ +Blockly.Flyout.prototype.dragAngleRange_ = 70; + +/** + * Creates the flyout's DOM. Only needs to be called once. + * @return {!Element} The flyout's SVG group. + */ +Blockly.Flyout.prototype.createDom = function() { + /* + + + + + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyFlyout'}, null); + this.svgBackground_ = Blockly.createSvgElement('path', + {'class': 'blocklyFlyoutBackground'}, this.svgGroup_); + this.svgGroup_.appendChild(this.workspace_.createDom()); + return this.svgGroup_; +}; + +/** + * Initializes the flyout. + * @param {!Blockly.Workspace} targetWorkspace The workspace in which to create + * new blocks. + */ +Blockly.Flyout.prototype.init = function(targetWorkspace) { + this.targetWorkspace_ = targetWorkspace; + this.workspace_.targetWorkspace = targetWorkspace; + // Add scrollbar. + this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, + this.horizontalLayout_, false); + + this.hide(); + + Array.prototype.push.apply(this.eventWrappers_, + Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.wheel_)); + if (!this.autoClose) { + this.filterWrapper_ = this.filterForCapacity_.bind(this); + this.targetWorkspace_.addChangeListener(this.filterWrapper_); + } + // Dragging the flyout up and down. + Array.prototype.push.apply(this.eventWrappers_, + Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_)); +}; + +/** + * Dispose of this flyout. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Flyout.prototype.dispose = function() { + this.hide(); + Blockly.unbindEvent_(this.eventWrappers_); + if (this.filterWrapper_) { + this.targetWorkspace_.removeChangeListener(this.filterWrapper_); + this.filterWrapper_ = null; + } + if (this.scrollbar_) { + this.scrollbar_.dispose(); + this.scrollbar_ = null; + } + if (this.workspace_) { + this.workspace_.targetWorkspace = null; + this.workspace_.dispose(); + this.workspace_ = null; + } + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.svgBackground_ = null; + this.targetWorkspace_ = null; +}; + +/** + * Get the width of the flyout. + * @return {number} The width of the flyout. + */ +Blockly.Flyout.prototype.getWidth = function() { + return this.width_; +}; + +/** + * Get the height of the flyout. + * @return {number} The width of the flyout. + */ +Blockly.Flyout.prototype.getHeight = function() { + return this.height_; +}; + +/** + * Return an object with all the metrics required to size scrollbars for the + * flyout. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the contents, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .absoluteTop: Top-edge of view. + * .viewLeft: Offset of the left edge of visible rectangle from parent, + * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .absoluteLeft: Left-edge of view. + * @return {Object} Contains size and position metrics of the flyout. + * @private + */ +Blockly.Flyout.prototype.getMetrics_ = function() { + if (!this.isVisible()) { + // Flyout is hidden. + return null; + } + + try { + var optionBox = this.workspace_.getCanvas().getBBox(); + } catch (e) { + // Firefox has trouble with hidden elements (Bug 528969). + var optionBox = {height: 0, y: 0, width: 0, x: 0}; + } + + var absoluteTop = this.SCROLLBAR_PADDING; + var absoluteLeft = this.SCROLLBAR_PADDING; + if (this.horizontalLayout_) { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + absoluteTop = 0; + } + var viewHeight = this.height_; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + viewHeight += this.MARGIN - this.SCROLLBAR_PADDING; + } + var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING; + } else { + absoluteLeft = 0; + var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING; + var viewWidth = this.width_; + if (!this.RTL) { + viewWidth -= this.SCROLLBAR_PADDING; + } + } + + var metrics = { + viewHeight: viewHeight, + viewWidth: viewWidth, + contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, + contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, + viewTop: -this.workspace_.scrollY, + viewLeft: -this.workspace_.scrollX, + contentTop: optionBox.y, + contentLeft: optionBox.x, + absoluteTop: absoluteTop, + absoluteLeft: absoluteLeft + }; + return metrics; +}; + +/** + * Sets the translation of the flyout to match the scrollbars. + * @param {!Object} xyRatio Contains a y property which is a float + * between 0 and 1 specifying the degree of scrolling and a + * similar x property. + * @private + */ +Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) { + var metrics = this.getMetrics_(); + // This is a fix to an apparent race condition. + if (!metrics) { + return; + } + if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) { + this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y; + } else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) { + this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x; + } + + this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, + this.workspace_.scrollY + metrics.absoluteTop); +}; + +/** + * Move the flyout to the edge of the workspace. + */ +Blockly.Flyout.prototype.position = function() { + if (!this.isVisible()) { + return; + } + var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); + if (!targetWorkspaceMetrics) { + // Hidden components will return null. + return; + } + var edgeWidth = this.horizontalLayout_ ? + targetWorkspaceMetrics.viewWidth : this.width_; + edgeWidth -= this.CORNER_RADIUS; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + edgeWidth *= -1; + } + + this.setBackgroundPath_(edgeWidth, + this.horizontalLayout_ ? this.height_ : + targetWorkspaceMetrics.viewHeight); + + var x = targetWorkspaceMetrics.absoluteLeft; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + x += targetWorkspaceMetrics.viewWidth; + x -= this.width_; + } + + var y = targetWorkspaceMetrics.absoluteTop; + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + y += targetWorkspaceMetrics.viewHeight; + y -= this.height_; + } + + this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')'); + + // Record the height for Blockly.Flyout.getMetrics_, or width if the layout is + // horizontal. + if (this.horizontalLayout_) { + this.width_ = targetWorkspaceMetrics.viewWidth; + } else { + this.height_ = targetWorkspaceMetrics.viewHeight; + } + + // Update the scrollbar (if one exists). + if (this.scrollbar_) { + this.scrollbar_.resize(); + } +}; + +/** + * Create and set the path for the visible boundaries of the flyout. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) { + if (this.horizontalLayout_) { + this.setBackgroundPathHorizontal_(width, height); + } else { + this.setBackgroundPathVertical_(width, height); + } +}; + +/** + * Create and set the path for the visible boundaries of the flyout in vertical + * mode. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) { + var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT; + // Decide whether to start on the left or right. + var path = ['M ' + (atRight ? this.width_ : 0) + ',0']; + // Top. + path.push('h', width); + // Rounded corner. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, + atRight ? 0 : 1, + atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, + this.CORNER_RADIUS); + // Side closest to workspace. + path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2)); + // Rounded corner. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, + atRight ? 0 : 1, + atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, + this.CORNER_RADIUS); + // Bottom. + path.push('h', -width); + path.push('z'); + this.svgBackground_.setAttribute('d', path.join(' ')); +}; + +/** + * Create and set the path for the visible boundaries of the flyout in + * horizontal mode. + * @param {number} width The width of the flyout, not including the + * rounded corners. + * @param {number} height The height of the flyout, not including + * rounded corners. + * @private + */ +Blockly.Flyout.prototype.setBackgroundPathHorizontal_ = function(width, + height) { + var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP; + // Start at top left. + var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)]; + + if (atTop) { + // Top. + path.push('h', width + this.CORNER_RADIUS); + // Right. + path.push('v', height); + // Bottom. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + -this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('h', -1 * (width - this.CORNER_RADIUS)); + // Left. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + -this.CORNER_RADIUS, -this.CORNER_RADIUS); + path.push('z'); + } else { + // Top. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + this.CORNER_RADIUS, -this.CORNER_RADIUS); + path.push('h', width - this.CORNER_RADIUS); + // Right. + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, + this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('v', height - this.CORNER_RADIUS); + // Bottom. + path.push('h', -width - this.CORNER_RADIUS); + // Left. + path.push('z'); + } + this.svgBackground_.setAttribute('d', path.join(' ')); +}; + +/** + * Scroll the flyout to the top. + */ +Blockly.Flyout.prototype.scrollToStart = function() { + this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? Infinity : 0); +}; + +/** + * Scroll the flyout. + * @param {!Event} e Mouse wheel scroll event. + * @private + */ +Blockly.Flyout.prototype.wheel_ = function(e) { + var delta = this.horizontalLayout_ ? e.deltaX : e.deltaY; + + if (delta) { + if (goog.userAgent.GECKO) { + // Firefox's deltas are a tenth that of Chrome/Safari. + delta *= 10; + } + var metrics = this.getMetrics_(); + var pos = this.horizontalLayout_ ? metrics.viewLeft + delta : + metrics.viewTop + delta; + var limit = this.horizontalLayout_ ? + metrics.contentWidth - metrics.viewWidth : + metrics.contentHeight - metrics.viewHeight; + pos = Math.min(pos, limit); + pos = Math.max(pos, 0); + this.scrollbar_.set(pos); + } + + // Don't scroll the page. + e.preventDefault(); + // Don't propagate mousewheel event (zooming). + e.stopPropagation(); +}; + +/** + * Is the flyout visible? + * @return {boolean} True if visible. + */ +Blockly.Flyout.prototype.isVisible = function() { + return this.svgGroup_ && this.svgGroup_.style.display == 'block'; +}; + +/** + * Hide and empty the flyout. + */ +Blockly.Flyout.prototype.hide = function() { + if (!this.isVisible()) { + return; + } + this.svgGroup_.style.display = 'none'; + // Delete all the event listeners. + for (var x = 0, listen; listen = this.listeners_[x]; x++) { + Blockly.unbindEvent_(listen); + } + this.listeners_.length = 0; + if (this.reflowWrapper_) { + this.workspace_.removeChangeListener(this.reflowWrapper_); + this.reflowWrapper_ = null; + } + // Do NOT delete the blocks here. Wait until Flyout.show. + // https://neil.fraser.name/news/2014/08/09/ +}; + +/** + * Show and populate the flyout. + * @param {!Array|string} xmlList List of blocks to show. + * Variables and procedures have a custom set of blocks. + */ +Blockly.Flyout.prototype.show = function(xmlList) { + this.hide(); + this.clearOldBlocks_(); + + if (xmlList == Blockly.Variables.NAME_TYPE) { + // Special category for variables. + xmlList = + Blockly.Variables.flyoutCategory(this.workspace_.targetWorkspace); + } else if (xmlList == Blockly.Procedures.NAME_TYPE) { + // Special category for procedures. + xmlList = + Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace); + } + + this.svgGroup_.style.display = 'block'; + // Create the blocks to be shown in this flyout. + var contents = []; + var gaps = []; + this.permanentlyDisabled_.length = 0; + for (var i = 0, xml; xml = xmlList[i]; i++) { + if (xml.tagName) { + var tagName = xml.tagName.toUpperCase(); + var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y; + if (tagName == 'BLOCK') { + var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); + if (curBlock.disabled) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabled_.push(curBlock); + } + contents.push({type: 'block', block: curBlock}); + var gap = parseInt(xml.getAttribute('gap'), 10); + gaps.push(isNaN(gap) ? default_gap : gap); + } else if (xml.tagName.toUpperCase() == 'SEP') { + // Change the gap between two blocks. + // + // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous block. + // Note that a deprecated method is to add a gap to a block. + // + var newGap = parseInt(xml.getAttribute('gap'), 10); + // Ignore gaps before the first block. + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; + } else { + gaps.push(default_gap); + } + } else if (tagName == 'BUTTON') { + var label = xml.getAttribute('text'); + var curButton = new Blockly.FlyoutButton(this.workspace_, + this.targetWorkspace_, label); + contents.push({type: 'button', button: curButton}); + gaps.push(default_gap); + } + } + } + + this.layout_(contents, gaps); + + // IE 11 is an incompetent browser that fails to fire mouseout events. + // When the mouse is over the background, deselect all blocks. + var deselectAll = function() { + var topBlocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = topBlocks[i]; i++) { + block.removeSelect(); + } + }; + + this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover', + this, deselectAll)); + + if (this.horizontalLayout_) { + this.height_ = 0; + } else { + this.width_ = 0; + } + this.reflow(); + + this.filterForCapacity_(); + + // Correctly position the flyout's scrollbar when it opens. + this.position(); + + this.reflowWrapper_ = this.reflow.bind(this); + this.workspace_.addChangeListener(this.reflowWrapper_); +}; + +/** + * Lay out the blocks in the flyout. + * @param {!Array.} contents The blocks and buttons to lay out. + * @param {!Array.} gaps The visible gaps between blocks. + * @private + */ +Blockly.Flyout.prototype.layout_ = function(contents, gaps) { + this.workspace_.scale = this.targetWorkspace_.scale; + var margin = this.MARGIN; + var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH; + var cursorY = margin; + if (this.horizontalLayout_ && this.RTL) { + contents = contents.reverse(); + } + + for (var i = 0, item; item = contents[i]; i++) { + if (item.type == 'block') { + var block = item.block; + var allBlocks = block.getDescendants(); + for (var j = 0, child; child = allBlocks[j]; j++) { + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such a + // block. + child.isInFlyout = true; + } + block.render(); + var root = block.getSvgRoot(); + var blockHW = block.getHeightWidth(); + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + if (this.horizontalLayout_) { + cursorX += tab; + } + block.moveBy((this.horizontalLayout_ && this.RTL) ? + cursorX + blockHW.width - tab : cursorX, + cursorY); + if (this.horizontalLayout_) { + cursorX += (blockHW.width + gaps[i] - tab); + } else { + cursorY += blockHW.height + gaps[i]; + } + + // Create an invisible rectangle under the block to act as a button. Just + // using the block as a button is poor, since blocks have holes in them. + var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null); + rect.tooltip = block; + Blockly.Tooltip.bindMouseEvents(rect); + // Add the rectangles under the blocks, so that the blocks' tooltips work. + this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); + block.flyoutRect_ = rect; + this.backgroundButtons_[i] = rect; + + this.addBlockListeners_(root, block, rect); + } else if (item.type == 'button') { + var button = item.button; + var buttonSvg = button.createDom(); + button.moveTo(cursorX, cursorY); + button.show(); + Blockly.bindEvent_(buttonSvg, 'mouseup', button, button.onMouseUp); + + this.buttons_.push(button); + if (this.horizontalLayout_) { + cursorX += (button.width + gaps[i]); + } else { + cursorY += button.height + gaps[i]; + } + } + } +}; + +/** + * Delete blocks and background buttons from a previous showing of the flyout. + * @private + */ +Blockly.Flyout.prototype.clearOldBlocks_ = function() { + // Delete any blocks from a previous showing. + var oldBlocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = oldBlocks[i]; i++) { + if (block.workspace == this.workspace_) { + block.dispose(false, false); + } + } + // Delete any background buttons from a previous showing. + for (var j = 0, rect; rect = this.backgroundButtons_[j]; j++) { + goog.dom.removeNode(rect); + } + this.backgroundButtons_.length = 0; + + for (var i = 0, button; button = this.buttons_[i]; i++) { + button.dispose(); + } + this.buttons_.length = 0; +}; + +/** + * Add listeners to a block that has been added to the flyout. + * @param {!Element} root The root node of the SVG group the block is in. + * @param {!Blockly.Block} block The block to add listeners for. + * @param {!Element} rect The invisible rectangle under the block that acts as + * a button for that block. + * @private + */ +Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) { + this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null, + this.blockMouseDown_(block))); + this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null, + this.blockMouseDown_(block))); + this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block, + block.addSelect)); + this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block, + block.removeSelect)); + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block, + block.addSelect)); + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block, + block.removeSelect)); +}; + +/** + * Handle a mouse-down on an SVG block in a non-closing flyout. + * @param {!Blockly.Block} block The flyout block to copy. + * @return {!Function} Function to call when block is clicked. + * @private + */ +Blockly.Flyout.prototype.blockMouseDown_ = function(block) { + var flyout = this; + return function(e) { + Blockly.terminateDrag_(); + Blockly.hideChaff(true); + if (Blockly.isRightButton(e)) { + // Right-click. + block.showContextMenu_(e); + } else { + // Left-click (or middle click) + Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED); + // Record the current mouse position. + flyout.startDragMouseY_ = e.clientY; + flyout.startDragMouseX_ = e.clientX; + Blockly.Flyout.startDownEvent_ = e; + Blockly.Flyout.startBlock_ = block; + Blockly.Flyout.startFlyout_ = flyout; + Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', flyout, flyout.onMouseUp_); + Blockly.Flyout.onMouseMoveBlockWrapper_ = Blockly.bindEvent_(document, + 'mousemove', flyout, flyout.onMouseMoveBlock_); + } + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + e.preventDefault(); + }; +}; + +/** + * Mouse down on the flyout background. Start a vertical scroll drag. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Flyout.prototype.onMouseDown_ = function(e) { + if (Blockly.isRightButton(e)) { + return; + } + Blockly.hideChaff(true); + this.dragMode_ = Blockly.DRAG_FREE; + this.startDragMouseY_ = e.clientY; + this.startDragMouseX_ = e.clientX; + Blockly.Flyout.startFlyout_ = this; + Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove', + this, this.onMouseMove_); + Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup', + this, Blockly.Flyout.terminateDrag_); + // This event has been handled. No need to bubble up to the document. + e.preventDefault(); + e.stopPropagation(); +}; + +/** + * Handle a mouse-up anywhere in the SVG pane. Is only registered when a + * block is clicked. We can't use mouseUp on the block since a fast-moving + * cursor can briefly escape the block before it catches up. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Flyout.prototype.onMouseUp_ = function(e) { + if (!this.workspace_.isDragging()) { + if (this.autoClose) { + this.createBlockFunc_(Blockly.Flyout.startBlock_)( + Blockly.Flyout.startDownEvent_); + } else if (!Blockly.WidgetDiv.isVisible()) { + Blockly.Events.fire( + new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'click', + undefined, undefined)); + } + } + Blockly.terminateDrag_(); +}; + +/** + * Handle a mouse-move to vertically drag the flyout. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Flyout.prototype.onMouseMove_ = function(e) { + var metrics = this.getMetrics_(); + if (this.horizontalLayout_) { + if (metrics.contentWidth - metrics.viewWidth < 0) { + return; + } + var dx = e.clientX - this.startDragMouseX_; + this.startDragMouseX_ = e.clientX; + var x = metrics.viewLeft - dx; + x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth); + this.scrollbar_.set(x); + } else { + if (metrics.contentHeight - metrics.viewHeight < 0) { + return; + } + var dy = e.clientY - this.startDragMouseY_; + this.startDragMouseY_ = e.clientY; + var y = metrics.viewTop - dy; + y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight); + this.scrollbar_.set(y); + } +}; + +/** + * Mouse button is down on a block in a non-closing flyout. Create the block + * if the mouse moves beyond a small radius. This allows one to play with + * fields without instantiating blocks that instantly self-destruct. + * @param {!Event} e Mouse move event. + * @private + */ +Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) { + if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 && + e.button == 0) { + /* HACK: + Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove events + on certain touch actions. Ignore events with these signatures. + This may result in a one-pixel blind spot in other browsers, + but this shouldn't be noticeable. */ + e.stopPropagation(); + return; + } + var dx = e.clientX - Blockly.Flyout.startDownEvent_.clientX; + var dy = e.clientY - Blockly.Flyout.startDownEvent_.clientY; + + var createBlock = this.determineDragIntention_(dx, dy); + if (createBlock) { + this.createBlockFunc_(Blockly.Flyout.startBlock_)( + Blockly.Flyout.startDownEvent_); + } else if (this.dragMode_ == Blockly.DRAG_FREE) { + // Do a scroll. + this.onMouseMove_(e); + } + e.stopPropagation(); +}; + +/** + * Determine the intention of a drag. + * Updates dragMode_ based on a drag delta and the current mode, + * and returns true if we should create a new block. + * @param {number} dx X delta of the drag. + * @param {number} dy Y delta of the drag. + * @return {boolean} True if a new block should be created. + * @private + */ +Blockly.Flyout.prototype.determineDragIntention_ = function(dx, dy) { + if (this.dragMode_ == Blockly.DRAG_FREE) { + // Once in free mode, always stay in free mode and never create a block. + return false; + } + var dragDistance = Math.sqrt(dx * dx + dy * dy); + if (dragDistance < this.DRAG_RADIUS) { + // Still within the sticky drag radius. + this.dragMode_ = Blockly.DRAG_STICKY; + return false; + } else { + if (this.isDragTowardWorkspace_(dx, dy) || !this.scrollbar_.isVisible()) { + // Immediately create a block. + return true; + } else { + // Immediately move to free mode - the drag is away from the workspace. + this.dragMode_ = Blockly.DRAG_FREE; + return false; + } + } +}; + +/** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * @param {number} dx X delta of the drag. + * @param {number} dy Y delta of the drag. + * @return {boolean} true if the drag is toward the workspace. + * @private + */ +Blockly.Flyout.prototype.isDragTowardWorkspace_ = function(dx, dy) { + // Direction goes from -180 to 180, with 0 toward the right and 90 on top. + var dragDirection = Math.atan2(dy, dx) / Math.PI * 180; + + var draggingTowardWorkspace = false; + var range = this.dragAngleRange_; + if (this.horizontalLayout_) { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + // Horizontal at top. + if (dragDirection < 90 + range && dragDirection > 90 - range) { + draggingTowardWorkspace = true; + } + } else { + // Horizontal at bottom. + if (dragDirection > -90 - range && dragDirection < -90 + range) { + draggingTowardWorkspace = true; + } + } + } else { + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { + // Vertical at left. + if (dragDirection < range && dragDirection > -range) { + draggingTowardWorkspace = true; + } + } else { + // Vertical at right. + if (dragDirection < -180 + range || dragDirection > 180 - range) { + draggingTowardWorkspace = true; + } + } + } + return draggingTowardWorkspace; +}; + +/** + * Create a copy of this block on the workspace. + * @param {!Blockly.Block} originBlock The flyout block to copy. + * @return {!Function} Function to call when block is clicked. + * @private + */ +Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) { + var flyout = this; + return function(e) { + if (Blockly.isRightButton(e)) { + // Right-click. Don't create a block, let the context menu show. + return; + } + if (originBlock.disabled) { + // Beyond capacity. + return; + } + Blockly.Events.disable(); + try { + var block = flyout.placeNewBlock_(originBlock); + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled()) { + Blockly.Events.setGroup(true); + Blockly.Events.fire(new Blockly.Events.Create(block)); + } + if (flyout.autoClose) { + flyout.hide(); + } else { + flyout.filterForCapacity_(); + } + // Start a dragging operation on the new block. + block.onMouseDown_(e); + Blockly.dragMode_ = Blockly.DRAG_FREE; + block.setDragging_(true); + }; +}; + +/** + * Copy a block from the flyout to the workspace and position it correctly. + * @param {!Blockly.Block} originBlock The flyout block to copy.. + * @return {!Blockly.Block} The new block in the main workspace. + * @private + */ +Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock) { + var targetWorkspace = this.targetWorkspace_; + var svgRootOld = originBlock.getSvgRoot(); + if (!svgRootOld) { + throw 'originBlock is not rendered.'; + } + // Figure out where the original block is on the screen, relative to the upper + // left corner of the main workspace. + var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace); + // Take into account that the flyout might have been scrolled horizontally + // (separately from the main workspace). + // Generally a no-op in vertical mode but likely to happen in horizontal + // mode. + var scrollX = this.workspace_.scrollX; + var scale = this.workspace_.scale; + xyOld.x += scrollX / scale - scrollX; + // If the flyout is on the right side, (0, 0) in the flyout is offset to + // the right of (0, 0) in the main workspace. Add an offset to take that + // into account. + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) { + scrollX = targetWorkspace.getMetrics().viewWidth - this.width_; + scale = targetWorkspace.scale; + // Scale the scroll (getSvgXY_ did not do this). + xyOld.x += scrollX / scale - scrollX; + } + + // Take into account that the flyout might have been scrolled vertically + // (separately from the main workspace). + // Generally a no-op in horizontal mode but likely to happen in vertical + // mode. + var scrollY = this.workspace_.scrollY; + scale = this.workspace_.scale; + xyOld.y += scrollY / scale - scrollY; + // If the flyout is on the bottom, (0, 0) in the flyout is offset to be below + // (0, 0) in the main workspace. Add an offset to take that into account. + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + scrollY = targetWorkspace.getMetrics().viewHeight - this.height_; + scale = targetWorkspace.scale; + xyOld.y += scrollY / scale - scrollY; + } + + // Create the new block by cloning the block in the flyout (via XML). + var xml = Blockly.Xml.blockToDom(originBlock); + var block = Blockly.Xml.domToBlock(xml, targetWorkspace); + var svgRootNew = block.getSvgRoot(); + if (!svgRootNew) { + throw 'block is not rendered.'; + } + // Figure out where the new block got placed on the screen, relative to the + // upper left corner of the workspace. This may not be the same as the + // original block because the flyout's origin may not be the same as the + // main workspace's origin. + var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace); + // Scale the scroll (getSvgXY_ did not do this). + xyNew.x += + targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX; + xyNew.y += + targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY; + // If the flyout is collapsible and the workspace can't be scrolled. + if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) { + xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale; + xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale; + } + + // Move the new block to where the old block is. + block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y); + return block; +}; + +/** + * Filter the blocks on the flyout to disable the ones that are above the + * capacity limit. + * @private + */ +Blockly.Flyout.prototype.filterForCapacity_ = function() { + var remainingCapacity = this.targetWorkspace_.remainingCapacity(); + var blocks = this.workspace_.getTopBlocks(false); + for (var i = 0, block; block = blocks[i]; i++) { + if (this.permanentlyDisabled_.indexOf(block) == -1) { + var allBlocks = block.getDescendants(); + block.setDisabled(allBlocks.length > remainingCapacity); + } + } +}; + +/** + * Return the deletion rectangle for this flyout. + * @return {goog.math.Rect} Rectangle in which to delete. + */ +Blockly.Flyout.prototype.getClientRect = function() { + if (!this.svgGroup_) { + return null; + } + + var flyoutRect = this.svgGroup_.getBoundingClientRect(); + // BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout + // area are still deleted. Must be larger than the largest screen size, + // but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE). + var BIG_NUM = 1000000000; + var x = flyoutRect.left; + var y = flyoutRect.top; + var width = flyoutRect.width; + var height = flyoutRect.height; + + if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { + return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2, + BIG_NUM + height); + } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) { + return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2, + BIG_NUM + height); + } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { + return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width, + BIG_NUM * 2); + } else { // Right + return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, BIG_NUM * 2); + } +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Flyout.terminateDrag_ = function() { + if (Blockly.Flyout.startFlyout_) { + Blockly.Flyout.startFlyout_.dragMode_ = Blockly.DRAG_NONE; + } + if (Blockly.Flyout.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseUpWrapper_); + Blockly.Flyout.onMouseUpWrapper_ = null; + } + if (Blockly.Flyout.onMouseMoveBlockWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveBlockWrapper_); + Blockly.Flyout.onMouseMoveBlockWrapper_ = null; + } + if (Blockly.Flyout.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveWrapper_); + Blockly.Flyout.onMouseMoveWrapper_ = null; + } + Blockly.Flyout.startDownEvent_ = null; + Blockly.Flyout.startBlock_ = null; + Blockly.Flyout.startFlyout_ = null; +}; + +/** + * Compute height of flyout. Position button under each block. + * For RTL: Lay out the blocks right-aligned. + * @param {!Array} blocks The blocks to reflow. + */ +Blockly.Flyout.prototype.reflowHorizontal = function(blocks) { + this.workspace_.scale = this.targetWorkspace_.scale; + var flyoutHeight = 0; + for (var i = 0, block; block = blocks[i]; i++) { + flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height); + } + flyoutHeight += this.MARGIN * 1.5; + flyoutHeight *= this.workspace_.scale; + flyoutHeight += Blockly.Scrollbar.scrollbarThickness; + if (this.height_ != flyoutHeight) { + for (var i = 0, block; block = blocks[i]; i++) { + var blockHW = block.getHeightWidth(); + if (block.flyoutRect_) { + block.flyoutRect_.setAttribute('width', blockHW.width); + block.flyoutRect_.setAttribute('height', blockHW.height); + // Rectangles behind blocks with output tabs are shifted a bit. + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockXY = block.getRelativeToSurfaceXY(); + block.flyoutRect_.setAttribute('y', blockXY.y); + block.flyoutRect_.setAttribute('x', + this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab); + // For hat blocks we want to shift them down by the hat height + // since the y coordinate is the corner, not the top of the hat. + var hatOffset = + block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0; + if (hatOffset) { + block.moveBy(0, hatOffset); + } + block.flyoutRect_.setAttribute('y', blockXY.y); + } + } + // Record the height for .getMetrics_ and .position. + this.height_ = flyoutHeight; + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); + } +}; + +/** + * Compute width of flyout. Position button under each block. + * For RTL: Lay out the blocks right-aligned. + * @param {!Array} blocks The blocks to reflow. + */ +Blockly.Flyout.prototype.reflowVertical = function(blocks) { + this.workspace_.scale = this.targetWorkspace_.scale; + var flyoutWidth = 0; + for (var i = 0, block; block = blocks[i]; i++) { + var width = block.getHeightWidth().width; + if (block.outputConnection) { + width -= Blockly.BlockSvg.TAB_WIDTH; + } + flyoutWidth = Math.max(flyoutWidth, width); + } + for (var i = 0, button; button = this.buttons_[i]; i++) { + flyoutWidth = Math.max(flyoutWidth, button.width); + } + flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH; + flyoutWidth *= this.workspace_.scale; + flyoutWidth += Blockly.Scrollbar.scrollbarThickness; + if (this.width_ != flyoutWidth) { + for (var i = 0, block; block = blocks[i]; i++) { + var blockHW = block.getHeightWidth(); + if (this.RTL) { + // With the flyoutWidth known, right-align the blocks. + var oldX = block.getRelativeToSurfaceXY().x; + var newX = flyoutWidth / this.workspace_.scale - this.MARGIN; + newX -= Blockly.BlockSvg.TAB_WIDTH; + block.moveBy(newX - oldX, 0); + } + if (block.flyoutRect_) { + block.flyoutRect_.setAttribute('width', blockHW.width); + block.flyoutRect_.setAttribute('height', blockHW.height); + // Blocks with output tabs are shifted a bit. + var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockXY = block.getRelativeToSurfaceXY(); + block.flyoutRect_.setAttribute('x', + this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab); + // For hat blocks we want to shift them down by the hat height + // since the y coordinate is the corner, not the top of the hat. + var hatOffset = + block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0; + if (hatOffset) { + block.moveBy(0, hatOffset); + } + block.flyoutRect_.setAttribute('y', blockXY.y); + } + } + // Record the width for .getMetrics_ and .position. + this.width_ = flyoutWidth; + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); + } +}; + +/** + * Reflow blocks and their buttons. + */ +Blockly.Flyout.prototype.reflow = function() { + if (this.reflowWrapper_) { + this.workspace_.removeChangeListener(this.reflowWrapper_); + } + var blocks = this.workspace_.getTopBlocks(false); + if (this.horizontalLayout_) { + this.reflowHorizontal(blocks); + } else { + this.reflowVertical(blocks); + } + if (this.reflowWrapper_) { + this.workspace_.addChangeListener(this.reflowWrapper_); + } +}; diff --git a/core/flyout.js.rej b/core/flyout.js.rej new file mode 100644 index 000000000..fcb010f80 --- /dev/null +++ b/core/flyout.js.rej @@ -0,0 +1,17 @@ +*************** +*** 304,310 **** + var block = Blockly.Xml.domToBlock( + /** @type {!Blockly.Workspace} */ (this.workspace_), xml); + blocks.push(block); +- gaps.push(margin * 3); + } + } + } +--- 318,324 ---- + var block = Blockly.Xml.domToBlock( + /** @type {!Blockly.Workspace} */ (this.workspace_), xml); + blocks.push(block); ++ gaps.push(margin * this.VERTICAL_SEPARATION_FACTOR); // [lyn, 10/06/13] introduced VERTICAL_SEPARATION_FACTOR + } + } + } diff --git a/core/inject.js.orig b/core/inject.js.orig new file mode 100644 index 000000000..57e3e51f4 --- /dev/null +++ b/core/inject.js.orig @@ -0,0 +1,378 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 Functions for injecting Blockly into a web page. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.inject'); + +goog.require('Blockly.Css'); +goog.require('Blockly.Options'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.dom'); +goog.require('goog.ui.Component'); +goog.require('goog.userAgent'); + + +/** + * Inject a Blockly editor into the specified container element (usually a div). + * @param {!Element|string} container Containing element, or its ID, + * or a CSS selector. + * @param {Object=} opt_options Optional dictionary of options. + * @return {!Blockly.Workspace} Newly created main workspace. + */ +Blockly.inject = function(container, opt_options) { + if (goog.isString(container)) { + container = document.getElementById(container) || + document.querySelector(container); + } + // Verify that the container is in document. + if (!goog.dom.contains(document, container)) { + throw 'Error: container is not in current document.'; + } + var options = new Blockly.Options(opt_options || {}); + var subContainer = goog.dom.createDom('div', 'injectionDiv'); + container.appendChild(subContainer); + var svg = Blockly.createDom_(subContainer, options); + var workspace = Blockly.createMainWorkspace_(svg, options); + Blockly.init_(workspace); + workspace.markFocused(); + Blockly.bindEvent_(svg, 'focus', workspace, workspace.markFocused); + Blockly.svgResize(workspace); + return workspace; +}; + +/** + * Create the SVG image. + * @param {!Element} container Containing element. + * @param {!Blockly.Options} options Dictionary of options. + * @return {!Element} Newly created SVG image. + * @private + */ +Blockly.createDom_ = function(container, options) { + // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying + // out content in RTL mode. Therefore Blockly forces the use of LTR, + // then manually positions content in RTL as needed. + container.setAttribute('dir', 'LTR'); + // Closure can be trusted to create HTML widgets with the proper direction. + goog.ui.Component.setDefaultRightToLeft(options.RTL); + + // Load CSS. + Blockly.Css.inject(options.hasCss, options.pathToMedia); + + // 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', + 'class': 'blocklySvg' + }, container); + /* + + ... filters go here ... + + */ + var defs = Blockly.createSvgElement('defs', {}, svg); + var rnd = String(Math.random()).substring(2); + /* + + + + + + + + + */ + var embossFilter = Blockly.createSvgElement('filter', + {'id': 'blocklyEmbossFilter' + rnd}, defs); + Blockly.createSvgElement('feGaussianBlur', + {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter); + var feSpecularLighting = Blockly.createSvgElement('feSpecularLighting', + {'in': 'blur', 'surfaceScale': 1, 'specularConstant': 0.5, + 'specularExponent': 10, 'lighting-color': 'white', 'result': 'specOut'}, + embossFilter); + Blockly.createSvgElement('fePointLight', + {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting); + Blockly.createSvgElement('feComposite', + {'in': 'specOut', 'in2': 'SourceAlpha', 'operator': 'in', + 'result': 'specOut'}, embossFilter); + Blockly.createSvgElement('feComposite', + {'in': 'SourceGraphic', 'in2': 'specOut', 'operator': 'arithmetic', + 'k1': 0, 'k2': 1, 'k3': 1, 'k4': 0}, embossFilter); + options.embossFilterId = embossFilter.id; + /* + + + + + */ + var disabledPattern = Blockly.createSvgElement('pattern', + {'id': 'blocklyDisabledPattern' + rnd, + 'patternUnits': 'userSpaceOnUse', + 'width': 10, 'height': 10}, defs); + Blockly.createSvgElement('rect', + {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern); + Blockly.createSvgElement('path', + {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); + options.disabledPatternId = disabledPattern.id; + /* + + + + + */ + var gridPattern = Blockly.createSvgElement('pattern', + {'id': 'blocklyGridPattern' + rnd, + 'patternUnits': 'userSpaceOnUse'}, defs); + if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) { + Blockly.createSvgElement('line', + {'stroke': options.gridOptions['colour']}, + gridPattern); + if (options.gridOptions['length'] > 1) { + Blockly.createSvgElement('line', + {'stroke': options.gridOptions['colour']}, + gridPattern); + } + // x1, y1, x1, x2 properties will be set later in updateGridPattern_. + } + options.gridPattern = gridPattern; + return svg; +}; + +/** + * Create a main workspace and add it to the SVG. + * @param {!Element} svg SVG element with pattern defined. + * @param {!Blockly.Options} options Dictionary of options. + * @return {!Blockly.Workspace} Newly created main workspace. + * @private + */ +Blockly.createMainWorkspace_ = function(svg, options) { + options.parentWorkspace = null; + var mainWorkspace = new Blockly.WorkspaceSvg(options); + mainWorkspace.scale = options.zoomOptions.startScale; + svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); + // A null translation will also apply the correct initial scale. + mainWorkspace.translate(0, 0); + mainWorkspace.markFocused(); + + if (!options.readOnly && !options.hasScrollbars) { + var workspaceChanged = function() { + if (Blockly.dragMode_ == Blockly.DRAG_NONE) { + var metrics = mainWorkspace.getMetrics(); + var edgeLeft = metrics.viewLeft + metrics.absoluteLeft; + var edgeTop = metrics.viewTop + metrics.absoluteTop; + if (metrics.contentTop < edgeTop || + metrics.contentTop + metrics.contentHeight > + metrics.viewHeight + edgeTop || + metrics.contentLeft < + (options.RTL ? metrics.viewLeft : edgeLeft) || + metrics.contentLeft + metrics.contentWidth > (options.RTL ? + metrics.viewWidth : metrics.viewWidth + edgeLeft)) { + // One or more blocks may be out of bounds. Bump them back in. + var MARGIN = 25; + var blocks = mainWorkspace.getTopBlocks(false); + for (var b = 0, block; block = blocks[b]; b++) { + var blockXY = block.getRelativeToSurfaceXY(); + var blockHW = block.getHeightWidth(); + // Bump any block that's above the top back inside. + var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y; + if (overflowTop > 0) { + block.moveBy(0, overflowTop); + } + // Bump any block that's below the bottom back inside. + var overflowBottom = + edgeTop + metrics.viewHeight - MARGIN - blockXY.y; + if (overflowBottom < 0) { + block.moveBy(0, overflowBottom); + } + // Bump any block that's off the left back inside. + var overflowLeft = MARGIN + edgeLeft - + blockXY.x - (options.RTL ? 0 : blockHW.width); + if (overflowLeft > 0) { + block.moveBy(overflowLeft, 0); + } + // Bump any block that's off the right back inside. + var overflowRight = edgeLeft + metrics.viewWidth - MARGIN - + blockXY.x + (options.RTL ? blockHW.width : 0); + if (overflowRight < 0) { + block.moveBy(overflowRight, 0); + } + } + } + } + }; + mainWorkspace.addChangeListener(workspaceChanged); + } + // The SVG is now fully assembled. + Blockly.svgResize(mainWorkspace); + Blockly.WidgetDiv.createDom(); + Blockly.Tooltip.createDom(); + return mainWorkspace; +}; + +/** + * Initialize Blockly with various handlers. + * @param {!Blockly.Workspace} mainWorkspace Newly created main workspace. + * @private + */ +Blockly.init_ = function(mainWorkspace) { + var options = mainWorkspace.options; + var svg = mainWorkspace.getParentSvg(); + + // Supress the browser's context menu. + Blockly.bindEvent_(svg, 'contextmenu', null, + function(e) { + if (!Blockly.isTargetInput_(e)) { + e.preventDefault(); + } + }); + + var workspaceResizeHandler = Blockly.bindEvent_(window, 'resize', null, + function() { + Blockly.hideChaff(true); + Blockly.svgResize(mainWorkspace); + }); + mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); + + Blockly.inject.bindDocumentEvents_(); + + if (options.languageTree) { + if (mainWorkspace.toolbox_) { + mainWorkspace.toolbox_.init(mainWorkspace); + } else if (mainWorkspace.flyout_) { + // Build a fixed flyout with the root blocks. + mainWorkspace.flyout_.init(mainWorkspace); + mainWorkspace.flyout_.show(options.languageTree.childNodes); + mainWorkspace.flyout_.scrollToStart(); + // Translate the workspace sideways to avoid the fixed flyout. + mainWorkspace.scrollX = mainWorkspace.flyout_.width_; + if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + mainWorkspace.scrollX *= -1; + } + mainWorkspace.translate(mainWorkspace.scrollX, 0); + } + } + + if (options.hasScrollbars) { + mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace); + mainWorkspace.scrollbar.resize(); + } + + // Load the sounds. + if (options.hasSounds) { + Blockly.inject.loadSounds_(options.pathToMedia, mainWorkspace); + } +}; + +/** + * Bind document events, but only once. Destroying and reinjecting Blockly + * should not bind again. + * Bind events for scrolling the workspace. + * Most of these events should be bound to the SVG's surface. + * However, 'mouseup' has to be on the whole document so that a block dragged + * out of bounds and released will know that it has been released. + * Also, 'keydown' has to be on the whole document since the browser doesn't + * understand a concept of focus on the SVG image. + * @private + */ +Blockly.inject.bindDocumentEvents_ = function() { + if (!Blockly.documentEventsBound_) { + Blockly.bindEvent_(document, 'keydown', null, Blockly.onKeyDown_); + Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_); + Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_); + // Don't use bindEvent_ for document's mouseup since that would create a + // corresponding touch handler that would squeltch the ability to interact + // with non-Blockly elements. + document.addEventListener('mouseup', Blockly.onMouseUp_, false); + // Some iPad versions don't fire resize after portrait to landscape change. + if (goog.userAgent.IPAD) { + Blockly.bindEvent_(window, 'orientationchange', document, function() { + // TODO(#397): Fix for multiple blockly workspaces. + Blockly.svgResize(Blockly.getMainWorkspace()); + }); + } + } + Blockly.documentEventsBound_ = true; +}; + +/** + * Load sounds for the given workspace. + * @param {string} pathToMedia The path to the media directory. + * @param {!Blockly.Workspace} workspace The workspace to load sounds for. + * @private + */ +Blockly.inject.loadSounds_ = function(pathToMedia, workspace) { + workspace.loadAudio_( + [pathToMedia + 'click.mp3', + pathToMedia + 'click.wav', + pathToMedia + 'click.ogg'], 'click'); + workspace.loadAudio_( + [pathToMedia + 'disconnect.wav', + pathToMedia + 'disconnect.mp3', + pathToMedia + 'disconnect.ogg'], 'disconnect'); + workspace.loadAudio_( + [pathToMedia + 'delete.mp3', + pathToMedia + 'delete.ogg', + pathToMedia + 'delete.wav'], 'delete'); + + // Bind temporary hooks that preload the sounds. + var soundBinds = []; + var unbindSounds = function() { + while (soundBinds.length) { + Blockly.unbindEvent_(soundBinds.pop()); + } + workspace.preloadAudio_(); + }; + // Android ignores any sound not loaded as a result of a user action. + soundBinds.push( + Blockly.bindEvent_(document, 'mousemove', null, unbindSounds)); + soundBinds.push( + Blockly.bindEvent_(document, 'touchstart', null, unbindSounds)); +}; + +/** + * Modify the block tree on the existing toolbox. + * @param {Node|string} tree DOM tree of blocks, or text representation of same. + */ +Blockly.updateToolbox = function(tree) { + console.warn('Deprecated call to Blockly.updateToolbox, ' + + 'use workspace.updateToolbox instead.'); + Blockly.getMainWorkspace().updateToolbox(tree); +}; diff --git a/core/inject.js.rej b/core/inject.js.rej new file mode 100644 index 000000000..2d6fdc651 --- /dev/null +++ b/core/inject.js.rej @@ -0,0 +1,129 @@ +*************** +*** 108,113 **** + if (hasCollapse === undefined) { + hasCollapse = hasCategories; + } + var hasComments = options['comments']; + if (hasComments === undefined) { + hasComments = hasCategories; +--- 108,114 ---- + if (hasCollapse === undefined) { + hasCollapse = hasCategories; + } ++ var configForTypeBlock = options['typeblock_config']; + var hasComments = options['comments']; + if (hasComments === undefined) { + hasComments = hasCategories; +*************** +*** 125,134 **** + if (hasScrollbars === undefined) { + hasScrollbars = true; + } + } + var enableRealtime = !!options['realtime']; + var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined; +- + Blockly.RTL = !!options['rtl']; + Blockly.collapse = hasCollapse; + Blockly.comments = hasComments; +--- 126,135 ---- + if (hasScrollbars === undefined) { + hasScrollbars = true; + } ++ var configForTypeBlock = null; + } + var enableRealtime = !!options['realtime']; + var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined; + Blockly.RTL = !!options['rtl']; + Blockly.collapse = hasCollapse; + Blockly.comments = hasComments; +*************** +*** 140,145 **** + Blockly.hasScrollbars = hasScrollbars; + Blockly.hasTrashcan = hasTrashcan; + Blockly.languageTree = tree; + Blockly.enableRealtime = enableRealtime; + Blockly.realtimeOptions = realtimeOptions; + }; +--- 141,147 ---- + Blockly.hasScrollbars = hasScrollbars; + Blockly.hasTrashcan = hasTrashcan; + Blockly.languageTree = tree; ++ Blockly.configForTypeBlock = configForTypeBlock; + Blockly.enableRealtime = enableRealtime; + Blockly.realtimeOptions = realtimeOptions; + }; +*************** +*** 271,276 **** + * @type {!Blockly.Flyout} + * @private + */ + Blockly.mainWorkspace.flyout_ = new Blockly.Flyout(); + var flyout = Blockly.mainWorkspace.flyout_; + var flyoutSvg = flyout.createDom(); +--- 273,280 ---- + * @type {!Blockly.Flyout} + * @private + */ ++ ++ /* + Blockly.mainWorkspace.flyout_ = new Blockly.Flyout(); + var flyout = Blockly.mainWorkspace.flyout_; + var flyoutSvg = flyout.createDom(); +*************** +*** 329,334 **** + } + }; + Blockly.addChangeListener(workspaceChanged); + } + } + +--- 333,339 ---- + } + }; + Blockly.addChangeListener(workspaceChanged); ++ */ + } + } + +*************** +*** 398,407 **** + Blockly.Toolbox.init(); + } else { + // Build a fixed flyout with the root blocks. +- Blockly.mainWorkspace.flyout_.init(Blockly.mainWorkspace, true); +- Blockly.mainWorkspace.flyout_.show(Blockly.languageTree.childNodes); +- // Translate the workspace sideways to avoid the fixed flyout. +- Blockly.mainWorkspace.scrollX = Blockly.mainWorkspace.flyout_.width_; + if (Blockly.RTL) { + Blockly.mainWorkspace.scrollX *= -1; + } +--- 403,414 ---- + Blockly.Toolbox.init(); + } else { + // Build a fixed flyout with the root blocks. ++ //if (Blockly.mainWorkspace.flyout_) { ++ Blockly.mainWorkspace.flyout_.init(Blockly.mainWorkspace, true); ++ Blockly.mainWorkspace.flyout_.show(Blockly.languageTree.childNodes); ++ // Translate the workspace sideways to avoid the fixed flyout. ++ Blockly.mainWorkspace.scrollX = Blockly.mainWorkspace.flyout_.width_; ++ //} + if (Blockly.RTL) { + Blockly.mainWorkspace.scrollX *= -1; + } +*************** +*** 418,423 **** + } + + Blockly.mainWorkspace.addTrashcan(); + + // Load the sounds. + Blockly.loadAudio_( +--- 425,431 ---- + } + + Blockly.mainWorkspace.addTrashcan(); ++ Blockly.mainWorkspace.addWarningIndicator(Blockly.mainWorkspace); + + // Load the sounds. + Blockly.loadAudio_( diff --git a/core/input.js b/core/input.js index 4b4eb1df9..b4d11d4e3 100644 --- a/core/input.js +++ b/core/input.js @@ -102,6 +102,10 @@ Blockly.Input.prototype.appendField = function(field, opt_name) { this.appendField(field.suffixField); } + //If it's a COLLAPSE_TEXT input, hide it by default + if (opt_name === 'COLLAPSED_TEXT') + this.sourceBlock_.getTitle_(opt_name).getRootElement().style.display = 'none'; + if (this.sourceBlock_.rendered) { this.sourceBlock_.render(); // Adding a field will cause the block to change shape. diff --git a/core/input.js.orig b/core/input.js.orig new file mode 100644 index 000000000..4b4eb1df9 --- /dev/null +++ b/core/input.js.orig @@ -0,0 +1,241 @@ +/** + * @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 Object representing an input (value, statement, or dummy). + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Input'); + +goog.require('Blockly.Connection'); +goog.require('Blockly.FieldLabel'); +goog.require('goog.asserts'); + + +/** + * Class for an input with an optional field. + * @param {number} type The type of the input. + * @param {string} name Language-neutral identifier which may used to find this + * input again. + * @param {!Blockly.Block} block The block containing this input. + * @param {Blockly.Connection} connection Optional connection for this input. + * @constructor + */ +Blockly.Input = function(type, name, block, connection) { + /** @type {number} */ + this.type = type; + /** @type {string} */ + this.name = name; + /** + * @type {!Blockly.Block} + * @private + */ + this.sourceBlock_ = block; + /** @type {Blockly.Connection} */ + this.connection = connection; + /** @type {!Array.} */ + this.fieldRow = []; +}; + +/** + * Alignment of input's fields (left, right or centre). + * @type {number} + */ +Blockly.Input.prototype.align = Blockly.ALIGN_LEFT; + +/** + * Is the input visible? + * @type {boolean} + * @private + */ +Blockly.Input.prototype.visible_ = true; + +/** + * Add an item to the end of the input's field row. + * @param {string|!Blockly.Field} field Something to add as a field. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this field again. Should be unique to the host block. + * @return {!Blockly.Input} The input being append to (to allow chaining). + */ +Blockly.Input.prototype.appendField = function(field, opt_name) { + // Empty string, Null or undefined generates no field, unless field is named. + if (!field && !opt_name) { + return this; + } + // Generate a FieldLabel when given a plain text field. + if (goog.isString(field)) { + field = new Blockly.FieldLabel(/** @type {string} */ (field)); + } + field.setSourceBlock(this.sourceBlock_); + if (this.sourceBlock_.rendered) { + field.init(); + } + field.name = opt_name; + + if (field.prefixField) { + // Add any prefix. + this.appendField(field.prefixField); + } + // Add the field to the field row. + this.fieldRow.push(field); + if (field.suffixField) { + // Add any suffix. + this.appendField(field.suffixField); + } + + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + // Adding a field will cause the block to change shape. + this.sourceBlock_.bumpNeighbours_(); + } + return this; +}; + +/** + * Add an item to the end of the input's field row. + * @param {*} field Something to add as a field. + * @param {string=} opt_name Language-neutral identifier which may used to find + * this field again. Should be unique to the host block. + * @return {!Blockly.Input} The input being append to (to allow chaining). + * @deprecated December 2013 + */ +Blockly.Input.prototype.appendTitle = function(field, opt_name) { + console.warn('Deprecated call to appendTitle, use appendField instead.'); + return this.appendField(field, opt_name); +}; + +/** + * Remove a field from this input. + * @param {string} name The name of the field. + * @throws {goog.asserts.AssertionError} if the field is not present. + */ +Blockly.Input.prototype.removeField = function(name) { + for (var i = 0, field; field = this.fieldRow[i]; i++) { + if (field.name === name) { + field.dispose(); + this.fieldRow.splice(i, 1); + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + // Removing a field will cause the block to change shape. + this.sourceBlock_.bumpNeighbours_(); + } + return; + } + } + goog.asserts.fail('Field "%s" not found.', name); +}; + +/** + * Gets whether this input is visible or not. + * @return {boolean} True if visible. + */ +Blockly.Input.prototype.isVisible = function() { + return this.visible_; +}; + +/** + * Sets whether this input is visible or not. + * Used to collapse/uncollapse a block. + * @param {boolean} visible True if visible. + * @return {!Array.} List of blocks to render. + */ +Blockly.Input.prototype.setVisible = function(visible) { + var renderList = []; + if (this.visible_ == visible) { + return renderList; + } + this.visible_ = visible; + + var display = visible ? 'block' : 'none'; + for (var y = 0, field; field = this.fieldRow[y]; y++) { + field.setVisible(visible); + } + if (this.connection) { + // Has a connection. + if (visible) { + renderList = this.connection.unhideAll(); + } else { + this.connection.hideAll(); + } + var child = this.connection.targetBlock(); + if (child) { + child.getSvgRoot().style.display = display; + if (!visible) { + child.rendered = false; + } + } + } + return renderList; +}; + +/** + * Change a connection's compatibility. + * @param {string|Array.|null} check Compatible value type or + * list of value types. Null if all types are compatible. + * @return {!Blockly.Input} The input being modified (to allow chaining). + */ +Blockly.Input.prototype.setCheck = function(check) { + if (!this.connection) { + throw 'This input does not have a connection.'; + } + this.connection.setCheck(check); + return this; +}; + +/** + * Change the alignment of the connection's field(s). + * @param {number} align One of Blockly.ALIGN_LEFT, ALIGN_CENTRE, ALIGN_RIGHT. + * In RTL mode directions are reversed, and ALIGN_RIGHT aligns to the left. + * @return {!Blockly.Input} The input being modified (to allow chaining). + */ +Blockly.Input.prototype.setAlign = function(align) { + this.align = align; + if (this.sourceBlock_.rendered) { + this.sourceBlock_.render(); + } + return this; +}; + +/** + * Initialize the fields on this input. + */ +Blockly.Input.prototype.init = function() { + if (!this.sourceBlock_.workspace.rendered) { + return; // Headless blocks don't need fields initialized. + } + for (var i = 0; i < this.fieldRow.length; i++) { + this.fieldRow[i].init(); + } +}; + +/** + * Sever all links to this input. + */ +Blockly.Input.prototype.dispose = function() { + for (var i = 0, field; field = this.fieldRow[i]; i++) { + field.dispose(); + } + if (this.connection) { + this.connection.dispose(); + } + this.sourceBlock_ = null; +}; diff --git a/core/input.js.rej b/core/input.js.rej new file mode 100644 index 000000000..76aeb84bd --- /dev/null +++ b/core/input.js.rej @@ -0,0 +1,23 @@ +*************** +*** 43,49 **** + * @constructor + */ + Blockly.Input = function(type, name, block, connection) { +- this.type = type; + this.name = name; + this.sourceBlock_ = block; + this.connection = connection; +--- 43,55 ---- + * @constructor + */ + Blockly.Input = function(type, name, block, connection) { ++ if (type == Blockly.INDENTED_VALUE){ ++ this.type = Blockly.INPUT_VALUE; ++ this.subtype = Blockly.INDENTED_VALUE; ++ } ++ else { ++ this.type = type; ++ } + this.name = name; + this.sourceBlock_ = block; + this.connection = connection; diff --git a/core/mutator.js.orig b/core/mutator.js.orig new file mode 100644 index 000000000..19a0fe86b --- /dev/null +++ b/core/mutator.js.orig @@ -0,0 +1,389 @@ +/** + * @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 Object representing a mutator dialog. A mutator allows the + * user to change the shape of a block using a nested blocks editor. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Mutator'); + +goog.require('Blockly.Bubble'); +goog.require('Blockly.Icon'); +goog.require('Blockly.WorkspaceSvg'); +goog.require('goog.Timer'); +goog.require('goog.dom'); + + +/** + * Class for a mutator dialog. + * @param {!Array.} quarkNames List of names of sub-blocks for flyout. + * @extends {Blockly.Icon} + * @constructor + */ +Blockly.Mutator = function(quarkNames) { + Blockly.Mutator.superClass_.constructor.call(this, null); + this.quarkNames_ = quarkNames; +}; +goog.inherits(Blockly.Mutator, Blockly.Icon); + +/** + * Width of workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceWidth_ = 0; + +/** + * Height of workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceHeight_ = 0; + +/** + * Draw the mutator icon. + * @param {!Element} group The icon group. + * @private + */ +Blockly.Mutator.prototype.drawIcon_ = function(group) { + // Square with rounded corners. + Blockly.createSvgElement('rect', + {'class': 'blocklyIconShape', + 'rx': '4', 'ry': '4', + 'height': '16', 'width': '16'}, + group); + // Gear teeth. + Blockly.createSvgElement('path', + {'class': 'blocklyIconSymbol', + 'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z'}, + group); + // Axle hole. + Blockly.createSvgElement('circle', + {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, + group); +}; + +/** + * Clicking on the icon toggles if the mutator bubble is visible. + * Disable if block is uneditable. + * @param {!Event} e Mouse click event. + * @private + * @override + */ +Blockly.Mutator.prototype.iconClick_ = function(e) { + if (this.block_.isEditable()) { + Blockly.Icon.prototype.iconClick_.call(this, e); + } +}; + +/** + * Create the editor for the mutator's bubble. + * @return {!Element} The top-level node of the editor. + * @private + */ +Blockly.Mutator.prototype.createEditor_ = function() { + /* Create the editor. Here's the markup that will be generated: + + [Workspace] + + */ + this.svgDialog_ = Blockly.createSvgElement('svg', + {'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH}, + null); + // Convert the list of names into a list of XML objects for the flyout. + if (this.quarkNames_.length) { + var quarkXml = goog.dom.createDom('xml'); + for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) { + quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName})); + } + } else { + var quarkXml = null; + } + var workspaceOptions = { + languageTree: quarkXml, + parentWorkspace: this.block_.workspace, + pathToMedia: this.block_.workspace.options.pathToMedia, + RTL: this.block_.RTL, + toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT : + Blockly.TOOLBOX_AT_LEFT, + horizontalLayout: false, + getMetrics: this.getFlyoutMetrics_.bind(this), + setMetrics: null + }; + this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); + this.workspace_.isMutator = true; + this.svgDialog_.appendChild( + this.workspace_.createDom('blocklyMutatorBackground')); + return this.svgDialog_; +}; + +/** + * Add or remove the UI indicating if this icon may be clicked or not. + */ +Blockly.Mutator.prototype.updateEditable = function() { + if (!this.block_.isInFlyout) { + if (this.block_.isEditable()) { + if (this.iconGroup_) { + Blockly.removeClass_(/** @type {!Element} */ (this.iconGroup_), + 'blocklyIconGroupReadonly'); + } + } else { + // Close any mutator bubble. Icon is not clickable. + this.setVisible(false); + if (this.iconGroup_) { + Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_), + 'blocklyIconGroupReadonly'); + } + } + } + // Default behaviour for an icon. + Blockly.Icon.prototype.updateEditable.call(this); +}; + +/** + * Callback function triggered when the bubble has resized. + * Resize the workspace accordingly. + * @private + */ +Blockly.Mutator.prototype.resizeBubble_ = function() { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; + var workspaceSize = this.workspace_.getCanvas().getBBox(); + var width; + if (this.block_.RTL) { + width = -workspaceSize.x; + } else { + width = workspaceSize.width + workspaceSize.x; + } + var height = workspaceSize.height + doubleBorderWidth * 3; + if (this.workspace_.flyout_) { + var flyoutMetrics = this.workspace_.flyout_.getMetrics_(); + height = Math.max(height, flyoutMetrics.contentHeight + 20); + } + width += doubleBorderWidth * 3; + // Only resize if the size difference is significant. Eliminates shuddering. + if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth || + Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) { + // Record some layout information for getFlyoutMetrics_. + this.workspaceWidth_ = width; + this.workspaceHeight_ = height; + // Resize the bubble. + this.bubble_.setBubbleSize(width + doubleBorderWidth, + height + doubleBorderWidth); + this.svgDialog_.setAttribute('width', this.workspaceWidth_); + this.svgDialog_.setAttribute('height', this.workspaceHeight_); + } + + if (this.block_.RTL) { + // Scroll the workspace to always left-align. + var translation = 'translate(' + this.workspaceWidth_ + ',0)'; + this.workspace_.getCanvas().setAttribute('transform', translation); + } + this.workspace_.resize(); +}; + +/** + * Show or hide the mutator bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +Blockly.Mutator.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + // No change. + return; + } + Blockly.Events.fire( + new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible)); + if (visible) { + // Create the bubble. + this.bubble_ = new Blockly.Bubble( + /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), + this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null); + var tree = this.workspace_.options.languageTree; + if (tree) { + this.workspace_.flyout_.init(this.workspace_); + this.workspace_.flyout_.show(tree.childNodes); + } + + this.rootBlock_ = this.block_.decompose(this.workspace_); + var blocks = this.rootBlock_.getDescendants(); + for (var i = 0, child; child = blocks[i]; i++) { + child.render(); + } + // The root block should not be dragable or deletable. + this.rootBlock_.setMovable(false); + this.rootBlock_.setDeletable(false); + if (this.workspace_.flyout_) { + var margin = this.workspace_.flyout_.CORNER_RADIUS * 2; + var x = this.workspace_.flyout_.width_ + margin; + } else { + var margin = 16; + var x = margin; + } + if (this.block_.RTL) { + x = -x; + } + this.rootBlock_.moveBy(x, margin); + // Save the initial connections, then listen for further changes. + if (this.block_.saveConnections) { + var thisMutator = this; + this.block_.saveConnections(this.rootBlock_); + this.sourceListener_ = function() { + thisMutator.block_.saveConnections(thisMutator.rootBlock_); + }; + this.block_.workspace.addChangeListener(this.sourceListener_); + } + this.resizeBubble_(); + // When the mutator's workspace changes, update the source block. + this.workspace_.addChangeListener(this.workspaceChanged_.bind(this)); + this.updateColour(); + } else { + // Dispose of the bubble. + this.svgDialog_ = null; + this.workspace_.dispose(); + this.workspace_ = null; + this.rootBlock_ = null; + this.bubble_.dispose(); + this.bubble_ = null; + this.workspaceWidth_ = 0; + this.workspaceHeight_ = 0; + if (this.sourceListener_) { + this.block_.workspace.removeChangeListener(this.sourceListener_); + this.sourceListener_ = null; + } + } +}; + +/** + * Update the source block when the mutator's blocks are changed. + * Bump down any block that's too high. + * Fired whenever a change is made to the mutator's workspace. + * @private + */ +Blockly.Mutator.prototype.workspaceChanged_ = function() { + if (Blockly.dragMode_ == Blockly.DRAG_NONE) { + var blocks = this.workspace_.getTopBlocks(false); + var MARGIN = 20; + for (var b = 0, block; block = blocks[b]; b++) { + var blockXY = block.getRelativeToSurfaceXY(); + var blockHW = block.getHeightWidth(); + if (blockXY.y + blockHW.height < MARGIN) { + // Bump any block that's above the top back inside. + block.moveBy(0, MARGIN - blockHW.height - blockXY.y); + } + } + } + + // When the mutator's workspace changes, update the source block. + if (this.rootBlock_.workspace == this.workspace_) { + Blockly.Events.setGroup(true); + var block = this.block_; + var oldMutationDom = block.mutationToDom(); + var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + // Switch off rendering while the source block is rebuilt. + var savedRendered = block.rendered; + block.rendered = false; + // Allow the source block to rebuild itself. + block.compose(this.rootBlock_); + // Restore rendering and show the changes. + block.rendered = savedRendered; + // Mutation may have added some elements that need initalizing. + block.initSvg(); + var newMutationDom = block.mutationToDom(); + var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); + if (oldMutation != newMutation) { + Blockly.Events.fire(new Blockly.Events.Change( + block, 'mutation', null, oldMutation, newMutation)); + // Ensure that any bump is part of this mutation's event group. + var group = Blockly.Events.getGroup(); + setTimeout(function() { + Blockly.Events.setGroup(group); + block.bumpNeighbours_(); + Blockly.Events.setGroup(false); + }, Blockly.BUMP_DELAY); + } + if (block.rendered) { + block.render(); + } + this.resizeBubble_(); + Blockly.Events.setGroup(false); + } +}; + +/** + * Return an object with all the metrics required to size scrollbars for the + * mutator flyout. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .absoluteTop: Top-edge of view. + * .absoluteLeft: Left-edge of view. + * @return {!Object} Contains size and position metrics of mutator dialog's + * workspace. + * @private + */ +Blockly.Mutator.prototype.getFlyoutMetrics_ = function() { + return { + viewHeight: this.workspaceHeight_, + viewWidth: this.workspaceWidth_, + absoluteTop: 0, + absoluteLeft: 0 + }; +}; + +/** + * Dispose of this mutator. + */ +Blockly.Mutator.prototype.dispose = function() { + this.block_.mutator = null; + Blockly.Icon.prototype.dispose.call(this); +}; + +/** + * Reconnect an block to a mutated input. + * @param {Blockly.Connection} connectionChild Connection on child block. + * @param {!Blockly.Block} block Parent block. + * @param {string} inputName Name of input on parent block. + * @return {boolean} True iff a reconnection was made, false otherwise. + */ +Blockly.Mutator.reconnect = function(connectionChild, block, inputName) { + if (!connectionChild || !connectionChild.getSourceBlock().workspace) { + return false; // No connection or block has been deleted. + } + var connectionParent = block.getInput(inputName).connection; + var currentParent = connectionChild.targetBlock(); + if ((!currentParent || currentParent == block) && + connectionParent.targetConnection != connectionChild) { + if (connectionParent.isConnected()) { + // There's already something connected here. Get rid of it. + connectionParent.disconnect(); + } + connectionParent.connect(connectionChild); + return true; + } + return false; +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +if (!goog.global['Blockly']['Mutator']) { + goog.global['Blockly']['Mutator'] = {}; +} +goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect; diff --git a/core/mutator.js.rej b/core/mutator.js.rej new file mode 100644 index 000000000..68c9f5e4c --- /dev/null +++ b/core/mutator.js.rej @@ -0,0 +1,63 @@ +*************** +*** 80,86 **** + {'class': 'blocklyIconMark', + 'x': Blockly.Icon.RADIUS, + 'y': 2 * Blockly.Icon.RADIUS - 4}, this.iconGroup_); +- this.iconMark_.appendChild(document.createTextNode('\u2605')); + }; + + /** +--- 80,87 ---- + {'class': 'blocklyIconMark', + 'x': Blockly.Icon.RADIUS, + 'y': 2 * Blockly.Icon.RADIUS - 4}, this.iconGroup_); ++ this.iconMark_.appendChild(document.createTextNode('\u2699')); ++ //this.iconMark_.appendChild(document.createTextNode('\u2605')); + }; + + /** +*************** +*** 122,127 **** + this.flyout_.autoClose = false; + this.svgDialog_.appendChild(this.flyout_.createDom()); + this.svgDialog_.appendChild(this.workspace_.createDom()); + return this.svgDialog_; + }; + +--- 123,136 ---- + this.flyout_.autoClose = false; + this.svgDialog_.appendChild(this.flyout_.createDom()); + this.svgDialog_.appendChild(this.workspace_.createDom()); ++ ++ //when mutator bubble is clicked, do not close mutator ++ Blockly.bindEvent_(this.svgDialog_, 'mousedown', this.svgDialog_, ++ function(e) { ++ e.preventDefault(); ++ e.stopPropagation(); ++ }); ++ + return this.svgDialog_; + }; + +*************** +*** 147,153 **** + */ + Blockly.Mutator.prototype.resizeBubble_ = function() { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; +- var workspaceSize = this.workspace_.getCanvas().getBBox(); + var flyoutMetrics = this.flyout_.getMetrics_(); + var width; + if (Blockly.RTL) { +--- 156,167 ---- + */ + Blockly.Mutator.prototype.resizeBubble_ = function() { + var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH; ++ try { ++ var workspaceSize = this.workspace_.getCanvas().getBBox(); ++ } catch (e) { ++ // Firefox has trouble with hidden elements (Bug 528969). ++ return; ++ } + var flyoutMetrics = this.flyout_.getMetrics_(); + var width; + if (Blockly.RTL) { diff --git a/core/procedures.js.orig b/core/procedures.js.orig new file mode 100644 index 000000000..beb4a17b8 --- /dev/null +++ b/core/procedures.js.orig @@ -0,0 +1,287 @@ +/** + * @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 Utility functions for handling procedures. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Procedures'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Field'); +goog.require('Blockly.Names'); +goog.require('Blockly.Workspace'); + + +/** + * Category to separate procedure names from variables and generated functions. + */ +Blockly.Procedures.NAME_TYPE = 'PROCEDURE'; + +/** + * Find all user-created procedure definitions in a workspace. + * @param {!Blockly.Workspace} root Root workspace. + * @return {!Array.>} Pair of arrays, the + * first contains procedures without return variables, the second with. + * Each procedure is defined by a three-element list of name, parameter + * list, and return value boolean. + */ +Blockly.Procedures.allProcedures = function(root) { + var blocks = root.getAllBlocks(); + var proceduresReturn = []; + var proceduresNoReturn = []; + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureDef) { + var tuple = blocks[i].getProcedureDef(); + if (tuple) { + if (tuple[2]) { + proceduresReturn.push(tuple); + } else { + proceduresNoReturn.push(tuple); + } + } + } + } + proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_); + proceduresReturn.sort(Blockly.Procedures.procTupleComparator_); + return [proceduresNoReturn, proceduresReturn]; +}; + +/** + * Comparison function for case-insensitive sorting of the first element of + * a tuple. + * @param {!Array} ta First tuple. + * @param {!Array} tb Second tuple. + * @return {number} -1, 0, or 1 to signify greater than, equality, or less than. + * @private + */ +Blockly.Procedures.procTupleComparator_ = function(ta, tb) { + return ta[0].toLowerCase().localeCompare(tb[0].toLowerCase()); +}; + +/** + * Ensure two identically-named procedures don't exist. + * @param {string} name Proposed procedure name. + * @param {!Blockly.Block} block Block to disambiguate. + * @return {string} Non-colliding name. + */ +Blockly.Procedures.findLegalName = function(name, block) { + if (block.isInFlyout) { + // Flyouts can have multiple procedures called 'do something'. + return name; + } + while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) { + // Collision with another procedure. + var r = name.match(/^(.*?)(\d+)$/); + if (!r) { + name += '2'; + } else { + name = r[1] + (parseInt(r[2], 10) + 1); + } + } + return name; +}; + +/** + * Does this procedure have a legal name? Illegal names include names of + * procedures already defined. + * @param {string} name The questionable name. + * @param {!Blockly.Workspace} workspace The workspace to scan for collisions. + * @param {Blockly.Block=} opt_exclude Optional block to exclude from + * comparisons (one doesn't want to collide with oneself). + * @return {boolean} True if the name is legal. + * @private + */ +Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) { + var blocks = workspace.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + if (blocks[i] == opt_exclude) { + continue; + } + if (blocks[i].getProcedureDef) { + var procName = blocks[i].getProcedureDef(); + if (Blockly.Names.equals(procName[0], name)) { + return false; + } + } + } + return true; +}; + +/** + * Rename a procedure. Called by the editable field. + * @param {string} name The proposed new name. + * @return {string} The accepted name. + * @this {!Blockly.Field} + */ +Blockly.Procedures.rename = function(name) { + // Strip leading and trailing whitespace. Beyond this, all names are legal. + name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); + + // Ensure two identically-named procedures don't exist. + var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_); + var oldName = this.text_; + if (oldName != name && oldName != legalName) { + // Rename any callers. + var blocks = this.sourceBlock_.workspace.getAllBlocks(); + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].renameProcedure) { + blocks[i].renameProcedure(oldName, legalName); + } + } + } + return legalName; +}; + +/** + * Construct the blocks required by the flyout for the procedure category. + * @param {!Blockly.Workspace} workspace The workspace contianing procedures. + * @return {!Array.} Array of XML block elements. + */ +Blockly.Procedures.flyoutCategory = function(workspace) { + var xmlList = []; + if (Blockly.Blocks['procedures_defnoreturn']) { + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_defnoreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (Blockly.Blocks['procedures_defreturn']) { + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_defreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (Blockly.Blocks['procedures_ifreturn']) { + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'procedures_ifreturn'); + block.setAttribute('gap', 16); + xmlList.push(block); + } + if (xmlList.length) { + // Add slightly larger gap between system blocks and user calls. + xmlList[xmlList.length - 1].setAttribute('gap', 24); + } + + function populateProcedures(procedureList, templateName) { + for (var i = 0; i < procedureList.length; i++) { + var name = procedureList[i][0]; + var args = procedureList[i][1]; + // + // + // + // + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', templateName); + block.setAttribute('gap', 16); + var mutation = goog.dom.createDom('mutation'); + mutation.setAttribute('name', name); + block.appendChild(mutation); + for (var j = 0; j < args.length; j++) { + var arg = goog.dom.createDom('arg'); + arg.setAttribute('name', args[j]); + mutation.appendChild(arg); + } + xmlList.push(block); + } + } + + var tuple = Blockly.Procedures.allProcedures(workspace); + populateProcedures(tuple[0], 'procedures_callnoreturn'); + populateProcedures(tuple[1], 'procedures_callreturn'); + return xmlList; +}; + +/** + * Find all the callers of a named procedure. + * @param {string} name Name of procedure. + * @param {!Blockly.Workspace} workspace The workspace to find callers in. + * @return {!Array.} Array of caller blocks. + */ +Blockly.Procedures.getCallers = function(name, workspace) { + var callers = []; + var blocks = workspace.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureCall) { + var procName = blocks[i].getProcedureCall(); + // Procedure name may be null if the block is only half-built. + if (procName && Blockly.Names.equals(procName, name)) { + callers.push(blocks[i]); + } + } + } + return callers; +}; + +/** + * When a procedure definition changes its parameters, find and edit all its + * callers. + * @param {!Blockly.Block} defBlock Procedure definition block. + */ +Blockly.Procedures.mutateCallers = function(defBlock) { + var oldRecordUndo = Blockly.Events.recordUndo; + var name = defBlock.getProcedureDef()[0]; + var xmlElement = defBlock.mutationToDom(true); + var callers = Blockly.Procedures.getCallers(name, defBlock.workspace); + for (var i = 0, caller; caller = callers[i]; i++) { + var oldMutationDom = caller.mutationToDom(); + var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + caller.domToMutation(xmlElement); + var newMutationDom = caller.mutationToDom(); + var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); + if (oldMutation != newMutation) { + // Fire a mutation on every caller block. But don't record this as an + // undo action since it is deterministically tied to the procedure's + // definition mutation. + Blockly.Events.recordUndo = false; + Blockly.Events.fire(new Blockly.Events.Change( + caller, 'mutation', null, oldMutation, newMutation)); + Blockly.Events.recordUndo = oldRecordUndo; + } + } +}; + +/** + * Find the definition block for the named procedure. + * @param {string} name Name of procedure. + * @param {!Blockly.Workspace} workspace The workspace to search. + * @return {Blockly.Block} The procedure definition block, or null not found. + */ +Blockly.Procedures.getDefinition = function(name, workspace) { + // Assume that a procedure definition is a top block. + var blocks = workspace.getTopBlocks(false); + for (var i = 0; i < blocks.length; i++) { + if (blocks[i].getProcedureDef) { + var tuple = blocks[i].getProcedureDef(); + if (tuple && Blockly.Names.equals(tuple[0], name)) { + return blocks[i]; + } + } + } + return null; +}; diff --git a/core/procedures.js.rej b/core/procedures.js.rej new file mode 100644 index 000000000..5d1a97784 --- /dev/null +++ b/core/procedures.js.rej @@ -0,0 +1,46 @@ +*************** +*** 99,104 **** + // Flyouts can have multiple procedures called 'procedure'. + return name; + } + while (!Blockly.Procedures.isLegalName(name, block.workspace, block)) { + // Collision with another procedure. + var r = name.match(/^(.*?)(\d+)$/); +--- 99,105 ---- + // Flyouts can have multiple procedures called 'procedure'. + return name; + } ++ name = name.replace(/\s+/g, ''); + while (!Blockly.Procedures.isLegalName(name, block.workspace, block)) { + // Collision with another procedure. + var r = name.match(/^(.*?)(\d+)$/); +*************** +*** 255,266 **** + * @param {!Blockly.Workspace} workspace The workspace to delete callers from. + * @param {!Array.} paramNames Array of new parameter names. + * @param {!Array.} paramIds Array of unique parameter IDs. + */ + Blockly.Procedures.mutateCallers = function(name, workspace, +- paramNames, paramIds) { + var callers = Blockly.Procedures.getCallers(name, workspace); + for (var x = 0; x < callers.length; x++) { +- callers[x].setProcedureParameters(paramNames, paramIds); + } + }; + +--- 256,270 ---- + * @param {!Blockly.Workspace} workspace The workspace to delete callers from. + * @param {!Array.} paramNames Array of new parameter names. + * @param {!Array.} paramIds Array of unique parameter IDs. ++ * @param {boolean} opt_intializeTracking indicate whether paramId tracking should start ++ * Is undefined (falsey) as default. // [lyn, 10/26/13] added this. + */ + Blockly.Procedures.mutateCallers = function(name, workspace, ++ paramNames, paramIds, ++ opt_initializeTracking) { + var callers = Blockly.Procedures.getCallers(name, workspace); + for (var x = 0; x < callers.length; x++) { ++ callers[x].setProcedureParameters(paramNames, paramIds, opt_initializeTracking); + } + }; + diff --git a/core/scrollbar.js b/core/scrollbar.js index da2dd942d..9df81a34b 100644 --- a/core/scrollbar.js +++ b/core/scrollbar.js @@ -74,6 +74,7 @@ Blockly.ScrollbarPair.prototype.dispose = function() { * Also reposition the corner rectangle. */ Blockly.ScrollbarPair.prototype.resize = function() { + var start = new Date().getTime(); // Look up the host metrics once, and use for both scrollbars. var hostMetrics = this.workspace_.getMetrics(); if (!hostMetrics) { @@ -125,9 +126,12 @@ Blockly.ScrollbarPair.prototype.resize = function() { this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) { this.corner_.setAttribute('y', this.hScroll.position_.y); } - // Cache the current metrics to potentially short-cut the next resize event. this.oldHostMetrics_ = hostMetrics; + var stop = new Date().getTime(); + var timeDiff = stop - start; + Blockly.Instrument.stats.scrollBarResizeCalls++; //***lyn + Blockly.Instrument.stats.scrollBarResizeTime += timeDiff; //***lyn }; /** diff --git a/core/scrollbar.js.orig b/core/scrollbar.js.orig new file mode 100644 index 000000000..da2dd942d --- /dev/null +++ b/core/scrollbar.js.orig @@ -0,0 +1,750 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 Library for creating scrollbars. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Scrollbar'); +goog.provide('Blockly.ScrollbarPair'); + +goog.require('goog.dom'); +goog.require('goog.events'); + + +/** + * Class for a pair of scrollbars. Horizontal and vertical. + * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbars to. + * @constructor + */ +Blockly.ScrollbarPair = function(workspace) { + this.workspace_ = workspace; + this.hScroll = new Blockly.Scrollbar(workspace, true, true); + this.vScroll = new Blockly.Scrollbar(workspace, false, true); + this.corner_ = Blockly.createSvgElement('rect', + {'height': Blockly.Scrollbar.scrollbarThickness, + 'width': Blockly.Scrollbar.scrollbarThickness, + 'class': 'blocklyScrollbarBackground'}, null); + Blockly.Scrollbar.insertAfter_(this.corner_, workspace.getBubbleCanvas()); +}; + +/** + * Previously recorded metrics from the workspace. + * @type {Object} + * @private + */ +Blockly.ScrollbarPair.prototype.oldHostMetrics_ = null; + +/** + * Dispose of this pair of scrollbars. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.ScrollbarPair.prototype.dispose = function() { + goog.dom.removeNode(this.corner_); + this.corner_ = null; + this.workspace_ = null; + this.oldHostMetrics_ = null; + this.hScroll.dispose(); + this.hScroll = null; + this.vScroll.dispose(); + this.vScroll = null; +}; + +/** + * Recalculate both of the scrollbars' locations and lengths. + * Also reposition the corner rectangle. + */ +Blockly.ScrollbarPair.prototype.resize = function() { + // Look up the host metrics once, and use for both scrollbars. + var hostMetrics = this.workspace_.getMetrics(); + if (!hostMetrics) { + // Host element is likely not visible. + return; + } + + // Only change the scrollbars if there has been a change in metrics. + var resizeH = false; + var resizeV = false; + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth || + this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight || + this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop || + this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) { + // The window has been resized or repositioned. + resizeH = true; + resizeV = true; + } else { + // Has the content been resized or moved? + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.contentWidth != hostMetrics.contentWidth || + this.oldHostMetrics_.viewLeft != hostMetrics.viewLeft || + this.oldHostMetrics_.contentLeft != hostMetrics.contentLeft) { + resizeH = true; + } + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.contentHeight != hostMetrics.contentHeight || + this.oldHostMetrics_.viewTop != hostMetrics.viewTop || + this.oldHostMetrics_.contentTop != hostMetrics.contentTop) { + resizeV = true; + } + } + if (resizeH) { + this.hScroll.resize(hostMetrics); + } + if (resizeV) { + this.vScroll.resize(hostMetrics); + } + + // Reposition the corner square. + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth || + this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) { + this.corner_.setAttribute('x', this.vScroll.position_.x); + } + if (!this.oldHostMetrics_ || + this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight || + this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) { + this.corner_.setAttribute('y', this.hScroll.position_.y); + } + + // Cache the current metrics to potentially short-cut the next resize event. + this.oldHostMetrics_ = hostMetrics; +}; + +/** + * Set the sliders of both scrollbars to be at a certain position. + * @param {number} x Horizontal scroll value. + * @param {number} y Vertical scroll value. + */ +Blockly.ScrollbarPair.prototype.set = function(x, y) { + // This function is equivalent to: + // this.hScroll.set(x); + // this.vScroll.set(y); + // However, that calls setMetrics twice which causes a chain of + // getAttribute->setAttribute->getAttribute resulting in an extra layout pass. + // Combining them speeds up rendering. + var xyRatio = {}; + + var hHandlePosition = x * this.hScroll.ratio_; + var vHandlePosition = y * this.vScroll.ratio_; + + var hBarLength = this.hScroll.scrollViewSize_; + var vBarLength = this.vScroll.scrollViewSize_; + + xyRatio.x = this.getRatio_(hHandlePosition, hBarLength); + xyRatio.y = this.getRatio_(vHandlePosition, vBarLength); + this.workspace_.setMetrics(xyRatio); + + this.hScroll.setHandlePosition(hHandlePosition); + this.vScroll.setHandlePosition(vHandlePosition); +}; + +/** + * Helper to calculate the ratio of handle position to scrollbar view size. + * @param {number} handlePosition The value of the handle. + * @param {number} viewSize The total size of the scrollbar's view. + * @return {number} Ratio. + * @private + */ +Blockly.ScrollbarPair.prototype.getRatio_ = function(handlePosition, viewSize) { + var ratio = handlePosition / viewSize; + if (isNaN(ratio)) { + return 0; + } + return ratio; +}; + +// -------------------------------------------------------------------- + +/** + * Class for a pure SVG scrollbar. + * This technique offers a scrollbar that is guaranteed to work, but may not + * look or behave like the system's scrollbars. + * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to. + * @param {boolean} horizontal True if horizontal, false if vertical. + * @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair. + * @constructor + */ +Blockly.Scrollbar = function(workspace, horizontal, opt_pair) { + this.workspace_ = workspace; + this.pair_ = opt_pair || false; + this.horizontal_ = horizontal; + this.oldHostMetrics_ = null; + + this.createDom_(); + + /** + * The upper left corner of the scrollbar's svg group. + * @type {goog.math.Coordinate} + * @private + */ + this.position_ = new goog.math.Coordinate(0, 0); + + if (horizontal) { + this.svgBackground_.setAttribute('height', + Blockly.Scrollbar.scrollbarThickness); + this.svgHandle_.setAttribute('height', + Blockly.Scrollbar.scrollbarThickness - 5); + this.svgHandle_.setAttribute('y', 2.5); + + this.lengthAttribute_ = 'width'; + this.positionAttribute_ = 'x'; + } else { + this.svgBackground_.setAttribute('width', + Blockly.Scrollbar.scrollbarThickness); + this.svgHandle_.setAttribute('width', + Blockly.Scrollbar.scrollbarThickness - 5); + this.svgHandle_.setAttribute('x', 2.5); + + this.lengthAttribute_ = 'height'; + this.positionAttribute_ = 'y'; + } + var scrollbar = this; + this.onMouseDownBarWrapper_ = Blockly.bindEvent_(this.svgBackground_, + 'mousedown', scrollbar, scrollbar.onMouseDownBar_); + this.onMouseDownHandleWrapper_ = Blockly.bindEvent_(this.svgHandle_, + 'mousedown', scrollbar, scrollbar.onMouseDownHandle_); +}; + +/** + * The size of the area within which the scrollbar handle can move. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.scrollViewSize_ = 0; + +/** + * The length of the scrollbar handle. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.handleLength_ = 0; + +/** + * The offset of the start of the handle from the start of the scrollbar range. + * @type {number} + * @private + */ +Blockly.Scrollbar.prototype.handlePosition_ = 0; + +/** + * Whether the scrollbar handle is visible. + * @type {boolean} + * @private + */ +Blockly.Scrollbar.prototype.isVisible_ = true; + +/** + * Width of vertical scrollbar or height of horizontal scrollbar. + * Increase the size of scrollbars on touch devices. + * Don't define if there is no document object (e.g. node.js). + */ +Blockly.Scrollbar.scrollbarThickness = 15; +if (goog.events.BrowserFeature.TOUCH_ENABLED) { + Blockly.Scrollbar.scrollbarThickness = 25; +} + +/** + * @param {!Object} first An object containing computed measurements of a + * workspace. + * @param {!Object} second Another object containing computed measurements of a + * workspace. + * @return {boolean} Whether the two sets of metrics are equivalent. + * @private + */ +Blockly.Scrollbar.metricsAreEquivalent_ = function(first, second) { + if (!(first && second)) { + return false; + } + + if (first.viewWidth != second.viewWidth || + first.viewHeight != second.viewHeight || + first.viewLeft != second.viewLeft || + first.viewTop != second.viewTop || + first.absoluteTop != second.absoluteTop || + first.absoluteLeft != second.absoluteLeft || + first.contentWidth != second.contentWidth || + first.contentHeight != second.contentHeight || + first.contentLeft != second.contentLeft || + first.contentTop != second.contentTop) { + return false; + } + + return true; +}; + +/** + * Dispose of this scrollbar. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Scrollbar.prototype.dispose = function() { + this.onMouseUpHandle_(); + Blockly.unbindEvent_(this.onMouseDownBarWrapper_); + this.onMouseDownBarWrapper_ = null; + Blockly.unbindEvent_(this.onMouseDownHandleWrapper_); + this.onMouseDownHandleWrapper_ = null; + + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + this.svgBackground_ = null; + this.svgHandle_ = null; + this.workspace_ = null; +}; + +/** + * Set the length of the scrollbar's handle and change the SVG attribute + * accordingly. + * @param {number} newLength The new scrollbar handle length. + */ +Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) { + this.handleLength_ = newLength; + this.svgHandle_.setAttribute(this.lengthAttribute_, this.handleLength_); +}; + +/** + * Set the offset of the scrollbar's handle and change the SVG attribute + * accordingly. + * @param {number} newPosition The new scrollbar handle offset. + */ +Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) { + this.handlePosition_ = newPosition; + this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_); +}; + +/** + * Set the size of the scrollbar's background and change the SVG attribute + * accordingly. + * @param {number} newSize The new scrollbar background length. + * @private + */ +Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) { + this.scrollViewSize_ = newSize; + this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_); +}; + +/** + * Set the position of the scrollbar's svg group. + * @param {number} x The new x coordinate. + * @param {number} y The new y coordinate. + */ +Blockly.Scrollbar.prototype.setPosition = function(x, y) { + this.position_.x = x; + this.position_.y = y; + + this.svgGroup_.setAttribute('transform', + 'translate(' + this.position_.x + ',' + this.position_.y + ')'); +}; + +/** + * Recalculate the scrollbar's location and its length. + * @param {Object=} opt_metrics A data structure of from the describing all the + * required dimensions. If not provided, it will be fetched from the host + * object. + */ +Blockly.Scrollbar.prototype.resize = function(opt_metrics) { + // Determine the location, height and width of the host element. + var hostMetrics = opt_metrics; + if (!hostMetrics) { + hostMetrics = this.workspace_.getMetrics(); + if (!hostMetrics) { + // Host element is likely not visible. + return; + } + } + + if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics, + this.oldHostMetrics_)) { + return; + } + this.oldHostMetrics_ = hostMetrics; + + /* hostMetrics is an object with the following properties. + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the content, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .viewLeft: Offset of left edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .absoluteTop: Top-edge of view. + * .absoluteLeft: Left-edge of view. + */ + if (this.horizontal_) { + this.resizeHorizontal_(hostMetrics); + } else { + this.resizeVertical_(hostMetrics); + } + // Resizing may have caused some scrolling. + this.onScroll_(); +}; + +/** + * Recalculate a horizontal scrollbar's location and length. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + * @private + */ +Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) { + // TODO: Inspect metrics to determine if we can get away with just a content + // resize. + this.resizeViewHorizontal(hostMetrics); +}; + +/** + * Recalculate a horizontal scrollbar's location on the screen and path length. + * This should be called when the layout or size of the window has changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { + var viewSize = hostMetrics.viewWidth - 1; + if (this.pair_) { + // Shorten the scrollbar to make room for the corner square. + viewSize -= Blockly.Scrollbar.scrollbarThickness; + } + this.setScrollViewSize_(Math.max(0, viewSize)); + + var xCoordinate = hostMetrics.absoluteLeft + 0.5; + if (this.pair_ && this.workspace_.RTL) { + xCoordinate += Blockly.Scrollbar.scrollbarThickness; + } + + // Horizontal toolbar should always be just above the bottom of the workspace. + var yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight - + Blockly.Scrollbar.scrollbarThickness - 0.5; + this.setPosition(xCoordinate, yCoordinate); + + // If the view has been resized, a content resize will also be necessary. The + // reverse is not true. + this.resizeContentHorizontal(hostMetrics); +}; + +/** + * Recalculate a horizontal scrollbar's location within its path and length. + * This should be called when the contents of the workspace have changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { + if (!this.pair_) { + // Only show the scrollbar if needed. + // Ideally this would also apply to scrollbar pairs, but that's a bigger + // headache (due to interactions with the corner square). + this.setVisible(this.scrollViewSize_ < hostMetrics.contentWidth); + } + + this.ratio_ = this.scrollViewSize_ / hostMetrics.contentWidth; + if (this.ratio_ == -Infinity || this.ratio_ == Infinity || + isNaN(this.ratio_)) { + this.ratio_ = 0; + } + + var handleLength = hostMetrics.viewWidth * this.ratio_; + this.setHandleLength_(Math.max(0, handleLength)); + + var handlePosition = (hostMetrics.viewLeft - hostMetrics.contentLeft) * + this.ratio_; + this.setHandlePosition(this.constrainHandle_(handlePosition)); +}; + +/** + * Recalculate a vertical scrollbar's location and length. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + * @private + */ +Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) { + // TODO: Inspect metrics to determine if we can get away with just a content + // resize. + this.resizeViewVertical(hostMetrics); +}; + +/** + * Recalculate a vertical scrollbar's location on the screen and path length. + * This should be called when the layout or size of the window has changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { + var viewSize = hostMetrics.viewHeight - 1; + if (this.pair_) { + // Shorten the scrollbar to make room for the corner square. + viewSize -= Blockly.Scrollbar.scrollbarThickness; + } + this.setScrollViewSize_(Math.max(0, viewSize)); + + var xCoordinate = hostMetrics.absoluteLeft + 0.5; + if (!this.workspace_.RTL) { + xCoordinate += hostMetrics.viewWidth - + Blockly.Scrollbar.scrollbarThickness - 1; + } + var yCoordinate = hostMetrics.absoluteTop + 0.5; + this.setPosition(xCoordinate, yCoordinate); + + // If the view has been resized, a content resize will also be necessary. The + // reverse is not true. + this.resizeContentVertical(hostMetrics); +}; + +/** + * Recalculate a vertical scrollbar's location within its path and length. + * This should be called when the contents of the workspace have changed. + * @param {!Object} hostMetrics A data structure describing all the + * required dimensions, possibly fetched from the host object. + */ +Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { + if (!this.pair_) { + // Only show the scrollbar if needed. + this.setVisible(this.scrollViewSize_ < hostMetrics.contentHeight); + } + + this.ratio_ = this.scrollViewSize_ / hostMetrics.contentHeight; + if (this.ratio_ == -Infinity || this.ratio_ == Infinity || + isNaN(this.ratio_)) { + this.ratio_ = 0; + } + + var handleLength = hostMetrics.viewHeight * this.ratio_; + this.setHandleLength_(Math.max(0, handleLength)); + + var handlePosition = (hostMetrics.viewTop - hostMetrics.contentTop) * + this.ratio_; + this.setHandlePosition(this.constrainHandle_(handlePosition)); +}; + +/** + * Create all the DOM elements required for a scrollbar. + * The resulting widget is not sized. + * @private + */ +Blockly.Scrollbar.prototype.createDom_ = function() { + /* Create the following DOM: + + + + + */ + var className = 'blocklyScrollbar' + + (this.horizontal_ ? 'Horizontal' : 'Vertical'); + this.svgGroup_ = Blockly.createSvgElement('g', {'class': className}, null); + this.svgBackground_ = Blockly.createSvgElement('rect', + {'class': 'blocklyScrollbarBackground'}, this.svgGroup_); + var radius = Math.floor((Blockly.Scrollbar.scrollbarThickness - 5) / 2); + this.svgHandle_ = Blockly.createSvgElement('rect', + {'class': 'blocklyScrollbarHandle', 'rx': radius, 'ry': radius}, + this.svgGroup_); + Blockly.Scrollbar.insertAfter_(this.svgGroup_, + this.workspace_.getBubbleCanvas()); +}; + +/** + * Is the scrollbar visible. Non-paired scrollbars disappear when they aren't + * needed. + * @return {boolean} True if visible. + */ +Blockly.Scrollbar.prototype.isVisible = function() { + return this.isVisible_; +}; + +/** + * Set whether the scrollbar is visible. + * Only applies to non-paired scrollbars. + * @param {boolean} visible True if visible. + */ +Blockly.Scrollbar.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + return; + } + // Ideally this would also apply to scrollbar pairs, but that's a bigger + // headache (due to interactions with the corner square). + if (this.pair_) { + throw 'Unable to toggle visibility of paired scrollbars.'; + } + + this.isVisible_ = visible; + + if (visible) { + this.svgGroup_.setAttribute('display', 'block'); + } else { + // Hide the scrollbar. + this.workspace_.setMetrics({x: 0, y: 0}); + this.svgGroup_.setAttribute('display', 'none'); + } +}; + +/** + * Scroll by one pageful. + * Called when scrollbar background is clicked. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) { + this.onMouseUpHandle_(); + if (Blockly.isRightButton(e)) { + // Right-click. + // Scrollbars have no context menu. + e.stopPropagation(); + return; + } + var mouseXY = Blockly.mouseToSvg(e, this.workspace_.getParentSvg(), + this.workspace_.getInverseScreenCTM()); + var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y; + + var handleXY = Blockly.getSvgXY_(this.svgHandle_, this.workspace_); + var handleStart = this.horizontal_ ? handleXY.x : handleXY.y; + var handlePosition = this.handlePosition_; + + var pageLength = this.handleLength_ * 0.95; + if (mouseLocation <= handleStart) { + // Decrease the scrollbar's value by a page. + handlePosition -= pageLength; + } else if (mouseLocation >= handleStart + this.handleLength_) { + // Increase the scrollbar's value by a page. + handlePosition += pageLength; + } + + this.setHandlePosition(this.constrainHandle_(handlePosition)); + + this.onScroll_(); + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Start a dragging operation. + * Called when scrollbar handle is clicked. + * @param {!Event} e Mouse down event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) { + this.onMouseUpHandle_(); + if (Blockly.isRightButton(e)) { + // Right-click. + // Scrollbars have no context menu. + e.stopPropagation(); + return; + } + // Look up the current translation and record it. + this.startDragHandle = this.handlePosition_; + // Record the current mouse position. + this.startDragMouse = this.horizontal_ ? e.clientX : e.clientY; + Blockly.Scrollbar.onMouseUpWrapper_ = Blockly.bindEvent_(document, + 'mouseup', this, this.onMouseUpHandle_); + Blockly.Scrollbar.onMouseMoveWrapper_ = Blockly.bindEvent_(document, + 'mousemove', this, this.onMouseMoveHandle_); + e.stopPropagation(); + e.preventDefault(); +}; + +/** + * Drag the scrollbar's handle. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) { + var currentMouse = this.horizontal_ ? e.clientX : e.clientY; + var mouseDelta = currentMouse - this.startDragMouse; + var handlePosition = this.startDragHandle + mouseDelta; + // Position the bar. + this.setHandlePosition(this.constrainHandle_(handlePosition)); + this.onScroll_(); +}; + +/** + * Stop binding to the global mouseup and mousemove events. + * @private + */ +Blockly.Scrollbar.prototype.onMouseUpHandle_ = function() { + Blockly.hideChaff(true); + if (Blockly.Scrollbar.onMouseUpWrapper_) { + Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_); + Blockly.Scrollbar.onMouseUpWrapper_ = null; + } + if (Blockly.Scrollbar.onMouseMoveWrapper_) { + Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_); + Blockly.Scrollbar.onMouseMoveWrapper_ = null; + } +}; + +/** + * Constrain the handle's position within the minimum (0) and maximum + * (length of scrollbar) values allowed for the scrollbar. + * @param {number} value Value that is potentially out of bounds. + * @return {number} Constrained value. + * @private + */ +Blockly.Scrollbar.prototype.constrainHandle_ = function(value) { + if (value <= 0 || isNaN(value) || this.scrollViewSize_ < this.handleLength_) { + value = 0; + } else { + value = Math.min(value, this.scrollViewSize_ - this.handleLength_); + } + return value; +}; + +/** + * Called when scrollbar is moved. + * @private + */ +Blockly.Scrollbar.prototype.onScroll_ = function() { + var ratio = this.handlePosition_ / this.scrollViewSize_; + if (isNaN(ratio)) { + ratio = 0; + } + var xyRatio = {}; + if (this.horizontal_) { + xyRatio.x = ratio; + } else { + xyRatio.y = ratio; + } + this.workspace_.setMetrics(xyRatio); +}; + +/** + * Set the scrollbar slider's position. + * @param {number} value The distance from the top/left end of the bar. + */ +Blockly.Scrollbar.prototype.set = function(value) { + this.setHandlePosition(this.constrainHandle_(value * this.ratio_)); + this.onScroll_(); +}; + +/** + * Insert a node after a reference node. + * Contrast with node.insertBefore function. + * @param {!Element} newNode New element to insert. + * @param {!Element} refNode Existing element to precede new node. + * @private + */ +Blockly.Scrollbar.insertAfter_ = function(newNode, refNode) { + var siblingNode = refNode.nextSibling; + var parentNode = refNode.parentNode; + if (!parentNode) { + throw 'Reference node has no parent.'; + } + if (siblingNode) { + parentNode.insertBefore(newNode, siblingNode); + } else { + parentNode.appendChild(newNode); + } +}; diff --git a/core/scrollbar.js.rej b/core/scrollbar.js.rej new file mode 100644 index 000000000..cd2d0eda8 --- /dev/null +++ b/core/scrollbar.js.rej @@ -0,0 +1,16 @@ +*************** +*** 27,32 **** + goog.provide('Blockly.Scrollbar'); + goog.provide('Blockly.ScrollbarPair'); + + goog.require('goog.userAgent'); + + +--- 27,33 ---- + goog.provide('Blockly.Scrollbar'); + goog.provide('Blockly.ScrollbarPair'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code + goog.require('goog.userAgent'); + + diff --git a/core/trashcan.js b/core/trashcan.js index 28baa0f20..30444c8f4 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -26,6 +26,7 @@ goog.provide('Blockly.Trashcan'); +goog.require('Blockly.Instrument'); // lyn's instrumentation code goog.require('goog.Timer'); goog.require('goog.dom'); goog.require('goog.math'); @@ -258,6 +259,10 @@ Blockly.Trashcan.prototype.position = function() { } this.svgGroup_.setAttribute('transform', 'translate(' + this.left_ + ',' + this.top_ + ')'); + var stop = new Date().getTime(); + var timeDiff = stop - start; + Blockly.Instrument.stats.trashCanPositionCalls++; //***lyn + Blockly.Instrument.stats.trashCanPositionTime += timeDiff; //***lyn }; /** diff --git a/core/trashcan.js.orig b/core/trashcan.js.orig new file mode 100644 index 000000000..28baa0f20 --- /dev/null +++ b/core/trashcan.js.orig @@ -0,0 +1,332 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2011 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 representing a trash can icon. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Trashcan'); + +goog.require('goog.Timer'); +goog.require('goog.dom'); +goog.require('goog.math'); +goog.require('goog.math.Rect'); + + +/** + * Class for a trash can. + * @param {!Blockly.Workspace} workspace The workspace to sit in. + * @constructor + */ +Blockly.Trashcan = function(workspace) { + this.workspace_ = workspace; +}; + +/** + * Width of both the trash can and lid images. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.WIDTH_ = 47; + +/** + * Height of the trashcan image (minus lid). + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.BODY_HEIGHT_ = 44; + +/** + * Height of the lid image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.LID_HEIGHT_ = 16; + +/** + * Distance between trashcan and bottom edge of workspace. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_BOTTOM_ = 20; + +/** + * Distance between trashcan and right edge of workspace. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_SIDE_ = 20; + +/** + * Extent of hotspot on all sides beyond the size of the image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.MARGIN_HOTSPOT_ = 10; + +/** + * Location of trashcan in sprite image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.SPRITE_LEFT_ = 0; + +/** + * Location of trashcan in sprite image. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.SPRITE_TOP_ = 32; + +/** + * Current open/close state of the lid. + * @type {boolean} + */ +Blockly.Trashcan.prototype.isOpen = false; + +/** + * The SVG group containing the trash can. + * @type {Element} + * @private + */ +Blockly.Trashcan.prototype.svgGroup_ = null; + +/** + * The SVG image element of the trash can lid. + * @type {Element} + * @private + */ +Blockly.Trashcan.prototype.svgLid_ = null; + +/** + * Task ID of opening/closing animation. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.lidTask_ = 0; + +/** + * Current state of lid opening (0.0 = closed, 1.0 = open). + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.lidOpen_ = 0; + +/** + * Left coordinate of the trash can. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.left_ = 0; + +/** + * Top coordinate of the trash can. + * @type {number} + * @private + */ +Blockly.Trashcan.prototype.top_ = 0; + +/** + * Create the trash can elements. + * @return {!Element} The trash can's SVG group. + */ +Blockly.Trashcan.prototype.createDom = function() { + /* Here's the markup that will be generated: + + + + + + + + + + + */ + this.svgGroup_ = Blockly.createSvgElement('g', + {'class': 'blocklyTrash'}, null); + var rnd = String(Math.random()).substring(2); + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyTrashBodyClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': this.WIDTH_, 'height': this.BODY_HEIGHT_, + 'y': this.LID_HEIGHT_}, + clip); + var body = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_, + 'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_, + 'clip-path': 'url(#blocklyTrashBodyClipPath' + rnd + ')'}, + this.svgGroup_); + body.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + this.workspace_.options.pathToMedia + Blockly.SPRITE.url); + + var clip = Blockly.createSvgElement('clipPath', + {'id': 'blocklyTrashLidClipPath' + rnd}, + this.svgGroup_); + Blockly.createSvgElement('rect', + {'width': this.WIDTH_, 'height': this.LID_HEIGHT_}, clip); + this.svgLid_ = Blockly.createSvgElement('image', + {'width': Blockly.SPRITE.width, 'x': -this.SPRITE_LEFT_, + 'height': Blockly.SPRITE.height, 'y': -this.SPRITE_TOP_, + 'clip-path': 'url(#blocklyTrashLidClipPath' + rnd + ')'}, + this.svgGroup_); + this.svgLid_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', + this.workspace_.options.pathToMedia + Blockly.SPRITE.url); + + Blockly.bindEvent_(this.svgGroup_, 'mouseup', this, this.click); + this.animateLid_(); + return this.svgGroup_; +}; + +/** + * Initialize the trash can. + * @param {number} bottom Distance from workspace bottom to bottom of trashcan. + * @return {number} Distance from workspace bottom to the top of trashcan. + */ +Blockly.Trashcan.prototype.init = function(bottom) { + this.bottom_ = this.MARGIN_BOTTOM_ + bottom; + this.setOpen_(false); + return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_; +}; + +/** + * Dispose of this trash can. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Trashcan.prototype.dispose = function() { + if (this.svgGroup_) { + goog.dom.removeNode(this.svgGroup_); + this.svgGroup_ = null; + } + this.svgLid_ = null; + this.workspace_ = null; + goog.Timer.clear(this.lidTask_); +}; + +/** + * Move the trash can to the bottom-right corner. + */ +Blockly.Trashcan.prototype.position = function() { + var metrics = this.workspace_.getMetrics(); + if (!metrics) { + // There are no metrics available (workspace is probably not visible). + return; + } + if (this.workspace_.RTL) { + this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness; + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) { + this.left_ += metrics.flyoutWidth; + if (this.workspace_.toolbox_) { + this.left_ += metrics.absoluteLeft; + } + } + } else { + this.left_ = metrics.viewWidth + metrics.absoluteLeft - + this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness; + + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { + this.left_ -= metrics.flyoutWidth; + } + } + this.top_ = metrics.viewHeight + metrics.absoluteTop - + (this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.bottom_; + + if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { + this.top_ -= metrics.flyoutHeight; + } + this.svgGroup_.setAttribute('transform', + 'translate(' + this.left_ + ',' + this.top_ + ')'); +}; + +/** + * Return the deletion rectangle for this trash can. + * @return {goog.math.Rect} Rectangle in which to delete. + */ +Blockly.Trashcan.prototype.getClientRect = function() { + if (!this.svgGroup_) { + return null; + } + + var trashRect = this.svgGroup_.getBoundingClientRect(); + var left = trashRect.left + this.SPRITE_LEFT_ - this.MARGIN_HOTSPOT_; + var top = trashRect.top + this.SPRITE_TOP_ - this.MARGIN_HOTSPOT_; + var width = this.WIDTH_ + 2 * this.MARGIN_HOTSPOT_; + var height = this.LID_HEIGHT_ + this.BODY_HEIGHT_ + 2 * this.MARGIN_HOTSPOT_; + return new goog.math.Rect(left, top, width, height); + +}; + +/** + * Flip the lid open or shut. + * @param {boolean} state True if open. + * @private + */ +Blockly.Trashcan.prototype.setOpen_ = function(state) { + if (this.isOpen == state) { + return; + } + goog.Timer.clear(this.lidTask_); + this.isOpen = state; + this.animateLid_(); +}; + +/** + * Rotate the lid open or closed by one step. Then wait and recurse. + * @private + */ +Blockly.Trashcan.prototype.animateLid_ = function() { + this.lidOpen_ += this.isOpen ? 0.2 : -0.2; + this.lidOpen_ = goog.math.clamp(this.lidOpen_, 0, 1); + var lidAngle = this.lidOpen_ * 45; + this.svgLid_.setAttribute('transform', 'rotate(' + + (this.workspace_.RTL ? -lidAngle : lidAngle) + ',' + + (this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' + + (this.LID_HEIGHT_ - 2) + ')'); + var opacity = goog.math.lerp(0.4, 0.8, this.lidOpen_); + this.svgGroup_.style.opacity = opacity; + if (this.lidOpen_ > 0 && this.lidOpen_ < 1) { + this.lidTask_ = goog.Timer.callOnce(this.animateLid_, 20, this); + } +}; + +/** + * Flip the lid shut. + * Called externally after a drag. + */ +Blockly.Trashcan.prototype.close = function() { + this.setOpen_(false); +}; + +/** + * Inspect the contents of the trash. + */ +Blockly.Trashcan.prototype.click = function() { + var dx = this.workspace_.startScrollX - this.workspace_.scrollX; + var dy = this.workspace_.startScrollY - this.workspace_.scrollY; + if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) { + return; + } + console.log('TODO: Inspect trash.'); +}; diff --git a/core/trashcan.js.rej b/core/trashcan.js.rej new file mode 100644 index 000000000..312458764 --- /dev/null +++ b/core/trashcan.js.rej @@ -0,0 +1,16 @@ +*************** +*** 218,223 **** + * @private + */ + Blockly.Trashcan.prototype.position_ = function() { + var metrics = this.workspace_.getMetrics(); + if (!metrics) { + // There are no metrics available (workspace is probably not visible). +--- 219,225 ---- + * @private + */ + Blockly.Trashcan.prototype.position_ = function() { ++ var start = new Date().getTime(); + var metrics = this.workspace_.getMetrics(); + if (!metrics) { + // There are no metrics available (workspace is probably not visible). diff --git a/core/typeblock.js b/core/typeblock.js new file mode 100644 index 000000000..12bf6f657 --- /dev/null +++ b/core/typeblock.js @@ -0,0 +1,644 @@ +//Copyright 2013 Massachusetts Institute of Technology. All rights reserved. + +/** + * @fileoverview File to handle 'Type Blocking'. When the user starts typing the + * name of a Block in the workspace, a series of suggestions will appear. Upon + * selecting one (enter key), the chosen block will be created in the workspace + * This file needs additional configuration through the inject method. + * @author josmasflores@gmail.com (Jose Dominguez) + */ +'use strict'; + +goog.provide('Blockly.TypeBlock'); +goog.require('Blockly.Xml'); + +goog.require('goog.events'); +goog.require('goog.events.KeyCodes'); +goog.require('goog.events.KeyHandler'); +goog.require('goog.ui.ac'); +goog.require('goog.style'); + +goog.require('goog.ui.ac.ArrayMatcher'); +goog.require('goog.ui.ac.AutoComplete'); +goog.require('goog.ui.ac.InputHandler'); +goog.require('goog.ui.ac.Renderer'); + +/** + * Main Type Block function for configuration. + * @param {Object} htmlConfig an object of the type: + { + frame: 'ai_frame', + typeBlockDiv: 'ai_type_block', + inputText: 'ac_input_text' + } + * stating the ids of the attributes to be used in the html enclosing page + * create a new block + */ +Blockly.TypeBlock = function( htmlConfig ){ + var frame = htmlConfig['frame']; + Blockly.TypeBlock.typeBlockDiv_ = htmlConfig['typeBlockDiv']; + Blockly.TypeBlock.inputText_ = htmlConfig['inputText']; + + Blockly.TypeBlock.docKh_ = new goog.events.KeyHandler(goog.dom.getElement(frame)); + Blockly.TypeBlock.inputKh_ = new goog.events.KeyHandler(goog.dom.getElement(Blockly.TypeBlock.inputText_)); + + Blockly.TypeBlock.handleKey = function(e){ + if (e.altKey || e.ctrlKey || e.metaKey || e.keycode === 9) return; // 9 is tab + //We need to duplicate delete handling here from blockly.js + if (e.keyCode === 8 || e.keyCode === 46) { + // Delete or backspace. + // If the panel is showing the panel, just return to allow deletion in the panel itself + if (goog.style.isElementShown(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_))) return; + // if id is empty, it is deleting inside a block title + if (e.target.id === '') return; + // only when selected and deletable, actually delete the block + if (Blockly.selected && Blockly.selected.deletable) { + Blockly.hideChaff(); + Blockly.selected.dispose(true, true); + } + // Stop the browser from going back to the previous page. + e.preventDefault(); + return; + } + if (e.keyCode === 27){ //Dismiss the panel with esc + Blockly.TypeBlock.hide(); + return; + } + // A way to know if the user is editing a block or trying to type a new one + if (e.target.id === '') return; + if (goog.style.isElementShown(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_))) { + // Enter in the panel makes it select an option + if (e.keyCode === 13) Blockly.TypeBlock.hide(); + } + else { + Blockly.TypeBlock.show(); + // Can't seem to make Firefox display first character, so keep all browsers from automatically + // displaying the first character and add it manually. + e.preventDefault(); + goog.dom.getElement(Blockly.TypeBlock.inputText_).value = + String.fromCharCode(e.charCode != null ? e.charCode : e.keycode); + } + }; + + goog.events.listen(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey); + // Create the auto-complete panel + Blockly.TypeBlock.createAutoComplete_(Blockly.TypeBlock.inputText_); +}; + +/** + * Div where the type block panel will be rendered + * @private + */ +Blockly.TypeBlock.typeBlockDiv_ = null; + +/** + * input text contained in the type block panel used as input + * @private + */ +Blockly.TypeBlock.inputText_ = null; + +/** + * Document key handler applied to the frame area, and used to catch keyboard + * events. It is detached when the Type Block panel is shown, and + * re-attached when the Panel is dismissed. + * @private + */ +Blockly.TypeBlock.docKh_ = null; + +/** + * Input key handler applied to the Type Block Panel, and used to catch + * keyboard events. It is attached when the Type Block panel is shown, and + * dettached when the Panel is dismissed. + * @private + */ +Blockly.TypeBlock.inputKh_ = null; + +/** + * Is the Type Block panel currently showing? + */ +Blockly.TypeBlock.visible = false; + +/** + * Mapping of options to show in the auto-complete panel. This maps the + * canonical name of the block, needed to create a new Blockly.Block, with the + * internationalised word or sentence used in typeblocks. Certain blocks do not only need the + * canonical block representation, but also values for dropdowns (name and value) + * - No dropdowns: this.typeblock: [{ translatedName: Blockly.LANG_VAR }] + * - With dropdowns: this.typeblock: [{ translatedName: Blockly.LANG_VAR }, + * dropdown: { + * titleName: 'TITLE', value: 'value' + * }] + * - Additional types can be used to mark a block as isProcedure or isGlobalVar. These are only + * used to manage the loading of options in the auto-complete matcher. + * @private + */ +Blockly.TypeBlock.TBOptions_ = {}; + +/** + * This array contains only the Keys of Blockly.TypeBlock.TBOptions_ to be used + * as options in the autocomplete widget. + * @private + */ +Blockly.TypeBlock.TBOptionsNames_ = []; + +/** + * pointer to the automcomplete widget to be able to change its contents when + * the Language tree is modified (additions, renaming, or deletions) + * @private + */ +Blockly.TypeBlock.ac_ = null; + +/** + * We keep a listener pointer in case of needing to unlisten to it. We only want + * one listener at a time, and a reload could create a second one, so we + * unlisten first and then listen back + * @private + */ +Blockly.TypeBlock.currentListener_ = null; + +/** + * function to hide the autocomplete panel. Also used from hideChaff in + * Blockly.js + */ +Blockly.TypeBlock.hide = function(){ +// if (Blockly.TypeBlock.typeBlockDiv_ == null) +// return; + goog.style.showElement(goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_), false); + goog.events.unlisten(Blockly.TypeBlock.inputKh_, 'key', Blockly.TypeBlock.handleKey); + goog.events.listen(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey); + Blockly.TypeBlock.visible = false; +}; + +/** + * function to show the auto-complete panel to start typing block names + */ +Blockly.TypeBlock.show = function(){ + this.lazyLoadOfOptions_(); + var panel = goog.dom.getElement(Blockly.TypeBlock.typeBlockDiv_); + goog.style.setStyle(panel, 'top', Blockly.latestClick.y); + goog.style.setStyle(panel, 'left', Blockly.latestClick.x); + goog.style.showElement(panel, true); + goog.dom.getElement(Blockly.TypeBlock.inputText_).focus(); + // If the input gets cleaned before adding the handler, all keys are read + // correctly (at times it was missing the first char) + goog.dom.getElement(Blockly.TypeBlock.inputText_).value = ''; + goog.events.unlisten(Blockly.TypeBlock.docKh_, 'key', Blockly.TypeBlock.handleKey); + goog.events.listen(Blockly.TypeBlock.inputKh_, 'key', Blockly.TypeBlock.handleKey); + Blockly.TypeBlock.visible = true; +}; + +/** + * Used as an optimisation trick to avoid reloading components and built-ins unless there is a real + * need to do so. needsReload.components can be set to true when a component changes. + * Defaults to true so that it loads the first time (set to null after loading in lazyLoadOfOptions_()) + * @type {{components: boolean}} + */ +Blockly.TypeBlock.needsReload = { + components: true +}; + +/** + * Lazily loading options because some of them are not available during bootstrapping, and some + * users will never use this functionality, so we avoid having to deal with changes such as handling + * renaming of variables and procedures (leaving it until the moment they are used, if ever). + * @private + */ +Blockly.TypeBlock.lazyLoadOfOptions_ = function () { + + // Optimisation to avoid reloading all components and built-in objects unless it is needed. + // needsReload.components is setup when adding/renaming/removing a component in components.js + if (this.needsReload.components){ + Blockly.TypeBlock.generateOptions(); + this.needsReload.components = null; + } + Blockly.TypeBlock.loadGlobalVariables_(); + Blockly.TypeBlock.loadProcedures_(); + this.reloadOptionsAfterChanges_(); +}; + +/** + * This function traverses the Language tree and re-creates all the options + * available for type blocking. It's needed in the case of modifying the + * Language tree after its creation (adding or renaming components, for instance). + * It also loads all the built-in blocks. + * + * call 'reloadOptionsAfterChanges_' after calling this. The function lazyLoadOfOptions_ is an + * example of how to call this function. + */ +Blockly.TypeBlock.generateOptions = function() { + + var buildListOfOptions = function() { + var listOfOptions = {}; + var typeblockArray; + for (var name in Blockly.Blocks) { + var block = Blockly.Blocks[name]; + if(block.typeblock){ + typeblockArray = block.typeblock; + if(typeof block.typeblock == "function") { + typeblockArray = block.typeblock(); + } + createOption(typeblockArray, name); + } + } + + function createOption(tb, canonicName){ + if (tb){ + goog.array.forEach(tb, function(dd){ + var dropDownValues = {}; + var mutatorAttributes = {}; + if (dd.dropDown){ + if (dd.dropDown.titleName && dd.dropDown.value){ + dropDownValues.titleName = dd.dropDown.titleName; + dropDownValues.value = dd.dropDown.value; + } + else { + throw new Error('TypeBlock not correctly set up for ' + canonicName); + } + } + if(dd.mutatorAttributes) { + mutatorAttributes = dd.mutatorAttributes; + } + listOfOptions[dd.translatedName] = { + canonicName: canonicName, + dropDown: dropDownValues, + mutatorAttributes: mutatorAttributes + }; + }); + } + } + + return listOfOptions; + }; + + // This is called once on startup, and it will contain all built-in blocks. After that, it can + // be called on demand (for instance in the function lazyLoadOfOptions_) + Blockly.TypeBlock.TBOptions_ = buildListOfOptions(); +}; + +/** + * This function reloads all the latest changes that might have occurred in the language tree or + * the structures containing procedures and variables. It only needs to be called once even if + * different sources are being updated at the same time (call on load proc, load vars, and generate + * options, only needs one call of this function; and example of that is lazyLoadOfOptions_ + * @private + */ +Blockly.TypeBlock.reloadOptionsAfterChanges_ = function () { + Blockly.TypeBlock.TBOptionsNames_ = goog.object.getKeys(Blockly.TypeBlock.TBOptions_); + goog.array.sort(Blockly.TypeBlock.TBOptionsNames_); + Blockly.TypeBlock.ac_.matcher_.setRows(Blockly.TypeBlock.TBOptionsNames_); +}; + +/** + * Loads all procedure names as options for TypeBlocking. It is used lazily from show(). + * Call 'reloadOptionsAfterChanges_' after calling this one. The function lazyLoadOfOptions_ is an + * example of how to call this function. + * @private + */ +Blockly.TypeBlock.loadProcedures_ = function(){ + // Clean up any previous procedures in the list. + Blockly.TypeBlock.TBOptions_ = goog.object.filter(Blockly.TypeBlock.TBOptions_, + function(opti){ return !opti.isProcedure;}); + + var procsNoReturn = createTypeBlockForProcedures_(false); + goog.array.forEach(procsNoReturn, function(pro){ + Blockly.TypeBlock.TBOptions_[pro.translatedName] = { + canonicName: 'procedures_callnoreturn', + dropDown: pro.dropDown, + isProcedure: true // this attribute is used to clean up before reloading + }; + }); + + var procsReturn = createTypeBlockForProcedures_(true); + goog.array.forEach(procsReturn, function(pro){ + Blockly.TypeBlock.TBOptions_[pro.translatedName] = { + canonicName: 'procedures_callreturn', + dropDown: pro.dropDown, + isProcedure: true + }; + }); + + /** + * Procedure names can be collected for both 'with return' and 'no return' varieties from + * getProcedureNames() + * @param {boolean} withReturn indicates if the query us for 'with':true or 'no':false return + * @returns {Array} array of the procedures requested + * @private + */ + function createTypeBlockForProcedures_(withReturn) { + var options = []; + var procNames = Blockly.AIProcedure.getProcedureNames(withReturn); + goog.array.forEach(procNames, function(proc){ + options.push( + { + translatedName: Blockly.LANG_PROCEDURES_CALLNORETURN_CALL + ' ' + proc[0], + dropDown: { + titleName: 'PROCNAME', + value: proc[0] + } + } + ); + }); + return options; + } +}; + +/** + * Loads all global variable names as options for TypeBlocking. It is used lazily from show(). + * Call 'reloadOptionsAfterChanges_' after calling this one. The function lazyLoadOfOptions_ is an + * example of how to call this function. + */ +Blockly.TypeBlock.loadGlobalVariables_ = function () { + //clean up any previous procedures in the list + Blockly.TypeBlock.TBOptions_ = goog.object.filter(Blockly.TypeBlock.TBOptions_, + function(opti){ return !opti.isGlobalvar;}); + + var globalVarNames = createTypeBlockForVariables_(); + goog.array.forEach(globalVarNames, function(varName){ + var canonicalN; + if (varName.translatedName.substring(0,3) === 'get') + canonicalN = 'lexical_variable_get'; + else + canonicalN = 'lexical_variable_set'; + Blockly.TypeBlock.TBOptions_[varName.translatedName] = { + canonicName: canonicalN, + dropDown: varName.dropDown, + isGlobalvar: true + }; + }); + + /** + * Create TypeBlock options for global variables (a setter and a getter for each). + * @returns {Array} array of global var options + */ + function createTypeBlockForVariables_() { + var options = []; + var varNames = Blockly.FieldLexicalVariable.getGlobalNames(); + // Make a setter and a getter for each of the names + goog.array.forEach(varNames, function(varName){ + options.push( + { + translatedName: 'get global ' + varName, + dropDown: { + titleName: 'VAR', + value: 'global ' + varName + } + } + ); + options.push( + { + translatedName: 'set global ' + varName, + dropDown: { + titleName: 'VAR', + value: 'global ' + varName + } + } + ); + }); + return options; + } +}; + +/** + * Creates the auto-complete panel, powered by Google Closure's ac widget + * @private + */ +Blockly.TypeBlock.createAutoComplete_ = function(inputText){ + Blockly.TypeBlock.TBOptionsNames_ = goog.object.getKeys( Blockly.TypeBlock.TBOptions_ ); + goog.array.sort(Blockly.TypeBlock.TBOptionsNames_); + goog.events.unlistenByKey(Blockly.TypeBlock.currentListener_); //if there is a key, unlisten + if (Blockly.TypeBlock.ac_) + Blockly.TypeBlock.ac_.dispose(); //Make sure we only have 1 at a time + + // 3 objects needed to create a goog.ui.ac.AutoComplete instance + var matcher = new Blockly.TypeBlock.ac.AIArrayMatcher(Blockly.TypeBlock.TBOptionsNames_, false); + var renderer = new goog.ui.ac.Renderer(); + var inputHandler = new goog.ui.ac.InputHandler(null, null, false); + + Blockly.TypeBlock.ac_ = new goog.ui.ac.AutoComplete(matcher, renderer, inputHandler); + Blockly.TypeBlock.ac_.setMaxMatches(100); //Renderer has a set height of 294px and a scroll bar. + inputHandler.attachAutoComplete(Blockly.TypeBlock.ac_); + inputHandler.attachInputs(goog.dom.getElement(inputText)); + + Blockly.TypeBlock.currentListener_ = goog.events.listen(Blockly.TypeBlock.ac_, + goog.ui.ac.AutoComplete.EventType.UPDATE, + function() { + var blockName = goog.dom.getElement(inputText).value; + var blockToCreate = goog.object.get(Blockly.TypeBlock.TBOptions_, blockName); + if (!blockToCreate) { + //If the input passed is not a block, check if it is a number or a pre-populated text block + var numberReg = new RegExp('^-?[0-9]\\d*(\.\\d+)?$', 'g'); + var numberMatch = numberReg.exec(blockName); + var textReg = new RegExp('^[\"|\']+', 'g'); + var textMatch = textReg.exec(blockName); + if (numberMatch && numberMatch.length > 0){ + blockToCreate = { + canonicName: 'math_number', + dropDown: { + titleName: 'NUM', + value: blockName + } + }; + } + else if (textMatch && textMatch.length === 1){ + blockToCreate = { + canonicName: 'text', + dropDown: { + titleName: 'TEXT', + value: blockName.substring(1) + } + }; + } + else + return; // block does not exist: return + } + + var blockToCreateName = ''; + var block; + if (blockToCreate.dropDown){ //All blocks should have a dropDown property, even if empty + blockToCreateName = blockToCreate.canonicName; + // components have mutator attributes we need to deal with. We can also add these for special blocks + // e.g., this is done for create empty list + if(!goog.object.isEmpty(blockToCreate.mutatorAttributes)) { + //construct xml + var xmlString = ' return + + // Are both blocks statement blocks? If so, connect created block below the selected block + if (blockSelected.outputConnection == null && createdBlock.outputConnection == null) { + createdBlock.previousConnection.connect(blockSelected.nextConnection); + return; + } + + // No connections? Try the parent (if it exists) + if (blockSelected.parentBlock_) { + //Is the parent block a statement? + if (blockSelected.parentBlock_.outputConnection == null) { + //Is the created block a statment? If so, connect it below the parent block, + // which is a statement + if(createdBlock.outputConnection == null) { + blockSelected.parentBlock_.nextConnection.connect(createdBlock.previousConnection); + return; + //If it's not, no connections should be made + } else return; + } + else { + //try the parent for other connections + Blockly.TypeBlock.connectIfPossible(blockSelected.parentBlock_, createdBlock); + //recursive call: creates the inner functions again, but should not be much + //overhead; if it is, optimise! + } + } + }; + +//-------------------------------------- +// A custom matcher for the auto-complete widget that can handle numbers as well as the default +// functionality of goog.ui.ac.ArrayMatcher +goog.provide('Blockly.TypeBlock.ac.AIArrayMatcher'); + +goog.require('goog.iter'); +goog.require('goog.string'); + +/** + * Extension of goog.ui.ac.ArrayMatcher so that it can handle any number typed in. + * @constructor + * @param {Array} rows Dictionary of items to match. Can be objects if they + * have a toString method that returns the value to match against. + * @param {boolean=} opt_noSimilar if true, do not do similarity matches for the + * input token against the dictionary. + * @extends {goog.ui.ac.ArrayMatcher} + */ +Blockly.TypeBlock.ac.AIArrayMatcher = function(rows, opt_noSimilar) { + goog.ui.ac.ArrayMatcher.call(rows, opt_noSimilar); + this.rows_ = rows; + this.useSimilar_ = !opt_noSimilar; +}; +goog.inherits(Blockly.TypeBlock.ac.AIArrayMatcher, goog.ui.ac.ArrayMatcher); + +/** + * @inheritDoc + */ +Blockly.TypeBlock.ac.AIArrayMatcher.prototype.requestMatchingRows = function(token, maxMatches, + matchHandler, opt_fullString) { + + var matches = this.getPrefixMatches(token, maxMatches); + + //Because we allow for similar matches, Button.Text will always appear before Text + //So we handle the 'text' case as a special case here + if (token === 'text' || token === 'Text'){ + goog.array.remove(matches, 'Text'); + goog.array.insertAt(matches, 'Text', 0); + } + + // Added code to handle any number typed in the widget (including negatives and decimals) + var reg = new RegExp('^-?[0-9]\\d*(\.\\d+)?$', 'g'); + var match = reg.exec(token); + if (match && match.length > 0){ + matches.push(token); + } + + // Added code to handle default values for text fields (they start with " or ') + var textReg = new RegExp('^[\"|\']+', 'g'); + var textMatch = textReg.exec(token); + if (textMatch && textMatch.length === 1){ + matches.push(token); + } + + if (matches.length === 0 && this.useSimilar_) { + matches = this.getSimilarRows(token, maxMatches); + } + + matchHandler(token, matches); +}; diff --git a/core/variables.js.orig b/core/variables.js.orig new file mode 100644 index 000000000..00c38ad21 --- /dev/null +++ b/core/variables.js.orig @@ -0,0 +1,273 @@ +/** + * @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 Utility functions for handling variables. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Variables'); + +goog.require('Blockly.Blocks'); +goog.require('Blockly.Workspace'); +goog.require('goog.string'); + + +/** + * Category to separate variable names from procedures and generated functions. + */ +Blockly.Variables.NAME_TYPE = 'VARIABLE'; + +/** + * Find all user-created variables that are in use in the workspace. + * For use by generators. + * @param {!Blockly.Block|!Blockly.Workspace} root Root block or workspace. + * @return {!Array.} Array of variable names. + */ +Blockly.Variables.allUsedVariables = function(root) { + var blocks; + if (root instanceof Blockly.Block) { + // Root is Block. + blocks = root.getDescendants(); + } else if (root.getAllBlocks) { + // Root is Workspace. + blocks = root.getAllBlocks(); + } else { + throw 'Not Block or Workspace: ' + root; + } + var variableHash = Object.create(null); + // Iterate through every block and add each variable to the hash. + for (var x = 0; x < blocks.length; x++) { + var blockVariables = blocks[x].getVars(); + if (blockVariables) { + for (var y = 0; y < blockVariables.length; y++) { + var varName = blockVariables[y]; + // Variable name may be null if the block is only half-built. + if (varName) { + variableHash[varName.toLowerCase()] = varName; + } + } + } + } + // Flatten the hash into a list. + var variableList = []; + for (var name in variableHash) { + variableList.push(variableHash[name]); + } + return variableList; +}; + +/** + * Find all variables that the user has created through the workspace or + * toolbox. For use by generators. + * @param {!Blockly.Workspace} root The workspace to inspect. + * @return {!Array.} Array of variable names. + */ +Blockly.Variables.allVariables = function(root) { + if (root instanceof Blockly.Block) { + // Root is Block. + console.warn('Deprecated call to Blockly.Variables.allVariables ' + + 'with a block instead of a workspace. You may want ' + + 'Blockly.Variables.allUsedVariables'); + } + return root.variableList; +}; + +/** + * Construct the blocks required by the flyout for the variable category. + * @param {!Blockly.Workspace} workspace The workspace contianing variables. + * @return {!Array.} Array of XML block elements. + */ +Blockly.Variables.flyoutCategory = function(workspace) { + var variableList = workspace.variableList; + variableList.sort(goog.string.caseInsensitiveCompare); + + var xmlList = []; + var button = goog.dom.createDom('button'); + button.setAttribute('text', Blockly.Msg.NEW_VARIABLE); + xmlList.push(button); + + if (variableList.length > 0) { + if (Blockly.Blocks['variables_set']) { + // + // item + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'variables_set'); + if (Blockly.Blocks['math_change']) { + block.setAttribute('gap', 8); + } else { + block.setAttribute('gap', 24); + } + var field = goog.dom.createDom('field', null, variableList[0]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + xmlList.push(block); + } + if (Blockly.Blocks['math_change']) { + // + // + // + // 1 + // + // + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'math_change'); + if (Blockly.Blocks['variables_get']) { + block.setAttribute('gap', 20); + } + var value = goog.dom.createDom('value'); + value.setAttribute('name', 'DELTA'); + block.appendChild(value); + + var field = goog.dom.createDom('field', null, variableList[0]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + + var shadowBlock = goog.dom.createDom('shadow'); + shadowBlock.setAttribute('type', 'math_number'); + value.appendChild(shadowBlock); + + var numberField = goog.dom.createDom('field', null, '1'); + numberField.setAttribute('name', 'NUM'); + shadowBlock.appendChild(numberField); + + xmlList.push(block); + } + + for (var i = 0; i < variableList.length; i++) { + if (Blockly.Blocks['variables_get']) { + // + // item + // + var block = goog.dom.createDom('block'); + block.setAttribute('type', 'variables_get'); + if (Blockly.Blocks['variables_set']) { + block.setAttribute('gap', 8); + } + var field = goog.dom.createDom('field', null, variableList[i]); + field.setAttribute('name', 'VAR'); + block.appendChild(field); + xmlList.push(block); + } + } + } + return xmlList; +}; + +/** +* Return a new variable name that is not yet being used. This will try to +* generate single letter variable names in the range 'i' to 'z' to start with. +* If no unique name is located it will try 'i' to 'z', 'a' to 'h', +* then 'i2' to 'z2' etc. Skip 'l'. + * @param {!Blockly.Workspace} workspace The workspace to be unique in. +* @return {string} New variable name. +*/ +Blockly.Variables.generateUniqueName = function(workspace) { + var variableList = workspace.variableList; + var newName = ''; + if (variableList.length) { + var nameSuffix = 1; + var letters = 'ijkmnopqrstuvwxyzabcdefgh'; // No 'l'. + var letterIndex = 0; + var potName = letters.charAt(letterIndex); + while (!newName) { + var inUse = false; + for (var i = 0; i < variableList.length; i++) { + if (variableList[i].toLowerCase() == potName) { + // This potential name is already used. + inUse = true; + break; + } + } + if (inUse) { + // Try the next potential name. + letterIndex++; + if (letterIndex == letters.length) { + // Reached the end of the character sequence so back to 'i'. + // a new suffix. + letterIndex = 0; + nameSuffix++; + } + potName = letters.charAt(letterIndex); + if (nameSuffix > 1) { + potName += nameSuffix; + } + } else { + // We can use the current potential name. + newName = potName; + } + } + } else { + newName = 'i'; + } + return newName; +}; + +/** + * Create a new variable on the given workspace. + * @param {!Blockly.Workspace} workspace The workspace on which to create the + * variable. + * @return {null|undefined|string} An acceptable new variable name, or null if + * change is to be aborted (cancel button), or undefined if an existing + * variable was chosen. + */ +Blockly.Variables.createVariable = function(workspace) { + while (true) { + var text = Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, ''); + if (text) { + if (workspace.variableIndexOf(text) != -1) { + window.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1', + text.toLowerCase())); + } else { + workspace.createVariable(text); + break; + } + } else { + text = null; + break; + } + } + return text; +}; + +/** + * Prompt the user for a new variable name. + * @param {string} promptText The string of the prompt. + * @param {string} defaultText The default value to show in the prompt's field. + * @return {?string} The new variable name, or null if the user picked + * something illegal. + */ +Blockly.Variables.promptName = function(promptText, defaultText) { + var newVar = window.prompt(promptText, defaultText); + // Merge runs of whitespace. Strip leading and trailing whitespace. + // Beyond this, all names are legal. + if (newVar) { + newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + if (newVar == Blockly.Msg.RENAME_VARIABLE || + newVar == Blockly.Msg.NEW_VARIABLE) { + // Ok, not ALL names are legal... + newVar = null; + } + } + return newVar; +}; diff --git a/core/variables.js.rej b/core/variables.js.rej new file mode 100644 index 000000000..6e78cf817 --- /dev/null +++ b/core/variables.js.rej @@ -0,0 +1,17 @@ +*************** +*** 30,36 **** + + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); +- goog.require('Blockly.Toolbox'); + goog.require('Blockly.Workspace'); + + +--- 30,36 ---- + + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); ++ //goog.require('Blockly.Toolbox'); + goog.require('Blockly.Workspace'); + + diff --git a/core/warning.js.orig b/core/warning.js.orig new file mode 100644 index 000000000..bffbf06df --- /dev/null +++ b/core/warning.js.orig @@ -0,0 +1,185 @@ +/** + * @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 Object representing a warning. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Warning'); + +goog.require('Blockly.Bubble'); +goog.require('Blockly.Icon'); + + +/** + * Class for a warning. + * @param {!Blockly.Block} block The block associated with this warning. + * @extends {Blockly.Icon} + * @constructor + */ +Blockly.Warning = function(block) { + Blockly.Warning.superClass_.constructor.call(this, block); + this.createIcon(); + // The text_ object can contain multiple warnings. + this.text_ = {}; +}; +goog.inherits(Blockly.Warning, Blockly.Icon); + +/** + * Does this icon get hidden when the block is collapsed. + */ +Blockly.Warning.prototype.collapseHidden = false; + +/** + * Draw the warning icon. + * @param {!Element} group The icon group. + * @private + */ +Blockly.Warning.prototype.drawIcon_ = function(group) { + // Triangle with rounded corners. + Blockly.createSvgElement('path', + {'class': 'blocklyIconShape', + 'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z'}, + group); + // Can't use a real '!' text character since different browsers and operating + // systems render it differently. + // Body of exclamation point. + Blockly.createSvgElement('path', + {'class': 'blocklyIconSymbol', + 'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z'}, + group); + // Dot of exclamation point. + Blockly.createSvgElement('rect', + {'class': 'blocklyIconSymbol', + 'x': '7', 'y': '11', 'height': '2', 'width': '2'}, + group); +}; + +/** + * Create the text for the warning's bubble. + * @param {string} text The text to display. + * @return {!SVGTextElement} The top-level node of the text. + * @private + */ +Blockly.Warning.textToDom_ = function(text) { + var paragraph = /** @type {!SVGTextElement} */ ( + Blockly.createSvgElement('text', + {'class': 'blocklyText blocklyBubbleText', + 'y': Blockly.Bubble.BORDER_WIDTH}, + null)); + var lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) { + var tspanElement = Blockly.createSvgElement('tspan', + {'dy': '1em', 'x': Blockly.Bubble.BORDER_WIDTH}, paragraph); + var textNode = document.createTextNode(lines[i]); + tspanElement.appendChild(textNode); + } + return paragraph; +}; + +/** + * Show or hide the warning bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +Blockly.Warning.prototype.setVisible = function(visible) { + if (visible == this.isVisible()) { + // No change. + return; + } + Blockly.Events.fire( + new Blockly.Events.Ui(this.block_, 'warningOpen', !visible, visible)); + if (visible) { + // Create the bubble to display all warnings. + var paragraph = Blockly.Warning.textToDom_(this.getText()); + this.bubble_ = new Blockly.Bubble( + /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), + paragraph, this.block_.svgPath_, this.iconXY_, null, null); + if (this.block_.RTL) { + // Right-align the paragraph. + // This cannot be done until the bubble is rendered on screen. + var maxWidth = paragraph.getBBox().width; + for (var i = 0, textElement; textElement = paragraph.childNodes[i]; i++) { + textElement.setAttribute('text-anchor', 'end'); + textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH); + } + } + this.updateColour(); + // Bump the warning into the right location. + var size = this.bubble_.getBubbleSize(); + this.bubble_.setBubbleSize(size.width, size.height); + } else { + // Dispose of the bubble. + this.bubble_.dispose(); + this.bubble_ = null; + this.body_ = null; + } +}; + +/** + * Bring the warning to the top of the stack when clicked on. + * @param {!Event} e Mouse up event. + * @private + */ +Blockly.Warning.prototype.bodyFocus_ = function(e) { + this.bubble_.promote_(); +}; + +/** + * Set this warning's text. + * @param {string} text Warning text (or '' to delete). + * @param {string} id An ID for this text entry to be able to maintain + * multiple warnings. + */ +Blockly.Warning.prototype.setText = function(text, id) { + if (this.text_[id] == text) { + return; + } + if (text) { + this.text_[id] = text; + } else { + delete this.text_[id]; + } + if (this.isVisible()) { + this.setVisible(false); + this.setVisible(true); + } +}; + +/** + * Get this warning's texts. + * @return {string} All texts concatenated into one string. + */ +Blockly.Warning.prototype.getText = function() { + var allWarnings = []; + for (var id in this.text_) { + allWarnings.push(this.text_[id]); + } + return allWarnings.join('\n'); +}; + +/** + * Dispose of this warning. + */ +Blockly.Warning.prototype.dispose = function() { + this.block_.warning = null; + Blockly.Icon.prototype.dispose.call(this); +}; diff --git a/core/warning.js.rej b/core/warning.js.rej new file mode 100644 index 000000000..65ced2804 --- /dev/null +++ b/core/warning.js.rej @@ -0,0 +1,60 @@ +*************** +*** 82,99 **** + ! + */ + var iconShield = Blockly.createSvgElement('path', +- {'class': 'blocklyIconShield', + 'd': 'M 2,15 Q -1,15 0.5,12 L 6.5,1.7 Q 8,-1 9.5,1.7 L 15.5,12 ' + + 'Q 17,15 14,15 z'}, + this.iconGroup_); + this.iconMark_ = Blockly.createSvgElement('text', +- {'class': 'blocklyIconMark', + 'x': Blockly.Icon.RADIUS, + 'y': 2 * Blockly.Icon.RADIUS - 3}, this.iconGroup_); + this.iconMark_.appendChild(document.createTextNode('!')); + }; + + /** + * Show or hide the warning bubble. + * @param {boolean} visible True if the bubble should be visible. + */ +--- 82,120 ---- + ! + */ + var iconShield = Blockly.createSvgElement('path', ++ {'class': 'blocklyWarningIconShield', + 'd': 'M 2,15 Q -1,15 0.5,12 L 6.5,1.7 Q 8,-1 9.5,1.7 L 15.5,12 ' + + 'Q 17,15 14,15 z'}, + this.iconGroup_); + this.iconMark_ = Blockly.createSvgElement('text', ++ {'class': 'blocklyWarningIconMark', + 'x': Blockly.Icon.RADIUS, + 'y': 2 * Blockly.Icon.RADIUS - 3}, this.iconGroup_); + this.iconMark_.appendChild(document.createTextNode('!')); + }; + + /** ++ * Create the text for the warning's bubble. ++ * @param {string} text The text to display. ++ * @return {!SVGTextElement} The top-level node of the text. ++ * @private ++ */ ++ Blockly.Warning.prototype.textToDom_ = function(text) { ++ var paragraph = /** @type {!SVGTextElement} */ ( ++ Blockly.createSvgElement( ++ 'text', {'class': 'blocklyText blocklyErrorWarningText', 'y': Blockly.Bubble.BORDER_WIDTH}, ++ null)); ++ var lines = text.split('\n'); ++ for (var i = 0; i < lines.length; i++) { ++ var tspanElement = Blockly.createSvgElement('tspan', ++ {'dy': '1em', 'x': Blockly.Bubble.BORDER_WIDTH}, paragraph); ++ var textNode = document.createTextNode(lines[i]); ++ tspanElement.appendChild(textNode); ++ } ++ return paragraph; ++ }; ++ ++ /** + * Show or hide the warning bubble. + * @param {boolean} visible True if the bubble should be visible. + */ diff --git a/core/workspace.js b/core/workspace.js index a8e97c04b..f54b8b8ac 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -129,6 +129,10 @@ Blockly.Workspace.prototype.addTopBlock = function(block) { } } } + if (this.warningIndicator) { + this.warningIndicator.dispose(); + this.warningIndicator = null; + } }; /** @@ -136,6 +140,8 @@ Blockly.Workspace.prototype.addTopBlock = function(block) { * @param {!Blockly.Block} block Block to remove. */ Blockly.Workspace.prototype.removeTopBlock = function(block) { + if (block.workspace == Blockly.mainWorkspace) //Do not reset arrangements for the flyout + Blockly.resetWorkspaceArrangements(); var found = false; for (var child, i = 0; child = this.topBlocks_[i]; i++) { if (child == block) { @@ -156,6 +162,7 @@ Blockly.Workspace.prototype.removeTopBlock = function(block) { * @return {!Array.} The top-level block objects. */ Blockly.Workspace.prototype.getTopBlocks = function(ordered) { + var start = new Date().getTime(); //*** lyn instrumentation // Copy the topBlocks_ list. var blocks = [].concat(this.topBlocks_); if (ordered && blocks.length > 1) { @@ -169,6 +176,10 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) { return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); }); } + var stop = new Date().getTime(); //*** lyn instrumentation + var timeDiff = stop - start; //*** lyn instrumentation + Blockly.Instrument.stats.getTopBlocksCalls++; + Blockly.Instrument.stats.getTopBlocksTime += timeDiff; return blocks; }; diff --git a/core/workspace.js.orig b/core/workspace.js.orig new file mode 100644 index 000000000..a8e97c04b --- /dev/null +++ b/core/workspace.js.orig @@ -0,0 +1,501 @@ +/** + * @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 Object representing a workspace. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Workspace'); + +goog.require('goog.math'); + + +/** + * Class for a workspace. This is a data structure that contains blocks. + * There is no UI, and can be created headlessly. + * @param {Blockly.Options} opt_options Dictionary of options. + * @constructor + */ +Blockly.Workspace = function(opt_options) { + /** @type {string} */ + this.id = Blockly.genUid(); + Blockly.Workspace.WorkspaceDB_[this.id] = this; + /** @type {!Blockly.Options} */ + this.options = opt_options || {}; + /** @type {boolean} */ + this.RTL = !!this.options.RTL; + /** @type {boolean} */ + this.horizontalLayout = !!this.options.horizontalLayout; + /** @type {number} */ + this.toolboxPosition = this.options.toolboxPosition; + + /** + * @type {!Array.} + * @private + */ + this.topBlocks_ = []; + /** + * @type {!Array.} + * @private + */ + this.listeners_ = []; + /** + * @type {!Array.} + * @private + */ + this.undoStack_ = []; + /** + * @type {!Array.} + * @private + */ + this.redoStack_ = []; + /** + * @type {!Object} + * @private + */ + this.blockDB_ = Object.create(null); + /* + * @type {!Array.} + * A list of all of the named variables in the workspace, including variables + * that are not currently in use. + */ + this.variableList = []; +}; + +/** + * Workspaces may be headless. + * @type {boolean} True if visible. False if headless. + */ +Blockly.Workspace.prototype.rendered = false; + +/** + * Maximum number of undo events in stack. + * @type {number} 0 to turn off undo, Infinity for unlimited. + */ +Blockly.Workspace.prototype.MAX_UNDO = 1024; + +/** + * Dispose of this workspace. + * Unlink from all DOM elements to prevent memory leaks. + */ +Blockly.Workspace.prototype.dispose = function() { + this.listeners_.length = 0; + this.clear(); + // Remove from workspace database. + delete Blockly.Workspace.WorkspaceDB_[this.id]; +}; + +/** + * Angle away from the horizontal to sweep for blocks. Order of execution is + * generally top to bottom, but a small angle changes the scan to give a bit of + * a left to right bias (reversed in RTL). Units are in degrees. + * See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling. + */ +Blockly.Workspace.SCAN_ANGLE = 3; + +/** + * Add a block to the list of top blocks. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.Workspace.prototype.addTopBlock = function(block) { + this.topBlocks_.push(block); + if (this.isFlyout) { + // This is for the (unlikely) case where you have a variable in a block in + // an always-open flyout. It needs to be possible to edit the block in the + // flyout, so the contents of the dropdown need to be correct. + var variables = Blockly.Variables.allUsedVariables(block); + for (var i = 0; i < variables.length; i++) { + if (this.variableList.indexOf(variables[i]) == -1) { + this.variableList.push(variables[i]); + } + } + } +}; + +/** + * Remove a block from the list of top blocks. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.Workspace.prototype.removeTopBlock = function(block) { + var found = false; + for (var child, i = 0; child = this.topBlocks_[i]; i++) { + if (child == block) { + this.topBlocks_.splice(i, 1); + found = true; + break; + } + } + if (!found) { + throw 'Block not present in workspace\'s list of top-most blocks.'; + } +}; + +/** + * Finds the top-level blocks and returns them. Blocks are optionally sorted + * by position; top to bottom (with slight LTR or RTL bias). + * @param {boolean} ordered Sort the list if true. + * @return {!Array.} The top-level block objects. + */ +Blockly.Workspace.prototype.getTopBlocks = function(ordered) { + // Copy the topBlocks_ list. + var blocks = [].concat(this.topBlocks_); + if (ordered && blocks.length > 1) { + var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE)); + if (this.RTL) { + offset *= -1; + } + blocks.sort(function(a, b) { + var aXY = a.getRelativeToSurfaceXY(); + var bXY = b.getRelativeToSurfaceXY(); + return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); + }); + } + return blocks; +}; + +/** + * Find all blocks in workspace. No particular order. + * @return {!Array.} Array of blocks. + */ +Blockly.Workspace.prototype.getAllBlocks = function() { + var blocks = this.getTopBlocks(false); + for (var i = 0; i < blocks.length; i++) { + blocks.push.apply(blocks, blocks[i].getChildren()); + } + return blocks; +}; + +/** + * Dispose of all blocks in workspace. + */ +Blockly.Workspace.prototype.clear = function() { + var existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) { + Blockly.Events.setGroup(true); + } + while (this.topBlocks_.length) { + this.topBlocks_[0].dispose(); + } + if (!existingGroup) { + Blockly.Events.setGroup(false); + } + + this.variableList.length = 0; +}; + +/** + * Walk the workspace and update the list of variables to only contain ones in + * use on the workspace. Use when loading new workspaces from disk. + * @param {boolean} clearList True if the old variable list should be cleared. + */ +Blockly.Workspace.prototype.updateVariableList = function(clearList) { + // TODO: Sort + if (!this.isFlyout) { + // Update the list in place so that the flyout's references stay correct. + if (clearList) { + this.variableList.length = 0; + } + var allVariables = Blockly.Variables.allUsedVariables(this); + for (var i = 0; i < allVariables.length; i++) { + this.createVariable(allVariables[i]); + } + } +}; + +/** + * Rename a variable by updating its name in the variable list. + * TODO: #468 + * @param {string} oldName Variable to rename. + * @param {string} newName New variable name. + */ +Blockly.Workspace.prototype.renameVariable = function(oldName, newName) { + // Find the old name in the list. + var variableIndex = this.variableIndexOf(oldName); + var newVariableIndex = this.variableIndexOf(newName); + + // We might be renaming to an existing name but with different case. If so, + // we will also update all of the blocks using the new name to have the + // correct case. + if (newVariableIndex != -1 && + this.variableList[newVariableIndex] != newName) { + var oldCase = this.variableList[newVariableIndex]; + } + + Blockly.Events.setGroup(true); + var blocks = this.getAllBlocks(); + // Iterate through every block. + for (var i = 0; i < blocks.length; i++) { + blocks[i].renameVar(oldName, newName); + if (oldCase) { + blocks[i].renameVar(oldCase, newName); + } + } + Blockly.Events.setGroup(false); + + + if (variableIndex == newVariableIndex || + variableIndex != -1 && newVariableIndex == -1) { + // Only changing case, or renaming to a completely novel name. + this.variableList[variableIndex] = newName; + } else if (variableIndex != -1 && newVariableIndex != -1) { + // Renaming one existing variable to another existing variable. + this.variableList.splice(variableIndex, 1); + // The case might have changed. + this.variableList[newVariableIndex] = newName; + } else { + this.variableList.push(newName); + console.log('Tried to rename an non-existent variable.'); + } +}; + +/** + * Create a variable with the given name. + * TODO: #468 + * @param {string} name The new variable's name. + */ +Blockly.Workspace.prototype.createVariable = function(name) { + var index = this.variableIndexOf(name); + if (index == -1) { + this.variableList.push(name); + } +}; + +/** + * Find all the uses of a named variable. + * @param {string} name Name of variable. + * @return {!Array.} Array of block usages. + */ +Blockly.Workspace.prototype.getVariableUses = function(name) { + var uses = []; + var blocks = this.getAllBlocks(); + // Iterate through every block and check the name. + for (var i = 0; i < blocks.length; i++) { + var blockVariables = blocks[i].getVars(); + if (blockVariables) { + for (var j = 0; j < blockVariables.length; j++) { + var varName = blockVariables[j]; + // Variable name may be null if the block is only half-built. + if (varName && Blockly.Names.equals(varName, name)) { + uses.push(blocks[i]); + } + } + } + } + return uses; +}; + +/** + * Delete a variables and all of its uses from this workspace. + * @param {string} name Name of variable to delete. + */ +Blockly.Workspace.prototype.deleteVariable = function(name) { + var variableIndex = this.variableIndexOf(name); + if (variableIndex != -1) { + var uses = this.getVariableUses(name); + if (uses.length > 1) { + for (var i = 0, block; block = uses[i]; i++) { + if (block.type == 'procedures_defnoreturn' || + block.type == 'procedures_defreturn') { + var procedureName = block.getFieldValue('NAME'); + window.alert( + Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.replace('%1', name). + replace('%2', procedureName)); + return; + } + } + var ok = window.confirm( + Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length). + replace('%2', name)); + if (!ok) { + return; + } + } + + Blockly.Events.setGroup(true); + for (var i = 0; i < uses.length; i++) { + uses[i].dispose(true, false); + } + Blockly.Events.setGroup(false); + this.variableList.splice(variableIndex, 1); + } +}; + +/** + * Check whether a variable exists with the given name. The check is + * case-insensitive. + * @param {string} name The name to check for. + * @return {number} The index of the name in the variable list, or -1 if it is + * not present. + */ +Blockly.Workspace.prototype.variableIndexOf = function(name) { + for (var i = 0, varname; varname = this.variableList[i]; i++) { + if (Blockly.Names.equals(varname, name)) { + return i; + } + } + return -1; +}; + +/** + * Returns the horizontal offset of the workspace. + * Intended for LTR/RTL compatibility in XML. + * Not relevant for a headless workspace. + * @return {number} Width. + */ +Blockly.Workspace.prototype.getWidth = function() { + return 0; +}; + +/** + * Obtain a newly created block. + * @param {?string} prototypeName Name of the language object containing + * type-specific functions for this block. + * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise + * create a new id. + * @return {!Blockly.Block} The created block. + */ +Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) { + return new Blockly.Block(this, prototypeName, opt_id); +}; + +/** + * The number of blocks that may be added to the workspace before reaching + * the maxBlocks. + * @return {number} Number of blocks left. + */ +Blockly.Workspace.prototype.remainingCapacity = function() { + if (isNaN(this.options.maxBlocks)) { + return Infinity; + } + return this.options.maxBlocks - this.getAllBlocks().length; +}; + +/** + * Undo or redo the previous action. + * @param {boolean} redo False if undo, true if redo. + */ +Blockly.Workspace.prototype.undo = function(redo) { + var inputStack = redo ? this.redoStack_ : this.undoStack_; + var outputStack = redo ? this.undoStack_ : this.redoStack_; + var inputEvent = inputStack.pop(); + if (!inputEvent) { + return; + } + var events = [inputEvent]; + // Do another undo/redo if the next one is of the same group. + while (inputStack.length && inputEvent.group && + inputEvent.group == inputStack[inputStack.length - 1].group) { + events.push(inputStack.pop()); + } + // Push these popped events on the opposite stack. + for (var i = 0, event; event = events[i]; i++) { + outputStack.push(event); + } + events = Blockly.Events.filter(events, redo); + Blockly.Events.recordUndo = false; + for (var i = 0, event; event = events[i]; i++) { + event.run(redo); + } + Blockly.Events.recordUndo = true; +}; + +/** + * Clear the undo/redo stacks. + */ +Blockly.Workspace.prototype.clearUndo = function() { + this.undoStack_.length = 0; + this.redoStack_.length = 0; + // Stop any events already in the firing queue from being undoable. + Blockly.Events.clearPendingUndo(); +}; + +/** + * When something in this workspace changes, call a function. + * @param {!Function} func Function to call. + * @return {!Function} Function that can be passed to + * removeChangeListener. + */ +Blockly.Workspace.prototype.addChangeListener = function(func) { + this.listeners_.push(func); + return func; +}; + +/** + * Stop listening for this workspace's changes. + * @param {Function} func Function to stop calling. + */ +Blockly.Workspace.prototype.removeChangeListener = function(func) { + var i = this.listeners_.indexOf(func); + if (i != -1) { + this.listeners_.splice(i, 1); + } +}; + +/** + * Fire a change event. + * @param {!Blockly.Events.Abstract} event Event to fire. + */ +Blockly.Workspace.prototype.fireChangeListener = function(event) { + if (event.recordUndo) { + this.undoStack_.push(event); + this.redoStack_.length = 0; + if (this.undoStack_.length > this.MAX_UNDO) { + this.undoStack_.unshift(); + } + } + for (var i = 0, func; func = this.listeners_[i]; i++) { + func(event); + } +}; + +/** + * Find the block on this workspace with the specified ID. + * @param {string} id ID of block to find. + * @return {Blockly.Block} The sought after block or null if not found. + */ +Blockly.Workspace.prototype.getBlockById = function(id) { + return this.blockDB_[id] || null; +}; + +/** + * Database of all workspaces. + * @private + */ +Blockly.Workspace.WorkspaceDB_ = Object.create(null); + +/** + * Find the workspace with the specified ID. + * @param {string} id ID of workspace to find. + * @return {Blockly.Workspace} The sought after workspace or null if not found. + */ +Blockly.Workspace.getById = function(id) { + return Blockly.Workspace.WorkspaceDB_[id] || null; +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear; +Blockly.Workspace.prototype['clearUndo'] = + Blockly.Workspace.prototype.clearUndo; +Blockly.Workspace.prototype['addChangeListener'] = + Blockly.Workspace.prototype.addChangeListener; +Blockly.Workspace.prototype['removeChangeListener'] = + Blockly.Workspace.prototype.removeChangeListener; diff --git a/core/workspace.js.rej b/core/workspace.js.rej new file mode 100644 index 000000000..a8d2feb23 --- /dev/null +++ b/core/workspace.js.rej @@ -0,0 +1,176 @@ +*************** +*** 26,31 **** + + goog.provide('Blockly.Workspace'); + + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); + goog.require('Blockly.ScrollbarPair'); +--- 26,33 ---- + + goog.provide('Blockly.Workspace'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code ++ + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); + goog.require('Blockly.ScrollbarPair'); +*************** +*** 90,95 **** + Blockly.Workspace.prototype.trashcan = null; + + /** + * PID of upcoming firing of a change event. Used to fire only one event + * after multiple changes. + * @type {?number} +--- 92,103 ---- + Blockly.Workspace.prototype.trashcan = null; + + /** ++ * The workspace's warning indicator. ++ * @type {Blockly.WarningIndicator} ++ */ ++ Blockly.Workspace.prototype.warningIndicator = null; ++ ++ /** + * PID of upcoming firing of a change event. Used to fire only one event + * after multiple changes. + * @type {?number} +*************** +*** 144,149 **** + }; + + /** + * Get the SVG element that forms the drawing surface. + * @return {!Element} SVG element. + */ +--- 156,175 ---- + }; + + /** ++ * Adds the warning indicator. ++ * @param {!Function} getMetrics A function that returns workspace's metrics. ++ */ ++ Blockly.Workspace.prototype.addWarningIndicator = function(getMetrics) { ++ if (Blockly.WarningIndicator && !this.readOnly) { ++ this.warningIndicator = new Blockly.WarningIndicator(getMetrics); ++ var svgWarningIndicator = this.warningIndicator.createDom(); ++ this.svgGroup_.insertBefore(svgWarningIndicator, this.svgBlockCanvas_); ++ this.warningIndicator.init(); ++ } ++ }; ++ ++ ++ /** + * Get the SVG element that forms the drawing surface. + * @return {!Element} SVG element. + */ +*************** +*** 164,169 **** + * @param {!Blockly.Block} block Block to remove. + */ + Blockly.Workspace.prototype.addTopBlock = function(block) { + this.topBlocks_.push(block); + if (Blockly.Realtime.isEnabled() && this == Blockly.mainWorkspace) { + Blockly.Realtime.addTopBlock(block); +--- 190,197 ---- + * @param {!Blockly.Block} block Block to remove. + */ + Blockly.Workspace.prototype.addTopBlock = function(block) { ++ if (block.workspace == Blockly.mainWorkspace) //Do not reset arrangements for the flyout ++ Blockly.resetWorkspaceArrangements(); + this.topBlocks_.push(block); + if (Blockly.Realtime.isEnabled() && this == Blockly.mainWorkspace) { + Blockly.Realtime.addTopBlock(block); +*************** +*** 177,186 **** + * @return {!Array.} Array of blocks. + */ + Blockly.Workspace.prototype.getAllBlocks = function() { + var blocks = this.getTopBlocks(false); +- for (var x = 0; x < blocks.length; x++) { + blocks.push.apply(blocks, blocks[x].getChildren()); + } + return blocks; + }; + +--- 212,242 ---- + * @return {!Array.} Array of blocks. + */ + Blockly.Workspace.prototype.getAllBlocks = function() { ++ var start = new Date().getTime(); //*** lyn instrumentation + var blocks = this.getTopBlocks(false); ++ Blockly.Instrument.stats.getAllBlocksAllocationCalls++; ++ if (Blockly.Instrument.useLynGetAllBlocksFix) { ++ // Lyn's version of getAllBlocks that avoids quadratic times for large numbers of blocks ++ // by mutating existing blocks array rather than creating new ones ++ for (var x = 0; x < blocks.length; x++) { ++ var children = blocks[x].getChildren(); ++ blocks.push.apply(blocks, children); ++ Blockly.Instrument.stats.getAllBlocksAllocationSpace += children.length; ++ } ++ } else { ++ // Neil's version that has quadratic time for large number of blocks ++ // because each call to concat creates *new* array, and so this code does a *lot* of heap ++ // allocation when there are a large number of blocks. ++ for (var x = 0; x < blocks.length; x++) { + blocks.push.apply(blocks, blocks[x].getChildren()); ++ Blockly.Instrument.stats.getAllBlocksAllocationCalls++; ++ Blockly.Instrument.stats.getAllBlocksAllocationSpace += blocks.length; ++ } + } ++ var stop = new Date().getTime(); //*** lyn instrumentation ++ var timeDiff = stop - start; //*** lyn instrumentation ++ Blockly.Instrument.stats.getAllBlocksCalls++; ++ Blockly.Instrument.stats.getAllBlocksTime += timeDiff; + return blocks; + }; + +*************** +*** 198,209 **** + * Render all blocks in workspace. + */ + Blockly.Workspace.prototype.render = function() { +- var renderList = this.getAllBlocks(); +- for (var x = 0, block; block = renderList[x]; x++) { +- if (!block.getChildren().length) { +- block.render(); + } + } + }; + + /** +--- 254,286 ---- + * Render all blocks in workspace. + */ + Blockly.Workspace.prototype.render = function() { ++ var start = new Date().getTime(); ++ // [lyn, 04/08/14] Get both top and all blocks for stats ++ var topBlocks = this.getTopBlocks(); ++ var allBlocks = this.getAllBlocks(); ++ if (Blockly.Instrument.useRenderDown) { ++ for (var t = 0, topBlock; topBlock = topBlocks[t]; t++) { ++ Blockly.Instrument.timer( ++ function () { topBlock.renderDown(); }, ++ function (result, timeDiffInner) { ++ Blockly.Instrument.stats.renderDownTime += timeDiffInner; ++ } ++ ); ++ } ++ } else { ++ var renderList = allBlocks; ++ for (var x = 0, block; block = renderList[x]; x++) { ++ if (!block.getChildren().length) { ++ block.render(); ++ } + } + } ++ var stop = new Date().getTime(); ++ var timeDiffOuter = stop - start; ++ Blockly.Instrument.stats.blockCount = allBlocks.length; ++ Blockly.Instrument.stats.topBlockCount = topBlocks.length; ++ Blockly.Instrument.stats.workspaceRenderCalls++; ++ Blockly.Instrument.stats.workspaceRenderTime += timeDiffOuter; + }; + + /** diff --git a/core/xml.js.orig b/core/xml.js.orig new file mode 100644 index 000000000..2567560cd --- /dev/null +++ b/core/xml.js.orig @@ -0,0 +1,566 @@ +/** + * @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 XML reader and writer. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Xml'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); + + +/** + * Encode a block tree as XML. + * @param {!Blockly.Workspace} workspace The workspace containing blocks. + * @return {!Element} XML document. + */ +Blockly.Xml.workspaceToDom = function(workspace) { + var xml = goog.dom.createDom('xml'); + var blocks = workspace.getTopBlocks(true); + for (var i = 0, block; block = blocks[i]; i++) { + xml.appendChild(Blockly.Xml.blockToDomWithXY(block)); + } + return xml; +}; + +/** + * Encode a block subtree as XML with XY coordinates. + * @param {!Blockly.Block} block The root block to encode. + * @return {!Element} Tree of XML elements. + */ +Blockly.Xml.blockToDomWithXY = function(block) { + var width; // Not used in LTR. + if (block.workspace.RTL) { + width = block.workspace.getWidth(); + } + var element = Blockly.Xml.blockToDom(block); + var xy = block.getRelativeToSurfaceXY(); + element.setAttribute('x', + Math.round(block.workspace.RTL ? width - xy.x : xy.x)); + element.setAttribute('y', Math.round(xy.y)); + return element; +}; + +/** + * Encode a block subtree as XML. + * @param {!Blockly.Block} block The root block to encode. + * @return {!Element} Tree of XML elements. + */ +Blockly.Xml.blockToDom = function(block) { + var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block'); + element.setAttribute('type', block.type); + element.setAttribute('id', block.id); + if (block.mutationToDom) { + // Custom data for an advanced block. + var mutation = block.mutationToDom(); + if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) { + element.appendChild(mutation); + } + } + function fieldToDom(field) { + if (field.name && field.EDITABLE) { + var container = goog.dom.createDom('field', null, field.getValue()); + container.setAttribute('name', field.name); + element.appendChild(container); + } + } + for (var i = 0, input; input = block.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + fieldToDom(field); + } + } + + var commentText = block.getCommentText(); + if (commentText) { + var commentElement = goog.dom.createDom('comment', null, commentText); + if (typeof block.comment == 'object') { + commentElement.setAttribute('pinned', block.comment.isVisible()); + var hw = block.comment.getBubbleSize(); + commentElement.setAttribute('h', hw.height); + commentElement.setAttribute('w', hw.width); + } + element.appendChild(commentElement); + } + + if (block.data) { + var dataElement = goog.dom.createDom('data', null, block.data); + element.appendChild(dataElement); + } + + for (var i = 0, input; input = block.inputList[i]; i++) { + var container; + var empty = true; + if (input.type == Blockly.DUMMY_INPUT) { + continue; + } else { + var childBlock = input.connection.targetBlock(); + if (input.type == Blockly.INPUT_VALUE) { + container = goog.dom.createDom('value'); + } else if (input.type == Blockly.NEXT_STATEMENT) { + container = goog.dom.createDom('statement'); + } + var shadow = input.connection.getShadowDom(); + if (shadow && (!childBlock || !childBlock.isShadow())) { + container.appendChild(Blockly.Xml.cloneShadow_(shadow)); + } + if (childBlock) { + container.appendChild(Blockly.Xml.blockToDom(childBlock)); + empty = false; + } + } + container.setAttribute('name', input.name); + if (!empty) { + element.appendChild(container); + } + } + if (block.inputsInlineDefault != block.inputsInline) { + element.setAttribute('inline', block.inputsInline); + } + if (block.isCollapsed()) { + element.setAttribute('collapsed', true); + } + if (block.disabled) { + element.setAttribute('disabled', true); + } + if (!block.isDeletable() && !block.isShadow()) { + element.setAttribute('deletable', false); + } + if (!block.isMovable() && !block.isShadow()) { + element.setAttribute('movable', false); + } + if (!block.isEditable()) { + element.setAttribute('editable', false); + } + + var nextBlock = block.getNextBlock(); + if (nextBlock) { + var container = goog.dom.createDom('next', null, + Blockly.Xml.blockToDom(nextBlock)); + element.appendChild(container); + } + var shadow = block.nextConnection && block.nextConnection.getShadowDom(); + if (shadow && (!nextBlock || !nextBlock.isShadow())) { + container.appendChild(Blockly.Xml.cloneShadow_(shadow)); + } + + return element; +}; + +/** + * Deeply clone the shadow's DOM so that changes don't back-wash to the block. + * @param {!Element} shadow A tree of XML elements. + * @return {!Element} A tree of XML elements. + * @private + */ +Blockly.Xml.cloneShadow_ = function(shadow) { + shadow = shadow.cloneNode(true); + // Walk the tree looking for whitespace. Don't prune whitespace in a tag. + var node = shadow; + var textNode; + while (node) { + if (node.firstChild) { + node = node.firstChild; + } else { + while (node && !node.nextSibling) { + textNode = node; + node = node.parentNode; + if (textNode.nodeType == 3 && textNode.data.trim() == '' && + node.firstChild != textNode) { + // Prune whitespace after a tag. + goog.dom.removeNode(textNode); + } + } + if (node) { + textNode = node; + node = node.nextSibling; + if (textNode.nodeType == 3 && textNode.data.trim() == '') { + // Prune whitespace before a tag. + goog.dom.removeNode(textNode); + } + } + } + } + return shadow; +}; + +/** + * Converts a DOM structure into plain text. + * Currently the text format is fairly ugly: all one line with no whitespace. + * @param {!Element} dom A tree of XML elements. + * @return {string} Text representation. + */ +Blockly.Xml.domToText = function(dom) { + var oSerializer = new XMLSerializer(); + return oSerializer.serializeToString(dom); +}; + +/** + * Converts a DOM structure into properly indented text. + * @param {!Element} dom A tree of XML elements. + * @return {string} Text representation. + */ +Blockly.Xml.domToPrettyText = function(dom) { + // This function is not guaranteed to be correct for all XML. + // But it handles the XML that Blockly generates. + var blob = Blockly.Xml.domToText(dom); + // Place every open and close tag on its own line. + var lines = blob.split('<'); + // Indent every line. + var indent = ''; + for (var i = 1; i < lines.length; i++) { + var line = lines[i]; + if (line[0] == '/') { + indent = indent.substring(2); + } + lines[i] = indent + '<' + line; + if (line[0] != '/' && line.slice(-2) != '/>') { + indent += ' '; + } + } + // Pull simple tags back together. + // E.g. + var text = lines.join('\n'); + text = text.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g, '$1'); + // Trim leading blank line. + return text.replace(/^\n/, ''); +}; + +/** + * Converts plain text into a DOM structure. + * Throws an error if XML doesn't parse. + * @param {string} text Text representation. + * @return {!Element} A tree of XML elements. + */ +Blockly.Xml.textToDom = function(text) { + var oParser = new DOMParser(); + var dom = oParser.parseFromString(text, 'text/xml'); + // The DOM should have one and only one top-level node, an XML tag. + if (!dom || !dom.firstChild || + dom.firstChild.nodeName.toLowerCase() != 'xml' || + dom.firstChild !== dom.lastChild) { + // Whatever we got back from the parser is not XML. + goog.asserts.fail('Blockly.Xml.textToDom did not obtain a valid XML tree.'); + } + return dom.firstChild; +}; + +/** + * Decode an XML DOM and create blocks on the workspace. + * @param {!Element} xml XML DOM. + * @param {!Blockly.Workspace} workspace The workspace. + */ +Blockly.Xml.domToWorkspace = function(xml, workspace) { + if (xml instanceof Blockly.Workspace) { + var swap = xml; + xml = workspace; + workspace = swap; + console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' + + 'swap the arguments.'); + } + var width; // Not used in LTR. + if (workspace.RTL) { + width = workspace.getWidth(); + } + Blockly.Field.startCache(); + // Safari 7.1.3 is known to provide node lists with extra references to + // children beyond the lists' length. Trust the length, do not use the + // looping pattern of checking the index for an object. + var childCount = xml.childNodes.length; + var existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) { + Blockly.Events.setGroup(true); + } + for (var i = 0; i < childCount; i++) { + var xmlChild = xml.childNodes[i]; + var name = xmlChild.nodeName.toLowerCase(); + if (name == 'block' || + (name == 'shadow' && !Blockly.Events.recordUndo)) { + // Allow top-level shadow blocks if recordUndo is disabled since + // that means an undo is in progress. Such a block is expected + // to be moved to a nested destination in the next operation. + var block = Blockly.Xml.domToBlock(xmlChild, workspace); + var blockX = parseInt(xmlChild.getAttribute('x'), 10); + var blockY = parseInt(xmlChild.getAttribute('y'), 10); + if (!isNaN(blockX) && !isNaN(blockY)) { + block.moveBy(workspace.RTL ? width - blockX : blockX, blockY); + } + } else if (name == 'shadow') { + goog.asserts.fail('Shadow block cannot be a top-level block.'); + } + } + if (!existingGroup) { + Blockly.Events.setGroup(false); + } + Blockly.Field.stopCache(); + + workspace.updateVariableList(false); +}; + +/** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Element} xmlBlock XML block element. + * @param {!Blockly.Workspace} workspace The workspace. + * @return {!Blockly.Block} The root block created. + */ +Blockly.Xml.domToBlock = function(xmlBlock, workspace) { + if (xmlBlock instanceof Blockly.Workspace) { + var swap = xmlBlock; + xmlBlock = workspace; + workspace = swap; + console.warn('Deprecated call to Blockly.Xml.domToBlock, ' + + 'swap the arguments.'); + } + // Create top-level block. + Blockly.Events.disable(); + try { + var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace); + if (workspace.rendered) { + // Hide connections to speed up assembly. + topBlock.setConnectionsHidden(true); + // Generate list of all blocks. + var blocks = topBlock.getDescendants(); + // Render each block. + for (var i = blocks.length - 1; i >= 0; i--) { + blocks[i].initSvg(); + } + for (var i = blocks.length - 1; i >= 0; i--) { + blocks[i].render(false); + } + // Populating the connection database may be defered until after the + // blocks have rendered. + setTimeout(function() { + if (topBlock.workspace) { // Check that the block hasn't been deleted. + topBlock.setConnectionsHidden(false); + } + }, 1); + topBlock.updateDisabled(); + // Allow the scrollbars to resize and move based on the new contents. + // TODO(@picklesrus): #387. Remove when domToBlock avoids resizing. + workspace.resizeContents(); + } + } finally { + Blockly.Events.enable(); + } + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Create(topBlock)); + } + return topBlock; +}; + +/** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Element} xmlBlock XML block element. + * @param {!Blockly.Workspace} workspace The workspace. + * @return {!Blockly.Block} The root block created. + * @private + */ +Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { + var block = null; + var prototypeName = xmlBlock.getAttribute('type'); + goog.asserts.assert(prototypeName, 'Block type unspecified: %s', + xmlBlock.outerHTML); + var id = xmlBlock.getAttribute('id'); + block = workspace.newBlock(prototypeName, id); + + var blockChild = null; + for (var i = 0, xmlChild; xmlChild = xmlBlock.childNodes[i]; i++) { + if (xmlChild.nodeType == 3) { + // Ignore any text at the level. It's all whitespace anyway. + continue; + } + var input; + + // Find any enclosed blocks or shadows in this tag. + var childBlockNode = null; + var childShadowNode = null; + for (var j = 0, grandchildNode; grandchildNode = xmlChild.childNodes[j]; + j++) { + if (grandchildNode.nodeType == 1) { + if (grandchildNode.nodeName.toLowerCase() == 'block') { + childBlockNode = grandchildNode; + } else if (grandchildNode.nodeName.toLowerCase() == 'shadow') { + childShadowNode = grandchildNode; + } + } + } + // Use the shadow block if there is no child block. + if (!childBlockNode && childShadowNode) { + childBlockNode = childShadowNode; + } + + var name = xmlChild.getAttribute('name'); + switch (xmlChild.nodeName.toLowerCase()) { + case 'mutation': + // Custom data for an advanced block. + if (block.domToMutation) { + block.domToMutation(xmlChild); + if (block.initSvg) { + // Mutation may have added some elements that need initalizing. + block.initSvg(); + } + } + break; + case 'comment': + block.setCommentText(xmlChild.textContent); + var visible = xmlChild.getAttribute('pinned'); + if (visible && !block.isInFlyout) { + // Give the renderer a millisecond to render and position the block + // before positioning the comment bubble. + setTimeout(function() { + if (block.comment && block.comment.setVisible) { + block.comment.setVisible(visible == 'true'); + } + }, 1); + } + var bubbleW = parseInt(xmlChild.getAttribute('w'), 10); + var bubbleH = parseInt(xmlChild.getAttribute('h'), 10); + if (!isNaN(bubbleW) && !isNaN(bubbleH) && + block.comment && block.comment.setVisible) { + block.comment.setBubbleSize(bubbleW, bubbleH); + } + break; + case 'data': + block.data = xmlChild.textContent; + break; + case 'title': + // Titles were renamed to field in December 2013. + // Fall through. + case 'field': + var field = block.getField(name); + if (!field) { + console.warn('Ignoring non-existent field ' + name + ' in block ' + + prototypeName); + break; + } + field.setValue(xmlChild.textContent); + break; + case 'value': + case 'statement': + input = block.getInput(name); + if (!input) { + console.warn('Ignoring non-existent input ' + name + ' in block ' + + prototypeName); + break; + } + if (childShadowNode) { + input.connection.setShadowDom(childShadowNode); + } + if (childBlockNode) { + blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode, + workspace); + if (blockChild.outputConnection) { + input.connection.connect(blockChild.outputConnection); + } else if (blockChild.previousConnection) { + input.connection.connect(blockChild.previousConnection); + } else { + goog.asserts.fail( + 'Child block does not have output or previous statement.'); + } + } + break; + case 'next': + if (childShadowNode && block.nextConnection) { + block.nextConnection.setShadowDom(childShadowNode); + } + if (childBlockNode) { + goog.asserts.assert(block.nextConnection, + 'Next statement does not exist.'); + // If there is more than one XML 'next' tag. + goog.asserts.assert(!block.nextConnection.isConnected(), + 'Next statement is already connected.'); + blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode, + workspace); + goog.asserts.assert(blockChild.previousConnection, + 'Next block does not have previous statement.'); + block.nextConnection.connect(blockChild.previousConnection); + } + break; + default: + // Unknown tag; ignore. Same principle as HTML parsers. + console.warn('Ignoring unknown tag: ' + xmlChild.nodeName); + } + } + + var inline = xmlBlock.getAttribute('inline'); + if (inline) { + block.setInputsInline(inline == 'true'); + } + var disabled = xmlBlock.getAttribute('disabled'); + if (disabled) { + block.setDisabled(disabled == 'true'); + } + var deletable = xmlBlock.getAttribute('deletable'); + if (deletable) { + block.setDeletable(deletable == 'true'); + } + var movable = xmlBlock.getAttribute('movable'); + if (movable) { + block.setMovable(movable == 'true'); + } + var editable = xmlBlock.getAttribute('editable'); + if (editable) { + block.setEditable(editable == 'true'); + } + var collapsed = xmlBlock.getAttribute('collapsed'); + if (collapsed) { + block.setCollapsed(collapsed == 'true'); + } + if (xmlBlock.nodeName.toLowerCase() == 'shadow') { + // Ensure all children are also shadows. + var children = block.getChildren(); + for (var i = 0, child; child = children[i]; i++) { + goog.asserts.assert(child.isShadow(), + 'Shadow block not allowed non-shadow child.'); + } + block.setShadow(true); + } + return block; +}; + +/** + * Remove any 'next' block (statements in a stack). + * @param {!Element} xmlBlock XML block element. + */ +Blockly.Xml.deleteNext = function(xmlBlock) { + for (var i = 0, child; child = xmlBlock.childNodes[i]; i++) { + if (child.nodeName.toLowerCase() == 'next') { + xmlBlock.removeChild(child); + break; + } + } +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +if (!goog.global['Blockly']['Xml']) { + goog.global['Blockly']['Xml'] = {}; +} +goog.global['Blockly']['Xml']['domToText'] = Blockly.Xml.domToText; +goog.global['Blockly']['Xml']['domToWorkspace'] = Blockly.Xml.domToWorkspace; +goog.global['Blockly']['Xml']['textToDom'] = Blockly.Xml.textToDom; +goog.global['Blockly']['Xml']['workspaceToDom'] = Blockly.Xml.workspaceToDom; diff --git a/core/xml.js.rej b/core/xml.js.rej new file mode 100644 index 000000000..0cc4a02ef --- /dev/null +++ b/core/xml.js.rej @@ -0,0 +1,251 @@ +*************** +*** 26,31 **** + + goog.provide('Blockly.Xml'); + + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); + +--- 26,33 ---- + + goog.provide('Blockly.Xml'); + ++ goog.require('Blockly.Instrument'); // lyn's instrumentation code ++ + // TODO(scr): Fix circular dependencies + // goog.require('Blockly.Block'); + +*************** +*** 212,234 **** + * @param {!Element} xml XML DOM. + */ + Blockly.Xml.domToWorkspace = function(workspace, xml) { +- var width +- if (Blockly.RTL) { +- width = workspace.getMetrics().viewWidth; +- } +- for (var x = 0, xmlChild; xmlChild = xml.childNodes[x]; x++) { +- if (xmlChild.nodeName.toLowerCase() == 'block') { +- var block = Blockly.Xml.domToBlock(workspace, xmlChild); +- var blockX = parseInt(xmlChild.getAttribute('x'), 10); +- var blockY = parseInt(xmlChild.getAttribute('y'), 10); +- if (!isNaN(blockX) && !isNaN(blockY)) { +- block.moveBy(Blockly.RTL ? width - blockX : blockX, blockY); + } +- } +- } + }; + + /** + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Blockly.Workspace} workspace The workspace. +--- 214,245 ---- + * @param {!Element} xml XML DOM. + */ + Blockly.Xml.domToWorkspace = function(workspace, xml) { ++ Blockly.Instrument.timer ( ++ function () { ++ var width; // Not used in LTR. ++ if (Blockly.RTL) { ++ width = workspace.getMetrics().viewWidth; ++ } ++ for (var x = 0, xmlChild; xmlChild = xml.childNodes[x]; x++) { ++ if (xmlChild.nodeName.toLowerCase() == 'block') { ++ var block = Blockly.Xml.domToBlock(workspace, xmlChild); ++ var blockX = parseInt(xmlChild.getAttribute('x'), 10); ++ var blockY = parseInt(xmlChild.getAttribute('y'), 10); ++ if (!isNaN(blockX) && !isNaN(blockY)) { ++ block.moveBy(Blockly.RTL ? width - blockX : blockX, blockY); ++ } ++ } ++ } ++ }, ++ function (result, timeDiff) { ++ Blockly.Instrument.stats.domToWorkspaceCalls++; ++ Blockly.Instrument.stats.domToWorkspaceTime = timeDiff; + } ++ ); + }; + + /** ++ * Wrapper for domToBlockInner that renders blocks. + * Decode an XML block tag and create a block (and possibly sub blocks) on the + * workspace. + * @param {!Blockly.Workspace} workspace The workspace. +*************** +*** 239,244 **** + * @private + */ + Blockly.Xml.domToBlock = function(workspace, xmlBlock, opt_reuseBlock) { + var block = null; + var prototypeName = xmlBlock.getAttribute('type'); + if (!prototypeName) { +--- 250,317 ---- + * @private + */ + Blockly.Xml.domToBlock = function(workspace, xmlBlock, opt_reuseBlock) { ++ return Blockly.Instrument.timer ( ++ function () { ++ var block = Blockly.Xml.domToBlockInner(workspace, xmlBlock, opt_reuseBlock); ++ Blockly.Instrument.timer ( ++ function () { ++ if (Blockly.Instrument.useRenderDown) { ++ block.renderDown(); ++ } ++ }, ++ function (result, timeDiffInner) { ++ if (Blockly.Instrument.useRenderDown) { ++ Blockly.Instrument.stats.renderDownTime += timeDiffInner; ++ } ++ } ++ ); ++ // [lyn, 07/03/2014] Special case to handle renaming of event parameters in i8n ++ if (block && block.type == "component_event") { ++ // Create a dictionary mapping default event parameter names appearing in body ++ // to their possibly translated names in some source language ++ var eventParamDict = Blockly.LexicalVariable.eventParameterDict(block); ++ var sourceEventParams = []; // Event parameter names in source language ++ var targetEventParams = []; // Event parameter names in target language ++ for (var key in eventParamDict) { ++ var sourceEventParam = eventParamDict[key]; ++ var targetEventParam = window.parent.BlocklyPanel_getLocalizedParameterName(key); ++ if (sourceEventParam != targetEventParam) { // Only add to translation if they're different ++ sourceEventParams.push(sourceEventParam); ++ targetEventParams.push(targetEventParam); ++ } ++ } ++ if (sourceEventParams.length > 0) { // Do we need to translated some source event parameters? ++ var childBlocks = block.getChildren(); // should be at most one body block ++ for (var j= 0, childBlock; childBlock = childBlocks[j]; j++) { ++ var freeSubstitution = new Blockly.Substitution(sourceEventParams, targetEventParams); ++ // renameFree does the translation. ++ Blockly.LexicalVariable.renameFree(childBlock, freeSubstitution); ++ } ++ } ++ } ++ return block; ++ }, ++ function (block, timeDiffOuter) { ++ Blockly.Instrument.stats.domToBlockCalls++; ++ Blockly.Instrument.stats.domToBlockTime += timeDiffOuter; ++ return block; ++ } ++ ); ++ } ++ ++ /** ++ * Version of domToBlock that does not render blocks. ++ * Decode an XML block tag and create a block (and possibly sub blocks) on the ++ * workspace. ++ * @param {!Blockly.Workspace} workspace The workspace. ++ * @param {!Element} xmlBlock XML block element. ++ * @param {boolean=} opt_reuseBlock Optional arg indicating whether to ++ * reinitialize an existing block. ++ * @return {!Blockly.Block} The root block created. ++ * @private ++ */ ++ Blockly.Xml.domToBlockInner = function(workspace, xmlBlock, opt_reuseBlock) { ++ Blockly.Instrument.stats.domToBlockInnerCalls++; + var block = null; + var prototypeName = xmlBlock.getAttribute('type'); + if (!prototypeName) { +*************** +*** 342,348 **** + } + if (firstRealGrandchild && + firstRealGrandchild.nodeName.toLowerCase() == 'block') { +- blockChild = Blockly.Xml.domToBlock(workspace, firstRealGrandchild, + opt_reuseBlock); + if (blockChild.outputConnection) { + input.connection.connect(blockChild.outputConnection); +--- 415,421 ---- + } + if (firstRealGrandchild && + firstRealGrandchild.nodeName.toLowerCase() == 'block') { ++ blockChild = Blockly.Xml.domToBlockInner(workspace, firstRealGrandchild, + opt_reuseBlock); + if (blockChild.outputConnection) { + input.connection.connect(blockChild.outputConnection); +*************** +*** 362,368 **** + // This could happen if there is more than one XML 'next' tag. + throw 'Next statement is already connected.'; + } +- blockChild = Blockly.Xml.domToBlock(workspace, firstRealGrandchild, + opt_reuseBlock); + if (!blockChild.previousConnection) { + throw 'Next block does not have previous statement.'; +--- 435,441 ---- + // This could happen if there is more than one XML 'next' tag. + throw 'Next statement is already connected.'; + } ++ blockChild = Blockly.Xml.domToBlockInner(workspace, firstRealGrandchild, + opt_reuseBlock); + if (!blockChild.previousConnection) { + throw 'Next block does not have previous statement.'; +*************** +*** 375,392 **** + } + } + + var collapsed = xmlBlock.getAttribute('collapsed'); + if (collapsed) { + block.setCollapsed(collapsed == 'true'); + } +- var next = block.getNextBlock(); +- if (next) { +- // Next block in a stack needs to square off its corners. +- // Rendering a child will render its parent. +- next.render(); +- } else { +- block.render(); +- } + return block; + }; + +--- 448,491 ---- + } + } + ++ // [lyn, 10/25/13] collapsing and friends need to be done *after* connections are made to sublocks. ++ // Otherwise, the subblocks won't be properly processed by block.setCollapsed and friends. ++ var inline = xmlBlock.getAttribute('inline'); ++ if (inline) { ++ block.setInputsInline(inline == 'true'); ++ } ++ var disabled = xmlBlock.getAttribute('disabled'); ++ if (disabled) { ++ block.setDisabled(disabled == 'true'); ++ } ++ var deletable = xmlBlock.getAttribute('deletable'); ++ if (deletable) { ++ block.setDeletable(deletable == 'true'); ++ } ++ var movable = xmlBlock.getAttribute('movable'); ++ if (movable) { ++ block.setMovable(movable == 'true'); ++ } ++ var editable = xmlBlock.getAttribute('editable'); ++ if (editable) { ++ block.setEditable(editable == 'true'); ++ } ++ ++ if (! Blockly.Instrument.useRenderDown) { ++ // Neil's original rendering code ++ var next = block.getNextBlock(); ++ if (next) { ++ // Next block in a stack needs to square off its corners. ++ // Rendering a child will render its parent. ++ next.render(); ++ } else { ++ block.render(); ++ } ++ } + var collapsed = xmlBlock.getAttribute('collapsed'); + if (collapsed) { + block.setCollapsed(collapsed == 'true'); + } + return block; + }; + diff --git a/i18n/common.py b/i18n/common.py index 90e584e16..fac4c39b6 100644 --- a/i18n/common.py +++ b/i18n/common.py @@ -3,7 +3,7 @@ # Code shared by translation conversion scripts. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ def read_json_file(filename): return defs except ValueError, e: print('Error reading ' + filename) - raise InputError(filename, str(e)) + raise InputError(file, str(e)) def _create_qqq_file(output_dir): diff --git a/i18n/create_messages.py b/i18n/create_messages.py index d32814f4a..cd63f1bd1 100755 --- a/i18n/create_messages.py +++ b/i18n/create_messages.py @@ -3,7 +3,7 @@ # Generate .js files defining Blockly core and language messages. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,14 +28,6 @@ from common import read_json_file _NEWLINE_PATTERN = re.compile('[\n\r]') -def string_is_ascii(s): - try: - s.decode('ascii') - return True - except UnicodeEncodeError: - return False - - def main(): """Generate .js files defining Blockly core and language messages.""" @@ -84,17 +76,10 @@ def main(): target_lang = filename[:filename.index('.')] if target_lang not in ('qqq', 'keys', 'synonyms'): target_defs = read_json_file(os.path.join(os.curdir, arg_file)) - - # Verify that keys are 'ascii' - bad_keys = [key for key in target_defs if not string_is_ascii(key)] - if bad_keys: - print(u'These keys in {0} contain non ascii characters: {1}'.format( - filename, ', '.join(bad_keys))) - # If there's a '\n' or '\r', remove it and print a warning. for key, value in target_defs.items(): if _NEWLINE_PATTERN.search(value): - print(u'WARNING: definition of {0} in {1} contained ' + print('WARNING: definition of {0} in {1} contained ' 'a newline character.'. format(key, arg_file)) target_defs[key] = _NEWLINE_PATTERN.sub(' ', value) @@ -133,10 +118,10 @@ goog.require('Blockly.Msg'); synonym_keys = [key for key in target_defs if key in synonym_defs] if not args.quiet: if extra_keys: - print(u'These extra keys appeared in {0}: {1}'.format( + print('These extra keys appeared in {0}: {1}'.format( filename, ', '.join(extra_keys))) if synonym_keys: - print(u'These synonym keys appeared in {0}: {1}'.format( + print('These synonym keys appeared in {0}: {1}'.format( filename, ', '.join(synonym_keys))) outfile.write(synonym_text) diff --git a/i18n/dedup_json.py b/i18n/dedup_json.py index 30e572dde..b4b51ba10 100755 --- a/i18n/dedup_json.py +++ b/i18n/dedup_json.py @@ -7,7 +7,7 @@ # output. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/i18n/js_to_json.py b/i18n/js_to_json.py index 197dc4357..097bd5493 100755 --- a/i18n/js_to_json.py +++ b/i18n/js_to_json.py @@ -3,7 +3,7 @@ # Gives the translation status of the specified apps and languages. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/i18n/json_to_js.py b/i18n/json_to_js.py index f8c20f6af..44ab97882 100755 --- a/i18n/json_to_js.py +++ b/i18n/json_to_js.py @@ -3,7 +3,7 @@ # Converts .json files into .js files for use within Blockly apps. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/i18n/status.py b/i18n/status.py new file mode 100644 index 000000000..64fa98da1 --- /dev/null +++ b/i18n/status.py @@ -0,0 +1,247 @@ +#!/usr/bin/python + +# Gives the translation status of the specified apps and languages. +# +# Copyright 2013 Google Inc. +# https://blockly.googlecode.com/ +# +# 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. + +"""Produce a table showing the translation status of each app by language. + +@author Ellen Spertus (ellen.spertus@gmail.com) +""" + +import argparse +import os +from common import read_json_file + +# Bogus language name representing all messages defined. +TOTAL = 'qqq' + +# List of key prefixes, which are app names, except for 'Apps', which +# has common messages. It is included here for convenience. +APPS = ['Apps', 'Code', 'Graph', 'Maze', 'Plane', 'Puzzle', 'Turtle'] + + +def get_prefix(s): + """Gets the portion of a string before the first period. + + Args: + s: A string. + + Returns: + The portion of the string before the first period, or the entire + string if it does not contain a period. + """ + return s.split('.')[0] + + +def get_prefix_count(prefix, arr): + """Counts how many strings in the array start with the prefix. + + Args: + prefix: The prefix string. + arr: An array of strings. + Returns: + The number of strings in arr starting with prefix. + """ + # This code was chosen for its elegance not its efficiency. + return len([elt for elt in arr if elt.startswith(prefix)]) + + +def output_as_html(messages, apps, verbose): + """Outputs the given prefix counts and percentages as HTML. + + Specifically, a sortable HTML table is produced, where the app names + are column headers, and one language is output per row. Entries + are color-coded based on the percent completeness. + + Args: + messages: A dictionary of dictionaries, where the outer keys are language + codes used by translatewiki (generally, ISO 639 language codes) or + the string TOTAL, used to indicate the total set of messages. The + inner dictionary makes message keys to values in that language. + apps: Apps to consider. + verbose: Whether to list missing keys. + """ + def generate_language_url(lang): + return 'https://translatewiki.net/wiki/Special:SupportedLanguages#' + lang + + def generate_number_as_percent(num, total, tag): + percent = num * 100 / total + if percent == 100: + color = 'green' + elif percent >= 90: + color = 'orange' + elif percent >= 60: + color = 'black' + else: + color = 'gray' + s = '{1} ({2}%)'.format(color, num, percent) + if verbose and percent < 100: + return '{1}'.format(tag, s) + else: + return s + + print('Blockly app translation status') + print("") + print('') + print('') + for lang in messages: + if lang != TOTAL: + print(''.format( + lang, generate_language_url(lang))) + for app in apps: + print '' + print('') + print('
Language' + + ''.join(apps) + '
{0}' + print(generate_number_as_percent( + get_prefix_count(app, messages[lang]), + get_prefix_count(app, messages[TOTAL]), + (lang + app))) + print '
ALL') + print(''.join([str(get_prefix_count(app, TOTAL)) for app in apps])) + print('
') + + if verbose: + for lang in messages: + if lang != TOTAL: + for app in apps: + if (get_prefix_count(app, messages[lang]) < + get_prefix_count(app, messages[TOTAL])): + print('
{1} ({0})'. + format(lang, app, generate_language_url(lang))) + print(' missing: ') + print(', '.join( + [key for key in messages[TOTAL] if + key.startswith(app) and key not in messages[lang]])) + print('

') + print('') + + +def output_as_text(messages, apps, verbose): + """Outputs the given prefix counts and percentages as text. + + Args: + messages: A dictionary of dictionaries, where the outer keys are language + codes used by translatewiki (generally, ISO 639 language codes) or + the string TOTAL, used to indicate the total set of messages. The + inner dictionary makes message keys to values in that language. + apps: Apps to consider. + verbose: Whether to list missing keys. + """ + def generate_number_as_percent(num, total): + return '{0} ({1}%)'.format(num, num * 100 / total) + MAX_WIDTH = len('999 (100%)') + 1 + FIELD_STRING = '{0: <' + str(MAX_WIDTH) + '}' + print(FIELD_STRING.format('Language') + ''.join( + [FIELD_STRING.format(app) for app in apps])) + print(('-' * (MAX_WIDTH - 1) + ' ') * (len(apps) + 1)) + for lang in messages: + if lang != TOTAL: + print(FIELD_STRING.format(lang) + + ''.join([FIELD_STRING.format(generate_number_as_percent( + get_prefix_count(app, messages[lang]), + get_prefix_count(app, messages[TOTAL]))) + for app in apps])) + print(FIELD_STRING.format(TOTAL) + + ''.join( + [FIELD_STRING.format(get_prefix_count(app, messages[TOTAL])) + for app in apps])) + if verbose: + for lang in messages: + if lang != TOTAL: + for app in apps: + missing = [key for key in messages[TOTAL] + if key.startswith(app) and key not in messages[lang]] + print('{0} {1}: Missing: {2}'.format( + app.upper(), lang, (', '.join(missing) if missing else 'none'))) + + +def output_as_csv(messages, apps): + """Outputs the given prefix counts and percentages as CSV. + + Args: + messages: A dictionary of dictionaries, where the outer keys are language + codes used by translatewiki (generally, ISO 639 language codes) or + the string TOTAL, used to indicate the total set of messages. The + inner dictionary makes message keys to values in that language. + apps: Apps to consider. + """ + # Header row. + print('Language, ' + ', ,'.join(apps)) + + # Total row. + # Put at top, rather than bottom, so it can be frozen. + print('TOTAL, ' + ', '.join( + [str(get_prefix_count(app, messages[TOTAL])) + ', ' + for app in apps])) + + # One line per language. + for lang in messages: + if lang != TOTAL: + print(lang + ', ' + ', '.join( + [str(get_prefix_count(app, messages[lang])) + + ', ' + + str((get_prefix_count(app, messages[lang]) * 1.0 / + get_prefix_count(app, messages[TOTAL]))) + for app in apps])) + + +def main(): + """Processes input files and outputs results in specified format. + """ + # Argument parsing. + parser = argparse.ArgumentParser( + description='Display translation status by app and language.') + parser.add_argument('--key_file', default='json' + os.path.sep + 'keys.json', + help='file with complete list of keys.') + parser.add_argument('--output', default='text', + choices=['text', 'html', 'csv'], + help='output format') + parser.add_argument('--verbose', action='store_true', default=False, + help='whether to indicate which messages were translated ' + '(only used in text and html output modes)') + parser.add_argument('--app', default=None, choices=APPS, + help='if set, only consider the specified app (prefix).') + parser.add_argument('lang_files', nargs='+', + help='names of JSON files to examine') + args = parser.parse_args() + apps = [args.app] if args.app else APPS + + + # Read in JSON files. + messages = {} # A dictionary of dictionaries. + messages[TOTAL] = read_json_file(args.key_file) + for lang_file in args.lang_files: + prefix = get_prefix(os.path.split(lang_file)[1]) + # Skip non-language files. + if prefix not in ['qqq', 'keys']: + messages[prefix] = read_json_file(lang_file) + + # Output results. + if args.output == 'text': + output_as_text(messages, apps, args.verbose) + elif args.output == 'html': + output_as_html(messages, apps, args.verbose) + elif args.output == 'csv': + output_as_csv(messages, apps) + else: + print('No output?!') + + +if __name__ == '__main__': + main() diff --git a/i18n/tests.py b/i18n/tests.py index 7e4fc49aa..f771a1ede 100644 --- a/i18n/tests.py +++ b/i18n/tests.py @@ -4,7 +4,7 @@ # Tests of i18n scripts. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/i18n/xliff_to_json.py b/i18n/xliff_to_json.py index b38b4d6ec..4665173c0 100755 --- a/i18n/xliff_to_json.py +++ b/i18n/xliff_to_json.py @@ -3,7 +3,7 @@ # Converts .xlf files into .json files for use at http://translatewiki.net. # # Copyright 2013 Google Inc. -# https://developers.google.com/blockly/ +# https://blockly.googlecode.com/ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/media/anon.jpeg b/media/anon.jpeg new file mode 100644 index 000000000..5ac84edbe Binary files /dev/null and b/media/anon.jpeg differ diff --git a/media/backpack-closed.png b/media/backpack-closed.png new file mode 100644 index 000000000..11f5733d1 Binary files /dev/null and b/media/backpack-closed.png differ diff --git a/media/backpack-empty.png b/media/backpack-empty.png new file mode 100644 index 000000000..2136f3506 Binary files /dev/null and b/media/backpack-empty.png differ diff --git a/media/backpack-full.png b/media/backpack-full.png new file mode 100644 index 000000000..dbc51107d Binary files /dev/null and b/media/backpack-full.png differ diff --git a/media/backpack-small.png b/media/backpack-small.png new file mode 100644 index 000000000..b6633decf Binary files /dev/null and b/media/backpack-small.png differ diff --git a/media/backpack-smaller.png b/media/backpack-smaller.png new file mode 100644 index 000000000..d4acea061 Binary files /dev/null and b/media/backpack-smaller.png differ diff --git a/media/backpack.mp3 b/media/backpack.mp3 new file mode 100644 index 000000000..a390e457d Binary files /dev/null and b/media/backpack.mp3 differ diff --git a/media/backpack.ogg b/media/backpack.ogg new file mode 100644 index 000000000..46b449e36 Binary files /dev/null and b/media/backpack.ogg differ diff --git a/media/backpack.wav b/media/backpack.wav new file mode 100644 index 000000000..99fffe907 Binary files /dev/null and b/media/backpack.wav differ diff --git a/media/blockly.css b/media/blockly.css new file mode 100644 index 000000000..04b06388e --- /dev/null +++ b/media/blockly.css @@ -0,0 +1,255 @@ +.blocklySvg { + background-color: #fff; +} +.blocklyWidgetDiv { + position: absolute; + display: none; + z-index: 999; +} +.blocklyDraggable { + cursor: url(handopen.cur) 8 5 , auto; +} +.blocklyResizeSE { + fill: #aaa; + cursor: se-resize; +} +.blocklyResizeSW { + fill: #aaa; + cursor: sw-resize; +} +.blocklyResizeLine { + stroke-width: 1; + stroke: #888; +} +.blocklyHighlightedConnectionPath { + stroke-width: 4px; + stroke: #fc3; + fill: none; +} +.blocklyPathLight { + fill: none; + stroke-width: 2; + stroke-linecap: round; +} +.blocklySelected>.blocklyPath { + stroke-width: 3px; + stroke: #fc3; +} +.blocklySelected>.blocklyPathLight { + display: none; +} +.blocklyDragging>.blocklyPath, .blocklyDragging>.blocklyPathLight { + fill-opacity: .8; + stroke-opacity: .8; +} +.blocklyDragging>.blocklyPathDark { + display: none; +} +.blocklyDisabled>.blocklyPath { + fill-opacity: .5; + stroke-opacity: .5; +} +.blocklyDisabled>.blocklyPathLight, .blocklyDisabled>.blocklyPathDark { + display: none; +} +.blocklyText { + cursor: default; + font-family: sans-serif; + font-size: 11pt; + fill: #fff; +} +.blocklyNonEditableText>text { + pointer-events: none; +} +.blocklyNonEditableText>rect, .blocklyEditableText>rect { + fill: #fff; + fill-opacity: .6; +} +.blocklyNonEditableText>text, .blocklyEditableText>text { + fill: #000; +} +.blocklyEditableText:hover>rect { + stroke-width: 2; + stroke: #fff; +} +.blocklySvg text { + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + cursor: inherit; +} +.blocklyHidden { + display: none; +} +.blocklyTooltipBackground { + fill: #ffffc7; + stroke-width: 1px; + stroke: #d8d8d8; +} +.blocklyTooltipShadow, .blocklyContextMenuShadow, .blocklyDropdownMenuShadow { + fill: #bbb; + filter: url(#blocklyShadowFilter); +} +.blocklyTooltipText { + font-family: sans-serif; + font-size: 9pt; + fill: #000; +} +.blocklyIconShield { + cursor: default; + fill: #00c; + stroke-width: 1px; + stroke: #ccc; +} +.blocklyIconGroup:hover>.blocklyIconShield { + fill: #00f; + stroke: #fff; +} +.blocklyIconGroup:hover>.blocklyIconMark { + fill: #fff; +} +.blocklyIconMark { + cursor: default !important; + font-family: sans-serif; + font-size: 9pt; + font-weight: bold; + fill: #ccc; + text-anchor: middle; +} +.blocklyMinimalBody { + margin: 0; + padding: 0; +} +.blocklyCommentTextarea { + margin: 0; + padding: 2px; + border: 0; + resize: none; + background-color: #ffc; +} +.blocklyHtmlInput { + font-family: sans-serif; + font-size: 11pt; + border: none; + outline: none; +} +.blocklyContextMenuBackground, .blocklyMutatorBackground { + fill: #fff; + stroke-width: 1; + stroke: #ddd; +} +.blocklyContextMenuOptions>.blocklyMenuDiv, .blocklyContextMenuOptions>.blocklyMenuDivDisabled, .blocklyDropdownMenuOptions>.blocklyMenuDiv { + fill: #fff; +} +.blocklyToolboxOptions>.blocklyMenuDiv { + fill: #ddd; +} +.blocklyToolboxOptions>.blocklyMenuDiv:hover { + fill: #e4e4e4; +} +.blocklyContextMenuOptions>.blocklyMenuDiv:hover>rect, .blocklyDropdownMenuOptions>.blocklyMenuDiv:hover>rect, .blocklyMenuSelected>rect { + fill: #57e; +} +.blocklyMenuText { + cursor: default !important; + font-family: sans-serif; + font-size: 15px; + fill: #000; +} +.blocklyContextMenuOptions>.blocklyMenuDiv:hover>.blocklyMenuText, .blocklyDropdownMenuOptions>.blocklyMenuDiv:hover>.blocklyMenuText, .blocklyMenuSelected>.blocklyMenuText { + fill: #fff; +} +.blocklyMenuDivDisabled>.blocklyMenuText { + fill: #ccc; +} +.blocklyToolboxBackground { + fill: #ddd; +} +.blocklyFlyoutBackground { + fill: #ddd; + fill-opacity: .8; +} +.blocklyColourBackground { + fill: #666; +} +.blocklyScrollbarBackground { + fill: #fff; + stroke-width: 1; + stroke: #e4e4e4; +} +.blocklyScrollbarKnob { + fill: #ccc; +} +.blocklyScrollbarBackground:hover+.blocklyScrollbarKnob, .blocklyScrollbarKnob:hover { + fill: #bbb; +} +.blocklyInvalidInput { + background: #faa; +} +.goog-palette { + outline: none; + cursor: default; +} +.goog-palette-table { + border: 1px solid #666; + border-collapse: collapse; +} +.goog-palette-cell { + height: 13px; + width: 15px; + margin: 0; + border: 0; + text-align: center; + vertical-align: middle; + border-right: 1px solid #666; + font-size: 1px; +} +.goog-palette-colorswatch { + position: relative; + height: 13px; + width: 15px; + border: 1px solid #666; +} +.goog-palette-cell-hover .goog-palette-colorswatch { + border: 1px solid #fff; +} +.goog-palette-cell-selected .goog-palette-colorswatch { + border: 1px solid #000; + color: #fff; +} + + +.blocklyErrorIconShield { + cursor: default; + fill: #f00; + stroke-width: 1px; + stroke: #ccc; +} +.blocklyIconGroup:hover>.blocklyErrorIconShield { + fill: #f00; + stroke: #fff; +} + +.blocklyWarningIconShield { + cursor: default; + fill: #ff5; + stroke-width: 1px; + stroke: #555; +} +.blocklyIconGroup:hover>.blocklyWarningIconShield { + fill: #ff0; + stroke: #000; +} + +.blocklyWarningIconMark { + cursor: default !important; + font-family: sans-serif; + font-size: 9pt; + font-weight: bold; + fill: #555; + text-anchor: middle; +} + +.blocklyIconGroup:hover>.blocklyWarningIconMark { + fill: #000; +} diff --git a/media/progress.gif b/media/progress.gif new file mode 100644 index 000000000..e97adb875 Binary files /dev/null and b/media/progress.gif differ