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