import os
import json
import platform
from pathlib import Path
from typing import Any, Dict, List
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
# Global base settings and theme path
base_settings_path: Path = Path(os.getcwd()) / ".config" / "settings.json"
base_theme_path: Path = Path(os.getcwd()) / "src" / "themes" / "themes.json"
[docs]
@logger.catch
def get_settings_path() -> Path:
"""Get the settings path based on the platform"""
home_dir: Path = Path.home()
settings_path: Path = base_settings_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"
)
if platform.system() == "Darwin":
settings_path = (
home_dir
/ "Library"
/ "Application Support"
/ "ManimStudio"
/ "settings.json"
)
return settings_path
settings_path: Path = get_settings_path()
if not settings_path.exists():
settings_path.parent.mkdir(parents=True, exist_ok=True)
with open(base_settings_path, "r") as file:
settings: Dict[str, Any] = json.load(file)
with open(settings_path, "w") as file:
json.dump(settings, file, indent=4)
[docs]
@logger.catch
def get_themes_path() -> Path:
"""Get the themes path"""
return base_theme_path
themes_path: Path = get_themes_path()
"""Settings"""
[docs]
@logger.catch
def load_settings() -> Dict[str, Any]:
"""Load the settings from the settings.json file"""
try:
with open(settings_path, "r") as file:
return json.load(file)
except Exception as e:
logger.error(e)
return {}
[docs]
@logger.catch
def update_settings(new_settings: Dict[str, Any]) -> bool:
"""Overwrite the settings file with the new settings"""
try:
with open(settings_path, "w") as file:
json.dump(new_settings, file, indent=4)
return True
except Exception as e:
logger.error(e)
return False
"""Themes"""
[docs]
@logger.catch
def load_themes():
"""Load the themes from the themes.json file"""
try:
with open(themes_path, "r") as file:
return json.load(file)
except Exception as e:
logger.error(e)
return {}
[docs]
@logger.catch
def load_current_theme() -> Dict[str, Any]:
"""Load the current theme"""
settings: Dict[str, Any] = load_settings()
try:
theme_module: str = settings["theme"].get("moduleName")
theme_name: str = settings["theme"].get("fileName")
with open(
themes_path.parent / theme_module / theme_name,
"r",
) as file:
return json.load(file)
except Exception as e:
logger.error(e)
return {}
"""Recent Projects Paths"""
[docs]
@logger.catch
def add_recent_project_creation_path(project_creation_path: str) -> None:
"""Add a recent project creation path to the settings file"""
settings: Dict[str, Any] = load_settings()
recent_creation_paths: List[str] = settings.get("recentProjectCreationPaths", [])
if project_creation_path not in recent_creation_paths:
recent_creation_paths.insert(0, project_creation_path)
if len(recent_creation_paths) > 10: # Keep only the 10 most recent
recent_creation_paths = recent_creation_paths[:10]
settings["recentProjectCreationPaths"] = recent_creation_paths
update_settings(settings)
[docs]
@logger.catch
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,
}
# Check if the project already exists in the list and update it
for project in recent_project_paths:
if project["path"] == project_path:
project.update(new_project_entry)
break
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)
[docs]
@logger.catch
def get_recent_project_creation_paths() -> List[str]:
"""Get the recent project creation paths"""
return load_settings().get("recentProjectCreationPaths", [])
[docs]
@logger.catch
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", [])
[docs]
@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)
[docs]
@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")
[docs]
@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
[docs]
@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()
[docs]
@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)