77import {
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' ;
1915import * as Constants from '../constants' ;
20- import { WorkspaceSvg } from 'blockly' ;
2116import { Navigation } from '../navigation' ;
2217import { getShortActionShortcut } from '../shortcut_formatting' ;
2318import { 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 */
3226const 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 */
4733export 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 ;
@@ -143,30 +124,67 @@ export class Clipboard {
143124 }
144125
145126 /**
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.
127+ * Precondition function for the cut context menu. This wraps the core cut
128+ * precondition to support context menus.
150129 *
151- * @param scope scope on which the menu was opened.
152- * @returns 'enabled', 'disabled', or 'hidden' as appropriate
130+ * @param scope scope of the shortcut or context menu item
131+ * @returns 'enabled' if the node can be cut , 'disabled' otherwise.
153132 */
154- private cutCopyPrecondition ( scope : ContextMenuRegistry . Scope ) : string {
133+ private cutPrecondition ( scope : ContextMenuRegistry . Scope ) : string {
155134 const focused = scope . focusedNode ;
135+ if ( ! focused || ! isCopyable ( focused ) ) return 'hidden' ;
136+
137+ const workspace = focused . workspace ;
138+ if ( ! ( workspace instanceof WorkspaceSvg ) ) return 'hidden' ;
139+
140+ if (
141+ this . oldCutShortcut ?. preconditionFn &&
142+ this . oldCutShortcut . preconditionFn ( workspace , scope )
143+ ) {
144+ return 'enabled' ;
145+ }
146+ return 'disabled' ;
147+ }
156148
149+ /**
150+ * Precondition function for the copy context menu. This wraps the core copy
151+ * precondition to support context menus.
152+ *
153+ * @param scope scope of the shortcut or context menu item
154+ * @returns 'enabled' if the node can be copied, 'disabled' otherwise.
155+ */
156+ private copyPrecondition ( scope : ContextMenuRegistry . Scope ) : string {
157+ const focused = scope . focusedNode ;
157158 if ( ! focused || ! isCopyable ( focused ) ) return 'hidden' ;
158159
159160 const workspace = focused . workspace ;
161+ if ( ! ( workspace instanceof WorkspaceSvg ) ) return 'hidden' ;
162+
160163 if (
161- ! workspace . isReadOnly ( ) &&
162- isDeletable ( focused ) &&
163- focused . isDeletable ( ) &&
164- isDraggable ( focused ) &&
165- focused . isMovable ( ) &&
166- ! focused . workspace . isFlyout
167- )
164+ this . oldCopyShortcut ?. preconditionFn &&
165+ this . oldCopyShortcut . preconditionFn ( workspace , scope )
166+ ) {
168167 return 'enabled' ;
168+ }
169+ return 'disabled' ;
170+ }
169171
172+ /**
173+ * Precondition function for the paste context menu. This wraps the core
174+ * paste precondition to support context menus.
175+ *
176+ * @param scope scope of the shortcut or context menu item
177+ * @returns 'enabled' if the node can be pasted, 'disabled' otherwise.
178+ */
179+ private pastePrecondition ( scope : ContextMenuRegistry . Scope ) : string {
180+ if ( ! this . copyWorkspace ) return 'disabled' ;
181+
182+ if (
183+ this . oldPasteShortcut ?. preconditionFn &&
184+ this . oldPasteShortcut . preconditionFn ( this . copyWorkspace , scope )
185+ ) {
186+ return 'enabled' ;
187+ }
170188 return 'disabled' ;
171189 }
172190
@@ -189,9 +207,10 @@ export class Clipboard {
189207 scope : ContextMenuRegistry . Scope ,
190208 ) {
191209 const didCut =
192- ! ! this . oldCutCallback &&
193- this . oldCutCallback ( workspace , e , shortcut , scope ) ;
210+ ! ! this . oldCutShortcut ?. callback &&
211+ this . oldCutShortcut . callback ( workspace , e , shortcut , scope ) ;
194212 if ( didCut ) {
213+ this . copyWorkspace = workspace ;
195214 showCutHint ( workspace ) ;
196215 }
197216 return didCut ;
@@ -202,20 +221,18 @@ export class Clipboard {
202221 * Identical to the one in core but pops a toast after succesful copy.
203222 */
204223 private registerCopyShortcut ( ) {
205- const oldCopyShortcut =
224+ this . oldCopyShortcut =
206225 ShortcutRegistry . registry . getRegistry ( ) [ ShortcutItems . names . COPY ] ;
207- if ( ! oldCopyShortcut )
226+ if ( ! this . oldCopyShortcut )
208227 throw new Error ( 'No copy keyboard shortcut registered initially' ) ;
209228
210- this . oldCopyCallback = oldCopyShortcut . callback ;
211-
212229 const copyShortcut : ShortcutRegistry . KeyboardShortcut = {
213230 name : Constants . SHORTCUT_NAMES . COPY ,
214- preconditionFn : oldCopyShortcut . preconditionFn ,
231+ preconditionFn : this . oldCopyShortcut . preconditionFn ,
215232 callback : this . copyCallback . bind ( this ) ,
216233 // The registry gives back keycodes as an object instead of an array
217234 // See https://github.com/google/blockly/issues/9008
218- keyCodes : oldCopyShortcut . keyCodes ,
235+ keyCodes : this . oldCopyShortcut . keyCodes ,
219236 allowCollision : false ,
220237 } ;
221238
@@ -237,7 +254,7 @@ export class Clipboard {
237254 '%1' ,
238255 getShortActionShortcut ( Constants . SHORTCUT_NAMES . COPY ) ,
239256 ) ,
240- preconditionFn : ( scope ) => this . cutCopyPrecondition ( scope ) ,
257+ preconditionFn : ( scope ) => this . copyPrecondition ( scope ) ,
241258 callback : ( scope , menuOpenEvent ) => {
242259 if ( ! isCopyable ( scope . focusedNode ) ) return false ;
243260 const ws = scope . focusedNode . workspace ;
@@ -271,9 +288,10 @@ export class Clipboard {
271288 scope : ContextMenuRegistry . Scope ,
272289 ) {
273290 const didCopy =
274- ! ! this . oldCopyCallback &&
275- this . oldCopyCallback ( workspace , e , shortcut , scope ) ;
291+ ! ! this . oldCopyShortcut ?. callback &&
292+ this . oldCopyShortcut . callback ( workspace , e , shortcut , scope ) ;
276293 if ( didCopy ) {
294+ this . copyWorkspace = workspace ;
277295 showCopiedHint ( workspace ) ;
278296 }
279297 return didCopy ;
@@ -284,38 +302,18 @@ export class Clipboard {
284302 * Identical to the one in core but clears any paste toasts after.
285303 */
286304 private registerPasteShortcut ( ) {
287- const oldPasteShortcut =
305+ this . oldPasteShortcut =
288306 ShortcutRegistry . registry . getRegistry ( ) [ ShortcutItems . names . PASTE ] ;
289- if ( ! oldPasteShortcut )
307+ if ( ! this . oldPasteShortcut )
290308 throw new Error ( 'No paste keyboard shortcut registered initially' ) ;
291309
292- this . oldPasteCallback = oldPasteShortcut . callback ;
293-
294310 const pasteShortcut : ShortcutRegistry . KeyboardShortcut = {
295311 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- } ,
312+ preconditionFn : this . oldPasteShortcut . preconditionFn ,
313+ callback : this . pasteCallback . bind ( this ) ,
316314 // The registry gives back keycodes as an object instead of an array
317315 // See https://github.com/google/blockly/issues/9008
318- keyCodes : oldPasteShortcut . keyCodes ,
316+ keyCodes : this . oldPasteShortcut . keyCodes ,
319317 allowCollision : false ,
320318 } ;
321319
@@ -337,17 +335,9 @@ export class Clipboard {
337335 '%1' ,
338336 getShortActionShortcut ( Constants . SHORTCUT_NAMES . PASTE ) ,
339337 ) ,
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- } ,
338+ preconditionFn : ( scope ) => this . pastePrecondition ( scope ) ,
349339 callback : ( scope : ContextMenuRegistry . Scope , menuOpenEvent : Event ) => {
350- const workspace = this . getPasteWorkspace ( scope ) ;
340+ const workspace = this . copyWorkspace ;
351341 if ( ! workspace ) return ;
352342 return this . pasteCallback ( workspace , menuOpenEvent , undefined , scope ) ;
353343 } ,
@@ -358,37 +348,6 @@ export class Clipboard {
358348 ContextMenuRegistry . registry . register ( pasteAction ) ;
359349 }
360350
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-
392351 /**
393352 * The callback for the paste action. Uses the registered version of the paste callback
394353 * to perform the paste logic, then clears any toasts about pasting.
@@ -408,8 +367,8 @@ export class Clipboard {
408367 scope : ContextMenuRegistry . Scope ,
409368 ) {
410369 const didPaste =
411- ! ! this . oldPasteCallback &&
412- this . oldPasteCallback ( workspace , e , shortcut , scope ) ;
370+ ! ! this . oldPasteShortcut ?. callback &&
371+ this . oldPasteShortcut . callback ( workspace , e , shortcut , scope ) ;
413372
414373 // Clear the paste hints regardless of whether something was pasted
415374 // Some implementations of paste are async and we should clear the hint
0 commit comments