feat: Implement drag-and-drop functionality for media items in the timeline and media bin

This commit is contained in:
klop51 2025-08-16 00:36:15 +02:00
parent 82082911b3
commit bc04aba0d7

View File

@ -57,13 +57,13 @@ from PyQt6.QtWidgets import (
)
from PyQt6.QtCore import (
Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect,
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint
)
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtGui import (
QPalette, QColor, QFont, QIcon, QKeySequence, QShortcut, QPixmap, QPainter,
QBrush, QPen, QPolygon, QAction, QCursor
QBrush, QPen, QPolygon, QAction, QCursor, QDrag
)
# Try to import MoviePy, handle if not available
@ -124,16 +124,23 @@ class TimelineWidget(QWidget):
def __init__(self):
super().__init__()
self.duration = 0.0
self.duration = 30.0 # Start with 30 seconds default duration
self.current_time = 0.0
self.zoom_level = 1.0
self.track_height = 70 # Adjusted for 5 tracks to fit properly
self.ruler_height = 30 # Increased ruler height
self.timeline_clips = [] # Store clips on timeline
self.selected_clip_index = None # Track selected clip
self.setAcceptDrops(True) # Enable drag and drop
self.setup_ui()
def setup_ui(self):
"""Setup timeline interface"""
self.setMinimumHeight(400) # Much larger for proper track visibility
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) # Allow keyboard focus
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, False) # Disable touch to avoid conflicts
self.setMouseTracking(False) # Ensure we only get mouse press events
print(f"🔧 Timeline widget setup: size policy, focus policy, mouse tracking configured")
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
@ -177,22 +184,107 @@ class TimelineWidget(QWidget):
painter.setPen(QPen(QColor(track_colors[i]), 2))
painter.drawRect(content_rect)
# Draw sample content blocks (visual representation)
if self.duration > 0:
content_width = content_rect.width()
# Draw a content block spanning part of the timeline
block_width = min(200, content_width // 2)
# Draw actual timeline clips for this track
# Use effective timeline duration for consistent positioning
effective_duration = self.get_effective_timeline_duration()
content_width = content_rect.width()
pixels_per_second = content_width / effective_duration
# Draw clips on this track
for clip_index, clip in enumerate(self.timeline_clips):
if clip['track'] == i:
# Calculate clip position and size
clip_x = content_rect.x() + (clip['start_time'] * pixels_per_second)
clip_width = max(60, clip['duration'] * pixels_per_second) # Minimum width for visibility
clip_rect = QRect(int(clip_x), content_rect.y() + 3,
int(clip_width), content_rect.height() - 6)
# Determine if this clip is selected
is_selected = (self.selected_clip_index == clip_index)
# Make clips more visible with brighter colors and gradients
if clip['type'] == 'video':
# Bright blue for video clips
clip_color = QColor("#4A90E2") if not is_selected else QColor("#00AA00") # Green when selected
border_color = QColor("#2E5C8A") if not is_selected else QColor("#00FF00") # Bright green border when selected
else:
# Bright red for audio clips
clip_color = QColor("#E74C3C") if not is_selected else QColor("#FF6600") # Orange when selected
border_color = QColor("#C0392B") if not is_selected else QColor("#FF8800") # Bright orange border when selected
# Fill with clip color
painter.fillRect(clip_rect, clip_color)
# Add a subtle gradient effect
gradient_rect = QRect(clip_rect.x(), clip_rect.y(),
clip_rect.width(), clip_rect.height() // 3)
lighter_color = clip_color.lighter(130)
painter.fillRect(gradient_rect, lighter_color)
# Draw border - thicker for selected clips
border_width = 3 if is_selected else 2
painter.setPen(QPen(border_color, border_width))
painter.drawRect(clip_rect)
# Draw inner highlight - brighter for selected clips
highlight_color = QColor("#ffffff") if not is_selected else QColor("#ffff00")
painter.setPen(QPen(highlight_color, 1))
inner_rect = QRect(clip_rect.x() + 1, clip_rect.y() + 1,
clip_rect.width() - 2, clip_rect.height() - 2)
painter.drawRect(inner_rect)
# Draw selection indicator for selected clips
if is_selected:
# Draw selection corners
corner_size = 8
painter.setPen(QPen(QColor("#ffff00"), 2))
painter.setBrush(QColor("#ffff00"))
# Top-left corner
painter.drawRect(clip_rect.x() - 2, clip_rect.y() - 2, corner_size, corner_size)
# Top-right corner
painter.drawRect(clip_rect.x() + clip_rect.width() - corner_size + 2, clip_rect.y() - 2, corner_size, corner_size)
# Bottom-left corner
painter.drawRect(clip_rect.x() - 2, clip_rect.y() + clip_rect.height() - corner_size + 2, corner_size, corner_size)
# Bottom-right corner
painter.drawRect(clip_rect.x() + clip_rect.width() - corner_size + 2, clip_rect.y() + clip_rect.height() - corner_size + 2, corner_size, corner_size)
# Draw clip filename with better contrast
painter.setFont(QFont("Arial", 9, QFont.Weight.Bold))
text_color = QColor("#ffffff") if not is_selected else QColor("#000000") # Black text on selected clips
painter.setPen(QPen(text_color, 1))
# Add text shadow for better readability (only for non-selected clips)
if not is_selected:
shadow_rect = QRect(clip_rect.x() + 3, clip_rect.y() + 1,
clip_rect.width() - 6, clip_rect.height())
painter.setPen(QPen(QColor("#000000"), 1))
painter.drawText(shadow_rect, Qt.AlignmentFlag.AlignCenter,
clip['filename'][:12] + "..." if len(clip['filename']) > 12 else clip['filename'])
# Draw actual text
text_rect = QRect(clip_rect.x() + 2, clip_rect.y(),
clip_rect.width() - 4, clip_rect.height())
painter.setPen(QPen(text_color, 1))
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter,
clip['filename'][:12] + "..." if len(clip['filename']) > 12 else clip['filename'])
# If no clips on this track, draw sample content block for visual reference
if not any(clip['track'] == i for clip in self.timeline_clips):
block_width = min(200, content_width // 4)
block_rect = QRect(content_rect.x() + 10, content_rect.y() + 5,
block_width, content_rect.height() - 10)
# Fill with track color
painter.fillRect(block_rect, QColor(track_colors[i]))
painter.setPen(QPen(QColor("#ffffff"), 1))
# Fill with track color (dimmed)
dim_color = QColor(track_colors[i])
dim_color.setAlpha(80)
painter.fillRect(block_rect, dim_color)
painter.setPen(QPen(QColor("#666666"), 1))
painter.drawRect(block_rect)
# Draw track name on the content block
painter.setFont(QFont("Arial", 10, QFont.Weight.Bold))
painter.setPen(QPen(QColor("#ffffff"), 1))
# Draw track name on the sample block
painter.setFont(QFont("Arial", 9))
painter.setPen(QPen(QColor("#999999"), 1))
text_rect = QRect(block_rect.x() + 5, block_rect.y(),
block_rect.width() - 10, block_rect.height())
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, track_names[i])
@ -208,57 +300,254 @@ class TimelineWidget(QWidget):
painter.drawLine(0, ruler_bottom, self.width(), ruler_bottom)
# Draw playhead on top of everything
if self.duration > 0:
playhead_x = 120 + ((self.current_time / self.duration) * (self.width() - 130))
painter.setPen(QPen(QColor("#ff4444"), 3))
painter.drawLine(int(playhead_x), ruler_bottom, int(playhead_x), self.height())
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
# Use current video time directly since timeline now accommodates video duration
timeline_position = self.current_time
playhead_x = 120 + ((timeline_position / effective_duration) * (self.width() - 130))
# Draw playhead handle
handle_rect = QRect(int(playhead_x) - 8, ruler_bottom - 15, 16, 15)
painter.fillRect(handle_rect, QColor("#ff4444"))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawRect(handle_rect)
# Only draw playhead if it's within the visible timeline area
if 120 <= playhead_x <= self.width() - 20:
painter.setPen(QPen(QColor("#ff4444"), 3))
painter.drawLine(int(playhead_x), ruler_bottom, int(playhead_x), self.height())
# Draw playhead handle
handle_rect = QRect(int(playhead_x) - 8, ruler_bottom - 15, 16, 15)
painter.fillRect(handle_rect, QColor("#ff4444"))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawRect(handle_rect)
def draw_time_ruler(self, painter):
"""Draw time ruler at the top"""
"""Draw time ruler at the top using effective timeline duration with smart scaling"""
painter.fillRect(0, 0, self.width(), self.ruler_height, QColor("#2d2d2d"))
if self.duration > 0:
# Calculate time intervals
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
# Calculate time intervals based on effective timeline
content_area_start = 120
content_area_width = self.width() - 130
pixels_per_second = content_area_width / self.duration
pixels_per_second = content_area_width / effective_duration
painter.setPen(QPen(QColor("#cccccc"), 1))
painter.setFont(QFont("Arial", 9))
# Draw second markers
for second in range(int(self.duration) + 1):
x = content_area_start + (second * pixels_per_second)
# Smart interval calculation based on timeline duration and available space
if effective_duration <= 300: # Under 5 minute
major_interval = 10 # Every 10 seconds
minor_interval = 1 # Minor marks every 1 seconds
elif effective_duration <= 1800: # Under 30 minutes
major_interval = 120 # Every 2 minutes
minor_interval = 60 # Minor marks every minute
else: # Over 30 minutes
major_interval = 300 # Every 5 minutes
minor_interval = 60 # Minor marks every 1 minutes
# Draw major time markers with labels
current_time = 0
while current_time <= effective_duration:
x = content_area_start + (current_time * pixels_per_second)
if x <= self.width() - 10:
painter.drawLine(int(x), self.ruler_height - 8, int(x), self.ruler_height)
# Draw major marker line
painter.setPen(QPen(QColor("#cccccc"), 2))
painter.drawLine(int(x), 0, int(x), self.ruler_height - 5)
# Draw time labels every 5 seconds
if second % 5 == 0:
painter.drawText(int(x) + 2, self.ruler_height - 12, f"{second}s")
# Format and draw time label
if current_time >= 60:
minutes = int(current_time // 60)
seconds = int(current_time % 60)
time_text = f"{minutes}:{seconds:02d}"
else:
time_text = f"{int(current_time)}s"
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawText(int(x) + 2, self.ruler_height - 12, time_text)
current_time += major_interval
# Draw minor time markers (no labels)
if minor_interval != major_interval:
painter.setPen(QPen(QColor("#888888"), 1))
current_time = 0
while current_time <= effective_duration:
if current_time % major_interval != 0: # Skip major intervals
x = content_area_start + (current_time * pixels_per_second)
if x <= self.width() - 10:
painter.drawLine(int(x), self.ruler_height - 12, int(x), self.ruler_height - 5)
current_time += minor_interval
# Draw ruler border
painter.setPen(QPen(QColor("#404040"), 1))
painter.drawRect(0, 0, self.width(), self.ruler_height)
def mousePressEvent(self, event):
"""Handle timeline clicking for seeking"""
if event.button() == Qt.MouseButton.LeftButton and self.duration > 0:
# Only allow seeking in the content area (not on track labels)
"""Handle timeline clicking for seeking and clip selection"""
print(f"🖱️ Timeline mousePressEvent called: button={event.button()}, pos=({event.position().x():.1f}, {event.position().y():.1f})")
if event.button() == Qt.MouseButton.LeftButton:
# Only allow operations in the content area (not on track labels)
content_area_start = 120
if event.position().x() >= content_area_start:
# Calculate time from click position relative to content area
content_width = self.width() - 130
click_x = event.position().x() - content_area_start
click_time = (click_x / content_width) * self.duration
self.current_time = max(0, min(click_time, self.duration))
self.positionChanged.emit(self.current_time)
self.update()
click_x = event.position().x()
click_y = event.position().y()
if click_x >= content_area_start:
# Check if CTRL is pressed for clip selection
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
if ctrl_pressed:
# CTRL+Click: Select/deselect clip without moving playhead
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
# CTRL+Click on clip: select it
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 CTRL+Click: Selected clip: {clip['filename']} on track {clip['track']}")
self.update()
else:
# CTRL+Click on empty area: just deselect
if self.selected_clip_index is not None:
print("🎯 CTRL+Click: Deselected all clips (playhead stays)")
self.selected_clip_index = None
self.update()
else:
print("🎯 CTRL+Click: No clips to deselect (playhead stays)")
# Do NOT move playhead when CTRL is pressed
else:
# Regular click without CTRL
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
# Click on clip: select it (no playhead movement)
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 Selected clip: {clip['filename']} on track {clip['track']}")
self.update()
else:
# Click on empty area: deselect clips and move playhead
# First handle deselection
if self.selected_clip_index is not None:
print("🎯 Deselected all clips")
self.selected_clip_index = None
# Then move playhead using effective timeline duration
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
content_width = self.width() - 130
click_x_relative = click_x - content_area_start
click_time = (click_x_relative / content_width) * effective_duration
self.current_time = max(0, min(click_time, self.duration))
print(f"⏭️ Playhead moved to {self.current_time:.2f}s")
self.positionChanged.emit(self.current_time)
self.update()
else:
print(f"🖱️ Click outside content area (x={click_x:.1f}, content_start={content_area_start})")
else:
print(f"🖱️ Non-left button click: {event.button()}")
def get_effective_timeline_duration(self):
"""Calculate the effective timeline duration for display and interaction
This uses a dynamic timeline approach that expands to accommodate
both clips and the loaded video duration.
"""
# Start with a minimum timeline duration
min_duration = 30.0
# Find the latest clip end time
latest_clip_time = 0
if self.timeline_clips:
for clip in self.timeline_clips:
clip_end = clip['start_time'] + clip['duration']
latest_clip_time = max(latest_clip_time, clip_end)
# Consider the video duration if a video is loaded
video_duration = getattr(self, 'duration', 0)
# Use the maximum of:
# 1. Minimum duration (30s)
# 2. Latest clip time + padding
# 3. Current video duration (if reasonable)
timeline_duration = max(
min_duration,
latest_clip_time + 10.0, # Add 10s padding after clips
min(video_duration, 3600) # Cap at 1 hour for UI performance
)
return timeline_duration
def get_clip_at_position(self, click_x, click_y):
"""Get clip index at the clicked position using effective timeline scaling"""
if not self.timeline_clips:
return None
content_area_start = 120
content_width = self.width() - 130
# Use effective timeline duration instead of video duration
effective_duration = self.get_effective_timeline_duration()
pixels_per_second = content_width / effective_duration
# Convert click position to time
click_x_relative = click_x - content_area_start
clicked_time = click_x_relative / pixels_per_second
# Get track from Y position
track_index = self.get_track_from_y(click_y)
if track_index < 0:
return None
# Find clip at this position
for i, clip in enumerate(self.timeline_clips):
if clip['track'] == track_index:
clip_start = clip['start_time']
clip_end = clip_start + clip['duration']
if clip_start <= clicked_time <= clip_end:
return i
return None
def select_clip_at_position(self, click_x, click_y):
"""Select clip at position without affecting playhead"""
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 Selected clip: {clip['filename']} on track {clip['track']}")
else:
self.selected_clip_index = None
print("🎯 Deselected all clips")
self.update()
def keyPressEvent(self, event):
"""Handle keyboard events for clip operations"""
if event.key() == Qt.Key.Key_Delete or event.key() == Qt.Key.Key_Backspace:
# Delete selected clip
self.delete_selected_clip()
else:
super().keyPressEvent(event)
def delete_selected_clip(self):
"""Delete the currently selected clip"""
if (self.selected_clip_index is not None and
0 <= self.selected_clip_index < len(self.timeline_clips)):
deleted_clip = self.timeline_clips[self.selected_clip_index]
del self.timeline_clips[self.selected_clip_index]
print(f"🗑️ Deleted clip: {deleted_clip['filename']}")
print(f"📊 Timeline now has {len(self.timeline_clips)} clip(s)")
# Clear selection
self.selected_clip_index = None
# Update display
self.update()
def set_duration(self, duration):
"""Set timeline duration"""
@ -269,6 +558,122 @@ class TimelineWidget(QWidget):
"""Set current playhead position"""
self.current_time = position
self.update()
def dragEnterEvent(self, event):
"""Handle drag enter event - check if it's a media item"""
if event.mimeData().hasFormat("application/x-media-item"):
event.acceptProposedAction()
# Visual feedback
self.setStyleSheet("""
TimelineWidget {
background-color: #2a3a2a;
border: 2px dashed #00aa00;
}
""")
else:
event.ignore()
def dragMoveEvent(self, event):
"""Handle drag move event"""
if event.mimeData().hasFormat("application/x-media-item"):
event.acceptProposedAction()
else:
event.ignore()
def dragLeaveEvent(self, event):
"""Handle drag leave event - restore normal appearance"""
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
def dropEvent(self, event):
"""Handle drop event - add media to timeline"""
# Restore normal appearance
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
if event.mimeData().hasFormat("application/x-media-item"):
# Get file path from mime data
file_path = event.mimeData().data("application/x-media-item").data().decode()
# Calculate drop position
drop_x = event.position().x()
drop_y = event.position().y()
# Check if dropped in valid area (content area)
content_area_start = 120
if drop_x >= content_area_start:
# If timeline has no duration, set a default duration of 30 seconds
if self.duration <= 0:
self.duration = 30.0
# Calculate time position
content_width = self.width() - 130
click_x = drop_x - content_area_start
time_position = (click_x / content_width) * self.duration
# Determine which track based on Y position
track_index = self.get_track_from_y(drop_y)
if track_index >= 0:
# Create clip info
clip_info = {
'file_path': file_path,
'filename': os.path.basename(file_path),
'start_time': max(0, time_position),
'duration': 3.0, # Default duration
'track': track_index,
'type': 'video' if self.is_video_file(file_path) else 'audio'
}
# Add to timeline clips
self.timeline_clips.append(clip_info)
self.update()
# Visual and audio feedback for successful drop
print(f"✅ Added {os.path.basename(file_path)} to track {track_index} at {time_position:.1f}s")
print(f"📊 Timeline now has {len(self.timeline_clips)} clip(s)")
# Trigger a repaint to immediately show the new clip
QApplication.processEvents()
event.acceptProposedAction()
else:
print("❌ Invalid track area")
event.ignore()
else:
print("❌ Dropped outside content area")
event.ignore()
else:
event.ignore()
def get_track_from_y(self, y_pos):
"""Get track index from Y position"""
ruler_bottom = self.ruler_height
if y_pos < ruler_bottom:
return -1 # Dropped on ruler
track_y = y_pos - ruler_bottom
track_index = int(track_y // self.track_height)
# Validate track index (5 tracks: 0-4)
if 0 <= track_index < 5:
return track_index
return -1
def is_video_file(self, file_path):
"""Check if file is a video file"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
ext = os.path.splitext(file_path)[1].lower()
return ext in video_extensions
class EffectsPanel(QWidget):
"""Professional effects panel with time-based controls"""
@ -448,15 +853,180 @@ class EffectsPanel(QWidget):
}
self.effectApplied.emit('text', params)
class DraggableMediaItem(QPushButton):
"""Draggable media item that can be dragged to timeline"""
mediaSelected = pyqtSignal(str) # file_path
def __init__(self, file_path, parent=None):
super().__init__(f"🎬 {os.path.basename(file_path)}", parent)
self.file_path = file_path
self.drag_start_position = QPoint()
self.dragging = False
# Store media info for drag operations
self.media_info = {
'path': file_path,
'filename': os.path.basename(file_path),
'type': 'video' if self.is_video_file(file_path) else 'audio'
}
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
self.clicked.connect(self.select_media)
def is_video_file(self, file_path):
"""Check if file is a video file"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
ext = os.path.splitext(file_path)[1].lower()
return ext in video_extensions
def select_media(self):
"""Emit signal for media selection"""
self.mediaSelected.emit(self.file_path)
print(f"🎬 Selected: {os.path.basename(self.file_path)}")
def mousePressEvent(self, event):
"""Handle mouse press - start potential drag operation"""
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_position = event.pos()
# Visual feedback for drag start
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 2px solid #00aaff;
border-radius: 3px;
background-color: #003366;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
""")
print(f"🎬 Started dragging: {self.media_info['filename']}")
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Handle mouse move - perform drag operation"""
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
if not self.dragging and (event.pos() - self.drag_start_position).manhattanLength() < QApplication.startDragDistance():
return
if not self.dragging:
self.dragging = True
# Start drag operation
drag = QDrag(self)
mimeData = QMimeData()
# Store media info in mime data
mimeData.setText(f"media_drag:{self.file_path}")
mimeData.setData("application/x-media-item", self.file_path.encode())
drag.setMimeData(mimeData)
# Create a more visible drag pixmap
pixmap = QPixmap(150, 40)
pixmap.fill(QColor(0, 170, 255, 200))
painter = QPainter(pixmap)
painter.setPen(QPen(QColor("#ffffff"), 2))
painter.setFont(QFont("Arial", 10, QFont.Weight.Bold))
painter.drawText(10, 25, f"🎬 {os.path.basename(self.file_path)[:12]}...")
painter.end()
drag.setPixmap(pixmap)
drag.setHotSpot(QPoint(75, 20))
# Execute drag
dropAction = drag.exec(Qt.DropAction.CopyAction)
# Reset dragging state
self.dragging = False
self.reset_appearance()
def mouseReleaseEvent(self, event):
"""Handle mouse release - end drag operation"""
self.reset_appearance()
super().mouseReleaseEvent(event)
def reset_appearance(self):
"""Reset button appearance to normal"""
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
class DropScrollArea(QScrollArea):
"""Custom scroll area that handles drag and drop events"""
def __init__(self, media_bin, parent=None):
super().__init__(parent)
self.media_bin = media_bin
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
"""Forward drag enter event to media bin"""
self.media_bin.dragEnterEvent(event)
def dragMoveEvent(self, event):
"""Forward drag move event to media bin"""
self.media_bin.dragMoveEvent(event)
def dragLeaveEvent(self, event):
"""Forward drag leave event to media bin"""
self.media_bin.dragLeaveEvent(event)
def dropEvent(self, event):
"""Forward drop event to media bin"""
self.media_bin.dropEvent(event)
class MediaBin(QWidget):
"""Professional media bin for file management"""
"""Professional media bin for file management with drag and drop support"""
mediaSelected = pyqtSignal(str) # file_path
def __init__(self):
super().__init__()
self.media_files = []
# Enable drag and drop from Windows Explorer
self.setAcceptDrops(True)
self.setup_ui()
self.auto_load_media()
def setup_ui(self):
"""Setup media bin interface"""
@ -503,9 +1073,11 @@ class MediaBin(QWidget):
# Add stretch at the end to push items to the top
self.media_list_layout.addStretch()
scroll_area = QScrollArea()
scroll_area = DropScrollArea(self)
scroll_area.setWidget(self.media_list)
scroll_area.setWidgetResizable(True)
# Store reference to handle events
self.scroll_area = scroll_area
scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
@ -546,36 +1118,102 @@ class MediaBin(QWidget):
if file_path not in self.media_files:
self.media_files.append(file_path)
# Create media item widget
media_item = QPushButton(f"🎬 {os.path.basename(file_path)}")
media_item.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
media_item.clicked.connect(lambda: self.select_media(file_path))
# Create draggable media item widget
media_item = DraggableMediaItem(file_path)
media_item.mediaSelected.connect(self.handle_media_selected)
# Insert before the stretch to keep items at the top
self.media_list_layout.insertWidget(self.media_list_layout.count() - 1, media_item)
print(f"📁 Added to media bin: {os.path.basename(file_path)}")
def handle_media_selected(self, file_path):
"""Handle media selection from draggable item"""
self.mediaSelected.emit(file_path)
def select_media(self, file_path):
"""Select media file"""
self.mediaSelected.emit(file_path)
print(f"🎬 Selected: {os.path.basename(file_path)}")
def dragEnterEvent(self, event):
"""Handle drag enter event - check if files are being dragged"""
if event.mimeData().hasUrls():
# Check if any of the URLs are video/audio files
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
ext = os.path.splitext(file_path)[1].lower()
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm',
'.mp3', '.wav', '.aac', '.ogg', '.m4a']
if ext in video_extensions:
event.acceptProposedAction()
# Visual feedback - change scroll area background color
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 2px dashed #00aa00;
border-radius: 4px;
background-color: #2a4a2a;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
return
event.ignore()
def dragMoveEvent(self, event):
"""Handle drag move event"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dragLeaveEvent(self, event):
"""Handle drag leave event - restore normal appearance"""
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
def dropEvent(self, event):
"""Handle drop event - add dropped files to media bin"""
# Restore normal appearance
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
ext = os.path.splitext(file_path)[1].lower()
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm',
'.mp3', '.wav', '.aac', '.ogg', '.m4a']
if ext in video_extensions:
self.add_media_to_list(file_path)
print(f"🎯 Dropped file: {os.path.basename(file_path)}")
event.acceptProposedAction()
else:
event.ignore()
class ProfessionalVideoEditor(QMainWindow):
"""Professional video editor with PyQt6"""