mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
fix!: refactor mutator icon (#7115)
* feat: add basic mutator icon * feat: add actual mutation behavior to icon * chore: add bumping blocks back into the bubble * fix: add updating block styles * feat: add static methods to mutator icon * chore: delete old mutator code * fix: use the new mutator icon * chore: docs and format * chore: my own comments * chore: first pass at PR comments * chore: make type strings internal * chore: add todo * chore: format * chore: move properties to module level * chore: fix using in demos * chore: move Mutator to icons.MutatorIcon * chore: move reconnect to connection * chore: move findParentWs to workspace * chore: properly override and call super * chore: remove bubbleIsVisible check * chore: change imports to import type * chore: use elvis operator * chore: update renamings * chore: reduce changes to js block files
This commit is contained in:
@@ -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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/** 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<T extends IIcon>(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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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';
|
||||
|
||||
350
core/icons/mutator_icon.ts
Normal file
350
core/icons/mutator_icon.ts
Normal file
@@ -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<typeof setTimeout> | 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();
|
||||
}
|
||||
}
|
||||
595
core/mutator.ts
595
core/mutator.ts
@@ -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<typeof setTimeout> | 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:
|
||||
<svg>
|
||||
[Workspace]
|
||||
</svg>
|
||||
*/
|
||||
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 <g> 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 <rect> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1467,5 +1467,16 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
oldName: 'Blockly.Mutator',
|
||||
newName: 'Blockly.icons.MutatorIcon',
|
||||
exports: {
|
||||
Mutator: {
|
||||
newExport: 'MutatorIcon',
|
||||
oldPath: 'Blockly.Mutator',
|
||||
newPath: 'Blocky.icons.MutatorIcon',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user