diff --git a/package.json b/package.json index 749cf4fe..5d1fc193 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pybricks/pybricks-code", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "license": "MIT", "author": "The Pybricks Authors", "repository": { diff --git a/src/actions/index.ts b/src/actions/index.ts index e4ded3ad..698e8d22 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -11,6 +11,7 @@ import { HubAction, HubMessageAction } from './hub'; import { LicenseAction } from './license'; import { BootloaderConnectionAction, + BootloaderDidFailToRequestAction, BootloaderDidRequestAction, BootloaderRequestAction, BootloaderResponseAction, @@ -31,6 +32,7 @@ export type Action = | BleUartAction | BootloaderConnectionAction | BootloaderDidRequestAction + | BootloaderDidFailToRequestAction | BootloaderRequestAction | BootloaderResponseAction | EditorAction diff --git a/src/actions/lwp3-bootloader.ts b/src/actions/lwp3-bootloader.ts index c0066c1b..07c74789 100644 --- a/src/actions/lwp3-bootloader.ts +++ b/src/actions/lwp3-bootloader.ts @@ -37,6 +37,10 @@ export enum BootloaderConnectionActionType { * Finished sending a message. */ DidSend = 'bootloader.action.connection.did.send', + /** + * Sending a message failed with error. + */ + DidFailToSend = 'bootloader.action.connection.did.failToSend', /** * The connection received a message. */ @@ -152,12 +156,18 @@ export function send( return { type: BootloaderConnectionActionType.Send, data, withResponse }; } -export type BootloaderConnectionDidSendAction = Action & { - err?: Error; +export type BootloaderConnectionDidSendAction = Action; + +export function didSend(): BootloaderConnectionDidSendAction { + return { type: BootloaderConnectionActionType.DidSend }; +} + +export type BootloaderConnectionDidFailToSendAction = Action & { + err: Error; }; -export function didSend(err?: Error): BootloaderConnectionDidSendAction { - return { type: BootloaderConnectionActionType.DidSend, err }; +export function didFailToSend(err: Error): BootloaderConnectionDidFailToSendAction { + return { type: BootloaderConnectionActionType.DidFailToSend, err }; } export type BootloaderConnectionDidReceiveAction = Action & { @@ -184,7 +194,7 @@ export type BootloaderConnectionAction = | BootloaderConnectionDidErrorAction | BootloaderConnectionSendAction | BootloaderConnectionDidSendAction - | BootloaderConnectionDidSendAction + | BootloaderConnectionDidFailToSendAction | BootloaderConnectionDidReceiveAction | BootloaderConnectionDidDisconnectAction; @@ -355,26 +365,57 @@ export type BootloaderDidRequestType = 'bootloader.action.did.request'; export const BootloaderDidRequestType = 'bootloader.action.did.request'; /** - * Action that indicates a request was sent or failed to send. + * Action that indicates a request was sent. */ export type BootloaderDidRequestAction = Action & { /** * The unique identifier of the action. */ id: number; - /** - * The error on failure or undefined on success. - */ - err?: Error; }; /** - * Creates an action that indicates a request was sent or failed to send. + * Creates an action that indicates a request was sent. * @param id The unique identifier of the action. - * @param err The error message on failure or undefined on success. */ -export function didRequest(id: number, err?: Error): BootloaderDidRequestAction { - return { type: BootloaderDidRequestType, id, err }; +export function didRequest(id: number): BootloaderDidRequestAction { + return { type: BootloaderDidRequestType, id }; +} + +/** + * Action type for bootloader did fail to request action. + */ +export type BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest'; + +/** + * Action type for bootloader did fail to request action. + */ +export const BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest'; + +/** + * Action that indicates a request failed to send. + */ +export type BootloaderDidFailToRequestAction = Action & { + /** + * The unique identifier of the action. + */ + id: number; + /** + * The error. + */ + err: Error; +}; + +/** + * Creates an action that indicates a request failed to send. + * @param id The unique identifier of the action. + * @param err The error message. + */ +export function didFailToRequest( + id: number, + err: Error, +): BootloaderDidFailToRequestAction { + return { type: BootloaderDidFailToRequestType, id, err }; } /** diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 669fd12b..c6588c7d 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -16,6 +16,7 @@ import { IAceEditor } from 'react-ace/lib/types'; import { connect } from 'react-redux'; import { Action, Dispatch } from '../actions'; import { setEditSession, storageChanged } from '../actions/editor'; +import { compile } from '../actions/mpy'; import { RootState } from '../reducers'; import { isMacOS } from '../utils/os'; import { EditorStringId } from './editor-i18n'; @@ -38,6 +39,7 @@ type StateProps = { type DispatchProps = { onSessionChanged: (session?: Ace.EditSession) => void; onProgramStorageChanged: (newValue: string) => void; + onCheck: (script: string) => void; }; type EditorProps = StateProps & DispatchProps & WithI18nProps; @@ -76,7 +78,7 @@ class Editor extends React.Component { } render(): JSX.Element { - const { darkMode, i18n, onSessionChanged } = this.props; + const { darkMode, i18n, onSessionChanged, onCheck } = this.props; return (
this.editor?.resize()}> @@ -97,12 +99,19 @@ class Editor extends React.Component { enableSnippets: true, }} onLoad={(e): void => { + // default binding is F2 which conflicts with 'check' + e.commands.byName['toggleFoldWidget'].bindKey = { + win: 'Shift-F2', + mac: 'Shift-F2', + }; + config.loadModule( 'ace/ext/menu_tools/get_editor_keyboard_shortcuts', (m) => { this.keyBindings = m.getEditorKeybordShortcuts(e); }, ); + config.loadModule('ace/ext/keybinding_menu', (m) => m.init(e), ); @@ -114,6 +123,12 @@ class Editor extends React.Component { localStorage.setItem('program', v); }} commands={[ + { + // command to check current program for errors + name: 'check', + bindKey: { win: 'F2', mac: 'F2' }, + exec: (editor) => onCheck(editor.getValue()), + }, { name: 'save', bindKey: { win: 'Ctrl-S', mac: 'Cmd-S' }, @@ -158,6 +173,12 @@ class Editor extends React.Component { icon="clipboard" label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'} /> + this.editor?.selectAll()} + text={i18n.translate(EditorStringId.SelectAll)} + icon="blank" + label={isMacOS() ? 'Cmd-A' : 'Ctrl-A'} + /> this.editor?.undo()} @@ -186,6 +207,9 @@ const mapStateToProps = (state: RootState): StateProps => ({ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ onSessionChanged: (s): Action => dispatch(setEditSession(s)), onProgramStorageChanged: (v): Action => dispatch(storageChanged(v)), + // REVISIT: the options here might need to be changed - hopefully there is + // one setting that works for all hub types for cases where we aren't connected. + onCheck: (script) => dispatch(compile(script)), }); export default connect( diff --git a/src/components/OpenFileButton.tsx b/src/components/OpenFileButton.tsx index e63c07e7..28402120 100644 --- a/src/components/OpenFileButton.tsx +++ b/src/components/OpenFileButton.tsx @@ -100,33 +100,38 @@ class OpenFileButton extends React.Component { position={Position.BOTTOM} hoverOpenDelay={tooltipDelay} > - + +
)} diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx index 54b869cb..84816b39 100644 --- a/src/components/Terminal.tsx +++ b/src/components/Terminal.tsx @@ -149,6 +149,11 @@ class Terminal extends React.Component { icon="clipboard" label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'} /> + this.xterm.selectAll()} + text={i18n.translate(TerminalStringId.SelectAll)} + icon="blank" + /> this.xterm.clear()} diff --git a/src/components/editor-i18n.en.json b/src/components/editor-i18n.en.json index 45f2b7b1..ba9898fa 100644 --- a/src/components/editor-i18n.en.json +++ b/src/components/editor-i18n.en.json @@ -3,6 +3,7 @@ "placeholder": "Write your program here...", "copy": "Copy", "paste": "Paste", + "selectAll": "Select All", "undo": "Undo", "redo": "Redo" } diff --git a/src/components/editor-i18n.ts b/src/components/editor-i18n.ts index afa1edc8..24faea36 100644 --- a/src/components/editor-i18n.ts +++ b/src/components/editor-i18n.ts @@ -7,6 +7,7 @@ export enum EditorStringId { Placeholder = 'editor.placeholder', Copy = 'editor.copy', Paste = 'editor.paste', + SelectAll = 'editor.selectAll', Undo = 'editor.undo', Redo = 'editor.redo', } diff --git a/src/components/images/settings.svg b/src/components/images/settings.svg index 559cd5f1..2755ef77 100644 --- a/src/components/images/settings.svg +++ b/src/components/images/settings.svg @@ -1,5 +1,5 @@ - + image/svg+xml + inkscape:current-layer="Layer_1" /> + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="clip-rule:evenodd;fill-rule:evenodd;fill:#ffffff;fill-opacity:1" /> + \ No newline at end of file diff --git a/src/components/terminal-i18n.en.json b/src/components/terminal-i18n.en.json index d0fd27a2..3575aa9b 100644 --- a/src/components/terminal-i18n.en.json +++ b/src/components/terminal-i18n.en.json @@ -2,6 +2,7 @@ "terminal": { "copy": "Copy", "paste": "Paste", + "selectAll": "Select All", "clear": "Clear" } } diff --git a/src/components/terminal-i18n.ts b/src/components/terminal-i18n.ts index ec84c3c2..774f98e4 100644 --- a/src/components/terminal-i18n.ts +++ b/src/components/terminal-i18n.ts @@ -6,5 +6,6 @@ export enum TerminalStringId { Copy = 'terminal.copy', Paste = 'terminal.paste', + SelectAll = 'terminal.selectAll', Clear = 'terminal.clear', } diff --git a/src/index.scss b/src/index.scss index a7ad1e20..3fe4babf 100644 --- a/src/index.scss +++ b/src/index.scss @@ -40,7 +40,11 @@ body { // global style tweaks -.#{$ns}-dialog { +.#{$ns}-toast { + user-select: text; +} + +.#{$ns}-button { user-select: none; } diff --git a/src/sagas/flash-firmware.test.ts b/src/sagas/flash-firmware.test.ts index 2f356a60..dcf9aa74 100644 --- a/src/sagas/flash-firmware.test.ts +++ b/src/sagas/flash-firmware.test.ts @@ -27,6 +27,7 @@ import { didConnect, didDisconnect, didFailToConnect, + didFailToRequest, didRequest, disconnect, eraseRequest, @@ -329,7 +330,7 @@ describe('flashFirmware', () => { // On city hub, we can end up in this situation. BLE writeValueWithResponse() // doesn't return until erasing is done, so there is a long window for // this to happen. - saga.put(didRequest(0, new Error('failed due to disconnect'))); + saga.put(didFailToRequest(0, new Error('failed due to disconnect'))); await saga.end(); }); @@ -387,7 +388,7 @@ describe('flashFirmware', () => { expect(action).toEqual(infoRequest(0)); const testError = new Error('test'); - saga.put(didRequest(0, testError)); + saga.put(didFailToRequest(0, testError)); // should get a failure to start diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index 2176f82d..7d3234e7 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -34,6 +34,8 @@ import { BootloaderChecksumResponseAction, BootloaderConnectionAction, BootloaderConnectionActionType, + BootloaderDidFailToRequestAction, + BootloaderDidFailToRequestType, BootloaderDidRequestAction, BootloaderDidRequestType, BootloaderEraseResponseAction, @@ -85,15 +87,25 @@ function* disconnectAndCancel(): SagaGenerator { } function* waitForDidRequest(id: number): SagaGenerator { - const request = yield* take( - (a: Action) => a.type === BootloaderDidRequestType && a.id === id, - ); - if (request.err) { - yield* put(didFailToFinish(FailToFinishReasonType.BleError, request.err)); + const { requested, failedToRequest } = yield* race({ + requested: take( + (a: Action) => a.type === BootloaderDidRequestType && a.id === id, + ), + failedToRequest: take( + (a: Action) => a.type === BootloaderDidFailToRequestType && a.id === id, + ), + }); + + if (failedToRequest) { + yield* put( + didFailToFinish(FailToFinishReasonType.BleError, failedToRequest.err), + ); yield* disconnectAndCancel(); } - return request; + defined(requested); + + return requested; } /** diff --git a/src/sagas/lwp3-bootloader-ble.ts b/src/sagas/lwp3-bootloader-ble.ts index 3d825535..69ad67cd 100644 --- a/src/sagas/lwp3-bootloader-ble.ts +++ b/src/sagas/lwp3-bootloader-ble.ts @@ -13,6 +13,7 @@ import { didConnect, didDisconnect, didFailToConnect, + didFailToSend, didReceive, didSend, } from '../actions/lwp3-bootloader'; @@ -34,7 +35,7 @@ function* write( } yield* put(didSend()); } catch (err) { - yield* put(didSend(err)); + yield* put(didFailToSend(err)); } } diff --git a/src/sagas/lwp3-bootloader-protocol.ts b/src/sagas/lwp3-bootloader-protocol.ts index cc858eb3..55f5922b 100644 --- a/src/sagas/lwp3-bootloader-protocol.ts +++ b/src/sagas/lwp3-bootloader-protocol.ts @@ -3,16 +3,25 @@ // File: sagas/lwp3-bootloader-protocol.ts // Handles LEGO Wireless Protocol v3 Bootloader protocol. -import { actionChannel, fork, put, take, takeEvery } from 'typed-redux-saga/macro'; +import { + actionChannel, + fork, + put, + race, + take, + takeEvery, +} from 'typed-redux-saga/macro'; import { Action } from '../actions'; import { BootloaderConnectionActionType, + BootloaderConnectionDidFailToSendAction, BootloaderConnectionDidReceiveAction, BootloaderConnectionDidSendAction, BootloaderRequestAction, BootloaderRequestActionType, checksumResponse, didError, + didFailToRequest, didRequest, eraseResponse, errorResponse, @@ -104,10 +113,20 @@ function* encodeRequest(): Generator { continue; } - const sent = yield* take( - BootloaderConnectionActionType.DidSend, - ); - yield* put(didRequest(action.id, sent.err)); + const { failedToSend } = yield* race({ + sent: take( + BootloaderConnectionActionType.DidSend, + ), + failedToSend: take( + BootloaderConnectionActionType.DidFailToSend, + ), + }); + + if (failedToSend) { + yield* put(didFailToRequest(action.id, failedToSend.err)); + } else { + yield* put(didRequest(action.id)); + } } }