From 7f6b7b49013ae64139fcbad8b34671d6ff3e86ce Mon Sep 17 00:00:00 2001 From: klop51 Date: Sat, 16 Aug 2025 15:04:09 +0200 Subject: [PATCH] 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. --- video_editor_enhanced.py | 1431 ++++++++++++++++++++++++++++++++++++++ video_editor_pyqt6.py | 255 ++++++- 2 files changed, 1662 insertions(+), 24 deletions(-) create mode 100644 video_editor_enhanced.py diff --git a/video_editor_enhanced.py b/video_editor_enhanced.py new file mode 100644 index 0000000..7b0d8e3 --- /dev/null +++ b/video_editor_enhanced.py @@ -0,0 +1,1431 @@ +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()) diff --git a/video_editor_pyqt6.py b/video_editor_pyqt6.py index a261864..b3505b5 100644 --- a/video_editor_pyqt6.py +++ b/video_editor_pyqt6.py @@ -57,7 +57,8 @@ from PyQt6.QtWidgets import ( ) from PyQt6.QtCore import ( Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, - QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint + QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint, + QMetaObject, Q_ARG ) from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from PyQt6.QtMultimediaWidgets import QVideoWidget @@ -1295,8 +1296,9 @@ class ProfessionalVideoEditor(QMainWindow): video_layout = QVBoxLayout(video_frame) video_layout.setContentsMargins(5, 5, 5, 5) # Add some padding - # Adaptive video container that matches video aspect ratio + # Adaptive video container that properly constrains video scaling video_container = QWidget() + video_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) video_container.setStyleSheet(""" QWidget { background-color: #2d2d2d; @@ -1305,21 +1307,16 @@ class ProfessionalVideoEditor(QMainWindow): } """) video_container_layout = QVBoxLayout(video_container) - video_container_layout.setContentsMargins(0, 0, 0, 0) + video_container_layout.setContentsMargins(5, 5, 5, 5) # Small padding to ensure video stays inside video_container_layout.setSpacing(0) - # Adaptive video widget with intelligent sizing + # Create video widget with proper container-based scaling self.video_widget = QVideoWidget() - # Set default size for 16:9 aspect ratio (1080p scaled down) - default_width = 640 # Standard width for video player - default_height = int(default_width * 9 / 16) # 16:9 aspect ratio = 360 + # Set the video widget to scale with its container + self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.video_widget.setMinimumSize(default_width, default_height) - self.video_widget.setMaximumHeight(600) # Limit maximum height - self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - - # Keep aspect ratio to prevent distortion, but adapt container size + # Keep aspect ratio and fit within container bounds (won't overflow) self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio) self.video_widget.setStyleSheet(""" @@ -1330,11 +1327,8 @@ class ProfessionalVideoEditor(QMainWindow): } """) - # Add video widget with top alignment to push it up - video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignTop) - - # Add stretch to push video to top and leave space at bottom - video_container_layout.addStretch() + # Add video widget centered in container with proper alignment + video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignCenter) # Store references for dynamic sizing self.video_container = video_container @@ -1369,7 +1363,7 @@ class ProfessionalVideoEditor(QMainWindow): # Timeline and playback controls timeline_frame = QFrame() - timeline_frame.setFixedHeight(500) # Much larger to accommodate all tracks properly + timeline_frame.setFixedHeight(300) # Reduced height to fit all tracks without cutting off timeline_frame.setStyleSheet(""" QFrame { background-color: #1e1e1e; @@ -1568,9 +1562,48 @@ class ProfessionalVideoEditor(QMainWindow): print(" ESC: Exit fullscreen") def resizeEvent(self, event): - """Handle window resize""" + """Handle window resize and update video scaling""" super().resizeEvent(event) + # Force video widget to update its size to fit within container + if hasattr(self, 'video_widget') and hasattr(self, 'video_container'): + # Give the layout system time to update + QTimer.singleShot(10, self.update_video_size) + + def update_video_size(self): + """Update video widget size to fit within its container""" + try: + if hasattr(self, 'video_widget') and hasattr(self, 'video_container'): + # Get the container's available size (minus padding) + container_size = self.video_container.size() + available_width = container_size.width() - 10 # Account for padding + available_height = container_size.height() - 10 # Account for padding + + # Calculate the maximum size while maintaining 16:9 aspect ratio + aspect_ratio = 16.0 / 9.0 + + # Try fitting by width first + width_constrained_height = available_width / aspect_ratio + if width_constrained_height <= available_height: + # Width is the limiting factor + new_width = available_width + new_height = int(width_constrained_height) + else: + # Height is the limiting factor + new_height = available_height + new_width = int(available_height * aspect_ratio) + + # Ensure minimum size + new_width = max(new_width, 320) + new_height = max(new_height, 180) + + # Apply the calculated size using size hints instead of fixed size + self.video_widget.setMinimumSize(new_width, new_height) + self.video_widget.setMaximumSize(new_width, new_height) + + except Exception as e: + print(f"⚠️ Error updating video size: {e}") + def setup_styling(self): """Apply professional dark theme""" self.setStyleSheet(""" @@ -1748,6 +1781,7 @@ class ProfessionalVideoEditor(QMainWindow): # Export button export_btn = QPushButton("🎬 Export Video") + export_btn.setObjectName("export_btn") export_btn.clicked.connect(self.export_video) export_btn.setStyleSheet(""" QPushButton { @@ -1790,18 +1824,188 @@ class ProfessionalVideoEditor(QMainWindow): f"Volume changed to {self.volume_factor.value():.1f}x") def export_video(self): - """Export the edited video""" + """Export the edited video with progress tracking""" if not self.current_video: QMessageBox.warning(self, "No Video", "Please load a video first.") return - filename = self.export_filename.text() + # Timeline clips are optional - we can export just the main video + # or main video with timeline clips overlaid + print(f"🎬 Exporting video: {self.current_video}") + if hasattr(self.timeline_widget, 'timeline_clips') and self.timeline_widget.timeline_clips: + print(f"📊 With {len(self.timeline_widget.timeline_clips)} timeline clips") + else: + print("📊 Main video only (no timeline clips)") + + filename = self.export_filename.text().strip() + if not filename: + filename = "edited_video.mp4" if not filename.endswith('.mp4'): filename += '.mp4' - QMessageBox.information(self, "Export Started", - f"Video export started: {filename}\n" - f"Quality: {self.export_quality.currentText()}") + # Create output path in the shorts folder + output_path = os.path.join("shorts", "edited", filename) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # Show progress bar and disable export button + self.export_progress.setVisible(True) + self.export_progress.setValue(0) + export_btn = self.sender() + export_btn.setEnabled(False) + export_btn.setText("⏳ Exporting...") + + # Start export in a separate thread + from threading import Thread + import time + + def export_thread(): + try: + self.export_timeline_video(output_path) + except Exception as e: + print(f"❌ Export error: {e}") + QMetaObject.invokeMethod(self, "export_finished", + Qt.ConnectionType.QueuedConnection, + Q_ARG(bool, False), + Q_ARG(str, str(e))) + else: + QMetaObject.invokeMethod(self, "export_finished", + Qt.ConnectionType.QueuedConnection, + Q_ARG(bool, True), + Q_ARG(str, output_path)) + + Thread(target=export_thread, daemon=True).start() + + @pyqtSlot(bool, str) + def export_finished(self, success, message): + """Handle export completion""" + # Hide progress bar and re-enable export button + self.export_progress.setVisible(False) + export_btn = self.findChild(QPushButton, "export_btn") + if not export_btn: + # Find by text if objectName not set + for btn in self.findChildren(QPushButton): + if "Export" in btn.text(): + export_btn = btn + break + + if export_btn: + export_btn.setEnabled(True) + export_btn.setText("🎬 Export Video") + + if success: + QMessageBox.information(self, "Export Complete", + f"Video exported successfully!\n\nSaved to: {message}") + else: + QMessageBox.critical(self, "Export Failed", + f"Export failed: {message}") + + def export_timeline_video(self, output_path): + """Export video with main video as base track and timeline clips overlaid""" + import cv2 + import numpy as np + + try: + # Check if we have a main video loaded + if not self.current_video: + raise Exception("No main video loaded") + + # Get timeline clips (these will be overlays) + clips = self.timeline_widget.timeline_clips + + # Update progress + QMetaObject.invokeMethod(self.export_progress, "setValue", + Qt.ConnectionType.QueuedConnection, Q_ARG(int, 10)) + + # Open main video to get its properties and duration + main_video_path = self.current_video + if not os.path.exists(main_video_path): + raise Exception(f"Main video not found: {main_video_path}") + + main_cap = cv2.VideoCapture(main_video_path) + if not main_cap.isOpened(): + raise Exception(f"Could not open main video: {main_video_path}") + + # Get video properties from main video + fps = int(main_cap.get(cv2.CAP_PROP_FPS)) + width = int(main_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(main_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + total_frames = int(main_cap.get(cv2.CAP_PROP_FRAME_COUNT)) + main_duration = total_frames / fps + + print(f"🎬 Main video: {main_video_path}") + print(f"📊 Properties: {width}x{height} @ {fps}fps, Duration: {main_duration:.2f}s") + + # Update progress + QMetaObject.invokeMethod(self.export_progress, "setValue", + Qt.ConnectionType.QueuedConnection, Q_ARG(int, 20)) + + # Create video writer + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + if not out.isOpened(): + main_cap.release() + raise Exception(f"Could not create output video: {output_path}") + + print(f"🎯 Processing {total_frames} frames...") + + # Process each frame + frame_count = 0 + while True: + ret, main_frame = main_cap.read() + if not ret: + break + + current_time = frame_count / fps + + # Start with the main video frame + output_frame = main_frame.copy() + + # Overlay any timeline clips that are active at this time + for clip in clips: + if clip['start_time'] <= current_time < clip['start_time'] + clip['duration']: + if clip['type'] == 'video': + # Calculate position in the clip + clip_relative_time = current_time - clip['start_time'] + + # Open clip video and seek to the right frame + clip_cap = cv2.VideoCapture(clip['filename']) + clip_cap.set(cv2.CAP_PROP_POS_MSEC, clip_relative_time * 1000) + clip_ret, clip_frame = clip_cap.read() + clip_cap.release() + + if clip_ret and clip_frame is not None: + # Resize clip frame to match main video + if clip_frame.shape[1] != width or clip_frame.shape[0] != height: + clip_frame = cv2.resize(clip_frame, (width, height)) + + # For now, replace the frame (in future could blend/overlay) + output_frame = clip_frame + + # Write the final frame + out.write(output_frame) + frame_count += 1 + + # Update progress periodically + if frame_count % 100 == 0: + progress = 30 + int((frame_count / total_frames) * 60) + QMetaObject.invokeMethod(self.export_progress, "setValue", + Qt.ConnectionType.QueuedConnection, Q_ARG(int, min(progress, 90))) + + # Cleanup + main_cap.release() + out.release() + + # Final progress update + QMetaObject.invokeMethod(self.export_progress, "setValue", + Qt.ConnectionType.QueuedConnection, Q_ARG(int, 100)) + + print(f"✅ Export completed: {output_path}") + print(f"📊 Processed {frame_count} frames") + + except Exception as e: + print(f"❌ Export error: {e}") + raise def setup_styling(self): """Apply professional dark theme""" @@ -1924,6 +2128,9 @@ class ProfessionalVideoEditor(QMainWindow): self.play_pause_btn.setEnabled(True) self.timeline_fullscreen_btn.setEnabled(True) + # Update video size to fit container after loading + QTimer.singleShot(100, self.update_video_size) + self.statusBar().showMessage(f"✅ Loaded: {os.path.basename(file_path)}") print(f"✅ Loaded video: {os.path.basename(file_path)}")