From e63a3a7d9f776388bd5d1719ea5c8f6babfee029 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 21 Feb 2018 19:49:25 -0500 Subject: [PATCH 1/7] Add duplicate button. Pass the costume ID to paint (the previous costume ID used matches when duplicate is used.) When switching to the costume tab, open the current costume. --- src/components/asset-panel/selector.jsx | 3 +++ src/containers/costume-tab.jsx | 22 +++++++++++++++++++--- src/containers/paint-editor-wrapper.jsx | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index 012206517..b69707781 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -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} /> ))} @@ -58,6 +60,7 @@ Selector.propTypes = { name: PropTypes.string.isRequired })), onDeleteClick: PropTypes.func, + onDuplicateClick: PropTypes.func, onItemClick: PropTypes.func.isRequired, selectedItemIndex: PropTypes.number.isRequired }; diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index bb48f1b5d..1692ddee6 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -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,6 +86,10 @@ class CostumeTab extends React.Component { handleDeleteCostume (costumeIndex) { this.props.vm.deleteCostume(costumeIndex); } + handleDuplicateCostume (costumeIndex) { + this.props.vm.duplicateCostume(costumeIndex); + this.setState({selectedCostumeIndex: costumeIndex + 1}); + } handleNewCostume () { if (!this.props.vm.editingTarget) return; const costumes = this.props.vm.editingTarget.getCostumes() || []; @@ -127,7 +142,6 @@ class CostumeTab extends React.Component { const addBlankMessage = target.isStage ? messages.addBlankBackdropMsg : messages.addBlankCostumeMsg; const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick; const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon; - return ( 1 ? this.handleDeleteCostume : null} + onDuplicateClick={this.handleDuplicateCostume} onItemClick={this.handleSelectCostume} > {target.costumes ? @@ -186,7 +201,8 @@ CostumeTab.propTypes = { id: PropTypes.shape({ costumes: PropTypes.arrayOf(PropTypes.shape({ url: PropTypes.string, - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + costumeId: PropTypes.number })) }) }), diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index 38cb3aa4a..ad248b83f 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -59,7 +59,7 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { name: costume && costume.name, rotationCenterX: costume && costume.rotationCenterX, rotationCenterY: costume && costume.rotationCenterY, - svgId: editingTarget && `${editingTarget}${costume.skinId}` + svgId: editingTarget && `${editingTarget}${costume.costumeId}` }; }; From 868e401a4092d2f53db823eeb485acd5251f587c Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 21 Feb 2018 19:54:51 -0500 Subject: [PATCH 2/7] Update gui to use getter for target costumes --- src/containers/costume-tab.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 1692ddee6..622709bdd 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -142,6 +142,7 @@ class CostumeTab extends React.Component { const addBlankMessage = target.isStage ? messages.addBlankBackdropMsg : messages.addBlankCostumeMsg; const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick; const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon; + return ( Date: Fri, 23 Feb 2018 11:13:41 -0500 Subject: [PATCH 3/7] Use promises --- src/containers/costume-tab.jsx | 7 ++++--- src/containers/paint-editor-wrapper.jsx | 2 +- src/containers/sound-tab.jsx | 8 ++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 622709bdd..e22baebf2 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -87,8 +87,9 @@ class CostumeTab extends React.Component { this.props.vm.deleteCostume(costumeIndex); } handleDuplicateCostume (costumeIndex) { - this.props.vm.duplicateCostume(costumeIndex); - this.setState({selectedCostumeIndex: costumeIndex + 1}); + this.props.vm.duplicateCostume(costumeIndex).then(() => { + this.setState({selectedCostumeIndex: costumeIndex + 1}); + }); } handleNewCostume () { if (!this.props.vm.editingTarget) return; @@ -203,7 +204,7 @@ CostumeTab.propTypes = { costumes: PropTypes.arrayOf(PropTypes.shape({ url: PropTypes.string, name: PropTypes.string.isRequired, - costumeId: PropTypes.number + skinId: PropTypes.number })) }) }), diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index ad248b83f..38cb3aa4a 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -59,7 +59,7 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { name: costume && costume.name, rotationCenterX: costume && costume.rotationCenterX, rotationCenterY: costume && costume.rotationCenterY, - svgId: editingTarget && `${editingTarget}${costume.costumeId}` + svgId: editingTarget && `${editingTarget}${costume.skinId}` }; }; diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index af95c6b0b..8686ae6f9 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -26,6 +26,7 @@ class SoundTab extends React.Component { bindAll(this, [ 'handleSelectSound', 'handleDeleteSound', + 'handleDuplicateSound', 'handleNewSound' ]); this.state = {selectedSoundIndex: 0}; @@ -56,6 +57,12 @@ class SoundTab extends React.Component { } } + handleDuplicateSound (soundIndex) { + this.props.vm.duplicateSound(soundIndex).then(() => { + this.setState({selectedSoundIndex: soundIndex + 1}); + }); + } + handleNewSound () { if (!this.props.vm.editingTarget) { return null; @@ -116,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] ? ( From 12b47ddf86fb381fb763b5b16c12bb3defb09c6c Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 23 Feb 2018 16:20:40 -0500 Subject: [PATCH 4/7] Bring in Karishmas changes from save-load --- src/containers/sound-editor.jsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 781b2cc5f..001224d56 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -1,6 +1,7 @@ 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, + (new Uint8Array(wavBuffer))); } handlePlay () { this.audioBufferPlayer.play( From 44ce6df59ecedbfe8b9f22aaedc32668d0253275 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 23 Feb 2018 16:49:29 -0500 Subject: [PATCH 5/7] Fix tests on node 6 --- src/containers/sound-editor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 001224d56..bbe032c0b 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -92,7 +92,7 @@ class SoundEditor extends React.Component { this.props.onUpdateSoundBuffer( this.props.soundIndex, this.audioBufferPlayer.buffer, - (new Uint8Array(wavBuffer))); + wavBuffer ? new Uint8Array(wavBuffer) : new Uint8Array()); } handlePlay () { this.audioBufferPlayer.play( From 6e315fdb5c290f2dc5d55f120303f022f9ab9218 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 26 Feb 2018 08:26:43 -0500 Subject: [PATCH 6/7] Add duplication integration tests for GUI --- test/helpers/selenium-helper.js | 1 + test/integration/costumes.test.js | 20 +++++++++++++++++++- test/integration/sounds.test.js | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index c8ead9072..b63ea2bf6 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -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']", diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index 98f31629a..f07c3d0f3 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -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"]'); diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 244ebb9b4..e31548684 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -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); From 73d195446dd5117a9dc2845a7cdac8edd34b34d4 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 26 Feb 2018 09:07:59 -0500 Subject: [PATCH 7/7] Update to scratch-vm with duplicate costume/sound api --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd46c50ee..e44c89a02 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "scratch-paint": "0.2.0-prerelease.20180222192821", "scratch-render": "0.1.0-prerelease.1516837442", "scratch-storage": "0.4.0", - "scratch-vm": "0.1.0-prerelease.1518812805-prerelease.1518812820", + "scratch-vm": "0.1.0-prerelease.1519653784-prerelease.1519653801", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.20.0",