Skip to content

Commit 49e7ca7

Browse files
authored
Merge pull request scratchfoundation#4542 from towerofnix/better-shareBlocksToTarget
Position blocks added from "share the love" within the viewport
2 parents ec67a44 + bd73f83 commit 49e7ca7

File tree

6 files changed

+144
-24
lines changed

6 files changed

+144
-24
lines changed

src/containers/blocks.jsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import ExtensionLibrary from './extension-library.jsx';
1414
import extensionData from '../lib/libraries/extensions/index.jsx';
1515
import CustomProcedures from './custom-procedures.jsx';
1616
import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
17-
import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
17+
import {BLOCKS_DEFAULT_SCALE, STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
1818
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
1919
import DragConstants from '../lib/drag-constants';
2020
import defineDynamicBlock from '../lib/define-dynamic-block';
@@ -25,6 +25,7 @@ import {activateColorPicker} from '../reducers/color-picker';
2525
import {closeExtensionLibrary, openSoundRecorder, openConnectionModal} from '../reducers/modals';
2626
import {activateCustomProcedures, deactivateCustomProcedures} from '../reducers/custom-procedures';
2727
import {setConnectionModalExtensionId} from '../reducers/connection-modal';
28+
import {updateMetrics} from '../reducers/workspace-metrics';
2829

2930
import {
3031
activateTab,
@@ -79,7 +80,6 @@ class Blocks extends React.Component {
7980
this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder;
8081

8182
this.state = {
82-
workspaceMetrics: {},
8383
prompt: null
8484
};
8585
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
@@ -301,14 +301,17 @@ class Blocks extends React.Component {
301301
onWorkspaceMetricsChange () {
302302
const target = this.props.vm.editingTarget;
303303
if (target && target.id) {
304-
const workspaceMetrics = Object.assign({}, this.state.workspaceMetrics, {
305-
[target.id]: {
304+
// Dispatch updateMetrics later, since onWorkspaceMetricsChange may be (very indirectly)
305+
// called from a reducer, i.e. when you create a custom procedure.
306+
// TODO: Is this a vehement hack?
307+
setTimeout(() => {
308+
this.props.updateMetrics({
309+
targetID: target.id,
306310
scrollX: this.workspace.scrollX,
307311
scrollY: this.workspace.scrollY,
308312
scale: this.workspace.scale
309-
}
310-
});
311-
this.setState({workspaceMetrics});
313+
});
314+
}, 0);
312315
}
313316
}
314317
onScriptGlowOn (data) {
@@ -355,7 +358,7 @@ class Blocks extends React.Component {
355358
this.props.updateToolboxState(toolboxXML);
356359
}
357360

358-
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
361+
if (this.props.vm.editingTarget && !this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) {
359362
this.onWorkspaceMetricsChange();
360363
}
361364

@@ -381,8 +384,8 @@ class Blocks extends React.Component {
381384
}
382385
this.workspace.addChangeListener(this.props.vm.blockListener);
383386

384-
if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
385-
const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
387+
if (this.props.vm.editingTarget && this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id]) {
388+
const {scrollX, scrollY, scale} = this.props.workspaceMetrics.targets[this.props.vm.editingTarget.id];
386389
this.workspace.scrollX = scrollX;
387390
this.workspace.scrollY = scrollY;
388391
this.workspace.scale = scale;
@@ -525,6 +528,8 @@ class Blocks extends React.Component {
525528
onRequestCloseExtensionLibrary,
526529
onRequestCloseCustomProcedures,
527530
toolboxXML,
531+
updateMetrics: updateMetricsProp,
532+
workspaceMetrics,
528533
...props
529534
} = this.props;
530535
/* eslint-enable no-unused-vars */
@@ -607,15 +612,19 @@ Blocks.propTypes = {
607612
}),
608613
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
609614
toolboxXML: PropTypes.string,
615+
updateMetrics: PropTypes.func,
610616
updateToolboxState: PropTypes.func,
611-
vm: PropTypes.instanceOf(VM).isRequired
617+
vm: PropTypes.instanceOf(VM).isRequired,
618+
workspaceMetrics: PropTypes.shape({
619+
targets: PropTypes.objectOf(PropTypes.object)
620+
})
612621
};
613622

614623
Blocks.defaultOptions = {
615624
zoom: {
616625
controls: true,
617626
wheel: true,
618-
startScale: 0.675
627+
startScale: BLOCKS_DEFAULT_SCALE
619628
},
620629
grid: {
621630
spacing: 40,
@@ -654,7 +663,8 @@ const mapStateToProps = state => ({
654663
locale: state.locales.locale,
655664
messages: state.locales.messages,
656665
toolboxXML: state.scratchGui.toolbox.toolboxXML,
657-
customProceduresVisible: state.scratchGui.customProcedures.active
666+
customProceduresVisible: state.scratchGui.customProcedures.active,
667+
workspaceMetrics: state.scratchGui.workspaceMetrics
658668
});
659669

660670
const mapDispatchToProps = dispatch => ({
@@ -676,6 +686,9 @@ const mapDispatchToProps = dispatch => ({
676686
},
677687
updateToolboxState: toolboxXML => {
678688
dispatch(updateToolbox(toolboxXML));
689+
},
690+
updateMetrics: metrics => {
691+
dispatch(updateMetrics(metrics));
679692
}
680693
});
681694

src/containers/target-pane.jsx

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {showStandardAlert, closeAlertWithId} from '../reducers/alerts';
1414
import {setRestore} from '../reducers/restore-deletion';
1515
import DragConstants from '../lib/drag-constants';
1616
import TargetPaneComponent from '../components/target-pane/target-pane.jsx';
17+
import {BLOCKS_DEFAULT_SCALE} from '../lib/layout-constants';
1718
import spriteLibraryContent from '../lib/libraries/sprites.json';
1819
import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
1920
import sharedMessages from '../lib/shared-messages';
@@ -156,10 +157,42 @@ class TargetPane extends React.Component {
156157
}
157158
handleBlockDragEnd (blocks) {
158159
if (this.props.hoveredTarget.sprite && this.props.hoveredTarget.sprite !== this.props.editingTarget) {
159-
this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite, this.props.editingTarget);
160+
this.shareBlocks(blocks, this.props.hoveredTarget.sprite, this.props.editingTarget);
160161
this.props.onReceivedBlocks(true);
161162
}
162163
}
164+
shareBlocks (blocks, targetId, optFromTargetId) {
165+
// Position the top-level block based on the scroll position.
166+
const topBlock = blocks.find(block => block.topLevel);
167+
if (topBlock) {
168+
let metrics;
169+
if (this.props.workspaceMetrics.targets[targetId]) {
170+
metrics = this.props.workspaceMetrics.targets[targetId];
171+
} else {
172+
metrics = {
173+
scrollX: 0,
174+
scrollY: 0,
175+
scale: BLOCKS_DEFAULT_SCALE
176+
};
177+
}
178+
179+
// Determine position of the top-level block based on the target's workspace metrics.
180+
const {scrollX, scrollY, scale} = metrics;
181+
const posY = -scrollY + 30;
182+
let posX;
183+
if (this.props.isRtl) {
184+
posX = scrollX + 30;
185+
} else {
186+
posX = -scrollX + 30;
187+
}
188+
189+
// Actually apply the position!
190+
topBlock.x = posX / scale;
191+
topBlock.y = posY / scale;
192+
}
193+
194+
this.props.vm.shareBlocksToTarget(blocks, targetId, optFromTargetId);
195+
}
163196
handleDrop (dragInfo) {
164197
const {sprite: targetId} = this.props.hoveredTarget;
165198
if (dragInfo.dragType === DragConstants.SPRITE) {
@@ -196,22 +229,25 @@ class TargetPane extends React.Component {
196229
} else if (dragInfo.dragType === DragConstants.BACKPACK_CODE) {
197230
fetchCode(dragInfo.payload.bodyUrl)
198231
.then(blocks => {
199-
this.props.vm.shareBlocksToTarget(blocks, targetId);
232+
this.shareBlocks(blocks, targetId);
200233
this.props.vm.refreshWorkspace();
201234
});
202235
}
203236
}
204237
}
205238
render () {
239+
/* eslint-disable no-unused-vars */
206240
const {
207-
onActivateTab, // eslint-disable-line no-unused-vars
208-
onReceivedBlocks, // eslint-disable-line no-unused-vars
209-
onHighlightTarget, // eslint-disable-line no-unused-vars
210-
dispatchUpdateRestore, // eslint-disable-line no-unused-vars
211-
onShowImporting, // eslint-disable-line no-unused-vars
212-
onCloseImporting, // eslint-disable-line no-unused-vars
241+
dispatchUpdateRestore,
242+
onActivateTab,
243+
onCloseImporting,
244+
onHighlightTarget,
245+
onReceivedBlocks,
246+
onShowImporting,
247+
workspaceMetrics,
213248
...componentProps
214249
} = this.props;
250+
/* eslint-enable no-unused-vars */
215251
return (
216252
<TargetPaneComponent
217253
{...componentProps}
@@ -254,10 +290,12 @@ TargetPane.propTypes = {
254290
const mapStateToProps = state => ({
255291
editingTarget: state.scratchGui.targets.editingTarget,
256292
hoveredTarget: state.scratchGui.hoveredTarget,
293+
isRtl: state.locales.isRtl,
294+
spriteLibraryVisible: state.scratchGui.modals.spriteLibrary,
257295
sprites: state.scratchGui.targets.sprites,
258296
stage: state.scratchGui.targets.stage,
259297
raiseSprites: state.scratchGui.blockDrag,
260-
spriteLibraryVisible: state.scratchGui.modals.spriteLibrary
298+
workspaceMetrics: state.scratchGui.workspaceMetrics
261299
});
262300

263301
const mapDispatchToProps = dispatch => ({

src/lib/layout-constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ const STAGE_DISPLAY_SIZES = keyMirror({
3737
small: null
3838
});
3939

40+
// zoom level to start with
41+
const BLOCKS_DEFAULT_SCALE = 0.675;
42+
4043
const STAGE_DISPLAY_SCALES = {};
4144
STAGE_DISPLAY_SCALES[STAGE_DISPLAY_SIZES.large] = 1; // large mode, wide browser (standard)
4245
STAGE_DISPLAY_SCALES[STAGE_DISPLAY_SIZES.largeConstrained] = 0.85; // large mode but narrow browser
@@ -50,6 +53,7 @@ export default {
5053
};
5154

5255
export {
56+
BLOCKS_DEFAULT_SCALE,
5357
STAGE_DISPLAY_SCALES,
5458
STAGE_DISPLAY_SIZES,
5559
STAGE_SIZE_MODES

src/reducers/gui.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import timeoutReducer, {timeoutInitialState} from './timeout';
2525
import toolboxReducer, {toolboxInitialState} from './toolbox';
2626
import vmReducer, {vmInitialState} from './vm';
2727
import vmStatusReducer, {vmStatusInitialState} from './vm-status';
28+
import workspaceMetricsReducer, {workspaceMetricsInitialState} from './workspace-metrics';
2829
import throttle from 'redux-throttle';
2930

3031
import decks from '../lib/libraries/decks/index.jsx';
@@ -57,7 +58,8 @@ const guiInitialState = {
5758
timeout: timeoutInitialState,
5859
toolbox: toolboxInitialState,
5960
vm: vmInitialState,
60-
vmStatus: vmStatusInitialState
61+
vmStatus: vmStatusInitialState,
62+
workspaceMetrics: workspaceMetricsInitialState
6163
};
6264

6365
const initPlayer = function (currentState) {
@@ -155,7 +157,8 @@ const guiReducer = combineReducers({
155157
timeout: timeoutReducer,
156158
toolbox: toolboxReducer,
157159
vm: vmReducer,
158-
vmStatus: vmStatusReducer
160+
vmStatus: vmStatusReducer,
161+
workspaceMetrics: workspaceMetricsReducer
159162
});
160163

161164
export {

src/reducers/workspace-metrics.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const UPDATE_METRICS = 'scratch-gui/workspace-metrics/UPDATE_METRICS';
2+
3+
const initialState = {
4+
targets: {}
5+
};
6+
7+
const reducer = function (state, action) {
8+
if (typeof state === 'undefined') state = initialState;
9+
10+
switch (action.type) {
11+
case UPDATE_METRICS:
12+
return Object.assign({}, state, {
13+
targets: Object.assign({}, state.targets, {
14+
[action.targetID]: {
15+
scrollX: action.scrollX,
16+
scrollY: action.scrollY,
17+
scale: action.scale
18+
}
19+
})
20+
});
21+
default:
22+
return state;
23+
}
24+
};
25+
26+
const updateMetrics = function (metrics) {
27+
return {
28+
type: UPDATE_METRICS,
29+
...metrics
30+
};
31+
};
32+
33+
export {
34+
reducer as default,
35+
initialState as workspaceMetricsInitialState,
36+
updateMetrics
37+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint-env jest */
2+
import workspaceMetricsReducer, {updateMetrics} from '../../../src/reducers/workspace-metrics';
3+
4+
test('initialState', () => {
5+
let defaultState;
6+
/* workspaceMetricsReducer(state, action) */
7+
expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toBeDefined();
8+
expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toEqual({targets: {}});
9+
});
10+
11+
test('updateMetrics action creator', () => {
12+
let defaultState;
13+
const action = updateMetrics({
14+
targetID: 'abcde',
15+
scrollX: 225,
16+
scrollY: 315,
17+
scale: 1.25
18+
});
19+
const resultState = workspaceMetricsReducer(defaultState, action);
20+
expect(Object.keys(resultState.targets).length).toBe(1);
21+
expect(resultState.targets.abcde).toBeDefined();
22+
expect(resultState.targets.abcde.scrollX).toBe(225);
23+
expect(resultState.targets.abcde.scrollY).toBe(315);
24+
expect(resultState.targets.abcde.scale).toBe(1.25);
25+
});

0 commit comments

Comments
 (0)