diff --git a/Blockly/html/agc/blocks/logic.js b/Blockly/html/agc/blocks/logic.js index c604429..06d4d58 100644 --- a/Blockly/html/agc/blocks/logic.js +++ b/Blockly/html/agc/blocks/logic.js @@ -5,7 +5,7 @@ Blockly.defineBlocksWithJsonArray([ { - 'type': 'agc_controls_if', + 'type': 'controls_if', 'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1', 'args0': [ { @@ -28,7 +28,7 @@ Blockly.defineBlocksWithJsonArray([ 'helpUrl': '%{BKY_CONTROLS_IF_HELPURL}', }, { - 'type': 'agc_controls_ifelse', + 'type': 'controls_ifelse', 'message0': '%{BKY_CONTROLS_IF_MSG_IF} %1', 'args0': [ { @@ -59,7 +59,7 @@ Blockly.defineBlocksWithJsonArray([ }, // Block for boolean data type: true and false. { - 'type': 'agc_logic_boolean', + 'type': 'logic_boolean', 'message0': '%1', 'args0': [ { @@ -78,7 +78,7 @@ Blockly.defineBlocksWithJsonArray([ }, // Block for comparison operator. { - 'type': 'agc_logic_compare', + 'type': 'logic_compare', 'message0': '%1 %2 %3', 'args0': [ { @@ -110,7 +110,7 @@ Blockly.defineBlocksWithJsonArray([ }, // Block for logical operations: 'and', 'or'. { - 'type': 'agc_logic_operation', + 'type': 'logic_operation', 'message0': '%1 %2 %3', 'args0': [ { @@ -140,7 +140,7 @@ Blockly.defineBlocksWithJsonArray([ }, // Block for negation. { - 'type': 'agc_logic_negate', + 'type': 'logic_negate', 'message0': '%{BKY_LOGIC_NEGATE_TITLE}', 'args0': [ { @@ -156,7 +156,7 @@ Blockly.defineBlocksWithJsonArray([ }, // Block for ternary operator. { - 'type': 'agc_logic_ternary', + 'type': 'logic_ternary', 'message0': '%{BKY_LOGIC_TERNARY_CONDITION} %1', 'args0': [ { @@ -186,3 +186,144 @@ Blockly.defineBlocksWithJsonArray([ 'extensions': ['logic_ternary'], }, ]); + +(function() { + /** + * Adds dynamic type validation for the left and right sides of a logic_compare + * block. + * @mixin + * @augments Blockly.Block + * @package + * @readonly + */ + const LOGIC_COMPARE_ONCHANGE_MIXIN = { + /** + * Called whenever anything on the workspace changes. + * Prevent mismatched types from being compared. + * @param {!Blockly.Events.Abstract} e Change event. + * @this {Blockly.Block} + */ + onchange: function(e) { + if (!this.prevBlocks_) { + this.prevBlocks_ = [null, null]; + } + + const blockA = this.getInputTargetBlock('A'); + const blockB = this.getInputTargetBlock('B'); + // Disconnect blocks that existed prior to this change if they don't match. + if (blockA && blockB && + !this.workspace.connectionChecker.doTypeChecks( + blockA.outputConnection, blockB.outputConnection)) { + // Mismatch between two inputs. Revert the block connections, + // bumping away the newly connected block(s). + Blockly.Events.setGroup(e.group); + const prevA = this.prevBlocks_[0]; + if (prevA !== blockA) { + blockA.unplug(); + if (prevA && !prevA.isDisposed() && !prevA.isShadow()) { + // The shadow block is automatically replaced during unplug(). + this.getInput('A').connection.connect(prevA.outputConnection); + } + } + const prevB = this.prevBlocks_[1]; + if (prevB !== blockB) { + blockB.unplug(); + if (prevB && !prevB.isDisposed() && !prevB.isShadow()) { + // The shadow block is automatically replaced during unplug(). + this.getInput('B').connection.connect(prevB.outputConnection); + } + } + this.bumpNeighbours(); + Blockly.Events.setGroup(false); + } + this.prevBlocks_[0] = this.getInputTargetBlock('A'); + this.prevBlocks_[1] = this.getInputTargetBlock('B'); + }, + }; + + /** + * "logic_compare" extension function. Adds type left and right side type + * checking to "logic_compare" blocks. + * @this {Blockly.Block} + * @package + * @readonly + */ + const LOGIC_COMPARE_EXTENSION = function() { + // Add onchange handler to ensure types are compatible. + this.mixin(LOGIC_COMPARE_ONCHANGE_MIXIN); + }; + + Blockly.Extensions.register('logic_compare', LOGIC_COMPARE_EXTENSION); + + /** + * Tooltip text, keyed by block OP value. Used by logic_compare and + * logic_operation blocks. + * @see {Blockly.Extensions#buildTooltipForDropdown} + * @package + * @readonly + */ + const TOOLTIPS_BY_OP = { + // logic_compare + 'EQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}', + 'NEQ': '%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}', + 'LT': '%{BKY_LOGIC_COMPARE_TOOLTIP_LT}', + 'LTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}', + 'GT': '%{BKY_LOGIC_COMPARE_TOOLTIP_GT}', + 'GTE': '%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}', + + // logic_operation + 'AND': '%{BKY_LOGIC_OPERATION_TOOLTIP_AND}', + 'OR': '%{BKY_LOGIC_OPERATION_TOOLTIP_OR}', + }; + + Blockly.Extensions.register('logic_op_tooltip', + Blockly.Extensions.buildTooltipForDropdown('OP', TOOLTIPS_BY_OP)); + + /** + * Adds type coordination between inputs and output. + * @mixin + * @augments Blockly.Block + * @package + * @readonly + */ + const LOGIC_TERNARY_ONCHANGE_MIXIN = { + prevParentConnection_: null, + + /** + * Called whenever anything on the workspace changes. + * Prevent mismatched types. + * @param {!Blockly.Events.Abstract} e Change event. + * @this {Blockly.Block} + */ + onchange: function(e) { + const blockA = this.getInputTargetBlock('THEN'); + const blockB = this.getInputTargetBlock('ELSE'); + const parentConnection = this.outputConnection.targetConnection; + // Disconnect blocks that existed prior to this change if they don't match. + if ((blockA || blockB) && parentConnection) { + for (let i = 0; i < 2; i++) { + const block = (i === 1) ? blockA : blockB; + if (block && + !block.workspace.connectionChecker.doTypeChecks( + block.outputConnection, parentConnection)) { + // Ensure that any disconnections are grouped with the causing event. + Blockly.Events.setGroup(e.group); + if (parentConnection === this.prevParentConnection_) { + this.unplug(); + parentConnection.getSourceBlock().bumpNeighbours(); + } else { + block.unplug(); + block.bumpNeighbours(); + } + Blockly.Events.setGroup(false); + } + } + } + this.prevParentConnection_ = parentConnection; + }, + }; + + Blockly.Extensions.registerMixin('logic_ternary', + LOGIC_TERNARY_ONCHANGE_MIXIN); + +})(); diff --git a/Blockly/html/agc/blocks/loops.js b/Blockly/html/agc/blocks/loops.js index a0104a6..7da5ba2 100644 --- a/Blockly/html/agc/blocks/loops.js +++ b/Blockly/html/agc/blocks/loops.js @@ -6,7 +6,7 @@ Blockly.defineBlocksWithJsonArray([ // Block for 'do while/until' loop. { - 'type': 'agc_controls_whileUntil', + 'type': 'controls_whileUntil', 'message0': '%1 %2', 'args0': [ { @@ -37,3 +37,11 @@ Blockly.defineBlocksWithJsonArray([ 'extensions': ['controls_whileUntil_tooltip'], }, ]); + + +Blockly.Extensions.register('controls_whileUntil_tooltip', + Blockly.Extensions.buildTooltipForDropdown( + 'MODE', { + 'WHILE': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}', + 'UNTIL': '%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}', +})); diff --git a/Blockly/html/agc/blocks/math.js b/Blockly/html/agc/blocks/math.js index 5edf836..a53eaf5 100644 --- a/Blockly/html/agc/blocks/math.js +++ b/Blockly/html/agc/blocks/math.js @@ -6,7 +6,7 @@ Blockly.defineBlocksWithJsonArray([ // Block for numeric value. { - 'type': 'agc_math_number', + 'type': 'math_number', 'message0': '%1', 'args0': [{ 'type': 'field_number', @@ -25,7 +25,7 @@ Blockly.defineBlocksWithJsonArray([ // Block for basic arithmetic operator. { - 'type': 'agc_math_arithmetic', + 'type': 'math_arithmetic', 'message0': '%1 %2 %3', 'args0': [ { @@ -57,7 +57,7 @@ Blockly.defineBlocksWithJsonArray([ // Block for random integer between 0 and [X-1]. { - "type": "agc_math_random_int_0", + "type": "math_random_int_0", "message0": "random integer to %1", "args0": [ { @@ -75,7 +75,7 @@ Blockly.defineBlocksWithJsonArray([ // Block for random integer between [X] and [Y]. { - "type": "agc_math_random_int", + "type": "math_random_int", "message0": "%{BKY_MATH_RANDOM_INT_TITLE}", "args0": [ { @@ -95,4 +95,81 @@ Blockly.defineBlocksWithJsonArray([ "tooltip": "%{BKY_MATH_RANDOM_INT_TOOLTIP}", "helpUrl": "%{BKY_MATH_RANDOM_INT_HELPURL}", }, + // Block for adding to a variable in place. + { + "type": "math_change", + "message0": "%{BKY_MATH_CHANGE_TITLE}", + "args0": [ + { + "type": "field_variable", + "name": "VAR", + "variable": "%{BKY_MATH_CHANGE_TITLE_ITEM}", + }, + { + "type": "input_value", + "name": "DELTA", + "check": "Number", + }, + ], + "previousStatement": null, + "nextStatement": null, + "style": "variable_blocks", + "helpUrl": "%{BKY_MATH_CHANGE_HELPURL}", + "extensions": ["math_change_tooltip"], + }, ]); + +// Update the tooltip of 'math_change' block to reference the variable. +Blockly.Extensions.register('math_change_tooltip', + Blockly.Extensions.buildTooltipWithFieldText( + '%{BKY_MATH_CHANGE_TOOLTIP}', 'VAR')); + + +(function() { + /** + * Mapping of math block OP value to tooltip message for blocks + * math_arithmetic, math_simple, math_trig, and math_on_lists. + * @see {Blockly.Extensions#buildTooltipForDropdown} + * @package + * @readonly + */ + const TOOLTIPS_BY_OP = { + // math_arithmetic + 'ADD': '%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}', + 'MINUS': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}', + 'MULTIPLY': '%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}', + 'DIVIDE': '%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}', + 'POWER': '%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}', + + // math_simple + 'ROOT': '%{BKY_MATH_SINGLE_TOOLTIP_ROOT}', + 'ABS': '%{BKY_MATH_SINGLE_TOOLTIP_ABS}', + 'NEG': '%{BKY_MATH_SINGLE_TOOLTIP_NEG}', + 'LN': '%{BKY_MATH_SINGLE_TOOLTIP_LN}', + 'LOG10': '%{BKY_MATH_SINGLE_TOOLTIP_LOG10}', + 'EXP': '%{BKY_MATH_SINGLE_TOOLTIP_EXP}', + 'POW10': '%{BKY_MATH_SINGLE_TOOLTIP_POW10}', + + // math_trig + 'SIN': '%{BKY_MATH_TRIG_TOOLTIP_SIN}', + 'COS': '%{BKY_MATH_TRIG_TOOLTIP_COS}', + 'TAN': '%{BKY_MATH_TRIG_TOOLTIP_TAN}', + 'ASIN': '%{BKY_MATH_TRIG_TOOLTIP_ASIN}', + 'ACOS': '%{BKY_MATH_TRIG_TOOLTIP_ACOS}', + 'ATAN': '%{BKY_MATH_TRIG_TOOLTIP_ATAN}', + + // math_on_lists + 'SUM': '%{BKY_MATH_ONLIST_TOOLTIP_SUM}', + 'MIN': '%{BKY_MATH_ONLIST_TOOLTIP_MIN}', + 'MAX': '%{BKY_MATH_ONLIST_TOOLTIP_MAX}', + 'AVERAGE': '%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}', + 'MEDIAN': '%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}', + 'MODE': '%{BKY_MATH_ONLIST_TOOLTIP_MODE}', + 'STD_DEV': '%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}', + 'RANDOM': '%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}', + }; + + Blockly.Extensions.register('math_op_tooltip', + Blockly.Extensions.buildTooltipForDropdown( + 'OP', TOOLTIPS_BY_OP)); +})(); diff --git a/Blockly/html/agc/blocks/procedures.js b/Blockly/html/agc/blocks/procedures.js new file mode 100644 index 0000000..98e486a --- /dev/null +++ b/Blockly/html/agc/blocks/procedures.js @@ -0,0 +1,460 @@ +/** + * @fileoverview AGC procedure blocks for Blockly. + */ +'use strict'; + + +Blockly.Blocks['procedures_defnoreturn'] = { + /** + * Block for defining a procedure with no return value. + * @this {Blockly.Block} + */ + init: function() { + const initName = Blockly.Procedures.findLegalName('', this); + const nameField = new Blockly.FieldTextInput(initName, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_TITLE']) + .appendField(nameField, 'NAME'); + this.appendStatementInput('STACK') + .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_DO']); + if ((this.workspace.options.comments || + (this.workspace.options.parentWorkspace && + this.workspace.options.parentWorkspace.options.comments)) && + Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']) { + this.setCommentText(Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']); + } + this.setStyle('procedure_blocks'); + this.setTooltip(Blockly.Msg['PROCEDURES_DEFNORETURN_TOOLTIP']); + this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFNORETURN_HELPURL']); + }, + /** + * Create XML to represent the argument inputs. + * Backwards compatible serialization implementation. + * @param {boolean=} opt_paramIds If true include the IDs of the parameter + * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. + * @return {!Element} XML storage element. + * @this {Blockly.Block} + */ + mutationToDom: function(opt_paramIds) { + const container = Blockly.utils.xml.createElement('mutation'); + if (opt_paramIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } + return container; + }, + /** + * Parse XML to restore the argument inputs. + * Backwards compatible serialization implementation. + * @param {!Element} xmlElement XML storage element. + * @this {Blockly.Block} + */ + domToMutation: function(xmlElement) { + Blockly.Procedures.mutateCallers(this); + }, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES NOT have a return value. + * @this {Blockly.Block} + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), [], false]; + }, + /** + * Return all variables referenced by this block. + * @return {!Array} List of variable models. + * @this {Blockly.Block} + */ + getVarModels: function() { + return []; + }, + /** + * Add custom menu options to this block's context menu. + * @param {!Array} options List of menu options to add to. + * @this {Blockly.Block} + */ + customContextMenu: function(options) { + if (this.isInFlyout) { + return; + } + // Add option to create caller. + const option = {enabled: true}; + const name = this.getFieldValue('NAME'); + option.text = Blockly.Msg['PROCEDURES_CREATE_DO'].replace('%1', name); + const xmlMutation = Blockly.utils.xml.createElement('mutation'); + xmlMutation.setAttribute('name', name); + const xmlBlock = Blockly.utils.xml.createElement('block'); + xmlBlock.setAttribute('type', this.callType_); + xmlBlock.appendChild(xmlMutation); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + }, + callType_: 'procedures_callnoreturn', +}; + +Blockly.Blocks['procedures_defreturn'] = { + /** + * Block for defining a procedure with a return value. + * @this {Blockly.Block} + */ + init: function() { + const initName = Blockly.Procedures.findLegalName('', this); + const nameField = new Blockly.FieldTextInput(initName, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_TITLE']) + .appendField(nameField, 'NAME'); + this.appendStatementInput('STACK') + .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_DO']); + this.appendValueInput('RETURN') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); + if ((this.workspace.options.comments || + (this.workspace.options.parentWorkspace && + this.workspace.options.parentWorkspace.options.comments)) && + Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']) { + this.setCommentText(Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']); + } + this.setStyle('procedure_blocks'); + this.setTooltip(Blockly.Msg['PROCEDURES_DEFRETURN_TOOLTIP']); + this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFRETURN_HELPURL']); + }, + mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES have a return value. + * @this {Blockly.Block} + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), [], true]; + }, + getVarModels: Blockly.Blocks['procedures_defnoreturn'].getVarModels, + customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu, + callType_: 'procedures_callreturn', +}; + + +Blockly.Blocks['procedures_callnoreturn'] = { + /** + * Block for calling a procedure with no return value. + * @this {Blockly.Block} + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField('', 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle('procedure_blocks'); + // Tooltip is set in renameProcedure. + this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLNORETURN_HELPURL']); + this.previousEnabledState_ = true; + }, + + /** + * Returns the name of the procedure this block calls. + * @return {string} Procedure name. + * @this {Blockly.Block} + */ + getProcedureCall: function() { + // The NAME field is guaranteed to exist, null will never be returned. + return /** @type {string} */ (this.getFieldValue('NAME')); + }, + /** + * Notification that a procedure is renaming. + * If the name matches this block's procedure, rename it. + * @param {string} oldName Previous name of procedure. + * @param {string} newName Renamed procedure. + * @this {Blockly.Block} + */ + renameProcedure: function(oldName, newName) { + if (Blockly.Names.equals(oldName, this.getProcedureCall())) { + this.setFieldValue(newName, 'NAME'); + const baseMsg = this.outputConnection ? + Blockly.Msg['PROCEDURES_CALLRETURN_TOOLTIP'] : + Blockly.Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; + this.setTooltip(baseMsg.replace('%1', newName)); + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * Backwards compatible serialization implementation. + * @return {!Element} XML storage element. + * @this {Blockly.Block} + */ + mutationToDom: function() { + const container = Blockly.utils.xml.createElement('mutation'); + container.setAttribute('name', this.getProcedureCall()); + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * Backwards compatible serialization implementation. + * @param {!Element} xmlElement XML storage element. + * @this {Blockly.Block} + */ + domToMutation: function(xmlElement) { + const name = xmlElement.getAttribute('name'); + this.renameProcedure(this.getProcedureCall(), name); + }, + /** + * Return all variables referenced by this block. + * @return {!Array} List of variable models. + * @this {Blockly.Block} + */ + getVarModels: function() { + return []; + }, + /** + * Procedure calls cannot exist without the corresponding procedure + * definition. Enforce this link whenever an event is fired. + * @param {!Blockly.Events.Abstract} event Change event. + * @this {Blockly.Block} + */ + onchange: function(event) { + if (!this.workspace || this.workspace.isFlyout) { + // Block is deleted or is in a flyout. + return; + } + if (!event.recordUndo) { + // Events not generated by user. Skip handling. + return; + } + if (event.type === Blockly.Events.BLOCK_CREATE && + event.ids.indexOf(this.id) !== -1) { + // Look for the case where a procedure call was created (usually through + // paste) and there is no matching definition. In this case, create + // an empty definition block with the correct signature. + const name = this.getProcedureCall(); + let def = Blockly.Procedures.getDefinition(name, this.workspace); + if (def && (def.type !== this.defType_)) { + // The signatures don't match. + def = null; + } + if (!def) { + Blockly.Events.setGroup(event.group); + /** + * Create matching definition block. + * + * + * + * test + * + * + */ + const xml = Blockly.utils.xml.createElement('xml'); + const block = Blockly.utils.xml.createElement('block'); + block.setAttribute('type', this.defType_); + const xy = this.getRelativeToSurfaceXY(); + const x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1); + const y = xy.y + Blockly.SNAP_RADIUS * 2; + block.setAttribute('x', x); + block.setAttribute('y', y); + const mutation = this.mutationToDom(); + block.appendChild(mutation); + const field = Blockly.utils.xml.createElement('field'); + field.setAttribute('name', 'NAME'); + let callName = this.getProcedureCall(); + if (!callName) { + // Rename if name is empty string. + callName = Blockly.Procedures.findLegalName('', this); + this.renameProcedure('', callName); + } + field.appendChild(Blockly.utils.xml.createTextNode(callName)); + block.appendChild(field); + xml.appendChild(block); + Blockly.Xml.domToWorkspace(xml, this.workspace); + Blockly.Events.setGroup(false); + } + } else if (event.type === Blockly.Events.BLOCK_DELETE) { + // Look for the case where a procedure definition has been deleted, + // leaving this block (a procedure call) orphaned. In this case, delete + // the orphan. + const name = this.getProcedureCall(); + const def = Blockly.Procedures.getDefinition(name, this.workspace); + if (!def) { + Blockly.Events.setGroup(event.group); + this.dispose(true); + Blockly.Events.setGroup(false); + } + } else if (event.type === Blockly.Events.CHANGE && event.element === 'disabled') { + const name = this.getProcedureCall(); + const def = Blockly.Procedures.getDefinition(name, this.workspace); + if (def && def.id === event.blockId) { + // in most cases the old group should be '' + const oldGroup = Blockly.Events.getGroup(); + if (oldGroup) { + // This should only be possible programmatically and may indicate a problem + // with event grouping. If you see this message please investigate. If the + // use ends up being valid we may need to reorder events in the undo stack. + console.log('Saw an existing group while responding to a definition change'); + } + Blockly.Events.setGroup(event.group); + if (event.newValue) { + this.previousEnabledState_ = this.isEnabled(); + this.setEnabled(false); + } else { + this.setEnabled(this.previousEnabledState_); + } + Blockly.Events.setGroup(oldGroup); + } + } + }, + /** + * Add menu option to find the definition block for this call. + * @param {!Array} options List of menu options to add to. + * @this {Blockly.Block} + */ + customContextMenu: function(options) { + if (!this.workspace.isMovable()) { + // If we center on the block and the workspace isn't movable we could + // loose blocks at the edges of the workspace. + return; + } + + const option = {enabled: true}; + option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF']; + const name = this.getProcedureCall(); + const workspace = this.workspace; + option.callback = function() { + const def = Blockly.Procedures.getDefinition(name, workspace); + if (def) { + workspace.centerOnBlock(def.id); + def.select(); + } + }; + options.push(option); + }, + defType_: 'procedures_defnoreturn', +}; + +Blockly.Blocks['procedures_callreturn'] = { + /** + * Block for calling a procedure with a return value. + * @this {Blockly.Block} + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField('', 'NAME'); + this.setOutput(true); + this.setStyle('procedure_blocks'); + // Tooltip is set in domToMutation. + this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLRETURN_HELPURL']); + this.previousEnabledState_ = true; + }, + + getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, + renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, + mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, + getVarModels: Blockly.Blocks['procedures_callnoreturn'].getVarModels, + onchange: Blockly.Blocks['procedures_callnoreturn'].onchange, + customContextMenu: + Blockly.Blocks['procedures_callnoreturn'].customContextMenu, + defType_: 'procedures_defreturn', +}; + +Blockly.Blocks['procedures_ifreturn'] = { + /** + * Block for conditionally returning a value from a procedure. + * @this {Blockly.Block} + */ + init: function() { + this.appendValueInput('CONDITION') + .setCheck('Boolean') + .appendField(Blockly.Msg['CONTROLS_IF_MSG_IF']); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle('procedure_blocks'); + this.setTooltip(Blockly.Msg['PROCEDURES_IFRETURN_TOOLTIP']); + this.setHelpUrl(Blockly.Msg['PROCEDURES_IFRETURN_HELPURL']); + this.hasReturnValue_ = true; + }, + /** + * Create XML to represent whether this block has a return value. + * @return {!Element} XML storage element. + * @this {Blockly.Block} + */ + mutationToDom: function() { + const container = Blockly.utils.xml.createElement('mutation'); + container.setAttribute('value', Number(this.hasReturnValue_)); + return container; + }, + /** + * Parse XML to restore whether this block has a return value. + * @param {!Element} xmlElement XML storage element. + * @this {Blockly.Block} + */ + domToMutation: function(xmlElement) { + const value = xmlElement.getAttribute('value'); + this.hasReturnValue_ = (value === '1'); + if (!this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE') + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); + } + }, + + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * @param {!Blockly.Events.Abstract} _e Change event. + * @this {Blockly.Block} + */ + onchange: function(_e) { + if (this.workspace.isDragging && this.workspace.isDragging()) { + return; // Don't change state at the start of a drag. + } + let legal = false; + // Is the block nested in a procedure? + let block = this; + do { + if (this.FUNCTION_TYPES.indexOf(block.type) !== -1) { + legal = true; + break; + } + block = block.getSurroundParent(); + } while (block); + if (legal) { + // If needed, toggle whether this block has a return value. + if (block.type === 'procedures_defnoreturn' && this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE') + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); + this.hasReturnValue_ = false; + } else if (block.type === 'procedures_defreturn' && + !this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']); + this.hasReturnValue_ = true; + } + this.setWarningText(null); + if (!this.isInFlyout) { + this.setEnabled(true); + } + } else { + this.setWarningText(Blockly.Msg['PROCEDURES_IFRETURN_WARNING']); + if (!this.isInFlyout && !this.getInheritedDisabled()) { + this.setEnabled(false); + } + } + }, + /** + * List of block types that are functions and thus do not need warnings. + * To add a new function type add this to your code: + * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); + */ + FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'], +}; diff --git a/Blockly/html/agc/blocks/variables.js b/Blockly/html/agc/blocks/variables.js index e321566..ca1b380 100644 --- a/Blockly/html/agc/blocks/variables.js +++ b/Blockly/html/agc/blocks/variables.js @@ -54,7 +54,7 @@ Blockly.defineBlocksWithJsonArray([ * @package * @readonly */ -Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { +Blockly.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { /** * Add menu option to create getter/setter block for this setter/getter. * @param {!Array} options List of menu options to add to. @@ -111,7 +111,7 @@ Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { * @param {!Blockly.Block} block The block with the variable to rename. * @return {!function()} A function that renames the variable. */ -Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY = function(block) { +Blockly.Variables.RENAME_OPTION_CALLBACK_FACTORY = function(block) { return function() { const workspace = block.workspace; const variable = block.getField('VAR').getVariable(); @@ -125,7 +125,7 @@ Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY = function(block) { * @param {!Blockly.Block} block The block with the variable to delete. * @return {!function()} A function that deletes the variable. */ -Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY = function(block) { +Blockly.Variables.DELETE_OPTION_CALLBACK_FACTORY = function(block) { return function() { const workspace = block.workspace; const variable = block.getField('VAR').getVariable(); @@ -135,4 +135,4 @@ Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY = function(block) { }; Blockly.Extensions.registerMixin('contextMenu_variableSetterGetter', - Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); + Blockly.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN); diff --git a/Blockly/html/agc/generator/generator.js b/Blockly/html/agc/generator/generator.js index ade78b7..665a811 100644 --- a/Blockly/html/agc/generator/generator.js +++ b/Blockly/html/agc/generator/generator.js @@ -134,7 +134,7 @@ AgcGenerator.scrub_ = function(block, code, opt_thisOnly) { let comment = block.getCommentText(); if (comment) { comment = Blockly.utils.string.wrap(comment, this.COMMENT_WRAP - 3); - commentCode += this.prefixLines(comment + '\n', '// '); + commentCode += this.prefixLines(comment + '\n', '# '); } // Collect comments for all value arguments. // Don't collect comments for nested statements. @@ -144,7 +144,7 @@ AgcGenerator.scrub_ = function(block, code, opt_thisOnly) { if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { - commentCode += this.prefixLines(comment, '// '); + commentCode += this.prefixLines(comment, '# '); } } } diff --git a/Blockly/html/agc/generator/logic.js b/Blockly/html/agc/generator/logic.js index 0d7f4be..ee8e70c 100644 --- a/Blockly/html/agc/generator/logic.js +++ b/Blockly/html/agc/generator/logic.js @@ -4,7 +4,7 @@ */ 'use strict'; -AgcGenerator['agc_controls_if'] = function(block) { +AgcGenerator['controls_if'] = function(block) { // If condition. const labelIf = 'IF' + AgcGenerator.getUniqueLabel(); const conditionCode = @@ -21,7 +21,7 @@ ${labelIf}-X return code; }; -AgcGenerator['agc_controls_ifelse'] = function(block) { +AgcGenerator['controls_ifelse'] = function(block) { // If/else condition. const labelIf = 'IF' + AgcGenerator.getUniqueLabel(); const conditionCode = @@ -42,14 +42,14 @@ ${labelIf}-X return code; }; -AgcGenerator['agc_logic_boolean'] = function(block) { +AgcGenerator['logic_boolean'] = function(block) { // Boolean values true and false. const boolean = block.getFieldValue('BOOL'); const code = `\tCA\tNUM${boolean}\n`; return code; }; -AgcGenerator['agc_logic_compare'] = function(block) { +AgcGenerator['logic_compare'] = function(block) { // Comparison operator. const OPERATORS = { 'EQ': '\tTCR\tBL-NOT', @@ -73,7 +73,7 @@ AgcGenerator['agc_logic_compare'] = function(block) { return code; }; -AgcGenerator['agc_logic_operation'] = function(block) { +AgcGenerator['logic_operation'] = function(block) { // Operations 'and', 'or'. const operator = block.getFieldValue('OP'); // AND or OR let argument0 = AgcGenerator.valueToCode(block, 'A'); @@ -98,11 +98,11 @@ AgcGenerator['agc_logic_operation'] = function(block) { ${argument1} \tTCR\tPUSH \tTCR\tBL-${operator} - `; +`; return code; }; -AgcGenerator['agc_logic_negate'] = function(block) { +AgcGenerator['logic_negate'] = function(block) { // Negation. const argument0 = AgcGenerator.valueToCode(block, 'BOOL') || AgcGenerator.default1; const code = ` @@ -112,7 +112,7 @@ ${argument0} return code; }; -AgcGenerator['agc_logic_ternary'] = function(block) { +AgcGenerator['logic_ternary'] = function(block) { // Ternary operator. const argument0 = AgcGenerator.valueToCode(block, 'ELSE') || AgcGenerator.default0; const argument1 = AgcGenerator.valueToCode(block, 'THEN') || AgcGenerator.default0; diff --git a/Blockly/html/agc/generator/loops.js b/Blockly/html/agc/generator/loops.js index 22bcacf..8efaf52 100644 --- a/Blockly/html/agc/generator/loops.js +++ b/Blockly/html/agc/generator/loops.js @@ -4,7 +4,7 @@ */ 'use strict'; -AgcGenerator['agc_controls_whileUntil'] = function(block) { +AgcGenerator['controls_whileUntil'] = function(block) { // Generator for 'do while/until' loop. const labelLoop = 'LOOP' + AgcGenerator.getUniqueLabel(); const conditionCode = diff --git a/Blockly/html/agc/generator/math.js b/Blockly/html/agc/generator/math.js index e4e2d93..58ca0b6 100644 --- a/Blockly/html/agc/generator/math.js +++ b/Blockly/html/agc/generator/math.js @@ -4,7 +4,7 @@ */ 'use strict'; -AgcGenerator['agc_math_number'] = function(block) { +AgcGenerator['math_number'] = function(block) { // Numeric value. const number = Number(block.getFieldValue('NUM')); AgcGenerator.provideFunction_('NUM' + number, [`NUM${number}\tDEC\t${number}`]); @@ -12,7 +12,7 @@ AgcGenerator['agc_math_number'] = function(block) { return code; }; -AgcGenerator['agc_math_arithmetic'] = function(block) { +AgcGenerator['math_arithmetic'] = function(block) { // Basic arithmetic operators. const OPERATORS = { 'ADD': 'MA-AD', @@ -32,7 +32,7 @@ ${argument0} return code; }; -AgcGenerator['agc_math_random_int_0'] = function(block) { +AgcGenerator['math_random_int_0'] = function(block) { const to = AgcGenerator.valueToCode(block, 'TO') || AgcGenerator.default1; const code = ` ${to} @@ -41,3 +41,16 @@ ${to} `; return code; }; + +AgcGenerator['math_change'] = function(block) { + // Add to a variable in place. + const argument0 = AgcGenerator.valueToCode(block, 'DELTA') || AgcGenerator.default0; + const varName = AgcGenerator.nameDB_.getName(block.getFieldValue('VAR'), + Blockly.VARIABLE_CATEGORY_NAME); + + const code = ` +${argument0} +\tADS\t${varName} +`; + return code; +}; diff --git a/Blockly/html/agc/generator/procedures.js b/Blockly/html/agc/generator/procedures.js new file mode 100644 index 0000000..8f7a088 --- /dev/null +++ b/Blockly/html/agc/generator/procedures.js @@ -0,0 +1,83 @@ +/** + * @fileoverview Generating AGC assembly for procedure blocks. + */ +'use strict'; + +AgcGenerator['procedures_defreturn'] = function(block) { + // Procedure definition. + const funcName = AgcGenerator.nameDB_.getName(block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME); + // Create the function definition. + // 1. Push the return pointer onto the stack. + // 2. Execute the body of the function. + const branch = AgcGenerator.statementToCode(block, 'STACK'); + let code = ` +${funcName}\tCA\tQ +\t\tTCR\tPUSH +${branch}`; + if (block.getInput('RETURN')) { + // 3. Execute the return value. + // 4. Preserve the A register (return value) into L. + // 5. Pop the return pointer from the stack into Q. + // 6. Restore the return value from L back into the A register. + // 7. Return from the function. + const returnValue = AgcGenerator.valueToCode(block, 'RETURN') || AgcGenerator.default0; + code += ` +${returnValue} +\t\tTS\tL +\t\tTCR\tPOP +\t\tTS\tQ +\t\tCA\tL +\t\tRETURN +`; + } else { + // 3. Pop the return pointer from the stack into Q. + // 4. Return from the function. + code += ` +\t\tTCR\tPOP +\t\tTS\tQ +\t\tRETURN +`; + } + code = AgcGenerator.scrub_(block, code); + // Add % so as not to collide with helper functions in definitions list. + AgcGenerator.definitions_['%' + funcName] = code; + return null; +}; + +// Defining a procedure without a return value uses the same generator as +// defining a procedure with a return value. +AgcGenerator['procedures_defnoreturn'] = AgcGenerator['procedures_defreturn']; + +AgcGenerator['procedures_callreturn'] = function(block) { + // Call a procedure with a return value. + const funcName = AgcGenerator.nameDB_.getName(block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME); + const code = `\t\tTCR\t${funcName}\n`; + return code; +}; + +// Calling a procedure without a return value uses the same generator as +// a calling procedure with a return value. +AgcGenerator['procedures_callnoreturn'] = AgcGenerator['procedures_callreturn']; + + +AgcGenerator['procedures_ifreturn'] = function(block) { + // Conditionally return value from a procedure. + const labelIf = 'IF' + AgcGenerator.getUniqueLabel(); + const condition = AgcGenerator.valueToCode(block, 'CONDITION') || + AgcGenerator.defaultFalse; + const returnValue = block.hasReturnValue_ ? + AgcGenerator.valueToCode(block, 'VALUE') || AgcGenerator.default0 : ''; + let code = ` +${condition} +\tEXTEND +\tBZF\t${labelIf} +\t\tTCR\tPOP +\t\tTS\tQ +${returnValue} +\tRETURN +${labelIf} +`; + return code; +}; diff --git a/Blockly/html/agc/generator/variables.js b/Blockly/html/agc/generator/variables.js index 529ecd3..66afcff 100644 --- a/Blockly/html/agc/generator/variables.js +++ b/Blockly/html/agc/generator/variables.js @@ -15,8 +15,7 @@ AgcGenerator['variables_get'] = function(block) { AgcGenerator['variables_set'] = function(block) { // Variable setter. - const argument0 = AgcGenerator.valueToCode( - block, 'VALUE') || + const argument0 = AgcGenerator.valueToCode(block, 'VALUE') || AgcGenerator.default0; const varName = AgcGenerator.nameDB_.getName( block.getFieldValue('VAR'), Blockly.VARIABLE_CATEGORY_NAME); diff --git a/Blockly/html/agc/index.html b/Blockly/html/agc/index.html index 19f1fc3..383ddc0 100644 --- a/Blockly/html/agc/index.html +++ b/Blockly/html/agc/index.html @@ -6,7 +6,6 @@ Blockly AGC - @@ -18,12 +17,14 @@ + + @@ -71,209 +72,55 @@

Blockly AGC