Skip to content

Commit d69cb2e

Browse files
committed
Add ability to create event handler blocks
1 parent c4dc25b commit d69cb2e

11 files changed

+430
-9
lines changed

src/blocks/mrc_call_python_function.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ export const pythonFromBlock = function(
612612
const functionName = callPythonFunctionBlock.mrcActualFunctionName
613613
? callPythonFunctionBlock.mrcActualFunctionName
614614
: block.getFieldValue(FIELD_FUNCTION_NAME);
615-
code = 'self.robot.' + componentName + '.' + functionName;
615+
code = 'self.' + componentName + '.' + functionName;
616616
break;
617617
}
618618
default:

src/blocks/mrc_class_method_def.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const CLASS_METHOD_DEF = {
9191
this.setStyle(MRC_STYLE_CLASS_BLOCKS);
9292
this.appendStatementInput('STACK').appendField('');
9393
this.mrcParameters = [];
94+
this.setPreviousStatement(false);
95+
this.setNextStatement(false);
9496
},
9597
/**
9698
* Returns the state of this block as a JSON serializable object.
@@ -353,12 +355,12 @@ export const pythonFromBlock = function (
353355
}
354356
if (block.mrcPythonMethodName == '__init__') {
355357
let class_specific = generator.getClassSpecificForInit();
356-
branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' +
358+
branch = generator.INDENT + 'super.__init__(' + class_specific + ')\n' +
357359
generator.defineClassVariables() + branch;
358360
}
359361
else if (funcName == 'update'){
360362
// Special case for update, to also call the update method of the base class
361-
branch = generator.INDENT + 'self.update()\n' + branch;
363+
branch = generator.INDENT + 'super.update()\n' + branch;
362364
}
363365
if (returnValue) {
364366
returnValue = generator.INDENT + 'return ' + returnValue + '\n';

src/blocks/mrc_event_handler.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Porpoiseful LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @fileoverview Blocks for event handlers
20+
* @author [email protected] (Alan Smith)
21+
*/
22+
import * as Blockly from 'blockly';
23+
import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles';
24+
import { createFieldNonEditableText } from '../fields/FieldNonEditableText'
25+
import { createFieldFlydown } from '../fields/field_flydown';
26+
import { Order } from 'blockly/python';
27+
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
28+
29+
export const BLOCK_NAME = 'mrc_event_handler';
30+
31+
export enum SenderType {
32+
ROBOT = 'robot',
33+
MECHANISM = 'mechanism',
34+
COMPONENT = 'component'
35+
}
36+
37+
export type Parameter = {
38+
name: string,
39+
type?: string,
40+
};
41+
42+
export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg;
43+
interface EventHandlerMixin extends EventHandlerMixinType {
44+
mrcPathOfSender: string,
45+
mrcTypeOfSender: SenderType,
46+
mrcParameters: Parameter[],
47+
}
48+
type EventHandlerMixinType = typeof EVENT_HANDLER;
49+
50+
51+
/** Extra state for serialising call_python_* blocks. */
52+
export type EventHandlerExtraState = {
53+
pathOfSender: string,
54+
typeOfSender: SenderType,
55+
56+
/** The parameters of the event handler. */
57+
params: Parameter[],
58+
};
59+
60+
const EVENT_HANDLER = {
61+
/**
62+
* Block initialization.
63+
*/
64+
init: function (this: EventHandlerBlock): void {
65+
this.appendDummyInput("TITLE")
66+
.appendField('On')
67+
.appendField(createFieldNonEditableText('sender'), 'SENDER')
68+
.appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME');
69+
this.appendDummyInput('PARAMS')
70+
.appendField('with')
71+
this.setOutput(false);
72+
this.setStyle(MRC_STYLE_EVENT_HANDLER);
73+
this.appendStatementInput('STACK').appendField('');
74+
this.mrcParameters = [];
75+
this.setPreviousStatement(false);
76+
this.setNextStatement(false);
77+
},
78+
/**
79+
* Returns the state of this block as a JSON serializable object.
80+
*/
81+
saveExtraState: function (
82+
this: EventHandlerBlock): EventHandlerExtraState {
83+
const extraState: EventHandlerExtraState = {
84+
pathOfSender: this.mrcPathOfSender,
85+
typeOfSender: this.mrcTypeOfSender,
86+
params: [],
87+
};
88+
this.mrcParameters.forEach((param) => {
89+
extraState.params.push({
90+
'name': param.name,
91+
'type': param.type,
92+
});
93+
});
94+
95+
return extraState;
96+
},
97+
/**
98+
* Applies the given state to this block.
99+
*/
100+
loadExtraState: function (
101+
this: EventHandlerBlock,
102+
extraState: EventHandlerExtraState
103+
): void {
104+
this.mrcParameters = [];
105+
this.mrcPathOfSender = extraState.pathOfSender;
106+
this.mrcTypeOfSender = extraState.typeOfSender;
107+
108+
extraState.params.forEach((param) => {
109+
this.mrcParameters.push({
110+
'name': param.name,
111+
'type': param.type,
112+
});
113+
});
114+
this.mrcUpdateParams();
115+
},
116+
/**
117+
* Update the block to reflect the newly loaded extra state.
118+
*/
119+
mrcUpdateParams: function (this: EventHandlerBlock) {
120+
if (this.mrcParameters.length > 0) {
121+
let input = this.getInput('PARAMS');
122+
if (input) {
123+
this.removeParameterFields(input);
124+
this.mrcParameters.forEach((param) => {
125+
const paramName = 'PARAM_' + param.name;
126+
input.appendField(createFieldFlydown(param.name, false), paramName);
127+
});
128+
}
129+
}else{
130+
this.removeInput('PARAMS', true);
131+
}
132+
},
133+
removeParameterFields: function (input: Blockly.Input) {
134+
const fieldsToRemove = input.fieldRow
135+
.filter(field => field.name?.startsWith('PARAM_'))
136+
.map(field => field.name!);
137+
138+
fieldsToRemove.forEach(fieldName => {
139+
input.removeField(fieldName);
140+
});
141+
},
142+
143+
};
144+
145+
146+
export const setup = function () {
147+
Blockly.Blocks[BLOCK_NAME] = EVENT_HANDLER;
148+
};
149+
150+
export const pythonFromBlock = function (
151+
block: EventHandlerBlock,
152+
generator: ExtendedPythonGenerator,
153+
) {
154+
const blocklyName = block.getFieldValue('SENDER') + '_' + block.getFieldValue('EVENT_NAME');
155+
156+
const funcName = generator.getProcedureName(blocklyName);
157+
158+
let xfix1 = '';
159+
if (generator.STATEMENT_PREFIX) {
160+
xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block);
161+
}
162+
if (generator.STATEMENT_SUFFIX) {
163+
xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block);
164+
}
165+
if (xfix1) {
166+
xfix1 = generator.prefixLines(xfix1, generator.INDENT);
167+
}
168+
let loopTrap = '';
169+
if (generator.INFINITE_LOOP_TRAP) {
170+
loopTrap = generator.prefixLines(
171+
generator.injectId(generator.INFINITE_LOOP_TRAP, block),
172+
generator.INDENT,
173+
);
174+
}
175+
let branch = '';
176+
if (block.getInput('STACK')) {
177+
branch = generator.statementToCode(block, 'STACK');
178+
}
179+
let returnValue = '';
180+
if (block.getInput('RETURN')) {
181+
returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || '';
182+
}
183+
let xfix2 = '';
184+
if (branch && returnValue) {
185+
// After executing the function body, revisit this block for the return.
186+
xfix2 = xfix1;
187+
}
188+
189+
if (returnValue) {
190+
returnValue = generator.INDENT + 'return ' + returnValue + '\n';
191+
} else if (!branch) {
192+
branch = generator.PASS;
193+
}
194+
195+
let params = block.mrcParameters;
196+
let paramString = "self";
197+
198+
if (params.length != 0) {
199+
block.mrcParameters.forEach((param) => {
200+
paramString += ', ' + param.name;
201+
});
202+
}
203+
204+
let code = 'def ' +
205+
funcName +
206+
'(' +
207+
paramString +
208+
'):\n';
209+
210+
code +=
211+
xfix1 +
212+
loopTrap +
213+
branch +
214+
xfix2 +
215+
returnValue;
216+
code = generator.scrub_(block, code);
217+
generator.addClassMethodDefinition(funcName, code);
218+
generator.addEventHandler(
219+
block.getFieldValue('SENDER'),
220+
block.getFieldValue('EVENT_NAME'),
221+
funcName);
222+
223+
return ''
224+
}

src/blocks/mrc_get_parameter.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {ExtendedPythonGenerator} from '../editor/extended_python_generator';
2727
import {createFieldNonEditableText} from '../fields/FieldNonEditableText';
2828
import {MRC_STYLE_VARIABLES} from '../themes/styles';
2929
import {BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def';
30+
import {BLOCK_NAME as MRC_EVENT_HANDLER, EventHandlerBlock} from './mrc_event_handler';
3031
import * as ChangeFramework from './utils/change_framework';
3132

3233

@@ -51,7 +52,7 @@ const GET_PARAMETER_BLOCK = {
5152
.appendField('parameter')
5253
.appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME');
5354

54-
this.setOutput(true, [OUTPUT_NAME, this.parameterType]);
55+
this.setOutput(true, this.parameterType);
5556
ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged);
5657
},
5758
setNameAndType: function(this: GetParameterBlock, name: string, type: string): void {
@@ -64,9 +65,8 @@ const GET_PARAMETER_BLOCK = {
6465
const blockBlock = block as Blockly.Block;
6566

6667
if (blockEvent.type === Blockly.Events.BLOCK_MOVE) {
67-
const parent = ChangeFramework.getParentOfType(block, MRC_CLASS_METHOD_DEF);
68-
69-
if (parent) {
68+
let parent = blockBlock.getRootBlock();
69+
if( parent.type === MRC_CLASS_METHOD_DEF) {
7070
// It is a class method definition, so we see if this variable is in it.
7171
const classMethodDefBlock = parent as ClassMethodDefBlock;
7272
for (const parameter of classMethodDefBlock.mrcParameters) {
@@ -77,6 +77,16 @@ const GET_PARAMETER_BLOCK = {
7777
}
7878
}
7979
}
80+
else if (parent.type === MRC_EVENT_HANDLER) {
81+
const classMethodDefBlock = parent as ClassMethodDefBlock;
82+
for (const parameter of classMethodDefBlock.mrcParameters) {
83+
if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) {
84+
// If it is, we allow it to stay.
85+
blockBlock.setWarningText(null);
86+
return;
87+
}
88+
}
89+
}
8090
// If we end up here it shouldn't be allowed
8191
block.unplug(true);
8292
blockBlock.setWarningText('Parameters can only go in their method\'s block.');

src/blocks/setup_custom_blocks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as OpModeDetails from './mrc_opmode_details';
1616
import * as Event from './mrc_event';
1717
import * as GetParameter from './mrc_get_parameter';
1818
import * as ParameterMutator from './mrc_param_container'
19+
import * as EventHandler from './mrc_event_handler';
1920

2021
const customBlocks = [
2122
CallPythonFunction,
@@ -34,7 +35,8 @@ const customBlocks = [
3435
OpModeDetails,
3536
Event,
3637
GetParameter,
37-
ParameterMutator
38+
ParameterMutator,
39+
EventHandler,
3840
];
3941

4042
export const setup = function(forBlock: any) {

src/editor/editor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import * as toolboxRobot from '../toolbox/toolbox_robot';
3131
//import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests';
3232
import { MethodsCategory} from '../toolbox/methods_category';
3333
import { EventsCategory} from '../toolbox/event_category';
34+
import { ComponentsCategory } from '../toolbox/components_category';
3435

3536

3637
const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
@@ -46,6 +47,7 @@ export class Editor {
4647
private storage: commonStorage.Storage;
4748
private methodsCategory: MethodsCategory;
4849
private eventsCategory: EventsCategory;
50+
private componentsCategory: ComponentsCategory;
4951
private currentModule: commonStorage.Module | null = null;
5052
private modulePath: string = '';
5153
private projectPath: string = '';
@@ -61,6 +63,7 @@ export class Editor {
6163
this.storage = storage;
6264
this.methodsCategory = new MethodsCategory(blocklyWorkspace);
6365
this.eventsCategory = new EventsCategory(blocklyWorkspace);
66+
this.componentsCategory = new ComponentsCategory(blocklyWorkspace);
6467
}
6568

6669
private onChangeWhileLoading(event: Blockly.Events.Abstract) {
@@ -124,6 +127,8 @@ export class Editor {
124127
this.currentModule = currentModule;
125128
this.methodsCategory.setCurrentModule(currentModule);
126129
this.eventsCategory.setCurrentModule(currentModule);
130+
this.componentsCategory.setCurrentModule(currentModule);
131+
127132
if (currentModule) {
128133
this.modulePath = currentModule.modulePath;
129134
this.projectPath = commonStorage.makeProjectPath(currentModule.projectName);

0 commit comments

Comments
 (0)