soiz1's picture
Upload 1525 files
f2bee8a verified
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape, defineMessages} from 'react-intl';
import monitorAdapter from '../lib/monitor-adapter.js';
import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx';
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
import {getVariable, setVariableValue} from '../lib/variable-utils';
import importCSV from '../lib/import-csv';
import downloadBlob from '../lib/download-blob';
import SliderPrompt from './slider-prompt.jsx';
import {connect} from 'react-redux';
import {Map} from 'immutable';
import VM from 'scratch-vm';
const availableModes = opcode => (
monitorModes.filter(mode => {
if (opcode === 'data_variable') {
return mode !== 'list';
} else if (opcode === 'data_listcontents') {
return mode === 'list';
} else if (opcode === 'canvas_canvasGetter') {
return mode === 'image';
}
return mode !== 'slider' && mode !== 'list';
})
);
const messages = defineMessages({
columnPrompt: {
defaultMessage: 'Which column should be used (1-{numberOfColumns})?',
description: 'Prompt for which column should be used',
id: 'gui.monitors.importListColumnPrompt'
}
});
class Monitor extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleDragEnd',
'handleHide',
'handleNextMode',
'handleSetModeToDefault',
'handleSetModeToLarge',
'handleSetModeToSlider',
'handleSliderPromptClose',
'handleSliderPromptOk',
'handleSliderPromptOpen',
'handleImport',
'handleExport',
'setElement'
]);
this.state = {
sliderPrompt: false
};
}
componentDidMount () {
let rect;
const isNum = num => typeof num === 'number' && !isNaN(num);
// Load the VM provided position if not loaded already
// If a monitor has numbers for the x and y positions, load the saved position.
// Otherwise, auto-position the monitor.
if (isNum(this.props.x) && isNum(this.props.y) &&
!this.props.monitorLayout.savedMonitorPositions[this.props.id]) {
rect = {
upperStart: {x: this.props.x, y: this.props.y},
lowerEnd: {x: this.props.x + this.element.offsetWidth, y: this.props.y + this.element.offsetHeight}
};
this.props.addMonitorRect(this.props.id, rect, true /* savePosition */);
} else { // Newly created user monitor
rect = getInitialPosition(
this.props.monitorLayout, this.props.id, this.element.offsetWidth, this.element.offsetHeight);
this.props.addMonitorRect(this.props.id, rect);
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
x: rect.upperStart.x,
y: rect.upperStart.y
}));
}
this.element.style.top = `${rect.upperStart.y}px`;
this.element.style.left = `${rect.upperStart.x}px`;
}
shouldComponentUpdate (nextProps, nextState) {
if (nextState !== this.state) {
return true;
}
for (const key of Object.getOwnPropertyNames(nextProps)) {
// skip all the other things to check custom monitors and see if they need an update
if (key === 'value' && typeof nextProps[key] === 'object') {
return !nextProps[key]._monitorUpToDate;
}
// Don't need to rerender when other monitors are moved.
// monitorLayout is only used during initial layout.
if (key !== 'monitorLayout' && nextProps[key] !== this.props[key]) {
return true;
}
}
return false;
}
componentDidUpdate () {
// tw: if monitor is not draggable (ie. not in editor), do not calculate size of monitor for performance
if (!this.props.draggable) {
return;
}
this.props.resizeMonitorRect(this.props.id, this.element.offsetWidth, this.element.offsetHeight);
}
componentWillUnmount () {
this.props.removeMonitorRect(this.props.id);
}
handleDragEnd (e, {x, y}) {
const newX = parseInt(this.element.style.left, 10) + x;
const newY = parseInt(this.element.style.top, 10) + y;
this.props.onDragEnd(
this.props.id,
newX,
newY
);
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
x: newX,
y: newY
}));
}
handleHide () {
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
visible: false
}));
}
handleNextMode () {
const modes = availableModes(this.props.opcode);
const modeIndex = modes.indexOf(this.props.mode);
const newMode = modes[(modeIndex + 1) % modes.length];
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
mode: newMode
}));
}
handleSetModeToDefault () {
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
mode: 'default'
}));
}
handleSetModeToLarge () {
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
mode: 'large'
}));
}
handleSetModeToSlider () {
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
mode: 'slider'
}));
}
handleSliderPromptClose () {
this.setState({sliderPrompt: false});
}
handleSliderPromptOpen () {
this.setState({sliderPrompt: true});
}
handleSliderPromptOk (min, max, isDiscrete) {
const realMin = Math.min(min, max);
const realMax = Math.max(min, max);
this.props.vm.runtime.requestUpdateMonitor(Map({
id: this.props.id,
sliderMin: realMin,
sliderMax: realMax,
isDiscrete: isDiscrete
}));
this.handleSliderPromptClose();
}
setElement (monitorElt) {
this.element = monitorElt;
}
handleImport () {
importCSV().then(async ({rows, text}) => {
const numberOfColumns = rows[0].length;
let columnNumber = 1;
if (numberOfColumns > 1) {
const msg = this.props.intl.formatMessage(messages.columnPrompt, {numberOfColumns});
// prompt() returns Promise in desktop app
columnNumber = parseInt(await prompt(msg), 10); // eslint-disable-line no-alert
}
let newListValue;
if (isNaN(columnNumber) || numberOfColumns === 1) {
newListValue = text.replace(/\r/g, '').split('\n');
} else {
newListValue = rows.map(row => row[columnNumber - 1])
.filter(item => typeof item === 'string'); // CSV importer can leave undefineds
}
const {vm, targetId, id: variableId} = this.props;
setVariableValue(vm, targetId, variableId, newListValue);
});
}
handleExport () {
const {vm, targetId, id: variableId} = this.props;
const variable = getVariable(vm, targetId, variableId);
const text = variable.value.join('\r\n');
const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
downloadBlob(`${variable.name}.txt`, blob);
}
render () {
const monitorProps = monitorAdapter(this.props);
const showSliderOption = availableModes(this.props.opcode).indexOf('slider') !== -1;
const isList = this.props.mode === 'list';
const isImage = this.props.mode === 'image';
return (
<React.Fragment>
{this.state.sliderPrompt && <SliderPrompt
isDiscrete={this.props.isDiscrete}
maxValue={parseFloat(this.props.max)}
minValue={parseFloat(this.props.min)}
onCancel={this.handleSliderPromptClose}
onOk={this.handleSliderPromptOk}
/>}
<MonitorComponent
componentRef={this.setElement}
{...monitorProps}
opcode={this.props.opcode}
draggable={this.props.draggable}
height={this.props.height}
isDiscrete={this.props.isDiscrete}
max={this.props.max}
min={this.props.min}
mode={this.props.mode}
targetId={this.props.targetId}
width={this.props.width}
onDragEnd={this.handleDragEnd}
onExport={isList || isImage ? this.handleExport : null}
onImport={isList || isImage ? this.handleImport : null}
onHide={this.handleHide}
onNextMode={this.handleNextMode}
onSetModeToDefault={isList || isImage ? null : this.handleSetModeToDefault}
onSetModeToLarge={isList || isImage ? null : this.handleSetModeToLarge}
onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null}
onSliderPromptOpen={this.handleSliderPromptOpen}
/>
</React.Fragment>
);
}
}
Monitor.propTypes = {
addMonitorRect: PropTypes.func.isRequired,
draggable: PropTypes.bool,
height: PropTypes.number,
id: PropTypes.string.isRequired,
intl: intlShape,
isDiscrete: PropTypes.bool,
max: PropTypes.number,
min: PropTypes.number,
mode: PropTypes.oneOf(['default', 'slider', 'large', 'list']),
monitorLayout: PropTypes.shape({
monitors: PropTypes.object, // eslint-disable-line react/forbid-prop-types
savedMonitorPositions: PropTypes.object // eslint-disable-line react/forbid-prop-types
}).isRequired,
onDragEnd: PropTypes.func.isRequired,
opcode: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
params: PropTypes.object, // eslint-disable-line react/no-unused-prop-types, react/forbid-prop-types
removeMonitorRect: PropTypes.func.isRequired,
resizeMonitorRect: PropTypes.func.isRequired,
spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
targetId: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]))
]), // eslint-disable-line react/no-unused-prop-types
vm: PropTypes.instanceOf(VM),
width: PropTypes.number,
x: PropTypes.number,
y: PropTypes.number
};
const mapStateToProps = state => ({
monitorLayout: state.scratchGui.monitorLayout,
vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
addMonitorRect: (id, rect, savePosition) =>
dispatch(addMonitorRect(id, rect.upperStart, rect.lowerEnd, savePosition)),
resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)),
removeMonitorRect: id => dispatch(removeMonitorRect(id))
});
export default injectIntl(connect(
mapStateToProps,
mapDispatchToProps
)(Monitor));