Skip to content
Snippets Groups Projects
Commit 716c979d authored by Ray Schamp's avatar Ray Schamp
Browse files

Recoupling refactor

Confine the logic for adding/removing event listeners between the VM and the components to the relevant components.  While this tightens the coupling between the component and the environment, it makes using the individual components more self-contained.
parent 51d5b7a9
No related branches found
No related tags found
No related merge requests found
const bindAll = require('lodash.bindall');
const defaultsDeep = require('lodash.defaultsdeep'); const defaultsDeep = require('lodash.defaultsdeep');
const React = require('react'); const React = require('react');
const ScratchBlocks = require('scratch-blocks'); const ScratchBlocks = require('scratch-blocks');
class Blocks extends React.Component { class Blocks extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'attachVM',
'detachVM',
'onStackGlowOn',
'onStackGlowOff',
'onBlockGlowOn',
'onBlockGlowOff',
'onVisualReport',
'onWorkspaceUpdate'
]);
}
componentDidMount () { componentDidMount () {
let workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); let workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
this.workspace = ScratchBlocks.inject(this.refs.scratchBlocks, workspaceConfig); this.workspace = ScratchBlocks.inject(this.refs.scratchBlocks, workspaceConfig);
this.props.onReceiveWorkspace(this.workspace); this.attachVM();
} }
componentWillUnmount () { componentWillUnmount () {
this.detachVM();
this.workspace.dispose(); this.workspace.dispose();
} }
attachVM () {
this.workspace.addChangeListener(this.props.vm.blockListener);
this.workspace.getFlyout().getWorkspace().addChangeListener(
this.props.vm.flyoutBlockListener
);
this.props.vm.on('STACK_GLOW_ON', this.onStackGlowOn);
this.props.vm.on('STACK_GLOW_OFF', this.onStackGlowOff);
this.props.vm.on('BLOCK_GLOW_ON', this.onBlockGlowOn);
this.props.vm.on('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.on('VISUAL_REPORT', this.onVisualReport);
this.props.vm.on('workspaceUpdate', this.onWorkspaceUpdate);
}
detachVM () {
this.props.vm.off('STACK_GLOW_ON', this.onStackGlowOn);
this.props.vm.off('STACK_GLOW_OFF', this.onStackGlowOff);
this.props.vm.off('BLOCK_GLOW_ON', this.onBlockGlowOn);
this.props.vm.off('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.off('VISUAL_REPORT', this.onVisualReport);
this.props.vm.off('workspaceUpdate', this.onWorkspaceUpdate);
}
onStackGlowOn (data) {
this.workspace.glowStack(data.id, true);
}
onStackGlowOff (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);
}
onWorkspaceUpdate (data) {
ScratchBlocks.Events.disable();
this.workspace.clear();
let dom = ScratchBlocks.Xml.textToDom(data.xml);
ScratchBlocks.Xml.domToWorkspace(dom, this.workspace);
ScratchBlocks.Events.enable();
}
render () { render () {
return ( return (
<div <div
...@@ -29,10 +86,7 @@ class Blocks extends React.Component { ...@@ -29,10 +86,7 @@ class Blocks extends React.Component {
} }
Blocks.propTypes = { Blocks.propTypes = {
onReceiveWorkspace: React.PropTypes.func,
options: React.PropTypes.shape({ options: React.PropTypes.shape({
// The toolbox is actually an element, but React doesn't agree :/
toolbox: React.PropTypes.object,
media: React.PropTypes.string, media: React.PropTypes.string,
zoom: React.PropTypes.shape({ zoom: React.PropTypes.shape({
controls: React.PropTypes.boolean, controls: React.PropTypes.boolean,
...@@ -72,7 +126,6 @@ Blocks.defaultOptions = { ...@@ -72,7 +126,6 @@ Blocks.defaultOptions = {
}; };
Blocks.defaultProps = { Blocks.defaultProps = {
onReceiveWorkspace: function () {},
options: Blocks.defaultOptions options: Blocks.defaultOptions
}; };
......
const bindAll = require('lodash.bindall');
const React = require('react'); const React = require('react');
const Blocks = require('./blocks'); const Blocks = require('./blocks');
...@@ -12,35 +11,24 @@ const VMManager = require('../lib/vm-manager'); ...@@ -12,35 +11,24 @@ const VMManager = require('../lib/vm-manager');
class GUI extends React.Component { class GUI extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, ['animate', 'onReceiveRenderer', 'onReceiveWorkspace']);
this.state = {};
this.vmManager = new VMManager(this.props.vm); this.vmManager = new VMManager(this.props.vm);
} }
componentDidMount () { componentDidMount () {
this.vmManager.attachKeyboardEvents(); this.vmManager.attachKeyboardEvents();
this.props.vm.loadProject(this.props.projectData); this.props.vm.loadProject(this.props.projectData);
this.props.vm.start(); this.props.vm.start();
requestAnimationFrame(this.animate); this.vmManager.startAnimation();
}
componentWillUnmount () {
this.vmManager.detachKeyboardEvents();
this.props.vm.stopAll();
this.vmManager.stopAnimation();
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (this.props.projectData !== nextProps.projectData) { if (this.props.projectData !== nextProps.projectData) {
this.props.vm.loadProject(nextProps.projectData); this.props.vm.loadProject(nextProps.projectData);
} }
} }
animate () {
this.props.vm.animationFrame();
requestAnimationFrame(this.animate);
}
onReceiveRenderer (renderer, stage) {
this.renderer = renderer;
this.stage = stage;
this.props.vm.attachRenderer(this.renderer);
this.vmManager.attachMouseEvents(this.stage);
}
onReceiveWorkspace (workspace) {
this.workspace = workspace;
this.vmManager.attachWorkspace(this.workspace);
}
render () { render () {
return ( return (
<div <div
...@@ -55,16 +43,13 @@ class GUI extends React.Component { ...@@ -55,16 +43,13 @@ class GUI extends React.Component {
> >
<GreenFlag vm={this.props.vm} /> <GreenFlag vm={this.props.vm} />
<StopAll vm={this.props.vm} /> <StopAll vm={this.props.vm} />
<Stage <Stage vm={this.props.vm} />
onReceiveRenderer={this.onReceiveRenderer}
/>
<SpriteSelector vm={this.props.vm} /> <SpriteSelector vm={this.props.vm} />
<Blocks <Blocks
options={{ options={{
media: this.props.basePath + 'static/blocks-media/' media: this.props.basePath + 'static/blocks-media/'
}} }}
vm={this.props.vm} vm={this.props.vm}
onReceiveWorkspace={this.onReceiveWorkspace}
/> />
</div> </div>
); );
......
...@@ -5,18 +5,89 @@ const Renderer = require('scratch-render'); ...@@ -5,18 +5,89 @@ const Renderer = require('scratch-render');
class Stage extends React.Component { class Stage extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, ['initRenderer']); bindAll(this, [
'attachMouseEvents',
'detachMouseEvents',
'onMouseUp',
'onMouseMove',
'onMouseDown'
]);
} }
initRenderer (stage) { componentDidMount () {
this.stage = stage; this.renderer = new Renderer(this.canvas);
this.renderer = new Renderer(stage); this.props.vm.attachRenderer(this.renderer);
this.props.onReceiveRenderer(this.renderer, this.stage); this.attachMouseEvents(this.canvas);
}
componentWillUnmount () {
this.detachMouseEvents(this.canvas);
}
attachMouseEvents (canvas) {
document.addEventListener('mousemove', this.onMouseMove);
canvas.addEventListener('mouseup', this.onMouseUp);
canvas.addEventListener('mousedown', this.onMouseDown);
}
detachMouseEvents (canvas) {
document.removeEventListener('mousemove', this.onMouseMove);
canvas.removeEventListener('mouseup', this.onMouseUp);
canvas.removeEventListener('mousedown', this.onMouseDown);
}
onMouseMove (e) {
let rect = this.canvas.getBoundingClientRect();
let coordinates = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.props.vm.postIOData('mouse', coordinates);
}
onMouseUp (e) {
let rect = this.canvas.getBoundingClientRect();
let data = {
isDown: false,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.props.vm.postIOData('mouse', data);
e.preventDefault();
}
onMouseDown (e) {
let rect = this.canvas.getBoundingClientRect();
let data = {
isDown: true,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.props.vm.postIOData('mouse', data);
e.preventDefault();
} }
render () {
return (
<StageCanvas
{... this.props}
canvasRef={canvas => this.canvas = canvas}
/>
);
}
}
Stage.propTypes = {
vm: React.PropTypes.shape({
attachRenderer: React.PropTypes.func,
postIOData: React.PropTypes.func
}).isRequired
};
class StageCanvas extends React.Component {
render () { render () {
return ( return (
<canvas <canvas
className="scratch-stage" className="scratch-stage"
ref={this.initRenderer} ref={this.props.canvasRef}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 10, top: 10,
...@@ -29,14 +100,14 @@ class Stage extends React.Component { ...@@ -29,14 +100,14 @@ class Stage extends React.Component {
} }
} }
Stage.propTypes = { StageCanvas.propTypes = {
onReceiveRenderer: React.PropTypes.func, canvasRef: React.PropTypes.func,
width: React.PropTypes.number, width: React.PropTypes.number,
height: React.PropTypes.number height: React.PropTypes.number
}; };
Stage.defaultProps = { StageCanvas.defaultProps = {
onReceiveRenderer: function () {}, canvasRef: function () {},
width: 480, width: 480,
height: 360 height: 360
}; };
......
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const ScratchBlocks = require('scratch-blocks');
class VMManager { class VMManager {
constructor (vm) { constructor (vm) {
bindAll( bindAll(this, [
this, 'attachKeyboardEvents',
['attachWorkspace', 'attachMouseEvents', 'attachKeyboardEvents'] 'detachKeyboardEvents',
); 'onKeyDown',
'onKeyUp',
'animate',
'startAnimation',
'stopAnimation'
]);
this.vm = vm; this.vm = vm;
} }
attachWorkspace (workspace) { startAnimation () {
workspace.addChangeListener(this.vm.blockListener); this.animationFrame = requestAnimationFrame(this.animate);
var flyoutWorkspace = workspace.getFlyout().getWorkspace();
flyoutWorkspace.addChangeListener(this.vm.flyoutBlockListener);
this.vm.on('STACK_GLOW_ON', data => workspace.glowStack(data.id, true));
this.vm.on('STACK_GLOW_OFF', data => workspace.glowStack(data.id, false));
this.vm.on('BLOCK_GLOW_ON', data => workspace.glowBlock(data.id, true));
this.vm.on('BLOCK_GLOW_OFF', data => workspace.glowBlock(data.id, false));
this.vm.on('VISUAL_REPORT', data => workspace.reportValue(data.id, data.value));
this.vm.on('workspaceUpdate', data => {
ScratchBlocks.Events.disable();
workspace.clear();
let dom = ScratchBlocks.Xml.textToDom(data.xml);
ScratchBlocks.Xml.domToWorkspace(dom, workspace);
ScratchBlocks.Events.enable();
});
} }
attachMouseEvents (stage) { stopAnimation () {
document.addEventListener('mousemove', e => { cancelAnimationFrame(this.animationFrame);
let rect = stage.getBoundingClientRect(); }
let coordinates = { animate () {
x: e.clientX - rect.left, this.vm.animationFrame();
y: e.clientY - rect.top, this.animationFrame = requestAnimationFrame(this.animate);
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.vm.postIOData('mouse', coordinates);
});
stage.addEventListener('mousedown', e => {
let rect = stage.getBoundingClientRect();
let data = {
isDown: true,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.vm.postIOData('mouse', data);
e.preventDefault();
});
stage.addEventListener('mouseup', e => {
let rect = stage.getBoundingClientRect();
let data = {
isDown: false,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
canvasWidth: rect.width,
canvasHeight: rect.height
};
this.vm.postIOData('mouse', data);
e.preventDefault();
});
} }
attachKeyboardEvents () { attachKeyboardEvents () {
// Feed keyboard events as VM I/O events. // Feed keyboard events as VM I/O events.
document.addEventListener('keydown', e => { document.addEventListener('keydown', this.onKeyDown);
// Don't capture keys intended for Blockly inputs. document.addEventListener('keyup', this.onKeyUp);
if (e.target != document && e.target != document.body) { }
return; detachKeyboardEvents () {
} document.removeEventListener('keydown', this.onKeyDown);
this.vm.postIOData('keyboard', { document.removeEventListener('keyup', this.onKeyUp);
keyCode: e.keyCode, }
isDown: true onKeyDown (e) {
}); // Don't capture keys intended for Blockly inputs.
e.preventDefault(); if (e.target != document && e.target != document.body) {
return;
}
this.vm.postIOData('keyboard', {
keyCode: e.keyCode,
isDown: true
}); });
document.addEventListener('keyup', e => { e.preventDefault();
// Always capture up events, }
// even those that have switched to other targets. onKeyUp (e) {
this.vm.postIOData('keyboard', { // Always capture up events,
keyCode: e.keyCode, // even those that have switched to other targets.
isDown: false this.vm.postIOData('keyboard', {
}); keyCode: e.keyCode,
// E.g., prevent scroll. isDown: false
if (e.target != document && e.target != document.body) {
e.preventDefault();
}
}); });
// E.g., prevent scroll.
if (e.target != document && e.target != document.body) {
e.preventDefault();
}
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment