Skip to content

Commit 494c252

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 494c252

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-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: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ 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);
39+
this._handleWindowBlur = this._handleWindowBlur.bind(this);
2940

3041
this._contentDiv.load("./components/EscDshotDirection/Body.html", () => {
3142
this._initializeDialog();
@@ -285,9 +296,196 @@ class EscDshotDirectionComponent {
285296
}
286297
}
287298

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

@@ -363,13 +561,21 @@ class EscDshotDirectionComponent {
363561
this._motorDriver.spinAllMotors();
364562

365563
this._activateWizardMotorButtons(0);
564+
565+
// NEW: Enable keyboard shortcuts when wizard starts spinning
566+
this._isWizardActive = true;
567+
this._enableKeyboardControl();
366568
}
367569

368570
_onStopWizardButtonClicked() {
369571
this._domSpinWizardButton.toggle(true);
370572
this._domSpinningWizard.toggle(false);
371573
this._motorDriver.stopAllMotorsNow();
372574
this._deactivateWizardMotorButtons();
575+
576+
// NEW: Disable keyboard shortcuts when wizard stops
577+
this._disableKeyboardControl();
578+
this._isWizardActive = false;
373579
}
374580

375581
_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)