Skip to content

Commit e53fb7c

Browse files
committed
Cleanup and fixes to cut/copy/paste
1 parent 9809ff2 commit e53fb7c

File tree

1 file changed

+62
-124
lines changed

1 file changed

+62
-124
lines changed

src/actions/clipboard.ts

Lines changed: 62 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,15 @@
77
import {
88
ContextMenuRegistry,
99
ShortcutRegistry,
10-
ICopyData,
1110
isCopyable,
12-
isDeletable,
13-
isDraggable,
1411
Msg,
1512
ShortcutItems,
16-
Flyout,
17-
getMainWorkspace,
13+
WorkspaceSvg,
1814
} from 'blockly';
1915
import * as Constants from '../constants';
20-
import {WorkspaceSvg} from 'blockly';
2116
import {Navigation} from '../navigation';
2217
import {getShortActionShortcut} from '../shortcut_formatting';
2318
import {clearPasteHints, showCopiedHint, showCutHint} from '../hints';
24-
import {IFocusableNode} from 'blockly/core';
2519

2620
/**
2721
* Weight for the first of these three items in the context menu.
@@ -31,29 +25,18 @@ import {IFocusableNode} from 'blockly/core';
3125
*/
3226
const BASE_WEIGHT = 12;
3327

34-
/** Type of the callback function for keyboard shortcuts. */
35-
type ShortcutCallback = (
36-
workspace: WorkspaceSvg,
37-
e: Event,
38-
shortcut: ShortcutRegistry.KeyboardShortcut,
39-
scope: ContextMenuRegistry.Scope,
40-
) => boolean;
41-
4228
/**
4329
* Logic and state for cut/copy/paste actions as both keyboard shortcuts
4430
* and context menu items.
4531
* In the long term, this will likely merge with the clipboard code in core.
4632
*/
4733
export class Clipboard {
48-
/** Data copied by the copy or cut keyboard shortcuts. */
49-
private copyData: ICopyData | null = null;
50-
5134
/** The workspace a copy or cut keyboard shortcut happened in. */
5235
private copyWorkspace: WorkspaceSvg | null = null;
5336

54-
private oldCutCallback: ShortcutCallback | undefined;
55-
private oldCopyCallback: ShortcutCallback | undefined;
56-
private oldPasteCallback: ShortcutCallback | undefined;
37+
private oldCutShortcut: ShortcutRegistry.KeyboardShortcut | undefined;
38+
private oldCopyShortcut: ShortcutRegistry.KeyboardShortcut | undefined;
39+
private oldPasteShortcut: ShortcutRegistry.KeyboardShortcut | undefined;
5740

5841
constructor(private navigation: Navigation) {}
5942

@@ -92,20 +75,18 @@ export class Clipboard {
9275
* Identical to the one in core but adds a toast after successful cut.
9376
*/
9477
private registerCutShortcut() {
95-
const oldCutShortcut =
78+
this.oldCutShortcut =
9679
ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.CUT];
97-
if (!oldCutShortcut)
80+
if (!this.oldCutShortcut)
9881
throw new Error('No cut keyboard shortcut registered initially');
9982

100-
this.oldCutCallback = oldCutShortcut.callback;
101-
10283
const cutShortcut: ShortcutRegistry.KeyboardShortcut = {
10384
name: Constants.SHORTCUT_NAMES.CUT,
104-
preconditionFn: oldCutShortcut.preconditionFn,
85+
preconditionFn: this.oldCutShortcut.preconditionFn,
10586
callback: this.cutCallback.bind(this),
10687
// The registry gives back keycodes as an object instead of an array
10788
// See https://github.com/google/blockly/issues/9008
108-
keyCodes: oldCutShortcut.keyCodes,
89+
keyCodes: this.oldCutShortcut.keyCodes,
10990
allowCollision: false,
11091
};
11192

@@ -127,7 +108,7 @@ export class Clipboard {
127108
'%1',
128109
getShortActionShortcut(Constants.SHORTCUT_NAMES.CUT),
129110
),
130-
preconditionFn: (scope) => this.cutCopyPrecondition(scope),
111+
preconditionFn: (scope) => this.cutPrecondition(scope),
131112
callback: (scope, menuOpenEvent) => {
132113
if (!isCopyable(scope.focusedNode)) return false;
133114
const ws = scope.focusedNode.workspace;
@@ -142,31 +123,47 @@ export class Clipboard {
142123
ContextMenuRegistry.registry.register(cutAction);
143124
}
144125

145-
/**
146-
* Precondition for cut and copy context menus. These are similar to the
147-
* ones in core but they don't check if a gesture is in progress,
148-
* because a gesture will always be in progress if the context menu
149-
* is open.
150-
*
151-
* @param scope scope on which the menu was opened.
152-
* @returns 'enabled', 'disabled', or 'hidden' as appropriate
153-
*/
154-
private cutCopyPrecondition(scope: ContextMenuRegistry.Scope): string {
126+
private cutPrecondition(scope: ContextMenuRegistry.Scope): string {
155127
const focused = scope.focusedNode;
128+
if (!focused || !isCopyable(focused)) return 'hidden';
156129

130+
const workspace = focused.workspace;
131+
if (!(workspace instanceof WorkspaceSvg)) return 'hidden';
132+
133+
if (
134+
this.oldCutShortcut?.preconditionFn &&
135+
this.oldCutShortcut.preconditionFn(workspace, scope)
136+
) {
137+
return 'enabled';
138+
}
139+
return 'disabled';
140+
}
141+
142+
private copyPrecondition(scope: ContextMenuRegistry.Scope): string {
143+
const focused = scope.focusedNode;
157144
if (!focused || !isCopyable(focused)) return 'hidden';
158145

159146
const workspace = focused.workspace;
147+
if (!(workspace instanceof WorkspaceSvg)) return 'hidden';
148+
160149
if (
161-
!workspace.isReadOnly() &&
162-
isDeletable(focused) &&
163-
focused.isDeletable() &&
164-
isDraggable(focused) &&
165-
focused.isMovable() &&
166-
!focused.workspace.isFlyout
167-
)
150+
this.oldCopyShortcut?.preconditionFn &&
151+
this.oldCopyShortcut.preconditionFn(workspace, scope)
152+
) {
168153
return 'enabled';
154+
}
155+
return 'disabled';
156+
}
157+
158+
private pastePrecondition(scope: ContextMenuRegistry.Scope): string {
159+
if (!this.copyWorkspace) return 'disabled';
169160

161+
if (
162+
this.oldPasteShortcut?.preconditionFn &&
163+
this.oldPasteShortcut.preconditionFn(this.copyWorkspace, scope)
164+
) {
165+
return 'enabled';
166+
}
170167
return 'disabled';
171168
}
172169

@@ -189,9 +186,10 @@ export class Clipboard {
189186
scope: ContextMenuRegistry.Scope,
190187
) {
191188
const didCut =
192-
!!this.oldCutCallback &&
193-
this.oldCutCallback(workspace, e, shortcut, scope);
189+
!!this.oldCutShortcut?.callback &&
190+
this.oldCutShortcut.callback(workspace, e, shortcut, scope);
194191
if (didCut) {
192+
this.copyWorkspace = workspace;
195193
showCutHint(workspace);
196194
}
197195
return didCut;
@@ -202,20 +200,18 @@ export class Clipboard {
202200
* Identical to the one in core but pops a toast after succesful copy.
203201
*/
204202
private registerCopyShortcut() {
205-
const oldCopyShortcut =
203+
this.oldCopyShortcut =
206204
ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.COPY];
207-
if (!oldCopyShortcut)
205+
if (!this.oldCopyShortcut)
208206
throw new Error('No copy keyboard shortcut registered initially');
209207

210-
this.oldCopyCallback = oldCopyShortcut.callback;
211-
212208
const copyShortcut: ShortcutRegistry.KeyboardShortcut = {
213209
name: Constants.SHORTCUT_NAMES.COPY,
214-
preconditionFn: oldCopyShortcut.preconditionFn,
210+
preconditionFn: this.oldCopyShortcut.preconditionFn,
215211
callback: this.copyCallback.bind(this),
216212
// The registry gives back keycodes as an object instead of an array
217213
// See https://github.com/google/blockly/issues/9008
218-
keyCodes: oldCopyShortcut.keyCodes,
214+
keyCodes: this.oldCopyShortcut.keyCodes,
219215
allowCollision: false,
220216
};
221217

@@ -237,7 +233,7 @@ export class Clipboard {
237233
'%1',
238234
getShortActionShortcut(Constants.SHORTCUT_NAMES.COPY),
239235
),
240-
preconditionFn: (scope) => this.cutCopyPrecondition(scope),
236+
preconditionFn: (scope) => this.copyPrecondition(scope),
241237
callback: (scope, menuOpenEvent) => {
242238
if (!isCopyable(scope.focusedNode)) return false;
243239
const ws = scope.focusedNode.workspace;
@@ -271,9 +267,10 @@ export class Clipboard {
271267
scope: ContextMenuRegistry.Scope,
272268
) {
273269
const didCopy =
274-
!!this.oldCopyCallback &&
275-
this.oldCopyCallback(workspace, e, shortcut, scope);
270+
!!this.oldCopyShortcut?.callback &&
271+
this.oldCopyShortcut.callback(workspace, e, shortcut, scope);
276272
if (didCopy) {
273+
this.copyWorkspace = workspace;
277274
showCopiedHint(workspace);
278275
}
279276
return didCopy;
@@ -284,38 +281,18 @@ export class Clipboard {
284281
* Identical to the one in core but clears any paste toasts after.
285282
*/
286283
private registerPasteShortcut() {
287-
const oldPasteShortcut =
284+
this.oldPasteShortcut =
288285
ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.PASTE];
289-
if (!oldPasteShortcut)
286+
if (!this.oldPasteShortcut)
290287
throw new Error('No paste keyboard shortcut registered initially');
291288

292-
this.oldPasteCallback = oldPasteShortcut.callback;
293-
294289
const pasteShortcut: ShortcutRegistry.KeyboardShortcut = {
295290
name: Constants.SHORTCUT_NAMES.PASTE,
296-
preconditionFn: (
297-
workspace: WorkspaceSvg,
298-
scope: ContextMenuRegistry.Scope,
299-
) => {
300-
// Don't use the workspace given as we don't want to paste in the flyout, for example
301-
const pasteWorkspace = this.getPasteWorkspace(scope);
302-
if (!pasteWorkspace || pasteWorkspace.isReadOnly()) return false;
303-
return true;
304-
},
305-
callback: (
306-
workspace: WorkspaceSvg,
307-
e: Event,
308-
shortcut: ShortcutRegistry.KeyboardShortcut,
309-
scope: ContextMenuRegistry.Scope,
310-
) => {
311-
// Don't use the workspace given as we don't want to paste in the flyout, for example
312-
const pasteWorkspace = this.getPasteWorkspace(scope);
313-
if (!pasteWorkspace) return false;
314-
return this.pasteCallback(pasteWorkspace, e, shortcut, scope);
315-
},
291+
preconditionFn: this.oldPasteShortcut.preconditionFn,
292+
callback: this.pasteCallback.bind(this),
316293
// The registry gives back keycodes as an object instead of an array
317294
// See https://github.com/google/blockly/issues/9008
318-
keyCodes: oldPasteShortcut.keyCodes,
295+
keyCodes: this.oldPasteShortcut.keyCodes,
319296
allowCollision: false,
320297
};
321298

@@ -337,17 +314,9 @@ export class Clipboard {
337314
'%1',
338315
getShortActionShortcut(Constants.SHORTCUT_NAMES.PASTE),
339316
),
340-
preconditionFn: (scope: ContextMenuRegistry.Scope) => {
341-
const workspace = this.getPasteWorkspace(scope);
342-
if (!workspace) return 'hidden';
343-
344-
// Unfortunately, this will return enabled even if nothing is in the clipboard
345-
// This is because the clipboard data is not actually exposed in core
346-
// so there's no way to check
347-
return workspace.isReadOnly() ? 'disabled' : 'enabled';
348-
},
317+
preconditionFn: (scope) => this.pastePrecondition(scope),
349318
callback: (scope: ContextMenuRegistry.Scope, menuOpenEvent: Event) => {
350-
const workspace = this.getPasteWorkspace(scope);
319+
const workspace = this.copyWorkspace;
351320
if (!workspace) return;
352321
return this.pasteCallback(workspace, menuOpenEvent, undefined, scope);
353322
},
@@ -358,37 +327,6 @@ export class Clipboard {
358327
ContextMenuRegistry.registry.register(pasteAction);
359328
}
360329

361-
/**
362-
* Gets the workspace where something should be pasted.
363-
* Tries to get the workspace the focusable item is on,
364-
* or the target workspace if the focusable item is in a flyout,
365-
* or falls back to the main workspace.
366-
*
367-
* @param scope scope from the action that initiated the paste
368-
* @returns a workspace to paste into if possible, otherwise null
369-
*/
370-
private getPasteWorkspace(scope: ContextMenuRegistry.Scope) {
371-
const focusTree = (scope.focusedNode as IFocusableNode).getFocusableTree();
372-
let workspace;
373-
if (focusTree instanceof WorkspaceSvg) {
374-
workspace = focusTree;
375-
} else if (focusTree instanceof Flyout) {
376-
// Seems like this case doesn't actually happen and a
377-
// (flyout) Workspace is returned instead, but it's possible
378-
workspace = focusTree.targetWorkspace;
379-
} else {
380-
// Give up and just paste in the main workspace
381-
workspace = getMainWorkspace() as WorkspaceSvg;
382-
}
383-
384-
if (!workspace) return null;
385-
// If we're trying to paste in a flyout, paste in the target workspace instead
386-
if (workspace.isFlyout)
387-
workspace = workspace.targetWorkspace as WorkspaceSvg;
388-
389-
return workspace;
390-
}
391-
392330
/**
393331
* The callback for the paste action. Uses the registered version of the paste callback
394332
* to perform the paste logic, then clears any toasts about pasting.
@@ -408,8 +346,8 @@ export class Clipboard {
408346
scope: ContextMenuRegistry.Scope,
409347
) {
410348
const didPaste =
411-
!!this.oldPasteCallback &&
412-
this.oldPasteCallback(workspace, e, shortcut, scope);
349+
!!this.oldPasteShortcut?.callback &&
350+
this.oldPasteShortcut.callback(workspace, e, shortcut, scope);
413351

414352
// Clear the paste hints regardless of whether something was pasted
415353
// Some implementations of paste are async and we should clear the hint

0 commit comments

Comments
 (0)