Skip to content

Commit

Permalink
fix: reenable support for checkboxes in the flyout (#43)
Browse files Browse the repository at this point in the history
* fix: reenable support for checkboxes in the flyout

* refactor: use a map instead of an object for storing checkboxes

* chore: remove debugging code

* refactor: improve variable names for checkbox position

* chore: fix line wrapping indentation

* refactor: don't store checkbox wrapper objects on blocks
  • Loading branch information
gonfunko authored Apr 26, 2024
1 parent 4f97982 commit e603c67
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 15 deletions.
4 changes: 2 additions & 2 deletions blocks_vertical/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ Blockly.Blocks['data_variable'] = {
}
],
"category": Categories.data,
"checkboxInFlyout": true,
"extensions": ["contextMenu_getVariableBlock", "colours_data", "output_string"]
});
this.checkboxInFlyout = true;
}
};

Expand Down Expand Up @@ -158,8 +158,8 @@ Blockly.Blocks['data_listcontents'] = {
],
"category": Categories.dataLists,
"extensions": ["contextMenu_getListBlock", "colours_data_lists", "output_string"],
"checkboxInFlyout": true
});
this.checkboxInFlyout = true;
}
};

Expand Down
6 changes: 3 additions & 3 deletions blocks_vertical/looks.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ Blockly.Blocks['looks_size'] = {
this.jsonInit({
"message0": Blockly.Msg.LOOKS_SIZE,
"category": Categories.looks,
"checkboxInFlyout": true,
"extensions": ["colours_looks", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down Expand Up @@ -510,9 +510,9 @@ Blockly.Blocks['looks_backdropnumbername'] = {
}
],
"category": Categories.looks,
"checkboxInFlyout": true,
"extensions": ["colours_looks", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand All @@ -535,9 +535,9 @@ Blockly.Blocks['looks_costumenumbername'] = {
}
],
"category": Categories.looks,
"checkboxInFlyout": true,
"extensions": ["colours_looks", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down
6 changes: 3 additions & 3 deletions blocks_vertical/motion.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,9 @@ Blockly.Blocks['motion_xposition'] = {
this.jsonInit({
"message0": Blockly.Msg.MOTION_XPOSITION,
"category": Categories.motion,
"checkboxInFlyout": true,
"extensions": ["colours_motion", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand All @@ -444,9 +444,9 @@ Blockly.Blocks['motion_yposition'] = {
this.jsonInit({
"message0": Blockly.Msg.MOTION_YPOSITION,
"category": Categories.motion,
"checkboxInFlyout": true,
"extensions": ["colours_motion", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand All @@ -459,9 +459,9 @@ Blockly.Blocks['motion_direction'] = {
this.jsonInit({
"message0": Blockly.Msg.MOTION_DIRECTION,
"category": Categories.motion,
"checkboxInFlyout": true,
"extensions": ["colours_motion", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down
10 changes: 5 additions & 5 deletions blocks_vertical/sensing.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ Blockly.Blocks['sensing_answer'] = {
this.jsonInit({
"message0": Blockly.Msg.SENSING_ANSWER,
"category": Categories.sensing,
"checkboxInFlyout": true,
"extensions": ["colours_sensing", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down Expand Up @@ -343,9 +343,9 @@ Blockly.Blocks['sensing_loudness'] = {
this.jsonInit({
"message0": Blockly.Msg.SENSING_LOUDNESS,
"category": Categories.sensing,
"checkboxInFlyout": true,
"extensions": ["colours_sensing", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down Expand Up @@ -374,9 +374,9 @@ Blockly.Blocks['sensing_timer'] = {
this.jsonInit({
"message0": Blockly.Msg.SENSING_TIMER,
"category": Categories.sensing,
"checkboxInFlyout": true,
"extensions": ["colours_sensing", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down Expand Up @@ -480,9 +480,9 @@ Blockly.Blocks['sensing_current'] = {
}
],
"category": Categories.sensing,
"checkboxInFlyout": true,
"extensions": ["colours_sensing", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand All @@ -509,9 +509,9 @@ Blockly.Blocks['sensing_username'] = {
this.jsonInit({
"message0": Blockly.Msg.SENSING_USERNAME,
"category": Categories.sensing,
"checkboxInFlyout": true,
"extensions": ["colours_sensing", "output_number"]
});
this.checkboxInFlyout = true;
}
};

Expand Down
1 change: 1 addition & 0 deletions blocks_vertical/sound.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,6 @@ Blockly.Blocks['sound_volume'] = {
"checkboxInFlyout": true,
"extensions": ["colours_sounds", "output_number"]
});
this.checkboxInFlyout = true;
}
};
204 changes: 204 additions & 0 deletions src/checkable_continuous_flyout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import * as Blockly from 'blockly';
import {ContinuousFlyout} from '@blockly/continuous-toolbox';

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
*/
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) {
super(workspaceOptions);
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();
}

show(flyoutDef) {
this.clearOldCheckboxes();
super.show(flyoutDef);
}

clearOldCheckboxes() {
for (const checkbox of this.checkboxes_.values()) {
checkbox.svgRoot.remove();
}
this.checkboxes_.clear();
}

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);
}

/**
* 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);
}

/**
* 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
*/
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');
}

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
*/
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));
}

/**
* 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
*/
getCheckboxState() {
// Patched by scratch-gui in src/lib/blocks.js.
return false;
}
}
5 changes: 3 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
ContinuousFlyout,
ContinuousMetrics,
} from '@blockly/continuous-toolbox';

import {CheckableContinuousFlyout} from './checkable_continuous_flyout.js';
import './scratch_continuous_category.js';

export * from 'blockly';
Expand All @@ -35,12 +35,13 @@ export * from '../core/colours.js';
export * from '../core/field_colour_slider.js';
export * from '../msg/scratch_msgs.js';
export {scratchBlocksUtils};
export {CheckableContinuousFlyout};

export function inject(container, options) {
Object.assign(options, {
plugins: {
toolbox: ContinuousToolbox,
flyoutsVerticalToolbox: ContinuousFlyout,
flyoutsVerticalToolbox: CheckableContinuousFlyout,
metricsManager: ContinuousMetrics,
},
});
Expand Down

0 comments on commit e603c67

Please sign in to comment.