From 9685498d219aa796a5e1a1bfafb3cf64834625e5 Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Mon, 9 Jun 2025 15:13:43 -0700 Subject: [PATCH] Add isCopyable and isCuttable as optional methods on ICopyable --- core/block_svg.ts | 10 +++ core/comments/rendered_workspace_comment.ts | 10 +++ core/interfaces/i_copyable.ts | 16 ++++- core/shortcut_items.ts | 77 ++++++++------------- 4 files changed, 62 insertions(+), 51 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 8ea26e354..501be1b59 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1721,6 +1721,16 @@ export class BlockSvg this.dragStrategy = dragStrategy; } + /** Returns whether this block is copyable or not. */ + isCopyable(): boolean { + return this.isOwnDeletable() && this.isOwnMovable(); + } + + /** Returns whether this block is cuttable or not. */ + isCuttable(): boolean { + return this.isDeletable() && this.isMovable(); + } + /** Returns whether this block is movable or not. */ override isMovable(): boolean { return this.dragStrategy.isMovable(); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 3a3d57a44..569905518 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -244,6 +244,16 @@ export class RenderedWorkspaceComment } } + /** Returns whether this comment is copyable or not */ + isCopyable(): boolean { + return this.isOwnMovable() && this.isOwnDeletable(); + } + + /** Returns whether this comment is cuttable or not */ + isCuttable(): boolean { + return this.isMovable() && this.isDeletable(); + } + /** Returns whether this comment is movable or not. */ isMovable(): boolean { return this.dragStrategy.isMovable(); diff --git a/core/interfaces/i_copyable.ts b/core/interfaces/i_copyable.ts index b653bd20a..4f5e4ab9a 100644 --- a/core/interfaces/i_copyable.ts +++ b/core/interfaces/i_copyable.ts @@ -15,6 +15,20 @@ export interface ICopyable extends ISelectable { * @returns Copy metadata. */ toCopyData(): T | null; + + /** + * Whether this instance is currently copyable. + * + * @returns True if it can currently be copied. + */ + isCopyable?(): boolean; + + /** + * Whether this instance is currently cuttable. + * + * @returns True if it can currently be cut. + */ + isCuttable?(): boolean; } export namespace ICopyable { @@ -25,7 +39,7 @@ export namespace ICopyable { export type ICopyData = ICopyable.ICopyData; -/** @returns true if the given object is copyable. */ +/** @returns true if the given object is an ICopyable. */ export function isCopyable(obj: any): obj is ICopyable { return obj.toCopyData !== undefined; } diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 161d5fceb..0da30de62 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -8,6 +8,7 @@ import {BlockSvg} from './block_svg.js'; import * as clipboard from './clipboard.js'; +import { RenderedWorkspaceComment } from './comments.js'; import * as eventUtils from './events/utils.js'; import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; @@ -106,68 +107,44 @@ let copyCoords: Coordinate | null = null; /** * Determine if a focusable node can be copied. * - * Unfortunately the ICopyable interface doesn't include an isCopyable - * method, so we must use some other criteria to make the decision. - * Specifically, - * - * - It must be an ICopyable. - * - So that a pasted copy can be manipluated and/or disposed of, it - * must be both an IDraggable and an IDeletable. - * - Additionally, both .isOwnMovable() and .isOwnDeletable() must return - * true (i.e., the copy could be moved and deleted). - * - * TODO(#9098): Revise these criteria. The latter criteria prevents - * shadow blocks from being copied; additionally, there are likely to - * be other circumstances were it is desirable to allow movable / - * copyable copies of a currently-unmovable / -copyable block to be - * made. + * This will use the isCopyable method if the node implements it, otherwise + * it will fall back to checking if the node is deletable and draggable not + * considering the workspace's edit state. * * @param focused The focused object. */ -function isCopyable( - focused: IFocusableNode, -): focused is ICopyable & IDeletable & IDraggable { - if (!(focused instanceof BlockSvg)) return false; - return ( - isICopyable(focused) && - isIDeletable(focused) && - focused.isOwnDeletable() && - isDraggable(focused) && - focused.isOwnMovable() - ); +function isCopyable(focused: IFocusableNode): boolean { + if (!isICopyable(focused) || !isIDeletable(focused) || !isDraggable(focused)) + return false; + if (focused.isCopyable !== undefined) { + return focused.isCopyable(); + } else if ( + focused instanceof BlockSvg || + focused instanceof RenderedWorkspaceComment + ) { + return focused.isOwnDeletable() && focused.isOwnMovable(); + } + // This isn't a class Blockly knows about, so fall back to the stricter + // checks for deletable and movable. + return focused.isDeletable() && focused.isMovable(); } /** * Determine if a focusable node can be cut. * - * Unfortunately the ICopyable interface doesn't include an isCuttable - * method, so we must use some other criteria to make the decision. - * Specifically, - * - * - It must be an ICopyable. - * - So that a pasted copy can be manipluated and/or disposed of, it - * must be both an IDraggable and an IDeletable. - * - Additionally, both .isMovable() and .isDeletable() must return - * true (i.e., can currently be moved and deleted). This is the main - * difference with isCopyable. - * - * TODO(#9098): Revise these criteria. The latter criteria prevents - * shadow blocks from being copied; additionally, there are likely to - * be other circumstances were it is desirable to allow movable / - * copyable copies of a currently-unmovable / -copyable block to be - * made. + * This will use the isCuttable method if the node implements it, otherwise + * it will fall back to checking if the node can be moved and deleted in its + * current workspace. * * @param focused The focused object. */ function isCuttable(focused: IFocusableNode): boolean { - if (!(focused instanceof BlockSvg)) return false; - return ( - isICopyable(focused) && - isIDeletable(focused) && - focused.isDeletable() && - isDraggable(focused) && - focused.isMovable() - ); + if (!isICopyable(focused) || !isIDeletable(focused) || !isDraggable(focused)) + return false; + if (focused.isCuttable !== undefined) { + return focused.isCuttable(); + } + return focused.isMovable() && focused.isDeletable(); } /**