diff --git a/src/checkable_continuous_flyout.js b/src/checkable_continuous_flyout.js index 9d75cd53be..81a83fdd96 100644 --- a/src/checkable_continuous_flyout.js +++ b/src/checkable_continuous_flyout.js @@ -1,92 +1,47 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + import * as Blockly from "blockly/core"; import { ContinuousFlyout } from "@blockly/continuous-toolbox"; +import { RecyclableBlockFlyoutInflater } from "./recyclable_block_flyout_inflater.js"; export class CheckableContinuousFlyout extends ContinuousFlyout { /** - * Size of a checkbox next to a variable reporter. - * @type {number} - * @const - */ - static CHECKBOX_SIZE = 25; - - /** - * Amount of touchable padding around reporter checkboxes. - * @type {number} - * @const - */ - static CHECKBOX_TOUCH_PADDING = 12; - - /** - * SVG path data for checkmark in checkbox. - * @type {string} - * @const - */ - static CHECKMARK_PATH = - "M" + - CheckableContinuousFlyout.CHECKBOX_SIZE / 4 + - " " + - CheckableContinuousFlyout.CHECKBOX_SIZE / 2 + - "L" + - (5 * CheckableContinuousFlyout.CHECKBOX_SIZE) / 12 + - " " + - (2 * CheckableContinuousFlyout.CHECKBOX_SIZE) / 3 + - "L" + - (3 * CheckableContinuousFlyout.CHECKBOX_SIZE) / 4 + - " " + - CheckableContinuousFlyout.CHECKBOX_SIZE / 3; - - /** - * Size of the checkbox corner radius - * @type {number} - * @const + * Creates a new CheckableContinuousFlyout. + * + * @param {!Blockly.Options} workspaceOptions Configuration options for the + * flyout workspace. */ - static CHECKBOX_CORNER_RADIUS = 5; - - /** - * @type {number} - * @const - */ - static CHECKBOX_MARGIN = ContinuousFlyout.prototype.MARGIN; - - /** - * Total additional width of a row that contains a checkbox. - * @type {number} - * @const - */ - static CHECKBOX_SPACE_X = - CheckableContinuousFlyout.CHECKBOX_SIZE + - 2 * CheckableContinuousFlyout.CHECKBOX_MARGIN; - constructor(workspaceOptions) { workspaceOptions.modalInputs = false; super(workspaceOptions); this.tabWidth_ = -2; this.MARGIN = 12; this.GAP_Y = 12; - CheckableContinuousFlyout.CHECKBOX_MARGIN = this.MARGIN; - - /** - * Map of checkboxes that correspond to monitored blocks. - * Each element is an object containing the SVG for the checkbox, a boolean - * for its checked state, and the block the checkbox is associated with. - * @type {!Object.} - * @private - */ - this.checkboxes_ = new Map(); - } - - initFlyoutButton_(button, x, y) { - if (button.isLabel()) { - button.height = 40; - } - super.initFlyoutButton_(button, x, y); } + /** + * Displays the given contents in the flyout. + * + * @param {!Object} flyoutDef The new contents to show in the flyout. + */ show(flyoutDef) { - this.clearOldCheckboxes(); super.show(flyoutDef); + const inflater = this.getInflaterForType("block"); + if (inflater instanceof RecyclableBlockFlyoutInflater) { + inflater.emptyRecycledBlocks(); + } } + /** + * Serializes a block to JSON in order to copy it to the main workspace. + * + * @param {!Blockly.BlockSvg} block The block to serialize. + * @returns {!Object} A JSON representation of the block. + */ serializeBlock(block) { const json = super.serializeBlock(block); // Delete the serialized block's ID so that a new one is generated when it is @@ -97,214 +52,75 @@ export class CheckableContinuousFlyout extends ContinuousFlyout { return json; } - clearOldCheckboxes() { - for (const checkbox of this.checkboxes_.values()) { - checkbox.svgRoot.remove(); - } - this.checkboxes_.clear(); - } - - layout_(contents, gaps) { - super.layout_(contents, gaps); - // We want large gaps between categories (see GAP_Y), but don't want those - // counted as part of the category for purposes of scrolling to show the - // category, so we reset/adjust the label gaps used for the scroll position - // calculation here. - this.labelGaps.fill( - this.getWorkspace().getRenderer().getConstants().GRID_UNIT - ); - } - - calculateBottomPadding(contentMetrics, viewMetrics) { - // Since we're messing with the alignment by munging the label gaps, we also - // need to adjust the bottom padding. - return ( - super.calculateBottomPadding(contentMetrics, viewMetrics) - - this.getWorkspace().getRenderer().getConstants().GRID_UNIT * 4 - ); + /** + * Set the state of a checkbox by block ID. + * @param {string} blockId ID of the block whose checkbox should be set + * @param {boolean} value Value to set the checkbox to. + * @public + */ + setCheckboxState(blockId, value) { + this.getWorkspace() + .getBlockById(blockId) + ?.getIcon("checkbox") + ?.setChecked(value); } - addBlockListeners_(root, block, rect) { - if (block.checkboxInFlyout) { - const coordinates = block.getRelativeToSurfaceXY(); - const checkbox = this.createCheckbox_( - block, - coordinates.x, - coordinates.y, - block.getHeightWidth() - ); - let moveX = coordinates.x; - if (this.RTL) { - moveX -= - CheckableContinuousFlyout.CHECKBOX_SIZE + - CheckableContinuousFlyout.CHECKBOX_MARGIN; - } else { - moveX += - CheckableContinuousFlyout.CHECKBOX_SIZE + - CheckableContinuousFlyout.CHECKBOX_MARGIN; - } - block.moveBy(moveX, 0); - this.listeners.push( - Blockly.browserEvents.bind( - checkbox.svgRoot, - "mousedown", - null, - this.checkboxClicked_(checkbox) - ) - ); - } - super.addBlockListeners_(root, block, rect); + getFlyoutScale() { + return 0.675; } - /** - * Respond to a click on a checkbox in the flyout. - * @param {!Object} checkboxObj An object containing the svg element of the - * checkbox, a boolean for the state of the checkbox, and the block the - * checkbox is associated with. - * @return {!Function} Function to call when checkbox is clicked. - * @private - */ - checkboxClicked_(checkboxObj) { - return function (e) { - this.setCheckboxState(checkboxObj.block.id, !checkboxObj.clicked); - // This event has been handled. No need to bubble up to the document. - e.stopPropagation(); - e.preventDefault(); - }.bind(this); + getWidth() { + return 250; } /** - * Create and place a checkbox corresponding to the given block. - * @param {!Blockly.Block} block The block to associate the checkbox to. - * @param {number} cursorX The x position of the cursor during this layout pass. - * @param {number} cursorY The y position of the cursor during this layout pass. - * @param {!{height: number, width: number}} blockHW The height and width of the - * block. - * @private + * Sets whether or not block recycling is enabled in the flyout. + * + * @param {boolean} enabled True if recycling should be enabled. */ - createCheckbox_(block, cursorX, cursorY, blockHW) { - var checkboxState = this.getCheckboxState(block.id); - var svgRoot = block.getSvgRoot(); - var extraSpace = - CheckableContinuousFlyout.CHECKBOX_SIZE + - CheckableContinuousFlyout.CHECKBOX_MARGIN; - var xOffset = this.RTL - ? this.getWidth() / this.workspace_.scale - extraSpace - : cursorX; - var yOffset = - cursorY + - blockHW.height / 2 - - CheckableContinuousFlyout.CHECKBOX_SIZE / 2; - var touchMargin = CheckableContinuousFlyout.CHECKBOX_TOUCH_PADDING; - var checkboxGroup = Blockly.utils.dom.createSvgElement( - "g", - { - transform: `translate(${xOffset}, ${yOffset})`, - fill: "transparent", - }, - null - ); - Blockly.utils.dom.createSvgElement( - "rect", - { - class: "blocklyFlyoutCheckbox", - height: CheckableContinuousFlyout.CHECKBOX_SIZE, - width: CheckableContinuousFlyout.CHECKBOX_SIZE, - rx: CheckableContinuousFlyout.CHECKBOX_CORNER_RADIUS, - ry: CheckableContinuousFlyout.CHECKBOX_CORNER_RADIUS, - }, - checkboxGroup - ); - Blockly.utils.dom.createSvgElement( - "path", - { - class: "blocklyFlyoutCheckboxPath", - d: CheckableContinuousFlyout.CHECKMARK_PATH, - }, - checkboxGroup - ); - Blockly.utils.dom.createSvgElement( - "rect", - { - class: "blocklyTouchTargetBackground", - x: -touchMargin + "px", - y: -touchMargin + "px", - height: CheckableContinuousFlyout.CHECKBOX_SIZE + 2 * touchMargin, - width: CheckableContinuousFlyout.CHECKBOX_SIZE + 2 * touchMargin, - }, - checkboxGroup - ); - var checkboxObj = { - svgRoot: checkboxGroup, - clicked: checkboxState, - block: block, - }; - - if (checkboxState) { - Blockly.utils.dom.addClass(checkboxObj.svgRoot, "checked"); + setRecyclingEnabled(enabled) { + const inflater = this.getInflaterForType("block"); + if (inflater instanceof RecyclableBlockFlyoutInflater) { + inflater.setRecyclingEnabled(enabled); } - - this.workspace_.getCanvas().insertBefore(checkboxGroup, svgRoot); - this.checkboxes_.set(block.id, checkboxObj); - return checkboxObj; } /** - * Set the state of a checkbox by block ID. - * @param {string} blockId ID of the block whose checkbox should be set - * @param {boolean} value Value to set the checkbox to. - * @public + * Records scroll position for each category in the toolbox. + * The scroll position is determined by the coordinates of each category's + * label after the entire flyout has been rendered. + * @package */ - setCheckboxState(blockId, value) { - var checkboxObj = this.checkboxes_.get(blockId); - if (!checkboxObj || checkboxObj.clicked === value) { - return; - } - - var oldValue = checkboxObj.clicked; - checkboxObj.clicked = value; - - if (checkboxObj.clicked) { - Blockly.utils.dom.addClass(checkboxObj.svgRoot, "checked"); - } else { - Blockly.utils.dom.removeClass(checkboxObj.svgRoot, "checked"); - } - - Blockly.Events.fire( - new Blockly.Events.BlockChange( - checkboxObj.block, - "checkbox", - null, - oldValue, - value + recordScrollPositions() { + // TODO(#211) Remove this once the continuous toolbox has been updated. + this.scrollPositions = []; + const categoryLabels = this.getContents() + .filter( + (item) => + item.type === "label" && + item.element.isLabel() && + this.getParentToolbox_().getCategoryByName( + item.element.getButtonText() + ) ) - ); + .map((item) => item.element); + for (const [index, label] of categoryLabels.entries()) { + this.scrollPositions.push({ + name: label.getButtonText(), + position: label.getPosition(), + }); + } } /** - * Gets the checkbox state for a block - * @param {string} blockId The ID of the block in question. - * @return {boolean} Whether the block is checked. - * @public + * Positions the contents of the flyout. + * + * @param {!Blockly.FlyoutItem[]} The flyout items to position. */ - getCheckboxState() { - // Patched by scratch-gui in src/lib/blocks.js. - return false; - } - - getFlyoutScale() { - return 0.675; - } - - getWidth() { - return 250; - } - - blockIsRecyclable_(block) { - const recyclable = super.blockIsRecyclable_(block); - // Exclude blocks with output connections, because they are able to report their current - // value in a popover and recycling them interacts poorly with the VM's maintenance of its - // state. - return recyclable && !block.outputConnection; + layout_(contents) { + // TODO(#211) Remove this once the continuous toolbox has been updated. + // Bypass the continuous flyout's layout method until the plugin is + // updated for the new flyout API. + Blockly.VerticalFlyout.prototype.layout_.call(this, contents); } } diff --git a/src/recyclable_block_flyout_inflater.js b/src/recyclable_block_flyout_inflater.js index 06a3062057..c3add1b3ce 100644 --- a/src/recyclable_block_flyout_inflater.js +++ b/src/recyclable_block_flyout_inflater.js @@ -5,6 +5,7 @@ */ import * as Blockly from "blockly/core"; +import { CheckboxBubble } from "./checkbox_bubble.js"; /** * A block inflater that caches and reuses blocks to improve performance. @@ -22,6 +23,25 @@ export class RecyclableBlockFlyoutInflater extends Blockly.BlockFlyoutInflater { */ recycledBlocks = new Map(); + /** + * Creates a block on the flyout workspace from the given block definition. + * + * @param {!Object} state A JSON representation of a block to load. + * @param {!Blockly.WorkspaceSvg} flyoutWorkspace The flyout's workspace. + * @returns {!Blockly.BlockSvg} The newly created block. + */ + load(state, flyoutWorkspace) { + const block = super.load(state, flyoutWorkspace); + if (block.checkboxInFlyout) { + block.moveBy( + CheckboxBubble.CHECKBOX_SIZE + CheckboxBubble.CHECKBOX_MARGIN, + 0 + ); + } + + return block; + } + /** * Toggles whether or not recycling is enabled. *