feat: Enhance ShortsEditorGUI with media bin for drag-and-drop functionality and timeline clip management

This commit is contained in:
klop51 2025-08-12 19:19:24 +02:00
parent d589700d7a
commit c9fc7c8d80

View File

@ -160,6 +160,10 @@ class ShortsEditorGUI:
# Timeline interaction state for drag-and-drop editing
# These variables track the current state of user interactions with timeline elements
self.dragging_clip = None # Clip currently being dragged by user
self.dragging_media = None # Media file being dragged from bin to timeline
self.resizing_clip = None # Clip being resized
self.moving_clip = None # Clip being moved
self.selected_timeline_clip = None # Index of selected timeline clip
self.drag_start_x = None # Mouse X position when drag started
self.drag_start_time = None # Original time position of dragged clip
self.drag_offset = 0 # Offset from clip start to mouse position
@ -181,6 +185,9 @@ class ShortsEditorGUI:
self.clip_thumbnails = {} # Dictionary storing thumbnail images by clip ID
self.audio_waveforms = {} # Dictionary storing waveform data by clip ID
# Timeline clips storage for media bin drops
self.timeline_clips = [] # List of all clips on the timeline
# Track control widget references for dynamic updates
# This allows us to update track control buttons when states change
self.track_widgets = {} # Dictionary storing widget references by track ID
@ -192,6 +199,7 @@ class ShortsEditorGUI:
'bg_primary': '#1a1a1a', # Main background - darkest for maximum contrast
'bg_secondary': '#2d2d2d', # Secondary panels - slightly lighter
'bg_tertiary': '#3d3d3d', # Buttons and controls - interactive elements
'bg_hover': '#404040', # Hover state - subtle highlight for interaction
'text_primary': '#ffffff', # Main text - high contrast for readability
'text_secondary': '#b8b8b8', # Secondary text - lower contrast for hierarchy
'accent_blue': '#007acc', # Primary actions - professional blue
@ -311,14 +319,22 @@ class ShortsEditorGUI:
main_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
main_frame.rowconfigure(0, weight=1) # Single row takes all vertical space
main_frame.columnconfigure(0, weight=2) # Left column gets 2/3 of width
main_frame.columnconfigure(1, weight=1) # Right column gets 1/3 of width
main_frame.columnconfigure(0, weight=0) # Media bin column (fixed width)
main_frame.columnconfigure(1, weight=2) # Left column gets 2/3 of remaining width
main_frame.columnconfigure(2, weight=1) # Right column gets 1/3 of width
# Left panel - Video preview and timeline workspace
# Media Bin - File library for drag and drop editing
# Container for video and audio files that can be dragged to timeline tracks
media_bin_frame = tk.Frame(main_frame, bg=self.colors['bg_tertiary'], width=200)
media_bin_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
media_bin_frame.pack_propagate(False) # Maintain fixed width
self.create_media_bin(media_bin_frame)
# Middle panel - Video preview and timeline workspace
# This is the main editing area where users see their video and timeline
# Organized vertically with video preview on top and timeline below
player_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
player_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
player_frame.grid(row=0, column=1, sticky="nsew", padx=(0, 5))
player_frame.rowconfigure(0, weight=1) # Video preview area (expandable)
player_frame.rowconfigure(1, weight=0) # Timeline area (fixed height)
player_frame.columnconfigure(0, weight=1) # Full width utilization
@ -463,11 +479,18 @@ class ShortsEditorGUI:
external_scrollbar_frame.pack(side="bottom", fill="x", padx=5, pady=2)
external_scrollbar_frame.pack_propagate(False)
# Professional tips label
tips_label = tk.Label(external_scrollbar_frame,
text="💡 CTRL+Click: Select clips • Delete/Backspace: Delete selected • Click: Move playhead",
bg=self.colors['bg_primary'], fg=self.colors['text_secondary'],
font=('Segoe UI', 8))
tips_label.pack(side="left", padx=5, pady=2)
# Horizontal scrollbar outside timeline but controlling it
self.external_h_scrollbar = ttk.Scrollbar(external_scrollbar_frame, orient="horizontal",
command=self.timeline_canvas.xview)
self.timeline_canvas.configure(xscrollcommand=self.external_h_scrollbar.set)
self.external_h_scrollbar.pack(fill="x", expand=True)
self.external_h_scrollbar.pack(side="right", fill="x", expand=True)
# Bind professional timeline events
self.timeline_canvas.bind("<Button-1>", self.timeline_click)
@ -476,6 +499,11 @@ class ShortsEditorGUI:
self.timeline_canvas.bind("<Button-3>", self.on_timeline_right_click)
self.timeline_canvas.bind("<Double-Button-1>", self.on_timeline_double_click)
# Keyboard shortcuts for timeline
self.timeline_canvas.bind("<KeyPress-Delete>", self.delete_selected_clip)
self.timeline_canvas.bind("<KeyPress-BackSpace>", self.delete_selected_clip)
self.timeline_canvas.focus_set() # Allow timeline to receive keyboard events
# Create track controls
self.create_track_controls()
@ -487,7 +515,7 @@ class ShortsEditorGUI:
# Right panel - Tools and effects
tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
tools_frame.grid(row=0, column=1, sticky="nsew", padx=(5, 0))
tools_frame.grid(row=0, column=2, sticky="nsew", padx=(5, 0))
# Tools header
tools_header = tk.Label(tools_frame, text="🛠️ Editing Tools",
@ -591,6 +619,329 @@ class ShortsEditorGUI:
for track_id, track_info in self.tracks.items():
self.create_track_control(track_id, track_info)
def create_media_bin(self, parent_frame):
"""Create media bin for drag and drop file management"""
# Media bin header
header = tk.Frame(parent_frame, bg=self.colors['bg_secondary'], height=40)
header.pack(fill="x", padx=5, pady=5)
header.pack_propagate(False)
title_label = tk.Label(header, text="📁 Media Bin",
fg=self.colors['text_primary'], bg=self.colors['bg_secondary'],
font=('Segoe UI', 12, 'bold'))
title_label.pack(side="left", padx=10, pady=10)
# Add file button
add_btn = tk.Button(header, text="+ Add", command=self.add_media_file,
bg=self.colors['accent_blue'], fg='white', bd=0,
font=('Segoe UI', 10), relief='flat')
add_btn.pack(side="right", padx=10, pady=5)
# Media files container with scrollbar
container = tk.Frame(parent_frame, bg=self.colors['bg_tertiary'])
container.pack(fill="both", expand=True, padx=5, pady=(0, 5))
# Create canvas for scrollable media list
self.media_canvas = tk.Canvas(container, bg=self.colors['bg_tertiary'],
highlightthickness=0)
scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.media_canvas.yview)
self.scrollable_media_frame = tk.Frame(self.media_canvas, bg=self.colors['bg_tertiary'])
self.scrollable_media_frame.bind(
"<Configure>",
lambda e: self.media_canvas.configure(scrollregion=self.media_canvas.bbox("all"))
)
self.media_canvas.create_window((0, 0), window=self.scrollable_media_frame, anchor="nw")
self.media_canvas.configure(yscrollcommand=scrollbar.set)
self.media_canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Initialize media files list
self.media_files = []
# Create drop zone with proper drag and drop support
drop_zone = tk.Frame(self.scrollable_media_frame, bg=self.colors['bg_secondary'],
height=100, relief='solid', bd=1)
drop_zone.pack(fill="x", padx=10, pady=10)
drop_zone.pack_propagate(False)
drop_label = tk.Label(drop_zone, text="🎬 Drop media files here\nor click 'Add' button",
fg=self.colors['text_secondary'], bg=self.colors['bg_secondary'],
font=('Segoe UI', 10), justify='center')
drop_label.pack(expand=True)
# Enable Windows Explorer drag and drop
self.setup_external_drop(drop_zone)
self.setup_external_drop(drop_label)
# Bind click to add files
drop_zone.bind("<Button-1>", lambda e: self.add_media_file())
drop_label.bind("<Button-1>", lambda e: self.add_media_file())
def add_media_file(self):
"""Add media file to the bin"""
from tkinter import filedialog
import os
file_types = [
("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv"),
("Audio files", "*.mp3 *.wav *.aac *.ogg *.m4a"),
("All files", "*.*")
]
# Use current directory or Desktop as default
initial_dir = os.path.join(os.path.expanduser("~"), "Desktop")
file_path = filedialog.askopenfilename(
title="Select media file",
filetypes=file_types,
initialdir=initial_dir
)
if file_path:
self.add_media_to_bin(file_path)
def add_media_to_bin(self, file_path):
"""Add media file to bin with preview"""
import os
filename = os.path.basename(file_path)
file_ext = os.path.splitext(filename)[1].lower()
# Determine file type
video_exts = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv']
audio_exts = ['.mp3', '.wav', '.aac', '.ogg', '.m4a']
if file_ext in video_exts:
file_type = "🎬"
type_name = "Video"
elif file_ext in audio_exts:
file_type = "🎵"
type_name = "Audio"
else:
file_type = "📄"
type_name = "File"
# Create media item frame
media_item = tk.Frame(self.scrollable_media_frame, bg=self.colors['bg_secondary'],
relief='solid', bd=1)
media_item.pack(fill="x", padx=10, pady=2)
# File info frame
info_frame = tk.Frame(media_item, bg=self.colors['bg_secondary'])
info_frame.pack(fill="x", padx=5, pady=5)
# File type icon and name
tk.Label(info_frame, text=file_type, bg=self.colors['bg_secondary'],
font=('Segoe UI', 16)).pack(side="left", padx=(0, 5))
name_frame = tk.Frame(info_frame, bg=self.colors['bg_secondary'])
name_frame.pack(side="left", fill="x", expand=True)
tk.Label(name_frame, text=filename[:25] + "..." if len(filename) > 25 else filename,
fg=self.colors['text_primary'], bg=self.colors['bg_secondary'],
font=('Segoe UI', 9, 'bold'), anchor="w").pack(anchor="w")
tk.Label(name_frame, text=f"{type_name}{file_ext.upper()}",
fg=self.colors['text_secondary'], bg=self.colors['bg_secondary'],
font=('Segoe UI', 8), anchor="w").pack(anchor="w")
# Store file info
media_info = {
'path': file_path,
'filename': filename,
'type': type_name.lower(),
'widget': media_item
}
self.media_files.append(media_info)
# Enable drag and drop from media item to timeline
self.setup_media_drag(media_item, media_info)
print(f"📁 Added {type_name.lower()}: {filename}")
def setup_external_drop(self, widget):
"""Setup drag and drop from Windows Explorer"""
try:
# Try to enable Windows file drag and drop
import tkinter.dnd as dnd
def drop_enter(event):
widget.configure(bg=self.colors['accent_green'])
return 'copy'
def drop_leave(event):
widget.configure(bg=self.colors['bg_secondary'])
def drop_action(event):
# Handle file drop from Windows Explorer
files = widget.tk.splitlist(event.data)
for file_path in files:
if file_path.startswith('{') and file_path.endswith('}'):
file_path = file_path[1:-1] # Remove braces
# Check if it's a media file
import os
if os.path.isfile(file_path):
ext = os.path.splitext(file_path)[1].lower()
media_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv',
'.mp3', '.wav', '.aac', '.ogg', '.m4a']
if ext in media_extensions:
self.add_media_to_bin(file_path)
print(f"📁 Dropped file: {os.path.basename(file_path)}")
widget.configure(bg=self.colors['bg_secondary'])
return 'copy'
# Register for file drops
widget.bind('<Drop>', drop_action)
widget.bind('<DragEnter>', drop_enter)
widget.bind('<DragLeave>', drop_leave)
# Enable as drop target
widget.drop_target_register('DND_Files')
except Exception as e:
print(f"⚠️ External drag-drop setup failed: {e}")
# Fallback to click-to-add only
pass
def setup_media_drag(self, widget, media_info):
"""Setup drag and drop functionality for media items"""
def start_drag(event):
# Store the media info for drag operation
self.dragging_media = media_info
# Visual feedback: blue border and background when dragging starts
widget.configure(bg=self.colors['accent_blue'], relief='solid', bd=2)
print(f"🎬 Started dragging: {media_info['filename']}")
def end_drag(event):
# Check if we're over the timeline
try:
timeline_x = self.timeline_canvas.winfo_rootx()
timeline_y = self.timeline_canvas.winfo_rooty()
timeline_width = self.timeline_canvas.winfo_width()
timeline_height = self.timeline_canvas.winfo_height()
mouse_x = event.x_root
mouse_y = event.y_root
print(f"🖱️ Drop coordinates: mouse({mouse_x},{mouse_y}), timeline({timeline_x},{timeline_y},{timeline_width},{timeline_height})")
# Check if dropped on timeline
if (timeline_x <= mouse_x <= timeline_x + timeline_width and
timeline_y <= mouse_y <= timeline_y + timeline_height):
self.handle_media_drop_on_timeline(mouse_x, mouse_y)
else:
print(f"❌ Dropped outside timeline area")
except Exception as e:
print(f"❌ Error during drag end: {e}")
# Reset appearance to normal
widget.configure(bg=self.colors['bg_secondary'], relief='solid', bd=1)
self.dragging_media = None
def on_drag(event):
# Visual feedback during drag - keep blue highlight
if hasattr(self, 'dragging_media') and self.dragging_media:
widget.configure(bg=self.colors['accent_blue'], relief='solid', bd=2)
def on_hover_enter(event):
# Highlight on mouse hover (but not as strong as drag)
if not (hasattr(self, 'dragging_media') and self.dragging_media):
widget.configure(bg=self.colors['bg_hover'], relief='solid', bd=1)
def on_hover_leave(event):
# Remove highlight when mouse leaves (unless dragging)
if not (hasattr(self, 'dragging_media') and self.dragging_media):
widget.configure(bg=self.colors['bg_secondary'], relief='solid', bd=1)
# Bind drag events
widget.bind("<Button-1>", start_drag)
widget.bind("<ButtonRelease-1>", end_drag)
widget.bind("<B1-Motion>", on_drag)
# Bind hover events for better visual feedback
widget.bind("<Enter>", on_hover_enter)
widget.bind("<Leave>", on_hover_leave)
# Make all child widgets draggable too
for child in widget.winfo_children():
child.bind("<Button-1>", start_drag)
child.bind("<ButtonRelease-1>", end_drag)
child.bind("<B1-Motion>", on_drag)
child.bind("<Enter>", on_hover_enter)
child.bind("<Leave>", on_hover_leave)
for grandchild in child.winfo_children():
grandchild.bind("<Button-1>", start_drag)
grandchild.bind("<ButtonRelease-1>", end_drag)
grandchild.bind("<B1-Motion>", on_drag)
grandchild.bind("<Enter>", on_hover_enter)
grandchild.bind("<Leave>", on_hover_leave)
def handle_media_drop_on_timeline(self, mouse_x, mouse_y):
"""Handle dropping media from bin onto timeline"""
if not self.dragging_media:
return
try:
# Convert screen coordinates to timeline coordinates
timeline_x = self.timeline_canvas.winfo_rootx()
timeline_y = self.timeline_canvas.winfo_rooty()
# Calculate relative position on timeline
rel_x = mouse_x - timeline_x
rel_y = mouse_y - timeline_y
# Convert to timeline time and track
# Use a safe scale calculation to avoid division by zero
scale = max(self.timeline_scale, 1.0) # Ensure scale is at least 1.0
time_position = rel_x / scale
# Determine which track based on Y position
target_track = None
for track_id, track_info in self.tracks.items():
track_top = track_info['y_offset']
track_bottom = track_top + track_info['height']
if track_top <= rel_y <= track_bottom:
# Check if media type matches track type
media_type = self.dragging_media['type']
track_type = track_info['type']
if (media_type == 'video' and track_type == 'video') or \
(media_type == 'audio' and track_type == 'audio'):
target_track = track_id
break
if target_track:
# Create a clip on the timeline with reasonable duration
clip_info = {
'file_path': self.dragging_media['path'],
'filename': self.dragging_media['filename'],
'start_time': max(0, time_position),
'duration': 3.0, # Reasonable default duration for timeline clips
'track': target_track,
'type': self.dragging_media['type']
}
# Add clip to timeline
if not hasattr(self, 'timeline_clips'):
self.timeline_clips = []
self.timeline_clips.append(clip_info)
self.update_timeline() # Refresh timeline display
print(f"✅ Added {self.dragging_media['filename']} to {target_track} at {time_position:.1f}s")
else:
print(f"❌ Cannot drop {self.dragging_media['type']} file on this track location")
except Exception as e:
print(f"❌ Error handling drop: {e}")
def draw_track_road_lines(self):
"""Draw road lines for track visual separation"""
# Clear existing lines
@ -1142,6 +1493,10 @@ class ShortsEditorGUI:
filename = os.path.basename(video_path)
self.current_file_label.config(text=filename)
# Add video to media bin automatically
if hasattr(self, 'media_files'):
self.add_media_to_bin(video_path)
# Update trim controls
self.trim_start_var.set(0.0)
self.trim_end_var.set(self.video_duration)
@ -1277,17 +1632,15 @@ class ShortsEditorGUI:
self.timeline_canvas.delete("all")
if not self.current_clip:
return
canvas_width = self.timeline_canvas.winfo_width()
canvas_height = self.timeline_canvas.winfo_height()
if canvas_width <= 1:
return
# Calculate timeline scale
self.timeline_scale = (canvas_width - 40) / max(self.video_duration, 1)
# Calculate timeline scale based on video duration or default
video_duration = getattr(self, 'video_duration', 10.0) # Default 10 seconds if no video
self.timeline_scale = (canvas_width - 40) / max(video_duration, 1)
# Draw timeline background
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
@ -1296,8 +1649,11 @@ class ShortsEditorGUI:
# Draw track road lines
self.draw_timeline_track_roads(canvas_width, canvas_height)
# Draw dropped clips from media bin
self.draw_timeline_clips(canvas_width, canvas_height)
# Draw time markers
for i in range(0, int(self.video_duration) + 1):
for i in range(0, int(video_duration) + 1):
x = 20 + i * self.timeline_scale
if x < canvas_width - 20:
self.timeline_canvas.create_line(x, 20, x, canvas_height - 20,
@ -1309,10 +1665,11 @@ class ShortsEditorGUI:
text=f"{i}s", fill=self.colors['text_secondary'],
font=self.fonts['caption'])
# Draw playhead
playhead_x = 20 + self.current_time * self.timeline_scale
self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20,
fill=self.colors['accent_blue'], width=3)
# Draw playhead if there's a current clip
if hasattr(self, 'current_clip') and self.current_clip:
playhead_x = 20 + self.current_time * self.timeline_scale
self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20,
fill=self.colors['accent_blue'], width=3)
# Draw playhead handle
self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25,
@ -1353,25 +1710,350 @@ class ShortsEditorGUI:
font=('Arial', 8, 'bold'), anchor="center", tags="track_roads"
)
def timeline_click(self, event):
"""Handle timeline click"""
if not self.current_clip:
def draw_timeline_clips(self, canvas_width, canvas_height):
"""Draw clips dropped from media bin onto timeline"""
if not hasattr(self, 'timeline_clips'):
return
for i, clip in enumerate(self.timeline_clips):
track_info = self.tracks.get(clip['track'])
if not track_info:
continue
# Calculate clip position and size with better scaling
clip_x = 20 + clip['start_time'] * self.timeline_scale
clip_width = max(clip['duration'] * self.timeline_scale, 20) # Minimum width of 20px
# Ensure clip fits within canvas bounds
max_x = canvas_width - 20
if clip_x + clip_width > max_x:
clip_width = max_x - clip_x
clip_y = track_info['y_offset'] + 2
clip_height = track_info['height'] - 4
# Only draw if clip is visible
if clip_x < canvas_width - 20 and clip_x + clip_width > 20:
# Determine if this clip is selected
is_selected = (hasattr(self, 'selected_timeline_clip') and
self.selected_timeline_clip == i)
# Clip background with selection highlighting
clip_color = '#4CAF50' if clip['type'] == 'video' else '#FF9800'
outline_color = '#FFD700' if is_selected else 'white' # Gold outline for selected
outline_width = 3 if is_selected else 2
clip_rect = self.timeline_canvas.create_rectangle(clip_x, clip_y, clip_x + clip_width, clip_y + clip_height,
fill=clip_color, outline=outline_color, width=outline_width,
tags=f"clip_{i}")
# Add resize handles (small rectangles on the edges)
handle_size = 8
handle_color = '#FFD700' if is_selected else 'white'
# Left resize handle
left_handle = self.timeline_canvas.create_rectangle(
clip_x, clip_y, clip_x + handle_size, clip_y + clip_height,
fill=handle_color, outline='gray', width=1, tags=f"resize_left_{i}")
# Right resize handle
right_handle = self.timeline_canvas.create_rectangle(
clip_x + clip_width - handle_size, clip_y, clip_x + clip_width, clip_y + clip_height,
fill=handle_color, outline='gray', width=1, tags=f"resize_right_{i}")
# Make clip draggable and resizable
self.setup_clip_interaction(clip_rect, left_handle, right_handle, i)
# Clip filename (truncated)
filename = clip['filename'][:12] + "..." if len(clip['filename']) > 12 else clip['filename']
if clip_width > 40: # Only show text if clip is wide enough
text_color = 'black' if is_selected else 'white'
self.timeline_canvas.create_text(clip_x + clip_width//2, clip_y + clip_height // 2,
text=filename, fill=text_color,
font=('Arial', 8, 'bold'), anchor='center',
tags=f"clip_text_{i}")
def setup_clip_interaction(self, clip_rect, left_handle, right_handle, clip_index):
"""Setup interaction for clip resizing and moving"""
def start_resize_left(event):
self.resizing_clip = {'index': clip_index, 'side': 'left', 'start_x': event.x}
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
def start_resize_right(event):
self.resizing_clip = {'index': clip_index, 'side': 'right', 'start_x': event.x}
self.timeline_canvas.configure(cursor="sb_h_double_arrow")
def start_move_clip(event):
self.moving_clip = {'index': clip_index, 'start_x': event.x}
self.timeline_canvas.configure(cursor="hand2")
def on_resize_drag(event):
if hasattr(self, 'resizing_clip') and self.resizing_clip:
self.handle_clip_resize(event)
elif hasattr(self, 'moving_clip') and self.moving_clip:
self.handle_clip_move(event)
def end_interaction(event):
if hasattr(self, 'resizing_clip'):
self.resizing_clip = None
if hasattr(self, 'moving_clip'):
self.moving_clip = None
self.timeline_canvas.configure(cursor="arrow")
# Keep the clip selected after interaction
if hasattr(self, 'selected_timeline_clip') and self.selected_timeline_clip == clip_index:
print(f"🎯 Clip {clip_index} modified and still selected")
self.update_timeline()
# Bind resize handles
self.timeline_canvas.tag_bind(left_handle, "<Button-1>", start_resize_left)
self.timeline_canvas.tag_bind(right_handle, "<Button-1>", start_resize_right)
# Bind clip body for moving
self.timeline_canvas.tag_bind(clip_rect, "<Button-1>", start_move_clip)
# Bind drag motion and release
self.timeline_canvas.tag_bind(left_handle, "<B1-Motion>", on_resize_drag)
self.timeline_canvas.tag_bind(right_handle, "<B1-Motion>", on_resize_drag)
self.timeline_canvas.tag_bind(clip_rect, "<B1-Motion>", on_resize_drag)
self.timeline_canvas.tag_bind(left_handle, "<ButtonRelease-1>", end_interaction)
self.timeline_canvas.tag_bind(right_handle, "<ButtonRelease-1>", end_interaction)
self.timeline_canvas.tag_bind(clip_rect, "<ButtonRelease-1>", end_interaction)
def handle_clip_resize(self, event):
"""Handle clip resizing"""
if not self.resizing_clip or not hasattr(self, 'timeline_clips'):
return
clip_index = self.resizing_clip['index']
if clip_index >= len(self.timeline_clips):
return
clip = self.timeline_clips[clip_index]
scale = max(self.timeline_scale, 1.0)
# Calculate time delta
time_delta = (event.x - self.resizing_clip['start_x']) / scale
if self.resizing_clip['side'] == 'left':
# Resize from left - adjust start time and duration
new_start = clip['start_time'] + time_delta
new_duration = clip['duration'] - time_delta
if new_start >= 0 and new_duration >= 0.5: # Minimum 0.5 second duration
clip['start_time'] = new_start
clip['duration'] = new_duration
elif self.resizing_clip['side'] == 'right':
# Resize from right - adjust duration only
new_duration = clip['duration'] + time_delta
if new_duration >= 0.5: # Minimum 0.5 second duration
clip['duration'] = new_duration
self.resizing_clip['start_x'] = event.x
def handle_clip_move(self, event):
"""Handle clip moving"""
if not self.moving_clip or not hasattr(self, 'timeline_clips'):
return
clip_index = self.moving_clip['index']
if clip_index >= len(self.timeline_clips):
return
clip = self.timeline_clips[clip_index]
scale = max(self.timeline_scale, 1.0)
# Calculate time delta
time_delta = (event.x - self.moving_clip['start_x']) / scale
new_start = clip['start_time'] + time_delta
if new_start >= 0: # Don't allow negative start times
clip['start_time'] = new_start
self.moving_clip['start_x'] = event.x
def check_timeline_clips_at_playhead(self):
"""Check if playhead is hitting any clips and trigger playback"""
if not hasattr(self, 'timeline_clips') or not self.timeline_clips:
return
current_time = self.current_time
for clip in self.timeline_clips:
clip_start = clip['start_time']
clip_end = clip_start + clip['duration']
# Check if playhead is within this clip
if clip_start <= current_time <= clip_end:
# Calculate relative time within the clip
relative_time = current_time - clip_start
if clip['type'] == 'video':
self.play_timeline_video_clip(clip, relative_time)
elif clip['type'] == 'audio':
self.play_timeline_audio_clip(clip, relative_time)
def play_timeline_video_clip(self, clip, relative_time):
"""Play video clip at the specified relative time"""
try:
# Load the video clip if it's different from current
if not hasattr(self, 'current_timeline_clip') or self.current_timeline_clip != clip['file_path']:
# For now, just display a frame from the clip
# This is a simplified implementation - you'd want to load the actual video
print(f"🎬 Playing video clip: {clip['filename']} at {relative_time:.1f}s")
self.current_timeline_clip = clip['file_path']
# Try to load and display the video clip
try:
import cv2
cap = cv2.VideoCapture(clip['file_path'])
if cap.isOpened():
# Seek to the relative time
fps = cap.get(cv2.CAP_PROP_FPS) or 30
frame_number = int(relative_time * fps)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
if ret:
# Display this frame in the video player
self.display_opencv_frame(frame)
cap.release()
except Exception as e:
print(f"⚠️ Error playing video clip: {e}")
except Exception as e:
print(f"⚠️ Timeline video playback error: {e}")
def play_timeline_audio_clip(self, clip, relative_time):
"""Play audio clip at the specified relative time"""
try:
# Audio playback would require a library like pygame or similar
print(f"🎵 Playing audio clip: {clip['filename']} at {relative_time:.1f}s")
# This is a placeholder - actual audio playback would need implementation
except Exception as e:
print(f"⚠️ Timeline audio playback error: {e}")
def display_opencv_frame(self, frame):
"""Display an OpenCV frame in the video player"""
try:
import cv2
from PIL import Image, ImageTk
# Convert BGR to RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to PIL Image
image = Image.fromarray(frame_rgb)
# Resize to fit the video player
if hasattr(self, 'video_canvas'):
canvas_width = self.video_canvas.winfo_width() or 400
canvas_height = self.video_canvas.winfo_height() or 300
image = image.resize((canvas_width, canvas_height), Image.Resampling.LANCZOS)
# Convert to PhotoImage
photo = ImageTk.PhotoImage(image)
# Display in video canvas
if hasattr(self, 'video_canvas'):
self.video_canvas.delete("all")
self.video_canvas.create_image(0, 0, anchor="nw", image=photo)
self.video_canvas.image = photo # Keep a reference
except Exception as e:
print(f"⚠️ Frame display error: {e}")
def timeline_click(self, event):
"""Handle timeline click - CTRL+click for clip selection, regular click for playhead"""
canvas_width = self.timeline_canvas.winfo_width()
click_x = event.x
click_y = event.y
# Check if CTRL key is pressed
ctrl_pressed = (event.state & 0x4) != 0 # CTRL key modifier
if ctrl_pressed:
# CTRL+Click: Select clip without moving playhead
self.select_clip_at_position(click_x, click_y)
else:
# Regular click: Move playhead (existing behavior)
if not self.current_clip:
return
# Convert click position to time
relative_x = click_x - 20
if relative_x >= 0 and relative_x <= canvas_width - 40:
clicked_time = relative_x / self.timeline_scale
clicked_time = max(0, min(clicked_time, self.video_duration))
# Update current time and display
self.current_time = clicked_time
self.display_frame_at_time(self.current_time)
self.update_timeline()
self.update_time_display()
def select_clip_at_position(self, click_x, click_y):
"""Select clip at the clicked position without moving playhead"""
if not hasattr(self, 'timeline_clips') or not self.timeline_clips:
return
# Convert click position to time
relative_x = click_x - 20
if relative_x >= 0 and relative_x <= canvas_width - 40:
clicked_time = relative_x / self.timeline_scale
clicked_time = max(0, min(clicked_time, self.video_duration))
if relative_x < 0:
return
# Update current time and display
self.current_time = clicked_time
self.display_frame_at_time(self.current_time)
self.update_timeline()
self.update_time_display()
clicked_time = relative_x / self.timeline_scale
# Find which clip was clicked
for i, clip in enumerate(self.timeline_clips):
clip_start = clip['start_time']
clip_end = clip_start + clip['duration']
# Check if click is within this clip's time range
if clip_start <= clicked_time <= clip_end:
# Check if click is within this clip's track (Y position)
track_info = self.tracks.get(clip['track'])
if track_info:
track_top = track_info['y_offset']
track_bottom = track_top + track_info['height']
if track_top <= click_y <= track_bottom:
# Select this clip
self.selected_timeline_clip = i
print(f"🎯 Selected clip: {clip['filename']} on {clip['track']}")
# Update timeline to show selection
self.update_timeline()
return
# No clip found at click position
self.selected_timeline_clip = None
print("🎯 Deselected all clips")
self.update_timeline()
def delete_selected_clip(self, event):
"""Delete the currently selected timeline clip"""
if (hasattr(self, 'selected_timeline_clip') and
self.selected_timeline_clip is not None and
hasattr(self, 'timeline_clips') and
self.timeline_clips):
if 0 <= self.selected_timeline_clip < len(self.timeline_clips):
deleted_clip = self.timeline_clips[self.selected_timeline_clip]
del self.timeline_clips[self.selected_timeline_clip]
print(f"🗑️ Deleted clip: {deleted_clip['filename']}")
# Clear selection
self.selected_timeline_clip = None
# Update timeline display
self.update_timeline()
def timeline_drag(self, event):
"""Handle timeline dragging"""
@ -1524,6 +2206,9 @@ class ShortsEditorGUI:
self.display_frame_at_time(self.current_time)
self.update_time_display()
# Check for timeline clips at current playhead position
self.check_timeline_clips_at_playhead()
# Frame rate control (approximately 30 FPS)
time.sleep(1/30)