diff --git a/blocks/lists.ts b/blocks/lists.ts index 96afa5d24..fab4871d8 100644 --- a/blocks/lists.ts +++ b/blocks/lists.ts @@ -15,7 +15,7 @@ import type {Connection} from '../core/connection.js'; import type {BlockSvg} from '../core/block_svg.js'; import type {FieldDropdown} from '../core/field_dropdown.js'; import {Msg} from '../core/msg.js'; -import {Mutator} from '../core/mutator.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; import type {Workspace} from '../core/workspace.js'; import { createBlockDefinitionsFromJsonArray, @@ -130,7 +130,7 @@ const LISTS_CREATE_WITH = { this.updateShape_(); this.setOutput(true, 'Array'); this.setMutator( - new Mutator(['lists_create_with_item'], this as unknown as BlockSvg) + new MutatorIcon(['lists_create_with_item'], this as unknown as BlockSvg) ); // BUG(#6905) this.setTooltip(Msg['LISTS_CREATE_WITH_TOOLTIP']); }, @@ -232,7 +232,7 @@ const LISTS_CREATE_WITH = { this.updateShape_(); // Reconnect any child blocks. for (let i = 0; i < this.itemCount_; i++) { - Mutator.reconnect(connections[i], this, 'ADD' + i); + connections[i]?.reconnect(this, 'ADD' + i); } }, /** diff --git a/blocks/logic.js b/blocks/logic.js index ee1228d98..f1d900f0a 100644 --- a/blocks/logic.js +++ b/blocks/logic.js @@ -24,13 +24,14 @@ const {Block} = goog.requireType('Blockly.Block'); /* eslint-disable-next-line no-unused-vars */ const BlockDefinition = Object; const {Msg} = goog.require('Blockly.Msg'); -const {Mutator} = goog.require('Blockly.Mutator'); /* eslint-disable-next-line no-unused-vars */ const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection'); /* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common'); /** @suppress {extraRequire} */ +goog.require('Blockly.Mutator'); +/** @suppress {extraRequire} */ goog.require('Blockly.FieldDropdown'); /** @suppress {extraRequire} */ goog.require('Blockly.FieldLabel'); @@ -519,10 +520,10 @@ const CONTROLS_IF_MUTATOR_MIXIN = { reconnectChildBlocks_: function( valueConnections, statementConnections, elseStatementConnection) { for (let i = 1; i <= this.elseifCount_; i++) { - Mutator.reconnect(valueConnections[i], this, 'IF' + i); - Mutator.reconnect(statementConnections[i], this, 'DO' + i); + valueConnections[i]?.reconnect(this, 'IF' + i); + statementConnections[i]?.reconnect(this, 'DO' + i); } - Mutator.reconnect(elseStatementConnection, this, 'ELSE'); + elseStatementConnection?.reconnect(this, 'ELSE'); }, }; diff --git a/blocks/procedures.js b/blocks/procedures.js index eebbb4ef0..505c80736 100644 --- a/blocks/procedures.js +++ b/blocks/procedures.js @@ -30,7 +30,7 @@ const {Block} = goog.requireType('Blockly.Block'); const BlockDefinition = Object; const {config} = goog.require('Blockly.config'); const {Msg} = goog.require('Blockly.Msg'); -const {Mutator} = goog.require('Blockly.Mutator'); +const {MutatorIcon: Mutator} = goog.require('Blockly.Mutator'); const {Names} = goog.require('Blockly.Names'); /* eslint-disable-next-line no-unused-vars */ const {VariableModel} = goog.requireType('Blockly.VariableModel'); @@ -290,7 +290,7 @@ const PROCEDURE_DEF_COMMON = { if (hasStatements) { this.setStatements_(true); // Restore the stack, if one was saved. - Mutator.reconnect(this.statementConnection_, this, 'STACK'); + this.statementConnection_?.reconnect(this, 'STACK'); this.statementConnection_ = null; } else { // Save the stack, then disconnect it. @@ -388,8 +388,9 @@ const PROCEDURE_DEF_COMMON = { displayRenamedVar_: function(oldName, newName) { this.updateParams_(); // Update the mutator's variables if the mutator is open. - if (this.mutator && this.mutator.isVisible()) { - const blocks = this.mutator.workspace_.getAllBlocks(false); + const mutator = this.getIcon(Mutator.TYPE); + if (mutator && mutator.bubbleIsVisible()) { + const blocks = mutator.getWorkspace().getAllBlocks(false); for (let i = 0, block; (block = blocks[i]); i++) { if (block.type === 'procedures_mutatorarg' && Names.equals(oldName, block.getFieldValue('NAME'))) { @@ -616,7 +617,7 @@ blocks['procedures_mutatorarg'] = { */ validator_: function(varName) { const sourceBlock = this.getSourceBlock(); - const outerWs = Mutator.findParentWs(sourceBlock.workspace); + const outerWs = sourceBlock.workspace.getRootWorkspace(); varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); if (!varName) { return null; @@ -667,7 +668,7 @@ blocks['procedures_mutatorarg'] = { * @this {FieldTextInput} */ deleteIntermediateVars_: function(newText) { - const outerWs = Mutator.findParentWs(this.getSourceBlock().workspace); + const outerWs = this.getSourceBlock().workspace.getRootWorkspace(); if (!outerWs) { return; } @@ -731,8 +732,9 @@ const PROCEDURE_CALL_COMMON = { // which might reappear if a param is reattached in the mutator. const defBlock = Procedures.getDefinition(this.getProcedureCall(), this.workspace); + const mutatorIcon = defBlock && defBlock.getIcon(Mutator.TYPE); const mutatorOpen = - defBlock && defBlock.mutator && defBlock.mutator.isVisible(); + mutatorIcon && mutatorIcon.bubbleIsVisible(); if (!mutatorOpen) { this.quarkConnections_ = {}; this.quarkIds_ = null; @@ -788,7 +790,7 @@ const PROCEDURE_CALL_COMMON = { const quarkId = this.quarkIds_[i]; if (quarkId in this.quarkConnections_) { const connection = this.quarkConnections_[quarkId]; - if (!Mutator.reconnect(connection, this, 'ARG' + i)) { + if (!connection?.reconnect(this, 'ARG' + i)) { // Block no longer exists or has been attached elsewhere. delete this.quarkConnections_[quarkId]; } diff --git a/blocks/text.ts b/blocks/text.ts index c6370e577..d33a4dda4 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -18,7 +18,7 @@ import {FieldImage} from '../core/field_image.js'; import {FieldDropdown} from '../core/field_dropdown.js'; import {FieldTextInput} from '../core/field_textinput.js'; import {Msg} from '../core/msg.js'; -import {Mutator} from '../core/mutator.js'; +import {MutatorIcon} from '../core/icons/mutator_icon.js'; import type {Workspace} from '../core/workspace.js'; import { createBlockDefinitionsFromJsonArray, @@ -832,7 +832,7 @@ const JOIN_MUTATOR_MIXIN = { this.updateShape_(); // Reconnect any child blocks. for (let i = 0; i < this.itemCount_; i++) { - Mutator.reconnect(connections[i]!, this, 'ADD' + i); + connections[i]?.reconnect(this, 'ADD' + i); } }, /** @@ -892,7 +892,7 @@ const JOIN_EXTENSION = function (this: JoinMutatorBlock) { this.itemCount_ = 2; this.updateShape_(); // Configure the mutator UI. - this.setMutator(new Mutator(['text_create_join_item'], this)); + this.setMutator(new MutatorIcon(['text_create_join_item'], this)); }; // Update the tooltip of 'text_append' block to reference the variable. diff --git a/core/block.ts b/core/block.ts index d725c3499..f1c63055e 100644 --- a/core/block.ts +++ b/core/block.ts @@ -35,8 +35,8 @@ import {Align, Input} from './inputs/input.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 type {Mutator} from './mutator.js'; import {CommentIcon} from './icons/comment_icon.js'; +import type {MutatorIcon} from './icons/mutator_icon.js'; import * as Tooltip from './tooltip.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; @@ -2208,7 +2208,7 @@ export class Block implements IASTNodeLocation, IDeletable { * * @param _mutator A mutator dialog instance or null to remove. */ - setMutator(_mutator: Mutator) { + setMutator(_mutator: MutatorIcon) { // NOOP. } @@ -2242,6 +2242,7 @@ export class Block implements IASTNodeLocation, IDeletable { return this.icons.some((icon) => icon.getType() === type); } + // TODO (#7126): Make this take in a generic type. /** * @returns The icon with the given type if it exists on the block, undefined * otherwise. diff --git a/core/block_svg.ts b/core/block_svg.ts index df3384550..eff6b8009 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -45,7 +45,7 @@ import {ASTNode} from './keyboard_nav/ast_node.js'; import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; import {MarkerManager} from './marker_manager.js'; import {Msg} from './msg.js'; -import type {Mutator} from './mutator.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; import {RenderedConnection} from './rendered_connection.js'; import type {IPathObject} from './renderers/common/i_path_object.js'; import * as blocks from './serialization/blocks.js'; @@ -108,7 +108,7 @@ export class BlockSvg private warningTextDb = new Map>(); /** Block's mutator icon (if any). */ - mutator: Mutator | null = null; + mutator: MutatorIcon | null = null; /** * Block's warning icon (if any). @@ -991,28 +991,16 @@ export class BlockSvg * * @param mutator A mutator dialog instance or null to remove. */ - override setMutator(mutator: Mutator | null) { - if (this.mutator && this.mutator !== mutator) { - this.mutator.dispose(); - } - if (mutator) { - mutator.setBlock(this); - this.mutator = mutator; - mutator.createIcon(); - } - if (this.rendered) { - // Icons must force an immediate render so that bubbles can be opened - // immedately at the correct position. - this.render(); - // Adding or removing a mutator icon will cause the block to change shape. - this.bumpNeighbours(); - } + override setMutator(mutator: MutatorIcon | null) { + this.removeIcon(MutatorIcon.TYPE); + if (mutator) this.addIcon(mutator); } override addIcon(icon: T): T { super.addIcon(icon); if (icon instanceof WarningIcon) this.warning = icon; + if (icon instanceof MutatorIcon) this.mutator = icon; if (this.rendered) { icon.initView(this.createIconPointerDownListener(icon)); @@ -1044,6 +1032,7 @@ export class BlockSvg const removed = super.removeIcon(type); if (type === WarningIcon.TYPE) this.warning = null; + if (type === MutatorIcon.TYPE) this.mutator = null; if (this.rendered) { // TODO: Change this based on #7068. @@ -1056,9 +1045,7 @@ export class BlockSvg // TODO: remove this implementation after #7038, #7039, and #7040 are // resolved. override getIcons(): AnyDuringMigration[] { - const icons: AnyDuringMigration = [...this.icons]; - if (this.mutator) icons.push(this.mutator); - return icons; + return [...this.icons]; } /** diff --git a/core/blockly.ts b/core/blockly.ts index 4e98d483e..71db6e3aa 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -174,7 +174,6 @@ import {MenuItem} from './menuitem.js'; import {MetricsManager} from './metrics_manager.js'; import {Msg, setLocale} from './msg.js'; import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js'; -import {Mutator} from './mutator.js'; import {Names} from './names.js'; import {Options} from './options.js'; import * as uiPosition from './positionable_helpers.js'; @@ -438,10 +437,6 @@ WorkspaceCommentSvg.prototype.showContextMenu = function ( ContextMenu.show(e, menuOptions, this.RTL); }; -Mutator.prototype.newWorkspaceSvg = function (options: Options): WorkspaceSvg { - return new WorkspaceSvg(options); -}; - MiniWorkspaceBubble.prototype.newWorkspaceSvg = function ( options: Options ): WorkspaceSvg { @@ -621,7 +616,6 @@ export {MarkerManager}; export {Menu}; export {MenuItem}; export {MetricsManager}; -export {Mutator}; export {Msg, setLocale}; export {Names}; export {Options}; diff --git a/core/bubbles/mini_workspace_bubble.ts b/core/bubbles/mini_workspace_bubble.ts index eee0acb1e..024c78963 100644 --- a/core/bubbles/mini_workspace_bubble.ts +++ b/core/bubbles/mini_workspace_bubble.ts @@ -73,11 +73,11 @@ export class MiniWorkspaceBubble extends Bubble { flyout?.show(options.languageTree); } - this.miniWorkspace.addChangeListener(this.updateBubbleSize.bind(this)); + this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this)); this.miniWorkspace .getFlyout() ?.getWorkspace() - ?.addChangeListener(this.updateBubbleSize.bind(this)); + ?.addChangeListener(this.onWorkspaceChange.bind(this)); this.updateBubbleSize(); } @@ -132,6 +132,46 @@ export class MiniWorkspaceBubble extends Bubble { } } + private onWorkspaceChange() { + this.bumpBlocksIntoBounds(); + this.updateBubbleSize(); + } + + /** + * Bumps blocks that are above the top or outside the start-side of the + * workspace back within the workspace. + * + * Blocks that are below the bottom or outside the end-side of the workspace + * are dealt with by resizing the workspace to show them. + */ + private bumpBlocksIntoBounds() { + if (this.miniWorkspace.isDragging()) return; + + const MARGIN = 20; + + for (const block of this.miniWorkspace.getTopBlocks(false)) { + const blockXY = block.getRelativeToSurfaceXY(); + + // Bump any block that's above the top back inside. + if (blockXY.y < MARGIN) { + block.moveBy(0, MARGIN - blockXY.y); + } + // Bump any block overlapping the flyout back inside. + if (block.RTL) { + let right = -MARGIN; + const flyout = this.miniWorkspace.getFlyout(); + if (flyout) { + right -= flyout.getWidth(); + } + if (blockXY.x > right) { + block.moveBy(right - blockXY.x, 0); + } + } else if (blockXY.x < MARGIN) { + block.moveBy(MARGIN - blockXY.x, 0); + } + } + } + /** * Updates the size of this bubble to account for the size of the * mini workspace. @@ -196,6 +236,20 @@ export class MiniWorkspaceBubble extends Bubble { return new Size(width, height); } + /** Reapplies styles to all of the blocks in the mini workspace. */ + updateBlockStyles() { + for (const block of this.miniWorkspace.getAllBlocks(false)) { + block.setStyle(block.getStyleName()); + } + + const flyoutWs = this.miniWorkspace.getFlyout()?.getWorkspace(); + if (flyoutWs) { + for (const block of flyoutWs.getAllBlocks(false)) { + block.setStyle(block.getStyleName()); + } + } + } + /** * Move this bubble during a drag. * diff --git a/core/connection.ts b/core/connection.ts index 9eb4edaca..c7c9a3da6 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -333,6 +333,36 @@ export class Connection implements IASTNodeLocationWithBlock { this.createShadowBlock(true); } + /** + * Reconnects this connection to the input with the given name on the given + * block. If there is already a connection connected to that input, that + * connection is disconnected. + * + * @param block The block to connect this connection to. + * @param inputName The name of the input to connect this connection to. + * @returns True if this connection was able to connect, false otherwise. + */ + reconnect(block: Block, inputName: string): boolean { + // No need to reconnect if this connection's block is deleted. + if (this.getSourceBlock().isDeadOrDying()) return false; + + const connectionParent = block.getInput(inputName)?.connection; + const currentParent = this.targetBlock(); + if ( + (!currentParent || currentParent === block) && + connectionParent && + connectionParent.targetConnection !== this + ) { + if (connectionParent.isConnected()) { + // There's already something connected here. Get rid of it. + connectionParent.disconnect(); + } + connectionParent.connect(this); + return true; + } + return false; + } + /** * Returns the block that this connection connects to. * diff --git a/core/events/events_block_change.ts b/core/events/events_block_change.ts index ab805c11e..8641f9da1 100644 --- a/core/events/events_block_change.ts +++ b/core/events/events_block_change.ts @@ -14,6 +14,8 @@ goog.declareModuleId('Blockly.Events.BlockChange'); import type {Block} from '../block.js'; import type {BlockSvg} from '../block_svg.js'; +import {MUTATOR_TYPE} from '../icons/icon_types.js'; +import {hasBubble} from '../interfaces/i_has_bubble.js'; import * as registry from '../registry.js'; import * as utilsXml from '../utils/xml.js'; import {Workspace} from '../workspace.js'; @@ -144,10 +146,10 @@ export class BlockChange extends BlockBase { ); } // Assume the block is rendered so that then we can check. - const blockSvg = block as BlockSvg; - if (blockSvg.mutator) { + const icon = block.getIcon(MUTATOR_TYPE); + if (icon && hasBubble(icon) && icon.bubbleIsVisible()) { // Close the mutator (if open) since we don't want to update it. - blockSvg.mutator.setVisible(false); + icon.setBubbleVisible(false); } const value = forward ? this.newValue : this.oldValue; switch (this.element) { diff --git a/core/extensions.ts b/core/extensions.ts index 5b6e3fd1c..1b133f7ef 100644 --- a/core/extensions.ts +++ b/core/extensions.ts @@ -10,7 +10,7 @@ goog.declareModuleId('Blockly.Extensions'); import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import {FieldDropdown} from './field_dropdown.js'; -import {Mutator} from './mutator.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; import * as parsing from './utils/parsing.js'; /** The set of all registered extensions, keyed by extension name/id. */ @@ -89,7 +89,7 @@ export function registerMutator( // Sanity checks passed. register(name, function (this: Block) { if (hasMutatorDialog) { - this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg)); + this.setMutator(new MutatorIcon(opt_blockList || [], this as BlockSvg)); } // Mixin the object. this.mixin(mixinObj); diff --git a/core/icons.ts b/core/icons.ts index 34b87e7e0..dc4af9de7 100644 --- a/core/icons.ts +++ b/core/icons.ts @@ -7,5 +7,6 @@ import {CommentIcon} from './icons/comment_icon.js'; import * as exceptions from './icons/exceptions.js'; import * as registry from './icons/registry.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; -export {CommentIcon, exceptions, registry}; +export {CommentIcon, exceptions, registry, MutatorIcon}; diff --git a/core/icons/icon_types.ts b/core/icons/icon_types.ts index b15af1150..1524f803d 100644 --- a/core/icons/icon_types.ts +++ b/core/icons/icon_types.ts @@ -4,11 +4,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** The type for a mutator icon. Used for registration and access. */ +/** + * The type for a mutator icon. Used for registration and access. + * + * @internal + */ export const MUTATOR_TYPE = 'mutator'; -/** The type for a warning icon. Used for registration and access. */ +/** + * The type for a warning icon. Used for registration and access. + * + * @internal + */ export const WARNING_TYPE = 'warning'; -/** The type for a warning icon. Used for registration and access. */ +/** + * The type for a comment icon. Used for registration and access. + * + * @internal + */ export const COMMENT_TYPE = 'comment'; diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts new file mode 100644 index 000000000..32039035f --- /dev/null +++ b/core/icons/mutator_icon.ts @@ -0,0 +1,350 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('Blockly.Mutator'); + +import type {Abstract} from '../events/events_abstract.js'; +import type {Block} from '../block.js'; +import {BlockChange} from '../events/events_block_change.js'; +import type {BlocklyOptions} from '../blockly_options.js'; +import type {BlockSvg} from '../block_svg.js'; +import type {Connection} from '../connection.js'; +import {Coordinate} from '../utils/coordinate.js'; +import * as dom from '../utils/dom.js'; +import * as eventUtils from '../events/utils.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import {Icon} from './icon.js'; +import {MiniWorkspaceBubble} from '../bubbles/mini_workspace_bubble.js'; +import {MUTATOR_TYPE} from './icon_types.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import * as deprecation from '../utils/deprecation.js'; + +/** The size of the mutator icon in workspace-scale units. */ +const SIZE = 17; + +/** + * The distance between the root block in the mini workspace and that + * workspace's edges. + */ +const WORKSPACE_MARGIN = 16; + +export class MutatorIcon extends Icon implements IHasBubble { + /** The type string used to identify this icon. */ + static readonly TYPE = MUTATOR_TYPE; + + /** + * The weight this icon has relative to other icons. Icons with more positive + * weight values are rendered farther toward the end of the block. + */ + static readonly WEIGHT = 1; + + /** The bubble used to show the mini workspace to the user. */ + private miniWorkspaceBubble: MiniWorkspaceBubble | null = null; + + /** The root block in the mini workspace. */ + private rootBlock: BlockSvg | null = null; + + /** The PID tracking updating the workkspace in response to user events. */ + private updateWorkspacePid: ReturnType | null = null; + + constructor( + private readonly flyoutBlockTypes: string[], + protected readonly sourceBlock: BlockSvg + ) { + super(sourceBlock); + } + + override getType() { + return MutatorIcon.TYPE; + } + + override initView(pointerdownListener: (e: PointerEvent) => void): void { + if (this.svgRoot) return; // Already initialized. + + super.initView(pointerdownListener); + + // Square with rounded corners. + dom.createSvgElement( + Svg.RECT, + { + 'class': 'blocklyIconShape', + 'rx': '4', + 'ry': '4', + 'height': '16', + 'width': '16', + }, + this.svgRoot + ); + // Gear teeth. + dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyIconSymbol', + 'd': + 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,' + + '0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,' + + '-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,' + + '-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,' + + '-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 ' + + '-0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,' + + '0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z', + }, + this.svgRoot + ); + // Axle hole. + dom.createSvgElement( + Svg.CIRCLE, + {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, + this.svgRoot + ); + } + + override dispose(): void { + super.dispose(); + this.miniWorkspaceBubble?.dispose(); + } + + override getWeight(): number { + return MutatorIcon.WEIGHT; + } + + override getSize(): Size { + return new Size(SIZE, SIZE); + } + + override applyColour(): void { + super.applyColour(); + this.miniWorkspaceBubble?.setColour(this.sourceBlock.style.colourPrimary); + this.miniWorkspaceBubble?.updateBlockStyles(); + } + + override updateCollapsed(): void { + super.updateCollapsed(); + if (this.sourceBlock.isCollapsed()) this.setBubbleVisible(false); + } + + override onLocationChange(blockOrigin: Coordinate): void { + super.onLocationChange(blockOrigin); + this.miniWorkspaceBubble?.setAnchorLocation(this.getAnchorLocation()); + } + + override onClick(): void { + super.onClick(); + this.setBubbleVisible(!this.bubbleIsVisible()); + } + + bubbleIsVisible(): boolean { + return !!this.miniWorkspaceBubble; + } + + setBubbleVisible(visible: boolean): void { + if (this.bubbleIsVisible() === visible) return; + + if (visible) { + this.miniWorkspaceBubble = new MiniWorkspaceBubble( + this.getMiniWorkspaceConfig(), + this.sourceBlock.workspace, + this.getAnchorLocation(), + this.getBubbleOwnerRect() + ); + this.applyColour(); + this.createRootBlock(); + this.addSaveConnectionsListener(); + this.miniWorkspaceBubble?.addWorkspaceChangeListener( + this.createMiniWorkspaceChangeListener() + ); + } else { + this.miniWorkspaceBubble?.dispose(); + this.miniWorkspaceBubble = null; + } + + eventUtils.fire( + new (eventUtils.get(eventUtils.BUBBLE_OPEN))( + this.sourceBlock, + visible, + 'mutator' + ) + ); + } + + /** @returns the configuration the mini workspace should have. */ + private getMiniWorkspaceConfig() { + const options: BlocklyOptions = { + 'disable': false, + 'media': this.sourceBlock.workspace.options.pathToMedia, + 'rtl': this.sourceBlock.RTL, + 'renderer': this.sourceBlock.workspace.options.renderer, + 'rendererOverrides': + this.sourceBlock.workspace.options.rendererOverrides ?? undefined, + }; + + if (this.flyoutBlockTypes.length) { + options.toolbox = { + 'kind': 'flyoutToolbox', + 'contents': this.flyoutBlockTypes.map((type) => ({ + 'kind': 'block', + 'type': type, + })), + }; + } + + return options; + } + + /** + * @returns the location the bubble should be anchored to. + * I.E. the middle of this icon. + */ + private getAnchorLocation(): Coordinate { + const midIcon = SIZE / 2; + return Coordinate.sum( + this.workspaceLocation, + new Coordinate(midIcon, midIcon) + ); + } + + /** + * @returns the rect the bubble should avoid overlapping. + * I.E. the block that owns this icon. + */ + private getBubbleOwnerRect(): Rect { + const bbox = this.sourceBlock.getSvgRoot().getBBox(); + return new Rect(bbox.y, bbox.y + bbox.height, bbox.x, bbox.x + bbox.width); + } + + /** Decomposes the source block to create blocks in the mini workspace. */ + private createRootBlock() { + this.rootBlock = this.sourceBlock.decompose!( + this.miniWorkspaceBubble!.getWorkspace() + )!; + + for (const child of this.rootBlock.getDescendants(false)) { + child.queueRender(); + } + + this.rootBlock.setMovable(false); + this.rootBlock.setDeletable(false); + + const flyoutWidth = + this.miniWorkspaceBubble?.getWorkspace()?.getFlyout()?.getWidth() ?? 0; + this.rootBlock.moveBy( + this.rootBlock.RTL ? -(flyoutWidth + WORKSPACE_MARGIN) : WORKSPACE_MARGIN, + WORKSPACE_MARGIN + ); + } + + /** Adds a listen to the source block that triggers saving connections. */ + private addSaveConnectionsListener() { + if (!this.sourceBlock.saveConnections || !this.rootBlock) return; + const saveConnectionsListener = () => { + if (!this.sourceBlock.saveConnections || !this.rootBlock) return; + this.sourceBlock.saveConnections(this.rootBlock); + }; + saveConnectionsListener(); + this.sourceBlock.workspace.addChangeListener(saveConnectionsListener); + } + + /** + * Creates a change listener to add to the mini workspace which recomposes + * the block. + */ + private createMiniWorkspaceChangeListener() { + return (e: Abstract) => { + if (!MutatorIcon.isIgnorableMutatorEvent(e) && !this.updateWorkspacePid) { + this.updateWorkspacePid = setTimeout(() => { + this.updateWorkspacePid = null; + this.recomposeSourceBlock(); + }, 0); + } + }; + } + + /** + * Returns true if the given event is not one the mutator needs to + * care about. + * + * @internal + */ + static isIgnorableMutatorEvent(e: Abstract) { + return ( + e.isUiEvent || + e.type === eventUtils.CREATE || + (e.type === eventUtils.CHANGE && + (e as BlockChange).element === 'disabled') + ); + } + + /** Recomposes the source block based on changes to the mini workspace. */ + private recomposeSourceBlock() { + if (!this.rootBlock) return; + + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) eventUtils.setGroup(true); + + const oldExtraState = BlockChange.getExtraBlockState_(this.sourceBlock); + this.sourceBlock.compose!(this.rootBlock); + const newExtraState = BlockChange.getExtraBlockState_(this.sourceBlock); + + if (oldExtraState !== newExtraState) { + eventUtils.fire( + new (eventUtils.get(eventUtils.BLOCK_CHANGE))( + this.sourceBlock, + 'mutation', + null, + oldExtraState, + newExtraState + ) + ); + } + + eventUtils.setGroup(existingGroup); + } + + /** @internal */ + getWorkspace(): WorkspaceSvg | undefined { + return this.miniWorkspaceBubble?.getWorkspace(); + } + + /** + * Reconnects the given connection to the mutated input on the given block. + * + * @deprecated Use connection.reconnect instead. To be removed in v11. + */ + static reconnect( + connectionChild: Connection | null, + block: Block, + inputName: string + ): boolean { + deprecation.warn( + 'MutatorIcon.reconnect', + 'v10', + 'v11', + 'connection.reconnect' + ); + if (!connectionChild) return false; + return connectionChild.reconnect(block, inputName); + } + + /** + * Returns the parent workspace of a workspace that is inside a mini workspace + * bubble, taking into account whether the workspace is a flyout. + * + * @deprecated Use workspace.getRootWorkspace. To be removed in v11. + */ + static findParentWs(workspace: WorkspaceSvg): WorkspaceSvg | null { + deprecation.warn( + 'MutatorIcon.findParentWs', + 'v10', + 'v11', + 'workspace.getRootWorkspace' + ); + return workspace.getRootWorkspace(); + } +} diff --git a/core/mutator.ts b/core/mutator.ts deleted file mode 100644 index bc5284de8..000000000 --- a/core/mutator.ts +++ /dev/null @@ -1,595 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Object representing a mutator dialog. A mutator allows the - * user to change the shape of a block using a nested blocks editor. - * - * @class - */ -import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Mutator'); - -// Unused import preserved for side-effects. Remove if unneeded. -import './events/events_bubble_open.js'; - -import type {Block} from './block.js'; -import type {BlockSvg} from './block_svg.js'; -import type {BlocklyOptions} from './blockly_options.js'; -import {Bubble} from './bubble_old.js'; -import {config} from './config.js'; -import type {Connection} from './connection.js'; -import type {Abstract} from './events/events_abstract.js'; -import {BlockChange} from './events/events_block_change.js'; -import * as eventUtils from './events/utils.js'; -import {Icon} from './icon_old.js'; -import {Options} from './options.js'; -import type {Coordinate} from './utils/coordinate.js'; -import * as dom from './utils/dom.js'; -import {Svg} from './utils/svg.js'; -import * as toolbox from './utils/toolbox.js'; -import * as xml from './utils/xml.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; - -/** - * Class for a mutator dialog. - */ -export class Mutator extends Icon { - private quarkNames: string[]; - - /** - * Workspace in the mutator's bubble. - * Due to legacy code in procedure block definitions, this name - * cannot change. - */ - private workspace_: WorkspaceSvg | null = null; - - /** Width of workspace. */ - private workspaceWidth = 0; - - /** Height of workspace. */ - private workspaceHeight = 0; - - /** - * The SVG element that is the parent of the mutator workspace, or null if - * not created. - */ - private svgDialog: SVGSVGElement | null = null; - - /** - * The root block of the mutator workspace, created by decomposing the - * source block. - */ - private rootBlock: BlockSvg | null = null; - - /** - * Function registered on the main workspace to update the mutator contents - * when the main workspace changes. - */ - private sourceListener: (() => void) | null = null; - - /** - * The PID associated with the updateWorkpace_ timeout, or null if no timeout - * is currently running. - */ - private updateWorkspacePid: ReturnType | null = null; - - /** @param quarkNames List of names of sub-blocks for flyout. */ - constructor(quarkNames: string[], block: BlockSvg) { - super(block); - this.quarkNames = quarkNames; - } - - /** - * Set the block this mutator is associated with. - * - * @param block The block associated with this mutator. - * @internal - */ - setBlock(block: BlockSvg) { - this.block_ = block; - } - - /** - * Returns the workspace inside this mutator icon's bubble. - * - * @returns The workspace inside this mutator icon's bubble or null if the - * mutator isn't open. - * @internal - */ - getWorkspace(): WorkspaceSvg | null { - return this.workspace_; - } - - /** - * Draw the mutator icon. - * - * @param group The icon group. - */ - protected override drawIcon_(group: Element) { - // Square with rounded corners. - dom.createSvgElement( - Svg.RECT, - { - 'class': 'blocklyIconShape', - 'rx': '4', - 'ry': '4', - 'height': '16', - 'width': '16', - }, - group - ); - // Gear teeth. - dom.createSvgElement( - Svg.PATH, - { - 'class': 'blocklyIconSymbol', - 'd': - 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,' + - '0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,' + - '-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,' + - '-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,' + - '-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 ' + - '-0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,' + - '0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z', - }, - group - ); - // Axle hole. - dom.createSvgElement( - Svg.CIRCLE, - {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, - group - ); - } - - /** - * Clicking on the icon toggles if the mutator bubble is visible. - * Disable if block is uneditable. - * - * @param e Mouse click event. - */ - protected override iconClick_(e: PointerEvent) { - if (this.getBlock().isEditable()) { - super.iconClick_(e); - } - } - - /** - * Create the editor for the mutator's bubble. - * - * @returns The top-level node of the editor. - */ - private createEditor(): SVGSVGElement { - /* Create the editor. Here's the markup that will be generated: - - [Workspace] - - */ - this.svgDialog = dom.createSvgElement(Svg.SVG, { - 'x': Bubble.BORDER_WIDTH, - 'y': Bubble.BORDER_WIDTH, - }); - // Convert the list of names into a list of XML objects for the flyout. - let quarkXml; - if (this.quarkNames.length) { - quarkXml = xml.createElement('xml'); - for (let i = 0, quarkName; (quarkName = this.quarkNames[i]); i++) { - const element = xml.createElement('block'); - element.setAttribute('type', quarkName); - quarkXml.appendChild(element); - } - } else { - quarkXml = null; - } - const block = this.getBlock(); - const workspaceOptions = new Options({ - // If you want to enable disabling, also remove the - // event filter from workspaceChanged_ . - 'disable': false, - 'parentWorkspace': block.workspace, - 'media': block.workspace.options.pathToMedia, - 'rtl': block.RTL, - 'horizontalLayout': false, - 'renderer': block.workspace.options.renderer, - 'rendererOverrides': block.workspace.options.rendererOverrides, - } as BlocklyOptions); - workspaceOptions.toolboxPosition = block.RTL - ? toolbox.Position.RIGHT - : toolbox.Position.LEFT; - const hasFlyout = !!quarkXml; - if (hasFlyout) { - workspaceOptions.languageTree = toolbox.convertToolboxDefToJson(quarkXml); - } - this.workspace_ = this.newWorkspaceSvg(workspaceOptions); - this.workspace_.internalIsMutator = true; - this.workspace_.addChangeListener(eventUtils.disableOrphans); - - // Mutator flyouts go inside the mutator workspace's rather than in - // a top level SVG. Instead of handling scale themselves, mutators - // inherit scale from the parent workspace. - // To fix this, scale needs to be applied at a different level in the DOM. - const flyoutSvg = hasFlyout ? this.workspace_.addFlyout(Svg.G) : null; - const background = this.workspace_.createDom('blocklyMutatorBackground'); - - if (flyoutSvg) { - // Insert the flyout after the but before the block canvas so that - // the flyout is underneath in z-order. This makes blocks layering during - // dragging work properly. - background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_); - } - this.svgDialog.appendChild(background); - - return this.svgDialog; - } - - /** - * @internal - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - newWorkspaceSvg(options: Options): WorkspaceSvg { - throw new Error( - 'The implementation of newWorkspaceSvg should be ' + - 'monkey-patched in by blockly.ts' - ); - } - - /** Add or remove the UI indicating if this icon may be clicked or not. */ - override updateEditable() { - super.updateEditable(); - if (!this.getBlock().isInFlyout) { - if (this.getBlock().isEditable()) { - if (this.iconGroup_) { - dom.removeClass(this.iconGroup_, 'blocklyIconGroupReadonly'); - } - } else { - // Close any mutator bubble. Icon is not clickable. - this.setVisible(false); - if (this.iconGroup_) { - dom.addClass(this.iconGroup_, 'blocklyIconGroupReadonly'); - } - } - } - } - - /** Resize the bubble to match the size of the workspace. */ - private resizeBubble() { - // If the bubble exists, the workspace also exists. - if (!this.workspace_) { - return; - } - const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH; - const canvas = this.workspace_.getCanvas(); - const workspaceSize = canvas.getBBox(); - let width = workspaceSize.width + workspaceSize.x; - let height = workspaceSize.height + doubleBorderWidth * 3; - const flyout = this.workspace_.getFlyout(); - if (flyout) { - const flyoutScrollMetrics = flyout - .getWorkspace() - .getMetricsManager() - .getScrollMetrics(); - height = Math.max(height, flyoutScrollMetrics.height + 20); - width += flyout.getWidth(); - } - const isRtl = this.getBlock().RTL; - if (isRtl) { - width = -workspaceSize.x; - } - width += doubleBorderWidth * 3; - // Only resize if the size difference is significant. Eliminates - // shuddering. - if ( - Math.abs(this.workspaceWidth - width) > doubleBorderWidth || - Math.abs(this.workspaceHeight - height) > doubleBorderWidth - ) { - // Record some layout information for workspace metrics. - this.workspaceWidth = width; - this.workspaceHeight = height; - // Resize the bubble. - this.bubble_!.setBubbleSize( - width + doubleBorderWidth, - height + doubleBorderWidth - ); - this.svgDialog!.setAttribute('width', `${width}`); - this.svgDialog!.setAttribute('height', `${height}`); - this.workspace_.setCachedParentSvgSize(width, height); - } - - if (isRtl) { - // Scroll the workspace to always left-align. - canvas.setAttribute('transform', `translate(${this.workspaceWidth}, 0)`); - } - this.workspace_.resize(); - } - - /** A method handler for when the bubble is moved. */ - private onBubbleMove() { - if (this.workspace_) { - this.workspace_.recordDragTargets(); - } - } - - /** - * Show or hide the mutator bubble. - * - * @param visible True if the bubble should be visible. - */ - override setVisible(visible: boolean) { - if (visible === this.isVisible()) { - // No change. - return; - } - const block = this.getBlock(); - if (visible) { - // Create the bubble. - this.bubble_ = new Bubble( - block.workspace, - this.createEditor(), - block.pathObject.svgPath, - this.iconXY_ as Coordinate, - null, - null - ); - // The workspace was created in createEditor. - const ws = this.workspace_!; - // Expose this mutator's block's ID on its top-level SVG group. - this.bubble_.setSvgId(block.id); - this.bubble_.registerMoveEvent(this.onBubbleMove.bind(this)); - const tree = ws.options.languageTree; - const flyout = ws.getFlyout(); - if (tree) { - flyout!.init(ws); - flyout!.show(tree); - } - - this.rootBlock = block.decompose!(ws)!; - const blocks = this.rootBlock.getDescendants(false); - for (let i = 0, child; (child = blocks[i]); i++) { - child.queueRender(); - } - // The root block should not be draggable or deletable. - this.rootBlock.setMovable(false); - this.rootBlock.setDeletable(false); - let margin; - let x; - if (flyout) { - margin = flyout.CORNER_RADIUS * 2; - x = this.rootBlock.RTL ? flyout.getWidth() + margin : margin; - } else { - margin = 16; - x = margin; - } - if (block.RTL) { - x = -x; - } - this.rootBlock.moveBy(x, margin); - // Save the initial connections, then listen for further changes. - if (block.saveConnections) { - const thisRootBlock = this.rootBlock; - block.saveConnections(thisRootBlock); - this.sourceListener = () => { - const currentBlock = this.getBlock(); - if (currentBlock.saveConnections) { - currentBlock.saveConnections(thisRootBlock); - } - }; - block.workspace.addChangeListener(this.sourceListener); - } - this.resizeBubble(); - // When the mutator's workspace changes, update the source block. - const boundListener = this.workspaceChanged.bind(this); - ws.addChangeListener(boundListener); - if (flyout) flyout.getWorkspace().addChangeListener(boundListener); - // Update the source block immediately after the bubble becomes visible. - this.updateWorkspace(); - this.applyColour(); - } else { - // Dispose of the bubble. - this.svgDialog = null; - this.workspace_!.dispose(); - this.workspace_ = null; - this.rootBlock = null; - this.bubble_?.dispose(); - this.bubble_ = null; - this.workspaceWidth = 0; - this.workspaceHeight = 0; - if (this.sourceListener) { - block.workspace.removeChangeListener(this.sourceListener); - this.sourceListener = null; - } - } - eventUtils.fire( - new (eventUtils.get(eventUtils.BUBBLE_OPEN))(block, visible, 'mutator') - ); - } - - /** - * Fired whenever a change is made to the mutator's workspace. - * - * @param e Custom data for event. - */ - private workspaceChanged(e: Abstract) { - if (!this.shouldIgnoreMutatorEvent_(e) && !this.updateWorkspacePid) { - this.updateWorkspacePid = setTimeout(() => { - this.updateWorkspacePid = null; - this.updateWorkspace(); - }, 0); - } - } - - /** - * Returns whether the given event in the mutator workspace should be ignored - * when deciding whether to update the workspace and compose the block or not. - * - * @param e The event. - * @returns Whether to ignore the event or not. - */ - shouldIgnoreMutatorEvent_(e: Abstract) { - return ( - e.isUiEvent || - e.type === eventUtils.CREATE || - (e.type === eventUtils.CHANGE && - (e as BlockChange).element === 'disabled') - ); - } - - /** - * Updates the source block when the mutator's blocks are changed. - * Bump down any block that's too high. - */ - private updateWorkspace() { - if (!this.workspace_!.isDragging()) { - const blocks = this.workspace_!.getTopBlocks(false); - const MARGIN = 20; - - for (let b = 0, block; (block = blocks[b]); b++) { - const blockXY = block.getRelativeToSurfaceXY(); - - // Bump any block that's above the top back inside. - if (blockXY.y < MARGIN) { - block.moveBy(0, MARGIN - blockXY.y); - } - // Bump any block overlapping the flyout back inside. - if (block.RTL) { - let right = -MARGIN; - const flyout = this.workspace_!.getFlyout(); - if (flyout) { - right -= flyout.getWidth(); - } - if (blockXY.x > right) { - block.moveBy(right - blockXY.x, 0); - } - } else if (blockXY.x < MARGIN) { - block.moveBy(MARGIN - blockXY.x, 0); - } - } - } - - // When the mutator's workspace changes, update the source block. - if (this.rootBlock && this.rootBlock.workspace === this.workspace_) { - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); - } - const block = this.getBlock(); - const oldExtraState = BlockChange.getExtraBlockState_(block); - - block.compose!(this.rootBlock); - - const newExtraState = BlockChange.getExtraBlockState_(block); - if (oldExtraState !== newExtraState) { - eventUtils.fire( - new (eventUtils.get(eventUtils.BLOCK_CHANGE))( - block, - 'mutation', - null, - oldExtraState, - newExtraState - ) - ); - // Ensure that any bump is part of this mutation's event group. - const mutationGroup = eventUtils.getGroup(); - setTimeout(function () { - const oldGroup = eventUtils.getGroup(); - eventUtils.setGroup(mutationGroup); - block.bumpNeighbours(); - eventUtils.setGroup(oldGroup); - }, config.bumpDelay); - } - - // Don't update the bubble until the drag has ended, to avoid moving - // blocks under the cursor. - if (!this.workspace_!.isDragging()) { - setTimeout(() => this.resizeBubble(), 0); - } - eventUtils.setGroup(existingGroup); - } - } - - /** Dispose of this mutator. */ - override dispose() { - this.getBlock().mutator = null; - super.dispose(); - } - - /** Update the styles on all blocks in the mutator. */ - updateBlockStyle() { - const ws = this.workspace_; - - if (ws && ws.getAllBlocks(false)) { - const workspaceBlocks = ws.getAllBlocks(false); - for (let i = 0, block; (block = workspaceBlocks[i]); i++) { - block.setStyle(block.getStyleName()); - } - - const flyout = ws.getFlyout(); - if (flyout) { - const flyoutBlocks = flyout.getWorkspace().getAllBlocks(false); - for (let i = 0, block; (block = flyoutBlocks[i]); i++) { - block.setStyle(block.getStyleName()); - } - } - } - } - - /** - * Reconnect an block to a mutated input. - * - * @param connectionChild Connection on child block. - * @param block Parent block. - * @param inputName Name of input on parent block. - * @returns True iff a reconnection was made, false otherwise. - */ - static reconnect( - connectionChild: Connection, - block: Block, - inputName: string - ): boolean { - if (!connectionChild || !connectionChild.getSourceBlock().workspace) { - return false; // No connection or block has been deleted. - } - const connectionParent = block.getInput(inputName)!.connection; - const currentParent = connectionChild.targetBlock(); - if ( - (!currentParent || currentParent === block) && - connectionParent && - connectionParent.targetConnection !== connectionChild - ) { - if (connectionParent.isConnected()) { - // There's already something connected here. Get rid of it. - connectionParent.disconnect(); - } - connectionParent.connect(connectionChild); - return true; - } - return false; - } - - /** - * Get the parent workspace of a workspace that is inside a mutator, taking - * into account whether it is a flyout. - * - * @param workspace The workspace that is inside a mutator. - * @returns The mutator's parent workspace or null. - */ - static findParentWs(workspace: WorkspaceSvg): WorkspaceSvg | null { - let outerWs = null; - if (workspace && workspace.options) { - const parent = workspace.options.parentWorkspace; - // If we were in a flyout in a mutator, need to go up two levels to find - // the actual parent. - if (workspace.isFlyout) { - if (parent && parent.options) { - outerWs = parent.options.parentWorkspace; - } - } else if (parent) { - outerWs = parent; - } - } - return outerWs; - } -} diff --git a/core/procedures.ts b/core/procedures.ts index dd1fa7df9..f352527ed 100644 --- a/core/procedures.ts +++ b/core/procedures.ts @@ -38,6 +38,7 @@ import * as utilsXml from './utils/xml.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +import {MutatorIcon} from './icons.js'; /** * String for use in the "custom" attribute of a category in toolbox XML. @@ -372,7 +373,9 @@ export function mutatorOpenListener(e: Abstract) { if (type !== 'procedures_defnoreturn' && type !== 'procedures_defreturn') { return; } - const workspace = block.mutator!.getWorkspace() as WorkspaceSvg; + const workspace = ( + block.getIcon(MutatorIcon.TYPE) as MutatorIcon + ).getWorkspace()!; updateMutatorFlyout(workspace); workspace.addChangeListener(mutatorChangeListener); } diff --git a/core/workspace.ts b/core/workspace.ts index b57498f98..f184ce6a8 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -804,6 +804,28 @@ export class Workspace implements IASTNodeLocation { return this.procedureMap; } + /** + * Returns the root workspace of this workspace if the workspace has + * parent(s). + * + * E.g. workspaces in flyouts and mini workspace bubbles have parent + * workspaces. + */ + getRootWorkspace(): Workspace | null { + let outerWs = null; + const parent = this.options.parentWorkspace; + // If we were in a flyout in a mutator, need to go up two levels to find + // the actual parent. + if (this.isFlyout) { + if (parent && parent.options) { + outerWs = parent.options.parentWorkspace; + } + } else if (parent) { + outerWs = parent; + } + return outerWs; + } + /** * Find the workspace with the specified ID. * diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index fd6b55af7..443249c8f 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -570,9 +570,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { if (blockStyleName) { const blockSvg = block as BlockSvg; blockSvg.setStyle(blockStyleName); - if (blockSvg.mutator) { - blockSvg.mutator.updateBlockStyle(); - } } } } @@ -2295,6 +2292,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { super.removeTopComment(comment); } + override getRootWorkspace(): WorkspaceSvg | null { + return super.getRootWorkspace() as WorkspaceSvg | null; + } + /** * Adds a bounded element to the list of top bounded elements. * diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index e05d55602..8818a74b9 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -336,7 +336,7 @@ Blockly.Blocks['field_dropdown'] = { this.updateShape_(); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.Mutator(['field_dropdown_option_text', + this.setMutator(new Blockly.icons.MutatorIcon(['field_dropdown_option_text', 'field_dropdown_option_image'])); this.setColour(160); this.setTooltip('Dropdown menu with a list of options.'); @@ -611,7 +611,7 @@ Blockly.Blocks['type_group'] = { this.typeCount_ = 2; this.updateShape_(); this.setOutput(true, 'Type'); - this.setMutator(new Blockly.Mutator(['type_group_item'])); + this.setMutator(new Blockly.icons.MutatorIcon(['type_group_item'])); this.setColour(230); this.setTooltip('Allows more than one type to be accepted.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677'); @@ -671,7 +671,7 @@ Blockly.Blocks['type_group'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 0; i < this.typeCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i); + connections[i]?.reconnect(this, 'TYPE' + i); } }, saveConnections: function(containerBlock) { diff --git a/demos/blockfactory_old/blocks.js b/demos/blockfactory_old/blocks.js index 11bc01112..9d6adc9e8 100644 --- a/demos/blockfactory_old/blocks.js +++ b/demos/blockfactory_old/blocks.js @@ -308,7 +308,7 @@ Blockly.Blocks['field_dropdown'] = { this.updateShape_(); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.Mutator(['field_dropdown_option'])); + this.setMutator(new Blockly.icons.MutatorIcon(['field_dropdown_option'])); this.setColour(160); this.setTooltip('Dropdown menu with a list of options.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); @@ -508,7 +508,7 @@ Blockly.Blocks['type_group'] = { this.typeCount_ = 2; this.updateShape_(); this.setOutput(true, 'Type'); - this.setMutator(new Blockly.Mutator(['type_group_item'])); + this.setMutator(new Blockly.icons.MutatorIcon(['type_group_item'])); this.setColour(230); this.setTooltip('Allows more than one type to be accepted.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677'); @@ -568,7 +568,7 @@ Blockly.Blocks['type_group'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 0; i < this.typeCount_; i++) { - Blockly.Mutator.reconnect(connections[i], this, 'TYPE' + i); + connections[i]?.reconnect(this, 'TYPE' + i); } }, saveConnections: function(containerBlock) { diff --git a/scripts/migration/renamings.json5 b/scripts/migration/renamings.json5 index d9239fe49..758c7318f 100644 --- a/scripts/migration/renamings.json5 +++ b/scripts/migration/renamings.json5 @@ -1467,5 +1467,16 @@ }, }, }, + { + oldName: 'Blockly.Mutator', + newName: 'Blockly.icons.MutatorIcon', + exports: { + Mutator: { + newExport: 'MutatorIcon', + oldPath: 'Blockly.Mutator', + newPath: 'Blocky.icons.MutatorIcon', + }, + }, + }, ], } diff --git a/tests/mocha/blocks/procedures_test.js b/tests/mocha/blocks/procedures_test.js index 69a6659df..6eb686cb3 100644 --- a/tests/mocha/blocks/procedures_test.js +++ b/tests/mocha/blocks/procedures_test.js @@ -90,8 +90,9 @@ suite('Procedures', function () { suite('adding procedure parameters', function () { test('the mutator flyout updates to avoid parameter name conflicts', function () { const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const origFlyoutParamName = mutatorWorkspace .getFlyout() .getWorkspace() @@ -123,8 +124,9 @@ suite('Procedures', function () { test('adding a parameter to the procedure updates procedure defs', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -147,8 +149,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -171,8 +174,9 @@ suite('Procedures', function () { test('undoing adding a procedure parameter removes it', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -195,8 +199,9 @@ suite('Procedures', function () { function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -224,8 +229,9 @@ suite('Procedures', function () { test('deleting a parameter from the procedure updates procedure defs', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -247,8 +253,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -269,8 +276,9 @@ suite('Procedures', function () { test('undoing deleting a procedure parameter adds it', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -295,8 +303,9 @@ suite('Procedures', function () { function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -322,8 +331,9 @@ suite('Procedures', function () { test('defs are updated for parameter renames', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -348,8 +358,9 @@ suite('Procedures', function () { test('defs are updated for parameter renames when two params exist', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock1.setFieldValue('param1', 'NAME'); @@ -378,8 +389,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -406,8 +418,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -428,8 +441,9 @@ suite('Procedures', function () { test('renaming a variable associated with a parameter updates procedure defs', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -437,7 +451,7 @@ suite('Procedures', function () { .getInput('STACK') .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - defBlock.mutator.setVisible(false); + mutatorIcon.setBubbleVisible(false); const variable = this.workspace.getVariable('param1', ''); this.workspace.renameVariableById(variable.getId(), 'new name'); @@ -455,8 +469,9 @@ suite('Procedures', function () { test('renaming a variable associated with a parameter updates mutator parameters', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -479,8 +494,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -488,7 +504,7 @@ suite('Procedures', function () { .getInput('STACK') .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - defBlock.mutator.setVisible(false); + mutatorIcon.setBubbleVisible(false); const variable = this.workspace.getVariable('param1', ''); this.workspace.renameVariableById(variable.getId(), 'new name'); @@ -507,8 +523,9 @@ suite('Procedures', function () { test('coalescing a variable associated with a parameter updates procedure defs', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -516,7 +533,7 @@ suite('Procedures', function () { .getInput('STACK') .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - defBlock.mutator.setVisible(false); + mutatorIcon.setBubbleVisible(false); const variable = this.workspace.getVariable('param1', ''); this.workspace.renameVariableById(variable.getId(), 'preCreatedVar'); @@ -534,8 +551,9 @@ suite('Procedures', function () { test('coalescing a variable associated with a parameter updates mutator parameters', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -558,8 +576,9 @@ suite('Procedures', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -567,7 +586,7 @@ suite('Procedures', function () { .getInput('STACK') .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - defBlock.mutator.setVisible(false); + mutatorIcon.setBubbleVisible(false); const variable = this.workspace.getVariable('param1', ''); this.workspace.renameVariableById(variable.getId(), 'preCreatedVar'); @@ -592,8 +611,9 @@ suite('Procedures', function () { test('undoing renaming a procedure parameter reverts the change', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -622,8 +642,9 @@ suite('Procedures', function () { test('undoing and redoing renaming a procedure maintains the same state', function () { // Create a stack of container, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock.setFieldValue('param1', 'NAME'); @@ -654,8 +675,9 @@ suite('Procedures', function () { test('reordering procedure parameters updates procedure blocks', function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock1.setFieldValue('param1', 'NAME'); @@ -690,8 +712,9 @@ suite('Procedures', function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock1.setFieldValue('param1', 'NAME'); @@ -739,8 +762,9 @@ suite('Procedures', function () { // Create a stack of container, parameter, parameter. const defBlock = createProcDefBlock(this.workspace); const callBlock = createProcCallBlock(this.workspace); - defBlock.mutator.setVisible(true); - const mutatorWorkspace = defBlock.mutator.getWorkspace(); + const mutatorIcon = defBlock.getIcon(Blockly.icons.MutatorIcon.TYPE); + mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); const containerBlock = mutatorWorkspace.getTopBlocks()[0]; const paramBlock1 = mutatorWorkspace.newBlock('procedures_mutatorarg'); paramBlock1.setFieldValue('param1', 'NAME'); @@ -1888,8 +1912,11 @@ suite('Procedures', function () { }); suite('Untyped Arguments', function () { function createMutator(argArray) { - this.defBlock.mutator.setVisible(true); - this.mutatorWorkspace = this.defBlock.mutator.getWorkspace(); + const mutatorIcon = this.defBlock.getIcon( + Blockly.icons.MutatorIcon.TYPE + ); + mutatorIcon.setBubbleVisible(true); + this.mutatorWorkspace = mutatorIcon.getWorkspace(); this.containerBlock = this.mutatorWorkspace.getTopBlocks()[0]; this.connection = this.containerBlock.getInput('STACK').connection; diff --git a/tests/mocha/mutator_test.js b/tests/mocha/mutator_test.js index 06f6918e5..e10caa576 100644 --- a/tests/mocha/mutator_test.js +++ b/tests/mocha/mutator_test.js @@ -35,8 +35,9 @@ suite('Mutator', function () { test('No change', function () { const block = createRenderedBlock(this.workspace, 'xml_block'); - block.mutator.setVisible(true); - const mutatorWorkspace = block.mutator.getWorkspace(); + const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); + icon.setBubbleVisible(true); + const mutatorWorkspace = icon.getWorkspace(); // Trigger mutator change listener. createRenderedBlock(mutatorWorkspace, 'checkbox_block'); assertEventNotFired(this.eventsFireStub, Blockly.Events.BlockChange, { @@ -46,8 +47,9 @@ suite('Mutator', function () { test('XML', function () { const block = createRenderedBlock(this.workspace, 'xml_block'); - block.mutator.setVisible(true); - const mutatorWorkspace = block.mutator.getWorkspace(); + const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); + icon.setBubbleVisible(true); + const mutatorWorkspace = icon.getWorkspace(); mutatorWorkspace .getBlockById('check_block') .setFieldValue('TRUE', 'CHECK'); @@ -65,8 +67,9 @@ suite('Mutator', function () { test('JSO', function () { const block = createRenderedBlock(this.workspace, 'jso_block'); - block.mutator.setVisible(true); - const mutatorWorkspace = block.mutator.getWorkspace(); + const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); + icon.setBubbleVisible(true); + const mutatorWorkspace = icon.getWorkspace(); mutatorWorkspace .getBlockById('check_block') .setFieldValue('TRUE', 'CHECK');