Skip to content

Commit e842a51

Browse files
committed
Add keyboard shortcuts to Motor Direction wizard
- Progressive spacebar workflow: Check checkbox → Start wizard → Spin motors - Hold spacebar to spin motors, release to stop - Number keys 1-8 toggle individual motor directions - Added visual tooltip showing keyboard shortcuts - Properly scoped keyboard handlers to dialog visibility - Fixed settings persistence when closing dialog via any method Improves user workflow by reducing mouse interaction and enabling faster motor direction configuration.
1 parent 105aa26 commit e842a51

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

src/components/EscDshotDirection/Body.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ <h4 id="escDshotDirectionDialog-SettingsAutoSaved-Normal" i18n="escDshotDirectio
2727
<div id="escDshotDirectionDialog-WizardDialog" class="display-contents">
2828
<a href="#" id="escDshotDirectionDialog-SpinWizard" class="regular-button" i18n="escDshotDirectionDialog-SpinWizard"></a>
2929
<div id="escDshotDirectionDialog-SpinningWizard" class="display-contents">
30+
<!-- Keyboard shortcuts tooltip -->
31+
<div class="keyboard-shortcuts-tooltip">
32+
<strong>⌨️ Keyboard Shortcuts:</strong>
33+
<span class="shortcut-item"><kbd>Space</kbd> = Spin/Stop Motors</span>
34+
<span class="shortcut-separator">|</span>
35+
<span class="shortcut-item"><kbd>1-8</kbd> = Toggle Direction</span>
36+
</div>
37+
3038
<h4 id="escDshotDirectionDialog-WizardActionHint" i18n="escDshotDirectionDialog-WizardActionHint"></h4>
3139
<h4 id="escDshotDirectionDialog-WizardActionHintSecondLine" i18n="escDshotDirectionDialog-WizardActionHintSecondLine"></h4>
3240
<div id="escDshotDirectionDialog-WizardMotorButtons">

src/components/EscDshotDirection/EscDshotDirectionComponent.js

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ class EscDshotDirectionComponent {
2626
this._allMotorsAreSpinning = false;
2727
this._spinDirectionToggleIsActive = true;
2828
this._activationButtonTimeoutId = null;
29+
this._isKeyboardControlEnabled = false;
30+
this._spacebarPressed = false;
31+
this._keyboardEventHandlerBound = false;
32+
this._isWizardActive = false;
33+
this._globalKeyboardActive = false;
34+
35+
// Bind methods to preserve 'this' context - CRITICAL for event handlers
36+
this._handleWizardKeyDown = this._handleWizardKeyDown.bind(this);
37+
this._handleWizardKeyUp = this._handleWizardKeyUp.bind(this);
38+
this._handleGlobalKeyDown = this._handleGlobalKeyDown.bind(this);
2939

3040
this._contentDiv.load("./components/EscDshotDirection/Body.html", () => {
3141
this._initializeDialog();
@@ -285,9 +295,193 @@ class EscDshotDirectionComponent {
285295
}
286296
}
287297

298+
_enableGlobalKeyboard() {
299+
if (this._globalKeyboardActive) return;
300+
301+
document.addEventListener("keydown", this._handleGlobalKeyDown, true);
302+
this._globalKeyboardActive = true;
303+
}
304+
305+
_disableGlobalKeyboard() {
306+
document.removeEventListener("keydown", this._handleGlobalKeyDown, true);
307+
this._globalKeyboardActive = false;
308+
}
309+
310+
_handleGlobalKeyDown(event) {
311+
// Only handle spacebar for wizard workflow progression
312+
if (event.code !== "Space" || event.repeat) {
313+
return;
314+
}
315+
316+
// Only process keyboard input if the dialog is actually visible
317+
// Check if either the warning content OR main content is visible
318+
const dialogIsVisible =
319+
(this._domWarningContentBlock && this._domWarningContentBlock.is(":visible")) ||
320+
(this._domMainContentBlock && this._domMainContentBlock.is(":visible"));
321+
322+
if (!dialogIsVisible) {
323+
return;
324+
}
325+
326+
// Step 1: Check the safety checkbox if it's not checked and warning is visible
327+
if (this._domWarningContentBlock.is(":visible") && !this._domAgreeSafetyCheckBox.is(":checked")) {
328+
event.preventDefault();
329+
event.stopPropagation();
330+
this._domAgreeSafetyCheckBox.prop("checked", true);
331+
this._domAgreeSafetyCheckBox.trigger("change");
332+
return;
333+
}
334+
335+
// Step 2: Start wizard if checkbox is checked and wizard isn't open yet
336+
if (this._domWarningContentBlock.is(":visible") && this._domAgreeSafetyCheckBox.is(":checked")) {
337+
event.preventDefault();
338+
event.stopPropagation();
339+
this._onStartWizardButtonClicked();
340+
return;
341+
}
342+
343+
// Step 3: Spin motors if wizard is open but not spinning yet
344+
if (
345+
this._domMainContentBlock.is(":visible") &&
346+
this._domSpinWizardButton.is(":visible") &&
347+
!this._isWizardActive
348+
) {
349+
event.preventDefault();
350+
event.stopPropagation();
351+
// Mark spacebar as pressed since we're transitioning to wizard control while key is down
352+
this._spacebarPressed = true;
353+
this._onSpinWizardButtonClicked();
354+
return;
355+
}
356+
357+
// Step 4: If wizard is active, let the wizard keyboard handler take over
358+
// (no action needed here, the _handleWizardKeyDown will handle it)
359+
}
360+
361+
_enableKeyboardControl() {
362+
if (this._keyboardEventHandlerBound) return;
363+
364+
// CRITICAL: Use capture phase (third parameter = true) for reliable event handling
365+
// This prevents other elements from stopping propagation before we handle the event
366+
document.addEventListener("keydown", this._handleWizardKeyDown, true);
367+
document.addEventListener("keyup", this._handleWizardKeyUp, true);
368+
369+
// SAFETY FEATURE: Stop motors if user switches windows while holding spacebar
370+
window.addEventListener("blur", () => {
371+
if (this._spacebarPressed) {
372+
this._spacebarPressed = false;
373+
this._handleSpacebarRelease();
374+
}
375+
});
376+
377+
this._keyboardEventHandlerBound = true;
378+
this._isKeyboardControlEnabled = true;
379+
}
380+
381+
_disableKeyboardControl() {
382+
document.removeEventListener("keydown", this._handleWizardKeyDown, true);
383+
document.removeEventListener("keyup", this._handleWizardKeyUp, true);
384+
window.removeEventListener("blur", this._handleWizardKeyDown);
385+
this._keyboardEventHandlerBound = false;
386+
this._isKeyboardControlEnabled = false;
387+
this._spacebarPressed = false;
388+
}
389+
390+
_handleWizardKeyDown(event) {
391+
// Only handle events when keyboard control is active
392+
if (!this._isKeyboardControlEnabled || !this._isWizardActive) {
393+
return;
394+
}
395+
396+
// SPACEBAR: Spin all motors (hold to spin, release to stop)
397+
if (event.code === "Space") {
398+
event.preventDefault();
399+
event.stopPropagation();
400+
// CRITICAL: Check !event.repeat to prevent multiple triggers when holding key
401+
if (!this._spacebarPressed && !event.repeat) {
402+
this._spacebarPressed = true;
403+
this._handleSpacebarPress();
404+
}
405+
return;
406+
}
407+
408+
// NUMBER KEYS 1-8: Toggle individual motor direction
409+
if (event.key >= "1" && event.key <= "8" && !event.repeat) {
410+
event.preventDefault();
411+
event.stopPropagation();
412+
const motorIndex = parseInt(event.key) - 1;
413+
414+
if (motorIndex < this._numberOfMotors) {
415+
this._toggleMotorDirection(motorIndex);
416+
}
417+
return;
418+
}
419+
}
420+
421+
_handleWizardKeyUp(event) {
422+
if (!this._isKeyboardControlEnabled || !this._isWizardActive) {
423+
return;
424+
}
425+
426+
// SPACEBAR RELEASE: Stop motors immediately
427+
if (event.code === "Space") {
428+
event.preventDefault();
429+
event.stopPropagation();
430+
if (this._spacebarPressed) {
431+
this._spacebarPressed = false;
432+
this._handleSpacebarRelease();
433+
}
434+
}
435+
}
436+
437+
_handleSpacebarPress() {
438+
this._motorDriver.spinAllMotors();
439+
}
440+
441+
_handleSpacebarRelease() {
442+
this._motorDriver.stopAllMotorsNow();
443+
}
444+
445+
_toggleMotorDirection(motorIndex) {
446+
const button = this._wizardMotorButtons[motorIndex];
447+
const currentlyReversed = button.hasClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
448+
449+
if (currentlyReversed) {
450+
button.removeClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
451+
this._motorDriver.setEscSpinDirection(motorIndex, DshotCommand.dshotCommands_e.DSHOT_CMD_SPIN_DIRECTION_1);
452+
} else {
453+
button.addClass(EscDshotDirectionComponent.PUSHED_BUTTON_CLASS);
454+
this._motorDriver.setEscSpinDirection(motorIndex, DshotCommand.dshotCommands_e.DSHOT_CMD_SPIN_DIRECTION_2);
455+
}
456+
}
457+
458+
open() {
459+
// Enable global keyboard when dialog is opened
460+
this._enableGlobalKeyboard();
461+
}
462+
288463
close() {
464+
// Disable keyboard handlers first to prevent any new input
465+
this._disableKeyboardControl();
466+
this._disableGlobalKeyboard();
467+
468+
// If wizard is active, deactivate buttons but DON'T clear the flag yet
469+
// This ensures pending motor direction commands complete
470+
if (this._isWizardActive) {
471+
this._deactivateWizardMotorButtons();
472+
}
473+
474+
// Stop motors (this adds stop commands to the queue)
289475
this._motorDriver.stopAllMotorsNow();
476+
477+
// Deactivate motor driver - this tells queue to stop AFTER processing current commands
478+
// This is critical - it allows direction change + save commands to complete
290479
this._motorDriver.deactivate();
480+
481+
// Clear wizard flag after motor driver deactivation
482+
this._isWizardActive = false;
483+
484+
// Reset GUI last
291485
this._resetGui();
292486
}
293487

@@ -363,13 +557,21 @@ class EscDshotDirectionComponent {
363557
this._motorDriver.spinAllMotors();
364558

365559
this._activateWizardMotorButtons(0);
560+
561+
// NEW: Enable keyboard shortcuts when wizard starts spinning
562+
this._isWizardActive = true;
563+
this._enableKeyboardControl();
366564
}
367565

368566
_onStopWizardButtonClicked() {
369567
this._domSpinWizardButton.toggle(true);
370568
this._domSpinningWizard.toggle(false);
371569
this._motorDriver.stopAllMotorsNow();
372570
this._deactivateWizardMotorButtons();
571+
572+
// NEW: Disable keyboard shortcuts when wizard stops
573+
this._disableKeyboardControl();
574+
this._isWizardActive = false;
373575
}
374576

375577
_toggleMainContent(value) {

src/css/tabs/motors.less

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,50 @@
192192
#escDshotDirectionDialog-Content {
193193
flex-grow: 1;
194194
}
195+
196+
// Keyboard shortcuts tooltip
197+
.keyboard-shortcuts-tooltip {
198+
background-color: var(--surface-200);
199+
border-left: 3px solid var(--accent-color);
200+
border-radius: 4px;
201+
padding: 10px 15px;
202+
margin: 10px 0;
203+
text-align: center;
204+
font-size: 0.9em;
205+
display: flex;
206+
align-items: center;
207+
justify-content: center;
208+
flex-wrap: wrap;
209+
gap: 8px;
210+
211+
strong {
212+
color: var(--accent-text);
213+
margin-right: 8px;
214+
}
215+
216+
.shortcut-item {
217+
display: inline-flex;
218+
align-items: center;
219+
gap: 4px;
220+
}
221+
222+
kbd {
223+
background-color: var(--surface-300);
224+
border: 1px solid var(--surface-500);
225+
border-radius: 3px;
226+
padding: 2px 6px;
227+
font-family: monospace;
228+
font-size: 0.85em;
229+
font-weight: bold;
230+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
231+
}
232+
233+
.shortcut-separator {
234+
color: var(--surface-500);
235+
margin: 0 4px;
236+
}
237+
}
238+
195239
#dialog-mixer-reset {
196240
width: 400px;
197241
height: fit-content;

src/js/tabs/motors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,7 @@ motors.initialize = async function (callback) {
13531353

13541354
$("#escDshotDirectionDialog-Open").click(function () {
13551355
$(document).on("keydown", onDocumentKeyPress);
1356+
escDshotDirectionComponent.open();
13561357
domEscDshotDirectionDialog[0].showModal();
13571358
});
13581359

0 commit comments

Comments
 (0)