Skip to content
Product
Solutions
Open Source
Pricing
Search
Sign in
Sign up
mit-cml
/
appinventor-sources
Public
Code
Issues
392
Pull requests
131
Projects
Wiki
Security
Insights
appinventor-sources/appinventor/blocklyeditor/src/blocks/components.js /
@elatoskinas
elatoskinas Implement Chart and related components (#1776)
Latest commit 3bd17ee 4 days ago
History
20 contributors
@ewpatton@jisqyv@BeksOmega@SusanRatiLane@graceRyu@shrutirij@thequixotic@fturbak@conorshipp@afmckinney@p4ssw0rd@tomiyee
1888 lines (1736 sloc) 78.8 KB
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2013-2022 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
/**
* @license
* @fileoverview Component blocks for Blockly, modified for MIT App Inventor.
* @author mckinney@mit.edu (Andrew F. McKinney)
* @author sharon@google.com (Sharon Perl)
* @author ewpatton@mit.edu (Evan W. Patton)
*/
'use strict';
goog.provide('AI.Blockly.Blocks.components');
goog.provide('AI.Blockly.ComponentBlock');
goog.require('Blockly.Blocks.Utilities');
Blockly.Blocks.components = {};
Blockly.ComponentBlock = {};
/*
* All component blocks have category=='Component'. In addition to the standard blocks fields,
* All regular component blocks have a field instanceName whose value is the name of their
* component. For example, the blocks representing a Button1.Click event has
* instanceName=='Button1'. All generic component blocks have a field typeName whose value is
* the name of their component type.
*/
/**
* Block Colors Hues (See blockly.js for Saturation and Value - fixed for
* all blocks)
*/
Blockly.ComponentBlock.COLOUR_EVENT = Blockly.CONTROL_CATEGORY_HUE;
Blockly.ComponentBlock.COLOUR_METHOD = Blockly.PROCEDURE_CATEGORY_HUE;
Blockly.ComponentBlock.COLOUR_GET = '#439970'; // [67, 153, 112]
Blockly.ComponentBlock.COLOUR_SET = '#266643'; // [38, 102, 67]
Blockly.ComponentBlock.COLOUR_COMPONENT = '#439970'; // [67, 153, 112]
Blockly.ComponentBlock.COMPONENT_SELECTOR = "COMPONENT_SELECTOR";
/**
* Add a menu option to the context menu for {@code block} to swap between
* the generic and specific versions of the block.
*
* @param {Blockly.BlockSvg} block the block to manipulate
* @param {Array.<{enabled,text,callback}>} options the menu options
*/
Blockly.ComponentBlock.addGenericOption = function(block, options) {
if ((block.type === 'component_event' && block.isGeneric) || block.typeName === 'Form') {
return; // Cannot make a generic component_event specific for now...
}
/**
* Helper function used to make component blocks generic.
*
* @param {Blockly.BlockSvg} block the block to be made generic
* @param {(!Element|boolean)=} opt_replacementDom DOM tree for a replacement block to use in the
* substitution, false if no substitution should be made, or undefined if the substitution should
* be inferred.
*/
function makeGeneric(block, opt_replacementDom) {
var instanceName = block.instanceName;
var mutation = block.mutationToDom();
var oldMutation = Blockly.Xml.domToText(mutation);
mutation.setAttribute('is_generic', 'true');
mutation.removeAttribute('instance_name');
var newMutation = Blockly.Xml.domToText(mutation);
block.domToMutation(mutation);
block.initSvg(); // block shape may have changed
block.render();
Blockly.Events.fire(new Blockly.Events.Change(
block, 'mutation', null, oldMutation, newMutation));
if (block.type === 'component_event') opt_replacementDom = false;
if (opt_replacementDom !== false) {
if (opt_replacementDom === undefined) {
var compBlockXml = '' +
'' +
'' + instanceName + '' +
'';
opt_replacementDom = Blockly.Xml.textToDom(compBlockXml).firstElementChild;
}
var replacement = Blockly.Xml.domToBlock(opt_replacementDom, block.workspace);
replacement.initSvg();
block.getInput('COMPONENT').connection.connect(replacement.outputConnection);
}
var group = Blockly.Events.getGroup();
setTimeout(function() {
Blockly.Events.setGroup(group);
// noinspection JSAccessibilityCheck
block.bumpNeighbours_();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY);
}
var item = { enabled: false };
if (block.isGeneric) {
var compBlock = block.getInputTargetBlock('COMPONENT');
item.enabled = compBlock && compBlock.type === 'component_component_block';
item.text = Blockly.BlocklyEditor.makeMenuItemWithHelp(Blockly.Msg.UNGENERICIZE_BLOCK,
'/reference/other/any-component-blocks.html');
item.callback = function () {
try {
Blockly.Events.setGroup(true);
var instanceName = compBlock.instanceName;
compBlock.dispose(true);
var mutation = block.mutationToDom();
var oldMutation = Blockly.Xml.domToText(mutation);
mutation.setAttribute('instance_name', instanceName);
mutation.setAttribute('is_generic', 'false');
var newMutation = Blockly.Xml.domToText(mutation);
block.domToMutation(mutation);
block.initSvg(); // block shape may have changed
block.render();
Blockly.Events.fire(new Blockly.Events.Change(
block, 'mutation', null, oldMutation, newMutation));
var group = Blockly.Events.getGroup();
setTimeout(function () {
Blockly.Events.setGroup(group);
// noinspection JSAccessibilityCheck
block.bumpNeighbours_();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY);
} finally {
Blockly.Events.setGroup(false);
}
};
} else if (block.type === 'component_event') {
item.enabled = true;
item.text = Blockly.BlocklyEditor.makeMenuItemWithHelp(Blockly.Msg.GENERICIZE_BLOCK,
'/reference/other/any-component-blocks.html');
item.callback = function() {
try {
Blockly.Events.setGroup(true);
var instanceName = block.instanceName;
var intlName = block.workspace.getComponentDatabase()
.getInternationalizedParameterName('component');
// Aggregate variables in scope
var namesInScope = {}, maxNum = 0;
var regex = new RegExp('^' + intlName + '([0-9]+)$');
var varDeclsWithIntlName = [];
block.walk(function(block) {
if (block.type === 'local_declaration_statement' ||
block.type === 'local_declaration_expression') {
var localNames = block.getVars();
localNames.forEach(function(varname) {
namesInScope[varname] = true;
var match = varname.match(regex);
if (match) {
maxNum = Math.max(maxNum, parseInt(match[1]));
}
if (varname === intlName) {
varDeclsWithIntlName.push(block);
}
});
}
});
// Rename local variable definition of i18n(component) to prevent
// variable capture
if (intlName in namesInScope) {
varDeclsWithIntlName.forEach(function(block) {
Blockly.LexicalVariable.renameParamFromTo(block, intlName, intlName + (maxNum + 1).toString(), true);
});
}
// Make generic the block and any descendants of the same component instance
var varBlockXml = '' +
'' +
'' + intlName + '';
var varBlockDom = Blockly.Xml.textToDom(varBlockXml).firstElementChild;
makeGeneric(block); // Do this first so 'component' is defined.
block.walk(function(block) {
if ((block.type === 'component_method' || block.type === 'component_set_get') &&
block.instanceName === instanceName) {
makeGeneric(/** @type Blockly.BlockSvg */ block, varBlockDom);
}
});
} finally {
Blockly.Events.setGroup(false);
}
};
} else {
item.enabled = true;
item.text = Blockly.BlocklyEditor.makeMenuItemWithHelp(Blockly.Msg.GENERICIZE_BLOCK,
'/reference/other/any-component-blocks.html');
item.callback = function() {
try {
Blockly.Events.setGroup(true);
makeGeneric(block);
} finally {
Blockly.Events.setGroup(false);
}
};
}
options.splice(options.length - 1, 0, item);
};
/**
* Marks the passed block as a badBlock() and disables it if the data associated
* with the block is not defined, or the data is marked as deprecated.
* @param {Blockly.BlockSvg} block The block to check for deprecation.
* @param {EventDescriptor|MethodDescriptor|PropertyDescriptor} data The data
* associated with the block which is possibly deprecated.
*/
Blockly.ComponentBlock.checkDeprecated = function(block, data) {
if (data && data.deprecated && block.workspace == Blockly.mainWorkspace) {
block.setDisabled(true);
}
}
/**
* Create an event block of the given type for a component with the given
* instance name. eventType is one of the "events" objects in a typeJsonString
* passed to Blockly.Component.add.
* @lends {Blockly.BlockSvg}
* @lends {Blockly.Block}
*/
Blockly.Blocks.component_event = {
category : 'Component',
blockType : 'event',
init: function() {
this.componentDropDown = Blockly.ComponentBlock.createComponentDropDown(this);
},
mutationToDom : function() {
var container = document.createElement('mutation');
container.setAttribute('component_type', this.typeName);
container.setAttribute('is_generic', this.isGeneric ? "true" : "false");
if (!this.isGeneric) {
container.setAttribute('instance_name', this.instanceName);//instance name not needed
}
container.setAttribute('event_name', this.eventName);
if (!this.horizontalParameters) {
container.setAttribute('vertical_parameters', "true"); // Only store an element for vertical
// The absence of this attribute means horizontal.
}
// Note that this.parameterNames only contains parameter names that have
// overridden the default event parameter names specified in the component
// DB
for (var i = 0; i < this.parameterNames.length; i++) {
container.setAttribute('param_name' + i, this.parameterNames[i]);
}
return container;
},
domToMutation : function(xmlElement) {
var oldRendered = this.rendered;
this.rendered = false;
var oldDo = null;
for (var i = 0, input; input = this.inputList[i]; i++) {
if (input.connection) {
if (input.name === 'DO') {
oldDo = input.connection.targetBlock();
}
var block = input.connection.targetBlock();
if (block) {
block.unplug();
}
}
input.dispose();
}
this.inputList.length = 0;
this.typeName = xmlElement.getAttribute('component_type');
this.eventName = xmlElement.getAttribute('event_name');
this.isGeneric = xmlElement.getAttribute('is_generic') == 'true';
if (!this.isGeneric) {
this.instanceName = xmlElement.getAttribute('instance_name');//instance name not needed
} else {
delete this.instanceName;
}
// this.parameterNames will be set to a list of names that will override the
// default names specified in the component DB. Note that some parameter
// names may be overridden while others may remain their defaults
this.parameterNames = [];
var numParams = this.getDefaultParameters_().length
for (var i = 0; i < numParams; i++) {
var paramName = xmlElement.getAttribute('param_name' + i);
// For now, we only allow explicit parameter names starting at the beginning
// of the parameter list. Some day we may allow an arbitrary subset of the
// event params to be explicitly specified.
if (!paramName) break;
this.parameterNames.push(paramName);
}
// Orient parameters horizontally by default
var horizParams = xmlElement.getAttribute('vertical_parameters') !== "true";
this.setColour(Blockly.ComponentBlock.COLOUR_EVENT);
var localizedEventName;
var eventType = this.getEventTypeObject();
var componentDb = this.getTopWorkspace().getComponentDatabase();
if (eventType) {
localizedEventName = componentDb.getInternationalizedEventName(eventType.name);
}
else {
localizedEventName = componentDb.getInternationalizedEventName(this.eventName);
}
if (!this.isGeneric) {
this.appendDummyInput('WHENTITLE').appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_TITLE_WHEN)
.appendField(this.componentDropDown, Blockly.ComponentBlock.COMPONENT_SELECTOR)
.appendField('.' + localizedEventName);
this.componentDropDown.setValue(this.instanceName);
} else {
this.appendDummyInput('WHENTITLE').appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_EVENT_TITLE
+ componentDb.getInternationalizedComponentType(this.typeName) + '.' + localizedEventName);
}
this.setParameterOrientation(horizParams);
var tooltipDescription;
if (eventType) {
tooltipDescription = componentDb.getInternationalizedEventDescription(this.getTypeName(), eventType.name,
eventType.description);
}
else {
tooltipDescription = componentDb.getInternationalizedEventDescription(this.getTypeName(), this.eventName);
}
this.setTooltip(tooltipDescription);
this.setPreviousStatement(false, null);
this.setNextStatement(false, null);
if (oldDo) {
this.getInput('DO').connection.connect(oldDo.previousConnection);
}
for (var i = 0, input; input = this.inputList[i]; i++) {
input.init();
}
// Set as badBlock if it doesn't exist.
this.verify();
// Disable it if it does exist and is deprecated.
Blockly.ComponentBlock.checkDeprecated(this, eventType);
this.rendered = oldRendered;
},
getTypeName: function() {
return this.typeName === 'Form' ? 'Screen' : this.typeName;
},
// [lyn, 10/24/13] Allow switching between horizontal and vertical display of arguments
// Also must create flydown params and DO input if they don't exist.
// TODO: consider using top.BlocklyPanel... instead of window.parent.BlocklyPanel
setParameterOrientation: function(isHorizontal) {
var params = this.getParameters();
if (!params) {
params = [];
}
var componentDb = this.getTopWorkspace().getComponentDatabase();
var oldDoInput = this.getInput("DO");
if (!oldDoInput || (isHorizontal !== this.horizontalParameters && params.length > 0)) {
this.horizontalParameters = isHorizontal;
var bodyConnection = null, i, param, newDoInput;
if (oldDoInput) {
bodyConnection = oldDoInput.connection.targetConnection; // Remember any body connection
}
if (this.horizontalParameters) { // Replace vertical by horizontal parameters
if (oldDoInput) {
// Remove inputs after title ...
for (i = 0; i < params.length; i++) {
this.removeInput('VAR' + i); // vertical parameters
}
this.removeInput('DO');
}
// ... and insert new ones:
if (params.length > 0) {
var paramInput = this.appendDummyInput('PARAMETERS')
.appendField(" ")
.setAlign(Blockly.ALIGN_LEFT);
for (i = 0; param = params[i]; i++) {
var field = new Blockly.FieldEventFlydown(
param, componentDb, Blockly.FieldFlydown.DISPLAY_BELOW);
paramInput.appendField(field, 'VAR' + i)
.appendField(" ");
}
}
newDoInput = this.appendStatementInput("DO")
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_TITLE_DO); // Hey, I like your new do!
if (bodyConnection) {
newDoInput.connection.connect(bodyConnection);
}
} else { // Replace horizontal by vertical parameters
if (oldDoInput) {
// Remove inputs after title ...
this.removeInput('PARAMETERS'); // horizontal parameters
this.removeInput('DO');
}
// ... and insert new ones:
// Vertically aligned parameters
for (i = 0; param = params[i]; i++) {
var field = new Blockly.FieldEventFlydown(param, componentDb);
this.appendDummyInput('VAR' + i)
.appendField(field, 'VAR' + i)
.setAlign(Blockly.ALIGN_RIGHT);
}
newDoInput = this.appendStatementInput("DO")
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_TITLE_DO);
if (bodyConnection) {
newDoInput.connection.connect(bodyConnection);
}
}
if (Blockly.Events.isEnabled()) {
// Trigger a Blockly UI change event
Blockly.Events.fire(new Blockly.Events.Ui(this, 'parameter_orientation',
(!this.horizontalParameters).toString(), this.horizontalParameters.toString()))
}
}
},
// Return a list of parameter names
getParameters: function () {
/** @type {EventDescriptor} */
var defaultParameters = this.getDefaultParameters_();
var explicitParameterNames = this.getExplicitParameterNames_();
var params = [];
for (var i = 0; i < defaultParameters.length; i++) {
var paramName = explicitParameterNames[i] || defaultParameters[i].name;
params.push({name: paramName, type: defaultParameters[i].type});
}
return params;
},
getDefaultParameters_: function () {
var eventType = this.getEventTypeObject();
if (this.isGeneric) {
return [
{name:'component', type:'component'},
{name:'notAlreadyHandled', type: 'boolean'}
].concat((eventType && eventType.parameters) || []);
}
return eventType && eventType.parameters;
},
getExplicitParameterNames_: function () {
return this.parameterNames;
},
// Renames the block's instanceName and type (set in BlocklyBlock constructor), and revises its title
rename : function(oldname, newname) {
if (this.instanceName == oldname) {
this.instanceName = newname;
this.componentDropDown.setValue(this.instanceName);
return true;
}
return false;
},
renameVar: function(oldName, newName) {
for (var i = 0, param = 'VAR' + i, input
; input = this.getFieldValue(param)
; i++, param = 'VAR' + i) {
if (Blockly.Names.equals(oldName, input)) {
this.setFieldValue(param, newName);
}
}
},
helpUrl : function() {
var url = Blockly.ComponentBlock.EVENTS_HELPURLS[this.getTypeName()];
if (url && url[0] == '/') {
var parts = url.split('#');
parts[1] = this.getTypeName() + '.' + this.eventName;
url = parts.join('#');
}
return url;
},
getVars: function() {
var varList = [];
for (var i = 0, input; input = this.getFieldValue('VAR' + i); i++) {
varList.push(input);
}
return varList;
},
getVarString: function() {
var varString = "";
for (var i = 0, param; param = this.getFieldValue('VAR' + i); i++) {
// [lyn, 10/13/13] get current name from block, not from underlying event (may have changed)
if(i != 0){
varString += " ";
}
varString += param;
}
return varString;
},
declaredNames: function() { // [lyn, 10/13/13] Interface with Blockly.LexicalVariable.renameParam
var names = [];
for (var i = 0, param; param = this.getField('VAR' + i); i++) {
names.push(param.getText());
if (param.eventparam && param.eventparam != param.getText()) {
names.push(param.eventparam);
}
}
return names;
},
declaredVariables: function() {
var names = [];
for (var i = 0, param; param = this.getField('VAR' + i); i++) {
names.push(param.getText());
}
return names;
},
blocksInScope: function() { // [lyn, 10/13/13] Interface with Blockly.LexicalVariable.renameParam
var doBlock = this.getInputTargetBlock('DO');
if (doBlock) {
return [doBlock];
} else {
return [];
}
},
/**
* Get the underlying event descriptor for the block.
* @returns {EventDescriptor}
*/
getEventTypeObject : function() {
return this.getTopWorkspace().getComponentDatabase().getEventForType(this.typeName, this.eventName);
},
typeblock : function(){
var componentDb = Blockly.mainWorkspace.getComponentDatabase();
var tb = [];
var types = {};
componentDb.forEachInstance(function(instance) {
types[instance.typeName] = true;
componentDb.forEventInType(instance.typeName, function(_, eventName) {
tb.push({
translatedName: Blockly.Msg.LANG_COMPONENT_BLOCK_TITLE_WHEN + instance.name + '.' +
componentDb.getInternationalizedEventName(eventName),
mutatorAttributes: {
component_type: instance.typeName,
instance_name: instance.name,
event_name: eventName
}
});
});
});
delete types['Form'];
Object.keys(types).forEach(function(typeName) {
componentDb.forEventInType(typeName, function(_, eventName) {
tb.push({
translatedName: Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_EVENT_TITLE +
componentDb.getInternationalizedComponentType(typeName) + '.' +
componentDb.getInternationalizedEventName(eventName),
mutatorAttributes: {
component_type: typeName,
is_generic: true,
event_name: eventName
}
});
});
});
return tb;
},
customContextMenu: function (options) {
Blockly.FieldParameterFlydown.addHorizontalVerticalOption(this, options);
Blockly.ComponentBlock.addGenericOption(this, options);
Blockly.BlocklyEditor.addPngExportOption(this, options);
Blockly.BlocklyEditor.addGenerateYailOption(this, options);
},
// check if the block corresponds to an event inside componentTypes[typeName].eventDictionary
verify : function () {
var validate = function() {
// check component type
var componentDb = this.getTopWorkspace().getComponentDatabase();
var componentType = componentDb.getType(this.typeName);
if (!componentType) {
return false; // component does NOT exist! should not happen!
}
var eventDictionary = componentType.eventDictionary;
/** @type {EventDescriptor} */
var event = eventDictionary[this.eventName];
// check event name
if (!event) {
return false; // no such event : this event was for another version! block is undefined!
}
// check parameters
var varList = this.getVars();
var params = event.parameters;
if (this.isGeneric) {
varList.splice(0, 2); // remove component and wasDefined parameters
// since we know they are well-defined
}
if (varList.length != params.length) {
return false; // parameters have changed
}
if ("true" === componentType.external) {
for (var x = 0; x < varList.length; ++x) {
var found = false;
for (var i = 0, param; param = params[i]; ++i) {
if (componentDb.getInternationalizedParameterName(param.name) == varList[x]) {
found = true;
break;
}
}
if (!found) {
return false; // parameter name changed
}
}
}
// No need to check event return type, events do not return.
return true; // passed all our tests! block is defined!
};
var isDefined = validate.call(this);
if (isDefined) {
this.notBadBlock();
} else {
this.badBlock();
}
},
// [lyn, 12/31/2013] Next two fields used to check for duplicate component event handlers
errors: [{name:"checkIfUndefinedBlock"},{name:"checkIfIAmADuplicateEventHandler"}, {name:"checkComponentNotExistsError"}],
onchange: function(e) {
if (e.isTransient) {
return false; // don't trigger error check on transient actions.
}
return this.workspace.getWarningHandler() && this.workspace.getWarningHandler().checkErrors(this);
}
};
/**
* Create a method block of the given type for a component with the given instance name. methodType
* is one of the "methods" objects in a typeJsonString passed to Blockly.Component.add.
* @lends {Blockly.BlockSvg}
* @lends {Blockly.Block}
*/
Blockly.Blocks.component_method = {
category : 'Component',
helpUrl : function() {
var url = Blockly.ComponentBlock.METHODS_HELPURLS[this.getTypeName()];
if (url && url[0] == '/') {
var parts = url.split('#');
parts[1] = this.getTypeName() + '.' + this.methodName;
url = parts.join('#');
}
return url;
},
mutationToDom : function() {
var container = document.createElement('mutation');
container.setAttribute('component_type', this.typeName);
container.setAttribute('method_name', this.methodName);
var isGenericString = "false";
if(this.isGeneric){
isGenericString = "true";
}
container.setAttribute('is_generic', isGenericString);
if(!this.isGeneric) {
container.setAttribute('instance_name', this.instanceName);//instance name not needed
}
if (!this.isGeneric && this.typeName == "Clock" &&
Blockly.ComponentBlock.isClockMethodName(this.methodName)) {
var timeUnit = this.getFieldValue('TIME_UNIT');
container.setAttribute('method_name', 'Add' + timeUnit);
container.setAttribute('timeUnit', timeUnit);
}
return container;
},
domToMutation : function(xmlElement) {
var oldRendered = this.rendered;
this.rendered = false;
var oldInputValues = [];
for (var i = 0, input; input = this.inputList[i]; i++) {
if (input.connection) {
var block = input.connection.targetBlock();
if (block) {
block.unplug();
}
oldInputValues.push(block);
} else {
oldInputValues.push(null);
}
input.dispose();
}
this.inputList.length = 0;
this.typeName = xmlElement.getAttribute('component_type');
this.methodName = xmlElement.getAttribute('method_name');
var isGenericString = xmlElement.getAttribute('is_generic');
this.isGeneric = isGenericString == 'true';
if(!this.isGeneric) {
this.instanceName = xmlElement.getAttribute('instance_name');//instance name not needed
} else {
delete this.instanceName;
}
this.setColour(Blockly.ComponentBlock.COLOUR_METHOD);
this.componentDropDown = Blockly.ComponentBlock.createComponentDropDown(this);
//for non-generic blocks, set the value of the component drop down
if(!this.isGeneric) {
this.componentDropDown.setValue(this.instanceName);
}
var componentDb = this.getTopWorkspace().getComponentDatabase();
/** @type {MethodDescriptor} */
var methodTypeObject = this.getMethodTypeObject();
var localizedMethodName;
if (methodTypeObject) {
localizedMethodName = componentDb.getInternationalizedMethodName(methodTypeObject.name);
} else {
localizedMethodName = this.methodName;
}
if(!this.isGeneric) {
if (this.typeName == "Clock" && Blockly.ComponentBlock.isClockMethodName(this.methodName)) {
var timeUnitDropDown = Blockly.ComponentBlock.createClockAddDropDown();
this.appendDummyInput()
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_METHOD_TITLE_CALL)
.appendField(this.componentDropDown, Blockly.ComponentBlock.COMPONENT_SELECTOR)
.appendField('.Add')
.appendField(timeUnitDropDown, "TIME_UNIT");
switch (this.methodName){
case "AddYears":
this.setFieldValue('Years', "TIME_UNIT");
break;
case "AddMonths":
this.setFieldValue('Months', "TIME_UNIT");
break;
case "AddWeeks":
this.setFieldValue('Weeks', "TIME_UNIT");
break;
case "AddDays":
this.setFieldValue('Days', "TIME_UNIT");
break;
case "AddHours":
this.setFieldValue('Hours', "TIME_UNIT");
break;
case "AddMinutes":
this.setFieldValue('Minutes', "TIME_UNIT");
break;
case "AddSeconds":
this.setFieldValue('Seconds', "TIME_UNIT");
break;
case "AddDuration":
this.setFieldValue('Duration', "TIME_UNIT");
break;
}
} else {
this.appendDummyInput()
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_METHOD_TITLE_CALL)
.appendField(this.componentDropDown, Blockly.ComponentBlock.COMPONENT_SELECTOR)
.appendField('.' + localizedMethodName);
}
this.componentDropDown.setValue(this.instanceName);
} else {
this.appendDummyInput()
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_METHOD_TITLE_CALL + componentDb.getInternationalizedComponentType(this.typeName) + '.' + localizedMethodName);
this.appendValueInput("COMPONENT")
.setCheck(this.typeName).appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_METHOD_TITLE_FOR_COMPONENT)
.setAlign(Blockly.ALIGN_RIGHT);
}
var tooltipDescription;
if (methodTypeObject) {
tooltipDescription = componentDb.getInternationalizedMethodDescription(this.getTypeName(), methodTypeObject.name,
methodTypeObject.description);
} else {
tooltipDescription = componentDb.getInternationalizedMethodDescription(this.getTypeName(), this.methodName);
}
this.setTooltip(tooltipDescription);
var params = [];
if (methodTypeObject) {
params = methodTypeObject.parameters;
}
oldInputValues.splice(0, oldInputValues.length - params.length);
for (var i = 0, param; param = params[i]; i++) {
var name = componentDb.getInternationalizedParameterName(param.name);
var check = this.getParamBlocklyType(param);
var input = this.appendValueInput("ARG" + i)
.appendField(name)
.setAlign(Blockly.ALIGN_RIGHT)
.setCheck(check);
if (oldInputValues[i] && input.connection) {
Blockly.Mutator.reconnect(oldInputValues[i].outputConnection, this, 'ARG' + i);
}
}
for (var i = 0, input; input = this.inputList[i]; i++) {
input.init();
}
if (!methodTypeObject) {
this.setOutput(false);
this.setPreviousStatement(false);
this.setNextStatement(false);
} // methodType.returnType is a Yail type
else if (methodTypeObject.returnType) {
this.setOutput(true, Blockly.Blocks.Utilities.YailTypeToBlocklyType(methodTypeObject.returnType,Blockly.Blocks.Utilities.OUTPUT));
} else {
this.setPreviousStatement(true);
this.setNextStatement(true);
}
this.errors = [{name:"checkIfUndefinedBlock"}, {name:"checkIsInDefinition"},
{name:"checkComponentNotExistsError"}, {name: "checkGenericComponentSocket"}];
// Set as badBlock if it doesn't exist.
this.verify();
// Disable it if it does exist and is deprecated.
Blockly.ComponentBlock.checkDeprecated(this, this.getMethodTypeObject());
this.rendered = oldRendered;
},
getTypeName: function() {
return this.typeName === 'Form' ? 'Screen' : this.typeName;
},
// Rename the block's instanceName, type, and reset its title
rename : function(oldname, newname) {
if (this.instanceName == oldname) {
this.instanceName = newname;
//var title = this.inputList[0].titleRow[0];
//title.setText('call ' + this.instanceName + '.' + this.methodType.name);
this.componentDropDown.setValue(this.instanceName);
return true;
}
return false;
},
/**
* Get the underlying method descriptor for the block.
* @returns {(MethodDescriptor|undefined)}
*/
getMethodTypeObject : function() {
return this.getTopWorkspace().getComponentDatabase()
.getMethodForType(this.typeName, this.methodName);
},
getParamBlocklyType : function(param) {
var check = [];
var blocklyType = Blockly.Blocks.Utilities.YailTypeToBlocklyType(
param.type, Blockly.Blocks.Utilities.INPUT);
if (blocklyType) {
if (Array.isArray(blocklyType)) {
// Clone array.
check = blocklyType.slice();
} else {
check.push(blocklyType);
}
}
var helperType = Blockly.Blocks.Utilities
.helperKeyToBlocklyType(param.helperKey, this);
if (helperType && helperType != blocklyType) {
check.push(helperType);
}
return !check.length ? null : check;
},
getReturnBlocklyType : function(methodObj) {
var check = [];
var blocklyType = Blockly.Blocks.Utilities.YailTypeToBlocklyType(
methodObj.returnType, Blockly.Blocks.Utilities.OUTPUT);
if (blocklyType) {
if (Array.isArray(blocklyType)) {
// Clone array.
check = blocklyType.slice();
} else {
check.push(blocklyType);
}
}
var helperType = Blockly.Blocks.Utilities
.helperKeyToBlocklyType(methodObj.returnHelperKey, this);
if (helperType && helperType != blocklyType) {
check.push(helperType);
}
return !check.length ? null : check;
},
/**
* Get a mapping from input names to {@link Blockly.Input}s.
* @returns {Object.}}
*/
getArgInputs: function() {
var argList = {};
for (var i = 0, input; input = this.getInput('ARG' + i); i++) {
if (input.fieldRow.length == 1) { // should only be 0 or 1
argList[input.fieldRow[0].getValue()] = input;
}
}
return argList;
},
/**
* Get an array of argument names in the block.
* @returns {Array.}
*/
getArgs: function() {
var argList = [];
for (var i = 0, input; input = this.getInput('ARG' + i); i++) {
if (input.fieldRow.length == 1) { // should only be 0 or 1
argList.push(input.fieldRow[0].getValue());
}
}
return argList;
},
typeblock : function(){
var componentDb = Blockly.mainWorkspace.getComponentDatabase();
var tb = [];
var typeNameDict = {};
componentDb.forEachInstance(function(instance) {
typeNameDict[instance.typeName] = true;
componentDb.forMethodInType(instance.typeName, function(_, methodName) {
tb.push({
translatedName: Blockly.Msg.LANG_COMPONENT_BLOCK_METHOD_TITLE_CALL + instance.name +
'.' + componentDb.getInternationalizedMethodName(methodName),
mutatorAttributes: {
component_type: instance.typeName,
instance_name: instance.name,
method_name: methodName,
is_generic: 'false'
}
});
});
});
delete typeNameDict['Form'];
Object.keys(typeNameDict).forEach(function (typeName) {
componentDb.forMethodInType(typeName, function (_, methodName) {
tb.push({
translatedName: Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_METHOD_TITLE_CALL +
componentDb.getInternationalizedComponentType(typeName) + '.' +
componentDb.getInternationalizedMethodName(methodName),
mutatorAttributes: {
component_type: typeName,
method_name: methodName,
is_generic: 'true'
}
});
});
});
return tb;
},
// check if block corresponds to a method inside componentTypes[typeName].methodDictionary
verify : function() {
var validate = function() {
// check component type
var componentDb = this.getTopWorkspace().getComponentDatabase();
var componentType = componentDb.getType(this.typeName);
if (!componentType) {
return false; // component does NOT exist! should not happen!
}
/** @type {MethodDescriptor} */
var method = componentDb.getMethodForType(this.typeName, this.methodName);
// check method name
if (!method) {
return false; // no such method : this method was for another version! block is undefined!
}
// check parameters
var argList = this.getArgs();
var argInputList = this.getArgInputs();
var params = method.parameters;
var modifiedParameters = false;
if (argList.length != params.length) {
modifiedParameters = true; // parameters have changed
}
for (var x = 0; x < argList.length; ++x) {
var found = false;
for (var i = 0, param; param = params[i]; ++i) {
if (componentDb.getInternationalizedParameterName(param.name) == argList[x]) {
var input = argInputList[argList[x]];
if (!input || !input.connection) {
modifiedParameters = true;
break; // invalid input or connection
}
var check = this.getParamBlocklyType(param);
input.setCheck(check);
found = true;
break;
}
}
if (!found) {
modifiedParameters = true;
}
}
// check return type
var modifiedReturnType = false;
if (method.returnType) {
if (!this.outputConnection) {
modifiedReturnType = true; // missing return type
}
else {
this.outputConnection.setCheck(this.getReturnBlocklyType(method));
}
}
else if (!method.returnType) {
if (this.outputConnection) {
modifiedReturnType = true; // unexpected return type
}
}
return !(modifiedParameters || modifiedReturnType);
// passed all our tests! block is defined!
};
var isDefined = validate.call(this);
if (isDefined) {
this.notBadBlock();
} else {
this.badBlock();
}
},
customContextMenu: function(options) {
Blockly.ComponentBlock.addGenericOption(this, options);
Blockly.Block.prototype.customContextMenu.call(this, options);
}
};
/**
* Create a property getter or setter block for a component with the given
* instance name. Blocks can also be generic or not, depending on the
* values of the attribute in the mutators.
* @lends {Blockly.BlockSvg}
* @lends {Blockly.Block}
*/
Blockly.Blocks.component_set_get = {
category : 'Component',
//this.blockType = 'getter',
helpUrl : function() {
var url = Blockly.ComponentBlock.PROPERTIES_HELPURLS[this.getTypeName()];
if (url && url[0] == '/') {
var parts = url.split('#');
parts[1] = this.getTypeName() + '.' + this.propertyName;
url = parts.join('#');
}
return url;
},
init: function() {
this.componentDropDown = Blockly.ComponentBlock.createComponentDropDown(this);
this.genericComponentInput = Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_SETTER_TITLE_OF_COMPONENT;
},
mutationToDom : function() {
var container = document.createElement('mutation');
container.setAttribute('component_type', this.typeName);
container.setAttribute('set_or_get', this.setOrGet);
container.setAttribute('property_name', this.propertyName);
var isGenericString = "false";
if(this.isGeneric){
isGenericString = "true";
}
container.setAttribute('is_generic', isGenericString);
if(!this.isGeneric) {
container.setAttribute('instance_name', this.instanceName);//instance name not needed
}
return container;
},
domToMutation : function(xmlElement) {
var oldRendered = this.rendered;
this.rendered = false;
var oldInput = this.setOrGet == "set" && this.getInputTargetBlock('VALUE');
for (var i = 0, input; input = this.inputList[i]; i++) {
if (input.connection) {
var block = input.connection.targetBlock();
if (block) {
if (block.isShadow()) {
block.dispose();
} else {
block.unplug();
}
}
}
input.dispose();
}
this.inputList.length = 0;
var componentDb = this.getTopWorkspace().getComponentDatabase();
this.typeName = xmlElement.getAttribute('component_type');
this.setOrGet = xmlElement.getAttribute('set_or_get');
this.propertyName = xmlElement.getAttribute('property_name');
this.propertyObject = this.getPropertyObject(this.propertyName);
var isGenericString = xmlElement.getAttribute('is_generic');
this.isGeneric = isGenericString == "true";
if(!this.isGeneric) {
this.instanceName = xmlElement.getAttribute('instance_name');//instance name not needed
} else {
delete this.instanceName;
}
if(this.setOrGet == "set"){
this.setColour(Blockly.ComponentBlock.COLOUR_SET);
} else {
this.setColour(Blockly.ComponentBlock.COLOUR_GET);
}
var tooltipDescription;
if (this.propertyName && this.propertyObject) {
tooltipDescription = componentDb.getInternationalizedPropertyDescription(
this.getTypeName(), this.propertyName, this.propertyObject.description);
} else {
tooltipDescription = Blockly.Msg.UNDEFINED_BLOCK_TOOLTIP;
}
var thisBlock = this;
var dropdown = new Blockly.FieldDropdown(
function() {
return thisBlock.getPropertyDropDownList();
},
// change the output type and tooltip to match the new selection
function(selection) {
this.setValue(selection);
thisBlock.propertyName = selection;
thisBlock.propertyObject = thisBlock.getPropertyObject(selection);
thisBlock.setTypeCheck();
if (thisBlock.propertyName) {
thisBlock.setTooltip(componentDb.getInternationalizedPropertyDescription(thisBlock.getTypeName(),
thisBlock.propertyName, thisBlock.propertyObject.description));
} else {
thisBlock.setTooltip(Blockly.Msg.UNDEFINED_BLOCK_TOOLTIP);
}
}
);
if(this.setOrGet == "get") {
//add output plug for get blocks
this.setOutput(true);
if(!this.isGeneric) {
//non-generic get
this.appendDummyInput()
.appendField(this.componentDropDown, Blockly.ComponentBlock.COMPONENT_SELECTOR)
.appendField('.')
.appendField(dropdown, "PROP");
} else {
//generic get
this.appendDummyInput()
.appendField(componentDb.getInternationalizedComponentType(this.typeName) + '.')
.appendField(dropdown, "PROP");
this.appendValueInput("COMPONENT")
.setCheck(this.typeName)
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_GETTER_TITLE_OF_COMPONENT)
.setAlign(Blockly.ALIGN_RIGHT);
}
} else { //this.setOrGet == "set"
//a notches for set block
this.setPreviousStatement(true);
this.setNextStatement(true);
if(!this.isGeneric) {
this.appendValueInput("VALUE")
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_SETTER_TITLE_SET)
.appendField(this.componentDropDown, Blockly.ComponentBlock.COMPONENT_SELECTOR)
.appendField('.')
.appendField(dropdown, "PROP")
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_SETTER_TITLE_TO);
} else {
//generic set
this.appendDummyInput()
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_SETTER_TITLE_SET +
componentDb.getInternationalizedComponentType(this.typeName) + '.')
.appendField(dropdown, "PROP");
this.appendValueInput("COMPONENT")
.setCheck(this.typeName)
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_SETTER_TITLE_OF_COMPONENT)
.setAlign(Blockly.ALIGN_RIGHT);
this.appendValueInput("VALUE")
.appendField(Blockly.Msg.LANG_COMPONENT_BLOCK_GENERIC_SETTER_TITLE_TO)
.setAlign(Blockly.ALIGN_RIGHT);
}
}
if (oldInput) {
this.getInput('VALUE').init();
Blockly.Mutator.reconnect(oldInput.outputConnection, this, 'VALUE');
}
//for non-generic blocks, set the value of the component drop down
if(!this.isGeneric) {
this.componentDropDown.setValue(this.instanceName);
}
//set value of property drop down
this.setFieldValue(this.propertyName,"PROP");
//add appropriate type checking to block
this.setTypeCheck();
this.setTooltip(tooltipDescription);
this.errors = [{name:"checkIfUndefinedBlock"}, {name:"checkIsInDefinition"},
{name:"checkComponentNotExistsError"}, {name: 'checkGenericComponentSocket'},
{name: 'checkEmptySetterSocket'}];
// Set as badBlock if it doesn't exist.
this.verify();
// Disable it if it does exist and is deprecated.
Blockly.ComponentBlock.checkDeprecated(this, this.propertyObject);
for (var i = 0, input; input = this.inputList[i]; i++) {
input.init();
}
this.rendered = oldRendered;
},
getTypeName: function() {
return this.typeName === 'Form' ? 'Screen' : this.typeName;
},
setTypeCheck : function() {
var inputOrOutput = Blockly.Blocks.Utilities.OUTPUT;
if(this.setOrGet == "set") {
inputOrOutput = Blockly.Blocks.Utilities.INPUT;
}
var newType = this.getPropertyBlocklyType(this.propertyName,inputOrOutput);
// This will disconnect the block if the new outputType doesn't match the
// socket the block is plugged into.
if(this.setOrGet == "get") {
this.outputConnection.setCheck(newType);
} else {
this.getInput("VALUE").connection.setCheck(newType);
}
},
getPropertyBlocklyType : function(propertyName,inputOrOutput) {
var check = [];
var yailType = "any"; // Necessary for undefined propertyObject.
var property = this.getPropertyObject(propertyName);
if (property) {
yailType = property.type;
}
var blocklyType = Blockly.Blocks.Utilities
.YailTypeToBlocklyType(yailType, inputOrOutput);
if (blocklyType) {
if (Array.isArray(blocklyType)) {
// Clone array.
check = blocklyType.slice();
} else {
check.push(blocklyType);
}
}
var helperType = Blockly.Blocks.Utilities
.helperKeyToBlocklyType(property.helperKey, this);
if (helperType && helperType != blocklyType) {
check.push(helperType);
}
return !check.length ? null : check;
},
getPropertyDropDownList : function() {
var componentDb = this.getTopWorkspace().getComponentDatabase();
var dropDownList = [];
var propertyNames = [this.propertyName];
if (this.propertyObject) {
if (this.propertyObject.deprecated == "true") { // [lyn, 2015/12/27] Handle deprecated properties specially
propertyNames = [this.propertyObject.name]; // Only list the deprecated property name and no others
} else if(this.setOrGet == "set") {
propertyNames = componentDb.getSetterNamesForType(this.typeName);
} else {
propertyNames = componentDb.getGetterNamesForType(this.typeName);
}
}
for(var i=0;i