Skip to content
Snippets Groups Projects
Commit ac123d62 authored by DD's avatar DD
Browse files

Merge branch 'develop' into blockDrag

parents cff8517b 95ef3e29
No related branches found
No related tags found
No related merge requests found
Showing
with 191 additions and 58 deletions
......@@ -13,6 +13,7 @@ const Selector = props => {
items,
selectedItemIndex,
onDeleteClick,
onDuplicateClick,
onItemClick
} = props;
......@@ -30,6 +31,7 @@ const Selector = props => {
selected={index === selectedItemIndex}
onClick={onItemClick}
onDeleteButtonClick={onDeleteClick}
onDuplicateButtonClick={onDuplicateClick}
/>
))}
</Box>
......@@ -58,6 +60,7 @@ Selector.propTypes = {
name: PropTypes.string.isRequired
})),
onDeleteClick: PropTypes.func,
onDuplicateClick: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
selectedItemIndex: PropTypes.number.isRequired
};
......
......@@ -35,16 +35,18 @@ const messages = defineMessages({
const GUIComponent = props => {
const {
activeTabIndex,
basePath,
blocksTabVisible,
children,
enableExtensions,
intl,
costumesTabVisible,
feedbackFormVisible,
vm,
previewInfoVisible,
intl,
onExtensionButtonClick,
onTabSelect,
tabIndex,
onActivateTab,
previewInfoVisible,
soundsTabVisible,
vm,
...componentProps
} = props;
if (children) {
......@@ -87,9 +89,10 @@ const GUIComponent = props => {
<Tabs
className={tabClassNames.tabs}
forceRenderTabPanel={true} // eslint-disable-line react/jsx-boolean-value
selectedIndex={activeTabIndex}
selectedTabClassName={tabClassNames.tabSelected}
selectedTabPanelClassName={tabClassNames.tabPanelSelected}
onSelect={onTabSelect}
onSelect={onActivateTab}
>
<TabList className={tabClassNames.tabList}>
<Tab className={tabClassNames.tab}>Blocks</Tab>
......@@ -100,7 +103,7 @@ const GUIComponent = props => {
<Box className={styles.blocksWrapper}>
<Blocks
grow={1}
isVisible={tabIndex === 0} // Blocks tab
isVisible={blocksTabVisible}
options={{
media: `${basePath}static/blocks-media/`
}}
......@@ -109,9 +112,7 @@ const GUIComponent = props => {
</Box>
<Box className={styles.extensionButtonContainer}>
<button
className={classNames(styles.extensionButton, {
[styles.hidden]: !enableExtensions
})}
className={styles.extensionButton}
title={intl.formatMessage(messages.addExtension)}
onClick={onExtensionButtonClick}
>
......@@ -124,10 +125,10 @@ const GUIComponent = props => {
</Box>
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
{costumesTabVisible ? <CostumeTab vm={vm} /> : null}
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
{tabIndex === 2 ? <SoundTab vm={vm} /> : null}
{soundsTabVisible ? <SoundTab vm={vm} /> : null}
</TabPanel>
</Tabs>
</Box>
......@@ -162,15 +163,17 @@ const GUIComponent = props => {
);
};
GUIComponent.propTypes = {
activeTabIndex: PropTypes.number,
basePath: PropTypes.string,
blocksTabVisible: PropTypes.bool,
children: PropTypes.node,
enableExtensions: PropTypes.bool,
costumesTabVisible: PropTypes.bool,
feedbackFormVisible: PropTypes.bool,
intl: intlShape.isRequired,
onActivateTab: PropTypes.func,
onExtensionButtonClick: PropTypes.func,
onTabSelect: PropTypes.func,
previewInfoVisible: PropTypes.bool,
tabIndex: PropTypes.number,
soundsTabVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
GUIComponent.defaultProps = {
......
......@@ -9,7 +9,8 @@ const categories = {
sensing: '#5CB1D6',
sound: '#CF63CF',
looks: '#9966FF',
motion: '#4C97FF'
motion: '#4C97FF',
list: '#FC662C'
};
const MonitorComponent = props => (
......
......@@ -252,7 +252,10 @@ class Blocks extends React.Component {
}
handleCustomProceduresClose (data) {
this.props.onRequestCloseCustomProcedures(data);
this.workspace.refreshToolboxSelection_();
const ws = this.workspace;
ws.refreshToolboxSelection_();
// @todo ensure this does not break on localization
ws.toolbox_.scrollToCategoryByName('My Blocks');
}
render () {
/* eslint-disable no-unused-vars */
......
......@@ -51,10 +51,21 @@ class CostumeTab extends React.Component {
bindAll(this, [
'handleSelectCostume',
'handleDeleteCostume',
'handleDuplicateCostume',
'handleNewCostume',
'handleNewBlankCostume'
]);
this.state = {selectedCostumeIndex: 0};
const {
editingTarget,
sprites,
stage
} = props;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (target && target.currentCostume) {
this.state = {selectedCostumeIndex: target.currentCostume};
} else {
this.state = {selectedCostumeIndex: 0};
}
}
componentWillReceiveProps (nextProps) {
const {
......@@ -75,9 +86,14 @@ class CostumeTab extends React.Component {
handleDeleteCostume (costumeIndex) {
this.props.vm.deleteCostume(costumeIndex);
}
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex).then(() => {
this.setState({selectedCostumeIndex: costumeIndex + 1});
});
}
handleNewCostume () {
if (!this.props.vm.editingTarget) return;
const costumes = this.props.vm.editingTarget.sprite.costumes || [];
const costumes = this.props.vm.editingTarget.getCostumes() || [];
this.setState({selectedCostumeIndex: Math.max(costumes.length - 1, 0)});
}
handleNewBlankCostume () {
......@@ -145,6 +161,7 @@ class CostumeTab extends React.Component {
items={target.costumes || []}
selectedItemIndex={this.state.selectedCostumeIndex}
onDeleteClick={target.costumes.length > 1 ? this.handleDeleteCostume : null}
onDuplicateClick={this.handleDuplicateCostume}
onItemClick={this.handleSelectCostume}
>
{target.costumes ?
......@@ -186,7 +203,8 @@ CostumeTab.propTypes = {
id: PropTypes.shape({
costumes: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string,
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
skinId: PropTypes.number
}))
})
}),
......
......@@ -2,23 +2,21 @@ import AudioEngine from 'scratch-audio';
import PropTypes from 'prop-types';
import React from 'react';
import VM from 'scratch-vm';
import bindAll from 'lodash.bindall';
import {connect} from 'react-redux';
import {openExtensionLibrary} from '../reducers/modals.js';
import {openExtensionLibrary} from '../reducers/modals';
import {
activateTab,
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX
} from '../reducers/editor-tab';
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
import GUIComponent from '../components/gui/gui.jsx';
class GUI extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleTabSelect'
]);
this.state = {tabIndex: 0};
}
componentDidMount () {
this.audioEngine = new AudioEngine();
this.props.vm.attachAudioEngine(this.audioEngine);
......@@ -34,9 +32,6 @@ class GUI extends React.Component {
componentWillUnmount () {
this.props.vm.stopAll();
}
handleTabSelect (tabIndex) {
this.setState({tabIndex});
}
render () {
const {
children,
......@@ -46,10 +41,7 @@ class GUI extends React.Component {
} = this.props;
return (
<GUIComponent
enableExtensions={window.location.search.includes('extensions')}
tabIndex={this.state.tabIndex}
vm={vm}
onTabSelect={this.handleTabSelect}
{...componentProps}
>
{children}
......@@ -69,12 +61,17 @@ GUI.propTypes = {
GUI.defaultProps = GUIComponent.defaultProps;
const mapStateToProps = state => ({
activeTabIndex: state.editorTab.activeTabIndex,
blocksTabVisible: state.editorTab.activeTabIndex === BLOCKS_TAB_INDEX,
costumesTabVisible: state.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
feedbackFormVisible: state.modals.feedbackForm,
previewInfoVisible: state.modals.previewInfo
previewInfoVisible: state.modals.previewInfo,
soundsTabVisible: state.editorTab.activeTabIndex === SOUNDS_TAB_INDEX
});
const mapDispatchToProps = dispatch => ({
onExtensionButtonClick: () => dispatch(openExtensionLibrary())
onExtensionButtonClick: () => dispatch(openExtensionLibrary()),
onActivateTab: tab => dispatch(activateTab(tab))
});
const ConnectedGUI = connect(
......
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import WavEncoder from 'wav-encoder';
import {connect} from 'react-redux';
......@@ -9,6 +10,7 @@ import {computeChunkedRMS} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
import log from '../lib/log.js';
const UNDO_STACK_SIZE = 99;
......@@ -72,11 +74,25 @@ class SoundEditor extends React.Component {
}
this.undoStack.push(this.copyCurrentBuffer());
}
// Encode the new sound into a wav so that it can be stored
let wavBuffer = null;
try {
wavBuffer = WavEncoder.encode.sync({
sampleRate: sampleRate,
channelData: [samples]
});
} catch (e) {
// This error state is mostly for the mock sounds used during testing.
// Any incorrect sound buffer trying to get interpretd as a Wav file
// should yield this error.
log.error(`Encountered error while trying to encode sound update: ${e}`);
}
this.resetState(samples, sampleRate);
this.props.onUpdateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer
);
this.audioBufferPlayer.buffer,
wavBuffer ? new Uint8Array(wavBuffer) : new Uint8Array());
}
handlePlay () {
this.audioBufferPlayer.play(
......
......@@ -26,6 +26,7 @@ class SoundTab extends React.Component {
bindAll(this, [
'handleSelectSound',
'handleDeleteSound',
'handleDuplicateSound',
'handleNewSound'
]);
this.state = {selectedSoundIndex: 0};
......@@ -51,6 +52,15 @@ class SoundTab extends React.Component {
handleDeleteSound (soundIndex) {
this.props.vm.deleteSound(soundIndex);
if (soundIndex >= this.state.selectedSoundIndex) {
this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)});
}
}
handleDuplicateSound (soundIndex) {
this.props.vm.duplicateSound(soundIndex).then(() => {
this.setState({selectedSoundIndex: soundIndex + 1});
});
}
handleNewSound () {
......@@ -113,6 +123,7 @@ class SoundTab extends React.Component {
}))}
selectedItemIndex={this.state.selectedSoundIndex}
onDeleteClick={this.handleDeleteSound}
onDuplicateClick={this.handleDuplicateSound}
onItemClick={this.handleSelectSound}
>
{sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? (
......
......@@ -22,7 +22,8 @@ class SpriteSelectorItem extends React.Component {
e.preventDefault();
this.props.onClick(this.props.id);
}
handleDelete () {
handleDelete (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
// @todo add i18n here
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to delete this?')) {
......
......@@ -31,6 +31,7 @@ class Stage extends React.Component {
'onMouseDown',
'onStartDrag',
'onStopDrag',
'onWheel',
'updateRect',
'questionListener',
'setCanvas'
......@@ -98,6 +99,7 @@ class Stage extends React.Component {
document.addEventListener('touchend', this.onMouseUp);
canvas.addEventListener('mousedown', this.onMouseDown);
canvas.addEventListener('touchstart', this.onMouseDown);
canvas.addEventListener('wheel', this.onWheel);
}
detachMouseEvents (canvas) {
document.removeEventListener('mousemove', this.onMouseMove);
......@@ -106,6 +108,7 @@ class Stage extends React.Component {
document.removeEventListener('touchend', this.onMouseUp);
canvas.removeEventListener('mousedown', this.onMouseDown);
canvas.removeEventListener('touchstart', this.onMouseDown);
canvas.removeEventListener('wheel', this.onWheel);
}
attachRectEvents () {
window.addEventListener('resize', this.updateRect);
......@@ -232,6 +235,13 @@ class Stage extends React.Component {
this.setState({colorInfo: null});
}
}
onWheel (e) {
const data = {
deltaX: e.deltaX,
deltaY: e.deltaY
};
this.props.vm.postIOData('mouseWheel', data);
}
cancelMouseDownTimeout () {
if (this.state.mouseDownTimeoutId !== null) {
clearTimeout(this.state.mouseDownTimeoutId);
......
......@@ -36,15 +36,15 @@ export default function (vm) {
};
const costumesMenu = function () {
if (vm.editingTarget && vm.editingTarget.sprite.costumes.length > 0) {
return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]);
if (vm.editingTarget && vm.editingTarget.getCostumes().length > 0) {
return vm.editingTarget.getCostumes().map(costume => [costume.name, costume.name]);
}
return [['', '']];
};
const backdropsMenu = function () {
if (vm.runtime.targets[0] && vm.runtime.targets[0].sprite.costumes.length > 0) {
return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name])
if (vm.runtime.targets[0] && vm.runtime.targets[0].getCostumes().length > 0) {
return vm.runtime.targets[0].getCostumes().map(costume => [costume.name, costume.name])
.concat([['next backdrop', 'next backdrop'], ['previous backdrop', 'previous backdrop']]);
}
return [['', '']];
......
......@@ -37,10 +37,10 @@ const opcodeMap = {
labelFn: params => params.VARIABLE
},
data_listcontents: {
category: 'data',
category: 'list',
labelFn: params => params.LIST
},
// Sound
sound_volume: {
category: 'sound',
......
const ACTIVATE_TAB = 'scratch-gui/navigation/ACTIVATE_TAB';
// Constants use numbers to make it easier to work with react-tabs
const BLOCKS_TAB_INDEX = 0;
const COSTUMES_TAB_INDEX = 1;
const SOUNDS_TAB_INDEX = 2;
const initialState = {
activeTabIndex: BLOCKS_TAB_INDEX
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case ACTIVATE_TAB:
return Object.assign({}, state, {
activeTabIndex: action.activeTabIndex
});
default:
return state;
}
};
const activateTab = function (tab) {
return {
type: ACTIVATE_TAB,
activeTabIndex: tab
};
};
export {
reducer as default,
activateTab,
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX
};
......@@ -2,6 +2,7 @@ import {combineReducers} from 'redux';
import colorPickerReducer from './color-picker';
import customProceduresReducer from './custom-procedures';
import blockDragReducer from './block-drag';
import editorTabReducer from './editor-tab';
import hoveredTargetReducer from './hovered-target';
import intlReducer from './intl';
import modalReducer from './modals';
......@@ -17,6 +18,7 @@ export default combineReducers({
blockDrag: blockDragReducer,
colorPicker: colorPickerReducer,
customProcedures: customProceduresReducer,
editorTab: editorTabReducer,
hoveredTarget: hoveredTargetReducer,
intl: intlReducer,
stageSize: stageSizeReducer,
......
......@@ -25,6 +25,7 @@ class SeleniumHelper {
// List of useful xpath scopes for finding elements
return {
blocksTab: "*[@id='react-tabs-1']",
costumesTab: "*[@id='react-tabs-3']",
modal: '*[@class="ReactModalPortal"]',
reportedValue: '*[@class="blocklyDropDownContent"]',
soundsTab: "*[@id='react-tabs-5']",
......
......@@ -7,7 +7,9 @@ const {
findByXpath,
getDriver,
getLogs,
loadUri
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
......@@ -36,6 +38,22 @@ describe('Working with costumes', () => {
await expect(logs).toEqual([]);
});
test('Duplicating a costume', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Costumes');
await rightClickText('costume1', scope.costumesTab);
await clickText('duplicate', scope.costumesTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for duplication to finish
// Make sure the duplicated costume is named correctly.
await clickText('costume3', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a backdrop', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
......
......@@ -63,6 +63,22 @@ describe('Working with sounds', () => {
await expect(logs).toEqual([]);
});
test('Duplicating a sound', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Sounds');
await rightClickText('Meow', scope.soundsTab);
await clickText('duplicate', scope.soundsTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error
// Make sure the duplicated sound is named correctly.
await clickText('Meow2', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// Regression test for gui issue #1320
test('Switching sprites with different numbers of sounds', async () => {
await loadUri(uri);
......
......@@ -111,10 +111,6 @@ module.exports = {
from: 'node_modules/scratch-blocks/media',
to: 'static/blocks-media'
}]),
new CopyWebpackPlugin([{
from: 'node_modules/scratch-vm/dist/node/assets',
to: 'static/extension-assets'
}]),
new CopyWebpackPlugin([{
from: 'extensions/**',
to: 'static',
......
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