Skip to content

Commit dcf2a24

Browse files
authored
Merge pull request #18 from lizlooney/pr_try_steps_block
Reconnect blocks if steps are reordered
2 parents 92b3ec7 + f51f3a9 commit dcf2a24

File tree

8 files changed

+134
-152
lines changed

8 files changed

+134
-152
lines changed

src/blocks/mrc_jump_to_step.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,21 @@ const JUMP_TO_STEP_BLOCK = {
4747
*/
4848
init: function (this: JumpToStepBlock): void {
4949
this.appendDummyInput()
50-
.appendField('Jump to')
50+
.appendField(Blockly.Msg.JUMP_TO)
5151
.appendField(createFieldNonEditableText(''), FIELD_STEP_NAME);
5252
this.setPreviousStatement(true, null);
5353
this.setInputsInline(true);
5454
this.setStyle(MRC_STYLE_VARIABLES);
55-
this.setTooltip('Jump to the specified step.');
55+
this.setTooltip(() => {
56+
const stepName = this.getFieldValue(FIELD_STEP_NAME);
57+
let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP;
58+
tooltip = tooltip.replace('{{stepName}}', stepName);
59+
return tooltip;
60+
});
5661
},
5762
/**
58-
* mrcOnMove is called when an EventBlock is moved.
59-
*/
63+
* mrcOnMove is called when a JumpToStepBlock is moved.
64+
*/
6065
mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void {
6166
this.checkBlockPlacement();
6267
},
@@ -68,14 +73,14 @@ const JUMP_TO_STEP_BLOCK = {
6873

6974
const rootBlock: Blockly.Block | null = this.getRootBlock();
7075
if (rootBlock.type === MRC_STEPS) {
71-
// This block is within a class method definition.
76+
// This block is within a steps block.
7277
const stepsBlock = rootBlock as StepsBlock;
73-
// Add the method's parameter names to legalStepNames.
78+
// Add the step names to legalStepNames.
7479
legalStepNames.push(...stepsBlock.mrcGetStepNames());
7580
}
7681

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

104109
return code;
105110
};
111+
112+
export function renameSteps(workspace: Blockly.Workspace, mapOldStepNameToNewStepName: {[newStepName: string]: string}): void {
113+
workspace.getBlocksByType(BLOCK_NAME, false).forEach((jumpBlock) => {
114+
const stepName = jumpBlock.getFieldValue(FIELD_STEP_NAME);
115+
if (stepName in mapOldStepNameToNewStepName) {
116+
const newStepName = mapOldStepNameToNewStepName[stepName];
117+
jumpBlock.setFieldValue(newStepName, FIELD_STEP_NAME);
118+
}
119+
});
120+
}

src/blocks/mrc_step_container.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ const FIELD_NAME = 'NAME';
6666
export type StepItemBlock = StepItemMixin & Blockly.BlockSvg;
6767
interface StepItemMixin extends StepItemMixinType {
6868
originalName: string,
69+
conditionShadowState?: any;
70+
conditionTargetConnection?: Blockly.Connection | null;
71+
statementTargetConnection?: Blockly.Connection | null;
6972
}
7073

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

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

196-
export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg {
199+
export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): StepContainerBlock {
197200
// First create the container block.
198-
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg;
201+
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as StepContainerBlock;
199202
containerBlock.initSvg();
200203

201204
// Then add one step item block for each step.

src/blocks/mrc_steps.ts

Lines changed: 92 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ import { Order } from 'blockly/python';
2525
import { MRC_STYLE_STEPS } from '../themes/styles';
2626
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2727
import { createStepFieldFlydown } from '../fields/field_flydown';
28-
import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step';
28+
import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step';
2929
import * as stepContainer from './mrc_step_container'
30-
import * as value from './utils/value';
30+
import { createBooleanShadowValue } from './utils/value';
3131
import * as toolboxItems from '../toolbox/items';
3232

3333
export const BLOCK_NAME = 'mrc_steps';
3434

3535
const INPUT_CONDITION_PREFIX = 'CONDITION_';
36-
const INPUT_STEP_PREFIX = 'STEP_';
36+
const INPUT_STATEMENT_PREFIX = 'STATEMENT_';
3737

3838
/** Extra state for serialising mrc_steps blocks. */
3939
type StepsExtraState = {
4040
/**
41-
* The steps
41+
* The step names.
4242
*/
4343
stepNames: string[],
4444
};
@@ -60,7 +60,6 @@ const STEPS = {
6060
this.setInputsInline(false);
6161
this.setStyle(MRC_STYLE_STEPS);
6262
this.setMutator(stepContainer.getMutatorIcon(this));
63-
this.updateShape_();
6463
},
6564
saveExtraState: function (this: StepsBlock): StepsExtraState {
6665
return {
@@ -71,65 +70,88 @@ const STEPS = {
7170
this.mrcStepNames = state.stepNames;
7271
this.updateShape_();
7372
},
74-
compose: function (this: StepsBlock, containerBlock: Blockly.Block) {
75-
if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) {
76-
throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME);
73+
/**
74+
* Populate the mutator's dialog with this block's components.
75+
*/
76+
decompose: function (this: StepsBlock, workspace: Blockly.Workspace): stepContainer.StepContainerBlock {
77+
const stepNames: string[] = [];
78+
this.mrcStepNames.forEach(step => {
79+
stepNames.push(step);
80+
});
81+
return stepContainer.createMutatorBlocks(workspace, stepNames);
82+
},
83+
/**
84+
* Store condition and statement connections on the StepItemBlocks
85+
*/
86+
saveConnections: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) {
87+
const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks();
88+
for (let i = 0; i < stepItemBlocks.length; i++) {
89+
const stepItemBlock = stepItemBlocks[i];
90+
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
91+
stepItemBlock.conditionShadowState =
92+
conditionInput && conditionInput.connection!.getShadowState(true);
93+
stepItemBlock.conditionTargetConnection =
94+
conditionInput && conditionInput.connection!.targetConnection;
95+
const statementInput = this.getInput(INPUT_STATEMENT_PREFIX + i);
96+
stepItemBlock.statementTargetConnection =
97+
statementInput && statementInput.connection!.targetConnection;
7798
}
78-
const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock;
79-
const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks();
80-
99+
},
100+
/**
101+
* Reconfigure this block based on the mutator dialog's components.
102+
*/
103+
compose: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) {
104+
const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {};
105+
const conditionShadowStates: Array<any> = [];
106+
const conditionTargetConnections: Array<Blockly.Connection | null> = [];
107+
const statementTargetConnections: Array<Blockly.Connection | null> = [];
108+
109+
const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks();
110+
111+
// Iterate through the step item blocks to:
112+
// - Update this.mrcStepNames
113+
// - Keep track of steps that were renamed
114+
// - Collect the condition and statement connections that were saved on the StepItemBlocks.
81115
this.mrcStepNames = [];
82116
stepItemBlocks.forEach((stepItemBlock) => {
83-
this.mrcStepNames.push(stepItemBlock.getName());
84-
});
85-
86-
// Update jump blocks for any renamed steps
87-
const workspace = this.workspace;
88-
const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false);
89-
stepItemBlocks.forEach((stepItemBlock) => {
90-
const oldName = stepItemBlock.getOriginalName();
91-
const newName = stepItemBlock.getName();
92-
if (oldName && oldName !== newName) {
93-
jumpBlocks.forEach((jumpBlock) => {
94-
if (jumpBlock.getFieldValue('STEP_NAME') === oldName) {
95-
jumpBlock.setFieldValue(newName, 'STEP_NAME');
96-
}
97-
});
117+
const oldStepName = stepItemBlock.getOriginalName();
118+
const newStepName = stepItemBlock.getName();
119+
stepItemBlock.setOriginalName(newStepName);
120+
this.mrcStepNames.push(newStepName);
121+
if (oldStepName !== newStepName) {
122+
mapOldStepNameToNewStepName[oldStepName] = newStepName;
98123
}
124+
conditionShadowStates.push(stepItemBlock.conditionShadowState);
125+
conditionTargetConnections.push(stepItemBlock.conditionTargetConnection as Blockly.Connection | null);
126+
statementTargetConnections.push(stepItemBlock.statementTargetConnection as Blockly.Connection | null);
99127
});
100128

101129
this.updateShape_();
102130

103-
// Add a shadow True block to each empty condition input.
104-
for (var i = 0; i < this.mrcStepNames.length; i++) {
131+
// Reconnect blocks.
132+
for (let i = 0; i < this.mrcStepNames.length; i++) {
133+
// Reconnect the condition.
134+
conditionTargetConnections[i]?.reconnect(this, INPUT_CONDITION_PREFIX + i);
135+
// Add the boolean shadow block to the condition input. This must be done after the condition
136+
// has been reconnected. If it is done before the condition is reconnected, the shadow will
137+
// become disconnected.
138+
const conditionShadowState = conditionShadowStates[i] || createBooleanShadowValue(true).shadow;
105139
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
106-
if (conditionInput && !conditionInput.connection?.targetConnection) {
107-
const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg;
108-
shadowBlock.setShadow(true);
109-
shadowBlock.setFieldValue('TRUE', 'BOOL');
110-
if (this.workspace.rendered) {
111-
shadowBlock.initSvg();
112-
shadowBlock.render();
113-
}
114-
conditionInput.connection?.connect(shadowBlock.outputConnection!);
115-
}
140+
conditionInput?.connection?.setShadowState(conditionShadowState as any);
141+
// Reconnect the statement.
142+
statementTargetConnections[i]?.reconnect(this, INPUT_STATEMENT_PREFIX + i);
143+
}
144+
145+
if (Object.keys(mapOldStepNameToNewStepName).length) {
146+
// Update jump blocks for any renamed steps.
147+
updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName);
116148
}
117-
},
118-
decompose: function (this: StepsBlock, workspace: Blockly.Workspace) {
119-
const stepNames: string[] = [];
120-
this.mrcStepNames.forEach(step => {
121-
stepNames.push(step);
122-
});
123-
return stepContainer.createMutatorBlocks(workspace, stepNames);
124149
},
125150
/**
126-
* mrcOnMutatorOpen is called when the mutator on an EventBlock is opened.
151+
* mrcOnMutatorOpen is called when the mutator on an StepsBlock is opened.
127152
*/
128153
mrcOnMutatorOpen: function (this: StepsBlock): void {
129154
stepContainer.onMutatorOpen(this);
130-
},
131-
mrcOnChange: function (this: StepsBlock): void {
132-
133155
},
134156
mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string {
135157
const oldName = this.mrcStepNames[step];
@@ -152,99 +174,31 @@ const STEPS = {
152174
}
153175
this.mrcStepNames[step] = currentName;
154176

155-
// Update all mrc_jump_to_step blocks that reference the old name
156177
if (oldName !== currentName) {
157-
const workspace = this.workspace;
158-
const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false);
159-
jumpBlocks.forEach((jumpBlock) => {
160-
if (jumpBlock.getFieldValue('STEP_NAME') === oldName) {
161-
jumpBlock.setFieldValue(currentName, 'STEP_NAME');
162-
}
163-
});
178+
// Update all mrc_jump_to_step blocks that reference the old name
179+
const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {};
180+
mapOldStepNameToNewStepName[oldName] = currentName;
181+
updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName);
164182
}
165183

166184
return currentName;
167185
},
168186
updateShape_: function (this: StepsBlock): void {
169-
// Build a map of step names to their current input indices
170-
const currentStepMap: { [stepName: string]: number } = {};
171-
let i = 0;
172-
while (this.getInput(INPUT_CONDITION_PREFIX + i)) {
173-
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i);
174-
const field = conditionInput?.fieldRow[0];
175-
if (field) {
176-
currentStepMap[field.getValue()] = i;
177-
}
178-
i++;
179-
}
180-
181-
// For each new step position, find where it currently is (if it exists)
182-
for (let j = 0; j < this.mrcStepNames.length; j++) {
183-
const stepName = this.mrcStepNames[j];
184-
const currentIndex = currentStepMap[stepName];
185-
186-
if (currentIndex !== undefined && currentIndex !== j) {
187-
// Step exists but is at wrong position - move it
188-
const conditionConnection = this.getInput(INPUT_CONDITION_PREFIX + currentIndex)?.connection?.targetConnection;
189-
const stepConnection = this.getInput(INPUT_STEP_PREFIX + currentIndex)?.connection?.targetConnection;
190-
191-
// Temporarily disconnect
192-
if (conditionConnection) {
193-
conditionConnection.disconnect();
194-
}
195-
if (stepConnection) {
196-
stepConnection.disconnect();
197-
}
198-
199-
// Remove old inputs
200-
this.removeInput(INPUT_CONDITION_PREFIX + currentIndex, false);
201-
this.removeInput(INPUT_STEP_PREFIX + currentIndex, false);
202-
203-
// Create new inputs at correct position
204-
const fieldFlydown = createStepFieldFlydown(stepName, true);
205-
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j));
206-
207-
this.appendValueInput(INPUT_CONDITION_PREFIX + j)
208-
.appendField(fieldFlydown)
209-
.setCheck('Boolean')
210-
.appendField(Blockly.Msg.REPEAT_UNTIL);
211-
this.appendStatementInput(INPUT_STEP_PREFIX + j);
212-
213-
// Reconnect
214-
if (conditionConnection) {
215-
this.getInput(INPUT_CONDITION_PREFIX + j)?.connection?.connect(conditionConnection);
216-
}
217-
if (stepConnection) {
218-
this.getInput(INPUT_STEP_PREFIX + j)?.connection?.connect(stepConnection);
219-
}
220-
221-
delete currentStepMap[stepName];
222-
} else if (currentIndex !== undefined) {
223-
// Step is at correct position - just update the field
224-
const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + j);
225-
const field = conditionInput?.fieldRow[0];
226-
if (field && field.getValue() !== stepName) {
227-
field.setValue(stepName);
228-
}
229-
delete currentStepMap[stepName];
230-
} else {
231-
// Step doesn't exist - create it
232-
const fieldFlydown = createStepFieldFlydown(stepName, true);
233-
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j));
234-
235-
this.appendValueInput(INPUT_CONDITION_PREFIX + j)
236-
.appendField(fieldFlydown)
237-
.setCheck('Boolean')
238-
.appendField(Blockly.Msg.REPEAT_UNTIL);
239-
this.appendStatementInput(INPUT_STEP_PREFIX + j);
240-
}
187+
// Remove all inputs.
188+
for (let i = 0; this.getInput(INPUT_CONDITION_PREFIX + i); i++) {
189+
this.removeInput(INPUT_CONDITION_PREFIX + i);
190+
this.removeInput(INPUT_STATEMENT_PREFIX + i);
241191
}
242-
243-
// Remove any leftover inputs (steps that were deleted)
244-
for (const stepName in currentStepMap) {
245-
const index = currentStepMap[stepName];
246-
this.removeInput(INPUT_CONDITION_PREFIX + index, false);
247-
this.removeInput(INPUT_STEP_PREFIX + index, false);
192+
// Add inputs for each step.
193+
for (let i = 0; i < this.mrcStepNames.length; i++) {
194+
const stepName = this.mrcStepNames[i];
195+
const fieldFlydown = createStepFieldFlydown(stepName, true);
196+
fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, i));
197+
this.appendValueInput(INPUT_CONDITION_PREFIX + i)
198+
.appendField(fieldFlydown)
199+
.setCheck('Boolean')
200+
.appendField(Blockly.Msg.REPEAT_UNTIL);
201+
this.appendStatementInput(INPUT_STATEMENT_PREFIX + i);
248202
}
249203
},
250204
mrcGetStepNames: function (this: StepsBlock): string[] {
@@ -276,11 +230,11 @@ export const pythonFromBlock = function (
276230
code += generator.INDENT + 'match self._current_step:\n';
277231
block.mrcStepNames.forEach((stepName, index) => {
278232
code += generator.INDENT.repeat(2) + `case "${stepName}":\n`;
279-
let stepCode = generator.statementToCode(block, INPUT_STEP_PREFIX + index);
233+
const stepCode = generator.statementToCode(block, INPUT_STATEMENT_PREFIX + index);
280234
if (stepCode !== '') {
281235
code += generator.prefixLines(stepCode, generator.INDENT.repeat(2));
282236
}
283-
let conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False';
237+
const conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False';
284238
code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n';
285239
if (index === block.mrcStepNames.length - 1) {
286240
code += generator.INDENT.repeat(4) + 'self._current_step = None\n';
@@ -300,6 +254,6 @@ export function createStepsBlock(): toolboxItems.Block {
300254
};
301255
const fields: {[key: string]: any} = {};
302256
const inputs: {[key: string]: any} = {};
303-
inputs[INPUT_CONDITION_PREFIX + 0] = value.createBooleanShadowValue(true);
257+
inputs[INPUT_CONDITION_PREFIX + 0] = createBooleanShadowValue(true);
304258
return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs);
305259
}

0 commit comments

Comments
 (0)