PortProtonQt/portprotonqt/theme_manager.py
Boris Yumankulov abec9bbef8
All checks were successful
Check Translations / check-translations (push) Successful in 15s
Code and build check / Check code (push) Successful in 1m21s
Code and build check / Build with uv (push) Successful in 47s
Move repo from git to gitea
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-01 15:21:32 +05:00

287 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import importlib.util
import os
from portprotonqt.logger import get_logger
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtGui import QIcon, QColor, QFontDatabase, QPixmap, QPainter
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
THEMES_DIRS = [
os.path.join(xdg_data_home, "PortProtonQT", "themes"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
def list_themes():
"""
Возвращает список доступных тем (названий папок) из каталогов THEMES_DIRS.
"""
themes = []
for themes_dir in THEMES_DIRS:
if os.path.exists(themes_dir):
for entry in os.listdir(themes_dir):
theme_path = os.path.join(themes_dir, entry)
if os.path.isdir(theme_path) and os.path.exists(os.path.join(theme_path, "styles.py")):
themes.append(entry)
return themes
def load_theme_screenshots(theme_name):
"""
Загружает все скриншоты из папки "screenshots", расположенной в папке темы.
Возвращает список кортежей (pixmap, filename).
Если папка отсутствует или пуста, возвращается пустой список.
"""
screenshots = []
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
screenshots_folder = os.path.join(theme_folder, "images", "screenshots")
if os.path.exists(screenshots_folder) and os.path.isdir(screenshots_folder):
for file in os.listdir(screenshots_folder):
screenshot_path = os.path.join(screenshots_folder, file)
if os.path.isfile(screenshot_path):
pixmap = QPixmap(screenshot_path)
if not pixmap.isNull():
screenshots.append((pixmap, file))
return screenshots
def load_theme_fonts(theme_name):
"""
Загружает все шрифты выбранной темы.
:param theme_name: Имя темы.
"""
QFontDatabase.removeAllApplicationFonts()
fonts_folder = None
if theme_name == "standart":
base_dir = os.path.dirname(os.path.abspath(__file__))
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
else:
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
possible_fonts_folder = os.path.join(theme_folder, "fonts")
if os.path.exists(possible_fonts_folder):
fonts_folder = possible_fonts_folder
break
if not fonts_folder or not os.path.exists(fonts_folder):
logger.error(f"Папка fonts не найдена для темы '{theme_name}'")
return
for filename in os.listdir(fonts_folder):
if filename.lower().endswith((".ttf", ".otf")):
font_path = os.path.join(fonts_folder, filename)
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
families = QFontDatabase.applicationFontFamilies(font_id)
logger.info(f"Шрифт {filename} успешно загружен: {families}")
else:
logger.error(f"Ошибка загрузки шрифта: {filename}")
def load_logo():
logo_path = None
base_dir = os.path.dirname(os.path.abspath(__file__))
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
file_extension = os.path.splitext(logo_path)[1].lower()
if file_extension == ".svg":
renderer = QSvgRenderer(logo_path)
if not renderer.isValid():
logger.error(f"Ошибка загрузки SVG логотипа: {logo_path}")
return None
pixmap = QPixmap(128, 128)
pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return pixmap
class ThemeWrapper:
"""
Обёртка для кастомной темы с поддержкой метаинформации.
При обращении к атрибуту сначала ищется его наличие в кастомной теме,
если атрибут отсутствует, значение берётся из стандартного модуля стилей.
"""
def __init__(self, custom_theme, metainfo=None):
self.custom_theme = custom_theme
self.metainfo = metainfo or {}
self.screenshots = load_theme_screenshots(self.metainfo.get("name", ""))
def __getattr__(self, name):
if hasattr(self.custom_theme, name):
return getattr(self.custom_theme, name)
import portprotonqt.themes.standart.styles as default_styles
return getattr(default_styles, name)
def load_theme(theme_name):
"""
Динамически загружает модуль стилей выбранной темы и метаинформацию.
Если выбрана стандартная тема, импортируется оригинальный styles.py.
Для кастомных тем возвращается обёртка, которая подставляет недостающие атрибуты.
"""
if theme_name == "standart":
import portprotonqt.themes.standart.styles as default_styles
return default_styles
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(themes_dir, theme_name)
styles_file = os.path.join(theme_folder, "styles.py")
if os.path.exists(styles_file):
spec = importlib.util.spec_from_file_location("theme_styles", styles_file)
if spec is None or spec.loader is None:
continue
custom_theme = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_theme)
meta = load_theme_metainfo(theme_name)
wrapper = ThemeWrapper(custom_theme, metainfo=meta)
wrapper.screenshots = load_theme_screenshots(theme_name)
return wrapper
raise FileNotFoundError(f"Файл стилей не найден для темы '{theme_name}'")
class ThemeManager:
"""
Класс для управления темами приложения.
Позволяет получить список доступных тем, загрузить и применить выбранную тему.
"""
def __init__(self):
self.current_theme_name = None
self.current_theme_module = None
def get_available_themes(self):
"""Возвращает список доступных тем."""
return list_themes()
def get_theme_logo(self):
"""Возвращает логотип для текущей или указанной темы."""
return load_logo()
def apply_theme(self, theme_name):
"""
Применяет выбранную тему: загружает модуль стилей, шрифты и логотип.
Если загрузка прошла успешно, сохраняет выбранную тему в конфигурации.
:param theme_name: Имя темы.
:return: Загруженный модуль темы (или обёртка).
"""
theme_module = load_theme(theme_name)
load_theme_fonts(theme_name)
self.current_theme_name = theme_name
self.current_theme_module = theme_module
save_theme_to_config(theme_name)
logger.info(f"Тема '{theme_name}' успешно применена")
return theme_module
def get_icon(self, icon_name, theme_name=None, as_path=False):
"""
Возвращает QIcon из папки icons текущей темы,
а если файл не найден, то из стандартной темы.
Если as_path=True, возвращает путь к иконке вместо QIcon.
"""
icon_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
has_extension = any(icon_name.lower().endswith(ext) for ext in supported_extensions)
base_name = icon_name if has_extension else icon_name
# Поиск иконки в папке текущей темы
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(str(themes_dir), str(theme_name))
icons_folder = os.path.join(theme_folder, "images", "icons")
# Если передано имя с расширением, проверяем только этот файл
if has_extension:
candidate = os.path.join(icons_folder, str(base_name))
if os.path.exists(candidate):
icon_path = candidate
break
else:
# Проверяем все поддерживаемые расширения
for ext in supported_extensions:
candidate = os.path.join(icons_folder, str(base_name) + str(ext))
if os.path.exists(candidate):
icon_path = candidate
break
if icon_path:
break
# Если не нашли используем стандартную тему
if not icon_path:
base_dir = os.path.dirname(os.path.abspath(__file__))
standard_icons_folder = os.path.join(base_dir, "themes", "standart", "images", "icons")
# Аналогично проверяем в стандартной теме
if has_extension:
icon_path = os.path.join(standard_icons_folder, base_name)
if not os.path.exists(icon_path):
icon_path = None
else:
for ext in supported_extensions:
candidate = os.path.join(standard_icons_folder, base_name + ext)
if os.path.exists(candidate):
icon_path = candidate
break
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Предупреждение: иконка '{icon_name}' не найдена")
return QIcon() if not as_path else None
if as_path:
return icon_path
return QIcon(icon_path)
def get_theme_image(self, image_name, theme_name=None):
"""
Возвращает путь к изображению из папки текущей темы.
Если не найдено, проверяет стандартную тему.
Принимает название иконки без расширения и находит соответствующий файл
с поддерживаемым расширением (.svg, .png, .jpg и др.).
"""
image_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
has_extension = any(image_name.lower().endswith(ext) for ext in supported_extensions)
base_name = image_name if has_extension else image_name
# Check theme-specific images
for themes_dir in THEMES_DIRS:
theme_folder = os.path.join(str(themes_dir), str(theme_name))
images_folder = os.path.join(theme_folder, "images")
if has_extension:
candidate = os.path.join(images_folder, str(base_name))
if os.path.exists(candidate):
image_path = candidate
break
else:
for ext in supported_extensions:
candidate = os.path.join(images_folder, str(base_name) + str(ext))
if os.path.exists(candidate):
image_path = candidate
break
if image_path:
break
# Check standard theme
if not image_path:
base_dir = os.path.dirname(os.path.abspath(__file__))
standard_images_folder = os.path.join(base_dir, "themes", "standart", "images")
if has_extension:
image_path = os.path.join(standard_images_folder, base_name)
if not os.path.exists(image_path):
image_path = None
else:
for ext in supported_extensions:
candidate = os.path.join(standard_images_folder, base_name + ext)
if os.path.exists(candidate):
image_path = candidate
break
return image_path