Skip to content
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

feat: merge copy paste work into develop #7379

Merged
merged 6 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {FieldLabel} from './field_label.js';
import type {Input} from './inputs/input.js';
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
import type {ICopyable} from './interfaces/i_copyable.js';
import type {IDraggable} from './interfaces/i_draggable.js';
import {IIcon} from './interfaces/i_icon.js';
import * as internalConstants from './internal_constants.js';
Expand All @@ -62,14 +62,19 @@ import type {WorkspaceSvg} from './workspace_svg.js';
import * as renderManagement from './render_management.js';
import * as deprecation from './utils/deprecation.js';
import {IconType} from './icons/icon_types.js';
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';

/**
* Class for a block's SVG representation.
* Not normally called directly, workspace.newBlock() is preferred.
*/
export class BlockSvg
extends Block
implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
implements
IASTNodeLocationSvg,
IBoundedElement,
ICopyable<BlockCopyData>,
IDraggable
{
/**
* Constant for identifying rows that are to be rendered inline.
Expand Down Expand Up @@ -823,18 +828,17 @@ export class BlockSvg
* Encode a block for copying.
*
* @returns Copy metadata, or null if the block is an insertion marker.
* @internal
*/
toCopyData(): CopyData | null {
toCopyData(): BlockCopyData | null {
if (this.isInsertionMarker_) {
return null;
}
return {
saveInfo: blocks.save(this, {
paster: BlockPaster.TYPE,
blockState: blocks.save(this, {
addCoordinates: true,
addNextBlocks: false,
}) as blocks.State,
source: this.workspace,
typeCounts: common.getBlockTypeCounts(this, true),
};
}
Expand Down
6 changes: 4 additions & 2 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.j
import {IComponent} from './interfaces/i_component.js';
import {IConnectionChecker} from './interfaces/i_connection_checker.js';
import {IContextMenu} from './interfaces/i_contextmenu.js';
import {ICopyable} from './interfaces/i_copyable.js';
import {ICopyable, isCopyable} from './interfaces/i_copyable.js';
import {IDeletable} from './interfaces/i_deletable.js';
import {IDeleteArea} from './interfaces/i_delete_area.js';
import {IDragTarget} from './interfaces/i_drag_target.js';
Expand All @@ -152,6 +152,7 @@ import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
import {IMetricsManager} from './interfaces/i_metrics_manager.js';
import {IMovable} from './interfaces/i_movable.js';
import {IObservable, isObservable} from './interfaces/i_observable.js';
import {IPaster, isPaster} from './interfaces/i_paster.js';
import {IPositionable} from './interfaces/i_positionable.js';
import {IRegistrable} from './interfaces/i_registrable.js';
import {ISelectable} from './interfaces/i_selectable.js';
Expand Down Expand Up @@ -591,7 +592,7 @@ export {IComponent};
export {IConnectionChecker};
export {IContextMenu};
export {icons};
export {ICopyable};
export {ICopyable, isCopyable};
export {IDeletable};
export {IDeleteArea};
export {IDragTarget};
Expand All @@ -606,6 +607,7 @@ export {Input};
export {inputs};
export {InsertionMarkerManager};
export {IObservable, isObservable};
export {IPaster, isPaster};
export {IPositionable};
export {IRegistrable};
export {ISelectable};
Expand Down
147 changes: 109 additions & 38 deletions core/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,78 +7,149 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.clipboard');

import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
import {BlockPaster} from './clipboard/block_paster.js';
import * as globalRegistry from './registry.js';
import {WorkspaceSvg} from './workspace_svg.js';
import * as registry from './clipboard/registry.js';
import {Coordinate} from './utils/coordinate.js';
import * as deprecation from './utils/deprecation.js';

/** Metadata about the object that is currently on the clipboard. */
let copyData: CopyData | null = null;
let stashedCopyData: ICopyData | null = null;

let stashedWorkspace: WorkspaceSvg | null = null;

/**
* Copy a block or workspace comment onto the local clipboard.
* Copy a copyable element onto the local clipboard.
*
* @param toCopy Block or Workspace Comment to be copied.
* @param toCopy The copyable element to be copied.
* @deprecated v11. Use `myCopyable.toCopyData()` instead. To be removed v12.
* @internal
*/
export function copy(toCopy: ICopyable) {
TEST_ONLY.copyInternal(toCopy);
export function copy<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
deprecation.warn(
'Blockly.clipboard.copy',
'v11',
'v12',
'myCopyable.toCopyData()',
);
return TEST_ONLY.copyInternal(toCopy);
}

/**
* Private version of copy for stubbing in tests.
*/
function copyInternal(toCopy: ICopyable) {
copyData = toCopy.toCopyData();
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
const data = toCopy.toCopyData();
stashedCopyData = data;
stashedWorkspace = (toCopy as any).workspace ?? null;
return data;
}

/**
* Paste a block or workspace comment on to the main workspace.
* Paste a pasteable element into the workspace.
*
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
* @internal
*/
export function paste(): ICopyable | null {
if (!copyData) {
return null;
}
// Pasting always pastes to the main workspace, even if the copy
// started in a flyout workspace.
let workspace = copyData.source;
if (workspace.isFlyout) {
workspace = workspace.targetWorkspace!;
}
if (
copyData.typeCounts &&
workspace.isCapacityAvailable(copyData.typeCounts)
) {
return workspace.paste(copyData.saveInfo);
export function paste<T extends ICopyData>(
copyData: T,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<T> | null;

/**
* Pastes the last copied ICopyable into the workspace.
*
* @returns the pasted thing if the paste was successful, null otherwise.
*/
export function paste(): ICopyable<ICopyData> | null;

/**
* Pastes the given data into the workspace, or the last copied ICopyable if
* no data is passed.
*
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
*/
export function paste<T extends ICopyData>(
copyData?: T,
workspace?: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<ICopyData> | null {
if (!copyData || !workspace) {
if (!stashedCopyData || !stashedWorkspace) return null;
return pasteFromData(stashedCopyData, stashedWorkspace);
}
return null;
return pasteFromData(copyData, workspace, coordinate);
}

/**
* Duplicate this block and its children, or a workspace comment.
* Paste a pasteable element into the workspace.
*
* @param toDuplicate Block or Workspace Comment to be duplicated.
* @returns The block or workspace comment that was duplicated, or null if the
* duplication failed.
* @param copyData The data to paste into the workspace.
* @param workspace The workspace to paste the data into.
* @param coordinate The location to paste the thing at.
* @returns The pasted thing if the paste was successful, null otherwise.
*/
function pasteFromData<T extends ICopyData>(
copyData: T,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): ICopyable<T> | null {
workspace = workspace.getRootWorkspace() ?? workspace;
return (globalRegistry
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
}

/**
* Duplicate this copy-paste-able element.
*
* @param toDuplicate The element to be duplicated.
* @returns The element that was duplicated, or null if the duplication failed.
* @deprecated v11. Use
* `Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)` instead.
* To be removed v12.
* @internal
*/
export function duplicate(toDuplicate: ICopyable): ICopyable | null {
export function duplicate<
U extends ICopyData,
T extends ICopyable<U> & IHasWorkspace,
>(toDuplicate: T): T | null {
deprecation.warn(
'Blockly.clipboard.duplicate',
'v11',
'v12',
'Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)',
);
return TEST_ONLY.duplicateInternal(toDuplicate);
}

/**
* Private version of duplicate for stubbing in tests.
*/
function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
const oldCopyData = copyData;
copy(toDuplicate);
const pastedThing =
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
copyData = oldCopyData;
return pastedThing;
function duplicateInternal<
U extends ICopyData,
T extends ICopyable<U> & IHasWorkspace,
>(toDuplicate: T): T | null {
const data = toDuplicate.toCopyData();
if (!data) return null;
return paste(data, toDuplicate.workspace) as T;
}

interface IHasWorkspace {
workspace: WorkspaceSvg;
}

export const TEST_ONLY = {
duplicateInternal,
copyInternal,
};

export {BlockPaster, registry};
120 changes: 120 additions & 0 deletions core/clipboard/block_paster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {BlockSvg} from '../block_svg.js';
import * as registry from './registry.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {IPaster} from '../interfaces/i_paster.js';
import {State, append} from '../serialization/blocks.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as eventUtils from '../events/utils.js';
import {config} from '../config.js';

export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
static TYPE = 'block';

paste(
copyData: BlockCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): BlockSvg | null {
if (!workspace.isCapacityAvailable(copyData.typeCounts!)) return null;

if (coordinate) {
copyData.blockState['x'] = coordinate.x;
copyData.blockState['y'] = coordinate.y;
}

eventUtils.disable();
let block;
try {
block = append(copyData.blockState, workspace) as BlockSvg;
moveBlockToNotConflict(block);
} finally {
eventUtils.enable();
}

if (!block) return block;

if (eventUtils.isEnabled() && !block.isShadow()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
}
block.select();
return block;
}
}

/**
* Moves the given block to a location where it does not: (1) overlap exactly
* with any other blocks, or (2) look like it is connected to any other blocks.
*
* Exported for testing.
*
* @param block The block to move to an unambiguous location.
* @internal
*/
export function moveBlockToNotConflict(block: BlockSvg) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already reviewed in another PR, so this doesn't have to block merging, I'm just curious. does bumpNeighbors not work for this?

This feels like really similar logic to what's happening in bumpNeighbors but you're not taking advantage of some of the methods we already have like we have a connection.neighbours method that takes in a snap radius, etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bumpNeighbors always moves whichever block has the inferior connection :/ We want to make sure we're only moving the block that's being pasted.

Wrt code reuse: we're using connection.closest instead of connection.neighbours. They both take in a snap radius and do similar things.

const workspace = block.workspace;
const snapRadius = config.snapRadius;
const coord = block.getRelativeToSurfaceXY();
const offset = new Coordinate(0, 0);
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
const otherCoords = workspace
.getAllBlocks(false)
.filter((otherBlock) => otherBlock.id != block.id)
.map((b) => b.getRelativeToSurfaceXY());

while (
blockOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords) ||
blockIsInSnapRadius(block, offset, snapRadius)
) {
if (workspace.RTL) {
offset.translate(-snapRadius, snapRadius * 2);
} else {
offset.translate(snapRadius, snapRadius * 2);
}
}

block!.moveTo(Coordinate.sum(coord, offset));
}

/**
* @returns true if the given block coordinates are less than a delta of 1 from
* any of the other coordinates.
*/
function blockOverlapsOtherExactly(
coord: Coordinate,
otherCoords: Coordinate[],
): boolean {
return otherCoords.some(
(otherCoord) =>
Math.abs(otherCoord.x - coord.x) <= 1 &&
Math.abs(otherCoord.y - coord.y) <= 1,
);
}

/**
* @returns true if the given block (when offset by the given amount) is close
* enough to any other connections (within the snap radius) that it looks
* like they could connect.
*/
function blockIsInSnapRadius(
block: BlockSvg,
offset: Coordinate,
snapRadius: number,
): boolean {
return block
.getConnections_(false)
.some((connection) => !!connection.closest(snapRadius, offset).connection);
}

export interface BlockCopyData extends ICopyData {
blockState: State;
typeCounts: {[key: string]: number};
}

registry.register(BlockPaster.TYPE, new BlockPaster());
Loading
Loading