+
+
+ ⌨️ Keyboard Shortcuts:
+ Space = Spin/Stop Motors
+ |
+ 1-8 = Toggle Direction
+
+
diff --git a/src/components/EscDshotDirection/EscDshotDirectionComponent.js b/src/components/EscDshotDirection/EscDshotDirectionComponent.js
index ee464f34be..de2bee768f 100644
--- a/src/components/EscDshotDirection/EscDshotDirectionComponent.js
+++ b/src/components/EscDshotDirection/EscDshotDirectionComponent.js
@@ -26,6 +26,17 @@ class EscDshotDirectionComponent {
this._allMotorsAreSpinning = false;
this._spinDirectionToggleIsActive = true;
this._activationButtonTimeoutId = null;
+ this._isKeyboardControlEnabled = false;
+ this._spacebarPressed = false;
+ this._keyboardEventHandlerBound = false;
+ this._isWizardActive = false;
+ this._globalKeyboardActive = false;
+
+ // Bind methods to preserve 'this' context - CRITICAL for event handlers
+ this._handleWizardKeyDown = this._handleWizardKeyDown.bind(this);
+ this._handleWizardKeyUp = this._handleWizardKeyUp.bind(this);
+ this._handleGlobalKeyDown = this._handleGlobalKeyDown.bind(this);
+ this._handleWindowBlur = this._handleWindowBlur.bind(this);
this._contentDiv.load("./components/EscDshotDirection/Body.html", () => {
this._initializeDialog();
@@ -285,9 +296,196 @@ class EscDshotDirectionComponent {
}
}
+ _enableGlobalKeyboard() {
+ if (this._globalKeyboardActive) return;
+
+ document.addEventListener("keydown", this._handleGlobalKeyDown, true);
+ this._globalKeyboardActive = true;
+ }
+
+ _disableGlobalKeyboard() {
+ document.removeEventListener("keydown", this._handleGlobalKeyDown, true);
+ this._globalKeyboardActive = false;
+ }
+
+ _handleGlobalKeyDown(event) {
+ // Only handle spacebar for wizard workflow progression
+ if (event.code !== "Space" || event.repeat) {
+ return;
+ }
+
+ // Only process keyboard input if the dialog is actually visible
+ // Check if either the warning content OR main content is visible
+ const dialogIsVisible =
+ (this._domWarningContentBlock && this._domWarningContentBlock.is(":visible")) ||
+ (this._domMainContentBlock && this._domMainContentBlock.is(":visible"));
+
+ if (!dialogIsVisible) {
+ return;
+ }
+
+ // Step 1: Check the safety checkbox if it's not checked and warning is visible
+ if (this._domWarningContentBlock.is(":visible") && !this._domAgreeSafetyCheckBox.is(":checked")) {
+ event.preventDefault();
+ event.stopPropagation();
+ this._domAgreeSafetyCheckBox.prop("checked", true);
+ this._domAgreeSafetyCheckBox.trigger("change");
+ return;
+ }
+
+ // Step 2: Start wizard if checkbox is checked and wizard isn't open yet
+ if (this._domWarningContentBlock.is(":visible") && this._domAgreeSafetyCheckBox.is(":checked")) {
+ event.preventDefault();
+ event.stopPropagation();
+ this._onStartWizardButtonClicked();
+ return;
+ }
+
+ // Step 3: Spin motors if wizard is open but not spinning yet
+ if (
+ this._domMainContentBlock.is(":visible") &&
+ this._domSpinWizardButton.is(":visible") &&
+ !this._isWizardActive
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ // Mark spacebar as pressed since we're transitioning to wizard control while key is down
+ this._spacebarPressed = true;
+ this._onSpinWizardButtonClicked();
+ return;
+ }
+
+ // Step 4: If wizard is active, let the wizard keyboard handler take over
+ // (no action needed here, the _handleWizardKeyDown will handle it)
+ }
+
+ _enableKeyboardControl() {
+ if (this._keyboardEventHandlerBound) return;
+
+ // CRITICAL: Use capture phase (third parameter = true) for reliable event handling
+ // This prevents other elements from stopping propagation before we handle the event
+ document.addEventListener("keydown", this._handleWizardKeyDown, true);
+ document.addEventListener("keyup", this._handleWizardKeyUp, true);
+
+ // SAFETY FEATURE: Stop motors if user switches windows while holding spacebar
+ window.addEventListener("blur", this._handleWindowBlur);
+
+ this._keyboardEventHandlerBound = true;
+ this._isKeyboardControlEnabled = true;
+ }
+
+ _disableKeyboardControl() {
+ document.removeEventListener("keydown", this._handleWizardKeyDown, true);
+ document.removeEventListener("keyup", this._handleWizardKeyUp, true);
+ window.removeEventListener("blur", this._handleWindowBlur);
+ this._keyboardEventHandlerBound = false;
+ this._isKeyboardControlEnabled = false;
+ this._spacebarPressed = false;
+ }
+
+ _handleWizardKeyDown(event) {
+ // Only handle events when keyboard control is active
+ if (!this._isKeyboardControlEnabled || !this._isWizardActive) {
+ return;
+ }
+
+ // SPACEBAR: Spin all motors (hold to spin, release to stop)
+ if (event.code === "Space") {
+ event.preventDefault();
+ event.stopPropagation();
+ // CRITICAL: Check !event.repeat to prevent multiple triggers when holding key
+ if (!this._spacebarPressed && !event.repeat) {
+ this._spacebarPressed = true;
+ this._handleSpacebarPress();
+ }
+ return;
+ }
+
+ // NUMBER KEYS 1-8: Toggle individual motor direction
+ if (event.key >= "1" && event.key <= "8" && !event.repeat) {
+ event.preventDefault();
+ event.stopPropagation();
+ const motorIndex = parseInt(event.key) - 1;
+
+ if (motorIndex < this._numberOfMotors) {
+ this._toggleMotorDirection(motorIndex);
+ }
+ return;
+ }
+ }
+
+ _handleWizardKeyUp(event) {
+ if (!this._isKeyboardControlEnabled || !this._isWizardActive) {
+ return;
+ }
+
+ // SPACEBAR RELEASE: Stop motors immediately
+ if (event.code === "Space") {
+ event.preventDefault();
+ event.stopPropagation();
+ if (this._spacebarPressed) {
+ this._spacebarPressed = false;
+ this._handleSpacebarRelease();
+ }
+ }
+ }
+
+ _handleSpacebarPress() {
+ this._motorDriver.spinAllMotors();
+ }
+
+ _handleSpacebarRelease() {
+ this._motorDriver.stopAllMotorsNow();
+ }
+
+ _handleWindowBlur() {
+ // SAFETY FEATURE: Stop motors if user switches windows while holding spacebar
+ if (this._spacebarPressed) {
+ this._spacebarPressed = false;
+ this._handleSpacebarRelease();
+ }
+ }
+
+ _toggleMotorDirection(motorIndex) {
+ const button = this._wizardMotorButtons[motorIndex];
+ const currentlyReversed = button.hasClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
+
+ if (currentlyReversed) {
+ button.removeClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
+ this._motorDriver.setEscSpinDirection(motorIndex, DshotCommand.dshotCommands_e.DSHOT_CMD_SPIN_DIRECTION_1);
+ } else {
+ button.addClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
+ this._motorDriver.setEscSpinDirection(motorIndex, DshotCommand.dshotCommands_e.DSHOT_CMD_SPIN_DIRECTION_2);
+ }
+ }
+
+ open() {
+ // Enable global keyboard when dialog is opened
+ this._enableGlobalKeyboard();
+ }
+
close() {
+ // Disable keyboard handlers first to prevent any new input
+ this._disableKeyboardControl();
+ this._disableGlobalKeyboard();
+
+ // If wizard is active, deactivate buttons but DON'T clear the flag yet
+ // This ensures pending motor direction commands complete
+ if (this._isWizardActive) {
+ this._deactivateWizardMotorButtons();
+ }
+
+ // Stop motors (this adds stop commands to the queue)
this._motorDriver.stopAllMotorsNow();
+
+ // Deactivate motor driver - this tells queue to stop AFTER processing current commands
+ // This is critical - it allows direction change + save commands to complete
this._motorDriver.deactivate();
+
+ // Clear wizard flag after motor driver deactivation
+ this._isWizardActive = false;
+
+ // Reset GUI last
this._resetGui();
}
@@ -363,6 +561,10 @@ class EscDshotDirectionComponent {
this._motorDriver.spinAllMotors();
this._activateWizardMotorButtons(0);
+
+ // NEW: Enable keyboard shortcuts when wizard starts spinning
+ this._isWizardActive = true;
+ this._enableKeyboardControl();
}
_onStopWizardButtonClicked() {
@@ -370,6 +572,10 @@ class EscDshotDirectionComponent {
this._domSpinningWizard.toggle(false);
this._motorDriver.stopAllMotorsNow();
this._deactivateWizardMotorButtons();
+
+ // NEW: Disable keyboard shortcuts when wizard stops
+ this._disableKeyboardControl();
+ this._isWizardActive = false;
}
_toggleMainContent(value) {
diff --git a/src/css/tabs/motors.less b/src/css/tabs/motors.less
index f008f063ad..6bdd3c30be 100644
--- a/src/css/tabs/motors.less
+++ b/src/css/tabs/motors.less
@@ -192,6 +192,50 @@
#escDshotDirectionDialog-Content {
flex-grow: 1;
}
+
+ // Keyboard shortcuts tooltip
+ .keyboard-shortcuts-tooltip {
+ background-color: var(--surface-200);
+ border-left: 3px solid var(--accent-color);
+ border-radius: 4px;
+ padding: 10px 15px;
+ margin: 10px 0;
+ text-align: center;
+ font-size: 0.9em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 8px;
+
+ strong {
+ color: var(--accent-text);
+ margin-right: 8px;
+ }
+
+ .shortcut-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ kbd {
+ background-color: var(--surface-300);
+ border: 1px solid var(--surface-500);
+ border-radius: 3px;
+ padding: 2px 6px;
+ font-family: monospace;
+ font-size: 0.85em;
+ font-weight: bold;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+
+ .shortcut-separator {
+ color: var(--surface-500);
+ margin: 0 4px;
+ }
+ }
+
#dialog-mixer-reset {
width: 400px;
height: fit-content;
diff --git a/src/js/tabs/motors.js b/src/js/tabs/motors.js
index 10a8fc42c0..a73aeabbc8 100644
--- a/src/js/tabs/motors.js
+++ b/src/js/tabs/motors.js
@@ -1353,6 +1353,7 @@ motors.initialize = async function (callback) {
$("#escDshotDirectionDialog-Open").click(function () {
$(document).on("keydown", onDocumentKeyPress);
+ escDshotDirectionComponent.open();
domEscDshotDirectionDialog[0].showModal();
});