Skip to content

Commit 81ffb60

Browse files
committed
Fixing keyboard shortcuts for pausing
1 parent 80210f0 commit 81ffb60

4 files changed

Lines changed: 130 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7474
- **Hold to scrub**: Holding down arrow keys continuously steps through frames at ~15 FPS
7575
- Slider updates in real-time to show current position during arrow key scrubbing
7676
- Automatically pauses when using arrows on playing video
77-
- **Pause/Play**: Spacebar to toggle pause/play for all cameras
77+
- **Pause/Play**: Spacebar to toggle pause/play
78+
- Press Spacebar alone to pause/play all cameras
79+
- Hold 1, 2, 3, or 4 while pressing Spacebar to pause/play specific quadrant only
80+
- Works with both number row keys and numpad keys
7881
- **Recording**: R key to start/stop recording on all live cameras
7982
- **Screenshot**: S key to take screenshots from all cameras
8083
- Cross-platform: Automatically uses CMD on macOS, CTRL on Windows/Linux
@@ -149,11 +152,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
149152

150153
### Fixed
151154

155+
- **Arrow Key Frame Scrubbing**: Fixed left/right arrow keys not working for frame stepping
156+
- Added missing main loop logic to check `holding_frame_back` and `holding_frame_forward` flags
157+
- Main loop now calls `step_frame_back()` and `step_frame_forward()` at ~15 FPS when keys are held
158+
- Arrow keys now properly step through frames and update sliders for both video files and live camera buffers
159+
- **Spacebar Pause/Play Toggling**: Changed to trigger on key release instead of key press
160+
- Pause/play action now executes when spacebar is RELEASED, not when pressed
161+
- Prevents intermediate toggles when holding spacebar and pressing different number keys
162+
- Allows user to hold spacebar, change which number key is pressed, then release to toggle the final target
163+
- Example: Hold Space, press 1, change mind and press 2 instead, release Space → only toggles quadrant 2
164+
- Eliminates unwanted "toggle all cameras" when user is about to specify a quadrant
152165
- **Keyboard Shortcut Key Constants**: Fixed `AttributeError` for non-existent DearPyGUI key constants
153166
- Removed references to `mvKey_Equal`, `mvKey_NumPadAdd`, and `mvKey_NumPadSubtract` (don't exist in DearPyGUI)
154167
- Now correctly uses `mvKey_Plus`, `mvKey_Minus`, `mvKey_Add`, and `mvKey_Subtract`
155168
- Fixed zoom shortcuts to use Plus/Minus keys that actually exist
156169
- All keyboard shortcuts now work without errors
170+
- **Numpad Support for Quadrant Selection**: Fixed numpad key support for quadrant-specific pause/play
171+
- Fixed `AttributeError` for incorrect constant `mvKey_Numpad1` (should be `mvKey_NumPad1` with capital P)
172+
- Spacebar + NumPad 1-4 now works correctly in addition to number row 1-4
173+
- Ensures consistent behavior across different keyboard layouts
174+
- **Right Command Key Support (macOS)**: Fixed modifier detection to include right Command key
175+
- Previously only checked left Command key, now checks both `mvKey_LWin` and `mvKey_RWin`
176+
- Ensures keyboard shortcuts work regardless of which Command key is pressed
157177
- **Keyboard Modifier Keys**: Fixed `AttributeError` for `dpg.mvKey_Control`
158178
- Now correctly uses `dpg.mvKey_LControl` and `dpg.mvKey_RControl`
159179
- Also checks left/right variants for Shift and Alt keys

gui/layout.py

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ def on_key_press_handler(sender, app_data):
11231123
# Check for modifier keys
11241124
# Note: DearPyGUI uses different constants for modifiers
11251125
modifier_pressed = {
1126-
'ctrl': dpg.is_key_down(dpg.mvKey_LControl) or dpg.is_key_down(dpg.mvKey_RControl) or (is_mac and dpg.is_key_down(dpg.mvKey_LWin)),
1126+
'ctrl': dpg.is_key_down(dpg.mvKey_LControl) or dpg.is_key_down(dpg.mvKey_RControl) or (is_mac and (dpg.is_key_down(dpg.mvKey_LWin) or dpg.is_key_down(dpg.mvKey_RWin))),
11271127
'shift': dpg.is_key_down(dpg.mvKey_LShift) or dpg.is_key_down(dpg.mvKey_RShift),
11281128
'alt': dpg.is_key_down(dpg.mvKey_LAlt) or dpg.is_key_down(dpg.mvKey_RAlt)
11291129
}
@@ -1135,6 +1135,10 @@ def on_key_press_handler(sender, app_data):
11351135
settings = get_settings()
11361136
shortcuts = settings.keyboard_shortcuts
11371137

1138+
# Debug logging for spacebar and number keys
1139+
if key == dpg.mvKey_Spacebar or key in [dpg.mvKey_1, dpg.mvKey_2, dpg.mvKey_3, dpg.mvKey_4]:
1140+
logger.info(f"KEY PRESS: key={key}, spacebar_held={state.spacebar_held}, current_target={state.spacebar_target_quadrant}")
1141+
11381142
# For arrow keys, just set flags - the main loop will handle continuous movement
11391143
if _check_shortcut_match(key, modifier_pressed, shortcuts.get('frame_back')):
11401144
state.holding_frame_back = True
@@ -1178,59 +1182,36 @@ def on_key_press_handler(sender, app_data):
11781182
dpg.set_value(zoom_label_tag, f"{camera.zoom_level:.2f}x")
11791183

11801184
elif _check_shortcut_match(key, modifier_pressed, shortcuts.get('pause_play')):
1181-
# Check if a number key (1-4) is also pressed for quadrant-specific pause
1185+
# Track that spacebar is held and update target whenever a number key changes
1186+
state.spacebar_held = True
1187+
1188+
# Check which quadrant is targeted (updates as number keys are pressed)
11821189
target_quadrant = None
1183-
if dpg.is_key_down(dpg.mvKey_1):
1190+
if dpg.is_key_down(dpg.mvKey_1) or dpg.is_key_down(dpg.mvKey_NumPad1):
11841191
target_quadrant = 0
1185-
elif dpg.is_key_down(dpg.mvKey_2):
1192+
elif dpg.is_key_down(dpg.mvKey_2) or dpg.is_key_down(dpg.mvKey_NumPad2):
11861193
target_quadrant = 1
1187-
elif dpg.is_key_down(dpg.mvKey_3):
1194+
elif dpg.is_key_down(dpg.mvKey_3) or dpg.is_key_down(dpg.mvKey_NumPad3):
11881195
target_quadrant = 2
1189-
elif dpg.is_key_down(dpg.mvKey_4):
1196+
elif dpg.is_key_down(dpg.mvKey_4) or dpg.is_key_down(dpg.mvKey_NumPad4):
11901197
target_quadrant = 3
11911198

1192-
if target_quadrant is not None:
1193-
logger.info(f"Keyboard: Toggle pause/play for quadrant {target_quadrant + 1}")
1194-
else:
1195-
logger.info("Keyboard: Toggle pause/play for all cameras")
1196-
1197-
for camera in state.cameras:
1198-
# Find which quadrant this camera is in
1199-
cam_quadrant = None
1200-
for cam_id, quad_pos in state.camera_positions.items():
1201-
if id(camera) == cam_id:
1202-
cam_quadrant = quad_pos
1203-
break
1204-
1205-
# Skip this camera if we're targeting a specific quadrant and this isn't it
1206-
if target_quadrant is not None and cam_quadrant != target_quadrant:
1207-
continue
1208-
1209-
if camera.is_video_file:
1210-
camera.paused = not camera.paused
1211-
# Update button labels
1212-
if cam_quadrant is not None:
1213-
pause_btn_tag = f"pause_btn_{cam_quadrant}"
1214-
if dpg.does_item_exist(pause_btn_tag):
1215-
dpg.configure_item(pause_btn_tag, label="Play" if camera.paused else "Pause")
1216-
elif not camera.is_video_file:
1217-
# For live cameras, use toggle_live_pause() like the button does
1218-
is_paused = camera.toggle_live_pause()
1219-
if cam_quadrant is not None:
1220-
pause_btn_tag = f"pause_btn_{cam_quadrant}"
1221-
if dpg.does_item_exist(pause_btn_tag):
1222-
dpg.configure_item(pause_btn_tag, label="Resume" if is_paused else "Pause")
1223-
1224-
# Show/hide slider and step buttons like the button callback does
1225-
slider_tag = f"live_slider_{cam_quadrant}"
1226-
step_back_tag = f"step_back_btn_{cam_quadrant}"
1227-
step_fwd_tag = f"step_fwd_btn_{cam_quadrant}"
1228-
if dpg.does_item_exist(slider_tag):
1229-
dpg.configure_item(slider_tag, show=is_paused)
1230-
if dpg.does_item_exist(step_back_tag):
1231-
dpg.configure_item(step_back_tag, show=is_paused)
1232-
if dpg.does_item_exist(step_fwd_tag):
1233-
dpg.configure_item(step_fwd_tag, show=is_paused)
1199+
state.spacebar_target_quadrant = target_quadrant
1200+
logger.info(f"Spacebar PRESSED: target updated to {target_quadrant}")
1201+
1202+
# Also update target when number keys are pressed while spacebar is held
1203+
elif state.spacebar_held and (key in [dpg.mvKey_1, dpg.mvKey_2, dpg.mvKey_3, dpg.mvKey_4,
1204+
dpg.mvKey_NumPad1, dpg.mvKey_NumPad2, dpg.mvKey_NumPad3, dpg.mvKey_NumPad4]):
1205+
logger.info(f"Number key detected: key={key}, mvKey_2={dpg.mvKey_2}, match={key == dpg.mvKey_2}")
1206+
if key == dpg.mvKey_1 or key == dpg.mvKey_NumPad1:
1207+
state.spacebar_target_quadrant = 0
1208+
elif key == dpg.mvKey_2 or key == dpg.mvKey_NumPad2:
1209+
state.spacebar_target_quadrant = 1
1210+
elif key == dpg.mvKey_3 or key == dpg.mvKey_NumPad3:
1211+
state.spacebar_target_quadrant = 2
1212+
elif key == dpg.mvKey_4 or key == dpg.mvKey_NumPad4:
1213+
state.spacebar_target_quadrant = 3
1214+
logger.info(f"Number key PRESSED while spacebar held: target updated to {state.spacebar_target_quadrant}")
12341215

12351216
elif _check_shortcut_match(key, modifier_pressed, shortcuts.get('record')):
12361217
logger.info("Keyboard: Toggle recording")
@@ -1332,13 +1313,17 @@ def on_key_release_handler(sender, app_data):
13321313

13331314
# Check for modifier keys
13341315
modifier_pressed = {
1335-
'ctrl': dpg.is_key_down(dpg.mvKey_LControl) or dpg.is_key_down(dpg.mvKey_RControl) or (is_mac and dpg.is_key_down(dpg.mvKey_LWin)),
1316+
'ctrl': dpg.is_key_down(dpg.mvKey_LControl) or dpg.is_key_down(dpg.mvKey_RControl) or (is_mac and (dpg.is_key_down(dpg.mvKey_LWin) or dpg.is_key_down(dpg.mvKey_RWin))),
13361317
'shift': dpg.is_key_down(dpg.mvKey_LShift) or dpg.is_key_down(dpg.mvKey_RShift),
13371318
'alt': dpg.is_key_down(dpg.mvKey_LAlt) or dpg.is_key_down(dpg.mvKey_RAlt)
13381319
}
13391320

13401321
key = app_data
13411322

1323+
# Debug logging
1324+
if key == dpg.mvKey_Spacebar:
1325+
logger.info(f"KEY RELEASE: key={key}, spacebar_held={state.spacebar_held}, target={state.spacebar_target_quadrant}")
1326+
13421327
# Load shortcuts from settings
13431328
from utils.settings import get_settings
13441329
settings = get_settings()
@@ -1350,6 +1335,64 @@ def on_key_release_handler(sender, app_data):
13501335
elif _check_shortcut_match(key, modifier_pressed, shortcuts.get('frame_forward')):
13511336
state.holding_frame_forward = False
13521337

1338+
# Execute pause/play when spacebar is released (not when pressed)
1339+
# Use the target that was set during the press phase
1340+
if _check_shortcut_match(key, modifier_pressed, shortcuts.get('pause_play')):
1341+
# Only execute if spacebar was actually held (prevents issues with key repeat)
1342+
if not state.spacebar_held:
1343+
return
1344+
1345+
target_quadrant = state.spacebar_target_quadrant
1346+
1347+
logger.info(f"Spacebar RELEASED: using target_quadrant={target_quadrant}")
1348+
1349+
if target_quadrant is not None:
1350+
logger.info(f"Keyboard: Toggle pause/play for quadrant {target_quadrant + 1}")
1351+
else:
1352+
logger.info("Keyboard: Toggle pause/play for all cameras")
1353+
1354+
# Reset state for next press
1355+
state.spacebar_held = False
1356+
state.spacebar_target_quadrant = None
1357+
1358+
for camera in state.cameras:
1359+
# Find which quadrant this camera is in
1360+
cam_quadrant = None
1361+
for cam_id, quad_pos in state.camera_positions.items():
1362+
if id(camera) == cam_id:
1363+
cam_quadrant = quad_pos
1364+
break
1365+
1366+
# Skip this camera if we're targeting a specific quadrant and this isn't it
1367+
if target_quadrant is not None and cam_quadrant != target_quadrant:
1368+
continue
1369+
1370+
if camera.is_video_file:
1371+
camera.paused = not camera.paused
1372+
# Update button labels
1373+
if cam_quadrant is not None:
1374+
pause_btn_tag = f"pause_btn_{cam_quadrant}"
1375+
if dpg.does_item_exist(pause_btn_tag):
1376+
dpg.configure_item(pause_btn_tag, label="Play" if camera.paused else "Pause")
1377+
elif not camera.is_video_file:
1378+
# For live cameras, use toggle_live_pause() like the button does
1379+
is_paused = camera.toggle_live_pause()
1380+
if cam_quadrant is not None:
1381+
pause_btn_tag = f"pause_btn_{cam_quadrant}"
1382+
if dpg.does_item_exist(pause_btn_tag):
1383+
dpg.configure_item(pause_btn_tag, label="Resume" if is_paused else "Pause")
1384+
1385+
# Show/hide slider and step buttons like the button callback does
1386+
slider_tag = f"live_slider_{cam_quadrant}"
1387+
step_back_tag = f"step_back_btn_{cam_quadrant}"
1388+
step_fwd_tag = f"step_fwd_btn_{cam_quadrant}"
1389+
if dpg.does_item_exist(slider_tag):
1390+
dpg.configure_item(slider_tag, show=is_paused)
1391+
if dpg.does_item_exist(step_back_tag):
1392+
dpg.configure_item(step_back_tag, show=is_paused)
1393+
if dpg.does_item_exist(step_fwd_tag):
1394+
dpg.configure_item(step_fwd_tag, show=is_paused)
1395+
13531396

13541397
def register_global_mouse_handlers():
13551398
"""Register global mouse and keyboard handlers"""

gui/state.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@
3131
holding_frame_back = False
3232
holding_frame_forward = False
3333
last_frame_step_time = 0.0 # Track when we last stepped a frame
34+
35+
# Track spacebar state for pause/play
36+
spacebar_held = False
37+
spacebar_target_quadrant = None # Which quadrant to toggle when spacebar is released

main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@ def on_startup_complete(athlete_name, bike_name):
172172
while dpg.is_dearpygui_running():
173173
# Only update camera textures if main UI is showing
174174
if show_main_ui:
175+
# Handle continuous frame scrubbing with arrow keys
176+
current_time = time.time()
177+
if state.holding_frame_back or state.holding_frame_forward:
178+
# Rate limit to ~15 FPS for smooth scrubbing
179+
if current_time - state.last_frame_step_time >= 1.0/15.0:
180+
if state.holding_frame_back:
181+
from gui.layout import step_frame_back
182+
step_frame_back()
183+
elif state.holding_frame_forward:
184+
from gui.layout import step_frame_forward
185+
step_frame_forward()
186+
state.last_frame_step_time = current_time
187+
175188
# Update camera textures
176189
for camera in state.cameras:
177190
camera.update_texture()
@@ -189,7 +202,6 @@ def on_startup_complete(athlete_name, bike_name):
189202

190203
# Resize quadrants and images periodically (check every 0.1 seconds)
191204
# But skip if we're currently dragging a divider
192-
current_time = time.time()
193205
if current_time - last_resize_time > 0.1:
194206
if not state.dragging_vertical_divider and not state.dragging_horizontal_divider:
195207
update_quadrant_sizes()

0 commit comments

Comments
 (0)