From 2049edf874e7de5b4259329a85ac4cf0048418d0 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 23 Jan 2021 19:22:02 -0600 Subject: [PATCH 1/9] split didSend and didFailToSend actions --- src/actions/lwp3-bootloader.ts | 20 +++++++++++++++----- src/sagas/lwp3-bootloader-ble.ts | 3 ++- src/sagas/lwp3-bootloader-protocol.ts | 23 ++++++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/actions/lwp3-bootloader.ts b/src/actions/lwp3-bootloader.ts index c0066c1b..fde0834d 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; 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..4918dccc 100644 --- a/src/sagas/lwp3-bootloader-protocol.ts +++ b/src/sagas/lwp3-bootloader-protocol.ts @@ -3,10 +3,18 @@ // 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, @@ -104,10 +112,15 @@ 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, + ), + }); + yield* put(didRequest(action.id, failedToSend?.err)); } } From a9d827d21dadd1ac1a68c6f154db518c4829ddc0 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 23 Jan 2021 19:34:49 -0600 Subject: [PATCH 2/9] split didRequest and didFailToRequest --- src/actions/index.ts | 2 ++ src/actions/lwp3-bootloader.ts | 49 ++++++++++++++++++++++----- src/sagas/flash-firmware.test.ts | 5 +-- src/sagas/flash-firmware.ts | 24 +++++++++---- src/sagas/lwp3-bootloader-protocol.ts | 8 ++++- 5 files changed, 70 insertions(+), 18 deletions(-) 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 fde0834d..07c74789 100644 --- a/src/actions/lwp3-bootloader.ts +++ b/src/actions/lwp3-bootloader.ts @@ -365,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/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-protocol.ts b/src/sagas/lwp3-bootloader-protocol.ts index 4918dccc..55f5922b 100644 --- a/src/sagas/lwp3-bootloader-protocol.ts +++ b/src/sagas/lwp3-bootloader-protocol.ts @@ -21,6 +21,7 @@ import { BootloaderRequestActionType, checksumResponse, didError, + didFailToRequest, didRequest, eraseResponse, errorResponse, @@ -120,7 +121,12 @@ function* encodeRequest(): Generator { BootloaderConnectionActionType.DidFailToSend, ), }); - yield* put(didRequest(action.id, failedToSend?.err)); + + if (failedToSend) { + yield* put(didFailToRequest(action.id, failedToSend.err)); + } else { + yield* put(didRequest(action.id)); + } } } From 9b0427fd3389c8264d9f14be9f842bff0fa0dcc8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 10:39:43 -0600 Subject: [PATCH 3/9] allow selecting text in dialogs and toasts It is useful to be able to copy this info when reporting bugs. Issue: pybricks/pybricks-support#228 --- src/index.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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; } From fcb0bf3d9c367f4911b7c0a8f928a276de8751ae Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 10:47:15 -0600 Subject: [PATCH 4/9] add Select All to terminal context menu The usual keyboard shortcut of Ctrl-A is not allowed since that triggers raw REPL mode in MicroPython. --- src/components/Terminal.tsx | 5 +++++ src/components/terminal-i18n.en.json | 1 + src/components/terminal-i18n.ts | 1 + 3 files changed, 7 insertions(+) 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/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', } From 73f155d37064fa8163ddb14955b70158f7c3ccc6 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 10:56:41 -0600 Subject: [PATCH 5/9] add Select All to editor context menu --- src/components/Editor.tsx | 6 ++++++ src/components/editor-i18n.en.json | 1 + src/components/editor-i18n.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 669fd12b..971d79d4 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -158,6 +158,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()} 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', } From c209bdb1aca0bf31d5ee90c3967e0fdb87900adf Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 11:27:40 -0600 Subject: [PATCH 6/9] replace settings icon with scaled up version This is a scaled up version of the icon from blueprintsjs. --- src/components/images/settings.svg | 112 ++++++----------------------- 1 file changed, 21 insertions(+), 91 deletions(-) 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 From 5614bb17478099aa2925def95b430230fd6b32ff Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 12:27:16 -0600 Subject: [PATCH 7/9] add check command to editor This adds a check command ala Mu editor. --- src/components/Editor.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 971d79d4..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' }, @@ -192,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( From de573407cb65e334d7df7411d1296660aa7e0dcf Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 13:31:42 -0600 Subject: [PATCH 8/9] Fix crash when tabbing through buttons This fixes a crash caused by react-dropzone due to the fact that button elements aren't compatible with whatever react-dropzone is looking for. TypeError: rootRef.current.isEqualNode is not a function (anonymous function) node_modules/react-dropzone/dist/es/index.js:473 470 | 471 | var onKeyDownCb = useCallback(function (event) { 472 | // Ignore keyboard events bubbling up the DOM tree > 473 | if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { | ^ 474 | return; 475 | } 476 | Wrapping it in a div element solves the problem. Also disable tabIndex on the dropzone to prevent the button from being focused twice when tabbing through the buttons. --- src/components/OpenFileButton.tsx | 55 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) 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} > - + +
)} From 3214dbb1694c6776106eb62216f7e113ea0f0322 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 24 Jan 2021 14:11:04 -0600 Subject: [PATCH 9/9] v1.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": {