diff --git a/README.md b/README.md index dd868a68a..f52084c69 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Device Simulator Express provides several commands in the Command Palette (F1 or In Device Simulator Express, you can use keyboard to interact with the device: -- Push Button `A & B: A B` +- Push Button `A for A, B for B, C for A & B` - Capacitive Touch Sensor `A1 – A7: SHIFT + 1~7` - Slider Switch: `SHIFT + S` - Refresh the simulator: `SHIFT + R` @@ -175,7 +175,7 @@ Using the simulator for the micro:bit is similar to using the one for the CPX. T Please review the CPX's ["How to use" guide](#How-to-use) for more info. ### Keybindings -- Push Button `A & B: A B` +- Push Button `A for A, B for B, C for A & B` - Refresh the simulator: `SHIFT + R` ## Contribute diff --git a/package.json b/package.json index 887c1efcf..0c716c8f6 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "type": "boolean", "default": false, "description": "%deviceSimulatorExpressExtension.configuration.properties.previewMode%", - "scope": "resource" + "scope": "resource" } } }, @@ -351,4 +351,4 @@ "extensionDependencies": [ "ms-python.python" ] -} \ No newline at end of file +} diff --git a/src/adafruit_circuitplayground/constants.py b/src/adafruit_circuitplayground/constants.py index 505fd5d57..a96083795 100644 --- a/src/adafruit_circuitplayground/constants.py +++ b/src/adafruit_circuitplayground/constants.py @@ -27,17 +27,6 @@ VALID_PIXEL_ASSIGN_ERROR = "The pixel color value should be a tuple with three values between 0 and 255 or a hexadecimal color between 0x000000 and 0xFFFFFF." -TELEMETRY_EVENT_NAMES = { - "TAPPED": "API.TAPPED", - "PLAY_FILE": "API.PLAY.FILE", - "PLAY_TONE": "API.PLAY.TONE", - "START_TONE": "API.START.TONE", - "STOP_TONE": "API.STOP.TONE", - "DETECT_TAPS": "API.DETECT.TAPS", - "ADJUST_THRESHOLD": "API.ADJUST.THRESHOLD", - "RED_LED": "API.RED.LED", - "PIXELS": "API.PIXELS", -} ERROR_SENDING_EVENT = "Error trying to send event to the process : " TIME_DELAY = 0.03 diff --git a/src/view/components/cpx/CpxImage.tsx b/src/view/components/cpx/CpxImage.tsx index fd3688b12..ee3d02201 100644 --- a/src/view/components/cpx/CpxImage.tsx +++ b/src/view/components/cpx/CpxImage.tsx @@ -27,14 +27,42 @@ export class CpxImage extends React.Component { initSvgStyle(svgElement, this.props.brightness); setupButtons(this.props); setupPins(this.props); - setupKeyPresses(this.props.onKeyEvent); + this.setupKeyPresses(this.props.onKeyEvent); setupSwitch(this.props); this.updateImage(); } } + componentWillUnmount() { + window.document.removeEventListener("keydown", this.handleKeyDown); + window.document.removeEventListener("keyup", this.handleKeyUp); + } componentDidUpdate() { this.updateImage(); } + setupKeyPresses = ( + onKeyEvent: (event: KeyboardEvent, active: boolean) => void + ) => { + window.document.addEventListener("keydown", this.handleKeyDown); + window.document.addEventListener("keyup", this.handleKeyUp); + }; + + handleKeyDown = (event: KeyboardEvent) => { + const keyEvents = [event.key, event.code]; + // Don't listen to keydown events for the switch, run button, restart button and enter key + if ( + !( + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.S) || + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) || + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.CAPITAL_R) || + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.ENTER) + ) + ) { + this.props.onKeyEvent(event, true); + } + }; + handleKeyUp = (event: KeyboardEvent) => { + this.props.onKeyEvent(event, false); + }; render() { return CPX_SVG; } @@ -309,32 +337,18 @@ const setupButton = (button: HTMLElement, className: string, props: IProps) => { } svgButton.onmousedown = e => props.onMouseDown(button, e); svgButton.onmouseup = e => props.onMouseUp(button, e); - svgButton.onkeydown = e => props.onKeyEvent(e, true); + svgButton.onkeydown = e => { + // ensure that the keydown is enter. + // Or else, if the key is a shortcut instead, + // it may register shortcuts twice + if (e.key === CONSTANTS.KEYBOARD_KEYS.ENTER) { + props.onKeyEvent(e, true); + } + }; svgButton.onkeyup = e => props.onKeyEvent(e, false); svgButton.onmouseleave = e => props.onMouseLeave(button, e); }; -const setupKeyPresses = ( - onKeyEvent: (event: KeyboardEvent, active: boolean) => void -) => { - window.document.addEventListener("keydown", event => { - const keyEvents = [event.key, event.code]; - // Don't listen to keydown events for the switch, run button and enter key - if ( - !( - keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.S) || - keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) || - keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.ENTER) - ) - ) { - onKeyEvent(event, true); - } - }); - window.document.addEventListener("keyup", event => - onKeyEvent(event, false) - ); -}; - const setupSwitch = (props: IProps): void => { const switchElement = window.document.getElementById("SWITCH"); const swInnerElement = window.document.getElementById("SWITCH_INNER"); diff --git a/src/view/components/cpx/CpxSimulator.tsx b/src/view/components/cpx/CpxSimulator.tsx index 673dd6857..21a13524a 100644 --- a/src/view/components/cpx/CpxSimulator.tsx +++ b/src/view/components/cpx/CpxSimulator.tsx @@ -132,6 +132,7 @@ class Simulator extends React.Component<{}, IState> { render() { const playStopImage = this.state.play_button ? StopLogo : PlayLogo; + const playStopLabel = this.state.play_button ? "stop" : "play"; return (
@@ -161,32 +162,33 @@ class Simulator extends React.Component<{}, IState> { onTogglePlay={this.togglePlayClick} onToggleRefresh={this.refreshSimulatorClick} playStopImage={playStopImage} + playStopLabel={playStopLabel} />
); } protected togglePlayClick() { - sendMessage(WEBVIEW_MESSAGES.TOGGLE_PLAY_STOP, { - selected_file: this.state.selected_file, - state: !this.state.play_button, - }); const button = window.document.getElementById(CONSTANTS.ID_NAME.PLAY_BUTTON) || window.document.getElementById(CONSTANTS.ID_NAME.STOP_BUTTON); if (button) { button.focus(); } + sendMessage(WEBVIEW_MESSAGES.TOGGLE_PLAY_STOP, { + selected_file: this.state.selected_file, + state: !this.state.play_button, + }); } protected refreshSimulatorClick() { - sendMessage(WEBVIEW_MESSAGES.REFRESH_SIMULATOR, true); const button = window.document.getElementById( CONSTANTS.ID_NAME.REFRESH_BUTTON ); if (button) { button.focus(); } + sendMessage(WEBVIEW_MESSAGES.REFRESH_SIMULATOR, true); } protected onSelectBlur(event: React.FocusEvent) { @@ -216,6 +218,12 @@ class Simulator extends React.Component<{}, IState> { element = window.document.getElementById( CONSTANTS.ID_NAME.BUTTON_B ); + } else if ( + [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.C) + ) { + element = window.document.getElementById( + CONSTANTS.ID_NAME.BUTTON_AB + ); } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.S) ) { diff --git a/src/view/components/microbit/MicrobitImage.tsx b/src/view/components/microbit/MicrobitImage.tsx index d06f4d0e4..326d0a611 100644 --- a/src/view/components/microbit/MicrobitImage.tsx +++ b/src/view/components/microbit/MicrobitImage.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { VIEW_STATE } from "../../constants"; import { ViewStateContext } from "../../context"; +import CONSTANTS, { MICROBIT_BUTTON_STYLING_CLASSES } from "../../constants"; import "../../styles/Microbit.css"; import { IRefObject, MicrobitSvg } from "./Microbit_svg"; @@ -11,6 +12,7 @@ interface EventTriggers { onMouseUp: (event: Event, buttonKey: string) => void; onMouseDown: (event: Event, buttonKey: string) => void; onMouseLeave: (event: Event, buttonKey: string) => void; + onKeyEvent: (event: KeyboardEvent, active: boolean, key: string) => void; } interface IProps { eventTriggers: EventTriggers; @@ -22,6 +24,11 @@ const BUTTON_CLASSNAME = { DEACTIVATED: "sim-button-deactivated", }; +export enum BUTTONS_KEYS { + BTN_A = "BTN_A", + BTN_B = "BTN_B", + BTN_AB = "BTN_AB", +} // Displays the SVG and call necessary svg modification. export class MicrobitImage extends React.Component { private svgRef: React.RefObject = React.createRef(); @@ -33,6 +40,7 @@ export class MicrobitImage extends React.Component { if (svgElement) { updateAllLeds(this.props.leds, svgElement.getLeds()); setupAllButtons(this.props.eventTriggers, svgElement.getButtons()); + this.setupKeyPresses(this.props.eventTriggers.onKeyEvent); } } componentDidUpdate() { @@ -48,9 +56,56 @@ export class MicrobitImage extends React.Component { } } } + componentWillUnmount() { + window.document.removeEventListener("keydown", this.handleKeyDown); + window.document.removeEventListener("keyup", this.handleKeyUp); + } + setupKeyPresses = ( + onKeyEvent: (event: KeyboardEvent, active: boolean, key: string) => void + ) => { + window.document.addEventListener("keydown", this.handleKeyDown); + window.document.addEventListener("keyup", this.handleKeyUp); + }; + handleKeyDown = (event: KeyboardEvent) => { + const keyEvents = [event.key, event.code]; + // Don't listen to keydown events for the run button, restart button and enter key + if ( + !( + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) || + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.CAPITAL_R) || + keyEvents.includes(CONSTANTS.KEYBOARD_KEYS.ENTER) + ) + ) { + this.props.eventTriggers.onKeyEvent(event, true, event.key); + } + }; + handleKeyUp = (event: KeyboardEvent) => { + this.props.eventTriggers.onKeyEvent(event, false, event.key); + }; render() { return ; } + public updateButtonAttributes(key: BUTTONS_KEYS, isActive: boolean) { + if (this.svgRef.current) { + const button = this.svgRef.current.getButtons()[key].current; + if (button) { + button.focus(); + if (isActive) { + button.children[0].setAttribute( + "class", + MICROBIT_BUTTON_STYLING_CLASSES.KEYPRESSED + ); + } else { + button.children[0].setAttribute( + "class", + MICROBIT_BUTTON_STYLING_CLASSES.DEFAULT + ); + } + button.setAttribute("pressed", `${isActive}`); + button.setAttribute("aria-pressed", `${isActive}`); + } + } + } } MicrobitImage.contextType = ViewStateContext; @@ -59,8 +114,8 @@ const setupButton = ( eventTriggers: EventTriggers, key: string ) => { - buttonElement.setAttribute("class", BUTTON_CLASSNAME.ACTIVE); buttonElement.onmousedown = e => { + buttonElement.focus(); eventTriggers.onMouseDown(e, key); }; buttonElement.onmouseup = e => { @@ -69,6 +124,16 @@ const setupButton = ( buttonElement.onmouseleave = e => { eventTriggers.onMouseLeave(e, key); }; + buttonElement.onkeydown = e => { + // ensure that the keydown is enter, + // or else it may register shortcuts twice + if (e.key === CONSTANTS.KEYBOARD_KEYS.ENTER) { + eventTriggers.onKeyEvent(e, true, key); + } + }; + buttonElement.onkeyup = e => { + eventTriggers.onKeyEvent(e, false, key); + }; }; const setupAllButtons = ( eventTriggers: EventTriggers, @@ -87,6 +152,8 @@ const disableAllButtons = (buttonRefs: IRefObject) => { ref.current.onmousedown = null; ref.current.onmouseup = null; ref.current.onmouseleave = null; + ref.current.onkeydown = null; + ref.current.onkeyup = null; ref.current.setAttribute("class", BUTTON_CLASSNAME.DEACTIVATED); } } diff --git a/src/view/components/microbit/MicrobitSimulator.tsx b/src/view/components/microbit/MicrobitSimulator.tsx index 3e0c9e1c0..c05611396 100644 --- a/src/view/components/microbit/MicrobitSimulator.tsx +++ b/src/view/components/microbit/MicrobitSimulator.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { + CONSTANTS, DEVICE_LIST_KEY, MICROBIT_BUTTONS_KEYS, WEBVIEW_MESSAGES, @@ -9,7 +10,7 @@ import StopLogo from "../../svgs/stop_svg"; import { sendMessage } from "../../utils/MessageUtils"; import Dropdown from "../Dropdown"; import ActionBar from "../simulator/ActionBar"; -import { MicrobitImage } from "./MicrobitImage"; +import { MicrobitImage, BUTTONS_KEYS } from "./MicrobitImage"; const DEFAULT_MICROBIT_STATE: IMicrobitState = { leds: [ @@ -35,6 +36,7 @@ interface IMicrobitState { buttons: { button_a: boolean; button_b: boolean }; } export class MicrobitSimulator extends React.Component { + private imageRef: React.RefObject = React.createRef(); constructor() { super({}); this.state = { @@ -44,6 +46,7 @@ export class MicrobitSimulator extends React.Component { active_editors: [], running_file: "", }; + this.onKeyEvent = this.onKeyEvent.bind(this); } handleMessage = (event: any): void => { const message = event.data; @@ -93,7 +96,7 @@ export class MicrobitSimulator extends React.Component { render() { const playStopImage = this.state.play_button ? StopLogo : PlayLogo; - + const playStopLabel = this.state.play_button ? "stop" : "play"; return (
@@ -108,10 +111,12 @@ export class MicrobitSimulator extends React.Component {
@@ -120,11 +125,18 @@ export class MicrobitSimulator extends React.Component { onTogglePlay={this.togglePlayClick} onToggleRefresh={this.refreshSimulatorClick} playStopImage={playStopImage} + playStopLabel={playStopLabel} />
); } protected togglePlayClick = () => { + const button = + window.document.getElementById(CONSTANTS.ID_NAME.PLAY_BUTTON) || + window.document.getElementById(CONSTANTS.ID_NAME.STOP_BUTTON); + if (button) { + button.focus(); + } sendMessage(WEBVIEW_MESSAGES.TOGGLE_PLAY_STOP, { selected_file: this.state.selected_file, state: !this.state.play_button, @@ -136,6 +148,12 @@ export class MicrobitSimulator extends React.Component { }); } protected refreshSimulatorClick = () => { + const button = window.document.getElementById( + CONSTANTS.ID_NAME.REFRESH_BUTTON + ); + if (button) { + button.focus(); + } sendMessage(WEBVIEW_MESSAGES.REFRESH_SIMULATOR, true); }; protected handleButtonClick = (key: string, isActive: boolean) => { @@ -170,9 +188,66 @@ export class MicrobitSimulator extends React.Component { event.preventDefault(); this.handleButtonClick(key, true); }; - protected onMouseLeave = (event: Event, key: string) => { event.preventDefault(); console.log(`To implement onMouseLeave ${key}`); }; + protected onKeyEvent(event: KeyboardEvent, active: boolean, key: string) { + event.stopPropagation(); + if ([event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.ENTER)) { + this.handleButtonClick(key, active); + if (this.imageRef.current) { + if (key === BUTTONS_KEYS.BTN_A) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); + } else if (key === BUTTONS_KEYS.BTN_B) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); + } else if (key === BUTTONS_KEYS.BTN_AB) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); + } + } + } else if ( + [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.A) + ) { + this.handleButtonClick(BUTTONS_KEYS.BTN_A, active); + if (this.imageRef.current) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); + } + } else if ( + [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.B) + ) { + this.handleButtonClick(BUTTONS_KEYS.BTN_B, active); + if (this.imageRef.current) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); + } + } else if ( + [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.C) + ) { + this.handleButtonClick(BUTTONS_KEYS.BTN_AB, active); + if (this.imageRef.current) { + this.imageRef.current.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); + } + } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) { + this.togglePlayClick(); + } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_R) { + this.refreshSimulatorClick(); + } + } } diff --git a/src/view/components/microbit/Microbit_svg.tsx b/src/view/components/microbit/Microbit_svg.tsx index 3cfe352e1..e2e147412 100644 --- a/src/view/components/microbit/Microbit_svg.tsx +++ b/src/view/components/microbit/Microbit_svg.tsx @@ -1677,12 +1677,12 @@ export class MicrobitSvg extends React.Component { focusable="true" tabIndex={0} role="button" - aria-label="A" + aria-label="a" style={{ fill: "rgb(151, 151, 151)" }} + ref={this.buttonRefs.BTN_A} > ) => void; onToggleRefresh: (event: React.MouseEvent) => void; playStopImage: JSX.Element; + playStopLabel: string; } // Component including the actions done on the Simulator (play/stop, refresh) class ActionBar extends React.Component { public render() { - const { onTogglePlay, onToggleRefresh, playStopImage } = this.props; + const { + onTogglePlay, + onToggleRefresh, + playStopImage, + playStopLabel, + } = this.props; return (