Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/EscDshotDirection/Body.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ <h4 id="escDshotDirectionDialog-SettingsAutoSaved-Normal" i18n="escDshotDirectio
<div id="escDshotDirectionDialog-WizardDialog" class="display-contents">
<a href="#" id="escDshotDirectionDialog-SpinWizard" class="regular-button" i18n="escDshotDirectionDialog-SpinWizard"></a>
<div id="escDshotDirectionDialog-SpinningWizard" class="display-contents">
<!-- Keyboard shortcuts tooltip -->
<div class="keyboard-shortcuts-tooltip">
<strong>⌨️ Keyboard Shortcuts:</strong>
<span class="shortcut-item"><kbd>Space</kbd> = Spin/Stop Motors</span>
<span class="shortcut-separator">|</span>
<span class="shortcut-item"><kbd>1-8</kbd> = Toggle Direction</span>
</div>

<h4 id="escDshotDirectionDialog-WizardActionHint" i18n="escDshotDirectionDialog-WizardActionHint"></h4>
<h4 id="escDshotDirectionDialog-WizardActionHintSecondLine" i18n="escDshotDirectionDialog-WizardActionHintSecondLine"></h4>
<div id="escDshotDirectionDialog-WizardMotorButtons">
Expand Down
206 changes: 206 additions & 0 deletions src/components/EscDshotDirection/EscDshotDirectionComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -363,13 +561,21 @@ class EscDshotDirectionComponent {
this._motorDriver.spinAllMotors();

this._activateWizardMotorButtons(0);

// NEW: Enable keyboard shortcuts when wizard starts spinning
this._isWizardActive = true;
this._enableKeyboardControl();
}

_onStopWizardButtonClicked() {
this._domSpinWizardButton.toggle(true);
this._domSpinningWizard.toggle(false);
this._motorDriver.stopAllMotorsNow();
this._deactivateWizardMotorButtons();

// NEW: Disable keyboard shortcuts when wizard stops
this._disableKeyboardControl();
this._isWizardActive = false;
}

_toggleMainContent(value) {
Expand Down
44 changes: 44 additions & 0 deletions src/css/tabs/motors.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/js/tabs/motors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ motors.initialize = async function (callback) {

$("#escDshotDirectionDialog-Open").click(function () {
$(document).on("keydown", onDocumentKeyPress);
escDshotDirectionComponent.open();
domEscDshotDirectionDialog[0].showModal();
});

Expand Down