Skip to content

Commit 0d5cc01

Browse files
authored
release: merge develop into v12.1.0
Merge pull request #9118 from maribethb/rc/v12.1.0
2 parents f416875 + 2ffd3cb commit 0d5cc01

5 files changed

Lines changed: 118 additions & 35 deletions

File tree

core/focus_manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,13 @@ export class FocusManager {
458458
};
459459
}
460460

461+
/**
462+
* @returns whether something is currently holding ephemeral focus
463+
*/
464+
ephemeralFocusTaken(): boolean {
465+
return this.currentlyHoldsEphemeralFocus;
466+
}
467+
461468
/**
462469
* Ensures that the manager is currently allowing operations that change its
463470
* internal focus state (such as via focusNode()).

core/layer_manager.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ export class LayerManager {
104104
moveToDragLayer(elem: IRenderedElement & IFocusableNode) {
105105
this.dragLayer?.appendChild(elem.getSvgRoot());
106106

107-
// Since moving the element to the drag layer will cause it to lose focus,
108-
// ensure it regains focus (to ensure proper highlights & sent events).
109-
getFocusManager().focusNode(elem);
107+
if (elem.canBeFocused()) {
108+
// Since moving the element to the drag layer will cause it to lose focus,
109+
// ensure it regains focus (to ensure proper highlights & sent events).
110+
getFocusManager().focusNode(elem);
111+
}
110112
}
111113

112114
/**
@@ -117,9 +119,11 @@ export class LayerManager {
117119
moveOffDragLayer(elem: IRenderedElement & IFocusableNode, layerNum: number) {
118120
this.append(elem, layerNum);
119121

120-
// Since moving the element off the drag layer will cause it to lose focus,
121-
// ensure it regains focus (to ensure proper highlights & sent events).
122-
getFocusManager().focusNode(elem);
122+
if (elem.canBeFocused()) {
123+
// Since moving the element off the drag layer will cause it to lose focus,
124+
// ensure it regains focus (to ensure proper highlights & sent events).
125+
getFocusManager().focusNode(elem);
126+
}
123127
}
124128

125129
/**

core/shortcut_items.ts

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {BlockSvg} from './block_svg.js';
1010
import * as clipboard from './clipboard.js';
1111
import * as eventUtils from './events/utils.js';
12+
import {getFocusManager} from './focus_manager.js';
1213
import {Gesture} from './gesture.js';
1314
import {
1415
ICopyable,
@@ -72,7 +73,9 @@ export function registerDelete() {
7273
focused != null &&
7374
isIDeletable(focused) &&
7475
focused.isDeletable() &&
75-
!Gesture.inProgress()
76+
!Gesture.inProgress() &&
77+
// Don't delete the block if a field editor is open
78+
!getFocusManager().ephemeralFocusTaken()
7679
);
7780
},
7881
callback(workspace, e, shortcut, scope) {
@@ -101,7 +104,7 @@ let copyWorkspace: WorkspaceSvg | null = null;
101104
let copyCoords: Coordinate | null = null;
102105

103106
/**
104-
* Determine if a focusable node can be copied using cut or copy.
107+
* Determine if a focusable node can be copied.
105108
*
106109
* Unfortunately the ICopyable interface doesn't include an isCopyable
107110
* method, so we must use some other criteria to make the decision.
@@ -110,8 +113,8 @@ let copyCoords: Coordinate | null = null;
110113
* - It must be an ICopyable.
111114
* - So that a pasted copy can be manipluated and/or disposed of, it
112115
* must be both an IDraggable and an IDeletable.
113-
* - Additionally, both .isMovable() and .isDeletable() must return
114-
* true (i.e., can currently be moved and deleted).
116+
* - Additionally, both .isOwnMovable() and .isOwnDeletable() must return
117+
* true (i.e., the copy could be moved and deleted).
115118
*
116119
* TODO(#9098): Revise these criteria. The latter criteria prevents
117120
* shadow blocks from being copied; additionally, there are likely to
@@ -124,6 +127,40 @@ let copyCoords: Coordinate | null = null;
124127
function isCopyable(
125128
focused: IFocusableNode,
126129
): focused is ICopyable<ICopyData> & IDeletable & IDraggable {
130+
if (!(focused instanceof BlockSvg)) return false;
131+
return (
132+
isICopyable(focused) &&
133+
isIDeletable(focused) &&
134+
focused.isOwnDeletable() &&
135+
isDraggable(focused) &&
136+
focused.isOwnMovable()
137+
);
138+
}
139+
140+
/**
141+
* Determine if a focusable node can be cut.
142+
*
143+
* Unfortunately the ICopyable interface doesn't include an isCuttable
144+
* method, so we must use some other criteria to make the decision.
145+
* Specifically,
146+
*
147+
* - It must be an ICopyable.
148+
* - So that a pasted copy can be manipluated and/or disposed of, it
149+
* must be both an IDraggable and an IDeletable.
150+
* - Additionally, both .isMovable() and .isDeletable() must return
151+
* true (i.e., can currently be moved and deleted). This is the main
152+
* difference with isCopyable.
153+
*
154+
* TODO(#9098): Revise these criteria. The latter criteria prevents
155+
* shadow blocks from being copied; additionally, there are likely to
156+
* be other circumstances were it is desirable to allow movable /
157+
* copyable copies of a currently-unmovable / -copyable block to be
158+
* made.
159+
*
160+
* @param focused The focused object.
161+
*/
162+
function isCuttable(focused: IFocusableNode): boolean {
163+
if (!(focused instanceof BlockSvg)) return false;
127164
return (
128165
isICopyable(focused) &&
129166
isIDeletable(focused) &&
@@ -148,28 +185,45 @@ export function registerCopy() {
148185
name: names.COPY,
149186
preconditionFn(workspace, scope) {
150187
const focused = scope.focusedNode;
188+
if (!(focused instanceof BlockSvg)) return false;
189+
190+
const targetWorkspace = workspace.isFlyout
191+
? workspace.targetWorkspace
192+
: workspace;
151193
return (
152-
!workspace.isReadOnly() &&
153-
!Gesture.inProgress() &&
154194
!!focused &&
195+
!!targetWorkspace &&
196+
!targetWorkspace.isReadOnly() &&
197+
!targetWorkspace.isDragging() &&
198+
!getFocusManager().ephemeralFocusTaken() &&
155199
isCopyable(focused)
156200
);
157201
},
158202
callback(workspace, e, shortcut, scope) {
159203
// Prevent the default copy behavior, which may beep or otherwise indicate
160204
// an error due to the lack of a selection.
161205
e.preventDefault();
162-
workspace.hideChaff();
206+
163207
const focused = scope.focusedNode;
164-
if (!focused || !isICopyable(focused)) return false;
165-
copyData = focused.toCopyData();
166-
copyWorkspace =
208+
if (!focused || !isCopyable(focused)) return false;
209+
let targetWorkspace: WorkspaceSvg | null =
167210
focused.workspace instanceof WorkspaceSvg
168211
? focused.workspace
169212
: workspace;
170-
copyCoords = isDraggable(focused)
171-
? focused.getRelativeToSurfaceXY()
172-
: null;
213+
targetWorkspace = targetWorkspace.isFlyout
214+
? targetWorkspace.targetWorkspace
215+
: targetWorkspace;
216+
if (!targetWorkspace) return false;
217+
218+
if (!focused.workspace.isFlyout) {
219+
targetWorkspace.hideChaff();
220+
}
221+
copyData = focused.toCopyData();
222+
copyWorkspace = targetWorkspace;
223+
copyCoords =
224+
isDraggable(focused) && focused.workspace == targetWorkspace
225+
? focused.getRelativeToSurfaceXY()
226+
: null;
173227
return !!copyData;
174228
},
175229
keyCodes: [ctrlC, metaC],
@@ -193,13 +247,11 @@ export function registerCut() {
193247
preconditionFn(workspace, scope) {
194248
const focused = scope.focusedNode;
195249
return (
196-
!workspace.isReadOnly() &&
197-
!Gesture.inProgress() &&
198250
!!focused &&
199-
isCopyable(focused) &&
200-
// Extra criteria for cut (not just copy):
201-
!focused.workspace.isFlyout &&
202-
focused.isDeletable()
251+
!workspace.isReadOnly() &&
252+
!workspace.isDragging() &&
253+
!getFocusManager().ephemeralFocusTaken() &&
254+
isCuttable(focused)
203255
);
204256
},
205257
callback(workspace, e, shortcut, scope) {
@@ -246,7 +298,16 @@ export function registerPaste() {
246298
const pasteShortcut: KeyboardShortcut = {
247299
name: names.PASTE,
248300
preconditionFn(workspace) {
249-
return !workspace.isReadOnly() && !Gesture.inProgress();
301+
const targetWorkspace = workspace.isFlyout
302+
? workspace.targetWorkspace
303+
: workspace;
304+
return (
305+
!!copyData &&
306+
!!targetWorkspace &&
307+
!targetWorkspace.isReadOnly() &&
308+
!targetWorkspace.isDragging() &&
309+
!getFocusManager().ephemeralFocusTaken()
310+
);
250311
},
251312
callback(workspace: WorkspaceSvg, e: Event) {
252313
if (!copyData || !copyWorkspace) return false;
@@ -305,7 +366,11 @@ export function registerUndo() {
305366
const undoShortcut: KeyboardShortcut = {
306367
name: names.UNDO,
307368
preconditionFn(workspace) {
308-
return !workspace.isReadOnly() && !Gesture.inProgress();
369+
return (
370+
!workspace.isReadOnly() &&
371+
!Gesture.inProgress() &&
372+
!getFocusManager().ephemeralFocusTaken()
373+
);
309374
},
310375
callback(workspace, e) {
311376
// 'z' for undo 'Z' is for redo.
@@ -340,7 +405,11 @@ export function registerRedo() {
340405
const redoShortcut: KeyboardShortcut = {
341406
name: names.REDO,
342407
preconditionFn(workspace) {
343-
return !Gesture.inProgress() && !workspace.isReadOnly();
408+
return (
409+
!Gesture.inProgress() &&
410+
!workspace.isReadOnly() &&
411+
!getFocusManager().ephemeralFocusTaken()
412+
);
344413
},
345414
callback(workspace, e) {
346415
// 'z' for undo 'Z' is for redo.

core/workspace_svg.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2806,10 +2806,11 @@ export class WorkspaceSvg
28062806
/** See IFocusableTree.onTreeBlur. */
28072807
onTreeBlur(nextTree: IFocusableTree | null): void {
28082808
// If the flyout loses focus, make sure to close it unless focus is being
2809-
// lost to the toolbox.
2809+
// lost to the toolbox or ephemeral focus.
28102810
if (this.isFlyout && this.targetWorkspace) {
28112811
// Only hide the flyout if the flyout's workspace is losing focus and that
2812-
// focus isn't returning to the flyout itself or the toolbox.
2812+
// focus isn't returning to the flyout itself, the toolbox, or ephemeral.
2813+
if (getFocusManager().ephemeralFocusTaken()) return;
28132814
const flyout = this.targetWorkspace.getFlyout();
28142815
const toolbox = this.targetWorkspace.getToolbox();
28152816
if (toolbox && nextTree === toolbox) return;

tests/mocha/shortcut_items_test.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,13 @@ suite('Keyboard Shortcut Items', function () {
181181
runReadOnlyTest(keyEvent, testCaseName);
182182
});
183183
});
184-
// Do not copy a block if a gesture is in progress.
185-
suite('Gesture in progress', function () {
184+
// Do not copy a block if a drag is in progress.
185+
suite('Drag in progress', function () {
186186
testCases.forEach(function (testCase) {
187187
const testCaseName = testCase[0];
188188
const keyEvent = testCase[1];
189189
test(testCaseName, function () {
190-
sinon.stub(Blockly.Gesture, 'inProgress').returns(true);
190+
sinon.stub(this.workspace, 'isDragging').returns(true);
191191
this.injectionDiv.dispatchEvent(keyEvent);
192192
sinon.assert.notCalled(this.copySpy);
193193
sinon.assert.notCalled(this.hideChaffSpy);
@@ -201,7 +201,7 @@ suite('Keyboard Shortcut Items', function () {
201201
const keyEvent = testCase[1];
202202
test(testCaseName, function () {
203203
sinon
204-
.stub(Blockly.common.getSelected(), 'isDeletable')
204+
.stub(Blockly.common.getSelected(), 'isOwnDeletable')
205205
.returns(false);
206206
this.injectionDiv.dispatchEvent(keyEvent);
207207
sinon.assert.notCalled(this.copySpy);
@@ -215,7 +215,9 @@ suite('Keyboard Shortcut Items', function () {
215215
const testCaseName = testCase[0];
216216
const keyEvent = testCase[1];
217217
test(testCaseName, function () {
218-
sinon.stub(Blockly.common.getSelected(), 'isMovable').returns(false);
218+
sinon
219+
.stub(Blockly.common.getSelected(), 'isOwnMovable')
220+
.returns(false);
219221
this.injectionDiv.dispatchEvent(keyEvent);
220222
sinon.assert.notCalled(this.copySpy);
221223
sinon.assert.notCalled(this.hideChaffSpy);

0 commit comments

Comments
 (0)