-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix: Make cut/copy/paste work consistently and as expected #9107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
796e57a
a8c44c7
c0ed717
857ecee
8f7ca4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,7 +104,7 @@ let copyWorkspace: WorkspaceSvg | null = null; | |
| let copyCoords: Coordinate | null = null; | ||
|
|
||
| /** | ||
| * Determine if a focusable node can be copied using cut or copy. | ||
| * 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. | ||
|
|
@@ -113,8 +113,8 @@ let copyCoords: Coordinate | null = null; | |
| * - 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). | ||
| * - 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 | ||
|
|
@@ -127,6 +127,40 @@ let copyCoords: Coordinate | null = null; | |
| function isCopyable( | ||
| focused: IFocusableNode, | ||
| ): focused is ICopyable<ICopyData> & IDeletable & IDraggable { | ||
| if (!(focused instanceof BlockSvg)) return false; | ||
| return ( | ||
| isICopyable(focused) && | ||
| isIDeletable(focused) && | ||
| focused.isOwnDeletable() && | ||
| isDraggable(focused) && | ||
| focused.isOwnMovable() | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 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. | ||
| * | ||
| * @param focused The focused object. | ||
| */ | ||
| function isCuttable(focused: IFocusableNode): boolean { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I created So |
||
| if (!(focused instanceof BlockSvg)) return false; | ||
| return ( | ||
| isICopyable(focused) && | ||
| isIDeletable(focused) && | ||
|
|
@@ -151,29 +185,45 @@ export function registerCopy() { | |
| name: names.COPY, | ||
| preconditionFn(workspace, scope) { | ||
| const focused = scope.focusedNode; | ||
| if (!(focused instanceof BlockSvg)) return false; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This prevents anything except blocks—e.g., workspace comments—from being copied. |
||
|
|
||
| const targetWorkspace = workspace.isFlyout | ||
| ? workspace.targetWorkspace | ||
| : workspace; | ||
| return ( | ||
| !workspace.isReadOnly() && | ||
| !Gesture.inProgress() && | ||
| !!focused && | ||
| isCopyable(focused) && | ||
| !getFocusManager().ephemeralFocusTaken() | ||
| !!targetWorkspace && | ||
| !targetWorkspace.isReadOnly() && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear to me that the main workspace being readonly should prevent blocks from being copied from the flyout. (It also isn't clear to me that it should prevent blocks from being copied from the main workspace either—but that was the previous behaviour so leaving it as-is for the time being is fine.) |
||
| !targetWorkspace.isDragging() && | ||
| !getFocusManager().ephemeralFocusTaken() && | ||
| isCopyable(focused) | ||
| ); | ||
| }, | ||
| callback(workspace, e, shortcut, scope) { | ||
| // Prevent the default copy behavior, which may beep or otherwise indicate | ||
| // an error due to the lack of a selection. | ||
| e.preventDefault(); | ||
| workspace.hideChaff(); | ||
|
|
||
| const focused = scope.focusedNode; | ||
| if (!focused || !isICopyable(focused)) return false; | ||
| copyData = focused.toCopyData(); | ||
| copyWorkspace = | ||
| if (!focused || !isCopyable(focused)) return false; | ||
| let targetWorkspace: WorkspaceSvg | null = | ||
| focused.workspace instanceof WorkspaceSvg | ||
| ? focused.workspace | ||
| : workspace; | ||
| copyCoords = isDraggable(focused) | ||
| ? focused.getRelativeToSurfaceXY() | ||
| : null; | ||
| targetWorkspace = targetWorkspace.isFlyout | ||
| ? targetWorkspace.targetWorkspace | ||
| : targetWorkspace; | ||
| if (!targetWorkspace) return false; | ||
|
|
||
| if (!focused.workspace.isFlyout) { | ||
| targetWorkspace.hideChaff(); | ||
| } | ||
| copyData = focused.toCopyData(); | ||
| copyWorkspace = targetWorkspace; | ||
| copyCoords = | ||
| isDraggable(focused) && focused.workspace == targetWorkspace | ||
| ? focused.getRelativeToSurfaceXY() | ||
| : null; | ||
| return !!copyData; | ||
| }, | ||
| keyCodes: [ctrlC, metaC], | ||
|
|
@@ -197,14 +247,11 @@ export function registerCut() { | |
| preconditionFn(workspace, scope) { | ||
| const focused = scope.focusedNode; | ||
| return ( | ||
| !workspace.isReadOnly() && | ||
| !Gesture.inProgress() && | ||
| !!focused && | ||
| isCopyable(focused) && | ||
| // Extra criteria for cut (not just copy): | ||
| !focused.workspace.isFlyout && | ||
| focused.isDeletable() && | ||
| !getFocusManager().ephemeralFocusTaken() | ||
| !workspace.isReadOnly() && | ||
| !workspace.isDragging() && | ||
| !getFocusManager().ephemeralFocusTaken() && | ||
| isCuttable(focused) | ||
| ); | ||
| }, | ||
| callback(workspace, e, shortcut, scope) { | ||
|
|
@@ -251,9 +298,14 @@ export function registerPaste() { | |
| const pasteShortcut: KeyboardShortcut = { | ||
| name: names.PASTE, | ||
| preconditionFn(workspace) { | ||
| const targetWorkspace = workspace.isFlyout | ||
| ? workspace.targetWorkspace | ||
| : workspace; | ||
| return ( | ||
| !workspace.isReadOnly() && | ||
| !Gesture.inProgress() && | ||
| !!copyData && | ||
| !!targetWorkspace && | ||
| !targetWorkspace.isReadOnly() && | ||
| !targetWorkspace.isDragging() && | ||
| !getFocusManager().ephemeralFocusTaken() | ||
| ); | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change makes things that we decided we did not want to make copyable copyable.
This includes shadow blocks, but also blocks that have custom
isDeletableandisMovablemethods.While I was working on #9084 we decided we did not want to do that in v12.
I'm not necessarily averse to making some things copyable (which were not previously copyable), but It is in my opinion a mistake to have done so without introducing an
isCopyablemethod onICopyableso that block definition authors can have more fine-grained control.