Skip to content

Commit 7dcbbbe

Browse files
authored
feat: Display action menu, containing insert action, for stack connections (#209)
* style: Use === instead of == * refactor: openActionMenu - Always call .openActionMenu() from the menu shortcut handler, regardless of the type of the current node; let openActionMenu decide if the action menu can be opened or not. - Pass workspace instead of curNode to .openActionMenu(). - Remove extraneous call to BlockSvg.prototype.showContextMenu() at the top of .openActionMenu that seems to have been left in accidentally while debugging PR #158, and that made the rest of the body of the method redundant. * feat: Make insert action compatible with stack connections Doesn't necessarily work properly with connections yet, but at least can appear in the action menu for them. * feat: Show action menu for NEXT/PREVIOUS connections
1 parent d84f46d commit 7dcbbbe

File tree

2 files changed

+146
-56
lines changed

2 files changed

+146
-56
lines changed

src/navigation.ts

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -724,22 +724,22 @@ export class Navigation {
724724
// Connect the moving block to the stationary connection using
725725
// the most plausible connection on the moving block.
726726
if (
727-
movingType == Blockly.ASTNode.types.BLOCK ||
728-
movingType == Blockly.ASTNode.types.STACK
727+
movingType === Blockly.ASTNode.types.BLOCK ||
728+
movingType === Blockly.ASTNode.types.STACK
729729
) {
730730
const stationaryAsConnection =
731731
stationaryLoc as Blockly.RenderedConnection;
732732
const movingAsBlock = movingLoc as Blockly.BlockSvg;
733733
return this.insertBlock(movingAsBlock, stationaryAsConnection);
734734
}
735-
} else if (stationaryType == Blockly.ASTNode.types.WORKSPACE) {
735+
} else if (stationaryType === Blockly.ASTNode.types.WORKSPACE) {
736736
const block = movingNode
737737
? (movingNode.getSourceBlock() as Blockly.BlockSvg)
738738
: null;
739739
return this.moveBlockToWorkspace(block, stationaryNode);
740740
} else if (
741-
stationaryType == Blockly.ASTNode.types.BLOCK &&
742-
movingType == Blockly.ASTNode.types.BLOCK
741+
stationaryType === Blockly.ASTNode.types.BLOCK &&
742+
movingType === Blockly.ASTNode.types.BLOCK
743743
) {
744744
// Insert the moving block above the stationary block, if the
745745
// appropriate connections exist.
@@ -783,19 +783,19 @@ export class Navigation {
783783
const cursorType = cursorNode.getType();
784784

785785
// Check the marker for invalid types.
786-
if (markerType == Blockly.ASTNode.types.FIELD) {
786+
if (markerType === Blockly.ASTNode.types.FIELD) {
787787
this.warn('Should not have been able to mark a field.');
788788
return false;
789-
} else if (markerType == Blockly.ASTNode.types.STACK) {
789+
} else if (markerType === Blockly.ASTNode.types.STACK) {
790790
this.warn('Should not have been able to mark a stack.');
791791
return false;
792792
}
793793

794794
// Check the cursor for invalid types.
795-
if (cursorType == Blockly.ASTNode.types.FIELD) {
795+
if (cursorType === Blockly.ASTNode.types.FIELD) {
796796
this.warn('Cannot attach a field to anything else.');
797797
return false;
798-
} else if (cursorType == Blockly.ASTNode.types.WORKSPACE) {
798+
} else if (cursorType === Blockly.ASTNode.types.WORKSPACE) {
799799
this.warn('Cannot attach a workspace to anything else.');
800800
return false;
801801
}
@@ -1255,9 +1255,9 @@ export class Navigation {
12551255
if (!cursor) return;
12561256
const curNode = cursor.getCurNode();
12571257
const nodeType = curNode.getType();
1258-
if (nodeType == Blockly.ASTNode.types.FIELD) {
1258+
if (nodeType === Blockly.ASTNode.types.FIELD) {
12591259
(curNode.getLocation() as Blockly.Field).showEditor();
1260-
} else if (nodeType == Blockly.ASTNode.types.BLOCK) {
1260+
} else if (nodeType === Blockly.ASTNode.types.BLOCK) {
12611261
const block = curNode.getLocation() as Blockly.Block;
12621262
if (!tryShowFullBlockFieldEditor(block)) {
12631263
const metaKey = navigator.platform.startsWith('Mac') ? 'Cmd' : 'Ctrl';
@@ -1271,10 +1271,10 @@ export class Navigation {
12711271
}
12721272
} else if (
12731273
curNode.isConnection() ||
1274-
nodeType == Blockly.ASTNode.types.WORKSPACE
1274+
nodeType === Blockly.ASTNode.types.WORKSPACE
12751275
) {
12761276
this.openToolboxOrFlyout(workspace);
1277-
} else if (nodeType == Blockly.ASTNode.types.STACK) {
1277+
} else if (nodeType === Blockly.ASTNode.types.STACK) {
12781278
this.warn('Cannot mark a stack.');
12791279
}
12801280
}
@@ -1294,55 +1294,90 @@ export class Navigation {
12941294
}
12951295

12961296
/**
1297-
* Show the action menu for a given node.
1297+
* Show the action menu for the current node.
12981298
*
12991299
* The action menu will contain entries for relevant actions for the
13001300
* node's location. If the location is a block, this will include
13011301
* the contents of the block's context menu (if any).
1302+
*
1303+
* Returns true if it is possible to open the action menu in the
1304+
* current location, even if the menu was not opened due there being
1305+
* no applicable menu items.
13021306
*/
1303-
openActionMenu(node: Blockly.ASTNode) {
1304-
const fakeEvent = fakeEventForNode(node);
1305-
(node.getLocation() as Blockly.BlockSvg).showContextMenu(fakeEvent);
1306-
1307+
openActionMenu(workspace: Blockly.WorkspaceSvg): boolean {
13071308
let menuOptions: Array<
13081309
| Blockly.ContextMenuRegistry.ContextMenuOption
13091310
| Blockly.ContextMenuRegistry.LegacyContextMenuOption
1310-
> | null = null;
1311+
> = [];
13111312
let rtl: boolean;
1312-
let workspace: Blockly.WorkspaceSvg;
13131313

1314+
const cursor = workspace.getCursor();
1315+
if (!cursor) throw new Error('workspace has no cursor');
1316+
const node = cursor.getCurNode();
13141317
const nodeType = node.getType();
13151318
switch (nodeType) {
13161319
case Blockly.ASTNode.types.BLOCK:
13171320
const block = node.getLocation() as Blockly.BlockSvg;
1318-
workspace = block.workspace as Blockly.WorkspaceSvg;
13191321
rtl = block.RTL;
1320-
13211322
// Reimplement BlockSvg.prototype.generateContextMenu as that
13221323
// method is protected.
1323-
if (!workspace.options.readOnly && !block.contextMenu) {
1324+
if (!workspace.options.readOnly && block.contextMenu) {
13241325
menuOptions =
13251326
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
13261327
Blockly.ContextMenuRegistry.ScopeType.BLOCK,
13271328
{block},
13281329
);
13291330

13301331
// Allow the block to add or modify menuOptions.
1331-
if (block.customContextMenu) {
1332-
block.customContextMenu(menuOptions);
1333-
}
1332+
block.customContextMenu?.(menuOptions);
13341333
}
13351334
// End reimplement.
13361335
break;
1336+
1337+
// case Blockly.ASTNode.types.INPUT:
1338+
case Blockly.ASTNode.types.NEXT:
1339+
case Blockly.ASTNode.types.PREVIOUS:
1340+
const connection = node.getLocation() as Blockly.Connection;
1341+
rtl = connection.getSourceBlock().RTL;
1342+
1343+
// Slightly hacky: get insert action from registry. Hacky
1344+
// because registry typings don't include {connection: ...} as
1345+
// a possible kind of scope.
1346+
const insertAction =
1347+
Blockly.ContextMenuRegistry.registry.getItem('insert');
1348+
if (!insertAction) throw new Error("can't find insert action");
1349+
const possibleOptions = [insertAction /* etc.*/];
1350+
1351+
// Check preconditions and get menu texts.
1352+
const scope = {
1353+
connection,
1354+
} as unknown as Blockly.ContextMenuRegistry.Scope;
1355+
for (const option of possibleOptions) {
1356+
const precondition = option.preconditionFn(scope);
1357+
if (precondition === 'hidden') continue;
1358+
const displayText =
1359+
typeof option.displayText === 'function'
1360+
? option.displayText(scope)
1361+
: option.displayText;
1362+
menuOptions.push({
1363+
text: displayText,
1364+
enabled: precondition === 'enabled',
1365+
callback: option.callback,
1366+
scope,
1367+
weight: option.weight,
1368+
});
1369+
}
1370+
break;
1371+
13371372
default:
1338-
throw new TypeError(
1339-
`unable to show action menu for ASTNode of type ${nodeType}`,
1340-
);
1373+
console.info(`No action menu for ASTNode of type ${nodeType}`);
1374+
return false;
13411375
}
13421376

1343-
if (!menuOptions || !menuOptions.length) return;
1344-
1377+
if (!menuOptions?.length) return true;
1378+
const fakeEvent = fakeEventForNode(node);
13451379
Blockly.ContextMenu.show(fakeEvent, menuOptions, rtl, workspace);
1380+
return true;
13461381
}
13471382

13481383
/**
@@ -1411,12 +1446,29 @@ export class Navigation {
14111446
* Create a fake PointerEvent for opening the action menu for the
14121447
* given ASTNode.
14131448
*
1414-
* Currently only works for block nodes.
1415-
*
14161449
* @param node The node to open the action menu for.
14171450
* @returns A synthetic pointerdown PointerEvent.
14181451
*/
14191452
function fakeEventForNode(node: Blockly.ASTNode): PointerEvent {
1453+
switch (node.getType()) {
1454+
case Blockly.ASTNode.types.BLOCK:
1455+
return fakeEventForBlockNode(node);
1456+
case Blockly.ASTNode.types.NEXT:
1457+
case Blockly.ASTNode.types.PREVIOUS:
1458+
return fakeEventForStackNode(node);
1459+
default:
1460+
throw new TypeError('unhandled node type');
1461+
}
1462+
}
1463+
1464+
/**
1465+
* Create a fake PointerEvent for opening the action menu for the
1466+
* given ASTNode of type BLOCK.
1467+
*
1468+
* @param node The node to open the action menu for.
1469+
* @returns A synthetic pointerdown PointerEvent.
1470+
*/
1471+
function fakeEventForBlockNode(node: Blockly.ASTNode): PointerEvent {
14201472
if (node.getType() !== Blockly.ASTNode.types.BLOCK) {
14211473
throw new TypeError('can only create PointerEvents for BLOCK nodes');
14221474
}
@@ -1449,6 +1501,36 @@ function fakeEventForNode(node: Blockly.ASTNode): PointerEvent {
14491501
});
14501502
}
14511503

1504+
/**
1505+
* Create a fake PointerEvent for opening the action menu for the
1506+
* given ASTNode of type NEXT or PREVIOUS.
1507+
*
1508+
* For now this just puts the action menu in the same place as the
1509+
* context menu for the source block.
1510+
*
1511+
* @param node The node to open the action menu for.
1512+
* @returns A synthetic pointerdown PointerEvent.
1513+
*/
1514+
function fakeEventForStackNode(node: Blockly.ASTNode): PointerEvent {
1515+
if (
1516+
node.getType() !== Blockly.ASTNode.types.NEXT &&
1517+
node.getType() !== Blockly.ASTNode.types.PREVIOUS
1518+
) {
1519+
throw new TypeError(
1520+
'can only create PointerEvents for NEXT / PREVIOUS nodes',
1521+
);
1522+
}
1523+
1524+
const connection = node.getLocation() as Blockly.Connection;
1525+
1526+
return fakeEventForBlockNode(
1527+
new Blockly.ASTNode(
1528+
Blockly.ASTNode.types.BLOCK,
1529+
connection.getSourceBlock(),
1530+
),
1531+
);
1532+
}
1533+
14521534
/**
14531535
* If this block has a full block field then show its editor.
14541536
*

0 commit comments

Comments
 (0)