From 16d1a6b3628e4670de6f39c05bcd1caaa1717319 Mon Sep 17 00:00:00 2001 From: "Evan W. Patton" Date: Wed, 28 Sep 2016 10:36:40 -0400 Subject: [PATCH] [WIP] Rebase from Blockly SVN r1757 to git 641d720 --- core/block.js | 30 +- core/block.js.orig | 1364 +++++++++++++++++++++++++++++ core/block.js.rej | 510 +++++++++++ core/block_svg.js.orig | 1629 +++++++++++++++++++++++++++++++++++ core/block_svg.js.rej | 231 +++++ core/blockly.js | 15 + core/blockly.js.orig | 453 ++++++++++ core/blockly.js.rej | 461 ++++++++++ core/blocks.js.orig | 33 + core/blocks.js.rej | 19 + core/bubble.js | 7 +- core/bubble.js.orig | 579 +++++++++++++ core/connection.js | 3 + core/connection.js.orig | 615 +++++++++++++ core/connection.js.rej | 71 ++ core/css.js | 85 +- core/css.js.orig | 786 +++++++++++++++++ core/css.js.rej | 56 ++ core/field.js.orig | 495 +++++++++++ core/field.js.rej | 37 + core/field_dropdown.js | 16 + core/field_dropdown.js.orig | 320 +++++++ core/flyout.js | 14 + core/flyout.js.orig | 1364 +++++++++++++++++++++++++++++ core/flyout.js.rej | 17 + core/inject.js.orig | 378 ++++++++ core/inject.js.rej | 129 +++ core/input.js | 4 + core/input.js.orig | 241 ++++++ core/input.js.rej | 23 + core/mutator.js.orig | 389 +++++++++ core/mutator.js.rej | 63 ++ core/procedures.js.orig | 287 ++++++ core/procedures.js.rej | 46 + core/scrollbar.js | 6 +- core/scrollbar.js.orig | 750 ++++++++++++++++ core/scrollbar.js.rej | 16 + core/trashcan.js | 5 + core/trashcan.js.orig | 332 +++++++ core/trashcan.js.rej | 16 + core/typeblock.js | 644 ++++++++++++++ core/variables.js.orig | 273 ++++++ core/variables.js.rej | 17 + core/warning.js.orig | 185 ++++ core/warning.js.rej | 60 ++ core/workspace.js | 11 + core/workspace.js.orig | 501 +++++++++++ core/workspace.js.rej | 176 ++++ core/xml.js.orig | 566 ++++++++++++ core/xml.js.rej | 251 ++++++ i18n/common.py | 4 +- i18n/create_messages.py | 23 +- i18n/dedup_json.py | 2 +- i18n/js_to_json.py | 2 +- i18n/json_to_js.py | 2 +- i18n/status.py | 247 ++++++ i18n/tests.py | 2 +- i18n/xliff_to_json.py | 2 +- media/anon.jpeg | Bin 0 -> 2064 bytes media/backpack-closed.png | Bin 0 -> 3917 bytes media/backpack-empty.png | Bin 0 -> 4937 bytes media/backpack-full.png | Bin 0 -> 5815 bytes media/backpack-small.png | Bin 0 -> 22312 bytes media/backpack-smaller.png | Bin 0 -> 19389 bytes media/backpack.mp3 | Bin 0 -> 17000 bytes media/backpack.ogg | Bin 0 -> 14118 bytes media/backpack.wav | Bin 0 -> 131370 bytes media/blockly.css | 255 ++++++ media/progress.gif | Bin 0 -> 19602 bytes 69 files changed, 15087 insertions(+), 31 deletions(-) create mode 100644 core/block.js.orig create mode 100644 core/block.js.rej create mode 100644 core/block_svg.js.orig create mode 100644 core/block_svg.js.rej create mode 100644 core/blockly.js.orig create mode 100644 core/blockly.js.rej create mode 100644 core/blocks.js.orig create mode 100644 core/blocks.js.rej create mode 100644 core/bubble.js.orig create mode 100644 core/connection.js.orig create mode 100644 core/connection.js.rej create mode 100644 core/css.js.orig create mode 100644 core/css.js.rej create mode 100644 core/field.js.orig create mode 100644 core/field.js.rej create mode 100644 core/field_dropdown.js.orig create mode 100644 core/flyout.js.orig create mode 100644 core/flyout.js.rej create mode 100644 core/inject.js.orig create mode 100644 core/inject.js.rej create mode 100644 core/input.js.orig create mode 100644 core/input.js.rej create mode 100644 core/mutator.js.orig create mode 100644 core/mutator.js.rej create mode 100644 core/procedures.js.orig create mode 100644 core/procedures.js.rej create mode 100644 core/scrollbar.js.orig create mode 100644 core/scrollbar.js.rej create mode 100644 core/trashcan.js.orig create mode 100644 core/trashcan.js.rej create mode 100644 core/typeblock.js create mode 100644 core/variables.js.orig create mode 100644 core/variables.js.rej create mode 100644 core/warning.js.orig create mode 100644 core/warning.js.rej create mode 100644 core/workspace.js.orig create mode 100644 core/workspace.js.rej create mode 100644 core/xml.js.orig create mode 100644 core/xml.js.rej create mode 100644 i18n/status.py create mode 100644 media/anon.jpeg create mode 100644 media/backpack-closed.png create mode 100644 media/backpack-empty.png create mode 100644 media/backpack-full.png create mode 100644 media/backpack-small.png create mode 100644 media/backpack-smaller.png create mode 100644 media/backpack.mp3 create mode 100644 media/backpack.ogg create mode 100644 media/backpack.wav create mode 100644 media/blockly.css create mode 100644 media/progress.gif 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 0000000000000000000000000000000000000000..5ac84edbe28c0aafe0b6254f1f22198bdcaff0d2 GIT binary patch literal 2064 zcmb7=c~q0x8OFaaVHd=xFam}+sKJgh7G#q^K%wk0MGspj(18(IMfMO9z*9RY0x2LE zc80|iK^T^RN>~R-G02jzM-l>L5E&*31W?M-WZJR%U;Ey3?!D(e=lwnJa}TBu=7A&5 z_D=Qy2m$~oUBCecumzMA6qOVel$8{fR8*7?scEXKsj8~!9M?Fac?zzlcM6U`oH=KF z;SADHAAvBiGB7kTv#_u@ecsy6+T7OovW1xp0;;H}sHv)HtE+39okg5A`@id;8-OVR ze1H!H^#BMAgu=jsK0sSKBovg5`?2KY6+kFNs+k=D0SE|@gD5CN6#p%Q5U3nX{^*xi z45NSLy~) z_PUPmn{ay81_C7BG`MOg^t>6TDmvm&cK=LnGOV-|UQd?j@x+6O&VOvxJLAHL-sgkG zSsqp49PGWDW~(jz6}NuGa}sdA_GOwC#RWBAWO24`(Vo{kxHSYCcOgIK5y<(}U48^Y*?}+8Z?+x)IsIgq^GX|_{U|b6quc2EjPsiY`*^m*>q$@ibuYcbtrfiv2Y*IC`^}DFzv%WxssWi zf;&$mF835yCx&Qj+cQ^6LzCd(x80xcHH&pHe?gYU_+^JT=8!_fnRHX1-#yp7_NSR2 zh9oV2Sd`>3nPrO=P;4YQ@{t@@2FOhrn3 zNgizv=~{qB4efCxyZI9W%iYftLD?>kM?*z|(!7V!oL4Cl1F*! zNiHpZo4CuLm#i|fPYQz}S+b_zN--?w&eD@$G2?b*d)Lyu_gKxdip_@}2@f*)6OnS` zF)PdR)X9DpI&N(*9}6$0`}w6BmIIb?1)bz-=Hreq6mhwE2Ftj#fh!ChNoz=%g$JVW z%tVbxS-3t|p_O2iF#4O-e@+sgNkS8TVoZjy0vE_J4%88{Te?v|>t@+25^JbhL|VMh zxG-eL3L1m&4TcYRPvQMtM7nV{ppI|d6SBI=OD^Gh$=U_U>{Lh7wIuKSg*Id+V>==qgU|R4Xp75s;I|ui8^> zYuJ&d)T$~HJ!+@dB*=h|YPisOtchV0{PCmYhU_9QwVVlQ(a!W>!`?RTCS)}WR9mrU z@+&dY5RaemcS`KphHs;GJ*C1SUw!qS>djg|Vi|mAM3kT96IqQ!{Jqc*L1hyr8cg<4 zn~#2Q=C%WYHZljhi92g(>SW=%|031NMd#v)k$3obDwf_y2u)g9)Q#H9)*Fjd-UuC| z;ndKz_0AE=Qw>g@@HWoFv_;Fa@9?*&3LDr+EGaO|VQ%g)5_yZKYl!}R%PW87nZR;m znFF{@?J06ktXC1XWiG#CY!iZGTPFpDn;Gt9e!LcAb1_>(kFu&!hqpUhI!toFFY;cd zRq-izT>bTFOtDX|<@e@CT6teNbu0XelOX{>F2XNO;i*-1d{@|1Rj(sgoUv7#w#*U+ zZ$0n)is7|!FPK8%>f=Kn(XnYeTCJ5>-C@E~Rs4tE>F~`PO!a=eolvK{ITb*7iFs%o zzkgH6n}ZA1Y4cuzxyXvZXVm;ox7`;b7c*{OqJ2Z{!JxGl7&;z}?{~T7>vh0D>aRn5 z+%C~=T_jcx2B*WlE!YApK0Il_&>nNxg;bYJC-HjIEe3?w=CnE4k1 CPj8O^ literal 0 HcmV?d00001 diff --git a/media/backpack-closed.png b/media/backpack-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..11f5733d121065a668331826566538368dc8dfa0 GIT binary patch literal 3917 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 b{}*5YKCVSxhh1b000000NkvXXu0mjfx$PXl literal 0 HcmV?d00001 diff --git a/media/backpack-empty.png b/media/backpack-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..2136f35062ee2ebbf0650f164f3a7750d608eebe GIT binary patch literal 4937 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 Bzw7`2 literal 0 HcmV?d00001 diff --git a/media/backpack-small.png b/media/backpack-small.png new file mode 100644 index 0000000000000000000000000000000000000000..b6633decf4f8e5568d5e6e35f5a9bf08049413e5 GIT binary patch literal 22312 zcmV*#KsvvPP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)xRzFEZK~#9!-Mwd%WLJ9M`8(&j^jRj$byZgzw1aJUU@)HK zh(k)`24z-^*w72beiD6)jo6Ki4TZE)EF~H(MUGZ8B!^=#GZ+kfqtU*ry31Fk&%FNZ zi*sFO7ckuzUq-(d`u1?#Xq3q9}ZMM<|qg}6|$j}a)c0zzzJX;m<868`?EkpDK$p|03k#LaFkNn z^$38Zaw_Cb*Yf21zi07kE*s)UmylT zrPNpmF;YtH3L)MAz6ZPwY@@YE{6z)97xSzNN5P>p(L!AYo(H}HJOiwu?Rgonc+koF zwVY;hi_ik!1KtI`1N<%8WCVcvq5S z(GbP&j?m8IF7QL(KLM|!eTXlVHu&P42@eRqieBB%0bd1PR7$N0AsT2~E+69aCVrnn zp_INwQCzqm6Du_#W%C4qqS;d||@C zf~v0or+_a3zXd#tw)Hh3M5^LH?#P~t-A{z6WR!2q~zQtCXA)rfE{ERH;|$)T=e>)f$fNV3`)SX(0_M1XDQ64+CflA)ZG& zqjN%te+OJsO2yep{|3SE8;41~Q>nkT&nqQz+u2L?QFr+c@)27ZfKt~Aw0r+>oEehdc;X33$ z?$-c}-zWeItG)p|4g3?}n@Xv3LWnxYXsGl(=|u~|fU)P&?smAlwaLxRTio5=WOL^( z+xNHF+}on#cJa_CGL?Z5>zhzAxdJM`*g|MU5Jz~zBTxYW5n&u+#tK{6R4Nq~mzH_# zt3&0$?H(z)YnWhb+h_O3n_h_G+cW?6EwJUsh>qG9`zRlL&Hb-F(X<1aQ zI+kN$&(x?iXK6HMXw+w@R%0+&YU=f zW7{aD5V^Cc(3J8fI!frGql0~5Cwc4uI7%MtC&2I%V0@-P__YF{Ap9lZ>%c!nJ9|s$ zRbPBI{jK^ohDg4?1kO*I*L%ZJt{yCb^Y4RW?3xFT^Z13;#&b9aWo1cB3pT6}5_qR4N zDpi(NPqTRD6su>?Fw<(`lq=XJ2g5Ycmsvq-HAKP;B`Tg|rDrRiM8N553gMniUWb?j zgOm~>bhHt89>ZRrd$(?L_02c=@SUF%jK;k3!k75hfAp{T?U%nsB?m!za@nPszEtRy z?xD@Y0O%xOcqh4ih|WKZe^ns-TEf7B@XrAMR4Mh(g%D4mWBuY!7bF+;sMq2B8&~+w z55LRb{_Jnr*tm6(atM~D(22%K^mr&6IgH^5E%Gam==s@_~0Wc-}f1#B6 zpM?;YfYQ{D1_7?;a`WCDzWe&$@t1%1SG;rMeS~APe(5sjp1jP;sZ&(ybu80D%H(Y- z#*ikaX<`@>)07!`V@in>vao`AfY6B(B1kDk9K}RYOcckcSP=^?XrTlmi57~V6Ge)^ zckx`8VXw!i-^Vm1Ltwfktii$CxED- zsqn`VF#HhsOSD$#Dy1G-8+@(+DA@dyN~u2r{zW2uXIl8NifMN{ym93%{_}tTbN>4e zzDK9uqjmZ;7oK~G(-$r=Ge4&tI@7>1Of1vHwoFXZz%+~u0ES^;NQof@hG}3V0Fdd* zp1SZ#X@SRaOcX_gQH&pij9d@T^U<7*KqY2EqL;5X8q+;GV(0E%_U>)5d;cDt`+NA^ z5q-DE&>IrO0e}0mA5b?deD#T!Y1Hb}>vbCSx~SG_l*(mFr4l8_!Ln@(BjXaLO+%XB zaDWA+)J&4?ya4g~wNAZJr)1mMmWgdy*p`W9Sy+~#?KGm`%@@MD!iq0C zd8xObcmUeLOZ)~6NX2k8=CIvm?0UHdk=ji3j*htf!4+=5cZGX5uHy}SoO+46Q9*eT z?NNu$sDn2i6ITSw)dkMfS7}vesMqQ=8g;7G8qG$Ng@pyy)>c_wUSVc-hGw%#wf2d8 zLVCnXseusUeKfD|r)X#Kp+WF@n*bYlQYrOqbTX$z!5Ihb=zVzWBmU&S{{?^c-M`@O z_9in+%RKSIm$-QOc@~$Ks8y?!9ShsGFbyMfzywmO?O166soa}S)ienZWB~wi(V-&& zNMfmkP)I2-gupUP1cE5>3xt&T!!bK|H@WiTpK#@kpJGQQCoi61<;-bT&z)gq z>X(>ZT%c4gV>>n~R`|ZpXfWjJyYKLqfAa77@E7l}Jh#eYt7oZ|tEi}umG4anTCHh~Py_->DHnK82=TuGe};}6eyL;ld1?R~cpUhW z5Mq_ddOQ5U=gli`^S}S+f6HHf|F7xxJDfOko+n@ZDi@!*%+lfl)sll@q^mqG2pE)* zNg__@RhtPhd5ub+11aJ>^h4<9L~bsW&`G@%05OXGaL8e|i|YpnA@D~-Ze96+pZu5a zuzmF=mtMKdxBlfH@$5@4QExVpNfe@#0s|_Q3bk5|O7%pb

ry1 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 Ts>J%;00000NkvXXu0mjfD+5gp literal 0 HcmV?d00001 diff --git a/media/backpack-smaller.png b/media/backpack-smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..d4acea061344307b6d45a66e148da2369e2ceb03 GIT binary patch literal 19389 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 L-`o6uV1fSw8`Sh* literal 0 HcmV?d00001 diff --git a/media/backpack.ogg b/media/backpack.ogg new file mode 100644 index 0000000000000000000000000000000000000000..46b449e36873b9f117c854265ae86799886bfcfd GIT binary patch literal 14118 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 literal 0 HcmV?d00001 diff --git a/media/backpack.wav b/media/backpack.wav new file mode 100644 index 0000000000000000000000000000000000000000..99fffe907cf2e9414567e39146e7b894f1a49612 GIT binary patch literal 131370 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

+}S5&*xcB+ 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

-lD-MQXL?UDmg=AGA0vx?TNF-1u@$HV82$WO2GV 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 I#fF;xznWFW2LJ#7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e97adb87586cfd3e6e9055bf80f39d03a455c3d1 GIT binary patch literal 19602 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 MOa`wDPDBC!2h(fb1poj5 literal 0 HcmV?d00001