diff --git a/croppa/main.py b/croppa/main.py index 1f39d18..6b65f31 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1346,7 +1346,7 @@ class VideoEditor: return (ix, iy, iw, ih), iz - def set_crop_zoom_keyframe_at_current_frame(self): + def _update_crop_zoom_keyframe_for_current_frame(self): """Create or update crop/zoom keyframe at current frame from current crop/zoom settings.""" frame_index = getattr(self, 'current_frame', 0) base_rect = self._get_base_crop_rect_for_frame(frame_index) @@ -1354,10 +1354,6 @@ class VideoEditor: 'crop_rect': base_rect, 'zoom_factor': self.zoom_factor, } - self.crop_rect = base_rect - self.clear_transformation_cache() - self.display_needs_update = True - self.show_feedback_message("Crop/zoom keyframe set") def delete_crop_zoom_keyframe_at_current_frame(self): """Delete crop/zoom keyframe at current frame if it exists.""" @@ -3151,22 +3147,118 @@ class VideoEditor: x, y, w, h = self.template_selection_rect cv2.rectangle(canvas, (x, y), (x + w, y + h), (255, 0, 255), 2) # Magenta for template selection - # Help overlay for keybindings + # Help overlay for keybindings (top-left, multi-column, all keybinds) if self.show_help_overlay: help_lines = [ + "Playback / Seek:", + " SPACE - play/pause (video only)", + " a / d - step -1 / +1 frame", + " A / D - auto-repeat seek -10 / +10 frames", + " Ctrl+A / Ctrl+D - auto-repeat seek -60 / +60 frames", + " , / . - prev / next cut marker", + " 1 / 2 - set cut start / end", + " Shift+1 / Shift+2 - jump to cut start / end", + " N / n - previous / next video", + "", + "Speed / Seek Multiplier:", + " w / s - speed up / slow down", + " Q / Y - increase / decrease seek multiplier", + "", + "View / Rotation / Screenshots:", + " f - toggle fullscreen", + " - / _ - rotate 90 degrees", + " S - save screenshot", + "", + "Brightness / Contrast:", + " e / E - increase / decrease brightness", + " r / R - increase / decrease contrast", + "", + "Crop / Zoom (mouse):", + " Shift+LMB drag - draw new crop", + " LMB drag inside crop - expand edge toward drag", + " LMB+RMB drag inside crop - contract opposite edge", + " scroll - zoom in/out", + " Shift+scroll - uniform crop expand/contract", + "", + "Crop / Zoom (keyboard):", + " J/K/L/H - expand up/down/left/right", + " k/j/h/l - contract from bottom/top/right/left", + " u - undo last crop", + " c - clear crop/zoom and keyframes", + " C - complete reset", + "", "Crop/Zoom Keyframes:", - " y - set/update keyframe at current frame", - " U - delete keyframe at current frame", - " i - clone previous keyframe to current frame", - " [ / ] - prev/next keyframe", - " ? - toggle this help", + " automatic per frame on crop/zoom change", + " 3 / 4 - prev / next crop/zoom keyframe", + " 5 - clone previous keyframe to current frame", + " 6 - delete keyframe at current frame", + "", + "Motion Tracking / Features / Templates:", + " v / V - toggle motion tracking / clear points", + " F - toggle feature tracking", + " T - extract features from visible area", + " g / G - toggle auto tracking / clear feature data", + " Z - switch detector type", + " z - toggle selective feature extraction mode", + " o - toggle optical flow", + " m / M - clear templates / toggle full-frame template matching", + " ; / : - prev / next template marker", + "", + "Frame Difference / Interesting Region:", + " ; - next interesting point / cancel search", + " ' - toggle interesting region selection", + " 9 / 0 - decrease / increase diff threshold", + " ) / = - threshold -10 / +10", + " 7 / 8 - decrease / increase diff gap", + " / / ( - diff gap -60 / +60", + "", + "Rendering / Misc:", + " b - render to new _edited_ file", + " ENTER - overwrite existing _edited_ file (render)", + " x - cancel render", + " p - toggle project view", + " t - toggle marker looping", + " ? - toggle this help overlay", ] - base_y = self.window_height - self.TIMELINE_HEIGHT - 10 - for idx, text in enumerate(help_lines): - y_pos = base_y - 18 * (len(help_lines) - idx) - cv2.putText(canvas, text, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX, self.FONT_SCALE_SMALL, (255, 255, 255), 2) - cv2.putText(canvas, text, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX, self.FONT_SCALE_SMALL, (0, 0, 0), 1) - + font_scale = 0.7 + font = cv2.FONT_HERSHEY_SIMPLEX + font_thick_white = 2 + font_thick_black = 0 + line_height = int(42 * font_scale) + margin_x = 30 + margin_y = 80 + + # Two columns: split by line count + total_lines = len(help_lines) + mid = (total_lines + 1) // 2 + columns = [ + (margin_x, help_lines[:mid]), + (margin_x + 920, help_lines[mid:]), + ] + + # First, compute the background rectangle bounds for both columns to cover all lines + max_lines_in_col = max(len(col[1]) for col in columns) + col_width = 890 # Leave some padding, adjust if needed + col_height = max_lines_in_col * line_height + 20 + bg_rect_margin = 12 + + # Draw black background rectangles for both columns + for idx, (col_x, lines) in enumerate(columns): + if len(lines) == 0: + continue + x1 = col_x - bg_rect_margin + y1 = margin_y - bg_rect_margin + x2 = col_x + col_width + bg_rect_margin + y2 = margin_y + len(lines) * line_height + bg_rect_margin + cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 0, 0), thickness=-1) + + # Draw text in red on top of the black backgrounds + for col_x, lines in columns: + for idx, text in enumerate(lines): + x_pos = col_x + y_pos = margin_y + idx * line_height + cv2.putText(canvas, text, (x_pos, y_pos), font, font_scale, (0, 0, 255), font_thick_white, lineType=cv2.LINE_AA) + cv2.putText(canvas, text, (x_pos, y_pos), font, font_scale, (0, 0, 0), font_thick_black, lineType=cv2.LINE_AA) # Draw previous and next tracking points with motion path visualization if not self.is_image_mode and self.tracking_points: prev_result = self._get_previous_tracking_point() @@ -3473,6 +3565,7 @@ class VideoEditor: self.crop_border_drag_start_rect = None self.crop_drag_edge = None self.crop_drag_mode = None + self._update_crop_zoom_keyframe_for_current_frame() self.save_state() # Handle crop selection (Shift + click and drag) @@ -3496,6 +3589,7 @@ class VideoEditor: print(f"DEBUG: Crop end screen_rect={self.crop_preview_rect}") # Convert screen coordinates to video coordinates self.set_crop_from_screen_coords(self.crop_preview_rect) + self._update_crop_zoom_keyframe_for_current_frame() self.crop_selecting = False self.crop_start_point = None self.crop_preview_rect = None @@ -3768,6 +3862,7 @@ class VideoEditor: self.zoom_factor = min(self.MAX_ZOOM, self.zoom_factor + self.ZOOM_INCREMENT) else: # Scroll down -> zoom out self.zoom_factor = max(self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT) + self._update_crop_zoom_keyframe_for_current_frame() self.clear_transformation_cache() def _set_crop_from_rotated_rect(self, rotated_rect): @@ -3970,6 +4065,9 @@ class VideoEditor: default_size, default_size ) + self._update_crop_zoom_keyframe_for_current_frame() + self.clear_transformation_cache() + self.save_state() return x, y, w, h = self.crop_rect @@ -4020,6 +4118,7 @@ class VideoEditor: new_w = w - amount self.crop_rect = (new_x, y, new_w, h) + self._update_crop_zoom_keyframe_for_current_frame() self.clear_transformation_cache() self.save_state() # Save state when crop is adjusted @@ -4040,6 +4139,9 @@ class VideoEditor: default_size, default_size ) + self._update_crop_zoom_keyframe_for_current_frame() + self.clear_transformation_cache() + self.save_state() return x, y, w, h = self.crop_rect @@ -4059,6 +4161,7 @@ class VideoEditor: new_h = max(10, h - contract_amount * 2) self.crop_rect = (new_x, new_y, new_w, new_h) + self._update_crop_zoom_keyframe_for_current_frame() self.clear_transformation_cache() self.save_state() @@ -5035,17 +5138,15 @@ class VideoEditor: self.show_help_overlay = not self.show_help_overlay self.display_needs_update = True - # Crop/zoom keyframe controls - elif key == ord("y"): - self.set_crop_zoom_keyframe_at_current_frame() - elif key == ord("U"): - self.delete_crop_zoom_keyframe_at_current_frame() - elif key == ord("i"): - self.clone_previous_crop_zoom_keyframe_to_current() - elif key == ord("["): + # Crop/zoom keyframe controls (automatic per frame; navigation + clone/delete) + elif key == ord("3"): self.jump_to_previous_crop_zoom_keyframe() - elif key == ord("]"): + elif key == ord("4"): self.jump_to_next_crop_zoom_keyframe() + elif key == ord("5"): + self.clone_previous_crop_zoom_keyframe_to_current() + elif key == ord("6"): + self.delete_crop_zoom_keyframe_at_current_frame() # Individual direction controls using shift combinations we can detect elif key == ord("J"): # Shift+i - expand up