diff --git a/docs/index.rst b/docs/index.rst index 49aa1ac..e2d7c21 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Welcome to ManimStudio's documentation! src/core/settings src/core/project_creation_dialog src/core/project_opening_dialog + src/core/videoeditor src/utils/logger_utility src/ui/welcome_ui src/ui/settings_ui diff --git a/docs/src/core/videoeditor.rst b/docs/src/core/videoeditor.rst new file mode 100644 index 0000000..ba9b812 --- /dev/null +++ b/docs/src/core/videoeditor.rst @@ -0,0 +1,7 @@ +videoeditor module +================== + +.. automodule:: videoeditor + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/src/core/main.py b/src/core/main.py index 3d8d308..581c7f7 100644 --- a/src/core/main.py +++ b/src/core/main.py @@ -1,18 +1,14 @@ from datetime import datetime import sys -import json from pathlib import Path -from typing import Optional, Dict, List -from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QDialog, QHeaderView -from PySide6.QtCore import Signal, Qt +from typing import Optional, Dict +from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QHeaderView +from PySide6.QtCore import Signal from PySide6.QtGui import ( - QPixmap, QResizeEvent, - QAction, QStandardItemModel, QStandardItem, ) -from PySide6.QtWidgets import QSizePolicy # Add the parent directory of 'src' to sys.path @@ -22,10 +18,9 @@ # UI imports -from src.ui.settings_ui import Ui_Form # noqa: E402 -from src.ui.videoeditor_ui import Ui_Form as Ui_VideoEditor # noqa: E402 from src.ui.welcome_ui import Ui_MainWindow # noqa: E402 + # Core imports from src.core.project_creation_dialog import ProjectCreationDialog # noqa: E402 from src.core.project_opening_dialog import ProjectOpeningDialog # noqa: E402 @@ -33,9 +28,11 @@ load_settings, load_themes, load_current_theme, - update_settings, get_recent_project_paths, + load_ui, + update_image, ) +from src.core.videoeditor import VideoEditorWindow # noqa: E402 # Utils imports from src.utils.logger_utility import logger # noqa: E402 @@ -56,38 +53,28 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.themes: Dict = load_themes() self.current_theme: Dict = load_current_theme() logger.info("Main window initialized") - self.load_ui() + self.ui = load_ui( + main_window=self, + ui=Ui_MainWindow(), + settings=self.settings, + current_theme=self.current_theme, + themes=self.themes, + ) + # Populate the recent projects list with the 10 latest projects + self.populate_recent_projects() + self.ui.recentProjectsTableView.doubleClicked.connect( + lambda: self.open_project_from_list( + self.ui.recentProjectsTableView.selectedIndexes()[0] + ) + ) @logger.catch def resizeEvent(self, event: QResizeEvent) -> None: """Resize event for the main window""" super().resizeEvent(event) - self.update_image() + update_image(ui=self.ui, current_theme=self.current_theme) self.update_table_view() - @logger.catch - def update_image(self) -> None: - """Update the image based on the theme and resize event.""" - - if ( - "latte" in self.current_theme["name"].lower() - or "light" in self.current_theme["name"].lower() - ): - image_path: str = "docs/_static/ManimStudioLogoLight.png" - else: - image_path: str = "docs/_static/ManimStudioLogoDark.png" - - pixmap: QPixmap = QPixmap(image_path) - if not pixmap.isNull(): - scaledPixmap: QPixmap = pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation, - ) - self.ui.label.setPixmap(scaledPixmap) - - self.ui.label.setAlignment(Qt.AlignmentFlag.AlignCenter) - @logger.catch def update_table_view(self) -> None: """Update the table view based on the theme and resize event.""" @@ -96,55 +83,6 @@ def update_table_view(self) -> None: QHeaderView.ResizeMode.Stretch ) - @logger.catch - def apply_stylesheet(self) -> None: - """Apply the stylesheet to the main window and update the image based on the theme""" - - # Set the custom stylesheet based on the current theme - self.customStyleSheet: str = f"background-color: {self.current_theme['background']}; color: {self.current_theme['font']}; border-color: {self.current_theme['primary']}; font-size: {self.settings['fontSize']}px; font-family: {self.settings['fontFamily']}; " - self.setStyleSheet(self.customStyleSheet) - - self.customMenubarStylesheet = str( - f"background-color: {self.current_theme['primary']}; color: {self.current_theme['font']}; border-color: {self.current_theme['primary']}; font-size: {self.settings['fontSize']}px; font-family: {self.settings['fontFamily']}; " - ) - self.ui.menubar.setStyleSheet(self.customMenubarStylesheet) - - # Update the image - self.update_image() - - self.styleSheetUpdated.emit(self.customStyleSheet) - logger.info("Stylesheet applied") - - @logger.catch - def load_ui(self) -> None: - """Load the UI from the .ui file""" - - self.ui: Ui_MainWindow = Ui_MainWindow() - self.ui.setupUi(self) - - self.ui.label.setSizePolicy( - QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) - ) - - # Apply the theme - self.apply_stylesheet() - - # Create new menubar item - settings_action = QAction("Settings", self) - settings_action.triggered.connect(self.open_settings_dialog) - self.ui.menubar.addAction(settings_action) - - self.ui.newProjectBtn.clicked.connect(self.show_project_creation_dialog) - self.ui.openProjectBtn.clicked.connect(self.show_project_open_dialog) - - # Populate the recent projects list with the 10 latest projects - self.populate_recent_projects() - self.ui.recentProjectsTableView.doubleClicked.connect( - self.open_project_from_list - ) - - logger.info("UI loaded") - @logger.catch def populate_recent_projects(self): # Create a model with 3 columns @@ -187,83 +125,6 @@ def open_project_from_list(self, index): # Now you can call the method to open the project self.open_video_editor(project_path) - @logger.catch - def open_settings_dialog(self) -> None: - """Open the settings dialog""" - self.settingsDialog: QDialog = QDialog() - self.uiSettings: Ui_Form = Ui_Form() - self.uiSettings.setupUi(self.settingsDialog) - - # Change window title - self.settingsDialog.setWindowTitle("Settings") - - # Inherit the theme from the main window - self.settingsDialog.setStyleSheet(self.customStyleSheet) - self.styleSheetUpdated.connect(self.settingsDialog.setStyleSheet) - - # Load settings and themes to the dialog - self.uiSettings.fontSizeSpinBox.setValue(self.settings["fontSize"]) - self.uiSettings.fontComboBox.setCurrentText(self.settings["fontFamily"]) - - self.uiSettings.themeComboBox.clear() - - # Populate the theme combobox with full theme data - for theme_module in self.themes: - for variant in theme_module["variants"]: - variant_data = json.dumps(variant) - self.uiSettings.themeComboBox.addItem(variant["name"], variant_data) - - # Set the current theme in the combobox - current_theme_index = self.uiSettings.themeComboBox.findText( - self.current_theme["name"] - ) - if current_theme_index >= 0: - self.uiSettings.themeComboBox.setCurrentIndex(current_theme_index) - - self.uiSettings.saveSettingsBtn.clicked.connect( - self.update_settings_from_dialog - ) - - self.settingsDialog.exec() - - @logger.catch - def update_settings_from_dialog(self) -> None: - """Update the settings from the dialog, and update the UI""" - - # Get recentProjects array from the current settings - recentProjectPaths: List[str] = self.settings.get("recentProjectPaths", []) - - # Get recentProjectCreationPaths array from the current settings - recentProjectCreationPaths: List[str] = self.settings.get( - "recentProjectCreationPaths", [] - ) - - # Get the current values from the dialog - fontSize: int = self.uiSettings.fontSizeSpinBox.value() - fontFamily: str = self.uiSettings.fontComboBox.currentText() - - # Extract full theme data from the selected item in the combobox - theme_data_json: str = self.uiSettings.themeComboBox.currentData() - selected_theme: Dict = json.loads(theme_data_json) - - # Create a new settings object - new_settings: Dict = { - "fontSize": fontSize, - "fontFamily": fontFamily, - "theme": selected_theme, - "recentProjectCreationPaths": recentProjectCreationPaths, - "recentProjectPaths": recentProjectPaths, - } - - # Pass the new settings to the settings module - if update_settings(new_settings): - # Update the global settings variable - self.settings = new_settings - # Update the current theme - self.current_theme = load_current_theme() - # Apply the stylesheet - self.apply_stylesheet() - @logger.catch def show_project_creation_dialog(self) -> None: """Show the project creation dialog""" @@ -304,12 +165,7 @@ def show_project_open_dialog(self) -> None: def open_video_editor(self, project_file_path: str) -> None: """Open the video editor with the project file""" try: - self.videoEditor: QDialog = QDialog() - self.uiVideoEditor: Ui_VideoEditor = Ui_VideoEditor() - self.uiVideoEditor.setupUi(self.videoEditor) - - # Apply the theme - self.videoEditor.setStyleSheet(self.customStyleSheet) + self.videoEditor = VideoEditorWindow(self) # Change window title as current project name with file path self.videoEditor.setWindowTitle(f"Manim Studio - {project_file_path}") @@ -319,7 +175,7 @@ def open_video_editor(self, project_file_path: str) -> None: self.close() - self.videoEditor.exec() + self.videoEditor.show() except Exception as e: logger.error(f"Error opening video editor: {e}") pass diff --git a/src/core/settings.py b/src/core/settings.py index 8904788..761b536 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -1,9 +1,16 @@ import os +import json import platform from pathlib import Path from typing import Any, Dict, List -import json +from PySide6.QtWidgets import QDialog, QSizePolicy +from PySide6.QtGui import QAction, QPixmap +from PySide6.QtCore import Qt +# UI imports +from src.ui.settings_ui import Ui_Form # noqa: E402 + +# Utils imports from src.utils.logger_utility import logger @@ -20,7 +27,9 @@ def get_settings_path() -> Path: if platform.system() == "Linux": settings_path = home_dir / ".config" / "ManimStudio" / "settings.json" if platform.system() == "Windows": - settings_path = home_dir / "AppData" / "Roaming" / "ManimStudio" / "settings.json" + settings_path = ( + home_dir / "AppData" / "Roaming" / "ManimStudio" / "settings.json" + ) if platform.system() == "Darwin": settings_path = ( home_dir @@ -106,6 +115,9 @@ def load_current_theme() -> Dict[str, Any]: return {} +"""Recent Projects Paths""" + + @logger.catch def add_recent_project_creation_path(project_creation_path: str) -> None: """Add a recent project creation path to the settings file""" @@ -124,18 +136,18 @@ def add_recent_project_path(project_path: str) -> None: """Add a recent project path to the settings file with last modification date and size""" settings: Dict[str, Any] = load_settings() recent_project_paths: List[Dict[str, Any]] = settings.get("recentProjectPaths", []) - + # Get last modification time and size modification_time = os.path.getmtime(project_path) size = os.path.getsize(project_path) - + # Create a new entry for the project new_project_entry = { "path": project_path, "last_modified": modification_time, - "size": size + "size": size, } - + # Check if the project already exists in the list and update it for project in recent_project_paths: if project["path"] == project_path: @@ -144,10 +156,10 @@ def add_recent_project_path(project_path: str) -> None: else: # If the project is not in the list, add it recent_project_paths.insert(0, new_project_entry) - + # Keep only the 10 most recent recent_project_paths = recent_project_paths[:10] - + settings["recentProjectPaths"] = recent_project_paths update_settings(settings) @@ -162,3 +174,158 @@ def get_recent_project_creation_paths() -> List[str]: def get_recent_project_paths() -> List[Dict[str, Any]]: """Get the recent project paths with their last modification date and size""" return load_settings().get("recentProjectPaths", []) + + +@logger.catch +def update_image(ui, current_theme) -> None: + """Update the image based on the theme and resize event.""" + if ( + "latte" in current_theme["name"].lower() + or "light" in current_theme["name"].lower() + ): + image_path: str = "docs/_static/ManimStudioLogoLight.png" + else: + image_path: str = "docs/_static/ManimStudioLogoDark.png" + pixmap: QPixmap = QPixmap(image_path) + if not pixmap.isNull() and not pixmap.size().isEmpty(): + scaledPixmap: QPixmap = pixmap.scaled( + ui.label.size(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + ui.label.setPixmap(scaledPixmap) + ui.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + +@logger.catch +def apply_stylesheet(main_window, ui, settings, current_theme) -> None: + """Apply the stylesheet to the main window and update the image based on the theme""" + + # Set the custom stylesheet based on the current theme + customStyleSheet: str = f"background-color: {current_theme['background']}; color: {current_theme['font']}; border-color: {current_theme['primary']}; font-size: {settings['fontSize']}px; font-family: {settings['fontFamily']}; " + main_window.customStyleSheet = customStyleSheet + main_window.setStyleSheet(customStyleSheet) + if hasattr(main_window, "styleSheetUpdated"): + main_window.styleSheetUpdated.emit(customStyleSheet) + + customMenubarStylesheet = str( + f"background-color: {current_theme['primary']}; color: {current_theme['font']}; border-color: {current_theme['primary']}; font-size: {settings['fontSize']}px; font-family: {settings['fontFamily']}; " + ) + + if hasattr(ui, "menubar"): + ui.menubar.setStyleSheet(customMenubarStylesheet) + else: + main_window.menuBar().setStyleSheet(customMenubarStylesheet) + + # Update the image + if hasattr(ui, "label"): + update_image(ui, current_theme) + + main_window.styleSheetUpdated.emit(customStyleSheet) + logger.info("Stylesheet applied") + + +@logger.catch +def load_ui(main_window, ui, settings, current_theme, themes): + """Load the UI from the .ui file""" + + ui.setupUi(main_window) + + ui.label.setSizePolicy( + QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + ) + + # Apply the theme + apply_stylesheet(main_window, ui, settings, current_theme) + + # Create new menubar item + settings_action = QAction("Settings", main_window) + settings_action.triggered.connect( + lambda: open_settings_dialog(main_window, settings, themes, current_theme) + ) + + ui.menubar.addAction(settings_action) + ui.newProjectBtn.clicked.connect(lambda: main_window.show_project_creation_dialog()) + ui.openProjectBtn.clicked.connect(lambda: main_window.show_project_open_dialog()) + + logger.info("UI loaded") + return ui + + +@logger.catch +def open_settings_dialog(main_window, settings, themes, current_theme) -> None: + """Open the settings dialog""" + + settingsDialog: QDialog = QDialog() + uiSettings: Ui_Form = Ui_Form() + uiSettings.setupUi(settingsDialog) + + # Change window title + settingsDialog.setWindowTitle("Settings") + + # Inherit the theme from the main window + settingsDialog.setStyleSheet(main_window.customStyleSheet) + main_window.styleSheetUpdated.connect(settingsDialog.setStyleSheet) + + # Load settings and themes to the dialog + uiSettings.fontSizeSpinBox.setValue(settings["fontSize"]) + uiSettings.fontComboBox.setCurrentText(settings["fontFamily"]) + uiSettings.themeComboBox.clear() + + # Populate the theme combobox with full theme data + for theme_module in themes: + for variant in theme_module["variants"]: + variant_data = json.dumps(variant) + uiSettings.themeComboBox.addItem(variant["name"], variant_data) + + # Set the current theme in the combobox + current_theme_index = uiSettings.themeComboBox.findText(current_theme["name"]) + + if current_theme_index >= 0: + uiSettings.themeComboBox.setCurrentIndex(current_theme_index) + uiSettings.saveSettingsBtn.clicked.connect( + lambda: update_settings_from_dialog( + main_window, uiSettings, settings, themes, current_theme + ) + ) + settingsDialog.exec() + + +@logger.catch +def update_settings_from_dialog( + main_window, uiSettings, settings, themes, current_theme +) -> None: + """Update the settings from the dialog, and update the UI""" + + # Get recentProjects array from the current settings + recentProjectPaths: List[str] = settings.get("recentProjectPaths", []) + + # Get recentProjectCreationPaths array from the current settings + recentProjectCreationPaths: List[str] = settings.get( + "recentProjectCreationPaths", [] + ) + + # Get the current values from the dialog + fontSize: int = uiSettings.fontSizeSpinBox.value() + fontFamily: str = uiSettings.fontComboBox.currentText() + + # Extract full theme data from the selected item in the combobox + theme_data_json: str = uiSettings.themeComboBox.currentData() + selected_theme: Dict = json.loads(theme_data_json) + + # Create a new settings object + new_settings: Dict = { + "fontSize": fontSize, + "fontFamily": fontFamily, + "theme": selected_theme, + "recentProjectCreationPaths": recentProjectCreationPaths, + "recentProjectPaths": recentProjectPaths, + } + # Pass the new settings to the settings module + if update_settings(new_settings): + # Update the global settings variable + settings = new_settings + # Update the current theme + current_theme = load_current_theme() + # Apply the stylesheet + apply_stylesheet(main_window, main_window.ui, settings, current_theme) diff --git a/src/core/videoeditor.py b/src/core/videoeditor.py new file mode 100644 index 0000000..ee61f51 --- /dev/null +++ b/src/core/videoeditor.py @@ -0,0 +1,60 @@ +from PySide6.QtWidgets import QMainWindow, QWidget +from PySide6.QtGui import QAction +from PySide6.QtCore import Signal + +# UI imports +from src.ui.videoeditor_ui import Ui_Form as Ui_VideoEditor + +# Core imports +from src.core.settings import ( + open_settings_dialog, + load_settings, + load_themes, + load_current_theme, + apply_stylesheet, +) + +# Utils imports +from src.utils.logger_utility import logger + + +class VideoEditorWindow(QMainWindow): + """Main window for the video editor application.""" + + styleSheetUpdated = Signal(str) + + @logger.catch + def __init__(self, parent=None): + """Initializer""" + super().__init__(parent) + + self.central_widget = QWidget(self) + self.ui = Ui_VideoEditor() + self.ui.setupUi(self.central_widget) + self.setCentralWidget( + self.central_widget + ) # Set the central widget of QMainWindow + + self.customStyleSheet = "" + + self.settings = load_settings() + self.themes = load_themes() + self.current_theme = load_current_theme() + + apply_stylesheet(self, self.ui, self.settings, self.current_theme) + + self.create_menubar() + + @logger.catch + def create_menubar(self): + """Create the menubar for the main window.""" + + settings_action = QAction("Settings", self) + settings_action.triggered.connect(self.open_settings) + self.menuBar().addAction(settings_action) + + @logger.catch + def open_settings(self): + """Open the settings dialog.""" + + open_settings_dialog(self, self.settings, self.themes, self.current_theme)