123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- /**
- * @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;
|