287 lines
13 KiB
Python
287 lines
13 KiB
Python
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
|