feat: add registering and serializing icons (#7063)

* feat: add registry for icons

* feat: add serialization of custom icons

* feat: add deserialization of custom icons

* chore: fixup deserialization

* chore: export icons registry

* chore: add tests for serialization and deserialization

* chore: move mocks and helpers to the top level

* chore: fix doc error

* chore: remove accidental only
This commit is contained in:
Beka Westberg
2023-05-15 09:03:04 -07:00
committed by GitHub
parent 642455cbed
commit f2221652d2
8 changed files with 224 additions and 39 deletions

View File

@@ -122,7 +122,7 @@ import {VerticalFlyout} from './flyout_vertical.js';
import {CodeGenerator} from './generator.js';
import {Gesture} from './gesture.js';
import {Grid} from './grid.js';
import {Icon} from './icon_old.js';
import {Icon} from './icons/icon.js';
import * as icons from './icons.js';
import {inject} from './inject.js';
import {Align, Input} from './inputs/input.js';

View File

@@ -5,5 +5,6 @@
*/
import * as exceptions from './icons/exceptions.js';
import * as registry from './icons/registry.js';
export {exceptions};
export {exceptions, registry};

32
core/icons/registry.ts Normal file
View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {Block} from '../block.js';
import type {IIcon} from '../interfaces/i_icon.js';
import * as registry from '../registry.js';
/**
* Registers the given icon so that it can be deserialized.
*
* @param type The type of the icon to register. This should be the same string
* that is returned from its `getType` method.
* @param iconConstructor The icon class/constructor to register.
*/
export function register(
type: string,
iconConstructor: new (block: Block) => IIcon
) {
registry.register(registry.Type.ICON, type, iconConstructor);
}
/**
* Unregisters the icon associated with the given type.
*
* @param type The type of the icon to unregister.
*/
export function unregister(type: string) {
registry.unregister(registry.Type.ICON, type);
}

View File

@@ -13,6 +13,7 @@ import type {IBlockDragger} from './interfaces/i_block_dragger.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import type {IFlyout} from './interfaces/i_flyout.js';
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
import type {IIcon} from './interfaces/i_icon.js';
import type {Input} from './inputs/input.js';
import type {ISerializer} from './interfaces/i_serializer.js';
import type {IToolbox} from './interfaces/i_toolbox.js';
@@ -92,6 +93,9 @@ export class Type<_T> {
/** @internal */
static SERIALIZER = new Type<ISerializer>('serializer');
/** @internal */
static ICON = new Type<IIcon>('icon');
}
/**

View File

@@ -12,7 +12,9 @@ import type {BlockSvg} from '../block_svg.js';
import type {Connection} from '../connection.js';
import * as eventUtils from '../events/utils.js';
import {inputTypes} from '../inputs/input_types.js';
import {isSerializable} from '../interfaces/i_serializable.js';
import type {ISerializer} from '../interfaces/i_serializer.js';
import * as registry from '../registry.js';
import {Size} from '../utils/size.js';
import * as utilsXml from '../utils/xml.js';
import type {Workspace} from '../workspace.js';
@@ -23,6 +25,7 @@ import {
MissingBlockType,
MissingConnection,
RealChildOfShadow,
UnregisteredIcon,
} from './exceptions.js';
import * as priorities from './priorities.js';
import * as serializationRegistry from './registry.js';
@@ -113,7 +116,7 @@ export function save(
saveExtraState(block, state as AnyDuringMigration);
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
// }' is not assignable to parameter of type 'State'.
saveIcons(block, state as AnyDuringMigration);
saveIcons(block, state as AnyDuringMigration, doFullSerialization);
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
// }' is not assignable to parameter of type 'State'.
saveFields(block, state as AnyDuringMigration, doFullSerialization);
@@ -208,19 +211,30 @@ function saveExtraState(block: Block, state: State) {
*
* @param block The block to serialize the icon state of.
* @param state The state object to append to.
* @param doFullSerialization Whether or not to serialize the full state of the
* icon (rather than possibly saving a reference to some state).
*/
function saveIcons(block: Block, state: State) {
// TODO(#2105): Remove this logic and put it in the icon.
function saveIcons(block: Block, state: State, doFullSerialization: boolean) {
const icons = Object.create(null);
for (const icon of block.getIcons()) {
if (isSerializable(icon)) {
icons[icon.getType()] = icon.saveState(doFullSerialization);
}
}
// TODO(#7038): Remove this logic and put it in the comment icon.
if (block.getCommentText()) {
state['icons'] = {
'comment': {
'text': block.getCommentText(),
'pinned': block.commentModel.pinned,
'height': Math.round(block.commentModel.size.height),
'width': Math.round(block.commentModel.size.width),
},
icons['comment'] = {
'text': block.getCommentText(),
'pinned': block.commentModel.pinned,
'height': Math.round(block.commentModel.size.height),
'width': Math.round(block.commentModel.size.width),
};
}
if (Object.keys(icons).length) {
state['icons'] = icons;
}
}
/**
@@ -575,10 +589,22 @@ function tryToConnectParent(
* @param state The state object to reference.
*/
function loadIcons(block: Block, state: State) {
if (!state['icons']) {
return;
if (!state['icons']) return;
const iconTypes = Object.keys(state['icons']);
for (const iconType of iconTypes) {
// TODO(#7038): Remove this special casing of comment..
if (iconType === 'comment') continue;
const iconState = state['icons'][iconType];
const constructor = registry.getClass(registry.Type.ICON, iconType, false);
if (!constructor) throw new UnregisteredIcon(iconType, block, state);
const icon = new constructor();
block.addIcon(icon);
if (isSerializable(icon)) icon.loadState(iconState);
}
// TODO(#2105): Remove this logic and put it in the icon.
// TODO(#7038): Remove this logic and put it in the icon.
const comment = state['icons']['comment'];
if (comment) {
block.setCommentText(comment['text']);

View File

@@ -86,3 +86,20 @@ block. It is an invariant of Blockly that shadow blocks only have shadow
children`);
}
}
export class UnregisteredIcon extends DeserializationError {
/**
* @param iconType The type of the unregistered icon we are attempting to
* deserialize.
* @param block The block we are attempting to add the unregistered icon to.
* @param state The state object representing the block.
*/
constructor(iconType: string, public block: Block, public state: State) {
super(
`Cannot add an icon of type '${iconType}' to the block ` +
`${block.toDevString()}, because there is no icon registered with ` +
`type '${iconType}'. Make sure that all of your icons have been ` +
`registered.`
);
}
}

View File

@@ -7,7 +7,6 @@
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.serialization.registry');
// eslint-disable-next-line no-unused-vars
import type {ISerializer} from '../interfaces/i_serializer.js';
import * as registry from '../registry.js';