Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update the flyout for compatibility with the new flyout API #209

Merged
merged 2 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 78 additions & 262 deletions src/checkable_continuous_flyout.js
Original file line number Diff line number Diff line change
@@ -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.<string, !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
Expand All @@ -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);
}
}
Loading