Browse Source
refactor(keyboard): Isolate keyboard shortcut logic
refactor(keyboard): Isolate keyboard shortcut logic
This commit extracts the keyboard shortcut handling logic from the main application entry point (`main.js`) into a new, dedicated module (`src/keyboard.js`). Previously, all keydown event listeners were defined directly in `main.js`, mixing UI initialization with user input handling. This refactoring improves code organization and modularity by separating concerns, making the system easier to maintain and debug. Key changes include: - A new `src/keyboard.js` file was created to house all keyboard-related functionality. - The `keydown` event listener and its handler function (`handleKeyDown`) were moved from `main.js` into the new module. - An `initKeyboardShortcuts` function is now exported from `keyboard.js` and called from `main.js` during application startup. - All dependencies required by the shortcut logic (from `dom.js`, `state.js`, `sync.js`, etc.) are now correctly imported into `keyboard.js`. This is a pure refactoring effort and introduces no changes to application behavior.refactor/sync-centralize
2 changed files with 195 additions and 161 deletions
@ -0,0 +1,193 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { |
||||
|
playPauseBtn, |
||||
|
videoPlayer, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
themeToggleBtn, |
||||
|
toggleTracks, |
||||
|
toggleVelocity, |
||||
|
toggleCloseUp, |
||||
|
togglePredictedPos, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
collapsibleMenu, |
||||
|
toggleMenuBtn, |
||||
|
closeMenuBtn, |
||||
|
updatePersistentOverlays, |
||||
|
} from "./dom.js"; |
||||
|
import { updateFrame, resetVisualization } from "./sync.js"; |
||||
|
import { VIDEO_FPS } from "./constants.js"; |
||||
|
import { findRadarFrameIndexForTime } from "./utils.js"; |
||||
|
|
||||
|
function handleKeyDown(event) { |
||||
|
// --- FIX APPLIED HERE ---
|
||||
|
// We only want to block shortcuts if the user is actively typing in a text or number input.
|
||||
|
// This allows shortcuts to work even when other elements, like the timeline slider, are focused.
|
||||
|
const isTextInputFocused = |
||||
|
event.target.tagName === "INPUT" && |
||||
|
(event.target.type === "text" || event.target.type === "number"); |
||||
|
if (isTextInputFocused) { |
||||
|
return; |
||||
|
} |
||||
|
// --- END OF FIX ---
|
||||
|
|
||||
|
const key = event.key; |
||||
|
// We can add any new shortcut keys to this array.
|
||||
|
const recognizedKeys = [ |
||||
|
"ArrowRight", |
||||
|
"ArrowLeft", |
||||
|
"ArrowUp", |
||||
|
"ArrowDown", |
||||
|
" ", |
||||
|
"1", |
||||
|
"2", |
||||
|
"3", |
||||
|
"4", |
||||
|
"t", |
||||
|
"d", |
||||
|
"g", |
||||
|
"r", |
||||
|
"p", |
||||
|
"a", |
||||
|
"s", |
||||
|
"m", |
||||
|
"q", |
||||
|
"c", |
||||
|
]; |
||||
|
|
||||
|
if (!appState.vizData || !recognizedKeys.includes(key)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
event.preventDefault(); |
||||
|
|
||||
|
// --- Spacebar for Play/Pause ---
|
||||
|
if (key === " ") { |
||||
|
playPauseBtn.click(); |
||||
|
} |
||||
|
|
||||
|
// --- Arrow keys for frame-by-frame seeking ---
|
||||
|
if (key === "ArrowRight" || key === "ArrowLeft") { |
||||
|
if (appState.isPlaying) { |
||||
|
playPauseBtn.click(); |
||||
|
} |
||||
|
let newFrame = appState.currentFrame; |
||||
|
if (key === "ArrowRight") { |
||||
|
newFrame = Math.min( |
||||
|
appState.vizData.radarFrames.length - 1, |
||||
|
appState.currentFrame + 1 |
||||
|
); |
||||
|
} else if (key === "ArrowLeft") { |
||||
|
newFrame = Math.max(0, appState.currentFrame - 1); |
||||
|
} |
||||
|
if (newFrame !== appState.currentFrame) { |
||||
|
updateFrame(newFrame, true); |
||||
|
// Manually trigger redraws since the animation loop is paused
|
||||
|
// This is the fix to ensure the radar plot updates on seek.
|
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Arrow keys for video frame-by-frame seeking ---
|
||||
|
if (key === "ArrowUp" || key === "ArrowDown") { |
||||
|
if (appState.isPlaying) { |
||||
|
playPauseBtn.click(); // Pause playback to allow for precise stepping
|
||||
|
} |
||||
|
|
||||
|
const frameDuration = 1 / VIDEO_FPS; |
||||
|
let newVideoTime = videoPlayer.currentTime; |
||||
|
|
||||
|
if (key === "ArrowUp") { |
||||
|
newVideoTime += frameDuration; |
||||
|
} else if (key === "ArrowDown") { |
||||
|
newVideoTime -= frameDuration; |
||||
|
} |
||||
|
|
||||
|
// Clamp the new time to be within the video's bounds
|
||||
|
videoPlayer.currentTime = Math.max( |
||||
|
0, |
||||
|
Math.min(newVideoTime, videoPlayer.duration) |
||||
|
); |
||||
|
|
||||
|
// Find the corresponding radar frame for the new video time
|
||||
|
const newFrameIndex = findRadarFrameIndexForTime( |
||||
|
videoPlayer.currentTime, |
||||
|
appState.vizData |
||||
|
); |
||||
|
|
||||
|
// Update the application state, but don't force another video seek
|
||||
|
updateFrame(newFrameIndex, false); |
||||
|
|
||||
|
// Manually trigger redraws since the animation loop is paused
|
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
||||
|
} |
||||
|
|
||||
|
// --- Number keys for color modes ---
|
||||
|
if (key >= "1" && key <= "4") { |
||||
|
const colorToggles = [ |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
]; |
||||
|
const toggleIndex = parseInt(key) - 1; |
||||
|
if (colorToggles[toggleIndex]) { |
||||
|
colorToggles[toggleIndex].click(); |
||||
|
} |
||||
|
} |
||||
|
if (key === "q") { |
||||
|
themeToggleBtn.click(); |
||||
|
} |
||||
|
if (key === "t") { |
||||
|
toggleTracks.click(); |
||||
|
} |
||||
|
if (key === "d") { |
||||
|
toggleVelocity.click(); |
||||
|
} |
||||
|
if (key === "g") { |
||||
|
toggleCloseUp.click(); |
||||
|
} |
||||
|
if (key === "r") { |
||||
|
resetVisualization(); |
||||
|
} |
||||
|
if (key === "c") { |
||||
|
appState.isRawOnlyMode = !appState.isRawOnlyMode; |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
} |
||||
|
if (key === "p") { |
||||
|
togglePredictedPos.click(); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
if (key === "s") { |
||||
|
toggleSnrColor.click(); |
||||
|
} |
||||
|
if (key === "a") { |
||||
|
toggleDebugOverlay.click(); |
||||
|
toggleDebug2Overlay.click(); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
// The 'a' key is a shortcut to toggle all debug overlays on/off.
|
||||
|
// The `updateDebugOverlay` and `updatePersistentOverlays` functions,
|
||||
|
// which are called by the toggle's 'change' event listener,
|
||||
|
// already handle the logic for showing/hiding the other overlays.
|
||||
|
} |
||||
|
if (key === "m") { |
||||
|
if (collapsibleMenu.classList.contains("-translate-x-full")) { |
||||
|
// If the menu is hidden (closed), trigger a click on the OPEN button.
|
||||
|
toggleMenuBtn.click(); |
||||
|
} else { |
||||
|
// If the menu is not hidden (it's open), trigger a click on the CLOSE button.
|
||||
|
closeMenuBtn.click(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function initKeyboardShortcuts() { |
||||
|
document.addEventListener("keydown", handleKeyDown); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue