Skip to content

Commit

Permalink
feat: allow overriding comment icons (#7937)
Browse files Browse the repository at this point in the history
* feat: add comment icon interface

* feat: have blocks construct comment icons from registry

* chore: add tests for setCommentText

* fix: typeguard
  • Loading branch information
BeksOmega authored Mar 15, 2024
1 parent e91dd20 commit 8821c83
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 9 deletions.
31 changes: 24 additions & 7 deletions core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ import {Input} from './inputs/input.js';
import {Align} from './inputs/align.js';
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
import type {IDeletable} from './interfaces/i_deletable.js';
import type {IIcon} from './interfaces/i_icon.js';
import {CommentIcon} from './icons/comment_icon.js';
import {type IIcon} from './interfaces/i_icon.js';
import {isCommentIcon} from './interfaces/i_comment_icon.js';
import type {MutatorIcon} from './icons/mutator_icon.js';
import * as Tooltip from './tooltip.js';
import * as arrayUtils from './utils/array.js';
Expand Down Expand Up @@ -2212,7 +2212,7 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns Block's comment.
*/
getCommentText(): string | null {
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
const comment = this.getIcon(IconType.COMMENT);
return comment?.getText() ?? null;
}

Expand All @@ -2222,19 +2222,36 @@ export class Block implements IASTNodeLocation, IDeletable {
* @param text The text, or null to delete.
*/
setCommentText(text: string | null) {
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
const comment = this.getIcon(IconType.COMMENT);
const oldText = comment?.getText() ?? null;
if (oldText === text) return;
if (text !== null) {
let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined;
let comment = this.getIcon(IconType.COMMENT);
if (!comment) {
comment = this.addIcon(new CommentIcon(this));
const commentConstructor = registry.getClass(
registry.Type.ICON,
IconType.COMMENT.toString(),
false,
);
if (!commentConstructor) {
throw new Error(
'No comment icon class is registered, so a comment cannot be set',
);
}
const icon = new commentConstructor(this);
if (!isCommentIcon(icon)) {
throw new Error(
'The class registered as a comment icon does not conform to the ' +
'ICommentIcon interface',
);
}
comment = this.addIcon(icon);
}
eventUtils.disable();
comment.setText(text);
eventUtils.enable();
} else {
this.removeIcon(CommentIcon.TYPE);
this.removeIcon(IconType.COMMENT);
}

eventUtils.fire(
Expand Down
4 changes: 2 additions & 2 deletions core/icons/icon_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {ICommentIcon} from '../interfaces/i_comment_icon.js';
import {IIcon} from '../interfaces/i_icon.js';
import {CommentIcon} from './comment_icon.js';
import {MutatorIcon} from './mutator_icon.js';
import {WarningIcon} from './warning_icon.js';

Expand All @@ -28,5 +28,5 @@ export class IconType<_T extends IIcon> {

static MUTATOR = new IconType<MutatorIcon>('mutator');
static WARNING = new IconType<WarningIcon>('warning');
static COMMENT = new IconType<CommentIcon>('comment');
static COMMENT = new IconType<ICommentIcon>('comment');
}
33 changes: 33 additions & 0 deletions core/interfaces/i_comment_icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {IconType} from '../icons.js';
import {IIcon, isIcon} from './i_icon.js';
import {Size} from '../utils/size.js';
import {IHasBubble, hasBubble} from './i_has_bubble.js';

export interface ICommentIcon extends IIcon, IHasBubble {
setText(text: string): void;

getText(): string;

setBubbleSize(size: Size): void;

getBubbleSize(): Size;
}

/** Checks whether the given object is an ICommentIcon. */
export function isCommentIcon(obj: Object): obj is ICommentIcon {
return (
isIcon(obj) &&
hasBubble(obj) &&
(obj as any)['setText'] !== undefined &&
(obj as any)['getText'] !== undefined &&
(obj as any)['setBubbleSize'] !== undefined &&
(obj as any)['getBubbleSize'] !== undefined &&
obj.getType() === IconType.COMMENT
);
}
88 changes: 88 additions & 0 deletions tests/mocha/block_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
createMockEvent,
} from './test_helpers/events.js';
import {MockIcon, MockBubbleIcon} from './test_helpers/icon_mocks.js';
import {IconType} from '../../build/src/core/icons/icon_types.js';

suite('Blocks', function () {
setup(function () {
Expand Down Expand Up @@ -1367,6 +1368,93 @@ suite('Blocks', function () {
});
});
});

suite('Constructing registered comment classes', function () {
class MockComment extends MockIcon {
getType() {
return Blockly.icons.IconType.COMMENT;
}

setText() {}

getText() {
return '';
}

setBubbleSize() {}

getBubbleSize() {
return Blockly.utils.Size(0, 0);
}

bubbleIsVisible() {
return true;
}

setBubbleVisible() {}
}

setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {});

this.block = this.workspace.newBlock('stack_block');
this.block.initSvg();
this.block.render();
});

teardown(function () {
workspaceTeardown.call(this, this.workspace);

Blockly.icons.registry.unregister(
Blockly.icons.IconType.COMMENT.toString(),
);
Blockly.icons.registry.register(
Blockly.icons.IconType.COMMENT,
Blockly.icons.CommentIcon,
);
});

test('setCommentText constructs the registered comment icon', function () {
Blockly.icons.registry.unregister(
Blockly.icons.IconType.COMMENT.toString(),
);
Blockly.icons.registry.register(
Blockly.icons.IconType.COMMENT,
MockComment,
);

this.block.setCommentText('test text');

chai.assert.instanceOf(
this.block.getIcon(Blockly.icons.IconType.COMMENT),
MockComment,
);
});

test('setCommentText throws if no icon is registered', function () {
Blockly.icons.registry.unregister(
Blockly.icons.IconType.COMMENT.toString(),
);

chai.assert.throws(() => {
this.block.setCommentText('test text');
}, 'No comment icon class is registered, so a comment cannot be set');
});

test('setCommentText throws if the icon is not an ICommentIcon', function () {
Blockly.icons.registry.unregister(
Blockly.icons.IconType.COMMENT.toString(),
);
Blockly.icons.registry.register(
Blockly.icons.IconType.COMMENT,
MockIcon,
);

chai.assert.throws(() => {
this.block.setCommentText('test text');
}, 'The class registered as a comment icon does not conform to the ICommentIcon interface');
});
});
});

suite('Getting/Setting Field (Values)', function () {
Expand Down

0 comments on commit 8821c83

Please sign in to comment.