feat: Enhance ShortsEditorGUI with media bin for drag-and-drop functionality and timeline clip management
This commit is contained in:
parent
d589700d7a
commit
c9fc7c8d80
739
video_editor.py
739
video_editor.py
@ -160,6 +160,10 @@ class ShortsEditorGUI:
|
|||||||
# Timeline interaction state for drag-and-drop editing
|
# Timeline interaction state for drag-and-drop editing
|
||||||
# These variables track the current state of user interactions with timeline elements
|
# These variables track the current state of user interactions with timeline elements
|
||||||
self.dragging_clip = None # Clip currently being dragged by user
|
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_x = None # Mouse X position when drag started
|
||||||
self.drag_start_time = None # Original time position of dragged clip
|
self.drag_start_time = None # Original time position of dragged clip
|
||||||
self.drag_offset = 0 # Offset from clip start to mouse position
|
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.clip_thumbnails = {} # Dictionary storing thumbnail images by clip ID
|
||||||
self.audio_waveforms = {} # Dictionary storing waveform data 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
|
# Track control widget references for dynamic updates
|
||||||
# This allows us to update track control buttons when states change
|
# This allows us to update track control buttons when states change
|
||||||
self.track_widgets = {} # Dictionary storing widget references by track ID
|
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_primary': '#1a1a1a', # Main background - darkest for maximum contrast
|
||||||
'bg_secondary': '#2d2d2d', # Secondary panels - slightly lighter
|
'bg_secondary': '#2d2d2d', # Secondary panels - slightly lighter
|
||||||
'bg_tertiary': '#3d3d3d', # Buttons and controls - interactive elements
|
'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_primary': '#ffffff', # Main text - high contrast for readability
|
||||||
'text_secondary': '#b8b8b8', # Secondary text - lower contrast for hierarchy
|
'text_secondary': '#b8b8b8', # Secondary text - lower contrast for hierarchy
|
||||||
'accent_blue': '#007acc', # Primary actions - professional blue
|
'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 = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
|
||||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
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.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(0, weight=0) # Media bin column (fixed width)
|
||||||
main_frame.columnconfigure(1, weight=1) # Right column gets 1/3 of 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
|
# This is the main editing area where users see their video and timeline
|
||||||
# Organized vertically with video preview on top and timeline below
|
# Organized vertically with video preview on top and timeline below
|
||||||
player_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
|
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(0, weight=1) # Video preview area (expandable)
|
||||||
player_frame.rowconfigure(1, weight=0) # Timeline area (fixed height)
|
player_frame.rowconfigure(1, weight=0) # Timeline area (fixed height)
|
||||||
player_frame.columnconfigure(0, weight=1) # Full width utilization
|
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(side="bottom", fill="x", padx=5, pady=2)
|
||||||
external_scrollbar_frame.pack_propagate(False)
|
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
|
# Horizontal scrollbar outside timeline but controlling it
|
||||||
self.external_h_scrollbar = ttk.Scrollbar(external_scrollbar_frame, orient="horizontal",
|
self.external_h_scrollbar = ttk.Scrollbar(external_scrollbar_frame, orient="horizontal",
|
||||||
command=self.timeline_canvas.xview)
|
command=self.timeline_canvas.xview)
|
||||||
self.timeline_canvas.configure(xscrollcommand=self.external_h_scrollbar.set)
|
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
|
# Bind professional timeline events
|
||||||
self.timeline_canvas.bind("<Button-1>", self.timeline_click)
|
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("<Button-3>", self.on_timeline_right_click)
|
||||||
self.timeline_canvas.bind("<Double-Button-1>", self.on_timeline_double_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
|
# Create track controls
|
||||||
self.create_track_controls()
|
self.create_track_controls()
|
||||||
|
|
||||||
@ -487,7 +515,7 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
# Right panel - Tools and effects
|
# Right panel - Tools and effects
|
||||||
tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
|
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
|
||||||
tools_header = tk.Label(tools_frame, text="🛠️ Editing Tools",
|
tools_header = tk.Label(tools_frame, text="🛠️ Editing Tools",
|
||||||
@ -591,6 +619,329 @@ class ShortsEditorGUI:
|
|||||||
for track_id, track_info in self.tracks.items():
|
for track_id, track_info in self.tracks.items():
|
||||||
self.create_track_control(track_id, track_info)
|
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):
|
def draw_track_road_lines(self):
|
||||||
"""Draw road lines for track visual separation"""
|
"""Draw road lines for track visual separation"""
|
||||||
# Clear existing lines
|
# Clear existing lines
|
||||||
@ -1142,6 +1493,10 @@ class ShortsEditorGUI:
|
|||||||
filename = os.path.basename(video_path)
|
filename = os.path.basename(video_path)
|
||||||
self.current_file_label.config(text=filename)
|
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
|
# Update trim controls
|
||||||
self.trim_start_var.set(0.0)
|
self.trim_start_var.set(0.0)
|
||||||
self.trim_end_var.set(self.video_duration)
|
self.trim_end_var.set(self.video_duration)
|
||||||
@ -1277,17 +1632,15 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
self.timeline_canvas.delete("all")
|
self.timeline_canvas.delete("all")
|
||||||
|
|
||||||
if not self.current_clip:
|
|
||||||
return
|
|
||||||
|
|
||||||
canvas_width = self.timeline_canvas.winfo_width()
|
canvas_width = self.timeline_canvas.winfo_width()
|
||||||
canvas_height = self.timeline_canvas.winfo_height()
|
canvas_height = self.timeline_canvas.winfo_height()
|
||||||
|
|
||||||
if canvas_width <= 1:
|
if canvas_width <= 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Calculate timeline scale
|
# Calculate timeline scale based on video duration or default
|
||||||
self.timeline_scale = (canvas_width - 40) / max(self.video_duration, 1)
|
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
|
# Draw timeline background
|
||||||
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
|
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
|
||||||
@ -1296,8 +1649,11 @@ class ShortsEditorGUI:
|
|||||||
# Draw track road lines
|
# Draw track road lines
|
||||||
self.draw_timeline_track_roads(canvas_width, canvas_height)
|
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
|
# 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
|
x = 20 + i * self.timeline_scale
|
||||||
if x < canvas_width - 20:
|
if x < canvas_width - 20:
|
||||||
self.timeline_canvas.create_line(x, 20, x, canvas_height - 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'],
|
text=f"{i}s", fill=self.colors['text_secondary'],
|
||||||
font=self.fonts['caption'])
|
font=self.fonts['caption'])
|
||||||
|
|
||||||
# Draw playhead
|
# Draw playhead if there's a current clip
|
||||||
playhead_x = 20 + self.current_time * self.timeline_scale
|
if hasattr(self, 'current_clip') and self.current_clip:
|
||||||
self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20,
|
playhead_x = 20 + self.current_time * self.timeline_scale
|
||||||
fill=self.colors['accent_blue'], width=3)
|
self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20,
|
||||||
|
fill=self.colors['accent_blue'], width=3)
|
||||||
|
|
||||||
# Draw playhead handle
|
# Draw playhead handle
|
||||||
self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25,
|
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"
|
font=('Arial', 8, 'bold'), anchor="center", tags="track_roads"
|
||||||
)
|
)
|
||||||
|
|
||||||
def timeline_click(self, event):
|
def draw_timeline_clips(self, canvas_width, canvas_height):
|
||||||
"""Handle timeline click"""
|
"""Draw clips dropped from media bin onto timeline"""
|
||||||
if not self.current_clip:
|
if not hasattr(self, 'timeline_clips'):
|
||||||
return
|
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()
|
canvas_width = self.timeline_canvas.winfo_width()
|
||||||
click_x = event.x
|
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
|
# Convert click position to time
|
||||||
relative_x = click_x - 20
|
relative_x = click_x - 20
|
||||||
if relative_x >= 0 and relative_x <= canvas_width - 40:
|
if relative_x < 0:
|
||||||
clicked_time = relative_x / self.timeline_scale
|
return
|
||||||
clicked_time = max(0, min(clicked_time, self.video_duration))
|
|
||||||
|
|
||||||
# Update current time and display
|
clicked_time = relative_x / self.timeline_scale
|
||||||
self.current_time = clicked_time
|
|
||||||
self.display_frame_at_time(self.current_time)
|
# Find which clip was clicked
|
||||||
self.update_timeline()
|
for i, clip in enumerate(self.timeline_clips):
|
||||||
self.update_time_display()
|
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):
|
def timeline_drag(self, event):
|
||||||
"""Handle timeline dragging"""
|
"""Handle timeline dragging"""
|
||||||
@ -1524,6 +2206,9 @@ class ShortsEditorGUI:
|
|||||||
self.display_frame_at_time(self.current_time)
|
self.display_frame_at_time(self.current_time)
|
||||||
self.update_time_display()
|
self.update_time_display()
|
||||||
|
|
||||||
|
# Check for timeline clips at current playhead position
|
||||||
|
self.check_timeline_clips_at_playhead()
|
||||||
|
|
||||||
# Frame rate control (approximately 30 FPS)
|
# Frame rate control (approximately 30 FPS)
|
||||||
time.sleep(1/30)
|
time.sleep(1/30)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user