641 lines
26 KiB
React
641 lines
26 KiB
React
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 {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
|
|
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
|
|
import DragConstants from '../lib/drag-constants';
|
|
|
|
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 {
|
|
activateTab,
|
|
SOUNDS_TAB_INDEX
|
|
} from '../reducers/editor-tab';
|
|
|
|
const addFunctionListener = (object, property, callback) => {
|
|
const oldFn = object[property];
|
|
object[property] = function () {
|
|
const result = oldFn.apply(this, arguments);
|
|
callback.apply(this, result);
|
|
return result;
|
|
};
|
|
};
|
|
|
|
const DroppableBlocks = DropAreaHOC([
|
|
DragConstants.BACKPACK_CODE
|
|
])(BlocksComponent);
|
|
|
|
class Blocks extends React.Component {
|
|
constructor (props) {
|
|
super(props);
|
|
this.ScratchBlocks = VMScratchBlocks(props.vm);
|
|
bindAll(this, [
|
|
'attachVM',
|
|
'detachVM',
|
|
'getToolboxXML',
|
|
'handleCategorySelected',
|
|
'handleConnectionModalStart',
|
|
'handleDrop',
|
|
'handleStatusButtonUpdate',
|
|
'handleOpenSoundRecorder',
|
|
'handlePromptStart',
|
|
'handlePromptCallback',
|
|
'handlePromptClose',
|
|
'handleCustomProceduresClose',
|
|
'onScriptGlowOn',
|
|
'onScriptGlowOff',
|
|
'onBlockGlowOn',
|
|
'onBlockGlowOff',
|
|
'handleExtensionAdded',
|
|
'handleBlocksInfoUpdate',
|
|
'onTargetsUpdate',
|
|
'onVisualReport',
|
|
'onWorkspaceUpdate',
|
|
'onWorkspaceMetricsChange',
|
|
'setBlocks',
|
|
'setLocale'
|
|
]);
|
|
this.ScratchBlocks.prompt = this.handlePromptStart;
|
|
this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart;
|
|
this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder;
|
|
|
|
this.state = {
|
|
workspaceMetrics: {},
|
|
prompt: null
|
|
};
|
|
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
|
|
this.toolboxUpdateQueue = [];
|
|
}
|
|
componentDidMount () {
|
|
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
|
|
this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures;
|
|
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
|
|
|
|
const workspaceConfig = defaultsDeep({},
|
|
Blocks.defaultOptions,
|
|
this.props.options,
|
|
{rtl: this.props.isRtl, toolbox: this.props.toolboxXML}
|
|
);
|
|
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
|
|
|
|
// Store the xml of the toolbox that is actually rendered.
|
|
// This is used in componentDidUpdate instead of prevProps, because
|
|
// 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);
|
|
|
|
this.attachVM();
|
|
// Only update blocks/vm locale when visible to avoid sizing issues
|
|
// If locale changes while not visible it will get handled in didUpdate
|
|
if (this.props.isVisible) {
|
|
this.setLocale();
|
|
}
|
|
}
|
|
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.locale !== nextProps.locale ||
|
|
this.props.anyModalVisible !== nextProps.anyModalVisible ||
|
|
this.props.stageSize !== nextProps.stageSize
|
|
);
|
|
}
|
|
componentDidUpdate (prevProps) {
|
|
// If any modals are open, call hideChaff to close z-indexed field editors
|
|
if (this.props.anyModalVisible && !prevProps.anyModalVisible) {
|
|
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
|
|
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
|
|
this.workspace.setVisible(true);
|
|
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();
|
|
} else {
|
|
this.props.vm.refreshWorkspace();
|
|
}
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
} else {
|
|
this.workspace.setVisible(false);
|
|
}
|
|
}
|
|
componentWillUnmount () {
|
|
this.detachVM();
|
|
this.workspace.dispose();
|
|
clearTimeout(this.toolboxUpdateTimeout);
|
|
}
|
|
requestToolboxUpdate () {
|
|
clearTimeout(this.toolboxUpdateTimeout);
|
|
this.toolboxUpdateTimeout = setTimeout(() => {
|
|
this.updateToolbox();
|
|
}, 0);
|
|
}
|
|
setLocale () {
|
|
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
|
|
this.props.vm.setLocale(this.props.locale, this.props.messages)
|
|
.then(() => {
|
|
this.workspace.getFlyout().setRecyclingEnabled(false);
|
|
this.props.vm.refreshWorkspace();
|
|
this.requestToolboxUpdate();
|
|
this.withToolboxUpdates(() => {
|
|
this.workspace.getFlyout().setRecyclingEnabled(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
updateToolbox () {
|
|
this.toolboxUpdateTimeout = false;
|
|
|
|
const categoryId = this.workspace.toolbox_.getSelectedCategoryId();
|
|
const offset = this.workspace.toolbox_.getCategoryScrollOffset();
|
|
this.workspace.updateToolbox(this.props.toolboxXML);
|
|
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 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 = [];
|
|
queue.forEach(fn => fn());
|
|
}
|
|
|
|
withToolboxUpdates (fn) {
|
|
// if there is a queued toolbox update, we need to wait
|
|
if (this.toolboxUpdateTimeout) {
|
|
this.toolboxUpdateQueue.push(fn);
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
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('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('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()
|
|
.getWorkspace()
|
|
.getBlockById(id);
|
|
if (block) {
|
|
block.inputList[0].fieldRow[0].setValue(value);
|
|
}
|
|
});
|
|
}
|
|
|
|
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());
|
|
});
|
|
}
|
|
}
|
|
onWorkspaceMetricsChange () {
|
|
const target = this.props.vm.editingTarget;
|
|
if (target && target.id) {
|
|
const workspaceMetrics = Object.assign({}, this.state.workspaceMetrics, {
|
|
[target.id]: {
|
|
scrollX: this.workspace.scrollX,
|
|
scrollY: this.workspace.scrollY,
|
|
scale: this.workspace.scale
|
|
}
|
|
});
|
|
this.setState({workspaceMetrics});
|
|
}
|
|
}
|
|
onScriptGlowOn (data) {
|
|
this.workspace.glowStack(data.id, true);
|
|
}
|
|
onScriptGlowOff (data) {
|
|
this.workspace.glowStack(data.id, false);
|
|
}
|
|
onBlockGlowOn (data) {
|
|
this.workspace.glowBlock(data.id, true);
|
|
}
|
|
onBlockGlowOff (data) {
|
|
this.workspace.glowBlock(data.id, false);
|
|
}
|
|
onVisualReport (data) {
|
|
this.workspace.reportValue(data.id, data.value);
|
|
}
|
|
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;
|
|
const stage = runtime.getTargetForStage();
|
|
if (!target) target = stage; // If no editingTarget, use the stage
|
|
|
|
const stageCostumes = stage.getCostumes();
|
|
const targetCostumes = target.getCostumes();
|
|
const targetSounds = target.getSounds();
|
|
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
|
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
|
|
targetCostumes[0].name,
|
|
stageCostumes[0].name,
|
|
targetSounds.length > 0 ? targetSounds[0].name : ''
|
|
);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
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.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
|
|
this.onWorkspaceMetricsChange();
|
|
}
|
|
|
|
// 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);
|
|
try {
|
|
this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
|
|
} catch (error) {
|
|
// The workspace is likely incomplete. What did update should be
|
|
// functional.
|
|
//
|
|
// Instead of throwing the error, by logging it and continuing as
|
|
// normal lets the other workspace update processes complete in the
|
|
// gui and vm, which lets the vm run even if the workspace is
|
|
// incomplete. Throwing the error would keep things like setting the
|
|
// correct editing target from happening which can interfere with
|
|
// some blocks and processes in the vm.
|
|
if (error.message) {
|
|
error.message = `Workspace Update Error: ${error.message}`;
|
|
}
|
|
log.error(error);
|
|
}
|
|
this.workspace.addChangeListener(this.props.vm.blockListener);
|
|
|
|
if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
|
|
const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
|
|
this.workspace.scrollX = scrollX;
|
|
this.workspace.scrollY = scrollY;
|
|
this.workspace.scale = scale;
|
|
this.workspace.resize();
|
|
}
|
|
|
|
// Clear the undo state of the workspace since this is a
|
|
// fresh workspace and we don't want any changes made to another sprites
|
|
// workspace to be 'undone' here.
|
|
this.workspace.clearUndo();
|
|
}
|
|
handleExtensionAdded (blocksInfo) {
|
|
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
|
|
// this actually defines blocks and MUST run regardless of the UI state
|
|
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
|
|
|
|
// Update the toolbox with new blocks
|
|
const toolboxXML = this.getToolboxXML();
|
|
if (toolboxXML) {
|
|
this.props.updateToolboxState(toolboxXML);
|
|
}
|
|
}
|
|
handleBlocksInfoUpdate (blocksInfo) {
|
|
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
|
|
this.handleExtensionAdded(blocksInfo);
|
|
}
|
|
handleCategorySelected (categoryId) {
|
|
const extension = extensionData.find(ext => ext.extensionId === categoryId);
|
|
if (extension && extension.launchPeripheralConnectionFlow) {
|
|
this.handleConnectionModalStart(categoryId);
|
|
}
|
|
|
|
this.withToolboxUpdates(() => {
|
|
this.workspace.toolbox_.setSelectedCategoryById(categoryId);
|
|
});
|
|
}
|
|
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;
|
|
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_LIST_MODAL_TITLE;
|
|
p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud;
|
|
this.setState(p);
|
|
}
|
|
handleConnectionModalStart (extensionId) {
|
|
this.props.onOpenConnectionModal(extensionId);
|
|
}
|
|
handleStatusButtonUpdate () {
|
|
this.ScratchBlocks.refreshStatusButtons(this.workspace);
|
|
}
|
|
handleOpenSoundRecorder () {
|
|
this.props.onOpenSoundRecorder();
|
|
}
|
|
|
|
/*
|
|
* Pass along information about proposed name and variable options (scope and isCloud)
|
|
* and additional potentially conflicting variable names from the VM
|
|
* to the variable validation prompt callback used in scratch-blocks.
|
|
*/
|
|
handlePromptCallback (input, variableOptions) {
|
|
this.state.prompt.callback(
|
|
input,
|
|
this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType),
|
|
variableOptions);
|
|
this.handlePromptClose();
|
|
}
|
|
handlePromptClose () {
|
|
this.setState({prompt: null});
|
|
}
|
|
handleCustomProceduresClose (data) {
|
|
this.props.onRequestCloseCustomProcedures(data);
|
|
const ws = this.workspace;
|
|
ws.refreshToolboxSelection_();
|
|
ws.toolbox_.scrollToCategoryById('myBlocks');
|
|
}
|
|
handleDrop (dragInfo) {
|
|
fetch(dragInfo.payload.bodyUrl)
|
|
.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 () {
|
|
/* eslint-disable no-unused-vars */
|
|
const {
|
|
anyModalVisible,
|
|
canUseCloud,
|
|
customProceduresVisible,
|
|
extensionLibraryVisible,
|
|
options,
|
|
stageSize,
|
|
vm,
|
|
isRtl,
|
|
isVisible,
|
|
onActivateColorPicker,
|
|
onOpenConnectionModal,
|
|
onOpenSoundRecorder,
|
|
updateToolboxState,
|
|
onActivateCustomProcedures,
|
|
onRequestCloseExtensionLibrary,
|
|
onRequestCloseCustomProcedures,
|
|
toolboxXML,
|
|
...props
|
|
} = this.props;
|
|
/* eslint-enable no-unused-vars */
|
|
return (
|
|
<React.Fragment>
|
|
<DroppableBlocks
|
|
componentRef={this.setBlocks}
|
|
onDrop={this.handleDrop}
|
|
{...props}
|
|
/>
|
|
{this.state.prompt ? (
|
|
<Prompt
|
|
defaultValue={this.state.prompt.defaultValue}
|
|
isStage={vm.runtime.getEditingTarget().isStage}
|
|
label={this.state.prompt.message}
|
|
showCloudOption={this.state.prompt.showCloudOption}
|
|
showVariableOptions={this.state.prompt.showVariableOptions}
|
|
title={this.state.prompt.title}
|
|
vm={vm}
|
|
onCancel={this.handlePromptClose}
|
|
onOk={this.handlePromptCallback}
|
|
/>
|
|
) : null}
|
|
{extensionLibraryVisible ? (
|
|
<ExtensionLibrary
|
|
vm={vm}
|
|
onCategorySelected={this.handleCategorySelected}
|
|
onRequestClose={onRequestCloseExtensionLibrary}
|
|
/>
|
|
) : null}
|
|
{customProceduresVisible ? (
|
|
<CustomProcedures
|
|
options={{
|
|
media: options.media
|
|
}}
|
|
onRequestClose={this.handleCustomProceduresClose}
|
|
/>
|
|
) : null}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
Blocks.propTypes = {
|
|
anyModalVisible: PropTypes.bool,
|
|
canUseCloud: PropTypes.bool,
|
|
customProceduresVisible: PropTypes.bool,
|
|
extensionLibraryVisible: PropTypes.bool,
|
|
isRtl: PropTypes.bool,
|
|
isVisible: PropTypes.bool,
|
|
locale: PropTypes.string.isRequired,
|
|
messages: PropTypes.objectOf(PropTypes.string),
|
|
onActivateColorPicker: PropTypes.func,
|
|
onActivateCustomProcedures: PropTypes.func,
|
|
onOpenConnectionModal: PropTypes.func,
|
|
onOpenSoundRecorder: PropTypes.func,
|
|
onRequestCloseCustomProcedures: PropTypes.func,
|
|
onRequestCloseExtensionLibrary: PropTypes.func,
|
|
options: PropTypes.shape({
|
|
media: PropTypes.string,
|
|
zoom: PropTypes.shape({
|
|
controls: PropTypes.bool,
|
|
wheel: PropTypes.bool,
|
|
startScale: PropTypes.number
|
|
}),
|
|
colours: PropTypes.shape({
|
|
workspace: PropTypes.string,
|
|
flyout: PropTypes.string,
|
|
toolbox: PropTypes.string,
|
|
toolboxSelected: PropTypes.string,
|
|
scrollbar: PropTypes.string,
|
|
scrollbarHover: PropTypes.string,
|
|
insertionMarker: PropTypes.string,
|
|
insertionMarkerOpacity: PropTypes.number,
|
|
fieldShadow: PropTypes.string,
|
|
dragShadowOpacity: PropTypes.number
|
|
}),
|
|
comments: PropTypes.bool,
|
|
collapse: PropTypes.bool
|
|
}),
|
|
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
|
|
toolboxXML: PropTypes.string,
|
|
updateToolboxState: PropTypes.func,
|
|
vm: PropTypes.instanceOf(VM).isRequired
|
|
};
|
|
|
|
Blocks.defaultOptions = {
|
|
zoom: {
|
|
controls: true,
|
|
wheel: true,
|
|
startScale: 0.675
|
|
},
|
|
grid: {
|
|
spacing: 40,
|
|
length: 2,
|
|
colour: '#ddd'
|
|
},
|
|
colours: {
|
|
workspace: '#F9F9F9',
|
|
flyout: '#F9F9F9',
|
|
toolbox: '#FFFFFF',
|
|
toolboxSelected: '#E9EEF2',
|
|
scrollbar: '#CECDCE',
|
|
scrollbarHover: '#CECDCE',
|
|
insertionMarker: '#000000',
|
|
insertionMarkerOpacity: 0.2,
|
|
fieldShadow: 'rgba(255, 255, 255, 0.3)',
|
|
dragShadowOpacity: 0.6
|
|
},
|
|
comments: true,
|
|
collapse: false,
|
|
sounds: false
|
|
};
|
|
|
|
Blocks.defaultProps = {
|
|
isVisible: true,
|
|
options: Blocks.defaultOptions
|
|
};
|
|
|
|
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,
|
|
messages: state.locales.messages,
|
|
toolboxXML: state.scratchGui.toolbox.toolboxXML,
|
|
customProceduresVisible: state.scratchGui.customProcedures.active
|
|
});
|
|
|
|
const mapDispatchToProps = dispatch => ({
|
|
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
|
|
onActivateCustomProcedures: (data, callback) => dispatch(activateCustomProcedures(data, callback)),
|
|
onOpenConnectionModal: id => {
|
|
dispatch(setConnectionModalExtensionId(id));
|
|
dispatch(openConnectionModal());
|
|
},
|
|
onOpenSoundRecorder: () => {
|
|
dispatch(activateTab(SOUNDS_TAB_INDEX));
|
|
dispatch(openSoundRecorder());
|
|
},
|
|
onRequestCloseExtensionLibrary: () => {
|
|
dispatch(closeExtensionLibrary());
|
|
},
|
|
onRequestCloseCustomProcedures: data => {
|
|
dispatch(deactivateCustomProcedures(data));
|
|
},
|
|
updateToolboxState: toolboxXML => {
|
|
dispatch(updateToolbox(toolboxXML));
|
|
}
|
|
});
|
|
|
|
export default errorBoundaryHOC('Blocks')(
|
|
connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(Blocks)
|
|
);
|