diff --git a/packages/scratch-gui/src/components/custom-procedures/custom-procedures.jsx b/packages/scratch-gui/src/components/custom-procedures/custom-procedures.jsx index 30eab853003..6aae3f37aba 100644 --- a/packages/scratch-gui/src/components/custom-procedures/custom-procedures.jsx +++ b/packages/scratch-gui/src/components/custom-procedures/custom-procedures.jsx @@ -24,6 +24,7 @@ const CustomProcedures = props => { it.toolboxItemDef_?.id === pendingId); + const name = item?.toolboxItemDef_?.name || item?.name_; + if (item && name) { + if (typeof toolbox.selectCategoryByName === 'function') { + toolbox.selectCategoryByName(name); + } + // ContinuousToolbox.selectCategoryByName updates the toolbox + // selection but does not scroll the flyout. The continuous + // flyout exposes scrollToCategory(item) for that. + const pendingFlyout = this.workspace.getFlyout?.(); + if (pendingFlyout && typeof pendingFlyout.scrollToCategory === 'function') { + pendingFlyout.scrollToCategory(item); + } + } + } } withToolboxUpdates (fn) { @@ -1006,6 +1030,18 @@ class Blocks extends React.Component { if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } + + // After the toolbox finishes its async rebuild, scroll the flyout to + // the newly added extension category. In scratch-blocks v1 the flyout + // automatically focused the just-added category, but the v2 + // continuous toolbox does not do this on its own — the flyout stays + // scrolled to wherever it was, so the user never sees the new blocks. + // + // `updateToolboxState` only dispatches the Redux update; the actual + // `workspace.updateToolbox(...)` rebuild happens later from + // `componentDidUpdate` -> `requestToolboxUpdate` (setTimeout 0). Mark + // the pending category and let the post-rebuild path scroll to it. + this._pendingScrollToCategoryId = categoryInfo.id; } handleBlocksInfoUpdate (categoryInfo) { // @todo Later we should replace this to avoid all the warnings from redefining blocks. @@ -1066,8 +1102,46 @@ class Blocks extends React.Component { handleCustomProceduresClose (data) { this.props.onRequestCloseCustomProcedures(data); const ws = this.workspace; - ws.refreshToolboxSelection_(); - ws.toolbox_.scrollToCategoryById('myBlocks'); + // scratch-blocks v2 renamed `refreshToolboxSelection_` → `refreshToolboxSelection`. + if (typeof ws.refreshToolboxSelection === 'function') { + ws.refreshToolboxSelection(); + } + // The new `procedures_definition` block has been created on the + // workspace by `createProcedureCallbackFactory`. The "My Blocks" + // toolbox category is dynamic (`custom="PROCEDURE"`), and its + // `procedures_call` flyout entry only appears after the toolbox is + // rebuilt. In scratch-blocks v1 `ContinuousToolbox.refreshSelection` + // rebuilt the flyout on every BLOCK_CREATE; v2 made that a no-op, + // so we must explicitly force a rebuild here. Defer until after the + // pending block-create renders flush, otherwise `forceRerender` + // sees the workspace mid-update. + const toolbox = ws.getToolbox?.(); + const myBlocksId = 'myBlocks'; + const scrollMyBlocks = () => { + const items = toolbox?.getToolboxItems?.() || []; + const item = items.find(it => it.toolboxItemDef_?.toolboxitemid === myBlocksId); + const name = item?.toolboxItemDef_?.name || item?.name_; + if (name && typeof toolbox.selectCategoryByName === 'function') { + toolbox.selectCategoryByName(name); + } + const flyout = ws.getFlyout?.(); + if (item && flyout && typeof flyout.scrollToCategory === 'function') { + flyout.scrollToCategory(item); + } + }; + if (toolbox && typeof toolbox.forceRerender === 'function') { + setTimeout(() => { + try { + toolbox.forceRerender(); + } catch (err) { + // forceRerender can throw if dispose paths race; the + // surrounding scroll still works without a rebuild. + } + scrollMyBlocks(); + }, 0); + } else { + scrollMyBlocks(); + } } handleDrop (dragInfo) { fetch(dragInfo.payload.bodyUrl) diff --git a/packages/scratch-gui/src/containers/custom-procedures.jsx b/packages/scratch-gui/src/containers/custom-procedures.jsx index df03fbecbc3..b0c7e4642c7 100644 --- a/packages/scratch-gui/src/containers/custom-procedures.jsx +++ b/packages/scratch-gui/src/containers/custom-procedures.jsx @@ -5,6 +5,8 @@ import React from 'react'; import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx'; import * as ScratchBlocks from 'scratch-blocks'; import {connect} from 'react-redux'; +import {DEFAULT_MODE, getColorsForMode} from '../lib/settings/color-mode'; +import {CAT_BLOCKS_THEME} from '../lib/settings/theme'; class CustomProcedures extends React.Component { constructor (props) { @@ -46,10 +48,22 @@ class CustomProcedures extends React.Component { setBlocks (blocksRef) { if (!blocksRef) return; this.blocks = blocksRef; + // scratch-blocks v2 renders blocks with the default (black) theme + // unless an explicit theme is supplied to `inject`. The main editor + // workspace passes `theme` + `scratchTheme`; we mirror that here so + // the procedure declaration block in the modal uses the same colours + // as the rest of the editor. const workspaceConfig = defaultsDeep({}, CustomProcedures.defaultOptions, this.props.options, - {rtl: this.props.isRtl} + { + rtl: this.props.isRtl, + theme: new ScratchBlocks.Theme( + this.props.colorMode || DEFAULT_MODE, + getColorsForMode(this.props.colorMode || DEFAULT_MODE), + ), + scratchTheme: this.props.useCatBlocks ? 'catblocks' : 'classic', + } ); // @todo This is a hack to make there be no toolbox. @@ -173,6 +187,8 @@ CustomProcedures.propTypes = { isRtl: PropTypes.bool, mutator: PropTypes.instanceOf(Element), onRequestClose: PropTypes.func.isRequired, + colorMode: PropTypes.string, + useCatBlocks: PropTypes.bool, options: PropTypes.shape({ media: PropTypes.string, zoom: PropTypes.shape({ @@ -202,7 +218,9 @@ CustomProcedures.defaultProps = { const mapStateToProps = state => ({ isRtl: state.locales.isRtl, - mutator: state.scratchGui.customProcedures.mutator + mutator: state.scratchGui.customProcedures.mutator, + colorMode: state.scratchGui.settings.colorMode, + useCatBlocks: state.scratchGui.settings.theme === CAT_BLOCKS_THEME, }); export default connect( diff --git a/packages/scratch-gui/src/lib/define-dynamic-block.js b/packages/scratch-gui/src/lib/define-dynamic-block.js index dee3c7fb4df..87334662167 100644 --- a/packages/scratch-gui/src/lib/define-dynamic-block.js +++ b/packages/scratch-gui/src/lib/define-dynamic-block.js @@ -422,12 +422,25 @@ const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extend this.needsBlockInfoUpdate = true; }, mutationToDom: function () { - const container = document.createElement('mutation'); - container.setAttribute('blockInfo', this.blockInfoText); + // `document.createElement('mutation')` creates an HTMLUnknownElement, + // which lowercases attribute names. After XMLSerializer round-trip + // (used by Blockly v2's JSON serialization for `extraState`), the + // camelCase attribute name `blockInfo` becomes `blockinfo`, and + // `getAttribute('blockInfo')` then returns `null` when re-parsed as + // XML. Use Blockly's `utils.xml.createElement` so the element is in + // the XML namespace and attribute case is preserved through the + // round-trip — and also fall back to lowercase `blockinfo` on the + // read side for any state already written with the lowercase form. + const xmlUtils = ScratchBlocks.utils && ScratchBlocks.utils.xml; + const container = xmlUtils && typeof xmlUtils.createElement === 'function' ? + xmlUtils.createElement('mutation') : + document.createElement('mutation'); + container.setAttribute('blockinfo', this.blockInfoText); return container; }, domToMutation: function (xmlElement) { - const blockInfoText = xmlElement.getAttribute('blockInfo'); + const blockInfoText = + xmlElement.getAttribute('blockinfo') || xmlElement.getAttribute('blockInfo'); if (!blockInfoText) return; // === Smalruby: Start of argumentsByMethod support === diff --git a/packages/scratch-gui/webpack.config.js b/packages/scratch-gui/webpack.config.js index 876717d8f31..df9db1a57d4 100644 --- a/packages/scratch-gui/webpack.config.js +++ b/packages/scratch-gui/webpack.config.js @@ -128,13 +128,30 @@ const baseConfig = new ScratchWebpackConfigBuilder( })) .addPlugin(new CopyWebpackPlugin({ patterns: [ + // scratch-blocks v2 (Blockly v12) re-exports some media (e.g. + // `disconnect.mp3`) from the underlying `blockly/media` folder + // rather than re-shipping them in `scratch-blocks/media`. Copy + // those first so that the scratch-blocks-specific assets below + // overwrite anything they need to customise. + { + from: '../../node_modules/blockly/media', + to: 'static/blocks-media/default', + noErrorOnMissing: true + }, + { + from: '../../node_modules/blockly/media', + to: 'static/blocks-media/high-contrast', + noErrorOnMissing: true + }, { from: '../../node_modules/scratch-blocks/media', - to: 'static/blocks-media/default' + to: 'static/blocks-media/default', + force: true }, { from: '../../node_modules/scratch-blocks/media', - to: 'static/blocks-media/high-contrast' + to: 'static/blocks-media/high-contrast', + force: true }, { // overwrite some of the default block media with high-contrast versions diff --git a/packages/scratch-vm/src/extensions/smalruby_ruby/translations.json b/packages/scratch-vm/src/extensions/smalruby_ruby/translations.json index 5ac09491472..2cfe87304e3 100644 --- a/packages/scratch-vm/src/extensions/smalruby_ruby/translations.json +++ b/packages/scratch-vm/src/extensions/smalruby_ruby/translations.json @@ -5,6 +5,7 @@ "smalrubyRuby.arrayMethod": "配列 [RECEIVER] . [METHOD]", "smalrubyRuby.hashMethod": "ハッシュ [RECEIVER] . [METHOD]", "smalrubyRuby.arrayMethodWithBlock": "配列 [RECEIVER] . [METHOD] do", + "smalrubyRuby.hashMethodWithBlock": "ハッシュ [RECEIVER] . [METHOD] do", "smalrubyRuby.numberMethodWithBlock": "数値 [RECEIVER] . [METHOD] do", "smalrubyRuby.blockParam": "ブロックパラメーター [PARAM]", "smalrubyRuby.returnValue": "戻り値", @@ -16,6 +17,7 @@ "smalrubyRuby.arrayMethod": "はいれつ [RECEIVER] . [METHOD]", "smalrubyRuby.hashMethod": "はっしゅ [RECEIVER] . [METHOD]", "smalrubyRuby.arrayMethodWithBlock": "はいれつ [RECEIVER] . [METHOD] do", + "smalrubyRuby.hashMethodWithBlock": "はっしゅ [RECEIVER] . [METHOD] do", "smalrubyRuby.numberMethodWithBlock": "すうち [RECEIVER] . [METHOD] do", "smalrubyRuby.blockParam": "ぶろっくぱらめーたー [PARAM]", "smalrubyRuby.returnValue": "もどりち",