Skip to content

Commit 100f43a

Browse files
authored
Add Event Handler (#126)
* Add ability to create event handler blocks * Add some more sample event handlers * Changed label to "When" to make it read cleaner * Used copilot to follow google coding guidelines * Put in comment that it is fake, and making it closer to looking real * Changed all if( to if (
1 parent 96f7ff7 commit 100f43a

11 files changed

+534
-14
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: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
23+
import * as Blockly from 'blockly';
24+
import {Order} from 'blockly/python';
25+
26+
import {ExtendedPythonGenerator} from '../editor/extended_python_generator';
27+
import {createFieldFlydown} from '../fields/field_flydown';
28+
import {createFieldNonEditableText} from '../fields/FieldNonEditableText';
29+
import {MRC_STYLE_EVENT_HANDLER} from '../themes/styles';
30+
31+
export const BLOCK_NAME = 'mrc_event_handler';
32+
33+
export enum SenderType {
34+
ROBOT = 'robot',
35+
MECHANISM = 'mechanism',
36+
COMPONENT = 'component'
37+
}
38+
39+
export interface Parameter {
40+
name: string;
41+
type?: string;
42+
}
43+
44+
export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg;
45+
46+
interface EventHandlerMixin extends EventHandlerMixinType {
47+
mrcPathOfSender: string;
48+
mrcTypeOfSender: SenderType;
49+
mrcParameters: Parameter[];
50+
}
51+
52+
type EventHandlerMixinType = typeof EVENT_HANDLER;
53+
54+
/** Extra state for serialising event handler blocks. */
55+
export interface EventHandlerExtraState {
56+
pathOfSender: string;
57+
typeOfSender: SenderType;
58+
/** The parameters of the event handler. */
59+
params: Parameter[];
60+
}
61+
62+
const EVENT_HANDLER = {
63+
/**
64+
* Block initialization.
65+
*/
66+
init(this: EventHandlerBlock): void {
67+
this.appendDummyInput('TITLE')
68+
.appendField('When')
69+
.appendField(createFieldNonEditableText('sender'), 'SENDER')
70+
.appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME');
71+
this.appendDummyInput('PARAMS')
72+
.appendField('with');
73+
this.setOutput(false);
74+
this.setStyle(MRC_STYLE_EVENT_HANDLER);
75+
this.appendStatementInput('STACK').appendField('');
76+
this.mrcParameters = [];
77+
this.setPreviousStatement(false);
78+
this.setNextStatement(false);
79+
},
80+
81+
/**
82+
* Returns the state of this block as a JSON serializable object.
83+
*/
84+
saveExtraState(this: EventHandlerBlock): EventHandlerExtraState {
85+
const extraState: EventHandlerExtraState = {
86+
pathOfSender: this.mrcPathOfSender,
87+
typeOfSender: this.mrcTypeOfSender,
88+
params: [],
89+
};
90+
91+
this.mrcParameters.forEach((param) => {
92+
extraState.params.push({
93+
name: param.name,
94+
type: param.type,
95+
});
96+
});
97+
98+
return extraState;
99+
},
100+
101+
/**
102+
* Applies the given state to this block.
103+
*/
104+
loadExtraState(this: EventHandlerBlock, extraState: EventHandlerExtraState): void {
105+
this.mrcParameters = [];
106+
this.mrcPathOfSender = extraState.pathOfSender;
107+
this.mrcTypeOfSender = extraState.typeOfSender;
108+
109+
extraState.params.forEach((param) => {
110+
this.mrcParameters.push({
111+
name: param.name,
112+
type: param.type,
113+
});
114+
});
115+
this.mrcUpdateParams();
116+
},
117+
118+
/**
119+
* Update the block to reflect the newly loaded extra state.
120+
*/
121+
mrcUpdateParams(this: EventHandlerBlock): void {
122+
if (this.mrcParameters.length > 0) {
123+
const input = this.getInput('PARAMS');
124+
if (input) {
125+
this.removeParameterFields(input);
126+
this.mrcParameters.forEach((param) => {
127+
const paramName = `PARAM_${param.name}`;
128+
input.appendField(createFieldFlydown(param.name, false), paramName);
129+
});
130+
}
131+
} else {
132+
this.removeInput('PARAMS', true);
133+
}
134+
},
135+
136+
/**
137+
* Removes parameter fields from the given input.
138+
*/
139+
removeParameterFields(input: Blockly.Input): void {
140+
const fieldsToRemove = input.fieldRow
141+
.filter(field => field.name?.startsWith('PARAM_'))
142+
.map(field => field.name!);
143+
144+
fieldsToRemove.forEach(fieldName => {
145+
input.removeField(fieldName);
146+
});
147+
},
148+
};
149+
150+
export function setup(): void {
151+
Blockly.Blocks[BLOCK_NAME] = EVENT_HANDLER;
152+
}
153+
154+
export function pythonFromBlock(
155+
block: EventHandlerBlock,
156+
generator: ExtendedPythonGenerator,
157+
): string {
158+
const blocklyName = `${block.getFieldValue('SENDER')}_${block.getFieldValue('EVENT_NAME')}`;
159+
const funcName = generator.getProcedureName(blocklyName);
160+
161+
let xfix1 = '';
162+
if (generator.STATEMENT_PREFIX) {
163+
xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block);
164+
}
165+
if (generator.STATEMENT_SUFFIX) {
166+
xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block);
167+
}
168+
if (xfix1) {
169+
xfix1 = generator.prefixLines(xfix1, generator.INDENT);
170+
}
171+
172+
let loopTrap = '';
173+
if (generator.INFINITE_LOOP_TRAP) {
174+
loopTrap = generator.prefixLines(
175+
generator.injectId(generator.INFINITE_LOOP_TRAP, block),
176+
generator.INDENT,
177+
);
178+
}
179+
180+
let branch = '';
181+
if (block.getInput('STACK')) {
182+
branch = generator.statementToCode(block, 'STACK');
183+
}
184+
185+
let returnValue = '';
186+
if (block.getInput('RETURN')) {
187+
returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || '';
188+
}
189+
190+
let xfix2 = '';
191+
if (branch && returnValue) {
192+
// After executing the function body, revisit this block for the return.
193+
xfix2 = xfix1;
194+
}
195+
196+
if (returnValue) {
197+
returnValue = `${generator.INDENT}return ${returnValue}\n`;
198+
} else if (!branch) {
199+
branch = generator.PASS;
200+
}
201+
202+
const params = block.mrcParameters;
203+
let paramString = 'self';
204+
205+
if (params.length !== 0) {
206+
block.mrcParameters.forEach((param) => {
207+
paramString += `, ${param.name}`;
208+
});
209+
}
210+
211+
let code = `def ${funcName}(${paramString}):\n`;
212+
code += xfix1 + loopTrap + branch + xfix2 + returnValue;
213+
code = generator.scrub_(block, code);
214+
215+
generator.addClassMethodDefinition(funcName, code);
216+
generator.addEventHandler(
217+
block.getFieldValue('SENDER'),
218+
block.getFieldValue('EVENT_NAME'),
219+
funcName);
220+
221+
return '';
222+
}

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)