Files
mixio/blockly/iot/workspace.js
Eason010212 5d80728be9 mixio 1.10.0
2023-03-10 18:03:02 +08:00

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();
}
};