mirror of
https://github.com/google/blockly.git
synced 2026-01-09 10:00:09 +01:00
FieldDropdown now has onItemSelected(..) method to handle the menu item selection action. Variable renames and deletes are now handled here, instead of during validation. Also fixes an issue deleting variables used by less than two blocks. Fixes #723.
503 lines
15 KiB
JavaScript
503 lines
15 KiB
JavaScript
/**
|
|
* @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.array');
|
|
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.<!Blockly.Block>}
|
|
* @private
|
|
*/
|
|
this.topBlocks_ = [];
|
|
/**
|
|
* @type {!Array.<!Function>}
|
|
* @private
|
|
*/
|
|
this.listeners_ = [];
|
|
/**
|
|
* @type {!Array.<!Blockly.Events.Abstract>}
|
|
* @private
|
|
*/
|
|
this.undoStack_ = [];
|
|
/**
|
|
* @type {!Array.<!Blockly.Events.Abstract>}
|
|
* @private
|
|
*/
|
|
this.redoStack_ = [];
|
|
/**
|
|
* @type {!Object}
|
|
* @private
|
|
*/
|
|
this.blockDB_ = Object.create(null);
|
|
/*
|
|
* @type {!Array.<string>}
|
|
* A list of all of the named variables in the workspace, including variables
|
|
* that are not currently in use.
|
|
*/
|
|
this.variableList = [];
|
|
};
|
|
|
|
/**
|
|
* Returns `true` if the workspace is visible and `false` if it's headless.
|
|
* @type {boolean}
|
|
*/
|
|
Blockly.Workspace.prototype.rendered = false;
|
|
|
|
/**
|
|
* Maximum number of undo events in stack. `0` turns off undo, `Infinity` sets it to unlimited.
|
|
* @type {number}
|
|
*/
|
|
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) {
|
|
if (!goog.array.remove(this.topBlocks_, block)) {
|
|
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.<!Blockly.Block>} 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.<!Blockly.Block>} 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.
|
|
// The case might have changed, so we update the destination ID.
|
|
this.variableList[newVariableIndex] = newName;
|
|
this.variableList.splice(variableIndex, 1);
|
|
} 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.<!Blockly.Block>} 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 workspace = this;
|
|
var variableIndex = this.variableIndexOf(name);
|
|
if (variableIndex != -1) {
|
|
// Check whether this variable is a function parameter before deleting.
|
|
var uses = this.getVariableUses(name);
|
|
for (var i = 0, block; block = uses[i]; i++) {
|
|
if (block.type == 'procedures_defnoreturn' ||
|
|
block.type == 'procedures_defreturn') {
|
|
var procedureName = block.getFieldValue('NAME');
|
|
Blockly.alert(
|
|
Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.
|
|
replace('%1', name).
|
|
replace('%2', procedureName));
|
|
return;
|
|
}
|
|
}
|
|
|
|
function doDeletion() {
|
|
Blockly.Events.setGroup(true);
|
|
for (var i = 0; i < uses.length; i++) {
|
|
uses[i].dispose(true, false);
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
workspace.variableList.splice(variableIndex, 1);
|
|
}
|
|
if (uses.length > 1) {
|
|
// Confirm before deleting multiple blocks.
|
|
Blockly.confirm(
|
|
Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length).
|
|
replace('%2', name),
|
|
function(ok) {
|
|
if (ok) {
|
|
doDeletion();
|
|
}
|
|
});
|
|
} else {
|
|
// No confirmation necessary for a single block.
|
|
doDeletion();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
goog.array.remove(this.listeners_, func);
|
|
};
|
|
|
|
/**
|
|
* 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;
|