diff --git a/src/components/EscDshotDirection/Body.html b/src/components/EscDshotDirection/Body.html index 0129468579..c6d9ea7cde 100644 --- a/src/components/EscDshotDirection/Body.html +++ b/src/components/EscDshotDirection/Body.html @@ -27,6 +27,14 @@

+ +
+ ⌨️ 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(); });