Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/blocks/mrc_jump_to_step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,21 @@ const JUMP_TO_STEP_BLOCK = {
*/
init: function (this: JumpToStepBlock): void {
this.appendDummyInput()
.appendField('Jump to')
.appendField(Blockly.Msg.JUMP_TO)
.appendField(createFieldNonEditableText(''), FIELD_STEP_NAME);
this.setPreviousStatement(true, null);
this.setInputsInline(true);
this.setStyle(MRC_STYLE_VARIABLES);
this.setTooltip('Jump to the specified step.');
this.setTooltip(() => {
const stepName = this.getFieldValue(FIELD_STEP_NAME);
let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP;
tooltip = tooltip.replace('{{stepName}}', stepName);
return tooltip;
});
},
/**
* mrcOnMove is called when an EventBlock is moved.
*/
* mrcOnMove is called when a JumpToStepBlock is moved.
*/
mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void {
this.checkBlockPlacement();
},
Expand All @@ -68,14 +73,14 @@ const JUMP_TO_STEP_BLOCK = {

const rootBlock: Blockly.Block | null = this.getRootBlock();
if (rootBlock.type === MRC_STEPS) {
// This block is within a class method definition.
// This block is within a steps block.
const stepsBlock = rootBlock as StepsBlock;
// Add the method's parameter names to legalStepNames.
// Add the step names to legalStepNames.
legalStepNames.push(...stepsBlock.mrcGetStepNames());
}

if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) {
// If this blocks's parameter name is in legalParameterNames, it's good.
// If this blocks's step name is in legalStepNames, it's good.
this.setWarningText(null, WARNING_ID_NOT_IN_STEP);
this.mrcHasWarning = false;
} else {
Expand Down Expand Up @@ -103,3 +108,13 @@ export const pythonFromBlock = function (

return code;
};

export function renameSteps(workspace: Blockly.Workspace, mapOldStepNameToNewStepName: {[newStepName: string]: string}): void {
workspace.getBlocksByType(BLOCK_NAME, false).forEach((jumpBlock) => {
const stepName = jumpBlock.getFieldValue(FIELD_STEP_NAME);
if (stepName in mapOldStepNameToNewStepName) {
const newStepName = mapOldStepNameToNewStepName[stepName];
jumpBlock.setFieldValue(newStepName, FIELD_STEP_NAME);
}
});
}
9 changes: 6 additions & 3 deletions src/blocks/mrc_step_container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const FIELD_NAME = 'NAME';
export type StepItemBlock = StepItemMixin & Blockly.BlockSvg;
interface StepItemMixin extends StepItemMixinType {
originalName: string,
conditionShadowState?: any;
conditionTargetConnection?: Blockly.Connection | null;
statementTargetConnection?: Blockly.Connection | null;
}

type StepItemMixinType = typeof STEP_ITEM;
Expand Down Expand Up @@ -174,7 +177,7 @@ function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abs
}

/**
* Called for mrc_event and mrc_class_method_def blocks when their mutator opesn.
* Called for mrc_steps blocks when their mutator opesn.
* Triggers a flyout update and adds an event listener to the mutator workspace.
*
* @param block The block whose mutator is open.
Expand All @@ -193,9 +196,9 @@ export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIc
return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block);
}

export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg {
export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): StepContainerBlock {
// First create the container block.
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg;
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as StepContainerBlock;
containerBlock.initSvg();

// Then add one step item block for each step.
Expand Down
230 changes: 92 additions & 138 deletions src/blocks/mrc_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ import { Order } from 'blockly/python';
import { MRC_STYLE_STEPS } from '../themes/styles';
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
import { createStepFieldFlydown } from '../fields/field_flydown';
import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step';
import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step';
import * as stepContainer from './mrc_step_container'
import * as value from './utils/value';
import { createBooleanShadowValue } from './utils/value';
import * as toolboxItems from '../toolbox/items';

export const BLOCK_NAME = 'mrc_steps';

const INPUT_CONDITION_PREFIX = 'CONDITION_';
const INPUT_STEP_PREFIX = 'STEP_';
const INPUT_STATEMENT_PREFIX = 'STATEMENT_';

/** Extra state for serialising mrc_steps blocks. */
type StepsExtraState = {
/**
* The steps
* The step names.
*/
stepNames: string[],
};
Expand All @@ -60,7 +60,6 @@ const STEPS = {
this.setInputsInline(false);
this.setStyle(MRC_STYLE_STEPS);
this.setMutator(stepContainer.getMutatorIcon(this));
this.updateShape_();
},
saveExtraState: function (this: StepsBlock): StepsExtraState {
return {
Expand All @@ -71,65 +70,88 @@ const STEPS = {
this.mrcStepNames = state.stepNames;
this.updateShape_();
},
compose: function (this: StepsBlock, containerBlock: Blockly.Block) {
if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) {
throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME);
/**
* Populate the mutator's dialog with this block's components.
*/
decompose: function (this: StepsBlock, workspace: Blockly.Workspace): stepContainer.StepContainerBlock {
const stepNames: string[] = [];
this.mrcStepNames.forEach(step => {
stepNames.push(step);
});
return stepContainer.createMutatorBlocks(workspace, stepNames);
},
/**
* Store condition and statement connections on the StepItemBlocks
*/
saveConnections: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) {
const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks();
for (let i = 0; i < stepItemBlocks.length; i++) {
const stepItemBlock = stepItemBlocks[i];
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
stepItemBlock.conditionShadowState =
conditionInput && conditionInput.connection!.getShadowState(true);
stepItemBlock.conditionTargetConnection =
conditionInput && conditionInput.connection!.targetConnection;
const statementInput = this.getInput(INPUT_STATEMENT_PREFIX + i);
stepItemBlock.statementTargetConnection =
statementInput && statementInput.connection!.targetConnection;
}
const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock;
const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks();

},
/**
* Reconfigure this block based on the mutator dialog's components.
*/
compose: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) {
const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {};
const conditionShadowStates: Array<any> = [];
const conditionTargetConnections: Array<Blockly.Connection | null> = [];
const statementTargetConnections: Array<Blockly.Connection | null> = [];

const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks();

// Iterate through the step item blocks to:
// - Update this.mrcStepNames
// - Keep track of steps that were renamed
// - Collect the condition and statement connections that were saved on the StepItemBlocks.
this.mrcStepNames = [];
stepItemBlocks.forEach((stepItemBlock) => {
this.mrcStepNames.push(stepItemBlock.getName());
});

// Update jump blocks for any renamed steps
const workspace = this.workspace;
const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false);
stepItemBlocks.forEach((stepItemBlock) => {
const oldName = stepItemBlock.getOriginalName();
const newName = stepItemBlock.getName();
if (oldName && oldName !== newName) {
jumpBlocks.forEach((jumpBlock) => {
if (jumpBlock.getFieldValue('STEP_NAME') === oldName) {
jumpBlock.setFieldValue(newName, 'STEP_NAME');
}
});
const oldStepName = stepItemBlock.getOriginalName();
const newStepName = stepItemBlock.getName();
stepItemBlock.setOriginalName(newStepName);
this.mrcStepNames.push(newStepName);
if (oldStepName !== newStepName) {
mapOldStepNameToNewStepName[oldStepName] = newStepName;
}
conditionShadowStates.push(stepItemBlock.conditionShadowState);
conditionTargetConnections.push(stepItemBlock.conditionTargetConnection as Blockly.Connection | null);
statementTargetConnections.push(stepItemBlock.statementTargetConnection as Blockly.Connection | null);
});

this.updateShape_();

// Add a shadow True block to each empty condition input.
for (var i = 0; i < this.mrcStepNames.length; i++) {
// Reconnect blocks.
for (let i = 0; i < this.mrcStepNames.length; i++) {
// Reconnect the condition.
conditionTargetConnections[i]?.reconnect(this, INPUT_CONDITION_PREFIX + i);
// Add the boolean shadow block to the condition input. This must be done after the condition
// has been reconnected. If it is done before the condition is reconnected, the shadow will
// become disconnected.
const conditionShadowState = conditionShadowStates[i] || createBooleanShadowValue(true).shadow;
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
if (conditionInput && !conditionInput.connection?.targetConnection) {
const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg;
shadowBlock.setShadow(true);
shadowBlock.setFieldValue('TRUE', 'BOOL');
if (this.workspace.rendered) {
shadowBlock.initSvg();
shadowBlock.render();
}
conditionInput.connection?.connect(shadowBlock.outputConnection!);
}
conditionInput?.connection?.setShadowState(conditionShadowState as any);
// Reconnect the statement.
statementTargetConnections[i]?.reconnect(this, INPUT_STATEMENT_PREFIX + i);
}

if (Object.keys(mapOldStepNameToNewStepName).length) {
// Update jump blocks for any renamed steps.
updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName);
}
},
decompose: function (this: StepsBlock, workspace: Blockly.Workspace) {
const stepNames: string[] = [];
this.mrcStepNames.forEach(step => {
stepNames.push(step);
});
return stepContainer.createMutatorBlocks(workspace, stepNames);
},
/**
* mrcOnMutatorOpen is called when the mutator on an EventBlock is opened.
* mrcOnMutatorOpen is called when the mutator on an StepsBlock is opened.
*/
mrcOnMutatorOpen: function (this: StepsBlock): void {
stepContainer.onMutatorOpen(this);
},
mrcOnChange: function (this: StepsBlock): void {

},
mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string {
const oldName = this.mrcStepNames[step];
Expand All @@ -152,99 +174,31 @@ const STEPS = {
}
this.mrcStepNames[step] = currentName;

// Update all mrc_jump_to_step blocks that reference the old name
if (oldName !== currentName) {
const workspace = this.workspace;
const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false);
jumpBlocks.forEach((jumpBlock) => {
if (jumpBlock.getFieldValue('STEP_NAME') === oldName) {
jumpBlock.setFieldValue(currentName, 'STEP_NAME');
}
});
// Update all mrc_jump_to_step blocks that reference the old name
const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {};
mapOldStepNameToNewStepName[oldName] = currentName;
updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName);
}

return currentName;
},
updateShape_: function (this: StepsBlock): void {
// Build a map of step names to their current input indices
const currentStepMap: { [stepName: string]: number } = {};
let i = 0;
while (this.getInput(INPUT_CONDITION_PREFIX + i)) {
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
const field = conditionInput?.fieldRow[0];
if (field) {
currentStepMap[field.getValue()] = i;
}
i++;
}

// For each new step position, find where it currently is (if it exists)
for (let j = 0; j < this.mrcStepNames.length; j++) {
const stepName = this.mrcStepNames[j];
const currentIndex = currentStepMap[stepName];

if (currentIndex !== undefined && currentIndex !== j) {
// Step exists but is at wrong position - move it
const conditionConnection = this.getInput(INPUT_CONDITION_PREFIX + currentIndex)?.connection?.targetConnection;
const stepConnection = this.getInput(INPUT_STEP_PREFIX + currentIndex)?.connection?.targetConnection;

// Temporarily disconnect
if (conditionConnection) {
conditionConnection.disconnect();
}
if (stepConnection) {
stepConnection.disconnect();
}

// Remove old inputs
this.removeInput(INPUT_CONDITION_PREFIX + currentIndex, false);
this.removeInput(INPUT_STEP_PREFIX + currentIndex, false);

// Create new inputs at correct position
const fieldFlydown = createStepFieldFlydown(stepName, true);
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j));

this.appendValueInput(INPUT_CONDITION_PREFIX + j)
.appendField(fieldFlydown)
.setCheck('Boolean')
.appendField(Blockly.Msg.REPEAT_UNTIL);
this.appendStatementInput(INPUT_STEP_PREFIX + j);

// Reconnect
if (conditionConnection) {
this.getInput(INPUT_CONDITION_PREFIX + j)?.connection?.connect(conditionConnection);
}
if (stepConnection) {
this.getInput(INPUT_STEP_PREFIX + j)?.connection?.connect(stepConnection);
}

delete currentStepMap[stepName];
} else if (currentIndex !== undefined) {
// Step is at correct position - just update the field
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + j);
const field = conditionInput?.fieldRow[0];
if (field && field.getValue() !== stepName) {
field.setValue(stepName);
}
delete currentStepMap[stepName];
} else {
// Step doesn't exist - create it
const fieldFlydown = createStepFieldFlydown(stepName, true);
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j));

this.appendValueInput(INPUT_CONDITION_PREFIX + j)
.appendField(fieldFlydown)
.setCheck('Boolean')
.appendField(Blockly.Msg.REPEAT_UNTIL);
this.appendStatementInput(INPUT_STEP_PREFIX + j);
}
// Remove all inputs.
for (let i = 0; this.getInput(INPUT_CONDITION_PREFIX + i); i++) {
this.removeInput(INPUT_CONDITION_PREFIX + i);
this.removeInput(INPUT_STATEMENT_PREFIX + i);
}

// Remove any leftover inputs (steps that were deleted)
for (const stepName in currentStepMap) {
const index = currentStepMap[stepName];
this.removeInput(INPUT_CONDITION_PREFIX + index, false);
this.removeInput(INPUT_STEP_PREFIX + index, false);
// Add inputs for each step.
for (let i = 0; i < this.mrcStepNames.length; i++) {
const stepName = this.mrcStepNames[i];
const fieldFlydown = createStepFieldFlydown(stepName, true);
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, i));
this.appendValueInput(INPUT_CONDITION_PREFIX + i)
.appendField(fieldFlydown)
.setCheck('Boolean')
.appendField(Blockly.Msg.REPEAT_UNTIL);
this.appendStatementInput(INPUT_STATEMENT_PREFIX + i);
}
},
mrcGetStepNames: function (this: StepsBlock): string[] {
Expand Down Expand Up @@ -276,11 +230,11 @@ export const pythonFromBlock = function (
code += generator.INDENT + 'match self._current_step:\n';
block.mrcStepNames.forEach((stepName, index) => {
code += generator.INDENT.repeat(2) + `case "${stepName}":\n`;
let stepCode = generator.statementToCode(block, INPUT_STEP_PREFIX + index);
const stepCode = generator.statementToCode(block, INPUT_STATEMENT_PREFIX + index);
if (stepCode !== '') {
code += generator.prefixLines(stepCode, generator.INDENT.repeat(2));
}
let conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False';
const conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False';
code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n';
if (index === block.mrcStepNames.length - 1) {
code += generator.INDENT.repeat(4) + 'self._current_step = None\n';
Expand All @@ -300,6 +254,6 @@ export function createStepsBlock(): toolboxItems.Block {
};
const fields: {[key: string]: any} = {};
const inputs: {[key: string]: any} = {};
inputs[INPUT_CONDITION_PREFIX + 0] = value.createBooleanShadowValue(true);
inputs[INPUT_CONDITION_PREFIX + 0] = createBooleanShadowValue(true);
return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs);
}
Loading