/** * @license * Visual Blocks Editor * * Copyright 2012 Google Inc. * https://developers.google.com/blockly/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Logic blocks for Blockly. * @author q.neutron@gmail.com (Quynh Neutron) */ 'use strict'; goog.provide('Blockly.Blocks.logic'); goog.require('Blockly.Blocks'); /** * Common HSV hue for all blocks in this category. */ Blockly.Blocks.logic.HUE = 210; Blockly.Blocks['controls_if'] = { /** * Block for if/elseif/else condition. * @this Blockly.Block */ init: function() { this.setHelpUrl(Blockly.Msg.CONTROLS_IF_HELPURL); this.setColour(Blockly.Blocks.logic.HUE); this.appendValueInput('IF0') .setCheck('Boolean') .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF); this.appendStatementInput('DO0') .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); this.setPreviousStatement(true); this.setNextStatement(true); this.setMutator(new Blockly.Mutator(['controls_if_elseif', 'controls_if_else'])); // Assign 'this' to a variable for use in the tooltip closure below. var thisBlock = this; this.setTooltip(function() { if (!thisBlock.elseifCount_ && !thisBlock.elseCount_) { return Blockly.Msg.CONTROLS_IF_TOOLTIP_1; } else if (!thisBlock.elseifCount_ && thisBlock.elseCount_) { return Blockly.Msg.CONTROLS_IF_TOOLTIP_2; } else if (thisBlock.elseifCount_ && !thisBlock.elseCount_) { return Blockly.Msg.CONTROLS_IF_TOOLTIP_3; } else if (thisBlock.elseifCount_ && thisBlock.elseCount_) { return Blockly.Msg.CONTROLS_IF_TOOLTIP_4; } return ''; }); this.elseifCount_ = 0; this.elseCount_ = 0; }, /** * Create XML to represent the number of else-if and else inputs. * @return {Element} XML storage element. * @this Blockly.Block */ mutationToDom: function() { if (!this.elseifCount_ && !this.elseCount_) { return null; } var container = document.createElement('mutation'); if (this.elseifCount_) { container.setAttribute('elseif', this.elseifCount_); } if (this.elseCount_) { container.setAttribute('else', 1); } return container; }, /** * Parse XML to restore the else-if and else inputs. * @param {!Element} xmlElement XML storage element. * @this Blockly.Block */ domToMutation: function(xmlElement) { this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif'), 10) || 0; this.elseCount_ = parseInt(xmlElement.getAttribute('else'), 10) || 0; this.updateShape_(); }, /** * Populate the mutator's dialog with this block's components. * @param {!Blockly.Workspace} workspace Mutator's workspace. * @return {!Blockly.Block} Root block in mutator. * @this Blockly.Block */ decompose: function(workspace) { var containerBlock = workspace.newBlock('controls_if_if'); containerBlock.initSvg(); var connection = containerBlock.nextConnection; for (var i = 1; i <= this.elseifCount_; i++) { var elseifBlock = workspace.newBlock('controls_if_elseif'); elseifBlock.initSvg(); connection.connect(elseifBlock.previousConnection); connection = elseifBlock.nextConnection; } if (this.elseCount_) { var elseBlock = workspace.newBlock('controls_if_else'); elseBlock.initSvg(); connection.connect(elseBlock.previousConnection); } return containerBlock; }, /** * Reconfigure this block based on the mutator dialog's components. * @param {!Blockly.Block} containerBlock Root block in mutator. * @this Blockly.Block */ compose: function(containerBlock) { var clauseBlock = containerBlock.nextConnection.targetBlock(); // Count number of inputs. this.elseifCount_ = 0; this.elseCount_ = 0; var valueConnections = [null]; var statementConnections = [null]; var elseStatementConnection = null; while (clauseBlock) { switch (clauseBlock.type) { case 'controls_if_elseif': this.elseifCount_++; valueConnections.push(clauseBlock.valueConnection_); statementConnections.push(clauseBlock.statementConnection_); break; case 'controls_if_else': this.elseCount_++; elseStatementConnection = clauseBlock.statementConnection_; break; default: throw 'Unknown block type.'; } clauseBlock = clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock(); } this.updateShape_(); // Reconnect any child blocks. for (var i = 1; i <= this.elseifCount_; i++) { Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i); Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i); } Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE'); }, /** * Store pointers to any connected child blocks. * @param {!Blockly.Block} containerBlock Root block in mutator. * @this Blockly.Block */ saveConnections: function(containerBlock) { var clauseBlock = containerBlock.nextConnection.targetBlock(); var i = 1; while (clauseBlock) { switch (clauseBlock.type) { case 'controls_if_elseif': var inputIf = this.getInput('IF' + i); var inputDo = this.getInput('DO' + i); clauseBlock.valueConnection_ = inputIf && inputIf.connection.targetConnection; clauseBlock.statementConnection_ = inputDo && inputDo.connection.targetConnection; i++; break; case 'controls_if_else': var inputDo = this.getInput('ELSE'); clauseBlock.statementConnection_ = inputDo && inputDo.connection.targetConnection; break; default: throw 'Unknown block type.'; } clauseBlock = clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock(); } }, /** * Modify this block to have the correct number of inputs. * @private * @this Blockly.Block */ updateShape_: function() { // Delete everything. if (this.getInput('ELSE')) { this.removeInput('ELSE'); } var i = 1; while (this.getInput('IF' + i)) { this.removeInput('IF' + i); this.removeInput('DO' + i); i++; } // Rebuild block. for (var i = 1; i <= this.elseifCount_; i++) { this.appendValueInput('IF' + i) .setCheck('Boolean') .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSEIF); this.appendStatementInput('DO' + i) .appendField(Blockly.Msg.CONTROLS_IF_MSG_THEN); } if (this.elseCount_) { this.appendStatementInput('ELSE') .appendField(Blockly.Msg.CONTROLS_IF_MSG_ELSE); } } }; Blockly.Blocks['controls_if_if'] = { /** * Mutator block for if container. * @this Blockly.Block */ init: function() { this.setColour(Blockly.Blocks.logic.HUE); this.appendDummyInput() .appendField(Blockly.Msg.CONTROLS_IF_IF_TITLE_IF); this.setNextStatement(true); this.setTooltip(Blockly.Msg.CONTROLS_IF_IF_TOOLTIP); this.contextMenu = false; } }; Blockly.Blocks['controls_if_elseif'] = { /** * Mutator bolck for else-if condition. * @this Blockly.Block */ init: function() { this.setColour(Blockly.Blocks.logic.HUE); this.appendDummyInput() .appendField(Blockly.Msg.CONTROLS_IF_ELSEIF_TITLE_ELSEIF); this.setPreviousStatement(true); this.setNextStatement(true); this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP); this.contextMenu = false; } }; Blockly.Blocks['controls_if_else'] = { /** * Mutator block for else condition. * @this Blockly.Block */ init: function() { this.setColour(Blockly.Blocks.logic.HUE); this.appendDummyInput() .appendField(Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE); this.setPreviousStatement(true); this.setTooltip(Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP); this.contextMenu = false; } }; Blockly.Blocks['logic_compare'] = { /** * Block for comparison operator. * @this Blockly.Block */ init: function() { var rtlOperators = [ ['=', 'EQ'], ['\u2260', 'NEQ'], ['\u200F<\u200F', 'LT'], ['\u200F\u2264\u200F', 'LTE'], ['\u200F>\u200F', 'GT'], ['\u200F\u2265\u200F', 'GTE'] ]; var ltrOperators = [ ['=', 'EQ'], ['\u2260', 'NEQ'], ['<', 'LT'], ['\u2264', 'LTE'], ['>', 'GT'], ['\u2265', 'GTE'] ]; var OPERATORS = this.RTL ? rtlOperators : ltrOperators; this.setHelpUrl(Blockly.Msg.LOGIC_COMPARE_HELPURL); this.setColour(Blockly.Blocks.logic.HUE); this.setOutput(true, 'Boolean'); this.appendValueInput('A'); this.appendValueInput('B') .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP'); this.setInputsInline(true); // Assign 'this' to a variable for use in the tooltip closure below. var thisBlock = this; this.setTooltip(function() { var op = thisBlock.getFieldValue('OP'); var TOOLTIPS = { 'EQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_EQ, 'NEQ': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_NEQ, 'LT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LT, 'LTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_LTE, 'GT': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GT, 'GTE': Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GTE }; return TOOLTIPS[op]; }); this.prevBlocks_ = [null, null]; }, /** * 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) { var blockA = this.getInputTargetBlock('A'); var blockB = this.getInputTargetBlock('B'); // Disconnect blocks that existed prior to this change if they don't match. if (blockA && blockB && !blockA.outputConnection.checkType_(blockB.outputConnection)) { // Mismatch between two inputs. Disconnect previous and bump it away. // Ensure that any disconnections are grouped with the causing event. Blockly.Events.setGroup(e.group); for (var i = 0; i < this.prevBlocks_.length; i++) { var block = this.prevBlocks_[i]; if (block === blockA || block === blockB) { block.unplug(); block.bumpNeighbours_(); } } Blockly.Events.setGroup(false); } this.prevBlocks_[0] = blockA; this.prevBlocks_[1] = blockB; } }; Blockly.Blocks['logic_operation'] = { /** * Block for logical operations: 'and', 'or'. * @this Blockly.Block */ init: function() { var OPERATORS = [[Blockly.Msg.LOGIC_OPERATION_AND, 'AND'], [Blockly.Msg.LOGIC_OPERATION_OR, 'OR']]; this.setHelpUrl(Blockly.Msg.LOGIC_OPERATION_HELPURL); this.setColour(Blockly.Blocks.logic.HUE); this.setOutput(true, 'Boolean'); this.appendValueInput('A') .setCheck('Boolean'); this.appendValueInput('B') .setCheck('Boolean') .appendField(new Blockly.FieldDropdown(OPERATORS), 'OP'); this.setInputsInline(true); // Assign 'this' to a variable for use in the tooltip closure below. var thisBlock = this; this.setTooltip(function() { var op = thisBlock.getFieldValue('OP'); var TOOLTIPS = { 'AND': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_AND, 'OR': Blockly.Msg.LOGIC_OPERATION_TOOLTIP_OR }; return TOOLTIPS[op]; }); } }; Blockly.Blocks['logic_negate'] = { /** * Block for negation. * @this Blockly.Block */ init: function() { this.jsonInit({ "message0": Blockly.Msg.LOGIC_NEGATE_TITLE, "args0": [ { "type": "input_value", "name": "BOOL", "check": "Boolean" } ], "output": "Boolean", "colour": Blockly.Blocks.logic.HUE, "tooltip": Blockly.Msg.LOGIC_NEGATE_TOOLTIP, "helpUrl": Blockly.Msg.LOGIC_NEGATE_HELPURL }); } }; Blockly.Blocks['logic_boolean'] = { /** * Block for boolean data type: true and false. * @this Blockly.Block */ init: function() { this.jsonInit({ "message0": "%1", "args0": [ { "type": "field_dropdown", "name": "BOOL", "options": [ [Blockly.Msg.LOGIC_BOOLEAN_TRUE, "TRUE"], [Blockly.Msg.LOGIC_BOOLEAN_FALSE, "FALSE"] ] } ], "output": "Boolean", "colour": Blockly.Blocks.logic.HUE, "tooltip": Blockly.Msg.LOGIC_BOOLEAN_TOOLTIP, "helpUrl": Blockly.Msg.LOGIC_BOOLEAN_HELPURL }); } }; Blockly.Blocks['logic_null'] = { /** * Block for null data type. * @this Blockly.Block */ init: function() { this.jsonInit({ "message0": Blockly.Msg.LOGIC_NULL, "output": null, "colour": Blockly.Blocks.logic.HUE, "tooltip": Blockly.Msg.LOGIC_NULL_TOOLTIP, "helpUrl": Blockly.Msg.LOGIC_NULL_HELPURL }); } }; Blockly.Blocks['logic_ternary'] = { /** * Block for ternary operator. * @this Blockly.Block */ init: function() { this.setHelpUrl(Blockly.Msg.LOGIC_TERNARY_HELPURL); this.setColour(Blockly.Blocks.logic.HUE); this.appendValueInput('IF') .setCheck('Boolean') .appendField(Blockly.Msg.LOGIC_TERNARY_CONDITION); this.appendValueInput('THEN') .appendField(Blockly.Msg.LOGIC_TERNARY_IF_TRUE); this.appendValueInput('ELSE') .appendField(Blockly.Msg.LOGIC_TERNARY_IF_FALSE); this.setOutput(true); this.setTooltip(Blockly.Msg.LOGIC_TERNARY_TOOLTIP); this.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) { var blockA = this.getInputTargetBlock('THEN'); var blockB = this.getInputTargetBlock('ELSE'); var parentConnection = this.outputConnection.targetConnection; // Disconnect blocks that existed prior to this change if they don't match. if ((blockA || blockB) && parentConnection) { for (var i = 0; i < 2; i++) { var block = (i == 1) ? blockA : blockB; if (block && !block.outputConnection.checkType_(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; } };