feat(main.py): implement auto-repeat seeking for video editor with configurable delays and rates
This commit is contained in:
		
							
								
								
									
										116
									
								
								croppa/main.py
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								croppa/main.py
									
									
									
									
									
								
							@@ -19,6 +19,13 @@ class VideoEditor:
 | 
			
		||||
    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
 | 
			
		||||
    TIMELINE_MARGIN = 20
 | 
			
		||||
@@ -90,6 +97,14 @@ class VideoEditor:
 | 
			
		||||
        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)
 | 
			
		||||
        self.crop_selecting = False
 | 
			
		||||
@@ -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,12 +1806,25 @@ 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"
 | 
			
		||||
            modifiers = cv2.getWindowProperty(window_title, cv2.WND_PROP_AUTOSIZE)
 | 
			
		||||
@@ -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}°")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user