Skip to content

Commit 80a6d85

Browse files
authored
refactor!: Use JSON instead of XML for defining dynamic toolbox categories. (#8658)
* refactor!: Use JSON instead of XML for defining dynamic toolbox categories. * chore: Fix tests. * chore: Remove unused import. * chore: Update docstrings. * chore: Revert removal of XML-based category functions. * chore: Add deprecation notices.
1 parent 4dcffa0 commit 80a6d85

6 files changed

Lines changed: 356 additions & 77 deletions

File tree

core/procedures.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {IProcedureModel} from './interfaces/i_procedure_model.js';
4242
import {Msg} from './msg.js';
4343
import {Names} from './names.js';
4444
import {ObservableProcedureMap} from './observable_procedure_map.js';
45+
import * as deprecation from './utils/deprecation.js';
46+
import type {FlyoutItemInfo} from './utils/toolbox.js';
4547
import * as utilsXml from './utils/xml.js';
4648
import * as Variables from './variables.js';
4749
import type {Workspace} from './workspace.js';
@@ -238,7 +240,7 @@ export function rename(this: Field, name: string): string {
238240
* @param workspace The workspace containing procedures.
239241
* @returns Array of XML block elements.
240242
*/
241-
export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
243+
function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] {
242244
const xmlList = [];
243245
if (Blocks['procedures_defnoreturn']) {
244246
// <block type="procedures_defnoreturn" gap="16">
@@ -322,6 +324,109 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
322324
return xmlList;
323325
}
324326

327+
/**
328+
* Internal wrapper that returns the contents of the procedure category.
329+
*
330+
* @internal
331+
* @param workspace The workspace to populate procedure blocks for.
332+
*/
333+
export function internalFlyoutCategory(
334+
workspace: WorkspaceSvg,
335+
): FlyoutItemInfo[] {
336+
return flyoutCategory(workspace, false);
337+
}
338+
339+
export function flyoutCategory(
340+
workspace: WorkspaceSvg,
341+
useXml: true,
342+
): Element[];
343+
export function flyoutCategory(
344+
workspace: WorkspaceSvg,
345+
useXml: false,
346+
): FlyoutItemInfo[];
347+
/**
348+
* Construct the blocks required by the flyout for the procedure category.
349+
*
350+
* @param workspace The workspace containing procedures.
351+
* @param useXml True to return the contents as XML, false to use JSON.
352+
* @returns List of flyout contents as either XML or JSON.
353+
*/
354+
export function flyoutCategory(
355+
workspace: WorkspaceSvg,
356+
useXml = true,
357+
): Element[] | FlyoutItemInfo[] {
358+
if (useXml) {
359+
deprecation.warn(
360+
'The XML return value of Blockly.Procedures.flyoutCategory()',
361+
'v12',
362+
'v13',
363+
'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.',
364+
);
365+
return xmlFlyoutCategory(workspace);
366+
}
367+
const blocks = [];
368+
if (Blocks['procedures_defnoreturn']) {
369+
blocks.push({
370+
'kind': 'block',
371+
'type': 'procedures_defnoreturn',
372+
'gap': 16,
373+
'fields': {
374+
'NAME': Msg['PROCEDURES_DEFNORETURN_PROCEDURE'],
375+
},
376+
});
377+
}
378+
if (Blocks['procedures_defreturn']) {
379+
blocks.push({
380+
'kind': 'block',
381+
'type': 'procedures_defreturn',
382+
'gap': 16,
383+
'fields': {
384+
'NAME': Msg['PROCEDURES_DEFRETURN_PROCEDURE'],
385+
},
386+
});
387+
}
388+
if (Blocks['procedures_ifreturn']) {
389+
blocks.push({
390+
'kind': 'block',
391+
'type': 'procedures_ifreturn',
392+
'gap': 16,
393+
});
394+
}
395+
if (blocks.length) {
396+
// Add slightly larger gap between system blocks and user calls.
397+
blocks[blocks.length - 1]['gap'] = 24;
398+
}
399+
400+
/**
401+
* Creates JSON block definitions for each of the given procedures.
402+
*
403+
* @param procedureList A list of procedures, each of which is defined by a
404+
* three-element list of name, parameter list, and return value boolean.
405+
* @param templateName The type of the block to generate.
406+
*/
407+
function populateProcedures(
408+
procedureList: ProcedureTuple[],
409+
templateName: string,
410+
) {
411+
for (const [name, args] of procedureList) {
412+
blocks.push({
413+
'kind': 'block',
414+
'type': templateName,
415+
'gap': 16,
416+
'extraState': {
417+
'name': name,
418+
'params': args,
419+
},
420+
});
421+
}
422+
}
423+
424+
const tuple = allProcedures(workspace);
425+
populateProcedures(tuple[0], 'procedures_callnoreturn');
426+
populateProcedures(tuple[1], 'procedures_callreturn');
427+
return blocks;
428+
}
429+
325430
/**
326431
* Updates the procedure mutator's flyout so that the arg block is not a
327432
* duplicate of another arg.

core/variables.ts

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.
1313
import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js';
1414
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
1515
import {Msg} from './msg.js';
16+
import * as deprecation from './utils/deprecation.js';
17+
import type {BlockInfo, FlyoutItemInfo} from './utils/toolbox.js';
1618
import * as utilsXml from './utils/xml.js';
1719
import type {Workspace} from './workspace.js';
1820
import type {WorkspaceSvg} from './workspace_svg.js';
@@ -84,19 +86,165 @@ export function allDeveloperVariables(workspace: Workspace): string[] {
8486
return Array.from(variables.values());
8587
}
8688

89+
/**
90+
* Internal wrapper that returns the contents of the variables category.
91+
*
92+
* @internal
93+
* @param workspace The workspace to populate variable blocks for.
94+
*/
95+
export function internalFlyoutCategory(
96+
workspace: WorkspaceSvg,
97+
): FlyoutItemInfo[] {
98+
return flyoutCategory(workspace, false);
99+
}
100+
101+
export function flyoutCategory(
102+
workspace: WorkspaceSvg,
103+
useXml: true,
104+
): Element[];
105+
export function flyoutCategory(
106+
workspace: WorkspaceSvg,
107+
useXml: false,
108+
): FlyoutItemInfo[];
87109
/**
88110
* Construct the elements (blocks and button) required by the flyout for the
89111
* variable category.
90112
*
91113
* @param workspace The workspace containing variables.
92-
* @returns Array of XML elements.
114+
* @param useXml True to return the contents as XML, false to use JSON.
115+
* @returns List of flyout contents as either XML or JSON.
93116
*/
94-
export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
117+
export function flyoutCategory(
118+
workspace: WorkspaceSvg,
119+
useXml = true,
120+
): Element[] | FlyoutItemInfo[] {
95121
if (!Blocks['variables_set'] && !Blocks['variables_get']) {
96122
console.warn(
97123
'There are no variable blocks, but there is a variable category.',
98124
);
99125
}
126+
127+
if (useXml) {
128+
deprecation.warn(
129+
'The XML return value of Blockly.Variables.flyoutCategory()',
130+
'v12',
131+
'v13',
132+
'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.',
133+
);
134+
return xmlFlyoutCategory(workspace);
135+
}
136+
137+
workspace.registerButtonCallback('CREATE_VARIABLE', function (button) {
138+
createVariableButtonHandler(button.getTargetWorkspace());
139+
});
140+
141+
return [
142+
{
143+
'kind': 'button',
144+
'text': '%{BKY_NEW_VARIABLE}',
145+
'callbackkey': 'CREATE_VARIABLE',
146+
},
147+
...jsonFlyoutCategoryBlocks(
148+
workspace,
149+
workspace.getVariablesOfType(''),
150+
true,
151+
),
152+
];
153+
}
154+
155+
/**
156+
* Returns the JSON definition for a variable field.
157+
*
158+
* @param variable The variable the field should reference.
159+
* @returns JSON for a variable field.
160+
*/
161+
function generateVariableFieldJson(variable: IVariableModel<IVariableState>) {
162+
return {
163+
'VAR': {
164+
'name': variable.getName(),
165+
'type': variable.getType(),
166+
},
167+
};
168+
}
169+
170+
/**
171+
* Construct the blocks required by the flyout for the variable category.
172+
*
173+
* @internal
174+
* @param workspace The workspace containing variables.
175+
* @param variables List of variables to create blocks for.
176+
* @param includeChangeBlocks True to include `change x by _` blocks.
177+
* @param getterType The type of the variable getter block to generate.
178+
* @param setterType The type of the variable setter block to generate.
179+
* @returns JSON list of blocks.
180+
*/
181+
export function jsonFlyoutCategoryBlocks(
182+
workspace: Workspace,
183+
variables: IVariableModel<IVariableState>[],
184+
includeChangeBlocks: boolean,
185+
getterType = 'variables_get',
186+
setterType = 'variables_set',
187+
): BlockInfo[] {
188+
includeChangeBlocks &&= Blocks['math_change'];
189+
190+
const blocks = [];
191+
const mostRecentVariable = variables.slice(-1)[0];
192+
if (mostRecentVariable) {
193+
// Show one setter block, with the name of the most recently created variable.
194+
if (Blocks[setterType]) {
195+
blocks.push({
196+
kind: 'block',
197+
type: setterType,
198+
gap: includeChangeBlocks ? 8 : 24,
199+
fields: generateVariableFieldJson(mostRecentVariable),
200+
});
201+
}
202+
203+
if (includeChangeBlocks) {
204+
blocks.push({
205+
'kind': 'block',
206+
'type': 'math_change',
207+
'gap': Blocks[getterType] ? 20 : 8,
208+
'fields': generateVariableFieldJson(mostRecentVariable),
209+
'inputs': {
210+
'DELTA': {
211+
'shadow': {
212+
'type': 'math_number',
213+
'fields': {
214+
'NUM': 1,
215+
},
216+
},
217+
},
218+
},
219+
});
220+
}
221+
}
222+
223+
if (Blocks[getterType]) {
224+
// Show one getter block for each variable, sorted in alphabetical order.
225+
blocks.push(
226+
...variables.sort(compareByName).map((variable) => {
227+
return {
228+
'kind': 'block',
229+
'type': getterType,
230+
'gap': 8,
231+
'fields': generateVariableFieldJson(variable),
232+
};
233+
}),
234+
);
235+
}
236+
237+
return blocks;
238+
}
239+
240+
/**
241+
* Construct the elements (blocks and button) required by the flyout for the
242+
* variable category.
243+
*
244+
* @param workspace The workspace containing variables.
245+
* @returns Array of XML elements.
246+
*/
247+
function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] {
100248
let xmlList = new Array<Element>();
101249
const button = document.createElement('button');
102250
button.setAttribute('text', '%{BKY_NEW_VARIABLE}');

0 commit comments

Comments
 (0)