Skip to content

Commit 7df501d

Browse files
authored
fix: Add isCopyable to the ICopyable interface and use it for cut/copy preconditions
Merge pull request #9134 from RoboErikG/is-copyable
2 parents 3e09a70 + 2bae8eb commit 7df501d

File tree

7 files changed

+241
-101
lines changed

7 files changed

+241
-101
lines changed

core/block.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ export class Block {
791791
isDeletable(): boolean {
792792
return (
793793
this.deletable &&
794+
!this.isInFlyout &&
794795
!this.shadow &&
795796
!this.isDeadOrDying() &&
796797
!this.workspace.isReadOnly()
@@ -824,6 +825,7 @@ export class Block {
824825
isMovable(): boolean {
825826
return (
826827
this.movable &&
828+
!this.isInFlyout &&
827829
!this.shadow &&
828830
!this.isDeadOrDying() &&
829831
!this.workspace.isReadOnly()

core/block_svg.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,11 @@ export class BlockSvg
17211721
this.dragStrategy = dragStrategy;
17221722
}
17231723

1724+
/** Returns whether this block is copyable or not. */
1725+
isCopyable(): boolean {
1726+
return this.isOwnDeletable() && this.isOwnMovable();
1727+
}
1728+
17241729
/** Returns whether this block is movable or not. */
17251730
override isMovable(): boolean {
17261731
return this.dragStrategy.isMovable();

core/comments/rendered_workspace_comment.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ export class RenderedWorkspaceComment
244244
}
245245
}
246246

247+
/** Returns whether this comment is copyable or not */
248+
isCopyable(): boolean {
249+
return this.isOwnMovable() && this.isOwnDeletable();
250+
}
251+
247252
/** Returns whether this comment is movable or not. */
248253
isMovable(): boolean {
249254
return this.dragStrategy.isMovable();

core/comments/workspace_comment.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ export class WorkspaceComment {
165165
* workspace is read-only.
166166
*/
167167
isMovable() {
168-
return this.isOwnMovable() && !this.workspace.isReadOnly();
168+
return (
169+
this.isOwnMovable() &&
170+
!this.workspace.isReadOnly() &&
171+
!this.workspace.isFlyout
172+
);
169173
}
170174

171175
/**
@@ -189,7 +193,8 @@ export class WorkspaceComment {
189193
return (
190194
this.isOwnDeletable() &&
191195
!this.isDeadOrDying() &&
192-
!this.workspace.isReadOnly()
196+
!this.workspace.isReadOnly() &&
197+
!this.workspace.isFlyout
193198
);
194199
}
195200

core/interfaces/i_copyable.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export interface ICopyable<T extends ICopyData> extends ISelectable {
1515
* @returns Copy metadata.
1616
*/
1717
toCopyData(): T | null;
18+
19+
/**
20+
* Whether this instance is currently copyable. The standard implementation
21+
* is to return true if isOwnDeletable and isOwnMovable return true.
22+
*
23+
* @returns True if it can currently be copied.
24+
*/
25+
isCopyable?(): boolean;
1826
}
1927

2028
export namespace ICopyable {
@@ -25,7 +33,7 @@ export namespace ICopyable {
2533

2634
export type ICopyData = ICopyable.ICopyData;
2735

28-
/** @returns true if the given object is copyable. */
36+
/** @returns true if the given object is an ICopyable. */
2937
export function isCopyable(obj: any): obj is ICopyable<ICopyData> {
3038
return obj.toCopyData !== undefined;
3139
}

core/shortcut_items.ts

Lines changed: 49 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,13 @@
88

99
import {BlockSvg} from './block_svg.js';
1010
import * as clipboard from './clipboard.js';
11+
import {RenderedWorkspaceComment} from './comments.js';
1112
import * as eventUtils from './events/utils.js';
1213
import {getFocusManager} from './focus_manager.js';
1314
import {Gesture} from './gesture.js';
14-
import {
15-
ICopyable,
16-
ICopyData,
17-
isCopyable as isICopyable,
18-
} from './interfaces/i_copyable.js';
19-
import {
20-
IDeletable,
21-
isDeletable as isIDeletable,
22-
} from './interfaces/i_deletable.js';
23-
import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
15+
import {ICopyData, isCopyable as isICopyable} from './interfaces/i_copyable.js';
16+
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
17+
import {isDraggable} from './interfaces/i_draggable.js';
2418
import {IFocusableNode} from './interfaces/i_focusable_node.js';
2519
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
2620
import {Coordinate} from './utils/coordinate.js';
@@ -100,74 +94,43 @@ export function registerDelete() {
10094
}
10195

10296
let copyData: ICopyData | null = null;
103-
let copyWorkspace: WorkspaceSvg | null = null;
10497
let copyCoords: Coordinate | null = null;
10598

10699
/**
107100
* Determine if a focusable node can be copied.
108101
*
109-
* Unfortunately the ICopyable interface doesn't include an isCopyable
110-
* method, so we must use some other criteria to make the decision.
111-
* Specifically,
112-
*
113-
* - It must be an ICopyable.
114-
* - So that a pasted copy can be manipluated and/or disposed of, it
115-
* must be both an IDraggable and an IDeletable.
116-
* - Additionally, both .isOwnMovable() and .isOwnDeletable() must return
117-
* true (i.e., the copy could be moved and deleted).
118-
*
119-
* TODO(#9098): Revise these criteria. The latter criteria prevents
120-
* shadow blocks from being copied; additionally, there are likely to
121-
* be other circumstances were it is desirable to allow movable /
122-
* copyable copies of a currently-unmovable / -copyable block to be
123-
* made.
102+
* This will use the isCopyable method if the node implements it, otherwise
103+
* it will fall back to checking if the node is deletable and draggable not
104+
* considering the workspace's edit state.
124105
*
125106
* @param focused The focused object.
126107
*/
127-
function isCopyable(
128-
focused: IFocusableNode,
129-
): 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-
);
108+
function isCopyable(focused: IFocusableNode): boolean {
109+
if (!isICopyable(focused) || !isIDeletable(focused) || !isDraggable(focused))
110+
return false;
111+
if (focused.isCopyable) {
112+
return focused.isCopyable();
113+
} else if (
114+
focused instanceof BlockSvg ||
115+
focused instanceof RenderedWorkspaceComment
116+
) {
117+
return focused.isOwnDeletable() && focused.isOwnMovable();
118+
}
119+
// This isn't a class Blockly knows about, so fall back to the stricter
120+
// checks for deletable and movable.
121+
return focused.isDeletable() && focused.isMovable();
138122
}
139123

140124
/**
141125
* Determine if a focusable node can be cut.
142126
*
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.
127+
* This will check if the node can be both copied and deleted in its current
128+
* workspace.
159129
*
160130
* @param focused The focused object.
161131
*/
162132
function isCuttable(focused: IFocusableNode): boolean {
163-
if (!(focused instanceof BlockSvg)) return false;
164-
return (
165-
isICopyable(focused) &&
166-
isIDeletable(focused) &&
167-
focused.isDeletable() &&
168-
isDraggable(focused) &&
169-
focused.isMovable()
170-
);
133+
return isCopyable(focused) && isIDeletable(focused) && focused.isDeletable();
171134
}
172135

173136
/**
@@ -185,15 +148,13 @@ export function registerCopy() {
185148
name: names.COPY,
186149
preconditionFn(workspace, scope) {
187150
const focused = scope.focusedNode;
188-
if (!(focused instanceof BlockSvg)) return false;
189151

190152
const targetWorkspace = workspace.isFlyout
191153
? workspace.targetWorkspace
192154
: workspace;
193155
return (
194156
!!focused &&
195157
!!targetWorkspace &&
196-
!targetWorkspace.isReadOnly() &&
197158
!targetWorkspace.isDragging() &&
198159
!getFocusManager().ephemeralFocusTaken() &&
199160
isCopyable(focused)
@@ -205,21 +166,17 @@ export function registerCopy() {
205166
e.preventDefault();
206167

207168
const focused = scope.focusedNode;
208-
if (!focused || !isCopyable(focused)) return false;
209-
let targetWorkspace: WorkspaceSvg | null =
210-
focused.workspace instanceof WorkspaceSvg
211-
? focused.workspace
212-
: workspace;
213-
targetWorkspace = targetWorkspace.isFlyout
214-
? targetWorkspace.targetWorkspace
215-
: targetWorkspace;
169+
if (!focused || !isICopyable(focused) || !isCopyable(focused))
170+
return false;
171+
const targetWorkspace = workspace.isFlyout
172+
? workspace.targetWorkspace
173+
: workspace;
216174
if (!targetWorkspace) return false;
217175

218176
if (!focused.workspace.isFlyout) {
219177
targetWorkspace.hideChaff();
220178
}
221179
copyData = focused.toCopyData();
222-
copyWorkspace = targetWorkspace;
223180
copyCoords =
224181
isDraggable(focused) && focused.workspace == targetWorkspace
225182
? focused.getRelativeToSurfaceXY()
@@ -256,27 +213,20 @@ export function registerCut() {
256213
},
257214
callback(workspace, e, shortcut, scope) {
258215
const focused = scope.focusedNode;
216+
if (!focused || !isCuttable(focused) || !isICopyable(focused)) {
217+
return false;
218+
}
219+
copyData = focused.toCopyData();
220+
copyCoords = isDraggable(focused)
221+
? focused.getRelativeToSurfaceXY()
222+
: null;
259223

260224
if (focused instanceof BlockSvg) {
261-
copyData = focused.toCopyData();
262-
copyWorkspace = workspace;
263-
copyCoords = focused.getRelativeToSurfaceXY();
264225
focused.checkAndDelete();
265-
return true;
266-
} else if (
267-
isIDeletable(focused) &&
268-
focused.isDeletable() &&
269-
isICopyable(focused)
270-
) {
271-
copyData = focused.toCopyData();
272-
copyWorkspace = workspace;
273-
copyCoords = isDraggable(focused)
274-
? focused.getRelativeToSurfaceXY()
275-
: null;
226+
} else if (isIDeletable(focused)) {
276227
focused.dispose();
277-
return true;
278228
}
279-
return false;
229+
return !!copyData;
280230
},
281231
keyCodes: [ctrlX, metaX],
282232
};
@@ -310,7 +260,11 @@ export function registerPaste() {
310260
);
311261
},
312262
callback(workspace: WorkspaceSvg, e: Event) {
313-
if (!copyData || !copyWorkspace) return false;
263+
if (!copyData) return false;
264+
const targetWorkspace = workspace.isFlyout
265+
? workspace.targetWorkspace
266+
: workspace;
267+
if (!targetWorkspace || targetWorkspace.isReadOnly()) return false;
314268

315269
if (e instanceof PointerEvent) {
316270
// The event that triggers a shortcut would conventionally be a KeyboardEvent.
@@ -319,32 +273,32 @@ export function registerPaste() {
319273
// at the mouse coordinates where the menu was opened, and this PointerEvent
320274
// is where the menu was opened.
321275
const mouseCoords = svgMath.screenToWsCoordinates(
322-
copyWorkspace,
276+
targetWorkspace,
323277
new Coordinate(e.clientX, e.clientY),
324278
);
325-
return !!clipboard.paste(copyData, copyWorkspace, mouseCoords);
279+
return !!clipboard.paste(copyData, targetWorkspace, mouseCoords);
326280
}
327281

328282
if (!copyCoords) {
329283
// If we don't have location data about the original copyable, let the
330284
// paster determine position.
331-
return !!clipboard.paste(copyData, copyWorkspace);
285+
return !!clipboard.paste(copyData, targetWorkspace);
332286
}
333287

334-
const {left, top, width, height} = copyWorkspace
288+
const {left, top, width, height} = targetWorkspace
335289
.getMetricsManager()
336290
.getViewMetrics(true);
337291
const viewportRect = new Rect(top, top + height, left, left + width);
338292

339293
if (viewportRect.contains(copyCoords.x, copyCoords.y)) {
340294
// If the original copyable is inside the viewport, let the paster
341295
// determine position.
342-
return !!clipboard.paste(copyData, copyWorkspace);
296+
return !!clipboard.paste(copyData, targetWorkspace);
343297
}
344298

345299
// Otherwise, paste in the middle of the viewport.
346300
const centerCoords = new Coordinate(left + width / 2, top + height / 2);
347-
return !!clipboard.paste(copyData, copyWorkspace, centerCoords);
301+
return !!clipboard.paste(copyData, targetWorkspace, centerCoords);
348302
},
349303
keyCodes: [ctrlV, metaV],
350304
};

0 commit comments

Comments
 (0)