diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e038790e476b6924e01ecfe1ece0526297f49768..7c757f392e94a8db2108ecea0f4cfc29e4ad0980 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -328,7 +328,10 @@ class Blocks extends React.Component { error.message = `Workspace Update Error: ${error.message}`; log.error(error); } - this.workspace.addChangeListener(this.props.vm.blockListener); + // All of the changes that happened during the load above are queued with + // timeouts, so re-enable the listener in the next tick, so it happens after + // the events are already fired. + setTimeout(() => this.workspace.addChangeListener(this.props.vm.blockListener)); if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) { const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id]; diff --git a/src/lib/project-fetcher-hoc.jsx b/src/lib/project-fetcher-hoc.jsx index ca847fe3fdd58e7df3d417d6a59217779da4511c..1879d69e582468e3af8ffa5377f7a80c1e5e544a 100644 --- a/src/lib/project-fetcher-hoc.jsx +++ b/src/lib/project-fetcher-hoc.jsx @@ -4,10 +4,12 @@ import {intlShape, injectIntl} from 'react-intl'; import bindAll from 'lodash.bindall'; import {connect} from 'react-redux'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, defaultProjectId, getIsFetchingWithId, + getIsShowingProject, onFetchedProjectData, projectError, setProjectId @@ -54,6 +56,9 @@ const ProjectFetcherHOC = function (WrappedComponent) { if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) { this.fetchProject(this.props.reduxProjectId, this.props.loadingState); } + if (this.props.isShowingProject && !prevProps.isShowingProject) { + this.props.onProjectLoaded(); + } } fetchProject (projectId, loadingState) { return storage @@ -123,6 +128,7 @@ const ProjectFetcherHOC = function (WrappedComponent) { const mapStateToProps = state => ({ isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState), + isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState), loadingState: state.scratchGui.projectState.loadingState, reduxProjectId: state.scratchGui.projectState.projectId }); @@ -130,7 +136,8 @@ const ProjectFetcherHOC = function (WrappedComponent) { onError: error => dispatch(projectError(error)), onFetchedProjectData: (projectData, loadingState) => dispatch(onFetchedProjectData(projectData, loadingState)), - setProjectId: projectId => dispatch(setProjectId(projectId)) + setProjectId: projectId => dispatch(setProjectId(projectId)), + onProjectLoaded: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index 3fbbad36129765a87910a5048600007f4064a668..df0d6fa6f2753fc9236296ff79745987bc6355db 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -9,6 +9,7 @@ import { showAlertWithTimeout, showStandardAlert } from '../reducers/alerts'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, autoUpdateProject, @@ -167,7 +168,10 @@ const ProjectSaverHOC = function (WrappedComponent) { storage.DataFormat.JSON, body, projectId - ); + ).then(response => { + this.props.onSetProjectUnchanged(); + return response; + }); }) .catch(err => { log.error(err); @@ -255,6 +259,7 @@ const ProjectSaverHOC = function (WrappedComponent) { onCreatedProject: (projectId, loadingState) => dispatch(doneCreatingProject(projectId, loadingState)), onCreateProject: () => dispatch(createProject()), onProjectError: error => dispatch(projectError(error)), + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()), onShowAlert: alertType => dispatch(showStandardAlert(alertType)), onShowCreateSuccessAlert: () => showAlertWithTimeout(dispatch, 'createSuccess'), onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'), diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 675deb04cd2458b180396b28503fc8c885fddaee..3e36865719974ecd995b1a30a5654abb13f101a5 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -8,6 +8,7 @@ import {connect} from 'react-redux'; import {updateTargets} from '../reducers/targets'; import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; +import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed'; import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status'; import {showExtensionAlert} from '../reducers/alerts'; import {updateMicIndicator} from '../reducers/mic-indicator'; @@ -24,6 +25,7 @@ const vmListenerHOC = function (WrappedComponent) { bindAll(this, [ 'handleKeyDown', 'handleKeyUp', + 'handleProjectChanged', 'handleTargetsUpdate' ]); // We have to start listening to the vm here rather than in @@ -39,6 +41,7 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff); this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); + this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged); this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted); this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowExtensionAlert); this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate); @@ -69,6 +72,9 @@ const vmListenerHOC = function (WrappedComponent) { document.removeEventListener('keyup', this.handleKeyUp); } } + handleProjectChanged () { + this.props.onProjectChanged(); + } handleTargetsUpdate (data) { if (this.props.shouldEmitTargetsUpdate) { this.props.onTargetsUpdate(data); @@ -167,6 +173,8 @@ const vmListenerHOC = function (WrappedComponent) { }, onProjectRunStart: () => dispatch(setRunningState(true)), onProjectRunStop: () => dispatch(setRunningState(false)), + onProjectChanged: () => dispatch(setProjectChanged()), + onProjectSaved: () => dispatch(setProjectUnchanged()), onRuntimeStarted: () => dispatch(setStartedState(true)), onTurboModeOn: () => dispatch(setTurboState(true)), onTurboModeOff: () => dispatch(setTurboState(false)), diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index 981fe8326f73baecc3576e24a47e55cc85b61fa2..d9efcd4de33900abfd8870d3d54a70a9cc155473 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, getIsLoadingWithId, @@ -53,6 +54,7 @@ const vmManagerHOC = function (WrappedComponent) { return this.props.vm.loadProject(this.props.projectData) .then(() => { this.props.onLoadedProject(this.props.loadingState, this.props.canSave); + this.props.onSetProjectUnchanged(); // If the vm is not running, call draw on the renderer manually // This draws the state of the loaded project with no blocks running @@ -123,7 +125,8 @@ const vmManagerHOC = function (WrappedComponent) { const mapDispatchToProps = dispatch => ({ onError: error => dispatch(projectError(error)), onLoadedProject: (loadingState, canSave) => - dispatch(onLoadedProject(loadingState, canSave)) + dispatch(onLoadedProject(loadingState, canSave)), + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 6ae91b95a6d986e1630a879c9f17d54e3e565c29..edffa1cf363316d0a38a705eb20edc2f064b9566 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -14,6 +14,7 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import projectChangedReducer, {projectChangedInitialState} from './project-changed'; import projectStateReducer, {projectStateInitialState} from './project-state'; import projectTitleReducer, {projectTitleInitialState} from './project-title'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; @@ -45,6 +46,7 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + projectChanged: projectChangedInitialState, projectState: projectStateInitialState, projectTitle: projectTitleInitialState, restoreDeletion: restoreDeletionInitialState, @@ -122,6 +124,7 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + projectChanged: projectChangedReducer, projectState: projectStateReducer, projectTitle: projectTitleReducer, restoreDeletion: restoreDeletionReducer, diff --git a/src/reducers/project-changed.js b/src/reducers/project-changed.js new file mode 100644 index 0000000000000000000000000000000000000000..c59f6a107ea4cc9b7ec6cb0c4c403fb29a41fbf5 --- /dev/null +++ b/src/reducers/project-changed.js @@ -0,0 +1,28 @@ +const SET_PROJECT_CHANGED = 'scratch-gui/project-changed/SET_PROJECT_CHANGED'; + +const initialState = false; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_PROJECT_CHANGED: + return action.changed; + default: + return state; + } +}; +const setProjectChanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: true +}); +const setProjectUnchanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: false +}); + +export { + reducer as default, + initialState as projectChangedInitialState, + setProjectChanged, + setProjectUnchanged +};