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: add ability to copy and paste workspace comments #8024

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 12 additions & 0 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,18 @@ WorkspaceSvg.prototype.newBlock = function (
return new BlockSvg(this, prototypeName, opt_id);
};

Workspace.prototype.newComment = function (
id?: string,
): comments.WorkspaceComment {
return new comments.WorkspaceComment(this, id);
};

WorkspaceSvg.prototype.newComment = function (
id?: string,
): comments.RenderedWorkspaceComment {
return new comments.RenderedWorkspaceComment(this, id);
};

WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
return new Trashcan(workspace);
};
Expand Down
77 changes: 63 additions & 14 deletions core/clipboard/workspace_comment_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,87 @@ import {IPaster} from '../interfaces/i_paster.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
import * as registry from './registry.js';
import * as commentSerialiation from '../serialization/workspace_comments.js';
import * as eventUtils from '../events/utils.js';
import * as common from '../common.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';

export class WorkspaceCommentPaster
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
implements IPaster<WorkspaceCommentCopyData, RenderedWorkspaceComment>
{
static TYPE = 'workspace-comment';

paste(
copyData: WorkspaceCommentCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): WorkspaceCommentSvg {
): RenderedWorkspaceComment | null {
const state = copyData.commentState;

if (coordinate) {
state.setAttribute('x', `${coordinate.x}`);
state.setAttribute('y', `${coordinate.y}`);
} else {
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
state.setAttribute('x', `${x}`);
state.setAttribute('y', `${y}`);
state['x'] = coordinate.x;
state['y'] = coordinate.y;
}

eventUtils.disable();
let comment;
try {
comment = commentSerialiation.append(
state,
workspace,
) as RenderedWorkspaceComment;
moveCommentToNotConflict(comment);
} finally {
eventUtils.enable();
}
return WorkspaceCommentSvg.fromXmlRendered(
copyData.commentState,
workspace,

if (!comment) return null;

if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment));
}
common.setSelected(comment);
return comment;
}
}

function moveCommentToNotConflict(comment: RenderedWorkspaceComment) {
const workspace = comment.workspace;
const translateDistance = 30;
const coord = comment.getRelativeToSurfaceXY();
const offset = new Coordinate(0, 0);
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
const otherCoords = workspace
.getTopComments(false)
.filter((otherComment) => otherComment.id != comment.id)
gonfunko marked this conversation as resolved.
Show resolved Hide resolved
.map((c) => c.getRelativeToSurfaceXY());

while (
commentOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords)
) {
offset.translate(
workspace.RTL ? -translateDistance : translateDistance,
translateDistance,
);
}

comment.moveTo(Coordinate.sum(coord, offset));
}

function commentOverlapsOtherExactly(
coord: Coordinate,
otherCoords: Coordinate[],
): boolean {
return otherCoords.some(
(otherCoord) =>
Math.abs(otherCoord.x - coord.x) <= 1 &&
Math.abs(otherCoord.y - coord.y) <= 1,
);
}
gonfunko marked this conversation as resolved.
Show resolved Hide resolved

export interface WorkspaceCommentCopyData extends ICopyData {
commentState: Element;
commentState: commentSerialiation.State;
}

registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());
22 changes: 21 additions & 1 deletion core/comments/rendered_workspace_comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import * as browserEvents from '../browser_events.js';
import * as common from '../common.js';
import {ISelectable} from '../interfaces/i_selectable.js';
import {IDeletable} from '../interfaces/i_deletable.js';
import {ICopyable} from '../interfaces/i_copyable.js';
import * as commentSerialization from '../serialization/workspace_comments.js';
import {
WorkspaceCommentPaster,
WorkspaceCommentCopyData,
} from '../clipboard/workspace_comment_paster.js';

export class RenderedWorkspaceComment
extends WorkspaceComment
Expand All @@ -27,7 +33,8 @@ export class RenderedWorkspaceComment
IRenderedElement,
IDraggable,
ISelectable,
IDeletable
IDeletable,
ICopyable<WorkspaceCommentCopyData>
{
/** The class encompassing the svg elements making up the workspace comment. */
private view: CommentView;
Expand Down Expand Up @@ -219,4 +226,17 @@ export class RenderedWorkspaceComment
unselect(): void {
dom.removeClass(this.getSvgRoot(), 'blocklySelected');
}

/**
* Returns a JSON serializable representation of this comment's state that
* can be used for pasting.
*/
toCopyData(): WorkspaceCommentCopyData | null {
return {
paster: WorkspaceCommentPaster.TYPE,
commentState: commentSerialization.save(this, {
addCoordinates: true,
}),
};
}
}
9 changes: 2 additions & 7 deletions core/serialization/workspace_comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@

import {ISerializer} from '../interfaces/i_serializer.js';
import {Workspace} from '../workspace.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as priorities from './priorities.js';
import {WorkspaceComment} from '../comments/workspace_comment.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
import type {WorkspaceComment} from '../comments/workspace_comment.js';
import * as eventUtils from '../events/utils.js';
import {Coordinate} from '../utils/coordinate.js';
import * as serializationRegistry from './registry.js';
Expand Down Expand Up @@ -70,10 +68,7 @@ export function append(
const prevRecordUndo = eventUtils.getRecordUndo();
eventUtils.setRecordUndo(recordUndo);

const comment =
workspace instanceof WorkspaceSvg
? new RenderedWorkspaceComment(workspace, state.id)
: new WorkspaceComment(workspace, state.id);
const comment = workspace.newComment(state.id);

if (state.text !== undefined) comment.setText(state.text);
if (state.x !== undefined || state.y !== undefined) {
Expand Down
35 changes: 25 additions & 10 deletions core/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import * as math from './utils/math.js';
import type * as toolbox from './utils/toolbox.js';
import {VariableMap} from './variable_map.js';
import type {VariableModel} from './variable_model.js';
import type {WorkspaceComment} from './workspace_comment.js';
import type {WorkspaceComment as OldWorkspaceComment} from './workspace_comment.js';
import {WorkspaceComment} from './comments/workspace_comment.js';
import {IProcedureMap} from './interfaces/i_procedure_map.js';
import {ObservableProcedureMap} from './observable_procedure_map.js';

Expand Down Expand Up @@ -100,8 +101,8 @@ export class Workspace implements IASTNodeLocation {
connectionChecker: IConnectionChecker;

private readonly topBlocks: Block[] = [];
private readonly topComments: WorkspaceComment[] = [];
private readonly commentDB = new Map<string, WorkspaceComment>();
private readonly topComments: OldWorkspaceComment[] = [];
private readonly commentDB = new Map<string, OldWorkspaceComment>();
private readonly listeners: Function[] = [];
protected undoStack_: Abstract[] = [];
protected redoStack_: Abstract[] = [];
Expand Down Expand Up @@ -168,8 +169,8 @@ export class Workspace implements IASTNodeLocation {
* a's index.
*/
private sortObjects_(
a: Block | WorkspaceComment,
b: Block | WorkspaceComment,
a: Block | OldWorkspaceComment,
b: Block | OldWorkspaceComment,
): number {
const offset =
Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1);
Expand Down Expand Up @@ -266,7 +267,7 @@ export class Workspace implements IASTNodeLocation {
* @param comment comment to add.
* @internal
*/
addTopComment(comment: WorkspaceComment) {
addTopComment(comment: OldWorkspaceComment) {
this.topComments.push(comment);

// Note: If the comment database starts to hold block comments, this may
Expand All @@ -287,7 +288,7 @@ export class Workspace implements IASTNodeLocation {
* @param comment comment to remove.
* @internal
*/
removeTopComment(comment: WorkspaceComment) {
removeTopComment(comment: OldWorkspaceComment) {
if (!arrayUtils.removeElem(this.topComments, comment)) {
throw Error(
"Comment not present in workspace's list of top-most " + 'comments.',
Expand All @@ -306,9 +307,9 @@ export class Workspace implements IASTNodeLocation {
* @returns The top-level comment objects.
* @internal
*/
getTopComments(ordered = false): WorkspaceComment[] {
getTopComments(ordered = false): OldWorkspaceComment[] {
// Copy the topComments list.
const comments = new Array<WorkspaceComment>().concat(this.topComments);
const comments = new Array<OldWorkspaceComment>().concat(this.topComments);
if (ordered && comments.length > 1) {
comments.sort(this.sortObjects_.bind(this));
}
Expand Down Expand Up @@ -515,6 +516,20 @@ export class Workspace implements IASTNodeLocation {
'monkey-patched in by blockly.ts',
);
}

/**
* Obtain a newly created comment.
*
* @param id Optional ID. Use this ID if provided, otherwise create a new
* ID.
* @returns The created comment.
*/
newComment(id?: string): WorkspaceComment {
throw new Error(
'The implementation of newComment should be ' +
'monkey-patched in by blockly.ts',
);
}
/* eslint-enable */

/**
Expand Down Expand Up @@ -736,7 +751,7 @@ export class Workspace implements IASTNodeLocation {
* @returns The sought after comment, or null if not found.
* @internal
*/
getCommentById(id: string): WorkspaceComment | null {
getCommentById(id: string): OldWorkspaceComment | null {
return this.commentDB.get(id) ?? null;
}

Expand Down
27 changes: 21 additions & 6 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ import * as VariablesDynamic from './variables_dynamic.js';
import * as WidgetDiv from './widgetdiv.js';
import {Workspace} from './workspace.js';
import {WorkspaceAudio} from './workspace_audio.js';
import {WorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceComment as OldWorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg as OldWorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceComment} from './comments/workspace_comment.js';
import {ZoomControls} from './zoom_controls.js';
import {ContextMenuOption} from './contextmenu_registry.js';
import * as renderManagement from './render_management.js';
Expand Down Expand Up @@ -1395,6 +1396,20 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
'monkey-patched in by blockly.ts',
);
}

/**
* Obtain a newly created comment.
*
* @param id Optional ID. Use this ID if provided, otherwise create a new
* ID.
* @returns The created comment.
*/
newComment(id?: string): WorkspaceComment {
throw new Error(
'The implementation of newComment should be ' +
'monkey-patched in by blockly.ts',
);
}
/* eslint-enable */

/**
Expand Down Expand Up @@ -2128,8 +2143,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @param comment comment to add.
*/
override addTopComment(comment: WorkspaceComment) {
this.addTopBoundedElement(comment as WorkspaceCommentSvg);
override addTopComment(comment: OldWorkspaceComment) {
this.addTopBoundedElement(comment as OldWorkspaceCommentSvg);
super.addTopComment(comment);
}

Expand All @@ -2138,8 +2153,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @param comment comment to remove.
*/
override removeTopComment(comment: WorkspaceComment) {
this.removeTopBoundedElement(comment as WorkspaceCommentSvg);
override removeTopComment(comment: OldWorkspaceComment) {
this.removeTopBoundedElement(comment as OldWorkspaceCommentSvg);
super.removeTopComment(comment);
}

Expand Down