diff --git a/croppa/main.py b/croppa/main.py index 8b58ef0..1415162 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -18,6 +18,13 @@ class VideoEditor: SPEED_INCREMENT = 0.2 MIN_PLAYBACK_SPEED = 0.1 MAX_PLAYBACK_SPEED = 10.0 + + # Auto-repeat seeking configuration + INITIAL_REPEAT_DELAY = 0.5 # seconds before auto-repeat starts + REPEAT_RATE_SLOW = 0.15 # seconds between seeks when holding key + REPEAT_RATE_FAST = 0.05 # seconds between seeks after holding for a while + FAST_SEEK_THRESHOLD = 2.0 # seconds before switching to fast repeat + DISPLAY_UPDATE_THROTTLE = 0.033 # minimum time between display updates (30 FPS) # Timeline configuration TIMELINE_HEIGHT = 60 @@ -89,6 +96,14 @@ class VideoEditor: self.current_seek_key = None self.key_first_press_time = 0 self.last_seek_time = 0 + + # Auto-repeat seeking state + self.auto_repeat_active = False + self.auto_repeat_key = None + self.auto_repeat_direction = 0 + self.auto_repeat_shift = False + self.auto_repeat_ctrl = False + self.last_display_update = 0 # Crop settings self.crop_rect = None # (x, y, width, height) @@ -499,6 +514,74 @@ class VideoEditor: self.seek_video(frames) + def start_auto_repeat_seek(self, key: int, direction: int, shift_pressed: bool, ctrl_pressed: bool): + """Start auto-repeat seeking for a held key""" + if self.is_image_mode: + return + + # If the same key is already being auto-repeated, don't restart + if (self.auto_repeat_active and + self.auto_repeat_key == key and + self.auto_repeat_direction == direction and + self.auto_repeat_shift == shift_pressed and + self.auto_repeat_ctrl == ctrl_pressed): + return + + self.auto_repeat_active = True + self.auto_repeat_key = key + self.auto_repeat_direction = direction + self.auto_repeat_shift = shift_pressed + self.auto_repeat_ctrl = ctrl_pressed + self.key_first_press_time = time.time() + self.last_seek_time = 0 + + # Do initial seek immediately + self.seek_video_with_modifier(direction, shift_pressed, ctrl_pressed) + + def stop_auto_repeat_seek(self): + """Stop auto-repeat seeking""" + self.auto_repeat_active = False + self.auto_repeat_key = None + self.auto_repeat_direction = 0 + self.auto_repeat_shift = False + self.auto_repeat_ctrl = False + + def update_auto_repeat_seek(self): + """Update auto-repeat seeking if active""" + if not self.auto_repeat_active or self.is_image_mode: + return + + current_time = time.time() + time_since_first_press = current_time - self.key_first_press_time + + # Determine repeat rate based on how long the key has been held + if time_since_first_press < self.INITIAL_REPEAT_DELAY: + # Still in initial delay period + return + elif time_since_first_press < self.FAST_SEEK_THRESHOLD: + # Slow repeat rate + repeat_rate = self.REPEAT_RATE_SLOW + else: + # Fast repeat rate + repeat_rate = self.REPEAT_RATE_FAST + + # Check if enough time has passed for the next seek + if current_time - self.last_seek_time >= repeat_rate: + self.seek_video_with_modifier( + self.auto_repeat_direction, + self.auto_repeat_shift, + self.auto_repeat_ctrl + ) + self.last_seek_time = current_time + + def should_update_display(self) -> bool: + """Check if display should be updated based on throttling""" + current_time = time.time() + if current_time - self.last_display_update >= self.DISPLAY_UPDATE_THROTTLE: + self.last_display_update = current_time + return True + return False + def seek_to_frame(self, frame_number: int): """Seek to specific frame""" self.current_frame = max(0, min(frame_number, self.total_frames - 1)) @@ -1723,11 +1806,24 @@ class VideoEditor: self.load_current_frame() while True: - # Only update display if needed - self.display_current_frame() + # Update auto-repeat seeking if active + self.update_auto_repeat_seek() + + # Only update display if needed and throttled + if self.should_update_display(): + self.display_current_frame() delay = self.calculate_frame_delay() if self.is_playing else 30 key = cv2.waitKey(delay) & 0xFF + + # Handle auto-repeat timeout - only stop if no key is pressed for a longer period + if key == 255 and self.auto_repeat_active: # 255 means no key pressed + # Check if enough time has passed since last key press + if time.time() - self.last_seek_time > 0.3: # 300ms timeout (increased) + self.stop_auto_repeat_seek() + elif key != 255 and self.auto_repeat_active: + # A key is pressed, update the last seek time to prevent timeout + self.last_seek_time = time.time() # Get modifier key states window_title = "Image Editor" if self.is_image_mode else "Video Editor" @@ -1736,10 +1832,12 @@ class VideoEditor: # We'll handle this through special key combinations if key == ord("q") or key == 27: # ESC + self.stop_auto_repeat_seek() break elif key == ord(" "): # Don't allow play/pause for images if not self.is_image_mode: + self.stop_auto_repeat_seek() # Stop seeking when toggling play/pause self.is_playing = not self.is_playing self.save_state() elif key == ord("a") or key == ord("A"): @@ -1747,27 +1845,25 @@ class VideoEditor: if not self.is_image_mode: # Check if it's uppercase A (Shift+A) if key == ord("A"): - self.seek_video_with_modifier( - -1, True, False - ) # Shift+A: -10 frames + self.start_auto_repeat_seek(key, -1, True, False) # Shift+A: -10 frames else: - self.seek_video_with_modifier(-1, False, False) # A: -1 frame + self.start_auto_repeat_seek(key, -1, False, False) # A: -1 frame elif key == ord("d") or key == ord("D"): # Seeking only for videos if not self.is_image_mode: # Check if it's uppercase D (Shift+D) if key == ord("D"): - self.seek_video_with_modifier(1, True, False) # Shift+D: +10 frames + self.start_auto_repeat_seek(key, 1, True, False) # Shift+D: +10 frames else: - self.seek_video_with_modifier(1, False, False) # D: +1 frame + self.start_auto_repeat_seek(key, 1, False, False) # D: +1 frame elif key == 1: # Ctrl+A # Seeking only for videos if not self.is_image_mode: - self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames + self.start_auto_repeat_seek(key, -1, False, True) # Ctrl+A: -60 frames elif key == 4: # Ctrl+D # Seeking only for videos if not self.is_image_mode: - self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames + self.start_auto_repeat_seek(key, 1, False, True) # Ctrl+D: +60 frames elif key == ord("-") or key == ord("_"): self.rotate_clockwise() print(f"Rotated to {self.rotation_angle}°")