diff --git a/docs/keyboard.md b/docs/keyboard.md index f1f13cfb..4c77174d 100644 --- a/docs/keyboard.md +++ b/docs/keyboard.md @@ -30,6 +30,7 @@ follows. | Ctrl + Alt + b | addCommandToBeginning | | Ctrl + Alt + e | addCommandToEnd | | Ctrl + Alt + d | deleteCurrentStep | +| Ctrl + Alt + l | deleteLastStep | | Ctrl + Alt + i | announceScene | | Ctrl + Alt + p | playPauseProgram | | Ctrl + Alt + r | refreshScene | @@ -85,6 +86,7 @@ with the starting key of a sequence. Those key bindings are as follows: | Alt + b | addCommandToBeginning | | Alt + e | addCommandToEnd | | Alt + d | deleteCurrentStep | +| Alt + l | deleteLastStep | | Alt + i | announceScene | | Alt + p | playPauseProgram | | Alt + r | refreshScene | diff --git a/src/App.js b/src/App.js index 69e1b3bd..dee24102 100644 --- a/src/App.js +++ b/src/App.js @@ -731,6 +731,11 @@ export class App extends React.Component { } } break; + case("deleteLastStep"): + if (!this.editingIsDisabled()) { + this.programChangeController.deleteLastStep(this.programBlockEditorRef.current); + } + break; case("deleteAll"): { if (!this.editingIsDisabled()) { const newProgramSequence = this.state.programSequence.updateProgram([]); diff --git a/src/KeyboardInputSchemes.js b/src/KeyboardInputSchemes.js index 6baa10be..ea9aede2 100644 --- a/src/KeyboardInputSchemes.js +++ b/src/KeyboardInputSchemes.js @@ -21,6 +21,7 @@ export type ActionName = | "addCommandToBeginning" | "addCommandToEnd" | "deleteCurrentStep" + | "deleteLastStep" | "announceScene" | "decreaseProgramSpeed" | "increaseProgramSpeed" @@ -294,6 +295,10 @@ const AltInputScheme: KeyboardInputScheme = Object.assign({ keyDef: { code: "KeyD", key: "d", altKey: true}, actionName: "deleteCurrentStep" }, + deleteLastStep: { + keyDef: { code: "KeyL", key: "l", altKey: true}, + actionName: "deleteLastStep" + }, announceScene: { keyDef: { code: "KeyI", key: "i", altKey: true}, actionName: "announceScene" @@ -359,6 +364,10 @@ const ControlAltInputScheme = Object.assign({ keyDef: { code: "KeyD", key: "d", altKey: true, ctrlKey: true}, actionName: "deleteCurrentStep" }, + deleteLastStep: { + keyDef: { code: "KeyL", key: "l", altKey: true, ctrlKey: true}, + actionName: "deleteLastStep" + }, announceScene: { keyDef: {code: "KeyI", key: "i", altKey: true, ctrlKey: true}, actionName: "announceScene" diff --git a/src/ProgramChangeController.js b/src/ProgramChangeController.js index 17a4ab71..50a2cd2d 100644 --- a/src/ProgramChangeController.js +++ b/src/ProgramChangeController.js @@ -63,15 +63,7 @@ export default class ProgramChangeController { // Check that the step to delete hasn't changed since the // user made the deletion if (command === state.programSequence.getProgramStepAt(index)) { - // Play the announcement - const commandString = this.intl.formatMessage({ - id: "Announcement." + command - }); - this.audioManager.playAnnouncement( - 'delete', - this.intl, - { command: commandString } - ); + this.playAnnouncementForDelete(command); // If there are steps following the one being deleted, focus // the next step. Otherwise, focus the final add node. @@ -94,14 +86,43 @@ export default class ProgramChangeController { }); } + deleteLastStep(programBlockEditor: ?ProgramBlockEditor) { + this.app.setState((state) => { + const index = state.programSequence.getProgramLength() - 1; + if (index >= 0) { + const command = state.programSequence.getProgramStepAt(index); + this.playAnnouncementForDelete(command); + + // As we are deleting the last step, focus the final add node. + if (programBlockEditor) { + programBlockEditor.focusAddNodeAfterUpdate(index); + } + + return { + programSequence: state.programSequence.deleteStep(index) + }; + } else { + return {}; + } + }); + } + // Internal methods playAnnouncementForAdd(command: string) { + this.playAnnouncementForChange(command, 'add'); + } + + playAnnouncementForDelete(command:string) { + this.playAnnouncementForChange(command, 'delete'); + } + + playAnnouncementForChange(command:string, changeType: string) { const commandString = this.intl.formatMessage({ - id: "Announcement." + (command || "") + id: "Announcement." + command }); this.audioManager.playAnnouncement( - 'add', + changeType, this.intl, { command: commandString } ); diff --git a/src/ProgramChangeController.test.js b/src/ProgramChangeController.test.js index dc8f6958..e12d63ce 100644 --- a/src/ProgramChangeController.test.js +++ b/src/ProgramChangeController.test.js @@ -295,3 +295,43 @@ describe('Test deleteProgramStep()', () => { }); }); + +describe('Test deleteLastStep()', () => { + test('When deleting the last step, then focus is set to the add-node after the program', (done) => { + expect.assertions(7); + + const { controller, appMock, audioManagerMock } = createProgramChangeController(); + + appMock.setState.mockImplementation((callback) => { + const newState = callback({ + programSequence: new ProgramSequence(['forward1', 'forward2'], 0) + }); + + // The program should be updated + expect(newState.programSequence.getProgram()).toStrictEqual( + ['forward1']); + + // The announcement should be made + expect(audioManagerMock.playAnnouncement.mock.calls.length).toBe(1); + expect(audioManagerMock.playAnnouncement.mock.calls[0][0]).toBe('delete'); + expect(audioManagerMock.playAnnouncement.mock.calls[0][2]).toStrictEqual({ + command: 'forward 2 squares' + }); + + // The add-node after the program should be focused + // $FlowFixMe: Jest mock API + const programBlockEditorMock = ProgramBlockEditor.mock.instances[0]; + expect(programBlockEditorMock.focusCommandBlockAfterUpdate.mock.calls.length).toBe(0); + expect(programBlockEditorMock.focusAddNodeAfterUpdate.mock.calls.length).toBe(1); + expect(programBlockEditorMock.focusAddNodeAfterUpdate.mock.calls[0][0]).toBe(1); + + done(); + }); + + // $FlowFixMe: Jest mock API + ProgramBlockEditor.mockClear(); + // $FlowFixMe: Jest mock API + controller.deleteLastStep(new ProgramBlockEditor()); + }); +}); +