Skip to content

Commit 956862d

Browse files
committed
Merge branch 'develop-1.5'
2 parents 53d2af2 + 75670c7 commit 956862d

72 files changed

Lines changed: 7161 additions & 1635 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
1.67 KB
Loading

docs/keyboard.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Those key bindings are as follows.
3737
| Move the current step to right | Ctrl + Alt + ] | ![Move to next icon](board-imgs/MoveNext.png) |
3838
| Play or pause the program | Ctrl + Alt + p | ![Play or pause program icon](board-imgs/PlayPause.png) |
3939
| Refresh the scene | Ctrl + Alt + r | ![Refresh scene icon](board-imgs/RefreshScene.png) |
40-
| Replace the current program step | Ctrl + Alt + c | Not available |
40+
| Replace the current program step | Ctrl + Alt + c | ![Replace the current program step icon](board-imgs/ReplaceCurrentStep.png) |
4141
| Show the keyboard shortcuts menu | ? | ![Show keyboard shortcuts menu icon](board-imgs/ShowKeyboardShortcuts.png) |
4242
| Stop the program from playing | Ctrl + Alt + s | ![Stop program icon](board-imgs/StopProgram.png) |
4343
| Change the current visual theme to the dark theme | Ctrl + Alt + x, t, 3 | ![Change to dark theme icon](board-imgs/ChangeToDarkTheme.png) |
@@ -94,7 +94,7 @@ Those key bindings are as follows:
9494
| Move the current step to right | Alt + ] | ![Move to next icon](board-imgs/MoveNext.png) |
9595
| Play or pause the program | Alt + p | ![Play or pause program icon](board-imgs/PlayPause.png) |
9696
| Refresh the scene | Alt + r | ![Refresh scene icon](board-imgs/RefreshScene.png) |
97-
| Replace the current program step | Alt + c | Not available |
97+
| Replace the current program step | Alt + c | ![Replace the current program step icon](board-imgs/ReplaceCurrentStep.png) |
9898
| Show the keyboard shortcuts menu | ? | ![Show keyboard shortcuts menu icon](board-imgs/ShowKeyboardShortcuts.png) |
9999
| Stop the program from playing | Alt + s | ![Stop program icon](board-imgs/StopProgram.png) |
100100
| Change the current visual theme to the dark theme | Alt + x, t, 3 | ![Change to dark theme icon](board-imgs/ChangeToDarkTheme.png) |

package-lock.json

Lines changed: 379 additions & 288 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "c2lc-coding-environment",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"private": true,
55
"dependencies": {
66
"bootstrap": "4.6.1",

src/ActionPanel.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ReactComponent as MovePreviousIcon } from './svg/MovePrevious.svg';
1010
import { ReactComponent as MoveNextIcon } from './svg/MoveNext.svg';
1111
import { ReactComponent as DeleteIcon } from './svg/Delete.svg';
1212
import { ReactComponent as ReplaceIcon } from './svg/replace.svg';
13-
import { focusByQuerySelector, isLoopBlock, moveToNextStepDisabled, moveToPreviousStepDisabled } from './Utils';
13+
import { focusByQuerySelector, isLoopBlock } from './Utils';
1414
import './ActionPanel.scss';
1515

1616
type ActionPanelProps = {
@@ -215,8 +215,8 @@ class ActionPanel extends React.Component<ActionPanelProps, {}> {
215215

216216
render() {
217217
const stepMessageData = this.makeStepMessageData();
218-
const moveToNextStepIsDisabled = moveToNextStepDisabled(this.props.programSequence, this.props.pressedStepIndex);
219-
const moveToPreviousStepIsDisabled = moveToPreviousStepDisabled(this.props.programSequence, this.props.pressedStepIndex);
218+
const moveToNextStepIsDisabled = this.props.programSequence.moveToNextStepDisabled(this.props.pressedStepIndex);
219+
const moveToPreviousStepIsDisabled = this.props.programSequence.moveToPreviousStepDisabled(this.props.pressedStepIndex);
220220
const replaceIsVisible = this.getReplaceIsVisible();
221221
const replaceIsDisabled = this.props.selectedCommandName == null;
222222
return (

src/App.js

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class App extends React.Component<AppProps, AppState> {
146146
constructor(props: any) {
147147
super(props);
148148

149-
this.version = '1.4';
149+
this.version = '1.5';
150150

151151
this.appContext = {
152152
bluetoothApiIsAvailable: FeatureDetection.bluetoothApiIsAvailable()
@@ -488,6 +488,21 @@ export class App extends React.Component<AppProps, AppState> {
488488
}
489489
}
490490

491+
editingIsDisabled(): boolean {
492+
return !(this.state.runningState === 'stopped'
493+
|| this.state.runningState === 'paused');
494+
}
495+
496+
refreshIsDisabled(): boolean {
497+
return this.state.runningState !== 'stopped';
498+
}
499+
500+
// API for Interpreter
501+
502+
getRunningState(): RunningState {
503+
return this.state.runningState;
504+
}
505+
491506
setRunningState(runningState: RunningState): void {
492507
this.setState((state) => {
493508
// If stop is requested when we are in the 'paused' state,
@@ -500,37 +515,14 @@ export class App extends React.Component<AppProps, AppState> {
500515
});
501516
}
502517

503-
// API for Interpreter
504-
505518
getProgramSequence(): ProgramSequence {
506519
return this.state.programSequence;
507520
}
508521

509-
getRunningState(): RunningState {
510-
return this.state.runningState;
511-
}
512-
513-
editingIsDisabled(): boolean {
514-
return !(this.state.runningState === 'stopped'
515-
|| this.state.runningState === 'paused');
516-
}
517-
518-
incrementProgramCounter(callback: () => void): void {
519-
this.setState((state) => {
520-
return {
521-
programSequence: state.programSequence.incrementProgramCounter()
522-
}
523-
}, callback);
524-
}
525-
526-
refreshIsDisabled(): boolean {
527-
return this.state.runningState !== 'stopped';
528-
}
529-
530-
updateProgramCounterAndLoopIterationsLeft(programCounter: number, loopIterationsLeft: Map<string, number>, callback: () => void): void {
522+
advanceProgramCounter(callback: () => void): void {
531523
this.setState((state) => {
532524
return {
533-
programSequence: state.programSequence.updateProgramCounterAndLoopIterationsLeft(programCounter, loopIterationsLeft)
525+
programSequence: state.programSequence.advanceProgramCounter(false)
534526
}
535527
}, callback);
536528
}
@@ -1458,6 +1450,7 @@ export class App extends React.Component<AppProps, AppState> {
14581450
world={this.state.settings.world}
14591451
startingX={this.state.startingX}
14601452
startingY={this.state.startingY}
1453+
runningState={this.state.runningState}
14611454
/>
14621455
</div>
14631456
<div className="App__world-container">
@@ -1557,6 +1550,9 @@ export class App extends React.Component<AppProps, AppState> {
15571550
addNodeExpandedMode={this.state.settings.addNodeExpandedMode}
15581551
theme={this.state.settings.theme}
15591552
world={this.state.settings.world}
1553+
scrollRightPaddingPx={256}
1554+
scrollLeftPaddingPx={128}
1555+
scrollTimeThresholdMs={400}
15601556
onChangeProgramSequence={this.handleProgramSequenceChange}
15611557
onInsertSelectedActionIntoProgram={this.handleProgramBlockEditorInsertSelectedAction}
15621558
onDeleteProgramStep={this.handleProgramBlockEditorDeleteStep}

src/AudioManagerImpl.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import CharacterState from './CharacterState';
55
import type {IntlShape} from 'react-intl';
66
import {AudioManager} from './types';
77
import SceneDimensions from './SceneDimensions';
8+
import {selectSpeechSynthesisVoice} from './Utils.js';
89

910
const SamplerDefs = {
1011
// The percussion instruments we use actually don't vary their pitch, so we use the same sample at different
@@ -204,6 +205,7 @@ export default class AudioManagerImpl implements AudioManager {
204205
right90: Sampler,
205206
right180: Sampler
206207
};
208+
speechSynthesisVoices: Array<SpeechSynthesisVoice>;
207209

208210
constructor(audioEnabled: boolean, announcementsEnabled: boolean, sonificationEnabled: boolean) {
209211
this.audioEnabled = audioEnabled;
@@ -215,6 +217,9 @@ export default class AudioManagerImpl implements AudioManager {
215217

216218
this.samplers = {};
217219

220+
this.updateSpeechSynthesisVoices();
221+
window.speechSynthesis.onvoiceschanged = this.updateSpeechSynthesisVoices;
222+
218223
Object.keys(SamplerDefs).forEach((samplerKey) => {
219224
const samplerDef = SamplerDefs[samplerKey];
220225
const sampler = new Sampler(samplerDef);
@@ -223,6 +228,10 @@ export default class AudioManagerImpl implements AudioManager {
223228
});
224229
}
225230

231+
updateSpeechSynthesisVoices = () => {
232+
this.speechSynthesisVoices = window.speechSynthesis.getVoices();
233+
};
234+
226235
playAnnouncement(messageIdSuffix: string, intl: IntlShape, messagePayload: any) {
227236
if (this.audioEnabled && this.announcementsEnabled) {
228237
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) {
@@ -231,6 +240,15 @@ export default class AudioManagerImpl implements AudioManager {
231240
const messageId = "Announcement." + messageIdSuffix;
232241
const toAnnounce = intl.formatMessage({ id: messageId}, messagePayload);
233242
const utterance = new SpeechSynthesisUtterance(toAnnounce);
243+
244+
// TODO: When we support non-English UI language(s),
245+
// ensure that the language is specified correctly
246+
utterance.voice = selectSpeechSynthesisVoice(
247+
'en',
248+
window.navigator.language,
249+
this.speechSynthesisVoices
250+
);
251+
234252
window.speechSynthesis.speak(utterance);
235253
}
236254
}

src/Character.test.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,28 @@ function findCharacter(wrapper) {
3434
}
3535

3636
describe('Right character should render based on world and theme props', () => {
37-
test('Sketchpad', () => {
38-
expect.assertions(1);
39-
const wrapper = createMountCharacter();
40-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Robot.svg');
37+
test('DeepOcean', () => {
38+
expect.assertions(3);
39+
const wrapper = createMountCharacter({world: 'DeepOcean'});
40+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine.svg');
41+
wrapper.setProps({theme: 'gray'});
42+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine-gray.svg');
43+
wrapper.setProps({theme: 'contrast'});
44+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine-contrast.svg');
4145
});
42-
test('Jungle', () => {
46+
test('Savannah', () => {
4347
expect.assertions(3);
44-
const wrapper = createMountCharacter({world: 'Jungle'});
45-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SafariJeep.svg');
48+
const wrapper = createMountCharacter({world: 'Savannah'});
49+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SavannahJeep.svg');
4650
wrapper.setProps({theme: 'gray'});
47-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SafariJeep-gray.svg');
51+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SavannahJeep-gray.svg');
4852
wrapper.setProps({theme: 'contrast'});
49-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SafariJeep-contrast.svg');
53+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SavannahJeep-contrast.svg');
54+
});
55+
test('Sketchpad', () => {
56+
expect.assertions(1);
57+
const wrapper = createMountCharacter();
58+
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Robot.svg');
5059
});
5160
test('Space', () => {
5261
expect.assertions(3);
@@ -57,13 +66,4 @@ describe('Right character should render based on world and theme props', () => {
5766
wrapper.setProps({theme: 'contrast'});
5867
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('SpaceShip-contrast.svg');
5968
});
60-
test('DeepOcean', () => {
61-
expect.assertions(3);
62-
const wrapper = createMountCharacter({world: 'DeepOcean'});
63-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine.svg');
64-
wrapper.setProps({theme: 'gray'});
65-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine-gray.svg');
66-
wrapper.setProps({theme: 'contrast'});
67-
expect(findCharacter(wrapper).get(0).type.render().props.children).toBe('Submarine-contrast.svg');
68-
});
6969
})

src/CharacterAriaLive.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('Character position gets updated on character-position div', () => {
6060
});
6161
// $FlowFixMe: Flow doesn't know about character-position div
6262
expect(document.getElementById('someAriaLiveRegionId').innerText).toBe('the robot is at column A, row 1 facing right');
63-
wrapper.setProps({runningState: 'stopped', world: 'Jungle', characterState: new CharacterState(2, 1, 2, [], new SceneDimensions(1, 100, 1, 100))});
63+
wrapper.setProps({runningState: 'stopped', world: 'Savannah', characterState: new CharacterState(2, 1, 2, [], new SceneDimensions(1, 100, 1, 100))});
6464
// $FlowFixMe: Flow doesn't know about character-position div
6565
expect(document.getElementById('someAriaLiveRegionId').innerText).toBe('the jeep is at column B, row 1 facing right');
6666
wrapper.setProps({runningState: 'stopped', world: 'Space', characterState: new CharacterState(3, 1, 2, [], new SceneDimensions(1, 100, 1, 100))});
@@ -69,7 +69,7 @@ describe('Character position gets updated on character-position div', () => {
6969
});
7070
test('When runningState prop is changed', () => {
7171
const wrapper = createMountCharacterAriaLive();
72-
wrapper.setProps({ runningState: 'pauseRequested', world: 'Jungle' });
72+
wrapper.setProps({ runningState: 'pauseRequested', world: 'Savannah' });
7373
// $FlowFixMe: Flow doesn't know about character-position div
7474
expect(document.getElementById('someAriaLiveRegionId').innerText).toBe('the jeep is at column A, row 1 facing right');
7575
wrapper.setProps({ runningState: 'stopRequested', world: 'Space' });

src/CharacterPositionController.test.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -204,27 +204,27 @@ describe('Using change character position by column/row labels', () => {
204204
test('Changing world changes the character icon', () => {
205205
expect.assertions(9);
206206
const { wrapper } = createShallowCharacterPositionController();
207-
// Space World
208-
wrapper.setProps({world: 'Space'});
209-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip.svg');
210-
wrapper.setProps({theme: 'gray'});
211-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip-gray.svg');
212-
wrapper.setProps({theme: 'contrast'});
213-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip-contrast.svg');
214-
// Jungle World
215-
wrapper.setProps({world: 'Jungle', theme: 'light'});
216-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SafariJeep.svg');
217-
wrapper.setProps({theme: 'gray'});
218-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SafariJeep-gray.svg');
219-
wrapper.setProps({theme: 'contrast'});
220-
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SafariJeep-contrast.svg');
221207
// DeepOcean World
222208
wrapper.setProps({world: 'DeepOcean', theme: 'light'});
223209
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('Submarine.svg');
224210
wrapper.setProps({theme: 'gray'});
225211
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('Submarine-gray.svg');
226212
wrapper.setProps({theme: 'contrast'});
227213
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('Submarine-contrast.svg');
214+
// Savannah World
215+
wrapper.setProps({world: 'Savannah', theme: 'light'});
216+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SavannahJeep.svg');
217+
wrapper.setProps({theme: 'gray'});
218+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SavannahJeep-gray.svg');
219+
wrapper.setProps({theme: 'contrast'});
220+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SavannahJeep-contrast.svg');
221+
// Space World
222+
wrapper.setProps({world: 'Space', theme: 'light'});
223+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip.svg');
224+
wrapper.setProps({theme: 'gray'});
225+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip-gray.svg');
226+
wrapper.setProps({theme: 'contrast'});
227+
expect(getCharacterIcon(wrapper).get(0).type.render().props.children).toBe('SpaceShip-contrast.svg');
228228
});
229229
test('When a world has enableFlipCharacter=true, character icon gets class names to rotate and enable flip', () => {
230230
expect.assertions(18);

0 commit comments

Comments
 (0)