From 905f043fec790596d3bd9fa1a5226098407da534 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 19 Apr 2024 08:07:58 -0700 Subject: [PATCH 01/32] fix: temporarily disable functionality in scratch-gui for compatibility with patched scratch-blocks (#1) --- src/containers/blocks.jsx | 55 ++++++++++++++++++++++--------------- src/lib/blocks.js | 40 +++++++++++++++++---------- src/lib/make-toolbox-xml.js | 20 +++++++------- 3 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 750be83fc25..bb7711a5167 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -95,14 +95,24 @@ class Blocks extends React.Component { this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; - this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; + // this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); + const theme = this.ScratchBlocks.Theme.defineTheme('Scratch', { + 'base': this.ScratchBlocks.Themes.Zelos, + 'startHats': true + }); const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme)} + { + rtl: this.props.isRtl, + toolbox: this.props.toolboxXML, + colours: getColorsForTheme(this.props.theme), + renderer: 'zelos', + theme: theme, + } ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -129,10 +139,11 @@ class Blocks extends React.Component { // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the // entire toolbox every time we reset the workspace. We call updateToolbox as a part of // componentDidUpdate so the toolbox will still correctly be updated - this.setToolboxRefreshEnabled = this.workspace.setToolboxRefreshEnabled.bind(this.workspace); - this.workspace.setToolboxRefreshEnabled = () => { - this.setToolboxRefreshEnabled(false); - }; + this.setToolboxRefreshEnabled = () => {}; + // this.workspace.setToolboxRefreshEnabled.bind(this.workspace); + // this.workspace.setToolboxRefreshEnabled = () => { + // this.setToolboxRefreshEnabled(false); + // }; // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); @@ -213,11 +224,11 @@ class Blocks extends React.Component { this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); this.props.vm.setLocale(this.props.locale, this.props.messages) .then(() => { - this.workspace.getFlyout().setRecyclingEnabled(false); + // this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); this.requestToolboxUpdate(); this.withToolboxUpdates(() => { - this.workspace.getFlyout().setRecyclingEnabled(true); + // this.workspace.getFlyout().setRecyclingEnabled(true); }); }); } @@ -225,8 +236,8 @@ class Blocks extends React.Component { updateToolbox () { this.toolboxUpdateTimeout = false; - const categoryId = this.workspace.toolbox_.getSelectedCategoryId(); - const offset = this.workspace.toolbox_.getCategoryScrollOffset(); + // const categoryId = this.workspace.toolbox_.getSelectedItem().getId(); + // const offset = this.workspace.toolbox_.getCategoryScrollOffset(); this.workspace.updateToolbox(this.props.toolboxXML); this._renderedToolboxXML = this.props.toolboxXML; @@ -235,13 +246,13 @@ class Blocks extends React.Component { // Using the setter function will rerender the entire toolbox which we just rendered. this.workspace.toolboxRefreshEnabled_ = true; - const currentCategoryPos = this.workspace.toolbox_.getCategoryPositionById(categoryId); - const currentCategoryLen = this.workspace.toolbox_.getCategoryLengthById(categoryId); - if (offset < currentCategoryLen) { - this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos + offset); - } else { - this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos); - } + // const currentCategoryPos = this.workspace.toolbox_.getCategoryPositionById(categoryId); + // const currentCategoryLen = this.workspace.toolbox_.getCategoryLengthById(categoryId); + // if (offset < currentCategoryLen) { + // this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos + offset); + // } else { + // this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos); + // } const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; @@ -329,16 +340,16 @@ class Blocks extends React.Component { } } onScriptGlowOn (data) { - this.workspace.glowStack(data.id, true); + // this.workspace.glowStack(data.id, true); } onScriptGlowOff (data) { - this.workspace.glowStack(data.id, false); + // this.workspace.glowStack(data.id, false); } onBlockGlowOn (data) { - this.workspace.glowBlock(data.id, true); + // this.workspace.glowBlock(data.id, true); } onBlockGlowOff (data) { - this.workspace.glowBlock(data.id, false); + // this.workspace.glowBlock(data.id, false); } onVisualReport (data) { this.workspace.reportValue(data.id, data.value); @@ -382,7 +393,7 @@ class Blocks extends React.Component { // Remove and reattach the workspace listener (but allow flyout events) this.workspace.removeChangeListener(this.props.vm.blockListener); - const dom = this.ScratchBlocks.Xml.textToDom(data.xml); + const dom = this.ScratchBlocks.utils.xml.textToDom(data.xml); try { this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace); } catch (error) { diff --git a/src/lib/blocks.js b/src/lib/blocks.js index f89f5fe0b02..97d5ec30dea 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -5,7 +5,7 @@ * @return {ScratchBlocks} ScratchBlocks connected with the vm */ export default function (vm, useCatBlocks) { - const ScratchBlocks = useCatBlocks ? require('cat-blocks') : require('scratch-blocks'); + const {ScratchBlocks} = useCatBlocks ? require('cat-blocks') : require('scratch-blocks'); const jsonForMenuBlock = function (name, menuOptionsFn, colors, start) { return { message0: '%1', @@ -82,7 +82,7 @@ export default function (vm, useCatBlocks) { } menu.push([ ScratchBlocks.ScratchMsgs.translate('SOUND_RECORD', 'record...'), - ScratchBlocks.recordSoundCallback + 'SOUND_RECORD' ]); return menu; }; @@ -158,6 +158,16 @@ export default function (vm, useCatBlocks) { ScratchBlocks.Blocks.sound_sounds_menu.init = function () { const json = jsonForMenuBlock('SOUND_MENU', soundsMenu, soundColors, []); this.jsonInit(json); + this.inputList[0].removeField('SOUND_MENU'); + this.inputList[0].appendField(new ScratchBlocks.FieldDropdown(() => { + return soundsMenu(); + }, (newValue) => { + if (newValue === 'SOUND_RECORD') { + ScratchBlocks.recordSoundCallback(); + return null; + } + return newValue; + }), 'SOUND_MENU'); }; ScratchBlocks.Blocks.looks_costume.init = function () { @@ -323,16 +333,16 @@ export default function (vm, useCatBlocks) { return monitoredBlock ? monitoredBlock.isMonitored : false; }; - ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { - if (vm.getPeripheralIsConnected(extensionId)) { - return ScratchBlocks.StatusButtonState.READY; - } - return ScratchBlocks.StatusButtonState.NOT_READY; - }; - - ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { - vm.runtime.emit('PLAY_NOTE', noteNum, extensionId); - }; + // ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { + // if (vm.getPeripheralIsConnected(extensionId)) { + // return ScratchBlocks.StatusButtonState.READY; + // } + // return ScratchBlocks.StatusButtonState.NOT_READY; + // }; + // + // ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { + // vm.runtime.emit('PLAY_NOTE', noteNum, extensionId); + // }; // Use a collator's compare instead of localeCompare which internally // creates a collator. Using this is a lot faster in browsers that create a @@ -341,9 +351,9 @@ export default function (vm, useCatBlocks) { sensitivity: 'base', numeric: true }); - ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) { - return collator.compare(str1, str2); - }; + // ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) { + // return collator.compare(str1, str2); + // }; // Blocks wants to know if 3D CSS transforms are supported. The cross // section of browsers Scratch supports and browsers that support 3D CSS diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 8d356e442d9..156f4cd2811 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,4 +1,4 @@ -import ScratchBlocks from 'scratch-blocks'; +import {ScratchBlocks} from 'scratch-blocks'; import {defaultColors} from './themes'; const categorySeparator = ''; @@ -13,7 +13,7 @@ const motion = function (isInitialSetup, isStage, targetId, colors) { ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? ` ` : ` @@ -158,7 +158,7 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -294,7 +294,7 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -350,7 +350,7 @@ const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { const events = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -391,7 +391,7 @@ const control = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` @@ -444,7 +444,7 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` @@ -526,7 +526,7 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` @@ -715,7 +715,7 @@ const variables = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` Date: Tue, 23 Apr 2024 09:45:13 -0700 Subject: [PATCH 02/32] fix: show the correct toolbox based on sprites or the stage being selected (#4) --- src/containers/blocks.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index bb7711a5167..c65f2d1e521 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -148,6 +148,7 @@ class Blocks extends React.Component { // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); + this.workspace.getToolbox().selectItemByPosition(0); this.attachVM(); // Only update blocks/vm locale when visible to avoid sizing issues @@ -224,11 +225,11 @@ class Blocks extends React.Component { this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); this.props.vm.setLocale(this.props.locale, this.props.messages) .then(() => { - // this.workspace.getFlyout().setRecyclingEnabled(false); + this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); this.requestToolboxUpdate(); this.withToolboxUpdates(() => { - // this.workspace.getFlyout().setRecyclingEnabled(true); + this.workspace.getFlyout().setRecyclingEnabled(true); }); }); } @@ -239,6 +240,7 @@ class Blocks extends React.Component { // const categoryId = this.workspace.toolbox_.getSelectedItem().getId(); // const offset = this.workspace.toolbox_.getCategoryScrollOffset(); this.workspace.updateToolbox(this.props.toolboxXML); + this.workspace.refreshToolboxSelection(); this._renderedToolboxXML = this.props.toolboxXML; // In order to catch any changes that mutate the toolbox during "normal runtime" From 59bf3c577d9382d4f43c35a37714725623c53783 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 23 Apr 2024 10:48:35 -0700 Subject: [PATCH 03/32] fix: modify inject options to reflect Scratch behaviors (#5) --- src/containers/blocks.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index c65f2d1e521..a667771fc08 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -664,9 +664,12 @@ Blocks.propTypes = { Blocks.defaultOptions = { zoom: { controls: true, - wheel: true, + wheel: false, startScale: BLOCKS_DEFAULT_SCALE }, + move: { + wheel: true, + }, grid: { spacing: 40, length: 2, @@ -674,7 +677,8 @@ Blocks.defaultOptions = { }, comments: true, collapse: false, - sounds: false + sounds: false, + trashcan: false, }; Blocks.defaultProps = { From df207e42d18b4f24cbf8028803caf397e996c7f9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 24 Apr 2024 12:28:10 -0700 Subject: [PATCH 04/32] fix: add support for Scratch-style procedures (#6) * fix: add support for Scratch-style procedures * refactor: remove underscore procedure creation callback --- src/containers/blocks.jsx | 14 ++++++++------ src/containers/custom-procedures.jsx | 12 +++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index a667771fc08..4507240e80a 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -79,7 +79,7 @@ class Blocks extends React.Component { 'setBlocks', 'setLocale' ]); - this.ScratchBlocks.prompt = this.handlePromptStart; + this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; @@ -91,12 +91,12 @@ class Blocks extends React.Component { } componentDidMount () { this.ScratchBlocks = VMScratchBlocks(this.props.vm, this.props.useCatBlocks); - this.ScratchBlocks.prompt = this.handlePromptStart; + this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; // this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; - this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; + this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); const theme = this.ScratchBlocks.Theme.defineTheme('Scratch', { @@ -115,6 +115,8 @@ class Blocks extends React.Component { } ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); + this.workspace.registerToolboxCategoryCallback('PROCEDURE', + this.ScratchBlocks.ScratchProcedures.getProceduresCategory); // Register buttons under new callback keys for creating variables, // lists, and procedures from extensions. @@ -124,7 +126,7 @@ class Blocks extends React.Component { const varListButtonCallback = type => (() => this.ScratchBlocks.Variables.createVariable(this.workspace, null, type)); const procButtonCallback = () => { - this.ScratchBlocks.Procedures.createProcedureDefCallback_(this.workspace); + this.ScratchBlocks.ScratchProcedures.createProcedureDefCallback(this.workspace); }; toolboxWorkspace.registerButtonCallback('MAKE_A_VARIABLE', varListButtonCallback('')); @@ -545,8 +547,8 @@ class Blocks extends React.Component { handleCustomProceduresClose (data) { this.props.onRequestCloseCustomProcedures(data); const ws = this.workspace; - ws.refreshToolboxSelection_(); - ws.toolbox_.scrollToCategoryById('myBlocks'); + this.updateToolbox(); + ws.getToolbox().selectCategoryByName('myBlocks'); } handleDrop (dragInfo) { fetch(dragInfo.payload.bodyUrl) diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx index e691b0466f7..de7ac0a0423 100644 --- a/src/containers/custom-procedures.jsx +++ b/src/containers/custom-procedures.jsx @@ -3,7 +3,7 @@ import defaultsDeep from 'lodash.defaultsdeep'; import PropTypes from 'prop-types'; import React from 'react'; import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx'; -import ScratchBlocks from 'scratch-blocks'; +import {ScratchBlocks} from 'scratch-blocks'; import {connect} from 'react-redux'; class CustomProcedures extends React.Component { @@ -37,11 +37,13 @@ class CustomProcedures extends React.Component { {rtl: this.props.isRtl} ); - // @todo This is a hack to make there be no toolbox. - const oldDefaultToolbox = ScratchBlocks.Blocks.defaultToolbox; - ScratchBlocks.Blocks.defaultToolbox = null; + const theme = ScratchBlocks.Theme.defineTheme('Scratch', { + 'base': ScratchBlocks.Themes.Zelos, + 'startHats': true + }); + workspaceConfig.theme = theme; + workspaceConfig.renderer = 'zelos'; this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig); - ScratchBlocks.Blocks.defaultToolbox = oldDefaultToolbox; // Create the procedure declaration block for editing the mutation. this.mutationRoot = this.workspace.newBlock('procedures_declaration'); From a7b1a10ce5a295277443bbb0b911340ab9532ff1 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 24 Apr 2024 15:40:57 -0700 Subject: [PATCH 05/32] fix: reenable the Scratch colour eyedropper --- src/containers/blocks.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 4507240e80a..2d55d4c1373 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -95,7 +95,7 @@ class Blocks extends React.Component { this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; - // this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; + this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); From dd98dd843fe55baaa555bfd6c15149883e80e896 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 26 Apr 2024 11:22:41 -0700 Subject: [PATCH 06/32] fix: patch the getCheckboxState method in the new flyout (#7) --- src/lib/blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 97d5ec30dea..3230729c8ad 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -328,7 +328,7 @@ export default function (vm, useCatBlocks) { this.jsonInit(json); }; - ScratchBlocks.VerticalFlyout.getCheckboxState = function (blockId) { + ScratchBlocks.CheckableContinuousFlyout.prototype.getCheckboxState = function (blockId) { const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; return monitoredBlock ? monitoredBlock.isMonitored : false; }; From 2d58403d1fd1b9d4020445ffe02365e5374ade84 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 30 Apr 2024 15:07:15 -0700 Subject: [PATCH 07/32] fix: select extension categories when added (#8) --- src/containers/blocks.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 2d55d4c1373..49f83a7e20e 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -500,7 +500,8 @@ class Blocks extends React.Component { } this.withToolboxUpdates(() => { - this.workspace.toolbox_.setSelectedCategoryById(categoryId); + const toolbox = this.workspace.getToolbox(); + toolbox.setSelectedItem(toolbox.getToolboxItemById(categoryId)); }); } setBlocks (blocks) { From 077415b55fb767dd9f42206ea2bfc97160272a4b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 30 Apr 2024 15:48:05 -0700 Subject: [PATCH 08/32] fix: Use toolboxitemid instead of id as the identifier attribute for toolbox categories (#9) --- src/lib/make-toolbox-xml.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 156f4cd2811..532f9e14c0a 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -13,7 +13,7 @@ const motion = function (isInitialSetup, isStage, targetId, colors) { ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? ` ` : ` @@ -158,7 +158,7 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -294,7 +294,7 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -350,7 +350,7 @@ const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { const events = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -392,7 +392,7 @@ const control = function (isInitialSetup, isStage, targetId, colors) { return ` @@ -445,7 +445,7 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { return ` ${isStage ? '' : ` @@ -527,7 +527,7 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { return ` @@ -716,7 +716,7 @@ const variables = function (isInitialSetup, isStage, targetId, colors) { return ` @@ -729,7 +729,7 @@ const myBlocks = function (isInitialSetup, isStage, targetId, colors) { return ` From c8bc88078361840eb9cc47762ebd1bb61a8b5075 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 1 May 2024 09:21:02 -0700 Subject: [PATCH 09/32] fix: preserve toolbox scroll position when switching between sprites/the stage (#10) --- src/containers/blocks.jsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 49f83a7e20e..2cba5b47081 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -239,8 +239,13 @@ class Blocks extends React.Component { updateToolbox () { this.toolboxUpdateTimeout = false; - // const categoryId = this.workspace.toolbox_.getSelectedItem().getId(); - // const offset = this.workspace.toolbox_.getCategoryScrollOffset(); + const scale = this.workspace.getFlyout().getWorkspace().scale; + const selectedCategoryName = this.workspace.getToolbox().getSelectedItem().getName(); + const selectedCategoryScrollPosition = this.workspace.getFlyout().getCategoryScrollPosition( + selectedCategoryName).y * scale; + const offsetWithinCategory = (this.workspace.getFlyout().getWorkspace().getMetrics().viewTop + - selectedCategoryScrollPosition); + this.workspace.updateToolbox(this.props.toolboxXML); this.workspace.refreshToolboxSelection(); this._renderedToolboxXML = this.props.toolboxXML; @@ -250,13 +255,10 @@ class Blocks extends React.Component { // Using the setter function will rerender the entire toolbox which we just rendered. this.workspace.toolboxRefreshEnabled_ = true; - // const currentCategoryPos = this.workspace.toolbox_.getCategoryPositionById(categoryId); - // const currentCategoryLen = this.workspace.toolbox_.getCategoryLengthById(categoryId); - // if (offset < currentCategoryLen) { - // this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos + offset); - // } else { - // this.workspace.toolbox_.setFlyoutScrollPos(currentCategoryPos); - // } + const newCategoryScrollPosition = this.workspace.getFlyout().getCategoryScrollPosition( + selectedCategoryName).y * scale; + this.workspace.getFlyout().getWorkspace().scrollbar.setY( + newCategoryScrollPosition + offsetWithinCategory); const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; From 1898c5281f8621c24d23c9066c5979d66ed32b77 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 3 May 2024 08:58:11 -0700 Subject: [PATCH 10/32] fix: call reportValue on the ScratchBlocks module instead of the workspace (#11) --- src/containers/blocks.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 2cba5b47081..e6faf341141 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -358,7 +358,7 @@ class Blocks extends React.Component { // this.workspace.glowBlock(data.id, false); } onVisualReport (data) { - this.workspace.reportValue(data.id, data.value); + this.ScratchBlocks.reportValue(data.id, data.value); } getToolboxXML () { // Use try/catch because this requires digging pretty deep into the VM From be4386ddf26bcd6923274d7abfa46cfbc199c75b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 6 May 2024 13:42:13 -0700 Subject: [PATCH 11/32] fix: call the new glow methods on ScratchBlocks (#12) * fix: call the new glow methods on ScratchBlocks * refactor: remove the glowBlock calls --- src/containers/blocks.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e6faf341141..c99bd6627c6 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -346,16 +346,16 @@ class Blocks extends React.Component { } } onScriptGlowOn (data) { - // this.workspace.glowStack(data.id, true); + this.ScratchBlocks.glowStack(data.id, true); } onScriptGlowOff (data) { - // this.workspace.glowStack(data.id, false); + this.ScratchBlocks.glowStack(data.id, false); } onBlockGlowOn (data) { - // this.workspace.glowBlock(data.id, true); + // No-op, support may be added in the future } onBlockGlowOff (data) { - // this.workspace.glowBlock(data.id, false); + // No-op, support may be added in the future } onVisualReport (data) { this.ScratchBlocks.reportValue(data.id, data.value); From 06b011fbd2c22a9e13bedce219a3b6d1c45ca818 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 7 May 2024 11:57:53 -0700 Subject: [PATCH 12/32] fix: adjust key event filtering to fix the when key pressed block (#13) --- src/lib/vm-listener-hoc.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index b6207a5b7dd..e94aa2541f8 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -85,7 +85,7 @@ const vmListenerHOC = function (WrappedComponent) { } handleKeyDown (e) { // Don't capture keys intended for Blockly inputs. - if (e.target !== document && e.target !== document.body) return; + if (e.target instanceof HTMLInputElement) return; const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key; this.props.vm.postIOData('keyboard', { From 6e56e523aa433b58f0b579c9aa5e19787b3b95a9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 16 May 2024 15:54:10 -0700 Subject: [PATCH 13/32] refactor: simplify toolbox refreshing behavior (#14) --- src/containers/blocks.jsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index c99bd6627c6..1284e6891a3 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -138,15 +138,6 @@ class Blocks extends React.Component { // the xml can change while e.g. on the costumes tab. this._renderedToolboxXML = this.props.toolboxXML; - // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the - // entire toolbox every time we reset the workspace. We call updateToolbox as a part of - // componentDidUpdate so the toolbox will still correctly be updated - this.setToolboxRefreshEnabled = () => {}; - // this.workspace.setToolboxRefreshEnabled.bind(this.workspace); - // this.workspace.setToolboxRefreshEnabled = () => { - // this.setToolboxRefreshEnabled(false); - // }; - // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); @@ -247,14 +238,9 @@ class Blocks extends React.Component { - selectedCategoryScrollPosition); this.workspace.updateToolbox(this.props.toolboxXML); - this.workspace.refreshToolboxSelection(); + this.workspace.getToolbox().forceRerender(); this._renderedToolboxXML = this.props.toolboxXML; - // In order to catch any changes that mutate the toolbox during "normal runtime" - // (variable changes/etc), re-enable toolbox refresh. - // Using the setter function will rerender the entire toolbox which we just rendered. - this.workspace.toolboxRefreshEnabled_ = true; - const newCategoryScrollPosition = this.workspace.getFlyout().getCategoryScrollPosition( selectedCategoryName).y * scale; this.workspace.getFlyout().getWorkspace().scrollbar.setY( From 668fbb90be3bfc7fd9566112c8ba6161cdae12a3 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 8 Jul 2024 14:52:42 -0700 Subject: [PATCH 14/32] fix: allow typing into comments (#15) --- src/lib/vm-listener-hoc.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index e94aa2541f8..f059c030baa 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -85,7 +85,7 @@ const vmListenerHOC = function (WrappedComponent) { } handleKeyDown (e) { // Don't capture keys intended for Blockly inputs. - if (e.target instanceof HTMLInputElement) return; + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key; this.props.vm.postIOData('keyboard', { From c92f244646c773c677cb5cb6db9a8cda16d0835a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 2 Aug 2024 13:30:06 -0700 Subject: [PATCH 15/32] feat: plumb Scratch variable support into the UI (#16) * chore: format blocks.jsx * feat: plumb Scratch variable support into the UI --- src/containers/blocks.jsx | 630 ++++++++++++++++++++++++-------------- 1 file changed, 396 insertions(+), 234 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 1284e6891a3..9fed0c3ed02 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -1,39 +1,49 @@ -import bindAll from 'lodash.bindall'; -import debounce from 'lodash.debounce'; -import defaultsDeep from 'lodash.defaultsdeep'; -import makeToolboxXML from '../lib/make-toolbox-xml'; -import PropTypes from 'prop-types'; -import React from 'react'; -import VMScratchBlocks from '../lib/blocks'; -import VM from 'scratch-vm'; - -import log from '../lib/log.js'; -import Prompt from './prompt.jsx'; -import BlocksComponent from '../components/blocks/blocks.jsx'; -import ExtensionLibrary from './extension-library.jsx'; -import extensionData from '../lib/libraries/extensions/index.jsx'; -import CustomProcedures from './custom-procedures.jsx'; -import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; -import {BLOCKS_DEFAULT_SCALE, STAGE_DISPLAY_SIZES} from '../lib/layout-constants'; -import DropAreaHOC from '../lib/drop-area-hoc.jsx'; -import DragConstants from '../lib/drag-constants'; -import defineDynamicBlock from '../lib/define-dynamic-block'; -import {DEFAULT_THEME, getColorsForTheme, themeMap} from '../lib/themes'; -import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../lib/themes/blockHelpers'; - -import {connect} from 'react-redux'; -import {updateToolbox} from '../reducers/toolbox'; -import {activateColorPicker} from '../reducers/color-picker'; -import {closeExtensionLibrary, openSoundRecorder, openConnectionModal} from '../reducers/modals'; -import {activateCustomProcedures, deactivateCustomProcedures} from '../reducers/custom-procedures'; -import {setConnectionModalExtensionId} from '../reducers/connection-modal'; -import {updateMetrics} from '../reducers/workspace-metrics'; -import {isTimeTravel2020} from '../reducers/time-travel'; +import bindAll from "lodash.bindall"; +import debounce from "lodash.debounce"; +import defaultsDeep from "lodash.defaultsdeep"; +import makeToolboxXML from "../lib/make-toolbox-xml"; +import PropTypes from "prop-types"; +import React from "react"; +import VMScratchBlocks from "../lib/blocks"; +import VM from "scratch-vm"; + +import log from "../lib/log.js"; +import Prompt from "./prompt.jsx"; +import BlocksComponent from "../components/blocks/blocks.jsx"; +import ExtensionLibrary from "./extension-library.jsx"; +import extensionData from "../lib/libraries/extensions/index.jsx"; +import CustomProcedures from "./custom-procedures.jsx"; +import errorBoundaryHOC from "../lib/error-boundary-hoc.jsx"; +import { + BLOCKS_DEFAULT_SCALE, + STAGE_DISPLAY_SIZES, +} from "../lib/layout-constants"; +import DropAreaHOC from "../lib/drop-area-hoc.jsx"; +import DragConstants from "../lib/drag-constants"; +import defineDynamicBlock from "../lib/define-dynamic-block"; +import { DEFAULT_THEME, getColorsForTheme, themeMap } from "../lib/themes"; +import { + injectExtensionBlockTheme, + injectExtensionCategoryTheme, +} from "../lib/themes/blockHelpers"; +import { connect } from "react-redux"; +import { updateToolbox } from "../reducers/toolbox"; +import { activateColorPicker } from "../reducers/color-picker"; +import { + closeExtensionLibrary, + openSoundRecorder, + openConnectionModal, +} from "../reducers/modals"; import { - activateTab, - SOUNDS_TAB_INDEX -} from '../reducers/editor-tab'; + activateCustomProcedures, + deactivateCustomProcedures, +} from "../reducers/custom-procedures"; +import { setConnectionModalExtensionId } from "../reducers/connection-modal"; +import { updateMetrics } from "../reducers/workspace-metrics"; +import { isTimeTravel2020 } from "../reducers/time-travel"; + +import { activateTab, SOUNDS_TAB_INDEX } from "../reducers/editor-tab"; const addFunctionListener = (object, property, callback) => { const oldFn = object[property]; @@ -44,94 +54,135 @@ const addFunctionListener = (object, property, callback) => { }; }; -const DroppableBlocks = DropAreaHOC([ - DragConstants.BACKPACK_CODE -])(BlocksComponent); +const DroppableBlocks = DropAreaHOC([DragConstants.BACKPACK_CODE])( + BlocksComponent +); class Blocks extends React.Component { - constructor (props) { + constructor(props) { super(props); this.ScratchBlocks = VMScratchBlocks(props.vm, false); bindAll(this, [ - 'attachVM', - 'detachVM', - 'getToolboxXML', - 'handleCategorySelected', - 'handleConnectionModalStart', - 'handleDrop', - 'handleStatusButtonUpdate', - 'handleOpenSoundRecorder', - 'handlePromptStart', - 'handlePromptCallback', - 'handlePromptClose', - 'handleCustomProceduresClose', - 'onScriptGlowOn', - 'onScriptGlowOff', - 'onBlockGlowOn', - 'onBlockGlowOff', - 'handleMonitorsUpdate', - 'handleExtensionAdded', - 'handleBlocksInfoUpdate', - 'onTargetsUpdate', - 'onVisualReport', - 'onWorkspaceUpdate', - 'onWorkspaceMetricsChange', - 'setBlocks', - 'setLocale' + "attachVM", + "detachVM", + "getToolboxXML", + "handleCategorySelected", + "handleConnectionModalStart", + "handleDrop", + "handleStatusButtonUpdate", + "handleOpenSoundRecorder", + "handlePromptStart", + "handlePromptCallback", + "handlePromptClose", + "handleCustomProceduresClose", + "onScriptGlowOn", + "onScriptGlowOff", + "onBlockGlowOn", + "onBlockGlowOff", + "handleMonitorsUpdate", + "handleExtensionAdded", + "handleBlocksInfoUpdate", + "onTargetsUpdate", + "onVisualReport", + "onWorkspaceUpdate", + "onWorkspaceMetricsChange", + "setBlocks", + "setLocale", ]); this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); - this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; + this.ScratchBlocks.statusButtonCallback = + this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; this.state = { - prompt: null + prompt: null, }; this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); this.toolboxUpdateQueue = []; } - componentDidMount () { - this.ScratchBlocks = VMScratchBlocks(this.props.vm, this.props.useCatBlocks); + componentDidMount() { + this.ScratchBlocks = VMScratchBlocks( + this.props.vm, + this.props.useCatBlocks + ); this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); - this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; + this.ScratchBlocks.statusButtonCallback = + this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; - this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; - this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; + this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = + this.props.onActivateColorPicker; + this.ScratchBlocks.ScratchProcedures.externalProcedureDefCallback = + this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - const theme = this.ScratchBlocks.Theme.defineTheme('Scratch', { - 'base': this.ScratchBlocks.Themes.Zelos, - 'startHats': true + const theme = this.ScratchBlocks.Theme.defineTheme("Scratch", { + base: this.ScratchBlocks.Themes.Zelos, + startHats: true, }); - const workspaceConfig = defaultsDeep({}, + const workspaceConfig = defaultsDeep( + {}, Blocks.defaultOptions, this.props.options, { rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme), - renderer: 'zelos', + renderer: "zelos", theme: theme, } ); - this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); - this.workspace.registerToolboxCategoryCallback('PROCEDURE', - this.ScratchBlocks.ScratchProcedures.getProceduresCategory); + this.workspace = this.ScratchBlocks.inject( + this.blocks, + workspaceConfig + ); + this.workspace.registerToolboxCategoryCallback( + "VARIABLE", + this.ScratchBlocks.ScratchVariables.getVariablesCategory + ); + this.workspace.registerToolboxCategoryCallback( + "PROCEDURE", + this.ScratchBlocks.ScratchProcedures.getProceduresCategory + ); + this.workspace.addChangeListener((event) => { + if ( + event.type === this.ScratchBlocks.Events.VAR_CREATE || + event.type === this.ScratchBlocks.Events.VAR_RENAME || + event.type === this.ScratchBlocks.Events.VAR_DELETE + ) { + this.requestToolboxUpdate(); + } + }); // Register buttons under new callback keys for creating variables, // lists, and procedures from extensions. const toolboxWorkspace = this.workspace.getFlyout().getWorkspace(); - const varListButtonCallback = type => - (() => this.ScratchBlocks.Variables.createVariable(this.workspace, null, type)); + const varListButtonCallback = (type) => () => + this.ScratchBlocks.ScratchVariables.createVariable( + this.workspace, + null, + type + ); const procButtonCallback = () => { - this.ScratchBlocks.ScratchProcedures.createProcedureDefCallback(this.workspace); + this.ScratchBlocks.ScratchProcedures.createProcedureDefCallback( + this.workspace + ); }; - toolboxWorkspace.registerButtonCallback('MAKE_A_VARIABLE', varListButtonCallback('')); - toolboxWorkspace.registerButtonCallback('MAKE_A_LIST', varListButtonCallback('list')); - toolboxWorkspace.registerButtonCallback('MAKE_A_PROCEDURE', procButtonCallback); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_VARIABLE", + varListButtonCallback("") + ); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_LIST", + varListButtonCallback("list") + ); + toolboxWorkspace.registerButtonCallback( + "MAKE_A_PROCEDURE", + procButtonCallback + ); // Store the xml of the toolbox that is actually rendered. // This is used in componentDidUpdate instead of prevProps, because @@ -139,8 +190,16 @@ class Blocks extends React.Component { this._renderedToolboxXML = this.props.toolboxXML; // @todo change this when blockly supports UI events - addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); - addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); + addFunctionListener( + this.workspace, + "translate", + this.onWorkspaceMetricsChange + ); + addFunctionListener( + this.workspace, + "zoom", + this.onWorkspaceMetricsChange + ); this.workspace.getToolbox().selectItemByPosition(0); this.attachVM(); @@ -150,19 +209,21 @@ class Blocks extends React.Component { this.setLocale(); } } - shouldComponentUpdate (nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || this._renderedToolboxXML !== nextProps.toolboxXML || - this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible || - this.props.customProceduresVisible !== nextProps.customProceduresVisible || + this.props.extensionLibraryVisible !== + nextProps.extensionLibraryVisible || + this.props.customProceduresVisible !== + nextProps.customProceduresVisible || this.props.locale !== nextProps.locale || this.props.anyModalVisible !== nextProps.anyModalVisible || this.props.stageSize !== nextProps.stageSize ); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { // If any modals are open, call hideChaff to close z-indexed field editors if (this.props.anyModalVisible && !prevProps.anyModalVisible) { this.ScratchBlocks.hideChaff(); @@ -171,22 +232,29 @@ class Blocks extends React.Component { // Only rerender the toolbox when the blocks are visible and the xml is // different from the previously rendered toolbox xml. // Do not check against prevProps.toolboxXML because that may not have been rendered. - if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) { + if ( + this.props.isVisible && + this.props.toolboxXML !== this._renderedToolboxXML + ) { this.requestToolboxUpdate(); } if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event("resize")); } return; } // @todo hack to resize blockly manually in case resize happened while hidden // @todo hack to reload the workspace due to gui bug #413 - if (this.props.isVisible) { // Scripts tab + if (this.props.isVisible) { + // Scripts tab this.workspace.setVisible(true); - if (prevProps.locale !== this.props.locale || this.props.locale !== this.props.vm.getLocale()) { + if ( + prevProps.locale !== this.props.locale || + this.props.locale !== this.props.vm.getLocale() + ) { // call setLocale if the locale has changed, or changed while the blocks were hidden. // vm.getLocale() will be out of sync if locale was changed while not visible this.setLocale(); @@ -195,12 +263,12 @@ class Blocks extends React.Component { this.requestToolboxUpdate(); } - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event("resize")); } else { this.workspace.setVisible(false); } } - componentWillUnmount () { + componentWillUnmount() { this.detachVM(); this.workspace.dispose(); clearTimeout(this.toolboxUpdateTimeout); @@ -208,15 +276,16 @@ class Blocks extends React.Component { // Clear the flyout blocks so that they can be recreated on mount. this.props.vm.clearFlyoutBlocks(); } - requestToolboxUpdate () { + requestToolboxUpdate() { clearTimeout(this.toolboxUpdateTimeout); this.toolboxUpdateTimeout = setTimeout(() => { this.updateToolbox(); }, 0); } - setLocale () { + setLocale() { this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - this.props.vm.setLocale(this.props.locale, this.props.messages) + this.props.vm + .setLocale(this.props.locale, this.props.messages) .then(() => { this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); @@ -227,31 +296,41 @@ class Blocks extends React.Component { }); } - updateToolbox () { + updateToolbox() { this.toolboxUpdateTimeout = false; const scale = this.workspace.getFlyout().getWorkspace().scale; - const selectedCategoryName = this.workspace.getToolbox().getSelectedItem().getName(); - const selectedCategoryScrollPosition = this.workspace.getFlyout().getCategoryScrollPosition( - selectedCategoryName).y * scale; - const offsetWithinCategory = (this.workspace.getFlyout().getWorkspace().getMetrics().viewTop - - selectedCategoryScrollPosition); + const selectedCategoryName = this.workspace + .getToolbox() + .getSelectedItem() + .getName(); + const selectedCategoryScrollPosition = + this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName).y * scale; + const offsetWithinCategory = + this.workspace.getFlyout().getWorkspace().getMetrics().viewTop - + selectedCategoryScrollPosition; this.workspace.updateToolbox(this.props.toolboxXML); this.workspace.getToolbox().forceRerender(); this._renderedToolboxXML = this.props.toolboxXML; - const newCategoryScrollPosition = this.workspace.getFlyout().getCategoryScrollPosition( - selectedCategoryName).y * scale; - this.workspace.getFlyout().getWorkspace().scrollbar.setY( - newCategoryScrollPosition + offsetWithinCategory); + const newCategoryScrollPosition = + this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName).y * scale; + this.workspace + .getFlyout() + .getWorkspace() + .scrollbar.setY(newCategoryScrollPosition + offsetWithinCategory); const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; - queue.forEach(fn => fn()); + queue.forEach((fn) => fn()); } - withToolboxUpdates (fn) { + withToolboxUpdates(fn) { // if there is a queued toolbox update, we need to wait if (this.toolboxUpdateTimeout) { this.toolboxUpdateQueue.push(fn); @@ -260,42 +339,68 @@ class Blocks extends React.Component { } } - attachVM () { + attachVM() { this.workspace.addChangeListener(this.props.vm.blockListener); - this.flyoutWorkspace = this.workspace - .getFlyout() - .getWorkspace(); - this.flyoutWorkspace.addChangeListener(this.props.vm.flyoutBlockListener); - this.flyoutWorkspace.addChangeListener(this.props.vm.monitorBlockListener); - this.props.vm.addListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); - this.props.vm.addListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); - this.props.vm.addListener('BLOCK_GLOW_ON', this.onBlockGlowOn); - this.props.vm.addListener('BLOCK_GLOW_OFF', this.onBlockGlowOff); - this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport); - this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate); - this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate); - this.props.vm.addListener('MONITORS_UPDATE', this.handleMonitorsUpdate); - this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); - this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); - this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); - } - detachVM () { - this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); - this.props.vm.removeListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); - this.props.vm.removeListener('BLOCK_GLOW_ON', this.onBlockGlowOn); - this.props.vm.removeListener('BLOCK_GLOW_OFF', this.onBlockGlowOff); - this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport); - this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate); - this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate); - this.props.vm.removeListener('MONITORS_UPDATE', this.handleMonitorsUpdate); - this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); - this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); - this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); - } - - updateToolboxBlockValue (id, value) { + this.flyoutWorkspace = this.workspace.getFlyout().getWorkspace(); + this.flyoutWorkspace.addChangeListener( + this.props.vm.flyoutBlockListener + ); + this.flyoutWorkspace.addChangeListener( + this.props.vm.monitorBlockListener + ); + this.props.vm.addListener("SCRIPT_GLOW_ON", this.onScriptGlowOn); + this.props.vm.addListener("SCRIPT_GLOW_OFF", this.onScriptGlowOff); + this.props.vm.addListener("BLOCK_GLOW_ON", this.onBlockGlowOn); + this.props.vm.addListener("BLOCK_GLOW_OFF", this.onBlockGlowOff); + this.props.vm.addListener("VISUAL_REPORT", this.onVisualReport); + this.props.vm.addListener("workspaceUpdate", this.onWorkspaceUpdate); + this.props.vm.addListener("targetsUpdate", this.onTargetsUpdate); + this.props.vm.addListener("MONITORS_UPDATE", this.handleMonitorsUpdate); + this.props.vm.addListener("EXTENSION_ADDED", this.handleExtensionAdded); + this.props.vm.addListener( + "BLOCKSINFO_UPDATE", + this.handleBlocksInfoUpdate + ); + this.props.vm.addListener( + "PERIPHERAL_CONNECTED", + this.handleStatusButtonUpdate + ); + this.props.vm.addListener( + "PERIPHERAL_DISCONNECTED", + this.handleStatusButtonUpdate + ); + } + detachVM() { + this.props.vm.removeListener("SCRIPT_GLOW_ON", this.onScriptGlowOn); + this.props.vm.removeListener("SCRIPT_GLOW_OFF", this.onScriptGlowOff); + this.props.vm.removeListener("BLOCK_GLOW_ON", this.onBlockGlowOn); + this.props.vm.removeListener("BLOCK_GLOW_OFF", this.onBlockGlowOff); + this.props.vm.removeListener("VISUAL_REPORT", this.onVisualReport); + this.props.vm.removeListener("workspaceUpdate", this.onWorkspaceUpdate); + this.props.vm.removeListener("targetsUpdate", this.onTargetsUpdate); + this.props.vm.removeListener( + "MONITORS_UPDATE", + this.handleMonitorsUpdate + ); + this.props.vm.removeListener( + "EXTENSION_ADDED", + this.handleExtensionAdded + ); + this.props.vm.removeListener( + "BLOCKSINFO_UPDATE", + this.handleBlocksInfoUpdate + ); + this.props.vm.removeListener( + "PERIPHERAL_CONNECTED", + this.handleStatusButtonUpdate + ); + this.props.vm.removeListener( + "PERIPHERAL_DISCONNECTED", + this.handleStatusButtonUpdate + ); + } + + updateToolboxBlockValue(id, value) { this.withToolboxUpdates(() => { const block = this.workspace .getFlyout() @@ -307,15 +412,21 @@ class Blocks extends React.Component { }); } - onTargetsUpdate () { + onTargetsUpdate() { if (this.props.vm.editingTarget && this.workspace.getFlyout()) { - ['glide', 'move', 'set'].forEach(prefix => { - this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString()); - this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString()); + ["glide", "move", "set"].forEach((prefix) => { + this.updateToolboxBlockValue( + `${prefix}x`, + Math.round(this.props.vm.editingTarget.x).toString() + ); + this.updateToolboxBlockValue( + `${prefix}y`, + Math.round(this.props.vm.editingTarget.y).toString() + ); }); } } - onWorkspaceMetricsChange () { + onWorkspaceMetricsChange() { const target = this.props.vm.editingTarget; if (target && target.id) { // Dispatch updateMetrics later, since onWorkspaceMetricsChange may be (very indirectly) @@ -326,32 +437,32 @@ class Blocks extends React.Component { targetID: target.id, scrollX: this.workspace.scrollX, scrollY: this.workspace.scrollY, - scale: this.workspace.scale + scale: this.workspace.scale, }); }, 0); } } - onScriptGlowOn (data) { + onScriptGlowOn(data) { this.ScratchBlocks.glowStack(data.id, true); } - onScriptGlowOff (data) { + onScriptGlowOff(data) { this.ScratchBlocks.glowStack(data.id, false); } - onBlockGlowOn (data) { + onBlockGlowOn(data) { // No-op, support may be added in the future } - onBlockGlowOff (data) { + onBlockGlowOff(data) { // No-op, support may be added in the future } - onVisualReport (data) { + onVisualReport(data) { this.ScratchBlocks.reportValue(data.id, data.value); } - getToolboxXML () { + getToolboxXML() { // Use try/catch because this requires digging pretty deep into the VM // Code inside intentionally ignores several error situations (no stage, etc.) // Because they would get caught by this try/catch try { - let {editingTarget: target, runtime} = this.props.vm; + let { editingTarget: target, runtime } = this.props.vm; const stage = runtime.getTargetForStage(); if (!target) target = stage; // If no editingTarget, use the stage @@ -362,24 +473,33 @@ class Blocks extends React.Component { this.props.vm.runtime.getBlocksXML(target), this.props.theme ); - return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, + return makeToolboxXML( + false, + target.isStage, + target.id, + dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, - targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', + targetSounds.length > 0 + ? targetSounds[targetSounds.length - 1].name + : "", getColorsForTheme(this.props.theme) ); } catch { return null; } } - onWorkspaceUpdate (data) { + onWorkspaceUpdate(data) { // When we change sprites, update the toolbox to have the new sprite's blocks const toolboxXML = this.getToolboxXML(); if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } - if (this.props.vm.editingTarget && !this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) { + if ( + this.props.vm.editingTarget && + !this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id] + ) { this.onWorkspaceMetricsChange(); } @@ -387,7 +507,10 @@ class Blocks extends React.Component { this.workspace.removeChangeListener(this.props.vm.blockListener); const dom = this.ScratchBlocks.utils.xml.textToDom(data.xml); try { - this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace); + this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml( + dom, + this.workspace + ); } catch (error) { // The workspace is likely incomplete. What did update should be // functional. @@ -405,8 +528,14 @@ class Blocks extends React.Component { } this.workspace.addChangeListener(this.props.vm.blockListener); - if (this.props.vm.editingTarget && this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) { - const {scrollX, scrollY, scale} = this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]; + if ( + this.props.vm.editingTarget && + this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id] + ) { + const { scrollX, scrollY, scale } = + this.props.workspaceMetrics.targets[ + this.props.vm.editingTarget.id + ]; this.workspace.scrollX = scrollX; this.workspace.scrollY = scrollY; this.workspace.scale = scale; @@ -418,14 +547,14 @@ class Blocks extends React.Component { // workspace to be 'undone' here. this.workspace.clearUndo(); } - handleMonitorsUpdate (monitors) { + handleMonitorsUpdate(monitors) { // Update the checkboxes of the relevant monitors. // TODO: What about monitors that have fields? See todo in scratch-vm blocks.js changeBlock: // https://github.com/LLK/scratch-vm/blob/2373f9483edaf705f11d62662f7bb2a57fbb5e28/src/engine/blocks.js#L569-L576 const flyout = this.workspace.getFlyout(); for (const monitor of monitors.values()) { - const blockId = monitor.get('id'); - const isVisible = monitor.get('visible'); + const blockId = monitor.get("id"); + const isVisible = monitor.get("visible"); flyout.setCheckboxState(blockId, isVisible); // We also need to update the isMonitored flag for this block on the VM, since it's used to determine // whether the checkbox is activated or not when the checkbox is re-displayed (e.g. local variables/blocks @@ -436,28 +565,37 @@ class Blocks extends React.Component { } } } - handleExtensionAdded (categoryInfo) { - const defineBlocks = blockInfoArray => { + handleExtensionAdded(categoryInfo) { + const defineBlocks = (blockInfoArray) => { if (blockInfoArray && blockInfoArray.length > 0) { const staticBlocksJson = []; const dynamicBlocksInfo = []; - blockInfoArray.forEach(blockInfo => { + blockInfoArray.forEach((blockInfo) => { if (blockInfo.info && blockInfo.info.isDynamic) { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { - staticBlocksJson.push(injectExtensionBlockTheme(blockInfo.json, this.props.theme)); + staticBlocksJson.push( + injectExtensionBlockTheme( + blockInfo.json, + this.props.theme + ) + ); } // otherwise it's a non-block entry such as '---' }); this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson); - dynamicBlocksInfo.forEach(blockInfo => { + dynamicBlocksInfo.forEach((blockInfo) => { // This is creating the block factory / constructor -- NOT a specific instance of the block. // The factory should only know static info about the block: the category info and the opcode. // Anything else will be picked up from the XML attached to the block instance. const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`; - const blockDefinition = - defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode); + const blockDefinition = defineDynamicBlock( + this.ScratchBlocks, + categoryInfo, + blockInfo, + extendedOpcode + ); this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition; }); } @@ -466,8 +604,12 @@ class Blocks extends React.Component { // scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block) // these actually define blocks and MUST run regardless of the UI state defineBlocks( - Object.getOwnPropertyNames(categoryInfo.customFieldTypes) - .map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition)); + Object.getOwnPropertyNames(categoryInfo.customFieldTypes).map( + (fieldTypeName) => + categoryInfo.customFieldTypes[fieldTypeName] + .scratchBlocksDefinition + ) + ); defineBlocks(categoryInfo.menus); defineBlocks(categoryInfo.blocks); @@ -477,12 +619,14 @@ class Blocks extends React.Component { this.props.updateToolboxState(toolboxXML); } } - handleBlocksInfoUpdate (categoryInfo) { + handleBlocksInfoUpdate(categoryInfo) { // @todo Later we should replace this to avoid all the warnings from redefining blocks. this.handleExtensionAdded(categoryInfo); } - handleCategorySelected (categoryId) { - const extension = extensionData.find(ext => ext.extensionId === categoryId); + handleCategorySelected(categoryId) { + const extension = extensionData.find( + (ext) => ext.extensionId === categoryId + ); if (extension && extension.launchPeripheralConnectionFlow) { this.handleConnectionModalStart(categoryId); } @@ -492,29 +636,35 @@ class Blocks extends React.Component { toolbox.setSelectedItem(toolbox.getToolboxItemById(categoryId)); }); } - setBlocks (blocks) { + setBlocks(blocks) { this.blocks = blocks; } - handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { - const p = {prompt: {callback, message, defaultValue}}; - p.prompt.title = optTitle ? optTitle : - this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; - p.prompt.varType = typeof optVarType === 'string' ? - optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; + handlePromptStart(message, defaultValue, callback, optTitle, optVarType) { + const p = { prompt: { callback, message, defaultValue } }; + p.prompt.title = optTitle + ? optTitle + : this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; + p.prompt.varType = + typeof optVarType === "string" + ? optVarType + : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && - p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && + p.prompt.title !== + this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; - p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; + p.prompt.showCloudOption = + optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE && + this.props.canUseCloud; this.setState(p); } - handleConnectionModalStart (extensionId) { + handleConnectionModalStart(extensionId) { this.props.onOpenConnectionModal(extensionId); } - handleStatusButtonUpdate () { + handleStatusButtonUpdate() { this.ScratchBlocks.refreshStatusButtons(this.workspace); } - handleOpenSoundRecorder () { + handleOpenSoundRecorder() { this.props.onOpenSoundRecorder(); } @@ -523,32 +673,40 @@ class Blocks extends React.Component { * and additional potentially conflicting variable names from the VM * to the variable validation prompt callback used in scratch-blocks. */ - handlePromptCallback (input, variableOptions) { + handlePromptCallback(input, variableOptions) { this.state.prompt.callback( input, - this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType), - variableOptions); + this.props.vm.runtime.getAllVarNamesOfType( + this.state.prompt.varType + ), + variableOptions + ); this.handlePromptClose(); } - handlePromptClose () { - this.setState({prompt: null}); + handlePromptClose() { + this.setState({ prompt: null }); } - handleCustomProceduresClose (data) { + handleCustomProceduresClose(data) { this.props.onRequestCloseCustomProcedures(data); const ws = this.workspace; this.updateToolbox(); - ws.getToolbox().selectCategoryByName('myBlocks'); + ws.getToolbox().selectCategoryByName("myBlocks"); } - handleDrop (dragInfo) { + handleDrop(dragInfo) { fetch(dragInfo.payload.bodyUrl) - .then(response => response.json()) - .then(blocks => this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id)) + .then((response) => response.json()) + .then((blocks) => + this.props.vm.shareBlocksToTarget( + blocks, + this.props.vm.editingTarget.id + ) + ) .then(() => { this.props.vm.refreshWorkspace(); this.updateToolbox(); // To show new variables/custom blocks }); } - render () { + render() { /* eslint-disable no-unused-vars */ const { anyModalVisible, @@ -585,10 +743,15 @@ class Blocks extends React.Component { @@ -635,10 +798,10 @@ Blocks.propTypes = { zoom: PropTypes.shape({ controls: PropTypes.bool, wheel: PropTypes.bool, - startScale: PropTypes.number + startScale: PropTypes.number, }), comments: PropTypes.bool, - collapse: PropTypes.bool + collapse: PropTypes.bool, }), stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, theme: PropTypes.oneOf(Object.keys(themeMap)), @@ -648,23 +811,23 @@ Blocks.propTypes = { useCatBlocks: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired, workspaceMetrics: PropTypes.shape({ - targets: PropTypes.objectOf(PropTypes.object) - }) + targets: PropTypes.objectOf(PropTypes.object), + }), }; Blocks.defaultOptions = { zoom: { controls: true, wheel: false, - startScale: BLOCKS_DEFAULT_SCALE + startScale: BLOCKS_DEFAULT_SCALE, }, move: { - wheel: true, + wheel: true, }, grid: { spacing: 40, length: 2, - colour: '#ddd' + colour: "#ddd", }, comments: true, collapse: false, @@ -675,14 +838,14 @@ Blocks.defaultOptions = { Blocks.defaultProps = { isVisible: true, options: Blocks.defaultOptions, - theme: DEFAULT_THEME + theme: DEFAULT_THEME, }; -const mapStateToProps = state => ({ - anyModalVisible: ( - Object.keys(state.scratchGui.modals).some(key => state.scratchGui.modals[key]) || - state.scratchGui.mode.isFullScreen - ), +const mapStateToProps = (state) => ({ + anyModalVisible: + Object.keys(state.scratchGui.modals).some( + (key) => state.scratchGui.modals[key] + ) || state.scratchGui.mode.isFullScreen, extensionLibraryVisible: state.scratchGui.modals.extensionLibrary, isRtl: state.locales.isRtl, locale: state.locales.locale, @@ -690,13 +853,15 @@ const mapStateToProps = state => ({ toolboxXML: state.scratchGui.toolbox.toolboxXML, customProceduresVisible: state.scratchGui.customProcedures.active, workspaceMetrics: state.scratchGui.workspaceMetrics, - useCatBlocks: isTimeTravel2020(state) + useCatBlocks: isTimeTravel2020(state), }); -const mapDispatchToProps = dispatch => ({ - onActivateColorPicker: callback => dispatch(activateColorPicker(callback)), - onActivateCustomProcedures: (data, callback) => dispatch(activateCustomProcedures(data, callback)), - onOpenConnectionModal: id => { +const mapDispatchToProps = (dispatch) => ({ + onActivateColorPicker: (callback) => + dispatch(activateColorPicker(callback)), + onActivateCustomProcedures: (data, callback) => + dispatch(activateCustomProcedures(data, callback)), + onOpenConnectionModal: (id) => { dispatch(setConnectionModalExtensionId(id)); dispatch(openConnectionModal()); }, @@ -707,20 +872,17 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseExtensionLibrary: () => { dispatch(closeExtensionLibrary()); }, - onRequestCloseCustomProcedures: data => { + onRequestCloseCustomProcedures: (data) => { dispatch(deactivateCustomProcedures(data)); }, - updateToolboxState: toolboxXML => { + updateToolboxState: (toolboxXML) => { dispatch(updateToolbox(toolboxXML)); }, - updateMetrics: metrics => { + updateMetrics: (metrics) => { dispatch(updateMetrics(metrics)); - } + }, }); -export default errorBoundaryHOC('Blocks')( - connect( - mapStateToProps, - mapDispatchToProps - )(Blocks) +export default errorBoundaryHOC("Blocks")( + connect(mapStateToProps, mapDispatchToProps)(Blocks) ); From ee80f13f40d89f5fd1c8d12a3ddf8470f70891a9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 5 Aug 2024 13:32:41 -0700 Subject: [PATCH 16/32] fix: specify the function to be used for prompting about variables (#17) --- src/containers/blocks.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 9fed0c3ed02..e0304e08e06 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -90,6 +90,9 @@ class Blocks extends React.Component { "setLocale", ]); this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); + this.ScratchBlocks.ScratchVariables.setPromptHandler( + this.handlePromptStart + ); this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; From 505011d64c3fb60c3e75565035bdf8ec49c75bac Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 6 Aug 2024 09:40:36 -0700 Subject: [PATCH 17/32] fix: update the toolbox in response to procedure deletion/creation (#18) --- src/containers/blocks.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e0304e08e06..6e59d3c5dec 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -151,7 +151,11 @@ class Blocks extends React.Component { if ( event.type === this.ScratchBlocks.Events.VAR_CREATE || event.type === this.ScratchBlocks.Events.VAR_RENAME || - event.type === this.ScratchBlocks.Events.VAR_DELETE + event.type === this.ScratchBlocks.Events.VAR_DELETE || + (event.type === this.ScratchBlocks.Events.BLOCK_DELETE && + event.oldJson.type === "procedures_definition") || + (event.type === this.ScratchBlocks.Events.BLOCK_CREATE && + event.json.type === "procedures_definition") ) { this.requestToolboxUpdate(); } From e4229133d884e85fbdcfd1bb7d1480702cddee2c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 12 Aug 2024 15:51:00 -0700 Subject: [PATCH 18/32] chore: don't specify the renderer/theme (#20) --- src/containers/blocks.jsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 6e59d3c5dec..cb4363e0ff4 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -119,10 +119,6 @@ class Blocks extends React.Component { this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - const theme = this.ScratchBlocks.Theme.defineTheme("Scratch", { - base: this.ScratchBlocks.Themes.Zelos, - startHats: true, - }); const workspaceConfig = defaultsDeep( {}, Blocks.defaultOptions, @@ -131,8 +127,6 @@ class Blocks extends React.Component { rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme), - renderer: "zelos", - theme: theme, } ); this.workspace = this.ScratchBlocks.inject( From f6dfd537e39069e281b706429081638d7ed48c0e Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 12 Aug 2024 15:53:44 -0700 Subject: [PATCH 19/32] fix: only refresh the toolbox when procedures are created via undo (#19) * fix: only refresh the toolbox when procedures are created via undo * chore: add comment clarifying procedure block creation handling --- src/containers/blocks.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index cb4363e0ff4..4a3b6dbd94a 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -148,8 +148,12 @@ class Blocks extends React.Component { event.type === this.ScratchBlocks.Events.VAR_DELETE || (event.type === this.ScratchBlocks.Events.BLOCK_DELETE && event.oldJson.type === "procedures_definition") || + // Only refresh the toolbox when procedure block creations are + // triggered by undoing a deletion (implied by recordUndo being + // false on the event). (event.type === this.ScratchBlocks.Events.BLOCK_CREATE && - event.json.type === "procedures_definition") + event.json.type === "procedures_definition" && + !event.recordUndo) ) { this.requestToolboxUpdate(); } From a690749bfca170313f0a5facf3181cc138fff0d5 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 21 Aug 2024 16:02:42 +0000 Subject: [PATCH 20/32] fix: add pinch to zoom support (#21) --- src/containers/blocks.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 4a3b6dbd94a..549763c5ded 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -823,7 +823,8 @@ Blocks.propTypes = { Blocks.defaultOptions = { zoom: { controls: true, - wheel: false, + wheel: true, + pinch: true, startScale: BLOCKS_DEFAULT_SCALE, }, move: { From 45d48031049274b50251a01b65370542d17414ee Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 3 Sep 2024 11:42:47 -0700 Subject: [PATCH 21/32] fix: make dropdown menu shadow block colors consistent (#22) * chore: format blocks.js * fix: define shadow block colors consistently with scratch-blocks * refactor: clean up sound menu recording handler --- src/lib/blocks.js | 337 ++++++++++++++++++++++++++++------------------ 1 file changed, 203 insertions(+), 134 deletions(-) diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 3230729c8ad..149d67897f9 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -5,126 +5,154 @@ * @return {ScratchBlocks} ScratchBlocks connected with the vm */ export default function (vm, useCatBlocks) { - const {ScratchBlocks} = useCatBlocks ? require('cat-blocks') : require('scratch-blocks'); - const jsonForMenuBlock = function (name, menuOptionsFn, colors, start) { + const { ScratchBlocks } = useCatBlocks + ? require("cat-blocks") + : require("scratch-blocks"); + const jsonForMenuBlock = function (name, menuOptionsFn, category, start) { return { - message0: '%1', + message0: "%1", args0: [ { - type: 'field_dropdown', + type: "field_dropdown", name: name, options: function () { return start.concat(menuOptionsFn()); - } - } + }, + }, ], inputsInline: true, - output: 'String', - colour: colors.secondary, - colourSecondary: colors.secondary, - colourTertiary: colors.tertiary, - colourQuaternary: colors.quaternary, - outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND + output: "String", + outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND, + extensions: [`colours_${category}`], }; }; - const jsonForHatBlockMenu = function (hatName, name, menuOptionsFn, colors, start) { + const jsonForHatBlockMenu = function ( + hatName, + name, + menuOptionsFn, + category, + start + ) { return { message0: hatName, args0: [ { - type: 'field_dropdown', + type: "field_dropdown", name: name, options: function () { return start.concat(menuOptionsFn()); - } - } + }, + }, ], - colour: colors.primary, - colourSecondary: colors.secondary, - colourTertiary: colors.tertiary, - colourQuaternary: colors.quaternary, - extensions: ['shape_hat'] + extensions: [`colours_${category}`, "shape_hat"], }; }; - const jsonForSensingMenus = function (menuOptionsFn) { return { message0: ScratchBlocks.Msg.SENSING_OF, args0: [ { - type: 'field_dropdown', - name: 'PROPERTY', + type: "field_dropdown", + name: "PROPERTY", options: function () { return menuOptionsFn(); - } - + }, }, { - type: 'input_value', - name: 'OBJECT' - } + type: "input_value", + name: "OBJECT", + }, ], output: true, - colour: ScratchBlocks.Colours.sensing.primary, - colourSecondary: ScratchBlocks.Colours.sensing.secondary, - colourTertiary: ScratchBlocks.Colours.sensing.tertiary, - colourQuaternary: ScratchBlocks.Colours.sensing.quaternary, - outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND + outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND, + extensions: ["colours_sensing"], }; }; const soundsMenu = function () { - let menu = [['', '']]; + let menu = [["", ""]]; if (vm.editingTarget && vm.editingTarget.sprite.sounds.length > 0) { - menu = vm.editingTarget.sprite.sounds.map(sound => [sound.name, sound.name]); + menu = vm.editingTarget.sprite.sounds.map((sound) => [ + sound.name, + sound.name, + ]); } menu.push([ - ScratchBlocks.ScratchMsgs.translate('SOUND_RECORD', 'record...'), - 'SOUND_RECORD' + ScratchBlocks.ScratchMsgs.translate("SOUND_RECORD", "record..."), + "SOUND_RECORD", ]); return menu; }; const costumesMenu = function () { if (vm.editingTarget && vm.editingTarget.getCostumes().length > 0) { - return vm.editingTarget.getCostumes().map(costume => [costume.name, costume.name]); + return vm.editingTarget + .getCostumes() + .map((costume) => [costume.name, costume.name]); } - return [['', '']]; + return [["", ""]]; }; const backdropsMenu = function () { - const next = ScratchBlocks.ScratchMsgs.translate('LOOKS_NEXTBACKDROP', 'next backdrop'); - const previous = ScratchBlocks.ScratchMsgs.translate('LOOKS_PREVIOUSBACKDROP', 'previous backdrop'); - const random = ScratchBlocks.ScratchMsgs.translate('LOOKS_RANDOMBACKDROP', 'random backdrop'); - if (vm.runtime.targets[0] && vm.runtime.targets[0].getCostumes().length > 0) { - return vm.runtime.targets[0].getCostumes().map(costume => [costume.name, costume.name]) - .concat([[next, 'next backdrop'], - [previous, 'previous backdrop'], - [random, 'random backdrop']]); + const next = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_NEXTBACKDROP", + "next backdrop" + ); + const previous = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_PREVIOUSBACKDROP", + "previous backdrop" + ); + const random = ScratchBlocks.ScratchMsgs.translate( + "LOOKS_RANDOMBACKDROP", + "random backdrop" + ); + if ( + vm.runtime.targets[0] && + vm.runtime.targets[0].getCostumes().length > 0 + ) { + return vm.runtime.targets[0] + .getCostumes() + .map((costume) => [costume.name, costume.name]) + .concat([ + [next, "next backdrop"], + [previous, "previous backdrop"], + [random, "random backdrop"], + ]); } - return [['', '']]; + return [["", ""]]; }; const backdropNamesMenu = function () { const stage = vm.runtime.getTargetForStage(); if (stage && stage.getCostumes().length > 0) { - return stage.getCostumes().map(costume => [costume.name, costume.name]); + return stage + .getCostumes() + .map((costume) => [costume.name, costume.name]); } - return [['', '']]; + return [["", ""]]; }; const spriteMenu = function () { const sprites = []; for (const targetId in vm.runtime.targets) { - if (!Object.prototype.hasOwnProperty.call(vm.runtime.targets, targetId)) continue; + if ( + !Object.prototype.hasOwnProperty.call( + vm.runtime.targets, + targetId + ) + ) + continue; if (vm.runtime.targets[targetId].isOriginal) { if (!vm.runtime.targets[targetId].isStage) { if (vm.runtime.targets[targetId] === vm.editingTarget) { continue; } - sprites.push([vm.runtime.targets[targetId].sprite.name, vm.runtime.targets[targetId].sprite.name]); + sprites.push([ + vm.runtime.targets[targetId].sprite.name, + vm.runtime.targets[targetId].sprite.name, + ]); } } } @@ -135,90 +163,100 @@ export default function (vm, useCatBlocks) { if (vm.editingTarget && vm.editingTarget.isStage) { const menu = spriteMenu(); if (menu.length === 0) { - return [['', '']]; // Empty menu matches Scratch 2 behavior + return [["", ""]]; // Empty menu matches Scratch 2 behavior } return menu; } - const myself = ScratchBlocks.ScratchMsgs.translate('CONTROL_CREATECLONEOF_MYSELF', 'myself'); - return [[myself, '_myself_']].concat(spriteMenu()); + const myself = ScratchBlocks.ScratchMsgs.translate( + "CONTROL_CREATECLONEOF_MYSELF", + "myself" + ); + return [[myself, "_myself_"]].concat(spriteMenu()); }; - const soundColors = ScratchBlocks.Colours.sounds; - - const looksColors = ScratchBlocks.Colours.looks; - - const motionColors = ScratchBlocks.Colours.motion; - - const sensingColors = ScratchBlocks.Colours.sensing; - - const controlColors = ScratchBlocks.Colours.control; - - const eventColors = ScratchBlocks.Colours.event; - ScratchBlocks.Blocks.sound_sounds_menu.init = function () { - const json = jsonForMenuBlock('SOUND_MENU', soundsMenu, soundColors, []); + const json = jsonForMenuBlock("SOUND_MENU", soundsMenu, "sounds", []); this.jsonInit(json); - this.inputList[0].removeField('SOUND_MENU'); - this.inputList[0].appendField(new ScratchBlocks.FieldDropdown(() => { - return soundsMenu(); - }, (newValue) => { - if (newValue === 'SOUND_RECORD') { - ScratchBlocks.recordSoundCallback(); - return null; + this.getField("SOUND_MENU").setValidator((newValue) => { + if (newValue === "SOUND_RECORD") { + ScratchBlocks.recordSoundCallback(); + return null; } return newValue; - }), 'SOUND_MENU'); + }); }; ScratchBlocks.Blocks.looks_costume.init = function () { - const json = jsonForMenuBlock('COSTUME', costumesMenu, looksColors, []); + const json = jsonForMenuBlock("COSTUME", costumesMenu, "looks", []); this.jsonInit(json); }; ScratchBlocks.Blocks.looks_backdrops.init = function () { - const json = jsonForMenuBlock('BACKDROP', backdropsMenu, looksColors, []); + const json = jsonForMenuBlock("BACKDROP", backdropsMenu, "looks", []); this.jsonInit(json); }; ScratchBlocks.Blocks.event_whenbackdropswitchesto.init = function () { const json = jsonForHatBlockMenu( ScratchBlocks.Msg.EVENT_WHENBACKDROPSWITCHESTO, - 'BACKDROP', backdropNamesMenu, eventColors, []); + "BACKDROP", + backdropNamesMenu, + "event", + [] + ); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_pointtowards_menu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_POINTTOWARDS_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TOWARDS', spriteMenu, motionColors, [ - [mouse, '_mouse_'] + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_POINTTOWARDS_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TOWARDS", spriteMenu, "motion", [ + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_goto_menu.init = function () { - const random = ScratchBlocks.ScratchMsgs.translate('MOTION_GOTO_RANDOM', 'random position'); - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_GOTO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [ - [random, '_random_'], - [mouse, '_mouse_'] + const random = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GOTO_RANDOM", + "random position" + ); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GOTO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TO", spriteMenu, "motion", [ + [random, "_random_"], + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.motion_glideto_menu.init = function () { - const random = ScratchBlocks.ScratchMsgs.translate('MOTION_GLIDETO_RANDOM', 'random position'); - const mouse = ScratchBlocks.ScratchMsgs.translate('MOTION_GLIDETO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [ - [random, '_random_'], - [mouse, '_mouse_'] + const random = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GLIDETO_RANDOM", + "random position" + ); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "MOTION_GLIDETO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("TO", spriteMenu, "motion", [ + [random, "_random_"], + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.sensing_of_object_menu.init = function () { - const stage = ScratchBlocks.ScratchMsgs.translate('SENSING_OF_STAGE', 'Stage'); - const json = jsonForMenuBlock('OBJECT', spriteMenu, sensingColors, [ - [stage, '_stage_'] + const stage = ScratchBlocks.ScratchMsgs.translate( + "SENSING_OF_STAGE", + "Stage" + ); + const json = jsonForMenuBlock("OBJECT", spriteMenu, "sensing", [ + [stage, "_stage_"], ]); this.jsonInit(json); }; @@ -230,7 +268,7 @@ export default function (vm, useCatBlocks) { // Get the sensing_of block from vm. let defaultSensingOfBlock; const blocks = vm.runtime.flyoutBlocks._blocks; - Object.keys(blocks).forEach(id => { + Object.keys(blocks).forEach((id) => { const block = blocks[id]; if (id === blockType || (block && block.opcode === blockType)) { defaultSensingOfBlock = block; @@ -241,18 +279,18 @@ export default function (vm, useCatBlocks) { // Called every time it opens since it depends on the values in the other block input. const menuFn = function () { const stageOptions = [ - [ScratchBlocks.Msg.SENSING_OF_BACKDROPNUMBER, 'backdrop #'], - [ScratchBlocks.Msg.SENSING_OF_BACKDROPNAME, 'backdrop name'], - [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNUMBER, "backdrop #"], + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNAME, "backdrop name"], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, "volume"], ]; const spriteOptions = [ - [ScratchBlocks.Msg.SENSING_OF_XPOSITION, 'x position'], - [ScratchBlocks.Msg.SENSING_OF_YPOSITION, 'y position'], - [ScratchBlocks.Msg.SENSING_OF_DIRECTION, 'direction'], - [ScratchBlocks.Msg.SENSING_OF_COSTUMENUMBER, 'costume #'], - [ScratchBlocks.Msg.SENSING_OF_COSTUMENAME, 'costume name'], - [ScratchBlocks.Msg.SENSING_OF_SIZE, 'size'], - [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + [ScratchBlocks.Msg.SENSING_OF_XPOSITION, "x position"], + [ScratchBlocks.Msg.SENSING_OF_YPOSITION, "y position"], + [ScratchBlocks.Msg.SENSING_OF_DIRECTION, "direction"], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENUMBER, "costume #"], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENAME, "costume name"], + [ScratchBlocks.Msg.SENSING_OF_SIZE, "size"], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, "volume"], ]; if (vm.editingTarget) { let lookupBlocks = vm.editingTarget.blocks; @@ -260,31 +298,44 @@ export default function (vm, useCatBlocks) { // The block doesn't exist, but should be in the flyout. Look there. if (!sensingOfBlock) { - sensingOfBlock = vm.runtime.flyoutBlocks.getBlock(blockId) || defaultSensingOfBlock; + sensingOfBlock = + vm.runtime.flyoutBlocks.getBlock(blockId) || + defaultSensingOfBlock; // If we still don't have a block, just return an empty list . This happens during // scratch blocks construction. if (!sensingOfBlock) { - return [['', '']]; + return [["", ""]]; } // The block was in the flyout so look up future block info there. lookupBlocks = vm.runtime.flyoutBlocks; } const sort = function (options) { - options.sort(ScratchBlocks.scratchBlocksUtils.compareStrings); + options.sort( + ScratchBlocks.scratchBlocksUtils.compareStrings + ); }; // Get all the stage variables (no lists) so we can add them to menu when the stage is selected. - const stageVariableOptions = vm.runtime.getTargetForStage().getAllVariableNamesInScopeByType(''); + const stageVariableOptions = vm.runtime + .getTargetForStage() + .getAllVariableNamesInScopeByType(""); sort(stageVariableOptions); - const stageVariableMenuItems = stageVariableOptions.map(variable => [variable, variable]); - if (sensingOfBlock.inputs.OBJECT.shadow !== sensingOfBlock.inputs.OBJECT.block) { + const stageVariableMenuItems = stageVariableOptions.map( + (variable) => [variable, variable] + ); + if ( + sensingOfBlock.inputs.OBJECT.shadow !== + sensingOfBlock.inputs.OBJECT.block + ) { // There's a block dropped on top of the menu. It'd be nice to evaluate it and // return the correct list, but that is tricky. Scratch2 just returns stage options // so just do that here too. return stageOptions.concat(stageVariableMenuItems); } - const menuBlock = lookupBlocks.getBlock(sensingOfBlock.inputs.OBJECT.shadow); + const menuBlock = lookupBlocks.getBlock( + sensingOfBlock.inputs.OBJECT.shadow + ); const selectedItem = menuBlock.fields.OBJECT.value; - if (selectedItem === '_stage_') { + if (selectedItem === "_stage_") { return stageOptions.concat(stageVariableMenuItems); } // Get all the local variables (no lists) and add them to the menu. @@ -292,13 +343,16 @@ export default function (vm, useCatBlocks) { let spriteVariableOptions = []; // The target should exist, but there are ways for it not to (e.g. #4203). if (target) { - spriteVariableOptions = target.getAllVariableNamesInScopeByType('', true); + spriteVariableOptions = + target.getAllVariableNamesInScopeByType("", true); sort(spriteVariableOptions); } - const spriteVariableMenuItems = spriteVariableOptions.map(variable => [variable, variable]); + const spriteVariableMenuItems = spriteVariableOptions.map( + (variable) => [variable, variable] + ); return spriteOptions.concat(spriteVariableMenuItems); } - return [['', '']]; + return [["", ""]]; }; const json = jsonForSensingMenus(menuFn); @@ -306,32 +360,47 @@ export default function (vm, useCatBlocks) { }; ScratchBlocks.Blocks.sensing_distancetomenu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('SENSING_DISTANCETO_POINTER', 'mouse-pointer'); - const json = jsonForMenuBlock('DISTANCETOMENU', spriteMenu, sensingColors, [ - [mouse, '_mouse_'] + const mouse = ScratchBlocks.ScratchMsgs.translate( + "SENSING_DISTANCETO_POINTER", + "mouse-pointer" + ); + const json = jsonForMenuBlock("DISTANCETOMENU", spriteMenu, "sensing", [ + [mouse, "_mouse_"], ]); this.jsonInit(json); }; ScratchBlocks.Blocks.sensing_touchingobjectmenu.init = function () { - const mouse = ScratchBlocks.ScratchMsgs.translate('SENSING_TOUCHINGOBJECT_POINTER', 'mouse-pointer'); - const edge = ScratchBlocks.ScratchMsgs.translate('SENSING_TOUCHINGOBJECT_EDGE', 'edge'); - const json = jsonForMenuBlock('TOUCHINGOBJECTMENU', spriteMenu, sensingColors, [ - [mouse, '_mouse_'], - [edge, '_edge_'] - ]); + const mouse = ScratchBlocks.ScratchMsgs.translate( + "SENSING_TOUCHINGOBJECT_POINTER", + "mouse-pointer" + ); + const edge = ScratchBlocks.ScratchMsgs.translate( + "SENSING_TOUCHINGOBJECT_EDGE", + "edge" + ); + const json = jsonForMenuBlock( + "TOUCHINGOBJECTMENU", + spriteMenu, + "sensing", + [ + [mouse, "_mouse_"], + [edge, "_edge_"], + ] + ); this.jsonInit(json); }; ScratchBlocks.Blocks.control_create_clone_of_menu.init = function () { - const json = jsonForMenuBlock('CLONE_OPTION', cloneMenu, controlColors, []); + const json = jsonForMenuBlock("CLONE_OPTION", cloneMenu, "control", []); this.jsonInit(json); }; - ScratchBlocks.CheckableContinuousFlyout.prototype.getCheckboxState = function (blockId) { - const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; - return monitoredBlock ? monitoredBlock.isMonitored : false; - }; + ScratchBlocks.CheckableContinuousFlyout.prototype.getCheckboxState = + function (blockId) { + const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; + return monitoredBlock ? monitoredBlock.isMonitored : false; + }; // ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { // if (vm.getPeripheralIsConnected(extensionId)) { @@ -348,8 +417,8 @@ export default function (vm, useCatBlocks) { // creates a collator. Using this is a lot faster in browsers that create a // collator for every localeCompare call. const collator = new Intl.Collator([], { - sensitivity: 'base', - numeric: true + sensitivity: "base", + numeric: true, }); // ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) { // return collator.compare(str1, str2); From b7d44ccb97e557e22be58d16266c9704e78a2b99 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 11 Sep 2024 15:11:46 -0700 Subject: [PATCH 22/32] refactor: use block styles instead of directly specifying block colors (#23) * chore: format monitor.jsx * chore: format define-dynamic-block.js * chore: format make-toolbox-xml.js * chore: format blockHelpers.js * chore: format theme definitions * chore: format custom-procedures.jsx * fix: use styles instead of colors for coloring dynamic blocks * refactor: define themes in the format expected by Blockly for BlockStyles * refactor: convert and inject Scratch themes into Blockly * chore: add comments clarifying the color/colour dichotomy --- src/components/monitor/monitor.jsx | 128 +++++----- src/containers/blocks.jsx | 46 +++- src/containers/custom-procedures.jsx | 120 +++++----- src/lib/define-dynamic-block.js | 107 +++++---- src/lib/make-toolbox-xml.js | 325 +++++++++++++++++++------- src/lib/themes/blockHelpers.js | 83 ++++--- src/lib/themes/default/index.js | 142 +++++------ src/lib/themes/high-contrast/index.js | 134 ++++++----- 8 files changed, 650 insertions(+), 435 deletions(-) diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index d9ceb926957..e398445708f 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -1,49 +1,56 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import Draggable from 'react-draggable'; -import {FormattedMessage} from 'react-intl'; -import {ContextMenuTrigger} from 'react-contextmenu'; -import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; -import Box from '../box/box.jsx'; -import DefaultMonitor from './default-monitor.jsx'; -import LargeMonitor from './large-monitor.jsx'; -import SliderMonitor from '../../containers/slider-monitor.jsx'; -import ListMonitor from '../../containers/list-monitor.jsx'; -import {getColorsForTheme} from '../../lib/themes/index.js'; +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import Draggable from "react-draggable"; +import { FormattedMessage } from "react-intl"; +import { ContextMenuTrigger } from "react-contextmenu"; +import { + BorderedMenuItem, + ContextMenu, + MenuItem, +} from "../context-menu/context-menu.jsx"; +import Box from "../box/box.jsx"; +import DefaultMonitor from "./default-monitor.jsx"; +import LargeMonitor from "./large-monitor.jsx"; +import SliderMonitor from "../../containers/slider-monitor.jsx"; +import ListMonitor from "../../containers/list-monitor.jsx"; +import { getColorsForTheme } from "../../lib/themes/index.js"; -import styles from './monitor.css'; +import styles from "./monitor.css"; -// Map category name to color name used in scratch-blocks Blockly.Colours +// Map category name to color name used in scratch-blocks Blockly.Colours. Note +// that Blockly uses the UK spelling of "colour", so fields that interact +// directly with Blockly follow that convention, while Scratch code uses the US +// spelling of "color". const categoryColorMap = { - data: 'data', - sensing: 'sensing', - sound: 'sounds', - looks: 'looks', - motion: 'motion', - list: 'data_lists', - extension: 'pen' + data: "data", + sensing: "sensing", + sound: "sounds", + looks: "looks", + motion: "motion", + list: "data_lists", + extension: "pen", }; const modes = { default: DefaultMonitor, large: LargeMonitor, slider: SliderMonitor, - list: ListMonitor + list: ListMonitor, }; const getCategoryColor = (theme, category) => { const colors = getColorsForTheme(theme); return { - background: colors[categoryColorMap[category]].primary, - text: colors.text + background: colors[categoryColorMap[category]].colourPrimary, + text: colors.text, }; }; -const MonitorComponent = props => ( +const MonitorComponent = (props) => ( ( {React.createElement(modes[props.mode], { - categoryColor: getCategoryColor(props.theme, props.category), - ...props + categoryColor: getCategoryColor( + props.theme, + props.category + ), + ...props, })} - {ReactDOM.createPortal(( + {ReactDOM.createPortal( // Use a portal to render the context menu outside the flow to avoid // positioning conflicts between the monitors `transform: scale` and // the context menus `position: fixed`. For more details, see // http://meyerweb.com/eric/thoughts/2011/09/12/un-fixing-fixed-elements-with-css-transforms/ - {props.onSetModeToDefault && + {props.onSetModeToDefault && ( - } - {props.onSetModeToLarge && + + )} + {props.onSetModeToLarge && ( - } - {props.onSetModeToSlider && + + )} + {props.onSetModeToSlider && ( - } - {props.onSliderPromptOpen && props.mode === 'slider' && + + )} + {props.onSliderPromptOpen && props.mode === "slider" && ( - } - {props.onImport && + + )} + {props.onImport && ( - } - {props.onExport && + + )} + {props.onExport && ( - } - {props.onHide && + + )} + {props.onHide && ( - } - - ), document.body)} + + )} + , + document.body + )} - ); const monitorModes = Object.keys(modes); @@ -149,15 +170,12 @@ MonitorComponent.propTypes = { onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func, onSliderPromptOpen: PropTypes.func, - theme: PropTypes.string.isRequired + theme: PropTypes.string.isRequired, }; MonitorComponent.defaultProps = { - category: 'extension', - mode: 'default' + category: "extension", + mode: "default", }; -export { - MonitorComponent as default, - monitorModes -}; +export { MonitorComponent as default, monitorModes }; diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 549763c5ded..557009feaba 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -23,8 +23,9 @@ import DragConstants from "../lib/drag-constants"; import defineDynamicBlock from "../lib/define-dynamic-block"; import { DEFAULT_THEME, getColorsForTheme, themeMap } from "../lib/themes"; import { - injectExtensionBlockTheme, + injectExtensionBlockIcons, injectExtensionCategoryTheme, + getExtensionColors, } from "../lib/themes/blockHelpers"; import { connect } from "react-redux"; @@ -126,7 +127,10 @@ class Blocks extends React.Component { { rtl: this.props.isRtl, toolbox: this.props.toolboxXML, - colours: getColorsForTheme(this.props.theme), + theme: new this.ScratchBlocks.Theme( + this.props.theme, + getColorsForTheme(this.props.theme) + ), } ); this.workspace = this.ScratchBlocks.inject( @@ -580,7 +584,7 @@ class Blocks extends React.Component { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { staticBlocksJson.push( - injectExtensionBlockTheme( + injectExtensionBlockIcons( blockInfo.json, this.props.theme ) @@ -617,7 +621,39 @@ class Blocks extends React.Component { ); defineBlocks(categoryInfo.menus); defineBlocks(categoryInfo.blocks); - + // Note that Blockly uses the UK spelling of "colour", so fields that + // interact directly with Blockly follow that convention, while Scratch + // code uses the US spelling of "color". + let colourPrimary = categoryInfo.color1; + let colourSecondary = categoryInfo.color2; + let colourTertiary = categoryInfo.color3; + let colourQuaternary = categoryInfo.color3; + if (this.props.theme !== DEFAULT_THEME) { + const colors = getExtensionColors(this.props.theme); + colourPrimary = colors.colourPrimary; + colourSecondary = colors.colourSecondary; + colourTertiary = colors.colourTertiary; + colourQuaternary = colors.colourQuaternary; + } + this.ScratchBlocks.getMainWorkspace() + .getTheme() + .setBlockStyle(categoryInfo.id, { + colourPrimary, + colourSecondary, + colourTertiary, + colourQuaternary, + }); + this.ScratchBlocks.getMainWorkspace() + .getTheme() + .setBlockStyle(`${categoryInfo.id}_selected`, { + colourPrimary: colourQuaternary, + colourSecondary: colourQuaternary, + colourTertiary: colourQuaternary, + colourQuaternary: colourQuaternary, + }); + this.ScratchBlocks.getMainWorkspace().setTheme( + this.ScratchBlocks.getMainWorkspace().getTheme() + ); // Update the toolbox with new blocks if possible const toolboxXML = this.getToolboxXML(); if (toolboxXML) { @@ -734,6 +770,7 @@ class Blocks extends React.Component { updateMetrics: updateMetricsProp, useCatBlocks, workspaceMetrics, + theme, ...props } = this.props; /* eslint-enable no-unused-vars */ @@ -776,6 +813,7 @@ class Blocks extends React.Component { media: options.media, }} onRequestClose={this.handleCustomProceduresClose} + theme={theme} /> ) : null} diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx index de7ac0a0423..20dd9146e3a 100644 --- a/src/containers/custom-procedures.jsx +++ b/src/containers/custom-procedures.jsx @@ -1,52 +1,53 @@ -import bindAll from 'lodash.bindall'; -import defaultsDeep from 'lodash.defaultsdeep'; -import PropTypes from 'prop-types'; -import React from 'react'; -import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx'; -import {ScratchBlocks} from 'scratch-blocks'; -import {connect} from 'react-redux'; +import bindAll from "lodash.bindall"; +import defaultsDeep from "lodash.defaultsdeep"; +import PropTypes from "prop-types"; +import React from "react"; +import CustomProceduresComponent from "../components/custom-procedures/custom-procedures.jsx"; +import { getColorsForTheme, themeMap } from "../lib/themes"; +import { ScratchBlocks } from "scratch-blocks"; +import { connect } from "react-redux"; class CustomProcedures extends React.Component { - constructor (props) { + constructor(props) { super(props); bindAll(this, [ - 'handleAddLabel', - 'handleAddBoolean', - 'handleAddTextNumber', - 'handleToggleWarp', - 'handleCancel', - 'handleOk', - 'setBlocks' + "handleAddLabel", + "handleAddBoolean", + "handleAddTextNumber", + "handleToggleWarp", + "handleCancel", + "handleOk", + "setBlocks", ]); this.state = { rtlOffset: 0, - warp: false + warp: false, }; } - componentWillUnmount () { + componentWillUnmount() { if (this.workspace) { this.workspace.dispose(); } } - setBlocks (blocksRef) { + setBlocks(blocksRef) { if (!blocksRef) return; this.blocks = blocksRef; - const workspaceConfig = defaultsDeep({}, + const workspaceConfig = defaultsDeep( + {}, CustomProcedures.defaultOptions, this.props.options, - {rtl: this.props.isRtl} + { rtl: this.props.isRtl } ); - const theme = ScratchBlocks.Theme.defineTheme('Scratch', { - 'base': ScratchBlocks.Themes.Zelos, - 'startHats': true - }); + const theme = new ScratchBlocks.Theme( + this.props.theme, + getColorsForTheme(this.props.theme) + ); workspaceConfig.theme = theme; - workspaceConfig.renderer = 'zelos'; this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig); // Create the procedure declaration block for editing the mutation. - this.mutationRoot = this.workspace.newBlock('procedures_declaration'); + this.mutationRoot = this.workspace.newBlock("procedures_declaration"); // Make the declaration immovable, undeletable and have no context menu this.mutationRoot.setMovable(false); this.mutationRoot.setDeletable(false); @@ -56,8 +57,9 @@ class CustomProcedures extends React.Component { this.mutationRoot.onChangeFn(); // Keep the block centered on the workspace const metrics = this.workspace.getMetrics(); - const {x, y} = this.mutationRoot.getRelativeToSurfaceXY(); - const dy = (metrics.viewHeight / 2) - (this.mutationRoot.height / 2) - y; + const { x, y } = this.mutationRoot.getRelativeToSurfaceXY(); + const dy = + metrics.viewHeight / 2 - this.mutationRoot.height / 2 - y; let dx; if (this.props.isRtl) { // // TODO: https://github.com/LLK/scratch-gui/issues/2838 @@ -69,8 +71,9 @@ class CustomProcedures extends React.Component { // Calculate a new left postion based on new width // Convert current x position into LTR (mirror) x position (uses original offset) // Use the difference between ltrX and mirrorX as the amount to move - const ltrX = ((metrics.viewWidth / 2) - (this.mutationRoot.width / 2) + 25); - const mirrorX = x - ((x - this.state.rtlOffset) * 2); + const ltrX = + metrics.viewWidth / 2 - this.mutationRoot.width / 2 + 25; + const mirrorX = x - (x - this.state.rtlOffset) * 2; if (mirrorX === ltrX) { return; } @@ -81,19 +84,25 @@ class CustomProcedures extends React.Component { if (this.mutationRoot.width < midPoint) { dx = ltrX; } else if (this.mutationRoot.width < metrics.viewWidth) { - dx = midPoint - ((metrics.viewWidth - this.mutationRoot.width) / 2); + dx = + midPoint - + (metrics.viewWidth - this.mutationRoot.width) / 2; } else { - dx = midPoint + (this.mutationRoot.width - metrics.viewWidth); + dx = + midPoint + + (this.mutationRoot.width - metrics.viewWidth); } this.mutationRoot.moveBy(dx, dy); - this.setState({rtlOffset: this.mutationRoot.getRelativeToSurfaceXY().x}); + this.setState({ + rtlOffset: this.mutationRoot.getRelativeToSurfaceXY().x, + }); return; } if (this.mutationRoot.width > metrics.viewWidth) { dx = dx + this.mutationRoot.width - metrics.viewWidth; } } else { - dx = (metrics.viewWidth / 2) - (this.mutationRoot.width / 2) - x; + dx = metrics.viewWidth / 2 - this.mutationRoot.width / 2 - x; // If the procedure declaration is wider than the view width, // keep the right-hand side of the procedure in view. if (this.mutationRoot.width > metrics.viewWidth) { @@ -105,42 +114,44 @@ class CustomProcedures extends React.Component { this.mutationRoot.domToMutation(this.props.mutator); this.mutationRoot.initSvg(); this.mutationRoot.render(); - this.setState({warp: this.mutationRoot.getWarp()}); + this.setState({ warp: this.mutationRoot.getWarp() }); // Allow the initial events to run to position this block, then focus. setTimeout(() => { this.mutationRoot.focusLastEditor_(); }); } - handleCancel () { + handleCancel() { this.props.onRequestClose(); } - handleOk () { - const newMutation = this.mutationRoot ? this.mutationRoot.mutationToDom(true) : null; + handleOk() { + const newMutation = this.mutationRoot + ? this.mutationRoot.mutationToDom(true) + : null; this.props.onRequestClose(newMutation); } - handleAddLabel () { + handleAddLabel() { if (this.mutationRoot) { this.mutationRoot.addLabelExternal(); } } - handleAddBoolean () { + handleAddBoolean() { if (this.mutationRoot) { this.mutationRoot.addBooleanExternal(); } } - handleAddTextNumber () { + handleAddTextNumber() { if (this.mutationRoot) { this.mutationRoot.addStringNumberExternal(); } } - handleToggleWarp () { + handleToggleWarp() { if (this.mutationRoot) { const newWarp = !this.mutationRoot.getWarp(); this.mutationRoot.setWarp(newWarp); - this.setState({warp: newWarp}); + this.setState({ warp: newWarp }); } } - render () { + render() { return ( ({ +const mapStateToProps = (state) => ({ isRtl: state.locales.isRtl, - mutator: state.scratchGui.customProcedures.mutator + mutator: state.scratchGui.customProcedures.mutator, }); -export default connect( - mapStateToProps -)(CustomProcedures); +export default connect(mapStateToProps)(CustomProcedures); diff --git a/src/lib/define-dynamic-block.js b/src/lib/define-dynamic-block.js index 5f6222ab0d6..248d69ae870 100644 --- a/src/lib/define-dynamic-block.js +++ b/src/lib/define-dynamic-block.js @@ -1,6 +1,6 @@ // TODO: access `BlockType` and `ArgumentType` without reaching into VM // Should we move these into a new extension support module or something? -import {ArgumentType, BlockType} from 'scratch-vm'; +import { ArgumentType, BlockType } from "scratch-vm"; /** * Define a block using extension info which has the ability to dynamically determine (and update) its layout. @@ -13,74 +13,72 @@ import {ArgumentType, BlockType} from 'scratch-vm'; * @param {string} extendedOpcode - The opcode for the block (including the extension ID). */ // TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime -const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({ +const defineDynamicBlock = ( + ScratchBlocks, + categoryInfo, + staticBlockInfo, + extendedOpcode +) => ({ init: function () { const blockJson = { type: extendedOpcode, inputsInline: true, category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3 + style: categoryInfo.id, }; // There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of // blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension // blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly // extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon. if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) { - blockJson.extensions = ['scratch_extension']; + blockJson.extensions = ["scratch_extension"]; } // initialize the basics of the block, to be overridden & extended later by `domToMutation` this.jsonInit(blockJson); // initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom` - this.blockInfoText = '{}'; + this.blockInfoText = "{}"; // we need a block info update (through `domToMutation`) before we have a completely initialized block this.needsBlockInfoUpdate = true; }, mutationToDom: function () { - const container = document.createElement('mutation'); - container.setAttribute('blockInfo', this.blockInfoText); + const container = document.createElement("mutation"); + container.setAttribute("blockInfo", this.blockInfoText); return container; }, domToMutation: function (xmlElement) { - const blockInfoText = xmlElement.getAttribute('blockInfo'); + const blockInfoText = xmlElement.getAttribute("blockInfo"); if (!blockInfoText) return; if (!this.needsBlockInfoUpdate) { - throw new Error('Attempted to update block info twice'); + throw new Error("Attempted to update block info twice"); } delete this.needsBlockInfoUpdate; this.blockInfoText = blockInfoText; const blockInfo = JSON.parse(blockInfoText); switch (blockInfo.blockType) { - case BlockType.COMMAND: - case BlockType.CONDITIONAL: - case BlockType.LOOP: - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); - this.setPreviousStatement(true); - this.setNextStatement(!blockInfo.isTerminal); - break; - case BlockType.REPORTER: - this.setOutput(true); - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND); - if (!blockInfo.disableMonitor) { - this.setCheckboxInFlyout(true); - } - break; - case BlockType.BOOLEAN: - this.setOutput(true); - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL); - break; - case BlockType.HAT: - case BlockType.EVENT: - this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); - this.setNextStatement(true); - break; - } - - if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) { - // `setColour` handles undefined parameters by adjusting defined colors - this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3); + case BlockType.COMMAND: + case BlockType.CONDITIONAL: + case BlockType.LOOP: + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); + this.setPreviousStatement(true); + this.setNextStatement(!blockInfo.isTerminal); + break; + case BlockType.REPORTER: + this.setOutput(true); + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND); + if (!blockInfo.disableMonitor) { + this.setCheckboxInFlyout(true); + } + break; + case BlockType.BOOLEAN: + this.setOutput(true); + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL); + break; + case BlockType.HAT: + case BlockType.EVENT: + this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE); + this.setNextStatement(true); + break; } // Layout block arguments @@ -88,20 +86,27 @@ const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extend const blockText = blockInfo.text; const args = []; let argCount = 0; - const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => { - const arg = blockInfo.arguments[argName]; - switch (arg.type) { - case ArgumentType.STRING: - args.push({type: 'input_value', name: argName}); - break; - case ArgumentType.BOOLEAN: - args.push({type: 'input_value', name: argName, check: 'Boolean'}); - break; + const scratchBlocksStyleText = blockText.replace( + /\[(.+?)]/g, + (match, argName) => { + const arg = blockInfo.arguments[argName]; + switch (arg.type) { + case ArgumentType.STRING: + args.push({ type: "input_value", name: argName }); + break; + case ArgumentType.BOOLEAN: + args.push({ + type: "input_value", + name: argName, + check: "Boolean", + }); + break; + } + return `%${++argCount}`; } - return `%${++argCount}`; - }); + ); this.interpolate_(scratchBlocksStyleText, args); - } + }, }); export default defineDynamicBlock; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 532f9e14c0a..7d6acc24aba 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,5 +1,5 @@ -import {ScratchBlocks} from 'scratch-blocks'; -import {defaultColors} from './themes'; +import { ScratchBlocks } from "scratch-blocks"; +import { defaultColors } from "./themes"; const categorySeparator = ''; @@ -8,15 +8,25 @@ const blockSeparator = ''; // At default scale, about 28px /* eslint-disable no-unused-vars */ const motion = function (isInitialSetup, isStage, targetId, colors) { const stageSelected = ScratchBlocks.ScratchMsgs.translate( - 'MOTION_STAGE_SELECTED', - 'Stage selected: no motion blocks' + "MOTION_STAGE_SELECTED", + "Stage selected: no motion blocks" ); - // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. + // Note: the category's secondaryColour matches up with the blocks' tertiary + // color, both used for border color. Since Blockly uses the UK spelling of + // "colour", certain attributes are named accordingly. return ` - - ${isStage ? ` + + ${ + isStage + ? ` - ` : ` + ` + : ` @@ -135,31 +145,52 @@ const motion = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - `} + ` + } ${categorySeparator} `; }; const xmlEscape = function (unsafe) { - return unsafe.replace(/[<>&'"]/g, c => { + return unsafe.replace(/[<>&'"]/g, (c) => { switch (c) { - case '<': return '<'; - case '>': return '>'; - case '&': return '&'; - case '\'': return '''; - case '"': return '"'; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + case "'": + return "'"; + case '"': + return """; } }); }; -const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName, colors) { - const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!'); - const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); +const looks = function ( + isInitialSetup, + isStage, + targetId, + costumeName, + backdropName, + colors +) { + const hello = ScratchBlocks.ScratchMsgs.translate("LOOKS_HELLO", "Hello!"); + const hmm = ScratchBlocks.ScratchMsgs.translate("LOOKS_HMM", "Hmm..."); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - - ${isStage ? '' : ` + + ${ + isStage + ? "" + : ` @@ -199,8 +230,11 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop ${blockSeparator} - `} - ${isStage ? ` + ` + } + ${ + isStage + ? ` @@ -216,7 +250,8 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - ` : ` + ` + : ` @@ -248,7 +283,8 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - `} + ` + } ${blockSeparator} @@ -266,7 +302,10 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop ${blockSeparator} - ${isStage ? '' : ` + ${ + isStage + ? "" + : ` ${blockSeparator} @@ -278,14 +317,19 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop - `} - ${isStage ? ` + ` + } + ${ + isStage + ? ` - ` : ` + ` + : ` - `} + ` + } ${categorySeparator} `; @@ -294,7 +338,12 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -350,15 +399,24 @@ const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { const events = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + - ${isStage ? ` + ${ + isStage + ? ` - ` : ` + ` + : ` - `} + ` + } ${blockSeparator} @@ -391,10 +449,13 @@ const control = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> @@ -419,13 +480,16 @@ const control = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} ${blockSeparator} - ${isStage ? ` + ${ + isStage + ? ` - ` : ` + ` + : ` @@ -433,22 +497,32 @@ const control = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${categorySeparator} `; }; const sensing = function (isInitialSetup, isStage, targetId, colors) { - const name = ScratchBlocks.ScratchMsgs.translate('SENSING_ASK_TEXT', 'What\'s your name?'); + const name = ScratchBlocks.ScratchMsgs.translate( + "SENSING_ASK_TEXT", + "What's your name?" + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - ${isStage ? '' : ` + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> + ${ + isStage + ? "" + : ` @@ -473,8 +547,12 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - `} - ${isInitialSetup ? '' : ` + ` + } + ${ + isInitialSetup + ? "" + : ` @@ -482,7 +560,8 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${blockSeparator} @@ -493,11 +572,15 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { - ${isStage ? '' : ` + ${ + isStage + ? "" + : ` ${blockSeparator} ''+ ${blockSeparator} - `} + ` + } ${blockSeparator} ${blockSeparator} @@ -520,16 +603,28 @@ const sensing = function (isInitialSetup, isStage, targetId, colors) { }; const operators = function (isInitialSetup, isStage, targetId, colors) { - const apple = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_APPLE', 'apple'); - const banana = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_BANANA', 'banana'); - const letter = ScratchBlocks.ScratchMsgs.translate('OPERATORS_LETTEROF_APPLE', 'a'); + const apple = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_JOIN_APPLE", + "apple" + ); + const banana = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_JOIN_BANANA", + "banana" + ); + const letter = ScratchBlocks.ScratchMsgs.translate( + "OPERATORS_LETTEROF_APPLE", + "a" + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` + colour="${colors.colourPrimary}" + secondaryColour="${colors.colourTertiary}"> @@ -633,7 +728,10 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { ${blockSeparator} - ${isInitialSetup ? '' : ` + ${ + isInitialSetup + ? "" + : ` @@ -677,7 +775,8 @@ const operators = function (isInitialSetup, isStage, targetId, colors) { - `} + ` + } ${blockSeparator} @@ -715,10 +814,13 @@ const variables = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -728,10 +830,13 @@ const myBlocks = function (isInitialSetup, isStage, targetId, colors) { // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -739,7 +844,7 @@ const myBlocks = function (isInitialSetup, isStage, targetId, colors) { /* eslint-enable no-unused-vars */ const xmlOpen = ''; -const xmlClose = ''; +const xmlClose = ""; /** * @param {!boolean} isInitialSetup - Whether the toolbox is for initial setup. If the mode is "initial setup", @@ -757,8 +862,16 @@ const xmlClose = ''; * @param {?object} colors - The colors for the theme. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ -const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [], - costumeName = '', backdropName = '', soundName = '', colors = defaultColors) { +const makeToolboxXML = function ( + isInitialSetup, + isStage = true, + targetId, + categoriesXML = [], + costumeName = "", + backdropName = "", + soundName = "", + colors = defaultColors +) { isStage = isInitialSetup || isStage; const gap = [categorySeparator]; @@ -767,8 +880,10 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ soundName = xmlEscape(soundName); categoriesXML = categoriesXML.slice(); - const moveCategory = categoryId => { - const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId); + const moveCategory = (categoryId) => { + const index = categoriesXML.findIndex( + (categoryInfo) => categoryInfo.id === categoryId + ); if (index >= 0) { // remove the category from categoriesXML and return its XML const [categoryInfo] = categoriesXML.splice(index, 1); @@ -776,28 +891,60 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } // return `undefined` }; - const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId, colors.motion); - const looksXML = moveCategory('looks') || - looks(isInitialSetup, isStage, targetId, costumeName, backdropName, colors.looks); - const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); - const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId, colors.event); - const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId, colors.control); - const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId, colors.sensing); - const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId, colors.operators); - const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId, colors.data); - const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId, colors.more); + const motionXML = + moveCategory("motion") || + motion(isInitialSetup, isStage, targetId, colors.motion); + const looksXML = + moveCategory("looks") || + looks( + isInitialSetup, + isStage, + targetId, + costumeName, + backdropName, + colors.looks + ); + const soundXML = + moveCategory("sound") || + sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); + const eventsXML = + moveCategory("event") || + events(isInitialSetup, isStage, targetId, colors.event); + const controlXML = + moveCategory("control") || + control(isInitialSetup, isStage, targetId, colors.control); + const sensingXML = + moveCategory("sensing") || + sensing(isInitialSetup, isStage, targetId, colors.sensing); + const operatorsXML = + moveCategory("operators") || + operators(isInitialSetup, isStage, targetId, colors.operators); + const variablesXML = + moveCategory("data") || + variables(isInitialSetup, isStage, targetId, colors.data); + const myBlocksXML = + moveCategory("procedures") || + myBlocks(isInitialSetup, isStage, targetId, colors.more); const everything = [ xmlOpen, - motionXML, gap, - looksXML, gap, - soundXML, gap, - eventsXML, gap, - controlXML, gap, - sensingXML, gap, - operatorsXML, gap, - variablesXML, gap, - myBlocksXML + motionXML, + gap, + looksXML, + gap, + soundXML, + gap, + eventsXML, + gap, + controlXML, + gap, + sensingXML, + gap, + operatorsXML, + gap, + variablesXML, + gap, + myBlocksXML, ]; for (const extensionCategory of categoriesXML) { @@ -805,7 +952,7 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } everything.push(xmlClose); - return everything.join('\n'); + return everything.join("\n"); }; export default makeToolboxXML; diff --git a/src/lib/themes/blockHelpers.js b/src/lib/themes/blockHelpers.js index b201241eebb..696c0b57855 100644 --- a/src/lib/themes/blockHelpers.js +++ b/src/lib/themes/blockHelpers.js @@ -1,19 +1,19 @@ -import {DEFAULT_THEME, getColorsForTheme, themeMap} from '.'; +import { DEFAULT_THEME, getColorsForTheme, themeMap } from "."; -const getBlockIconURI = extensionIcons => { +const getBlockIconURI = (extensionIcons) => { if (!extensionIcons) return null; return extensionIcons.blockIconURI || extensionIcons.menuIconURI; }; -const getCategoryIconURI = extensionIcons => { +const getCategoryIconURI = (extensionIcons) => { if (!extensionIcons) return null; return extensionIcons.menuIconURI || extensionIcons.blockIconURI; }; // scratch-blocks colours has a pen property that scratch-gui uses for all extensions -const getExtensionColors = theme => getColorsForTheme(theme).pen; +const getExtensionColors = (theme) => getColorsForTheme(theme).pen; /** * Applies extension color theme to categories. @@ -33,32 +33,52 @@ const injectExtensionCategoryTheme = (dynamicBlockXML, theme) => { const parser = new DOMParser(); const serializer = new XMLSerializer(); - return dynamicBlockXML.map(extension => { - const dom = parser.parseFromString(extension.xml, 'text/xml'); + return dynamicBlockXML.map((extension) => { + const dom = parser.parseFromString(extension.xml, "text/xml"); - dom.documentElement.setAttribute('colour', extensionColors.primary); + // This element is deserialized by Blockly, which uses the UK spelling + // of "colour". + dom.documentElement.setAttribute( + "colour", + extensionColors.colourPrimary + ); // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. - dom.documentElement.setAttribute('secondaryColour', extensionColors.tertiary); - - const categoryIconURI = getCategoryIconURI(extensionIcons[extension.id]); + dom.documentElement.setAttribute( + "secondaryColour", + extensionColors.colourTertiary + ); + + const categoryIconURI = getCategoryIconURI( + extensionIcons[extension.id] + ); if (categoryIconURI) { - dom.documentElement.setAttribute('iconURI', categoryIconURI); + dom.documentElement.setAttribute("iconURI", categoryIconURI); } return { ...extension, - xml: serializer.serializeToString(dom) + xml: serializer.serializeToString(dom), }; }); }; -const injectBlockIcons = (blockInfoJson, theme) => { +const injectExtensionBlockIcons = (blockInfoJson, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return blockInfoJson; + // Block icons are the first element of `args0` - if (!blockInfoJson.args0 || blockInfoJson.args0.length < 1 || - blockInfoJson.args0[0].type !== 'field_image') return blockInfoJson; + if ( + !blockInfoJson.args0 || + blockInfoJson.args0.length < 1 || + blockInfoJson.args0[0].type !== "field_image" + ) + return blockInfoJson; const extensionIcons = themeMap[theme].extensions; - const extensionId = blockInfoJson.type.substring(0, blockInfoJson.type.indexOf('_')); + const extensionId = blockInfoJson.type.substring( + 0, + blockInfoJson.type.indexOf("_") + ); const blockIconURI = getBlockIconURI(extensionIcons[extensionId]); if (!blockIconURI) return blockInfoJson; @@ -70,35 +90,14 @@ const injectBlockIcons = (blockInfoJson, theme) => { return { ...value, - src: blockIconURI + src: blockIconURI, }; - }) - }; -}; - -/** - * Applies extension color theme to static block json. - * No changes are applied if called with the default theme, allowing extensions to provide their own colors. - * @param {object} blockInfoJson - Static block json - * @param {string} theme - Theme name - * @returns {object} Block info json with updated colors. The original blockInfoJson is not modified. - */ -const injectExtensionBlockTheme = (blockInfoJson, theme) => { - // Don't do any manipulation for the default theme - if (theme === DEFAULT_THEME) return blockInfoJson; - - const extensionColors = getExtensionColors(theme); - - return { - ...injectBlockIcons(blockInfoJson, theme), - colour: extensionColors.primary, - colourSecondary: extensionColors.secondary, - colourTertiary: extensionColors.tertiary, - colourQuaternary: extensionColors.quaternary + }), }; }; export { - injectExtensionBlockTheme, - injectExtensionCategoryTheme + injectExtensionBlockIcons, + injectExtensionCategoryTheme, + getExtensionColors, }; diff --git a/src/lib/themes/default/index.js b/src/lib/themes/default/index.js index 3a754dca37f..7047a1c7fd6 100644 --- a/src/lib/themes/default/index.js +++ b/src/lib/themes/default/index.js @@ -1,105 +1,105 @@ +// This object is passed directly to Blockly, hence the colour* fields need to +// be named exactly as they are, including the UK spelling of "colour". const blockColors = { motion: { - primary: '#4C97FF', - secondary: '#4280D7', - tertiary: '#3373CC', - quaternary: '#3373CC' + colourPrimary: "#4C97FF", + colourSecondary: "#4280D7", + colourTertiary: "#3373CC", + colourQuaternary: "#3373CC", }, looks: { - primary: '#9966FF', - secondary: '#855CD6', - tertiary: '#774DCB', - quaternary: '#774DCB' + colourPrimary: "#9966FF", + colourSecondary: "#855CD6", + colourTertiary: "#774DCB", + colourQuaternary: "#774DCB", }, sounds: { - primary: '#CF63CF', - secondary: '#C94FC9', - tertiary: '#BD42BD', - quaternary: '#BD42BD' + colourPrimary: "#CF63CF", + colourSecondary: "#C94FC9", + colourTertiary: "#BD42BD", + colourQuaternary: "#BD42BD", }, control: { - primary: '#FFAB19', - secondary: '#EC9C13', - tertiary: '#CF8B17', - quaternary: '#CF8B17' + colourPrimary: "#FFAB19", + colourSecondary: "#EC9C13", + colourTertiary: "#CF8B17", + colourQuaternary: "#CF8B17", }, event: { - primary: '#FFBF00', - secondary: '#E6AC00', - tertiary: '#CC9900', - quaternary: '#CC9900' + colourPrimary: "#FFBF00", + colourSecondary: "#E6AC00", + colourTertiary: "#CC9900", + colourQuaternary: "#CC9900", }, sensing: { - primary: '#5CB1D6', - secondary: '#47A8D1', - tertiary: '#2E8EB8', - quaternary: '#2E8EB8' + colourPrimary: "#5CB1D6", + colourSecondary: "#47A8D1", + colourTertiary: "#2E8EB8", + colourQuaternary: "#2E8EB8", }, pen: { - primary: '#0fBD8C', - secondary: '#0DA57A', - tertiary: '#0B8E69', - quaternary: '#0B8E69' + colourPrimary: "#0fBD8C", + colourSecondary: "#0DA57A", + colourTertiary: "#0B8E69", + colourQuaternary: "#0B8E69", }, operators: { - primary: '#59C059', - secondary: '#46B946', - tertiary: '#389438', - quaternary: '#389438' + colourPrimary: "#59C059", + colourSecondary: "#46B946", + colourTertiary: "#389438", + colourQuaternary: "#389438", }, data: { - primary: '#FF8C1A', - secondary: '#FF8000', - tertiary: '#DB6E00', - quaternary: '#DB6E00' + colourPrimary: "#FF8C1A", + colourSecondary: "#FF8000", + colourTertiary: "#DB6E00", + colourQuaternary: "#DB6E00", }, // This is not a new category, but rather for differentiation // between lists and scalar variables. data_lists: { - primary: '#FF661A', - secondary: '#FF5500', - tertiary: '#E64D00', - quaternary: '#E64D00' + colourPrimary: "#FF661A", + colourSecondary: "#FF5500", + colourTertiary: "#E64D00", + colourQuaternary: "#E64D00", }, more: { - primary: '#FF6680', - secondary: '#FF4D6A', - tertiary: '#FF3355', - quaternary: '#FF3355' + colourPrimary: "#FF6680", + colourSecondary: "#FF4D6A", + colourTertiary: "#FF3355", + colourQuaternary: "#FF3355", }, - text: '#FFFFFF', - workspace: '#F9F9F9', - toolboxHover: '#4C97FF', - toolboxSelected: '#E9EEF2', - toolboxText: '#575E75', - toolbox: '#FFFFFF', - flyout: '#F9F9F9', - scrollbar: '#CECDCE', - scrollbarHover: '#CECDCE', - textField: '#FFFFFF', - textFieldText: '#575E75', - insertionMarker: '#000000', + text: "#FFFFFF", + workspace: "#F9F9F9", + toolboxHover: "#4C97FF", + toolboxSelected: "#E9EEF2", + toolboxText: "#575E75", + toolbox: "#FFFFFF", + flyout: "#F9F9F9", + scrollbar: "#CECDCE", + scrollbarHover: "#CECDCE", + textField: "#FFFFFF", + textFieldText: "#575E75", + insertionMarker: "#000000", insertionMarkerOpacity: 0.2, dragShadowOpacity: 0.6, - stackGlow: '#FFF200', + stackGlow: "#FFF200", stackGlowSize: 4, stackGlowOpacity: 1, - replacementGlow: '#FFFFFF', + replacementGlow: "#FFFFFF", replacementGlowSize: 2, replacementGlowOpacity: 1, - colourPickerStroke: '#FFFFFF', + colourPickerStroke: "#FFFFFF", // CSS colours: support RGBA - fieldShadow: 'rgba(255, 255, 255, 0.3)', - dropDownShadow: 'rgba(0, 0, 0, .3)', - numPadBackground: '#547AB2', - numPadBorder: '#435F91', - numPadActiveBackground: '#435F91', - numPadText: 'white', // Do not use hex here, it cannot be inlined with data-uri SVG - valueReportBackground: '#FFFFFF', - valueReportBorder: '#AAAAAA', - menuHover: 'rgba(0, 0, 0, 0.2)' + fieldShadow: "rgba(255, 255, 255, 0.3)", + dropDownShadow: "rgba(0, 0, 0, .3)", + numPadBackground: "#547AB2", + numPadBorder: "#435F91", + numPadActiveBackground: "#435F91", + numPadText: "white", // Do not use hex here, it cannot be inlined with data-uri SVG + valueReportBackground: "#FFFFFF", + valueReportBorder: "#AAAAAA", + menuHover: "rgba(0, 0, 0, 0.2)", }; -export { - blockColors -}; +export { blockColors }; diff --git a/src/lib/themes/high-contrast/index.js b/src/lib/themes/high-contrast/index.js index 8a5dd941559..853ff7947ce 100644 --- a/src/lib/themes/high-contrast/index.js +++ b/src/lib/themes/high-contrast/index.js @@ -1,110 +1,108 @@ -import musicIcon from './extensions/musicIcon.svg'; -import penIcon from './extensions/penIcon.svg'; -import text2speechIcon from './extensions/text2speechIcon.svg'; -import translateIcon from './extensions/translateIcon.svg'; -import videoSensingIcon from './extensions/videoSensingIcon.svg'; +import musicIcon from "./extensions/musicIcon.svg"; +import penIcon from "./extensions/penIcon.svg"; +import text2speechIcon from "./extensions/text2speechIcon.svg"; +import translateIcon from "./extensions/translateIcon.svg"; +import videoSensingIcon from "./extensions/videoSensingIcon.svg"; +// This object is passed directly to Blockly, hence the colour* fields need to +// be named exactly as they are, including the UK spelling of "colour". const blockColors = { motion: { - primary: '#80B5FF', - secondary: '#B3D2FF', - tertiary: '#3373CC', - quaternary: '#CCE1FF' + colourPrimary: "#80B5FF", + colourSecondary: "#B3D2FF", + colourTertiary: "#3373CC", + colourQuaternary: "#CCE1FF", }, looks: { - primary: '#CCB3FF', - secondary: '#DDCCFF', - tertiary: '#774DCB', - quaternary: '#EEE5FF' + colourPrimary: "#CCB3FF", + colourSecondary: "#DDCCFF", + colourTertiary: "#774DCB", + colourQuaternary: "#EEE5FF", }, sounds: { - primary: '#E19DE1', - secondary: '#FFB3FF', - tertiary: '#BD42BD', - quaternary: '#FFCCFF' - + colourPrimary: "#E19DE1", + colourSecondary: "#FFB3FF", + colourTertiary: "#BD42BD", + colourQuaternary: "#FFCCFF", }, control: { - primary: '#FFBE4C', - secondary: '#FFDA99', - tertiary: '#CF8B17', - quaternary: '#FFE3B3' + colourPrimary: "#FFBE4C", + colourSecondary: "#FFDA99", + colourTertiary: "#CF8B17", + colourQuaternary: "#FFE3B3", }, event: { - primary: '#FFD966', - secondary: '#FFECB3', - tertiary: '#CC9900', - quaternary: '#FFF2CC' + colourPrimary: "#FFD966", + colourSecondary: "#FFECB3", + colourTertiary: "#CC9900", + colourQuaternary: "#FFF2CC", }, sensing: { - primary: '#85C4E0', - secondary: '#AED8EA', - tertiary: '#2E8EB8', - quaternary: '#C2E2F0' + colourPrimary: "#85C4E0", + colourSecondary: "#AED8EA", + colourTertiary: "#2E8EB8", + colourQuaternary: "#C2E2F0", }, pen: { - primary: '#13ECAF', - secondary: '#75F0CD', - tertiary: '#0B8E69', - quaternary: '#A3F5DE' + colourPrimary: "#13ECAF", + colourSecondary: "#75F0CD", + colourTertiary: "#0B8E69", + colourQuaternary: "#A3F5DE", }, operators: { - primary: '#7ECE7E', - secondary: '#B5E3B5', - tertiary: '#389438', - quaternary: '#DAF1DA' + colourPrimary: "#7ECE7E", + colourSecondary: "#B5E3B5", + colourTertiary: "#389438", + colourQuaternary: "#DAF1DA", }, data: { - primary: '#FFA54C', - secondary: '#FFCC99', - tertiary: '#DB6E00', - quaternary: '#FFE5CC' + colourPrimary: "#FFA54C", + colourSecondary: "#FFCC99", + colourTertiary: "#DB6E00", + colourQuaternary: "#FFE5CC", }, // This is not a new category, but rather for differentiation // between lists and scalar variables. data_lists: { - primary: '#FF9966', - secondary: '#FFCAB0', // I don't think this is used, b/c we don't have any droppable fields in list blocks - tertiary: '#E64D00', - quaternary: '#FFDDCC' + colourPrimary: "#FF9966", + colourSecondary: "#FFCAB0", // I don't think this is used, b/c we don't have any droppable fields in list blocks + colourTertiary: "#E64D00", + colourQuaternary: "#FFDDCC", }, more: { - primary: '#FF99AA', - secondary: '#FFCCD5', - tertiary: '#FF3355', - quaternary: '#FFE5EA' - }, - text: '#000000', - textFieldText: '#000000', // Text inside of inputs e.g. 90 in [point in direction (90)] - toolboxText: '#000000', // Toolbox text, color picker text (used to be #575E75) + colourPrimary: "#FF99AA", + colourSecondary: "#FFCCD5", + colourTertiary: "#FF3355", + colourQuaternary: "#FFE5EA", + }, + text: "#000000", + textFieldText: "#000000", // Text inside of inputs e.g. 90 in [point in direction (90)] + toolboxText: "#000000", // Toolbox text, color picker text (used to be #575E75) // The color that the category menu label (e.g. 'motion', 'looks', etc.) changes to on hover - toolboxHover: '#3373CC', - insertionMarker: '#000000', + toolboxHover: "#3373CC", + insertionMarker: "#000000", insertionMarkerOpacity: 0.2, - fieldShadow: 'rgba(255, 255, 255, 0.3)', + fieldShadow: "rgba(255, 255, 255, 0.3)", dragShadowOpacity: 0.6, - menuHover: 'rgba(255, 255, 255, 0.3)' + menuHover: "rgba(255, 255, 255, 0.3)", }; const extensions = { music: { - blockIconURI: musicIcon + blockIconURI: musicIcon, }, pen: { - blockIconURI: penIcon + blockIconURI: penIcon, }, text2speech: { - blockIconURI: text2speechIcon + blockIconURI: text2speechIcon, }, translate: { - blockIconURI: translateIcon + blockIconURI: translateIcon, }, videoSensing: { - blockIconURI: videoSensingIcon - } + blockIconURI: videoSensingIcon, + }, }; -export { - blockColors, - extensions -}; +export { blockColors, extensions }; From fe03cce8343cac36de15f3a195a8062cfd478f94 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 12 Sep 2024 09:42:07 -0700 Subject: [PATCH 23/32] refactor: improve efficiency of toolbox updates (#24) --- src/containers/blocks.jsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 557009feaba..9a35e1f1f60 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -238,16 +238,6 @@ class Blocks extends React.Component { this.ScratchBlocks.hideChaff(); } - // Only rerender the toolbox when the blocks are visible and the xml is - // different from the previously rendered toolbox xml. - // Do not check against prevProps.toolboxXML because that may not have been rendered. - if ( - this.props.isVisible && - this.props.toolboxXML !== this._renderedToolboxXML - ) { - this.requestToolboxUpdate(); - } - if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size @@ -269,7 +259,6 @@ class Blocks extends React.Component { this.setLocale(); } else { this.props.vm.refreshWorkspace(); - this.requestToolboxUpdate(); } window.dispatchEvent(new Event("resize")); @@ -298,7 +287,6 @@ class Blocks extends React.Component { .then(() => { this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); - this.requestToolboxUpdate(); this.withToolboxUpdates(() => { this.workspace.getFlyout().setRecyclingEnabled(true); }); @@ -744,7 +732,6 @@ class Blocks extends React.Component { ) .then(() => { this.props.vm.refreshWorkspace(); - this.updateToolbox(); // To show new variables/custom blocks }); } render() { From 1745c59d1bbdb210c370e7eab2ca9cb97d3f8c56 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 16 Sep 2024 09:48:17 -0700 Subject: [PATCH 24/32] fix: partially roll back flyout optimization (#25) --- src/containers/blocks.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 9a35e1f1f60..76e1da4b7af 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -238,6 +238,16 @@ class Blocks extends React.Component { this.ScratchBlocks.hideChaff(); } + // Only rerender the toolbox when the blocks are visible and the xml is + // different from the previously rendered toolbox xml. + // Do not check against prevProps.toolboxXML because that may not have been rendered. + if ( + this.props.isVisible && + this.props.toolboxXML !== this._renderedToolboxXML + ) { + this.requestToolboxUpdate(); + } + if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size From d174ae90bd49bc72c510ed113702869831a48bb0 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 18 Sep 2024 12:04:08 -0700 Subject: [PATCH 25/32] fix: prevent exception when switching languages (#27) --- src/containers/blocks.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 76e1da4b7af..73fb611cdbd 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -323,14 +323,17 @@ class Blocks extends React.Component { this.workspace.getToolbox().forceRerender(); this._renderedToolboxXML = this.props.toolboxXML; - const newCategoryScrollPosition = + const newCategoryScrollPosition = this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName); + if (newCategoryScrollPosition) { this.workspace .getFlyout() - .getCategoryScrollPosition(selectedCategoryName).y * scale; - this.workspace - .getFlyout() - .getWorkspace() - .scrollbar.setY(newCategoryScrollPosition + offsetWithinCategory); + .getWorkspace() + .scrollbar.setY( + newCategoryScrollPosition.y * scale + offsetWithinCategory + ); + } const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; From 8b1bf240cefa0e457c874b69b642625b89d008b0 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 18 Sep 2024 12:12:28 -0700 Subject: [PATCH 26/32] fix: fix bug that prevented displaying the procedure editor modal on mobile (#26) --- src/containers/blocks.jsx | 1 + src/containers/custom-procedures.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 73fb611cdbd..9780761eb7d 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -877,6 +877,7 @@ Blocks.defaultOptions = { collapse: false, sounds: false, trashcan: false, + modalInputs: false, }; Blocks.defaultProps = { diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx index 20dd9146e3a..068b9097b16 100644 --- a/src/containers/custom-procedures.jsx +++ b/src/containers/custom-procedures.jsx @@ -193,6 +193,7 @@ CustomProcedures.defaultOptions = { comments: false, collapse: false, scrollbars: true, + modalInputs: false, }; CustomProcedures.defaultProps = { From 9bd036b1ef32ba25bcb4e8eff651881c86293737 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 2 Oct 2024 09:21:22 -0700 Subject: [PATCH 27/32] fix: avoid clearing the state of the sensing_of block by refreshing the toolbox (#28) --- src/containers/blocks.jsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 9780761eb7d..a70f7fdd373 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -145,7 +145,8 @@ class Blocks extends React.Component { "PROCEDURE", this.ScratchBlocks.ScratchProcedures.getProceduresCategory ); - this.workspace.addChangeListener((event) => { + + this.toolboxUpdateChangeListener = (event) => { if ( event.type === this.ScratchBlocks.Events.VAR_CREATE || event.type === this.ScratchBlocks.Events.VAR_RENAME || @@ -161,7 +162,8 @@ class Blocks extends React.Component { ) { this.requestToolboxUpdate(); } - }); + }; + this.workspace.addChangeListener(this.toolboxUpdateChangeListener); // Register buttons under new callback keys for creating variables, // lists, and procedures from extensions. @@ -515,6 +517,7 @@ class Blocks extends React.Component { // Remove and reattach the workspace listener (but allow flyout events) this.workspace.removeChangeListener(this.props.vm.blockListener); + this.workspace.removeChangeListener(this.toolboxUpdateChangeListener); const dom = this.ScratchBlocks.utils.xml.textToDom(data.xml); try { this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml( @@ -556,6 +559,15 @@ class Blocks extends React.Component { // fresh workspace and we don't want any changes made to another sprites // workspace to be 'undone' here. this.workspace.clearUndo(); + // Let events get flushed before readding the toolbox-updater listener + // to avoid unneeded refreshes. + requestAnimationFrame(() => { + setTimeout(() => { + this.workspace.addChangeListener( + this.toolboxUpdateChangeListener + ); + }); + }); } handleMonitorsUpdate(monitors) { // Update the checkboxes of the relevant monitors. From 67a5e0525f5bff0a3ce0dba094a1669718c6bc09 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Oct 2024 08:23:44 -0700 Subject: [PATCH 28/32] refactor: fix compatibility with checkboxes and category status indicators (#29) * refactor: fix compatibility with checkboxes and category status indicators * chore: remove errant logging --- src/containers/blocks.jsx | 6 +++--- src/lib/blocks.js | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index a70f7fdd373..173a7fecb84 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -94,7 +94,7 @@ class Blocks extends React.Component { this.ScratchBlocks.ScratchVariables.setPromptHandler( this.handlePromptStart ); - this.ScratchBlocks.statusButtonCallback = + this.ScratchBlocks.StatusIndicatorLabel.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; @@ -110,7 +110,7 @@ class Blocks extends React.Component { this.props.useCatBlocks ); this.ScratchBlocks.dialog.setPrompt(this.handlePromptStart); - this.ScratchBlocks.statusButtonCallback = + this.ScratchBlocks.StatusIndicatorLabel.statusButtonCallback = this.handleConnectionModalStart; this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder; @@ -716,7 +716,7 @@ class Blocks extends React.Component { this.props.onOpenConnectionModal(extensionId); } handleStatusButtonUpdate() { - this.ScratchBlocks.refreshStatusButtons(this.workspace); + this.workspace.getFlyout().refreshStatusButtons(); } handleOpenSoundRecorder() { this.props.onOpenSoundRecorder(); diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 149d67897f9..298307cab35 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -396,19 +396,20 @@ export default function (vm, useCatBlocks) { this.jsonInit(json); }; - ScratchBlocks.CheckableContinuousFlyout.prototype.getCheckboxState = - function (blockId) { - const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; - return monitoredBlock ? monitoredBlock.isMonitored : false; - }; + ScratchBlocks.CheckboxBubble.prototype.isChecked = function (blockId) { + const monitoredBlock = vm.runtime.monitorBlocks._blocks[blockId]; + return monitoredBlock ? monitoredBlock.isMonitored : false; + }; + + ScratchBlocks.StatusIndicatorLabel.prototype.getExtensionState = function ( + extensionId + ) { + if (vm.getPeripheralIsConnected(extensionId)) { + return ScratchBlocks.StatusButtonState.READY; + } + return ScratchBlocks.StatusButtonState.NOT_READY; + }; - // ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { - // if (vm.getPeripheralIsConnected(extensionId)) { - // return ScratchBlocks.StatusButtonState.READY; - // } - // return ScratchBlocks.StatusButtonState.NOT_READY; - // }; - // // ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { // vm.runtime.emit('PLAY_NOTE', noteNum, extensionId); // }; From b8f19dc5d3cd215e0d5003c566c29aee2f825de9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 4 Feb 2025 15:51:39 -0800 Subject: [PATCH 29/32] fix: fix preserving toolbox scroll position for new continuous toolbox plugin (#30) --- src/containers/blocks.jsx | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 173a7fecb84..92a1a3d5064 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -316,27 +316,28 @@ class Blocks extends React.Component { const selectedCategoryScrollPosition = this.workspace .getFlyout() - .getCategoryScrollPosition(selectedCategoryName).y * scale; + .getCategoryScrollPosition(selectedCategoryName) * scale; const offsetWithinCategory = this.workspace.getFlyout().getWorkspace().getMetrics().viewTop - selectedCategoryScrollPosition; this.workspace.updateToolbox(this.props.toolboxXML); + this.workspace.getToolbox().runAfterRerender(() => { + const newCategoryScrollPosition = this.workspace + .getFlyout() + .getCategoryScrollPosition(selectedCategoryName); + if (newCategoryScrollPosition) { + this.workspace + .getFlyout() + .getWorkspace() + .scrollbar.setY( + newCategoryScrollPosition * scale + offsetWithinCategory + ); + } + }); this.workspace.getToolbox().forceRerender(); this._renderedToolboxXML = this.props.toolboxXML; - const newCategoryScrollPosition = this.workspace - .getFlyout() - .getCategoryScrollPosition(selectedCategoryName); - if (newCategoryScrollPosition) { - this.workspace - .getFlyout() - .getWorkspace() - .scrollbar.setY( - newCategoryScrollPosition.y * scale + offsetWithinCategory - ); - } - const queue = this.toolboxUpdateQueue; this.toolboxUpdateQueue = []; queue.forEach((fn) => fn()); From 05702612208df624eb62270331a4726f7d71eaa5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Feb 2025 13:46:06 -0800 Subject: [PATCH 30/32] fix: fix issue that could prevent variables from appearing in the toolbox (#31) --- src/containers/blocks.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 92a1a3d5064..e41ecdec7ce 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -299,6 +299,7 @@ class Blocks extends React.Component { .then(() => { this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); + this.requestToolboxUpdate(); this.withToolboxUpdates(() => { this.workspace.getFlyout().setRecyclingEnabled(true); }); From 6e46a184a325d2257ad313d08e5f99fb90daec59 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 1 Aug 2025 08:48:04 -0700 Subject: [PATCH 31/32] fix: use serialization wrapper (#32) --- src/containers/blocks.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e41ecdec7ce..8c5c4da0f63 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -522,7 +522,7 @@ class Blocks extends React.Component { this.workspace.removeChangeListener(this.toolboxUpdateChangeListener); const dom = this.ScratchBlocks.utils.xml.textToDom(data.xml); try { - this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml( + this.ScratchBlocks.clearWorkspaceAndLoadFromXml( dom, this.workspace ); From fb762c2671d73570fe4724453e86d15acb65b588 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 1 Aug 2025 08:49:17 -0700 Subject: [PATCH 32/32] fix: plumb `FieldNote` through to the VM (#33) --- src/lib/blocks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 298307cab35..474943751d4 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -410,9 +410,9 @@ export default function (vm, useCatBlocks) { return ScratchBlocks.StatusButtonState.NOT_READY; }; - // ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { - // vm.runtime.emit('PLAY_NOTE', noteNum, extensionId); - // }; + ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) { + vm.runtime.emit("PLAY_NOTE", noteNum, extensionId); + }; // Use a collator's compare instead of localeCompare which internally // creates a collator. Using this is a lot faster in browsers that create a