Skip to content

Commit 43dfe2e

Browse files
authored
Merge pull request #9606 from RaspberryPiFoundation/main
chore: Merge `main` into `v13`
2 parents 26d0351 + a16580a commit 43dfe2e

9 files changed

Lines changed: 421 additions & 290 deletions

File tree

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/blockly/core/block_svg.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,32 @@ export class BlockSvg
870870
return this.svgGroup;
871871
}
872872

873+
/**
874+
* Returns the closest live block to this one, if any.
875+
*/
876+
private getNearestNeighbour() {
877+
if (!this.workspace.rendered) return null;
878+
879+
const blocks = this.workspace
880+
.getAllBlocks(false)
881+
.filter((block) => !block.isDeadOrDying());
882+
let nearestNeighbour = null;
883+
let closestDistance = Number.MAX_SAFE_INTEGER;
884+
const self = this.getRelativeToSurfaceXY();
885+
for (const block of blocks) {
886+
const other = block.getRelativeToSurfaceXY();
887+
const distance = Math.sqrt(
888+
Math.pow(other.x - self.x, 2) + Math.pow(other.y - self.y, 2),
889+
);
890+
if (distance < closestDistance) {
891+
nearestNeighbour = block;
892+
closestDistance = distance;
893+
}
894+
}
895+
896+
return nearestNeighbour;
897+
}
898+
873899
/**
874900
* Dispose of this block.
875901
*
@@ -911,7 +937,15 @@ export class BlockSvg
911937
if (parent) {
912938
focusManager.focusNode(parent);
913939
} else {
914-
setTimeout(() => focusManager.focusTree(this.workspace), 0);
940+
const nearestNeighbour = this.getNearestNeighbour();
941+
if (nearestNeighbour) {
942+
focusManager.focusNode(nearestNeighbour);
943+
} else {
944+
setTimeout(() => {
945+
if (!this.workspace.rendered) return;
946+
focusManager.focusTree(this.workspace);
947+
}, 0);
948+
}
915949
}
916950
}
917951

packages/blockly/core/contextmenu.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ function haltPropagation(e: Event) {
238238
export function hide() {
239239
WidgetDiv.hideIfOwner(dummyOwner);
240240
currentBlock = null;
241+
menu_?.dispose();
242+
menu_ = null;
241243
}
242244

243245
/**
@@ -293,3 +295,10 @@ export function callbackFactory(
293295
return newBlock;
294296
};
295297
}
298+
299+
/**
300+
* Returns the contextual menu if it is currently being shown.
301+
*/
302+
export function getMenu(): Menu | null {
303+
return menu_;
304+
}

packages/blockly/core/shortcut_items.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import {BlockSvg} from './block_svg.js';
1010
import * as clipboard from './clipboard.js';
1111
import {RenderedWorkspaceComment} from './comments.js';
12+
import * as contextmenu from './contextmenu.js';
1213
import * as eventUtils from './events/utils.js';
1314
import {getFocusManager} from './focus_manager.js';
15+
import {hasContextMenu} from './interfaces/i_contextmenu.js';
1416
import {isCopyable as isICopyable} from './interfaces/i_copyable.js';
1517
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
1618
import {isDraggable} from './interfaces/i_draggable.js';
@@ -33,6 +35,7 @@ export enum names {
3335
PASTE = 'paste',
3436
UNDO = 'undo',
3537
REDO = 'redo',
38+
MENU = 'menu',
3639
}
3740

3841
/**
@@ -134,10 +137,7 @@ function isCuttable(focused: IFocusableNode): boolean {
134137
*/
135138
export function registerCopy() {
136139
const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
137-
KeyCodes.CTRL,
138-
]);
139-
const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [
140-
KeyCodes.META,
140+
KeyCodes.CTRL_CMD,
141141
]);
142142

143143
const copyShortcut: KeyboardShortcut = {
@@ -179,7 +179,7 @@ export function registerCopy() {
179179
: undefined;
180180
return !!clipboard.copy(focused, copyCoords);
181181
},
182-
keyCodes: [ctrlC, metaC],
182+
keyCodes: [ctrlC],
183183
};
184184
ShortcutRegistry.registry.register(copyShortcut);
185185
}
@@ -189,10 +189,7 @@ export function registerCopy() {
189189
*/
190190
export function registerCut() {
191191
const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
192-
KeyCodes.CTRL,
193-
]);
194-
const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [
195-
KeyCodes.META,
192+
KeyCodes.CTRL_CMD,
196193
]);
197194

198195
const cutShortcut: KeyboardShortcut = {
@@ -224,7 +221,7 @@ export function registerCut() {
224221
}
225222
return !!copyData;
226223
},
227-
keyCodes: [ctrlX, metaX],
224+
keyCodes: [ctrlX],
228225
};
229226

230227
ShortcutRegistry.registry.register(cutShortcut);
@@ -235,10 +232,7 @@ export function registerCut() {
235232
*/
236233
export function registerPaste() {
237234
const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
238-
KeyCodes.CTRL,
239-
]);
240-
const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [
241-
KeyCodes.META,
235+
KeyCodes.CTRL_CMD,
242236
]);
243237

244238
const pasteShortcut: KeyboardShortcut = {
@@ -309,7 +303,7 @@ export function registerPaste() {
309303
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
310304
return !!clipboard.paste(copyData, targetWorkspace, centerCoords);
311305
},
312-
keyCodes: [ctrlV, metaV],
306+
keyCodes: [ctrlV],
313307
};
314308

315309
ShortcutRegistry.registry.register(pasteShortcut);
@@ -320,10 +314,7 @@ export function registerPaste() {
320314
*/
321315
export function registerUndo() {
322316
const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
323-
KeyCodes.CTRL,
324-
]);
325-
const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
326-
KeyCodes.META,
317+
KeyCodes.CTRL_CMD,
327318
]);
328319

329320
const undoShortcut: KeyboardShortcut = {
@@ -342,7 +333,7 @@ export function registerUndo() {
342333
e.preventDefault();
343334
return true;
344335
},
345-
keyCodes: [ctrlZ, metaZ],
336+
keyCodes: [ctrlZ],
346337
};
347338
ShortcutRegistry.registry.register(undoShortcut);
348339
}
@@ -353,13 +344,10 @@ export function registerUndo() {
353344
*/
354345
export function registerRedo() {
355346
const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
356-
KeyCodes.CTRL,
357-
KeyCodes.SHIFT,
358-
]);
359-
const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [
360-
KeyCodes.META,
347+
KeyCodes.CTRL_CMD,
361348
KeyCodes.SHIFT,
362349
]);
350+
363351
// Ctrl-y is redo in Windows. Command-y is never valid on Macs.
364352
const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [
365353
KeyCodes.CTRL,
@@ -381,11 +369,40 @@ export function registerRedo() {
381369
e.preventDefault();
382370
return true;
383371
},
384-
keyCodes: [ctrlShiftZ, metaShiftZ, ctrlY],
372+
keyCodes: [ctrlShiftZ, ctrlY],
385373
};
386374
ShortcutRegistry.registry.register(redoShortcut);
387375
}
388376

377+
/**
378+
* Keyboard shortcut to show the context menu on ctrl/cmd+Enter.
379+
*/
380+
export function registerShowContextMenu() {
381+
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
382+
KeyCodes.ENTER,
383+
[KeyCodes.CTRL_CMD],
384+
);
385+
386+
const contextMenuShortcut: KeyboardShortcut = {
387+
name: names.MENU,
388+
preconditionFn: (workspace) => {
389+
return !workspace.isDragging();
390+
},
391+
callback: (workspace, e) => {
392+
const target = getFocusManager().getFocusedNode();
393+
if (hasContextMenu(target)) {
394+
target.showContextMenu(e);
395+
contextmenu.getMenu()?.highlightNext();
396+
397+
return true;
398+
}
399+
return false;
400+
},
401+
keyCodes: [ctrlEnter],
402+
};
403+
ShortcutRegistry.registry.register(contextMenuShortcut);
404+
}
405+
389406
/**
390407
* Registers all default keyboard shortcut item. This should be called once per
391408
* instance of KeyboardShortcutRegistry.
@@ -400,6 +417,7 @@ export function registerDefaultShortcuts() {
400417
registerPaste();
401418
registerUndo();
402419
registerRedo();
420+
registerShowContextMenu();
403421
}
404422

405423
registerDefaultShortcuts();

packages/blockly/core/utils/keycodes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import * as userAgent from '../utils/useragent.js';
8+
79
// Former goog.module ID: Blockly.utils.KeyCodes
810

911
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
@@ -151,4 +153,10 @@ export enum KeyCodes {
151153
// indicates a hardware/bios problem.
152154
// http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
153155
PHANTOM = 255,
156+
157+
// The primary modifier key on the current platform, i.e. Command on Apple
158+
// platforms and Control elsewhere.
159+
CTRL_CMD = userAgent.MAC || userAgent.IPHONE || userAgent.IPAD
160+
? MAC_WK_CMD_LEFT
161+
: CTRL,
154162
}

packages/blockly/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blockly",
3-
"version": "12.3.1",
3+
"version": "12.4.1",
44
"description": "Blockly is a library for building visual programming editors.",
55
"keywords": [
66
"blockly"

packages/blockly/tests/mocha/block_test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,4 +2946,119 @@ suite('Blocks', function () {
29462946
);
29472947
});
29482948
});
2949+
2950+
suite('Disposal focus management', function () {
2951+
setup(function () {
2952+
this.workspace = Blockly.inject('blocklyDiv');
2953+
const firstBlock = this.workspace.newBlock('stack_block');
2954+
firstBlock.moveBy(-500, -500);
2955+
});
2956+
2957+
test('Deleting the sole block on the workspace focuses the workspace', function () {
2958+
const block = this.workspace.getTopBlocks(false)[0];
2959+
Blockly.getFocusManager().focusNode(block);
2960+
block.dispose();
2961+
this.clock.runAll();
2962+
2963+
assert.strictEqual(
2964+
Blockly.getFocusManager().getFocusedNode(),
2965+
this.workspace,
2966+
'Focus should move to the workspace when the focused block is deleted',
2967+
);
2968+
});
2969+
2970+
test('Deleting a block with several adjacent blocks focuses the closest one', function () {
2971+
this.workspace.newBlock('stack_block');
2972+
const blockMiddle = this.workspace.newBlock('stack_block');
2973+
const blockRight = this.workspace.newBlock('stack_block');
2974+
blockMiddle.moveBy(60, 0);
2975+
blockRight.moveBy(100, 0);
2976+
2977+
Blockly.getFocusManager().focusNode(blockMiddle);
2978+
blockMiddle.dispose();
2979+
this.clock.runAll();
2980+
2981+
const focused = Blockly.getFocusManager().getFocusedNode();
2982+
assert.strictEqual(
2983+
focused,
2984+
blockRight,
2985+
'Focus should move to the closest remaining block (blockRight at (100, 0))',
2986+
);
2987+
});
2988+
2989+
test('Bulk deleting blocks does not focus another dying block', function () {
2990+
const blocks = this.workspace.getTopBlocks(false);
2991+
for (let i = 0; i < 5; i++) {
2992+
blocks.push(this.workspace.newBlock('stack_block'));
2993+
}
2994+
2995+
// Focus the last block we added; clearing the workspace proceeds in block
2996+
// creation order, so if we focused an earlier block, it would (correctly)
2997+
// assign focus to a later-added block which is not yet dying, on down the
2998+
// chain. If we focus the last block, by the time deletion gets to it, all
2999+
// the other blocks will have already been marked as disposing, and should
3000+
// thus be ineligible to be focused.
3001+
Blockly.getFocusManager().focusNode(
3002+
this.workspace.getTopBlocks(false)[5],
3003+
);
3004+
3005+
const spy = sinon.spy(Blockly.getFocusManager(), 'focusNode');
3006+
3007+
this.workspace.clear();
3008+
this.clock.runAll();
3009+
3010+
for (const block of blocks) {
3011+
assert.isFalse(spy.calledWith(block));
3012+
}
3013+
assert.strictEqual(
3014+
Blockly.getFocusManager().getFocusedNode(),
3015+
this.workspace,
3016+
'Focus should move to the workspace, not a dying peer block',
3017+
);
3018+
3019+
spy.restore();
3020+
});
3021+
3022+
test('Deleting a block focuses its parent block', function () {
3023+
const parent = this.workspace.newBlock('stack_block');
3024+
const child = this.workspace.newBlock('stack_block');
3025+
parent.nextConnection.connect(child.previousConnection);
3026+
3027+
Blockly.getFocusManager().focusNode(child);
3028+
child.dispose();
3029+
this.clock.runAll();
3030+
3031+
assert.strictEqual(
3032+
Blockly.getFocusManager().getFocusedNode(),
3033+
parent,
3034+
'Focus should move to the parent block when a connected child is deleted',
3035+
);
3036+
});
3037+
3038+
test('Deleting an unfocused block does not change focus', function () {
3039+
const a = this.workspace.getTopBlocks(false)[0];
3040+
const b = this.workspace.newBlock('stack_block');
3041+
this.workspace.newBlock('stack_block');
3042+
3043+
Blockly.getFocusManager().focusNode(a);
3044+
b.dispose();
3045+
this.clock.runAll();
3046+
3047+
assert.strictEqual(
3048+
Blockly.getFocusManager().getFocusedNode(),
3049+
a,
3050+
'Focus should not change when an unfocused block is deleted',
3051+
);
3052+
});
3053+
3054+
test('Disposing a workspace with a focused block succeeds', function () {
3055+
Blockly.getFocusManager().focusNode(
3056+
this.workspace.getTopBlocks(false)[0],
3057+
);
3058+
this.workspace.dispose();
3059+
this.clock.runAll();
3060+
3061+
// No assert, this just shouldn't throw.
3062+
});
3063+
});
29493064
});

0 commit comments

Comments
 (0)