1422 lines
49 KiB
JavaScript
1422 lines
49 KiB
JavaScript
// -*- mode: javascript; js-indent-level: 2; -*-
|
|
// Copyright © 2016-2018 Massachusetts Institute of Technology. All rights reserved.
|
|
|
|
/**
|
|
* @license
|
|
* @fileoverview Visual blocks editor for MIT App Inventor
|
|
* App Inventor extensions to Blockly's SVG Workspace
|
|
*
|
|
* @author ewpatton@mit.edu (Evan W. Patton)
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
//goog.provide('AI.Blockly.WorkspaceSvg');
|
|
|
|
//goog.require('Blockly.WorkspaceSvg');
|
|
|
|
/**
|
|
* AI2 Blocks Drawer
|
|
* @type {Blockly.Drawer}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.drawer_ = null;
|
|
|
|
/**
|
|
* The workspace's backpack (if any).
|
|
* @type {Blockly.Backpack}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.backpack_ = null;
|
|
|
|
/**
|
|
* The workspace's component database.
|
|
* @type {Blockly.ComponentDatabase}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.componentDb_ = null;
|
|
|
|
/**
|
|
* The workspace's typeblock instance.
|
|
* @type {Blockly.TypeBlock}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.typeBlock_ = null;
|
|
|
|
/**
|
|
* Shared flydown for parameters and variables.
|
|
* @type {Blockly.Flydown}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.flydown_ = null;
|
|
|
|
/**
|
|
* A list of blocks that need rendering the next time the workspace is shown.
|
|
* @type {?Array.<Blockly.BlockSvg>}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.blocksNeedingRendering = null;
|
|
|
|
/**
|
|
* latest clicked position is used to open the type blocking suggestions window
|
|
* Initial position is 0,0
|
|
* @type {{x: number, y: number}}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.latestClick = { x: 0, y: 0 };
|
|
|
|
/**
|
|
* Whether the workspace elements are hidden
|
|
* @type {boolean}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.chromeHidden = false;
|
|
|
|
/**
|
|
* Wrap the onMouseClick_ event to handle additional behaviors.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.onMouseDown_ = (function(func) {
|
|
if (func.isWrapped) {
|
|
return func;
|
|
} else {
|
|
var f = function(e) {
|
|
try {
|
|
var metrics = Blockly.mainWorkspace.getMetrics();
|
|
var point = Blockly.utils.mouseToSvg(e, this.getParentSvg(), this.getInverseScreenCTM());
|
|
point.x = (point.x + metrics.viewLeft) / this.scale;
|
|
point.y = (point.y + metrics.viewTop) / this.scale;
|
|
this.latestClick = point;
|
|
return func.call(this, e);
|
|
} finally {
|
|
// focus the workspace's parent typeblocking and other keystrokes
|
|
this.getTopWorkspace().getParentSvg().parentNode.focus();
|
|
//if drawer exists and supposed to close
|
|
if (this.drawer_ && this.drawer_.flyout_.autoClose) {
|
|
this.drawer_.hide();
|
|
}
|
|
if (this.backpack_ && this.backpack_.flyout_.autoClose) {
|
|
this.backpack_.hide();
|
|
}
|
|
|
|
//Closes mutators
|
|
var blocks = this.getAllBlocks();
|
|
var numBlocks = blocks.length;
|
|
var temp_block = null;
|
|
for(var i = 0; i < numBlocks; i++) {
|
|
temp_block = blocks[i];
|
|
if(temp_block.mutator){
|
|
//deselect block in mutator workspace
|
|
if(Blockly.selected && Blockly.selected.workspace && Blockly.selected.workspace!=Blockly.mainWorkspace){
|
|
Blockly.selected.unselect();
|
|
}
|
|
blocks[i].mutator.setVisible(false);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
f.isWrapper = true;
|
|
return f;
|
|
}
|
|
})(Blockly.WorkspaceSvg.prototype.onMouseDown_);
|
|
|
|
Blockly.WorkspaceSvg.prototype.createDom = (function(func) {
|
|
if (func.isWrapped) {
|
|
return func;
|
|
} else {
|
|
var f = function() {
|
|
var self = /** @type {Blockly.WorkspaceSvg} */ this;
|
|
var result = func.apply(this, Array.prototype.slice.call(arguments));
|
|
// BEGIN: Configure drag and drop of blocks images to workspace
|
|
result.addEventListener('dragenter', function(e) {
|
|
if (e.dataTransfer.types.indexOf('Files') >= 0 ||
|
|
e.dataTransfer.types.indexOf('text/uri-list') >= 0) {
|
|
self.svgBackground_.style.fill = 'rgba(0, 255, 0, 0.3)';
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
e.preventDefault();
|
|
}
|
|
}, true);
|
|
result.addEventListener('dragover', function(e) {
|
|
if (e.dataTransfer.types.indexOf('Files') >= 0 ||
|
|
e.dataTransfer.types.indexOf('text/uri-list') >= 0) {
|
|
self.svgBackground_.style.fill = 'rgba(0, 255, 0, 0.3)';
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
e.preventDefault();
|
|
}
|
|
}, true);
|
|
result.addEventListener('dragleave', function(e) {
|
|
self.setGridSettings(self.options.gridOptions['enabled'], self.options.gridOptions['snap']);
|
|
}, true);
|
|
result.addEventListener('dragexit', function(e) {
|
|
self.setGridSettings(self.options.gridOptions['enabled'], self.options.gridOptions['snap']);
|
|
}, true);
|
|
result.addEventListener('drop', function(e) {
|
|
self.setGridSettings(self.options.gridOptions['enabled'], self.options.gridOptions['snap']);
|
|
if (e.dataTransfer.types.indexOf('Files') >= 0) {
|
|
if (e.dataTransfer.files.item(0).type === 'image/png') {
|
|
e.preventDefault();
|
|
var metrics = Blockly.mainWorkspace.getMetrics();
|
|
var point = Blockly.utils.mouseToSvg(e, self.getParentSvg(), self.getInverseScreenCTM());
|
|
point.x = (point.x + metrics.viewLeft) / self.scale;
|
|
point.y = (point.y + metrics.viewTop) / self.scale;
|
|
Blockly.importPngAsBlock(self, point, e.dataTransfer.files.item(0));
|
|
}
|
|
} else if (e.dataTransfer.types.indexOf('text/uri-list') >= 0) {
|
|
var data = e.dataTransfer.getData('text/uri-list')
|
|
if (data.match(/\.png$/)) {
|
|
e.preventDefault();
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
var metrics = Blockly.mainWorkspace.getMetrics();
|
|
var point = Blockly.utils.mouseToSvg(e, self.getParentSvg(), self.getInverseScreenCTM());
|
|
point.x = (point.x + metrics.viewLeft) / self.scale;
|
|
point.y = (point.y + metrics.viewTop) / self.scale;
|
|
Blockly.importPngAsBlock(self, point, xhr.response);
|
|
}
|
|
};
|
|
xhr.responseType = 'blob';
|
|
xhr.open('GET', data, true);
|
|
xhr.send();
|
|
}
|
|
}
|
|
});
|
|
// END: Configure drag and drop of blocks images to workspace
|
|
return result;
|
|
};
|
|
f.isWrapper = true;
|
|
return f;
|
|
}
|
|
})(Blockly.WorkspaceSvg.prototype.createDom);
|
|
|
|
Blockly.WorkspaceSvg.prototype.dispose = (function(func) {
|
|
if (func.isWrapped) {
|
|
return func;
|
|
} else {
|
|
var wrappedFunc = function() {
|
|
func.call(this);
|
|
if (this.backpack_) {
|
|
this.backpack_.dispose();
|
|
return null;
|
|
}
|
|
};
|
|
wrappedFunc.isWrapped = true;
|
|
return wrappedFunc;
|
|
}
|
|
})(Blockly.WorkspaceSvg.prototype.dispose);
|
|
|
|
/**
|
|
* Add the warning handler.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addWarningHandler = function() {
|
|
if (!this.warningHandler_) {
|
|
this.warningHandler_ = new Blockly.WarningHandler(this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Adds the warning indicator.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addWarningIndicator = function() {
|
|
if (!this.options.readOnly && this.warningIndicator_ == null) {
|
|
if (!this.warningHandler_) {
|
|
this.warningHandler_ = new Blockly.WarningHandler(this);
|
|
}
|
|
this.warningIndicator_ = new Blockly.WarningIndicator(this);
|
|
var svgWarningIndicator = this.warningIndicator_.createDom();
|
|
this.svgGroup_.appendChild(svgWarningIndicator);
|
|
this.warningIndicator_.init();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add a backpack.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addBackpack = function() {
|
|
if (Blockly.Backpack && !this.options.readOnly) {
|
|
this.backpack_ = new Blockly.Backpack(this, {
|
|
scrollbars: true,
|
|
media: './assets/',
|
|
disabledPatternId: this.options.disabledPatternId,
|
|
});
|
|
var svgBackpack = this.backpack_.createDom(this);
|
|
this.svgGroup_.appendChild(svgBackpack);
|
|
this.backpack_.init();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle backpack rescaling
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setScale = (function(func) {
|
|
if (func.isWrapped) {
|
|
return func;
|
|
} else {
|
|
var wrappedFunction = function(newScale) {
|
|
func.call(this, newScale);
|
|
if (this.backpack_) {
|
|
this.backpack_.flyout_.reflow();
|
|
}
|
|
};
|
|
wrappedFunction.isWrapped = true;
|
|
return wrappedFunction;
|
|
}
|
|
})(Blockly.WorkspaceSvg.prototype.setScale);
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Hide the blocks drawer.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.hideDrawer = function() {
|
|
if (this.drawer_)
|
|
this.drawer_.hide();
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Show the blocks drawer for the built-in category.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.showBuiltin = function(name) {
|
|
if (this.drawer_)
|
|
this.drawer_.showBuiltin(name);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Show the drawer with generic blocks for a component type.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.showGeneric = function(name) {
|
|
if (this.drawer_)
|
|
this.drawer_.showGeneric(name);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Show the drawer for a component instance.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.showComponent = function(component) {
|
|
if (this.drawer_)
|
|
this.drawer_.showComponent(component);
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Check whether the drawer is showing.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isDrawerShowing = function() {
|
|
if (this.drawer_) {
|
|
return this.drawer_.isShowing();
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Render the workspace.
|
|
* @param {Array.<Blockly.BlockSvg>=} blocks
|
|
*/
|
|
// Override Blockly's render with optimized version from lyn
|
|
Blockly.WorkspaceSvg.prototype.render = function(blocks) {
|
|
this.rendered = true;
|
|
this.bulkRendering = true;
|
|
Blockly.Field.startCache();
|
|
try {
|
|
if (Blockly.Instrument.isOn) {
|
|
var start = new Date().getTime();
|
|
}
|
|
// [lyn, 04/08/14] Get both top and all blocks for stats
|
|
var topBlocks = blocks || this.getTopBlocks(/* ordered */ false);
|
|
var allBlocks = this.getAllBlocks();
|
|
if (Blockly.Instrument.useRenderDown) {
|
|
for (var t = 0, topBlock; topBlock = topBlocks[t]; t++) {
|
|
Blockly.Instrument.timer(
|
|
function () {
|
|
topBlock.render(false);
|
|
},
|
|
function (result, timeDiffInner) {
|
|
Blockly.Instrument.stats.renderDownTime += timeDiffInner;
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
for (var x = 0, block; block = allBlocks[x]; x++) {
|
|
if (!block.getChildren().length) {
|
|
block.render();
|
|
}
|
|
}
|
|
}
|
|
if (Blockly.Instrument.isOn) {
|
|
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;
|
|
}
|
|
} finally {
|
|
this.bulkRendering = false;
|
|
this.requestConnectionDBUpdate();
|
|
Blockly.Field.stopCache(); // must balance with startCache() call above
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Obtain the {@link Blockly.ComponentDatabase} associated with the workspace.
|
|
*
|
|
* @returns {!Blockly.ComponentDatabase}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getComponentDatabase = function() {
|
|
return this.componentDb_;
|
|
};
|
|
|
|
/**
|
|
* Obtain the {@link Blockly.ProcedureDatabase} associated with the workspace.
|
|
* @returns {!Blockly.ProcedureDatabase}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getProcedureDatabase = function() {
|
|
return this.procedureDb_;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Add a new component to the workspace.
|
|
*
|
|
* @param {string} uid
|
|
* @param {string} instanceName
|
|
* @param {string} typeName
|
|
* @returns {Blockly.WorkspaceSvg} The workspace for call chaining.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addComponent = function(uid, instanceName, typeName) {
|
|
if (this.componentDb_.addInstance(uid, instanceName, typeName)) {
|
|
this.typeBlock_.needsReload.components = true;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Remove a component from the workspace.
|
|
*
|
|
* @param {string} uid The component's unique identifier
|
|
* @returns {Blockly.WorkspaceSvg} The workspace for call chaining.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.removeComponent = function(uid) {
|
|
var component = this.componentDb_.getInstance(uid);
|
|
|
|
// Fixes #1175
|
|
if (this.drawer_ && component.name === this.drawer_.lastComponent) {
|
|
this.drawer_.hide();
|
|
}
|
|
|
|
if (!this.componentDb_.removeInstance(uid)) {
|
|
return this;
|
|
}
|
|
this.typeBlock_.needsReload.components = true;
|
|
var blocks = this.getAllBlocks();
|
|
for (var i = 0, block; block = blocks[i]; ++i) {
|
|
if (block.category == 'Component'
|
|
&& block.getFieldValue('COMPONENT_SELECTOR') == component.name) {
|
|
block.dispose(true);
|
|
}
|
|
}
|
|
Blockly.hideChaff();
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Rename a component in the workspace.
|
|
*
|
|
* @param {!string} uid The unique identifier of the component.
|
|
* @param {!string} oldName The previous name of the component.
|
|
* @param {!string} newName The new name of the component.
|
|
* @returns {Blockly.WorkspaceSvg} The workspace for call chaining.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.renameComponent = function(uid, oldName, newName) {
|
|
if (!this.componentDb_.renameInstance(uid, oldName, newName)) {
|
|
console.log('Renaming: No such component instance ' + oldName + '; aborting.');
|
|
return this;
|
|
}
|
|
this.typeBlock_.needsReload.components = true;
|
|
var blocks = this.getAllBlocks();
|
|
for (var i = 0, block; block = blocks[i]; ++i) {
|
|
if (block.category == 'Component' && block.rename(oldName, newName)) {
|
|
this.blocksNeedingRendering.push(block);
|
|
}
|
|
}
|
|
Blockly.hideChaff();
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java.
|
|
/**
|
|
* Populate the component type database with the components encoded by
|
|
* strComponentInfos.
|
|
*
|
|
* @param {string} strComponentInfos String containing JSON-encoded
|
|
* @param {Object.<string, string>} translations Translation dictionary provided by GWT
|
|
* component information.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.populateComponentTypes = function(strComponentInfos, translations) {
|
|
this.componentDb_.populateTypes(JSON.parse(strComponentInfos));
|
|
this.componentDb_.populateTranslations(translations);
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java.
|
|
/**
|
|
* Loads the contents of a blocks file into the workspace.
|
|
*
|
|
* @param {!string} formJson JSON string containing structure of the Form
|
|
* @param {!string} blocksContent XML serialization of the blocks
|
|
* @returns {Blockly.WorkspaceSvg} The workspace for call chaining.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.loadBlocksFile = function(formJson, blocksContent) {
|
|
if (blocksContent.length != 0) {
|
|
try {
|
|
Blockly.Events.disable();
|
|
this.isLoading = true;
|
|
if (Blockly.Versioning.upgrade(formJson, blocksContent, this)) {
|
|
var self = this;
|
|
setTimeout(function() {
|
|
self.fireChangeListener(new AI.Events.ForceSave(self));
|
|
});
|
|
}
|
|
} finally {
|
|
this.isLoading = false;
|
|
Blockly.Events.enable();
|
|
}
|
|
if (this.getCanvas() != null) {
|
|
this.render();
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Verifies all of the blocks on the workspace and adds error icons if
|
|
* any problems are identified.
|
|
*
|
|
* @returns {Blockly.WorkspaceSvg} The workspace for call chaining.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.verifyAllBlocks = function() {
|
|
var blocks = this.getAllBlocks();
|
|
for (var i = 0, block; block = blocks[i]; ++i) {
|
|
if (block.category == 'Component') {
|
|
block.verify();
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
/**
|
|
* Saves the workspace as an XML file and returns the contents as a
|
|
* string.
|
|
*
|
|
* @param {boolean} prettify Specify true if the resulting workspace should be pretty-printed.
|
|
* @returns {string} XML serialization of the workspace's blocks.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.saveBlocksFile = function(prettify) {
|
|
return Blockly.SaveFile.get(prettify, this);
|
|
};
|
|
|
|
/**
|
|
* Generate the YAIL for the blocks workspace.
|
|
*
|
|
* @param {string} formJson
|
|
* @param {string} packageName
|
|
* @param {boolean=false} opt_repl
|
|
* @returns String containing YAIL to be sent to the phone.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getFormYail = function(formJson, packageName, opt_repl) {
|
|
return Blockly.Yail.getFormYail(formJson, packageName, !!opt_repl, this);
|
|
};
|
|
|
|
/**
|
|
* Get the warning handler for the workspace.
|
|
* @returns {Blockly.WarningHandler}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getWarningHandler = function() {
|
|
if (!this.warningHandler_) {
|
|
this.warningHandler_ = new Blockly.WarningHandler(this);
|
|
}
|
|
return this.warningHandler_;
|
|
};
|
|
|
|
/**
|
|
* Get the warning indicator UI element.
|
|
* @returns {Blockly.WarningIndicator}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getWarningIndicator = function() {
|
|
return this.warningIndicator_;
|
|
};
|
|
|
|
//noinspection JSUnusedGlobalSymbols Called from BlocklyPanel.java
|
|
Blockly.WorkspaceSvg.prototype.exportBlocksImageToUri = function(cb) {
|
|
Blockly.ExportBlocksImage.getUri(cb, this);
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.getFlydown = function() {
|
|
return this.flydown_;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.hideChaff = function(opt_allowToolbox) {
|
|
this.flydown_ && this.flydown_.hide();
|
|
this.typeBlock_ && this.typeBlock_.hide();
|
|
if (!opt_allowToolbox) { // Fixes #1269
|
|
this.backpack_ && this.backpack_.hide();
|
|
}
|
|
this.setScrollbarsVisible(true);
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.activate = function() {
|
|
Blockly.mainWorkspace = this;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.buildComponentMap = function(warnings, errors, forRepl, compileUnattachedBlocks) {
|
|
var map = {components: {}, globals: []};
|
|
var blocks = this.getTopBlocks(/* ordered */ true);
|
|
for (var i = 0, block; block = blocks[i]; ++i) {
|
|
if (block.type == 'procedures_defnoreturn' || block.type == 'procedures_defreturn' || block.type == 'global_declaration') {
|
|
map.globals.push(block);
|
|
} else if (block.category == 'Component' && block.type == 'event') {
|
|
if (block.isGeneric) {
|
|
map.globals.push(block);
|
|
continue;
|
|
}
|
|
var instanceName = block.instanceName;
|
|
if (!map.components[instanceName]) {
|
|
map.components[instanceName] = [];
|
|
}
|
|
map.components[instanceName].push(block);
|
|
}
|
|
}
|
|
return map;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.resize = (function(resize) {
|
|
return function() {
|
|
resize.call(this);
|
|
if (this.warningIndicator_ && this.warningIndicator_.position_) {
|
|
this.warningIndicator_.position_();
|
|
}
|
|
if (this.backpack_ && this.backpack_.position_) {
|
|
this.backpack_.position_();
|
|
}
|
|
return this;
|
|
};
|
|
})(Blockly.WorkspaceSvg.prototype.resize);
|
|
|
|
Blockly.WorkspaceSvg.prototype.customContextMenu = function(menuOptions) {
|
|
var self = this;
|
|
function addResetArrangements(callback) {
|
|
return function() {
|
|
try {
|
|
callback.call();
|
|
} finally {
|
|
self.resetArrangements();
|
|
}
|
|
};
|
|
}
|
|
function instrument(callback) {
|
|
return function() {
|
|
Blockly.Instrument.initializeStats('expandAllCollapsedBlocks');
|
|
Blockly.Instrument.timer(
|
|
function() { callback.call(self); },
|
|
function(result, timeDiff) {
|
|
Blockly.Instrument.stats.totalTime = timeDiff;
|
|
Blockly.Instrument.displayStats('expandAllCollapsedBlocks');
|
|
});
|
|
};
|
|
}
|
|
for (var i = 0; i < menuOptions.length; ++i) {
|
|
if (menuOptions[i].text == Blockly.Msg.COLLAPSE_ALL) {
|
|
menuOptions[i].callback = addResetArrangements(menuOptions[i].callback);
|
|
} else if (menuOptions[i].text == Blockly.Msg.EXPAND_ALL) {
|
|
menuOptions[i].callback = instrument(addResetArrangements(menuOptions[i].callback));
|
|
}
|
|
}
|
|
|
|
var exportOption = {enabled: true};
|
|
exportOption.text = Blockly.Msg.EXPORT_IMAGE;
|
|
exportOption.callback = function() {
|
|
Blockly.ExportBlocksImage.onclickExportBlocks(Blockly.getMainWorkspace().getMetrics());
|
|
};
|
|
menuOptions.splice(3, 0, exportOption);
|
|
|
|
//Show or hide workspace SVG elements backpack, zoom, and trashcan
|
|
var workspaceOption = {enabled: true};
|
|
workspaceOption.text = this.chromeHidden ? Blockly.Msg.SHOW : Blockly.Msg.HIDE;
|
|
var displayStyle = this.chromeHidden ? 'block' : 'none';
|
|
workspaceOption.callback= function() {
|
|
self.backpack_.svgGroup_.style.display=displayStyle;
|
|
self.trashcan.svgGroup_.style.display=displayStyle;
|
|
self.zoomControls_.svgGroup_.style.display=displayStyle;
|
|
self.warningIndicator_.svgGroup_.style.display=displayStyle;
|
|
self.chromeHidden = !self.chromeHidden;
|
|
};
|
|
menuOptions.push(workspaceOption);
|
|
|
|
// Arrange blocks in row order.
|
|
var arrangeOptionH = {enabled: (Blockly.workspace_arranged_position !== Blockly.BLKS_HORIZONTAL)};
|
|
arrangeOptionH.text = Blockly.Msg.ARRANGE_H;
|
|
arrangeOptionH.callback = function(opt_type) {
|
|
opt_type = opt_type instanceof goog.events.Event ? null : opt_type;
|
|
arrangeBlocks(opt_type? opt_type : Blockly.workspace_arranged_type, Blockly.BLKS_HORIZONTAL);
|
|
};
|
|
menuOptions.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(opt_type) {
|
|
opt_type = opt_type instanceof goog.events.Event ? null : opt_type;
|
|
arrangeBlocks(opt_type? opt_type : Blockly.workspace_arranged_type, Blockly.BLKS_VERTICAL);
|
|
};
|
|
menuOptions.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 Procedures the comparator is the NAME(for definitions) or PROCNAME (for calls)
|
|
* In the case of 'Components' the comparator is the type name, instance name, then event name
|
|
* @param {!Blockly.Block} block the block that will be compared in the sortByCategory function
|
|
* @returns {string} text to be used in the comparison
|
|
*/
|
|
function comparisonName(block) {
|
|
// Add trailing numbers to represent their sequence
|
|
if (block.category == 'Variables') {
|
|
return ('a,' + block.type + ',' + block.getVars().join(','));
|
|
}
|
|
if (block.category === 'Procedures') {
|
|
// sort procedure definitions before calls
|
|
if (block.type.indexOf('procedures_def') == 0) {
|
|
return ('b,a:' + (block.getFieldValue('NAME') || block.getFieldValue('PROCNAME')));
|
|
} else {
|
|
return ('b,b:'+ (block.getFieldValue('NAME') || block.getFieldValue('PROCNAME')));
|
|
}
|
|
}
|
|
if (block.category == 'Component') {
|
|
var component = block.type + ',' + block.typeName + ','
|
|
+ (block.isGeneric ? '!GENERIC!' : block.instanceName) + ',';
|
|
// sort Component blocks first, then events, methods, getters, or setters
|
|
if (block.type == 'component_event') {
|
|
component += block.eventName;
|
|
} else if (block.type == 'component_method') {
|
|
component += block.methodName;
|
|
} else if (block.type == 'component_set_get') {
|
|
component += block.setOrGet + block.propertyName;
|
|
} else {
|
|
// component blocks
|
|
component += '.Component';
|
|
}
|
|
return ('c,' + component);
|
|
}
|
|
// Floating blocks that are not Component
|
|
return ('d,' + block.type);
|
|
}
|
|
|
|
/**
|
|
* Function used to compare two strings with text and numbers
|
|
* @param {string} a first string to be compared
|
|
* @param {string} b second string to be compared
|
|
* @returns {number} returns 0 if the strings are equal, and -1 or 1 if they are not
|
|
*/
|
|
function compareStrTextNum(strA,strB) {
|
|
// Use Regular Expression to match text and numbers
|
|
var regexStrA = strA.match(/^(.*?)([0-9]+)/i);
|
|
var regexStrB = strB.match(/^(.*?)([0-9]+)/i);
|
|
|
|
// There are numbers in the strings, compare numbers
|
|
if (regexStrA != null && regexStrB != null) {
|
|
if (regexStrA[1] < regexStrB[1]) {
|
|
return -1;
|
|
} else if (regexStrA[1] > regexStrB[1]) {
|
|
return 1;
|
|
} else {
|
|
return parseInt(regexStrA[2]) - parseInt(regexStrB[2]);
|
|
}
|
|
} else {
|
|
return strA.localeCompare(strB, undefined, {numeric:true});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 (a.category != b.category) {
|
|
return comparatorA.localeCompare(comparatorB, undefined, {numeric:true});
|
|
}
|
|
|
|
// Sort by Category First, also handles other floating blocks
|
|
if (a.category == b.category && a.category != "Component") {
|
|
// Remove '1,'
|
|
comparatorA = comparatorA.substr(2);
|
|
comparatorB = comparatorB.substr(2);
|
|
var res = compareStrTextNum(comparatorA, comparatorB);
|
|
if (a.category == "Variables" && a.type == b.type) {
|
|
// Sort Variables
|
|
if (a.type == "global_declaration") {
|
|
// initialize variables, extract just global variable names
|
|
var nameA = a.svgGroup_.textContent;
|
|
// remove substring "initialize global<varname>to" and only keep <varname>
|
|
nameA = nameA.substring(17, nameA.length - 2);
|
|
var nameB = b.svgGroup_.textContent;
|
|
nameB = nameB.substring(17, nameB.length - 2);
|
|
res = compareStrTextNum(nameA, nameB);
|
|
} else {
|
|
var nameA = a.fieldVar_.text_;
|
|
var nameB = b.fieldVar_.text_;
|
|
if (nameA.includes("global") && nameB.includes("global")) {
|
|
// Global Variables and get variable names, remove "global"
|
|
res = compareStrTextNum(nameA.substring(6), nameB.substring(6));
|
|
}else {
|
|
// Other floating variables
|
|
res = compareStrTextNum(nameA, nameB);
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// 3.Component event handlers, lexicographically sorted by
|
|
// type name, instance name, then event name
|
|
if (a.category == "Component" && b.category == "Component" && a.eventName && b.eventName) {
|
|
if (a.typeName == b.typeName) {
|
|
if (a.instanceName == b.instanceName) {
|
|
return 0;
|
|
} else if (!a.instanceName) {
|
|
return -1;
|
|
} else if (!b.instanceName) {
|
|
return 1;
|
|
}
|
|
return compareStrTextNum(a.instanceName, b.instanceName);
|
|
}
|
|
return comparatorA.localeCompare(comparatorB, undefined, {numeric:true});
|
|
}
|
|
|
|
// 4. For Component blocks, sorted internally first by type,
|
|
// whether they are generic (generics precede specifics),
|
|
// then by instance name (for specific blocks),
|
|
// then by method/property name.
|
|
if (a.category == "Component" && b.category == "Component") {
|
|
var geneA = ',2';
|
|
if (a.isGeneric) {
|
|
geneA = ',1';
|
|
}
|
|
|
|
var geneB = ',2';
|
|
if (b.isGeneric) {
|
|
geneB = ',1';
|
|
}
|
|
|
|
var componentA = a.type + geneA;
|
|
var componentB = b.type + geneB;
|
|
|
|
var res = componentA.localeCompare(componentB, undefined, {numeric:true});
|
|
if (res == 0) {
|
|
// compare type names
|
|
res = compareStrTextNum(a.typeName, b.typeName);
|
|
}
|
|
//the comparator is the type name, instance name, then event name
|
|
if (res == 0) {
|
|
if (a.instanceName && b.instanceName) {
|
|
res = compareStrTextNum(a.instanceName, b.instanceName);
|
|
}
|
|
// Compare property names
|
|
var prop_method_A = a.propertyName || a.methodName;
|
|
var prop_method_B = b.propertyName || b.methodName;
|
|
res = prop_method_A.toLowerCase().localeCompare(prop_method_B.toLowerCase(), undefined, {numeric:true});
|
|
}
|
|
return res;
|
|
}
|
|
|
|
}
|
|
|
|
// Arranges block in layout (Horizontal or Vertical).
|
|
function arrangeBlocks(type, layout) {
|
|
Blockly.Events.setGroup(true); // group these movements together
|
|
// start arrangement
|
|
var workspaceId = Blockly.mainWorkspace.id;
|
|
Blockly.Events.fire(new AI.Events.StartArrangeBlocks(workspaceId));
|
|
Blockly.workspace_arranged_type = type;
|
|
Blockly.workspace_arranged_position = layout;
|
|
Blockly.workspace_arranged_latest_position = layout;
|
|
var event = new AI.Events.EndArrangeBlocks(workspaceId, type, layout);
|
|
var SPACER = 25;
|
|
var topblocks = Blockly.mainWorkspace.getTopBlocks(/* ordered */ 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 spacing = Blockly.mainWorkspace.options.gridOptions.spacing;
|
|
var spacingInv = 1 / spacing;
|
|
var snap = Blockly.mainWorkspace.options.gridOptions.snap ?
|
|
function(x) { return (Math.ceil(x * spacingInv) - .5) * spacing; } : function(x) { return x; };
|
|
var viewLeft = snap(metrics.viewLeft + 5);
|
|
var viewTop = snap(metrics.viewTop + 5);
|
|
var x = viewLeft;
|
|
var y = viewTop;
|
|
var wsRight = viewLeft + metrics.viewWidth / Blockly.mainWorkspace.scale;
|
|
var wsBottom = viewTop + metrics.viewHeight / Blockly.mainWorkspace.scale;
|
|
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 = snap(x + blkWidth + SPACER);
|
|
if (blkHgt > maxHgt) // Remember highest block
|
|
maxHgt = blkHgt;
|
|
} else {
|
|
y = snap(y + maxHgt + SPACER);
|
|
maxHgt = blkHgt;
|
|
x = viewLeft;
|
|
blk.moveBy(x - blkXY.x, y - blkXY.y);
|
|
blk.select();
|
|
x = snap(x + blkWidth + SPACER);
|
|
}
|
|
break;
|
|
case Blockly.BLKS_VERTICAL:
|
|
if (y < wsBottom) {
|
|
blk.moveBy(x - blkXY.x, y - blkXY.y);
|
|
blk.select();
|
|
y = snap(y + blkHgt + SPACER);
|
|
if (blkWidth > maxWidth) // Remember widest block
|
|
maxWidth = blkWidth;
|
|
} else {
|
|
x = snap(x + maxWidth + SPACER);
|
|
maxWidth = blkWidth;
|
|
y = viewTop;
|
|
blk.moveBy(x - blkXY.x, y - blkXY.y);
|
|
blk.select();
|
|
y = snap(y + blkHgt + SPACER);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
Blockly.Events.fire(event); // end arrangement
|
|
Blockly.Events.setGroup(false);
|
|
setTimeout(function() {
|
|
Blockly.workspace_arranged_type = type;
|
|
Blockly.workspace_arranged_position = layout;
|
|
Blockly.workspace_arranged_latest_position = layout;
|
|
}); // need to run after all events have run
|
|
}
|
|
|
|
// Sort by Category.
|
|
var sortOptionCat = {enabled: (Blockly.workspace_arranged_type !== Blockly.BLKS_CATEGORY)};
|
|
sortOptionCat.text = Blockly.Msg.SORT_C;
|
|
sortOptionCat.callback = function() {
|
|
rearrangeWorkspace(Blockly.BLKS_CATEGORY);
|
|
};
|
|
menuOptions.push(sortOptionCat);
|
|
|
|
// Called after a sort or collapse/expand to redisplay blocks.
|
|
function rearrangeWorkspace(opt_type) {
|
|
//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(opt_type);
|
|
else if (Blockly.workspace_arranged_latest_position === Blockly.BLKS_VERTICAL)
|
|
arrangeOptionV.callback(opt_type);
|
|
}
|
|
|
|
// Enable all blocks
|
|
var enableAll = {enabled: true};
|
|
enableAll.text = Blockly.Msg.ENABLE_ALL_BLOCKS;
|
|
enableAll.callback = function() {
|
|
var allBlocks = Blockly.mainWorkspace.getAllBlocks();
|
|
Blockly.Events.setGroup(true);
|
|
for (var x = 0, block; block = allBlocks[x]; x++) {
|
|
block.setDisabled(false);
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
};
|
|
menuOptions.push(enableAll);
|
|
|
|
// Disable all blocks
|
|
var disableAll = {enabled: true};
|
|
disableAll.text = Blockly.Msg.DISABLE_ALL_BLOCKS;
|
|
disableAll.callback = function() {
|
|
var allBlocks = Blockly.mainWorkspace.getAllBlocks();
|
|
Blockly.Events.setGroup(true);
|
|
for (var x = 0, block; block = allBlocks[x]; x++) {
|
|
block.setDisabled(true);
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
};
|
|
menuOptions.push(disableAll);
|
|
|
|
// Show all comments
|
|
var showAll = {enabled: true};
|
|
showAll.text = Blockly.Msg.SHOW_ALL_COMMENTS;
|
|
showAll.callback = function() {
|
|
var allBlocks = Blockly.mainWorkspace.getAllBlocks();
|
|
Blockly.Events.setGroup(true);
|
|
for (var x = 0, block; block = allBlocks[x]; x++) {
|
|
if (block.comment != null && !block.isCollapsed()) {
|
|
block.comment.setVisible(true);
|
|
}
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
};
|
|
menuOptions.push(showAll);
|
|
|
|
// Hide all comments
|
|
var hideAll = {enabled: true};
|
|
hideAll.text = Blockly.Msg.HIDE_ALL_COMMENTS;
|
|
hideAll.callback = function() {
|
|
var allBlocks = Blockly.mainWorkspace.getAllBlocks();
|
|
Blockly.Events.setGroup(true);
|
|
for (var x = 0, block; block = allBlocks[x]; x++) {
|
|
if (block.comment != null && !block.isCollapsed()) {
|
|
block.comment.setVisible(false);
|
|
}
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
};
|
|
menuOptions.push(hideAll);
|
|
|
|
// Copy all blocks to backpack option.
|
|
var backpackCopyAll = {enabled: true};
|
|
backpackCopyAll.text = Blockly.Msg.COPY_ALLBLOCKS;
|
|
backpackCopyAll.callback = function() {
|
|
if (Blockly.getMainWorkspace().hasBackpack()) {
|
|
Blockly.getMainWorkspace().getBackpack().addAllToBackpack();
|
|
}
|
|
};
|
|
menuOptions.push(backpackCopyAll);
|
|
|
|
// Retrieve from backpack option.
|
|
var backpackRetrieve = {enabled: true};
|
|
backpackRetrieve.text = Blockly.Msg.BACKPACK_GET + " (" +
|
|
Blockly.getMainWorkspace().getBackpack().count() + ")";
|
|
backpackRetrieve.callback = function() {
|
|
if (Blockly.getMainWorkspace().hasBackpack()) {
|
|
Blockly.getMainWorkspace().getBackpack().pasteBackpack();
|
|
}
|
|
};
|
|
menuOptions.push(backpackRetrieve);
|
|
|
|
// Enable grid
|
|
var gridOption = {enabled: true};
|
|
gridOption.text = this.options.gridOptions['enabled'] ? Blockly.Msg.DISABLE_GRID :
|
|
Blockly.Msg.ENABLE_GRID;
|
|
gridOption.callback = function() {
|
|
self.options.gridOptions['enabled'] = !self.options.gridOptions['enabled'];
|
|
self.options.gridOptions['snap'] = self.options.gridOptions['enabled'] && top.BlocklyPanel_getSnapEnabled();
|
|
if (self.options.gridOptions['enabled']) {
|
|
// add grid
|
|
self.svgBackground_.setAttribute('style', 'fill: url(#' + self.options.gridPattern.id + ');');
|
|
} else {
|
|
// remove grid
|
|
self.svgBackground_.setAttribute('style', 'fill: white;');
|
|
}
|
|
if (top.BlocklyPanel_setGridEnabled) {
|
|
top.BlocklyPanel_setGridEnabled(self.options.gridOptions['enabled']);
|
|
top.BlocklyPanel_saveUserSettings();
|
|
}
|
|
};
|
|
menuOptions.push(gridOption);
|
|
|
|
if (this.options.gridOptions['enabled']) {
|
|
// Enable Snapping
|
|
var snapOption = {enabled: this.options.gridOptions['enabled']};
|
|
snapOption.text = this.options.gridOptions['snap'] ? Blockly.Msg.DISABLE_SNAPPING :
|
|
Blockly.Msg.ENABLE_SNAPPING;
|
|
snapOption.callback = function() {
|
|
self.options.gridOptions['snap'] = !self.options.gridOptions['snap'];
|
|
if (top.BlocklyPanel_setSnapEnabled) {
|
|
top.BlocklyPanel_setSnapEnabled(self.options.gridOptions['enabled']);
|
|
top.BlocklyPanel_saveUserSettings();
|
|
}
|
|
};
|
|
menuOptions.push(snapOption);
|
|
}
|
|
|
|
// Option to get help.
|
|
var helpOption = {enabled: false};
|
|
helpOption.text = Blockly.Msg.HELP;
|
|
helpOption.callback = function() {};
|
|
menuOptions.push(helpOption);
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
|
|
if (this.trashcan) {
|
|
this.deleteAreaTrash_ = this.trashcan.getClientRect();
|
|
} else {
|
|
this.deleteAreaTrash_ = null;
|
|
}
|
|
if (this.isMutator) {
|
|
if (this.flyout_) {
|
|
this.deleteAreaToolbox_ = this.flyout_.getClientRect();
|
|
} else if (this.toolbox_) {
|
|
this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
|
|
} else {
|
|
this.deleteAreaToolbox_ = null;
|
|
}
|
|
} else {
|
|
this.deleteAreaToolbox_ = null;
|
|
}
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.getBackpack = function() {
|
|
return this.backpack_;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.hasBackpack = function() {
|
|
return this.backpack_ != null;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
|
|
Blockly.terminateDrag_();
|
|
if (e.eventPhase == 3) {
|
|
if (e.ctrlKey == true) {
|
|
// multi-touch pinch gesture
|
|
if (e.deltaY == 0) {
|
|
// Multi-stage wheel movement triggers jumpy zoom-in then zoom-out behavior
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
var delta = e.deltaY > 0 ? -1 : 1;
|
|
var position = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
|
|
this.getInverseScreenCTM());
|
|
this.zoom(position.x, position.y, delta);
|
|
} else {
|
|
// pan using mouse wheel
|
|
this.scrollX -= e.deltaX;
|
|
this.scrollY -= e.deltaY;
|
|
this.updateGridPattern_();
|
|
if (this.scrollbar) {
|
|
// can only pan if scrollbars exist
|
|
this.scrollbar.resize();
|
|
} else {
|
|
this.translate(this.scrollX, this.scrollY);
|
|
}
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.setGridSettings = function(enabled, snap) {
|
|
this.options.gridOptions['enabled'] = enabled;
|
|
this.options.gridOptions['snap'] = enabled && snap;
|
|
if (this.svgBackground_) {
|
|
if (this.options.gridOptions['enabled']) {
|
|
// add grid
|
|
this.svgBackground_.setAttribute('style', 'fill: url(#' + this.options.gridPattern.id + ');');
|
|
} else {
|
|
// remove grid
|
|
this.svgBackground_.setAttribute('style', 'fill: white;');
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds a map of component name -> top level blocks for that component.
|
|
* A special entry for "globals" maps to top-level global definitions.
|
|
*
|
|
* @param warnings a Map that will be filled with warnings for troublesome blocks
|
|
* @param errors a list that will be filled with error messages
|
|
* @param forRepl whether this is executed for REPL
|
|
* @param compileUnattachedBlocks whether to compile unattached blocks
|
|
* @returns object mapping component names to the top-level blocks for that component in the
|
|
* workspace. For each component C the object contains a field "component.C" whose
|
|
* value is an array of blocks. In addition, the object contains a field named "globals"
|
|
* whose value is an array of all valid top-level blocks not associated with a
|
|
* component (procedure and variable definitions)
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.buildComponentMap = function(warnings, errors, forRepl, compileUnattachedBlocks) {
|
|
var map = {};
|
|
map.components = {};
|
|
map.globals = [];
|
|
|
|
// TODO: populate warnings, errors as we traverse the top-level blocks
|
|
|
|
var blocks = this.getTopBlocks(true);
|
|
for (var x = 0, block; block = blocks[x]; x++) {
|
|
|
|
// TODO: deal with unattached blocks that are not valid top-level definitions. Valid blocks
|
|
// are events, variable definitions, or procedure definitions.
|
|
|
|
if (!block.category) {
|
|
continue;
|
|
}
|
|
if (block.type == 'procedures_defnoreturn' || block.type == 'procedures_defreturn' || block.type == 'global_declaration') {
|
|
map.globals.push(block);
|
|
// TODO: eventually deal with variable declarations, once we have them
|
|
} else if (block.category == 'Component') {
|
|
var instanceName = block.instanceName;
|
|
if(block.blockType != "event") {
|
|
continue;
|
|
}
|
|
if (block.isGeneric) {
|
|
map.globals.push(block);
|
|
continue;
|
|
}
|
|
if (!map.components[instanceName]) {
|
|
map.components[instanceName] = []; // first block we've found for this component
|
|
}
|
|
|
|
// TODO: check for duplicate top-level blocks (e.g., two event handlers with same name) -
|
|
// or better yet, prevent these from happening!
|
|
|
|
map.components[instanceName].push(block);
|
|
}
|
|
}
|
|
return map;
|
|
};
|
|
|
|
/**
|
|
* Get the topmost workspace in the workspace hierarchy.
|
|
* @returns {Blockly.WorkspaceSvg}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getTopWorkspace = function() {
|
|
var parent = this;
|
|
while (parent.targetWorkspace) {
|
|
parent = parent.targetWorkspace;
|
|
}
|
|
return parent;
|
|
};
|
|
|
|
Blockly.WorkspaceSvg.prototype.fireChangeListener = function(event) {
|
|
Blockly.WorkspaceSvg.superClass_.fireChangeListener.call(this, event);
|
|
if (event instanceof Blockly.Events.Move) {
|
|
// Reset arrangement parameters
|
|
Blockly.workspace_arranged_latest_position = null;
|
|
Blockly.workspace_arranged_position = null;
|
|
Blockly.workspace_arranged_type = null;
|
|
var oldParent = this.blockDB_[event.oldParentId],
|
|
block = this.blockDB_[event.blockId];
|
|
oldParent && this.requestErrorChecking(oldParent);
|
|
block && this.requestErrorChecking(block);
|
|
}
|
|
|
|
// Double-click to collapse/expand blocks
|
|
if (event instanceof Blockly.Events.Ui && event.element === 'click') {
|
|
if (this.doubleClickPid_) {
|
|
clearTimeout(this.doubleClickPid_);
|
|
this.doubleClickPid_ = undefined;
|
|
if (event.blockId === this.doubleClickBlock_) {
|
|
// double click
|
|
var block = this.getBlockById(this.doubleClickBlock_);
|
|
block.setCollapsed(!block.isCollapsed());
|
|
return;
|
|
}
|
|
}
|
|
if (!this.doubleClickPid_) {
|
|
this.doubleClickBlock_ = event.blockId;
|
|
this.doubleClickPid_ = setTimeout(function() {
|
|
this.doubleClickPid_ = undefined;
|
|
}.bind(this), 500); // windows uses 500ms as the default speed; seems reasonable enough
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Request a re-render the workspace. If <code>block</code> is provided, only descendants of
|
|
* <code>block</code>'s top-most block will be rendered. This may be called multiple times to queue
|
|
* many blocks to be rendered.
|
|
* @param {Blockly.BlockSvg=} block
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.requestRender = function(block) {
|
|
if (!this.pendingRender) {
|
|
this.needsRendering = [];
|
|
this.pendingBlockIds = {};
|
|
this.pendingRenderFunc = function() {
|
|
try {
|
|
this.render(this.needsRendering.length === 0 ? undefined : this.needsRendering);
|
|
} finally {
|
|
this.pendingRender = null;
|
|
}
|
|
}.bind(this);
|
|
if (this.svgGroup_.parentElement.parentElement.parentElement.style.display === 'none') {
|
|
this.pendingRender = true;
|
|
} else {
|
|
this.pendingRender = setTimeout(this.pendingRenderFunc, 0);
|
|
}
|
|
}
|
|
if (block) {
|
|
// Rendering uses Blockly.BlockSvg.renderDown, so we only need a list of the topmost blocks
|
|
while (block.getParent()) {
|
|
block = /** @type {Blockly.BlockSvg} */ block.getParent();
|
|
}
|
|
if (!(block.id in this.pendingBlockIds)) {
|
|
this.pendingBlockIds[block.id] = true;
|
|
this.needsRendering.push(block);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Request error checking on the specified block. This will queue error checking events until the
|
|
* next time the JavaScript thread relinquishes control to the UI thread.
|
|
* @param {Blockly.BlockSvg=} block
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.requestErrorChecking = function(block) {
|
|
if (!this.warningHandler_) {
|
|
return; // no error checking before warning handler exists
|
|
}
|
|
if (this.checkAllBlocks) {
|
|
return; // already planning to check all blocks
|
|
}
|
|
if (!this.pendingErrorCheck) {
|
|
this.needsErrorCheck = [];
|
|
this.pendingErrorBlockIds = {};
|
|
this.checkAllBlocks = !!block;
|
|
this.pendingErrorCheck = setTimeout(function() {
|
|
try {
|
|
var handler = this.getWarningHandler();
|
|
if (handler) { // not true for flyouts and before the main workspace is rendered.
|
|
goog.array.forEach(this.checkAllBlocks ? this.getAllBlocks() : this.needsErrorCheck,
|
|
function(block) {
|
|
handler.checkErrors(block);
|
|
});
|
|
}
|
|
} finally {
|
|
this.pendingErrorCheck = null;
|
|
this.checkAllBlocks = false;
|
|
// Let any disposed blocks be GCed...
|
|
this.needsErrorCheck = null;
|
|
this.pendingErrorBlockIds = null;
|
|
}
|
|
}.bind(this));
|
|
}
|
|
if (block && !(block.id in this.pendingErrorBlockIds)) {
|
|
while (block.getParent()) {
|
|
block = /** @type {Blockly.BlockSvg} */ block.getParent();
|
|
}
|
|
var pendingBlocks = [block];
|
|
while (pendingBlocks.length > 0) {
|
|
block = pendingBlocks.shift();
|
|
if (!(block.id in this.pendingErrorBlockIds)) {
|
|
this.pendingErrorBlockIds[block.id] = true;
|
|
this.needsErrorCheck.push(block);
|
|
}
|
|
Array.prototype.push.apply(pendingBlocks, block.getChildren());
|
|
}
|
|
} else if (!block) {
|
|
// schedule all blocks
|
|
this.checkAllBlocks = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sort the workspace's connection database. This only needs to be called if the bulkRendering
|
|
* property of the workspace is set to true to false as any connections that Blockly attempted to
|
|
* update during that time may be incorrectly ordered in the database.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.sortConnectionDB = function() {
|
|
goog.array.forEach(this.connectionDBList, function(connectionDB) {
|
|
connectionDB.sort(function(a, b) {
|
|
return a.y_ - b.y_;
|
|
});
|
|
// If we are rerendering due to a new error, we only redraw the error block, which means that
|
|
// we can't clear the database, otherwise all other connections disappear. Instead, we add
|
|
// the moved connections anyway, and at this point we can remove the duplicate entries in the
|
|
// database. We remove after sorting so that the operation is O(n) rather than O(n^2). This
|
|
// assumption may break in the future if Blockly decides on a different mechanism for indexing
|
|
// connections.
|
|
connectionDB.removeDupes();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Request an update to the connection database's order due to movement of a block while a bulk
|
|
* rendering operation was in progress.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.requestConnectionDBUpdate = function() {
|
|
if (!this.pendingConnectionDBUpdate) {
|
|
this.pendingConnectionDBUpdate = setTimeout(function() {
|
|
try {
|
|
this.sortConnectionDB();
|
|
} finally {
|
|
this.pendingConnectionDBUpdate = null;
|
|
}
|
|
}.bind(this));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Scroll the workspace to center on the given block.
|
|
* @param {?string} id ID of block center on.
|
|
* @public
|
|
*/
|
|
// TODO: This is code from a later version of Blockly. Remove on next Blockly update.
|
|
Blockly.WorkspaceSvg.prototype.centerOnBlock = function(id) {
|
|
if (!this.scrollbar) {
|
|
console.warn('Tried to scroll a non-scrollable workspace.');
|
|
return;
|
|
}
|
|
|
|
var block = this.getBlockById(id);
|
|
if (!block) {
|
|
return;
|
|
}
|
|
|
|
// XY is in workspace coordinates.
|
|
var xy = block.getRelativeToSurfaceXY();
|
|
// Height/width is in workspace units.
|
|
var heightWidth = block.getHeightWidth();
|
|
|
|
// Find the enter of the block in workspace units.
|
|
var blockCenterY = xy.y + heightWidth.height / 2;
|
|
|
|
// In RTL the block's position is the top right of the block, not top left.
|
|
var multiplier = this.RTL ? -1 : 1;
|
|
var blockCenterX = xy.x + (multiplier * heightWidth.width / 2);
|
|
|
|
// Workspace scale, used to convert from workspace coordinates to pixels.
|
|
var scale = this.scale;
|
|
|
|
// Center in pixels. 0, 0 is at the workspace origin. These numbers may
|
|
// be negative.
|
|
var pixelX = blockCenterX * scale;
|
|
var pixelY = blockCenterY * scale;
|
|
|
|
var metrics = this.getMetrics();
|
|
|
|
// Scrolling to here would put the block in the top-left corner of the
|
|
// visible workspace.
|
|
var scrollToBlockX = pixelX - metrics.contentLeft;
|
|
var scrollToBlockY = pixelY - metrics.contentTop;
|
|
|
|
// viewHeight and viewWidth are in pixels.
|
|
var halfViewWidth = metrics.viewWidth / 2;
|
|
var halfViewHeight = metrics.viewHeight / 2;
|
|
|
|
// Put the block in the center of the visible workspace instead.
|
|
var scrollToCenterX = scrollToBlockX - halfViewWidth;
|
|
var scrollToCenterY = scrollToBlockY - halfViewHeight;
|
|
|
|
Blockly.hideChaff();
|
|
var event = new AI.Events.WorkspaceMove(this.id);
|
|
this.scrollbar.set(scrollToCenterX, scrollToCenterY);
|
|
event.recordNew();
|
|
Blockly.Events.fire(event);
|
|
|
|
};
|
|
|
|
/*
|
|
* Refresh the state of the backpack. Called from BlocklyPanel.java
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.refreshBackpack = function() {
|
|
if (this.backpack_) {
|
|
this.backpack_.resize();
|
|
}
|
|
}; |