ShortGenerator/video_editor_enhanced.py
klop51 7f6b7b4901 Enhance video widget scaling and export functionality
- Improved video widget scaling to fit within its container while maintaining aspect ratio.
- Added dynamic resizing of the video widget on window resize events.
- Implemented a separate thread for video export to prevent UI freezing and added progress tracking.
- Enhanced export process to include timeline clips as overlays on the main video.
- Updated export completion handling with user feedback and error reporting.
- Adjusted layout and styling for better user experience.
2025-08-16 15:04:09 +02:00

1432 lines
49 KiB
Python

import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTreeWidget, QTreeWidgetItem, QLabel,
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableWidget,
QTableWidgetItem, QToolBar, QStyle, QTabWidget, QFrame, QProgressBar,
QSlider, QPushButton, QSpinBox, QComboBox, QScrollArea, QGroupBox,
QCheckBox, QLineEdit, QTextEdit, QDockWidget, QListWidget, QGridLayout,
QSpacerItem, QSizePolicy, QStatusBar, QMenuBar, QMenu, QListWidgetItem,
QStackedWidget, QFormLayout
)
from PyQt6.QtGui import (
QAction, QIcon, QPalette, QColor, QPainter, QPen, QFont, QPixmap,
QBrush, QLinearGradient, QKeySequence
)
from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QRect, QPoint
class EnhancedTimelineWidget(QWidget):
"""Professional timeline widget with advanced features like Adobe Premiere."""
timecode_changed = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.current_frame = 0
self.total_frames = 7200 # 5 minutes at 24fps
self.fps = 24
self.zoom_level = 50
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(1)
# Timeline toolbar
toolbar_widget = self.create_timeline_toolbar()
layout.addWidget(toolbar_widget)
# Timecode display
timecode_widget = self.create_timecode_display()
layout.addWidget(timecode_widget)
# Time ruler
ruler_widget = self.create_time_ruler()
layout.addWidget(ruler_widget)
# Main timeline tracks
tracks_widget = self.create_timeline_tracks()
layout.addWidget(tracks_widget, 1) # Takes remaining space
def create_timeline_toolbar(self):
"""Create enhanced timeline toolbar with professional controls."""
toolbar = QWidget()
toolbar.setFixedHeight(35)
toolbar.setStyleSheet("""
QWidget {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #404040, stop:1 #2a2a2a);
border-bottom: 1px solid #555555;
}
""")
layout = QHBoxLayout(toolbar)
layout.setContentsMargins(8, 4, 8, 4)
layout.setSpacing(12)
# Zoom controls
zoom_group = QWidget()
zoom_layout = QHBoxLayout(zoom_group)
zoom_layout.setContentsMargins(0, 0, 0, 0)
zoom_layout.setSpacing(4)
zoom_label = QLabel("Zoom:")
zoom_label.setStyleSheet("color: #cccccc; font-size: 11px; font-weight: bold;")
self.zoom_slider = QSlider(Qt.Orientation.Horizontal)
self.zoom_slider.setRange(10, 200)
self.zoom_slider.setValue(self.zoom_level)
self.zoom_slider.setFixedWidth(120)
self.zoom_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #555555;
height: 4px;
background: #333333;
border-radius: 2px;
}
QSlider::handle:horizontal {
background: #0078d4;
border: 1px solid #005a9e;
width: 12px;
margin: -4px 0;
border-radius: 6px;
}
""")
zoom_value = QLabel(f"{self.zoom_level}%")
zoom_value.setStyleSheet("color: #cccccc; font-size: 11px; min-width: 35px;")
zoom_value.setAlignment(Qt.AlignmentFlag.AlignCenter)
zoom_layout.addWidget(zoom_label)
zoom_layout.addWidget(self.zoom_slider)
zoom_layout.addWidget(zoom_value)
# Connect zoom slider
self.zoom_slider.valueChanged.connect(lambda v: (
zoom_value.setText(f"{v}%"),
setattr(self, 'zoom_level', v),
self.update_timeline_zoom()
))
# Snap and magnetism controls
snap_group = QWidget()
snap_layout = QHBoxLayout(snap_group)
snap_layout.setContentsMargins(0, 0, 0, 0)
snap_layout.setSpacing(6)
self.snap_btn = QPushButton("🧲")
self.snap_btn.setCheckable(True)
self.snap_btn.setChecked(True)
self.snap_btn.setToolTip("Snap to clips and markers")
self.snap_btn.setFixedSize(28, 22)
self.magnet_btn = QPushButton("")
self.magnet_btn.setCheckable(True)
self.magnet_btn.setToolTip("Magnetic timeline")
self.magnet_btn.setFixedSize(28, 22)
for btn in [self.snap_btn, self.magnet_btn]:
btn.setStyleSheet("""
QPushButton {
background: #555555;
border: 1px solid #666666;
border-radius: 3px;
color: white;
font-size: 12px;
}
QPushButton:checked {
background: #0078d4;
border-color: #005a9e;
}
QPushButton:hover {
background: #666666;
}
""")
snap_layout.addWidget(self.snap_btn)
snap_layout.addWidget(self.magnet_btn)
# Track visibility and lock controls
track_controls = QWidget()
track_layout = QHBoxLayout(track_controls)
track_layout.setContentsMargins(0, 0, 0, 0)
track_layout.setSpacing(4)
track_height_label = QLabel("Height:")
track_height_label.setStyleSheet("color: #cccccc; font-size: 11px; font-weight: bold;")
self.track_height_combo = QComboBox()
self.track_height_combo.addItems(["Tiny", "Small", "Medium", "Large", "Extra Large"])
self.track_height_combo.setCurrentText("Medium")
self.track_height_combo.setFixedWidth(90)
self.track_height_combo.setStyleSheet("""
QComboBox {
background: #555555;
border: 1px solid #666666;
border-radius: 3px;
padding: 2px 8px;
color: white;
font-size: 11px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
image: none;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #cccccc;
}
""")
track_layout.addWidget(track_height_label)
track_layout.addWidget(self.track_height_combo)
# Playhead controls
playhead_group = QWidget()
playhead_layout = QHBoxLayout(playhead_group)
playhead_layout.setContentsMargins(0, 0, 0, 0)
playhead_layout.setSpacing(4)
frame_label = QLabel("Frame:")
frame_label.setStyleSheet("color: #cccccc; font-size: 11px; font-weight: bold;")
self.frame_spinbox = QSpinBox()
self.frame_spinbox.setRange(0, self.total_frames)
self.frame_spinbox.setValue(0)
self.frame_spinbox.setFixedWidth(80)
self.frame_spinbox.setStyleSheet("""
QSpinBox {
background: #555555;
border: 1px solid #666666;
border-radius: 3px;
padding: 2px;
color: white;
font-size: 11px;
}
""")
playhead_layout.addWidget(frame_label)
playhead_layout.addWidget(self.frame_spinbox)
# Assemble toolbar
layout.addWidget(zoom_group)
layout.addItem(QSpacerItem(10, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum))
layout.addWidget(snap_group)
layout.addItem(QSpacerItem(10, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum))
layout.addWidget(track_controls)
layout.addItem(QSpacerItem(20, 1, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
layout.addWidget(playhead_group)
return toolbar
def create_timecode_display(self):
"""Create professional timecode display."""
timecode_widget = QWidget()
timecode_widget.setFixedHeight(40)
timecode_widget.setStyleSheet("""
QWidget {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #1a1a1a, stop:1 #0d0d0d);
border-top: 1px solid #333333;
border-bottom: 1px solid #333333;
}
""")
layout = QHBoxLayout(timecode_widget)
layout.setContentsMargins(10, 0, 10, 0)
# Current timecode
self.timecode_label = QLabel("00:00:00:00")
self.timecode_label.setStyleSheet("""
QLabel {
color: #00ff41;
font-family: 'Courier New', monospace;
font-size: 18px;
font-weight: bold;
background: #1a1a1a;
padding: 4px 8px;
border: 1px solid #333333;
border-radius: 3px;
}
""")
# Duration display
duration_label = QLabel("Duration: 00:05:00:00")
duration_label.setStyleSheet("""
QLabel {
color: #cccccc;
font-family: 'Courier New', monospace;
font-size: 12px;
background: transparent;
}
""")
layout.addWidget(self.timecode_label)
layout.addStretch()
layout.addWidget(duration_label)
return timecode_widget
def create_time_ruler(self):
"""Create professional time ruler with frame markers."""
ruler_widget = QWidget()
ruler_widget.setFixedHeight(25)
ruler_widget.setStyleSheet("""
QWidget {
background: #2a2a2a;
border-bottom: 1px solid #555555;
}
""")
# This would normally have custom painting for time markers
# For now, just a placeholder
layout = QHBoxLayout(ruler_widget)
layout.setContentsMargins(80, 0, 0, 0) # Align with tracks
return ruler_widget
def create_timeline_tracks(self):
"""Create enhanced timeline tracks with professional styling."""
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Track headers (left side)
headers_widget = self.create_track_headers()
layout.addWidget(headers_widget)
# Timeline area (right side) with scroll
timeline_scroll = QScrollArea()
timeline_scroll.setWidgetResizable(True)
timeline_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
timeline_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
timeline_scroll.setStyleSheet("""
QScrollArea {
border: none;
background: #1a1a1a;
}
QScrollBar:horizontal {
background: #333333;
height: 12px;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background: #666666;
border-radius: 6px;
min-width: 20px;
}
""")
# Create wide timeline content for horizontal scrolling
timeline_content = QWidget()
timeline_content.setFixedSize(4000, 400) # Wide for scrolling
timeline_content.setStyleSheet("background: #1a1a1a;")
timeline_scroll.setWidget(timeline_content)
layout.addWidget(timeline_scroll, 1)
return container
def create_track_headers(self):
"""Create professional track headers with controls."""
headers_widget = QWidget()
headers_widget.setFixedWidth(80)
headers_widget.setStyleSheet("""
QWidget {
background: #2d2d2d;
border-right: 1px solid #555555;
}
""")
layout = QVBoxLayout(headers_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(1)
# Track configurations
tracks = [
("V4", "#4a90e2", 50),
("V3", "#5ba0f2", 50),
("V2", "#6bb0ff", 50),
("V1", "#7bc0ff", 50),
("A4", "#e24a4a", 35),
("A3", "#f25b5b", 35),
("A2", "#ff6b6b", 35),
("A1", "#ff7b7b", 35),
]
for name, color, height in tracks:
track_header = self.create_single_track_header(name, color, height)
layout.addWidget(track_header)
layout.addStretch()
return headers_widget
def create_single_track_header(self, name, color, height):
"""Create individual track header with controls."""
header = QWidget()
header.setFixedHeight(height)
header.setStyleSheet(f"""
QWidget {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {color}, stop:1 #2d2d2d);
border-bottom: 1px solid #555555;
}}
""")
layout = QHBoxLayout(header)
layout.setContentsMargins(4, 2, 4, 2)
layout.setSpacing(2)
# Track name
name_label = QLabel(name)
name_label.setStyleSheet("""
QLabel {
color: white;
font-weight: bold;
font-size: 11px;
background: transparent;
}
""")
# Visibility toggle
vis_btn = QPushButton("👁")
vis_btn.setCheckable(True)
vis_btn.setChecked(True)
vis_btn.setFixedSize(16, 16)
vis_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
font-size: 10px;
}
QPushButton:!checked {
color: #666666;
}
""")
# Lock toggle
lock_btn = QPushButton("🔒")
lock_btn.setCheckable(True)
lock_btn.setFixedSize(16, 16)
lock_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
font-size: 10px;
}
QPushButton:checked {
color: #ff6b6b;
}
""")
layout.addWidget(name_label)
layout.addStretch()
layout.addWidget(vis_btn)
layout.addWidget(lock_btn)
return header
def update_timeline_zoom(self):
"""Update timeline zoom level."""
# This would update the timeline scale
pass
def update_timecode(self, frame):
"""Update timecode display."""
hours = frame // (self.fps * 3600)
minutes = (frame % (self.fps * 3600)) // (self.fps * 60)
seconds = (frame % (self.fps * 60)) // self.fps
frames = frame % self.fps
timecode = f"{hours:02d}:{minutes:02d}:{seconds:02d}:{frames:02d}"
self.timecode_label.setText(timecode)
self.timecode_changed.emit(timecode)
class ProfessionalVideoEditor(QMainWindow):
"""Enhanced video editor with professional Adobe Premiere-like interface."""
def __init__(self):
super().__init__()
self.setWindowTitle("Professional Video Editor - Enhanced Prototype")
self.setMinimumSize(1400, 900)
self.setup_ui()
self.setup_dark_theme()
self.setup_menus()
self.setup_status_bar()
def setup_ui(self):
"""Setup the main user interface."""
# Create central splitter
main_splitter = QSplitter(Qt.Orientation.Horizontal)
self.setCentralWidget(main_splitter)
# Left panel with project browser and effects
left_panel = self.create_left_panel()
main_splitter.addWidget(left_panel)
# Right panel with monitors and timeline
right_panel = self.create_right_panel()
main_splitter.addWidget(right_panel)
# Set splitter proportions
main_splitter.setStretchFactor(0, 1)
main_splitter.setStretchFactor(1, 4)
def create_left_panel(self):
"""Create enhanced left panel with dockable widgets."""
left_widget = QWidget()
left_widget.setFixedWidth(300)
layout = QVBoxLayout(left_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Create tabbed interface
tabs = QTabWidget()
tabs.setTabPosition(QTabWidget.TabPosition.North)
# Project panel
project_panel = self.create_project_panel()
tabs.addTab(project_panel, "📁 Project")
# Effects panel
effects_panel = self.create_effects_panel()
tabs.addTab(effects_panel, "✨ Effects")
# Audio panel
audio_panel = self.create_audio_panel()
tabs.addTab(audio_panel, "🎵 Audio")
layout.addWidget(tabs)
return left_widget
def create_project_panel(self):
"""Create enhanced project panel."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(5, 5, 5, 5)
# Search bar
search_bar = QLineEdit()
search_bar.setPlaceholderText("🔍 Search media...")
search_bar.setStyleSheet("""
QLineEdit {
padding: 6px;
border: 1px solid #555555;
border-radius: 3px;
background: #333333;
color: white;
}
""")
# Project tree
project_tree = QTreeWidget()
project_tree.setHeaderLabel("Media Browser")
project_tree.setStyleSheet("""
QTreeWidget {
background: #2d2d2d;
border: 1px solid #555555;
color: white;
font-size: 12px;
}
QTreeWidget::item:selected {
background: #0078d4;
}
""")
# Add sample content
self.populate_project_tree(project_tree)
layout.addWidget(search_bar)
layout.addWidget(project_tree)
return widget
def populate_project_tree(self, tree):
"""Populate project tree with sample content."""
# Video folder
video_folder = QTreeWidgetItem(tree, ["📹 Video Files"])
video_folder.setExpanded(True)
QTreeWidgetItem(video_folder, ["🎬 interview_01.mp4"])
QTreeWidgetItem(video_folder, ["🎬 broll_nature.mov"])
QTreeWidgetItem(video_folder, ["🎬 talking_head.avi"])
QTreeWidgetItem(video_folder, ["🎬 product_demo.mkv"])
# Audio folder
audio_folder = QTreeWidgetItem(tree, ["🎵 Audio Files"])
audio_folder.setExpanded(True)
QTreeWidgetItem(audio_folder, ["🎼 background_music.mp3"])
QTreeWidgetItem(audio_folder, ["🎙️ voiceover.wav"])
QTreeWidgetItem(audio_folder, ["🔊 sound_effects.aiff"])
# Graphics folder
graphics_folder = QTreeWidgetItem(tree, ["🖼️ Graphics"])
QTreeWidgetItem(graphics_folder, ["🏷️ lower_third.png"])
QTreeWidgetItem(graphics_folder, ["📊 chart_template.psd"])
# Sequences folder
sequences_folder = QTreeWidgetItem(tree, ["📽️ Sequences"])
QTreeWidgetItem(sequences_folder, ["✂️ Main_Edit_v01"])
QTreeWidgetItem(sequences_folder, ["✂️ Rough_Cut"])
def create_effects_panel(self):
"""Create effects and transitions panel with shared Effect Controls box below."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(8)
# Top section: Effects and Transitions tabs
effects_tabs = QTabWidget()
effects_tabs.setTabPosition(QTabWidget.TabPosition.North)
effects_tabs.setMaximumHeight(200) # Limit height to leave space for controls
# Effects tab - contains effect options/categories
video_effects = QListWidget()
effects_list = [
"🌈 Color Correction",
"🔄 Transform",
"⚡ Blur & Sharpen",
"🎨 Stylize",
"📐 Distort",
"🎭 Keying",
"⏰ Time",
"🔊 Audio Effects"
]
video_effects.addItems(effects_list)
video_effects.setStyleSheet("""
QListWidget {
background: #2d2d2d;
border: 1px solid #555555;
color: white;
font-size: 11px;
}
QListWidget::item {
padding: 8px 12px;
border-bottom: 1px solid #444444;
}
QListWidget::item:selected {
background: #0078d4;
}
QListWidget::item:hover:!selected {
background: #404040;
}
""")
# Connect effect selection to update controls below
video_effects.currentRowChanged.connect(self.on_effect_selection_changed)
# Transitions tab
transitions = QListWidget()
transition_list = [
"🔄 Cross Dissolve",
"⬛ Fade to Black",
"⬜ Fade to White",
"📱 Push",
"🌀 Spin",
"🔍 Zoom",
"📐 Wipe",
"✨ Morph"
]
transitions.addItems(transition_list)
transitions.setStyleSheet(video_effects.styleSheet())
# Connect transition selection to update controls below
transitions.currentRowChanged.connect(self.on_transition_selection_changed)
# Add tabs
effects_tabs.addTab(video_effects, "Effects")
effects_tabs.addTab(transitions, "Transitions")
# Bottom section: Effect Controls box
controls_group = QGroupBox("Effect Controls")
controls_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #555555;
border-radius: 5px;
margin-top: 10px;
color: white;
background: #2d2d2d;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
# Create the effect controls content (parameters only)
effect_controls_content = self.create_effect_controls_content()
controls_layout = QVBoxLayout(controls_group)
controls_layout.addWidget(effect_controls_content)
# Store reference to tabs for selection handling
self.effects_tabs_widget = effects_tabs
# Assemble the complete panel
layout.addWidget(effects_tabs)
layout.addWidget(controls_group, 1) # Give more space to controls
return widget
def create_effect_controls_content(self):
"""Create the Effect Controls content with parameter controls only."""
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(10, 10, 10, 10)
# Header showing current effect/transition
self.current_control_label = QLabel("Select an effect or transition to adjust its parameters")
self.current_control_label.setStyleSheet("""
QLabel {
color: #cccccc;
font-size: 12px;
font-weight: bold;
background: #404040;
padding: 8px;
border-radius: 4px;
border: 1px solid #555555;
}
""")
layout.addWidget(self.current_control_label)
# Parameter controls area (no selection list, just controls)
self.parameters_stack = QStackedWidget()
self.parameters_stack.setStyleSheet("""
QStackedWidget {
background: #333333;
border: 1px solid #555555;
border-radius: 4px;
}
""")
# Create parameter widgets for different types of effects
self.param_widgets = {}
self.param_widgets["Brightness & Contrast"] = self._create_brightness_contrast_widget()
self.param_widgets["Color Balance"] = self._create_color_balance_widget()
self.param_widgets["Volume"] = self._create_volume_widget()
self.param_widgets["Speed"] = self._create_speed_widget()
self.param_widgets["Transition"] = self._create_transition_widget()
# Add parameter widgets to stack
for widget in self.param_widgets.values():
self.parameters_stack.addWidget(widget)
# Default message widget
default_widget = QWidget()
default_layout = QVBoxLayout(default_widget)
default_layout.addStretch()
default_label = QLabel("👆 Select an effect or transition above\nto see its controls here")
default_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
default_label.setStyleSheet("""
QLabel {
color: #888888;
font-size: 14px;
line-height: 1.5;
}
""")
default_layout.addWidget(default_label)
default_layout.addStretch()
self.parameters_stack.addWidget(default_widget)
# Set default to the message widget
self.parameters_stack.setCurrentWidget(default_widget)
layout.addWidget(self.parameters_stack, 1)
return container
def on_effect_selection_changed(self, index: int):
"""Handle effect selection from Effects tab."""
effects_list_widget = self.effects_tabs_widget.widget(0) # Effects tab
if isinstance(effects_list_widget, QListWidget):
item = effects_list_widget.item(index)
if item:
effect_name = item.text()
self.current_control_label.setText(f"Effect Controls: {effect_name}")
# Map effect to appropriate control widget
effect_mappings = {
"🌈 Color Correction": "Brightness & Contrast",
"🔄 Transform": "Speed",
"⚡ Blur & Sharpen": "Brightness & Contrast",
"🎨 Stylize": "Color Balance",
"📐 Distort": "Speed",
"🎭 Keying": "Color Balance",
"⏰ Time": "Speed",
"🔊 Audio Effects": "Volume"
}
control_type = effect_mappings.get(effect_name)
if control_type and control_type in self.param_widgets:
widget = self.param_widgets[control_type]
self.parameters_stack.setCurrentWidget(widget)
def on_transition_selection_changed(self, index: int):
"""Handle transition selection from Transitions tab."""
transitions_list_widget = self.effects_tabs_widget.widget(1) # Transitions tab
if isinstance(transitions_list_widget, QListWidget):
item = transitions_list_widget.item(index)
if item:
transition_name = item.text()
self.current_control_label.setText(f"Transition Controls: {transition_name}")
# All transitions use the transition control widget
widget = self.param_widgets["Transition"]
self.parameters_stack.setCurrentWidget(widget)
def _create_transition_widget(self) -> QWidget:
"""Create Transition control panel."""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(12)
# Duration slider
duration_slider = QSlider(Qt.Orientation.Horizontal)
duration_slider.setMinimum(1)
duration_slider.setMaximum(120) # 1-120 frames
duration_slider.setValue(30) # Default 30 frames
duration_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Duration (frames):", duration_slider)
# Ease In/Out controls
ease_in_slider = QSlider(Qt.Orientation.Horizontal)
ease_in_slider.setMinimum(0)
ease_in_slider.setMaximum(100)
ease_in_slider.setValue(25)
ease_in_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Ease In:", ease_in_slider)
ease_out_slider = QSlider(Qt.Orientation.Horizontal)
ease_out_slider.setMinimum(0)
ease_out_slider.setMaximum(100)
ease_out_slider.setValue(25)
ease_out_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Ease Out:", ease_out_slider)
return widget
def _create_brightness_contrast_widget(self) -> QWidget:
"""Create Brightness & Contrast control panel."""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(12)
# Brightness slider
bright_slider = QSlider(Qt.Orientation.Horizontal)
bright_slider.setMinimum(0)
bright_slider.setMaximum(100)
bright_slider.setValue(50)
bright_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Brightness:", bright_slider)
# Contrast slider
contrast_slider = QSlider(Qt.Orientation.Horizontal)
contrast_slider.setMinimum(0)
contrast_slider.setMaximum(100)
contrast_slider.setValue(50)
contrast_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Contrast:", contrast_slider)
return widget
def _create_color_balance_widget(self) -> QWidget:
"""Create Color Balance control panel."""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(12)
# Red channel
red_spin = QSpinBox()
red_spin.setRange(-100, 100)
red_spin.setValue(0)
red_spin.setStyleSheet(self._get_spinbox_style())
layout.addRow("Red:", red_spin)
# Green channel
green_spin = QSpinBox()
green_spin.setRange(-100, 100)
green_spin.setValue(0)
green_spin.setStyleSheet(self._get_spinbox_style())
layout.addRow("Green:", green_spin)
# Blue channel
blue_spin = QSpinBox()
blue_spin.setRange(-100, 100)
blue_spin.setValue(0)
blue_spin.setStyleSheet(self._get_spinbox_style())
layout.addRow("Blue:", blue_spin)
return widget
def _create_volume_widget(self) -> QWidget:
"""Create Volume control panel."""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(12)
# Volume slider
vol_slider = QSlider(Qt.Orientation.Horizontal)
vol_slider.setRange(0, 100)
vol_slider.setValue(100)
vol_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Volume:", vol_slider)
# Pan slider
pan_slider = QSlider(Qt.Orientation.Horizontal)
pan_slider.setRange(-100, 100)
pan_slider.setValue(0)
pan_slider.setStyleSheet(self._get_slider_style())
layout.addRow("Pan:", pan_slider)
return widget
def _create_speed_widget(self) -> QWidget:
"""Create Speed control panel."""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(12)
# Speed spinbox
speed_spin = QSpinBox()
speed_spin.setRange(10, 500)
speed_spin.setValue(100)
speed_spin.setSuffix("%")
speed_spin.setStyleSheet(self._get_spinbox_style())
layout.addRow("Speed:", speed_spin)
return widget
def _get_slider_style(self) -> str:
"""Return consistent slider styling."""
return """
QSlider::groove:horizontal {
border: 1px solid #555555;
height: 6px;
background: #404040;
border-radius: 3px;
}
QSlider::handle:horizontal {
background: #0078d4;
border: 2px solid #0078d4;
width: 16px;
border-radius: 8px;
margin: -5px 0;
}
QSlider::handle:horizontal:hover {
background: #106ebe;
border-color: #106ebe;
}
"""
def _get_spinbox_style(self) -> str:
"""Return consistent spinbox styling."""
return """
QSpinBox {
background: #404040;
border: 1px solid #555555;
border-radius: 3px;
padding: 4px;
color: white;
font-size: 11px;
min-width: 70px;
}
QSpinBox:focus {
border-color: #0078d4;
}
QSpinBox::up-button, QSpinBox::down-button {
background: #555555;
border: none;
width: 16px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background: #666666;
}
"""
def create_audio_panel(self):
"""Create audio mixing panel."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(5, 5, 5, 5)
# Audio mixer
mixer_group = QGroupBox("Audio Mixer")
mixer_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #555555;
border-radius: 5px;
margin-top: 10px;
color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
mixer_layout = QGridLayout(mixer_group)
# Create 4 audio channel strips
for i in range(4):
channel_widget = self.create_audio_channel(f"A{i+1}")
mixer_layout.addWidget(channel_widget, 0, i)
layout.addWidget(mixer_group)
layout.addStretch()
return widget
def create_audio_channel(self, name):
"""Create individual audio channel strip."""
channel = QWidget()
channel.setFixedWidth(60)
layout = QVBoxLayout(channel)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
# Channel label
label = QLabel(name)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("color: white; font-size: 10px; font-weight: bold;")
# Volume fader
volume_slider = QSlider(Qt.Orientation.Vertical)
volume_slider.setRange(-60, 12)
volume_slider.setValue(0)
volume_slider.setFixedHeight(100)
volume_slider.setStyleSheet("""
QSlider::groove:vertical {
background: #333333;
width: 6px;
border-radius: 3px;
}
QSlider::handle:vertical {
background: #0078d4;
height: 12px;
border-radius: 6px;
margin: 0 -3px;
}
""")
# Mute button
mute_btn = QPushButton("M")
mute_btn.setCheckable(True)
mute_btn.setFixedSize(20, 20)
mute_btn.setStyleSheet("""
QPushButton {
background: #555555;
border: 1px solid #666666;
border-radius: 3px;
color: white;
font-size: 10px;
font-weight: bold;
}
QPushButton:checked {
background: #ff6b6b;
}
""")
# Solo button
solo_btn = QPushButton("S")
solo_btn.setCheckable(True)
solo_btn.setFixedSize(20, 20)
solo_btn.setStyleSheet(mute_btn.styleSheet().replace("#ff6b6b", "#ffaa00"))
layout.addWidget(label)
layout.addWidget(volume_slider)
layout.addWidget(mute_btn)
layout.addWidget(solo_btn)
return channel
def create_right_panel(self):
"""Create right panel with monitors and timeline."""
right_splitter = QSplitter(Qt.Orientation.Vertical)
# Top: Monitor panels
monitors_widget = self.create_monitor_panels()
right_splitter.addWidget(monitors_widget)
# Bottom: Timeline
timeline_widget = EnhancedTimelineWidget()
right_splitter.addWidget(timeline_widget)
# Set proportions
right_splitter.setStretchFactor(0, 2)
right_splitter.setStretchFactor(1, 1)
return right_splitter
def create_monitor_panels(self):
"""Create enhanced monitor panels."""
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(8)
# Source monitor
source_monitor = self.create_monitor("Source Monitor", "#1a1a2e")
layout.addWidget(source_monitor)
# Program monitor
program_monitor = self.create_monitor("Program Monitor", "#1a2e1a")
layout.addWidget(program_monitor)
# Audio meters
audio_meters = self.create_audio_meters()
layout.addWidget(audio_meters)
return container
def create_monitor(self, title, accent_color):
"""Create individual monitor with controls."""
monitor = QFrame()
monitor.setFrameShape(QFrame.Shape.Box)
monitor.setStyleSheet(f"""
QFrame {{
background: black;
border: 2px solid {accent_color};
border-radius: 5px;
}}
""")
layout = QVBoxLayout(monitor)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
# Monitor title
title_label = QLabel(title)
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet(f"""
QLabel {{
color: white;
font-weight: bold;
font-size: 12px;
background: {accent_color};
padding: 2px;
border-radius: 3px;
}}
""")
# Video display area
video_area = QLabel("📺 Video Preview")
video_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
video_area.setStyleSheet("""
QLabel {
color: #666666;
font-size: 24px;
background: #0a0a0a;
border: 1px dashed #333333;
border-radius: 3px;
}
""")
# Monitor controls
controls = QWidget()
controls_layout = QHBoxLayout(controls)
controls_layout.setContentsMargins(0, 0, 0, 0)
# Play/pause button
play_btn = QPushButton("▶️")
play_btn.setFixedSize(25, 20)
# Frame navigation
prev_btn = QPushButton("⏮️")
prev_btn.setFixedSize(25, 20)
next_btn = QPushButton("⏭️")
next_btn.setFixedSize(25, 20)
for btn in [play_btn, prev_btn, next_btn]:
btn.setStyleSheet("""
QPushButton {
background: #333333;
border: 1px solid #555555;
border-radius: 3px;
color: white;
font-size: 10px;
}
QPushButton:hover {
background: #444444;
}
""")
controls_layout.addWidget(prev_btn)
controls_layout.addWidget(play_btn)
controls_layout.addWidget(next_btn)
controls_layout.addStretch()
layout.addWidget(title_label)
layout.addWidget(video_area, 1)
layout.addWidget(controls)
return monitor
def create_audio_meters(self):
"""Create professional audio level meters."""
meters_widget = QWidget()
meters_widget.setFixedWidth(60)
layout = QVBoxLayout(meters_widget)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(1)
# Title
title = QLabel("Audio Levels")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("""
QLabel {
color: white;
font-size: 10px;
font-weight: bold;
background: #2a2a2a;
padding: 2px;
border-radius: 3px;
}
""")
# Create stereo meters
meters_container = QWidget()
meters_layout = QHBoxLayout(meters_container)
meters_layout.setContentsMargins(0, 0, 0, 0)
meters_layout.setSpacing(2)
for channel in ["L", "R"]:
channel_widget = QWidget()
channel_layout = QVBoxLayout(channel_widget)
channel_layout.setContentsMargins(0, 0, 0, 0)
channel_layout.setSpacing(1)
# Channel label
ch_label = QLabel(channel)
ch_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
ch_label.setStyleSheet("color: white; font-size: 8px;")
# Level meter
meter = QProgressBar()
meter.setOrientation(Qt.Orientation.Vertical)
meter.setRange(-60, 0)
meter.setValue(-20)
meter.setTextVisible(False)
meter.setFixedWidth(12)
meter.setStyleSheet("""
QProgressBar {
background: #1a1a1a;
border: 1px solid #333333;
border-radius: 2px;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:1, x2:0, y2:0,
stop:0 #00ff00, stop:0.7 #ffff00, stop:1 #ff0000);
border-radius: 1px;
}
""")
channel_layout.addWidget(ch_label)
channel_layout.addWidget(meter)
meters_layout.addWidget(channel_widget)
layout.addWidget(title)
layout.addWidget(meters_container, 1)
return meters_widget
def setup_menus(self):
"""Setup professional menu system."""
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("File")
new_action = QAction("📄 New Project", self)
new_action.setShortcut(QKeySequence.StandardKey.New)
new_action.triggered.connect(self.new_project)
file_menu.addAction(new_action)
open_action = QAction("📂 Open Project", self)
open_action.setShortcut(QKeySequence.StandardKey.Open)
open_action.triggered.connect(self.open_project)
file_menu.addAction(open_action)
file_menu.addSeparator()
save_action = QAction("💾 Save Project", self)
save_action.setShortcut(QKeySequence.StandardKey.Save)
save_action.triggered.connect(self.save_project)
file_menu.addAction(save_action)
save_as_action = QAction("💾 Save As...", self)
save_as_action.setShortcut(QKeySequence.StandardKey.SaveAs)
save_as_action.triggered.connect(self.save_as_project)
file_menu.addAction(save_as_action)
file_menu.addSeparator()
import_action = QAction("📥 Import Media", self)
import_action.setShortcut(QKeySequence("Ctrl+I"))
import_action.triggered.connect(self.import_media)
file_menu.addAction(import_action)
export_action = QAction("📤 Export Media", self)
export_action.setShortcut(QKeySequence("Ctrl+M"))
export_action.triggered.connect(self.export_media)
file_menu.addAction(export_action)
# Edit menu
edit_menu = menubar.addMenu("Edit")
undo_action = QAction("↶ Undo", self)
undo_action.setShortcut(QKeySequence.StandardKey.Undo)
undo_action.triggered.connect(self.undo)
edit_menu.addAction(undo_action)
redo_action = QAction("↷ Redo", self)
redo_action.setShortcut(QKeySequence.StandardKey.Redo)
redo_action.triggered.connect(self.redo)
edit_menu.addAction(redo_action)
edit_menu.addSeparator()
cut_action = QAction("✂️ Cut", self)
cut_action.setShortcut(QKeySequence.StandardKey.Cut)
cut_action.triggered.connect(self.cut)
edit_menu.addAction(cut_action)
copy_action = QAction("📋 Copy", self)
copy_action.setShortcut(QKeySequence.StandardKey.Copy)
copy_action.triggered.connect(self.copy)
edit_menu.addAction(copy_action)
paste_action = QAction("📌 Paste", self)
paste_action.setShortcut(QKeySequence.StandardKey.Paste)
paste_action.triggered.connect(self.paste)
edit_menu.addAction(paste_action)
# Window menu
window_menu = menubar.addMenu("Window")
reset_action = QAction("🔄 Reset Layout", self)
reset_action.triggered.connect(self.reset_layout)
window_menu.addAction(reset_action)
window_menu.addSeparator()
project_action = QAction("📁 Project Panel", self)
project_action.triggered.connect(lambda: self.toggle_panel("project"))
window_menu.addAction(project_action)
effects_action = QAction("✨ Effects Panel", self)
effects_action.triggered.connect(lambda: self.toggle_panel("effects"))
window_menu.addAction(effects_action)
audio_action = QAction("🎵 Audio Mixer", self)
audio_action.triggered.connect(lambda: self.toggle_panel("audio"))
window_menu.addAction(audio_action)
def setup_status_bar(self):
"""Setup professional status bar."""
status = self.statusBar()
status.setStyleSheet("""
QStatusBar {
background: #2a2a2a;
border-top: 1px solid #555555;
color: white;
font-size: 11px;
}
""")
status.showMessage("Ready - Professional Video Editor Prototype")
def setup_dark_theme(self):
"""Apply comprehensive dark theme."""
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QWidget {
background-color: #1e1e1e;
color: #ffffff;
}
QTabWidget::pane {
border: 1px solid #404040;
background-color: #2d2d2d;
border-radius: 3px;
}
QTabBar::tab {
background-color: #404040;
color: #ffffff;
padding: 8px 16px;
border: none;
margin-right: 1px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
QTabBar::tab:selected {
background-color: #0078d4;
}
QTabBar::tab:hover:!selected {
background-color: #555555;
}
QSplitter::handle {
background-color: #404040;
width: 3px;
height: 3px;
}
QSplitter::handle:hover {
background-color: #0078d4;
}
QMenuBar {
background-color: #2d2d2d;
color: white;
border-bottom: 1px solid #404040;
}
QMenuBar::item:selected {
background-color: #0078d4;
}
QMenu {
background-color: #2d2d2d;
color: white;
border: 1px solid #404040;
}
QMenu::item:selected {
background-color: #0078d4;
}
""")
# Menu action handlers
def new_project(self): pass
def open_project(self): pass
def save_project(self): pass
def save_as_project(self): pass
def import_media(self): pass
def export_media(self): pass
def undo(self): pass
def redo(self): pass
def cut(self): pass
def copy(self): pass
def paste(self): pass
def reset_layout(self): pass
def toggle_panel(self, panel_name): pass
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ProfessionalVideoEditor()
window.show()
sys.exit(app.exec())