feat(theme-manager): implement singleton and caching for improved theme handling
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
|||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
theme_manager = ThemeManager()
|
||||||
|
|
||||||
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
||||||
"""
|
"""
|
||||||
@@ -93,8 +94,7 @@ class GameLaunchDialog(QDialog):
|
|||||||
"""Modal dialog to indicate game launch progress, similar to Steam's launch dialog."""
|
"""Modal dialog to indicate game launch progress, similar to Steam's launch dialog."""
|
||||||
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
|
def __init__(self, parent=None, game_name=None, theme=None, target_exe=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme_manager = ThemeManager()
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
|
|
||||||
self.game_name = game_name
|
self.game_name = game_name
|
||||||
self.target_exe = target_exe # Store the target executable name
|
self.target_exe = target_exe # Store the target executable name
|
||||||
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
||||||
@@ -122,7 +122,7 @@ class GameLaunchDialog(QDialog):
|
|||||||
layout.addWidget(self.progress_bar)
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
# Cancel button
|
# Cancel button
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
@@ -172,8 +172,7 @@ class GameLaunchDialog(QDialog):
|
|||||||
class FileExplorer(QDialog):
|
class FileExplorer(QDialog):
|
||||||
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.theme_manager = ThemeManager()
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
|
|
||||||
self.file_signal = FileSelectedSignal()
|
self.file_signal = FileSelectedSignal()
|
||||||
self.file_filter = file_filter # Store the file filter
|
self.file_filter = file_filter # Store the file filter
|
||||||
self.directory_only = directory_only # Store the directory_only flag
|
self.directory_only = directory_only # Store the directory_only flag
|
||||||
@@ -271,8 +270,8 @@ class FileExplorer(QDialog):
|
|||||||
# Кнопки
|
# Кнопки
|
||||||
self.button_layout = QHBoxLayout()
|
self.button_layout = QHBoxLayout()
|
||||||
self.button_layout.setSpacing(10)
|
self.button_layout.setSpacing(10)
|
||||||
self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply"))
|
self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply"))
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.button_layout.addWidget(self.select_button)
|
self.button_layout.addWidget(self.select_button)
|
||||||
@@ -405,7 +404,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем смонтированные диски
|
# Добавляем смонтированные диски
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
drive_name = os.path.basename(drive) or drive.split('/')[-1] or drive
|
||||||
button = AutoSizeButton(drive_name, icon=self.theme_manager.get_icon("mount_point"))
|
button = AutoSizeButton(drive_name, icon=theme_manager.get_icon("mount_point"))
|
||||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
button.clicked.connect(lambda checked, path=drive: self.change_drive(path))
|
||||||
@@ -415,7 +414,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем избранные папки
|
# Добавляем избранные папки
|
||||||
for folder in favorite_folders:
|
for folder in favorite_folders:
|
||||||
folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
|
folder_name = os.path.basename(folder) or folder.split('/')[-1] or folder
|
||||||
button = AutoSizeButton(folder_name, icon=self.theme_manager.get_icon("folder"))
|
button = AutoSizeButton(folder_name, icon=theme_manager.get_icon("folder"))
|
||||||
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
|
button.clicked.connect(lambda checked, path=folder: self.change_drive(path))
|
||||||
@@ -483,7 +482,7 @@ class FileExplorer(QDialog):
|
|||||||
try:
|
try:
|
||||||
if self.current_path != "/":
|
if self.current_path != "/":
|
||||||
item = QListWidgetItem("../")
|
item = QListWidgetItem("../")
|
||||||
folder_icon = self.theme_manager.get_icon("folder")
|
folder_icon = theme_manager.get_icon("folder")
|
||||||
# Ensure the icon is a QIcon
|
# Ensure the icon is a QIcon
|
||||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||||
folder_icon = QIcon(folder_icon)
|
folder_icon = QIcon(folder_icon)
|
||||||
@@ -498,7 +497,7 @@ class FileExplorer(QDialog):
|
|||||||
# Добавляем директории
|
# Добавляем директории
|
||||||
for d in sorted(dirs):
|
for d in sorted(dirs):
|
||||||
item = QListWidgetItem(f"{d}/")
|
item = QListWidgetItem(f"{d}/")
|
||||||
folder_icon = self.theme_manager.get_icon("folder")
|
folder_icon = theme_manager.get_icon("folder")
|
||||||
# Ensure the icon is a QIcon
|
# Ensure the icon is a QIcon
|
||||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||||
folder_icon = QIcon(folder_icon)
|
folder_icon = QIcon(folder_icon)
|
||||||
@@ -589,8 +588,7 @@ class AddGameDialog(QDialog):
|
|||||||
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
|
def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
|
from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт
|
||||||
self.theme_manager = ThemeManager()
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config())
|
|
||||||
self.edit_mode = edit_mode
|
self.edit_mode = edit_mode
|
||||||
self.original_name = game_name
|
self.original_name = game_name
|
||||||
self.last_exe_path = exe_path # Store last selected exe path
|
self.last_exe_path = exe_path # Store last selected exe path
|
||||||
@@ -626,7 +624,7 @@ class AddGameDialog(QDialog):
|
|||||||
if exe_path:
|
if exe_path:
|
||||||
self.exeEdit.setText(exe_path)
|
self.exeEdit.setText(exe_path)
|
||||||
|
|
||||||
exeBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
|
exeBrowseButton = AutoSizeButton(_("Browse..."), icon=theme_manager.get_icon("search"))
|
||||||
exeBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
exeBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
exeBrowseButton.clicked.connect(self.browseExe)
|
exeBrowseButton.clicked.connect(self.browseExe)
|
||||||
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
|
exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки
|
||||||
@@ -648,7 +646,7 @@ class AddGameDialog(QDialog):
|
|||||||
if cover_path:
|
if cover_path:
|
||||||
self.coverEdit.setText(cover_path)
|
self.coverEdit.setText(cover_path)
|
||||||
|
|
||||||
coverBrowseButton = AutoSizeButton(_("Browse..."), icon=self.theme_manager.get_icon("search"))
|
coverBrowseButton = AutoSizeButton(_("Browse..."), icon=theme_manager.get_icon("search"))
|
||||||
coverBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
coverBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
coverBrowseButton.clicked.connect(self.browseCover)
|
coverBrowseButton.clicked.connect(self.browseCover)
|
||||||
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
|
coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки
|
||||||
@@ -677,8 +675,8 @@ class AddGameDialog(QDialog):
|
|||||||
# Dialog buttons
|
# Dialog buttons
|
||||||
self.button_layout = QHBoxLayout()
|
self.button_layout = QHBoxLayout()
|
||||||
self.button_layout.setSpacing(10)
|
self.button_layout.setSpacing(10)
|
||||||
self.select_button = AutoSizeButton(_("Apply"), icon=self.theme_manager.get_icon("apply"))
|
self.select_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply"))
|
||||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel"))
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
self.button_layout.addWidget(self.select_button)
|
self.button_layout.addWidget(self.select_button)
|
||||||
|
@@ -14,6 +14,7 @@ THEMES_DIRS = [
|
|||||||
os.path.join(xdg_data_home, "PortProtonQt", "themes"),
|
os.path.join(xdg_data_home, "PortProtonQt", "themes"),
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
|
||||||
]
|
]
|
||||||
|
_loaded_theme = None
|
||||||
|
|
||||||
# Запрещенные модули и функции
|
# Запрещенные модули и функции
|
||||||
FORBIDDEN_MODULES = {
|
FORBIDDEN_MODULES = {
|
||||||
@@ -101,9 +102,13 @@ def load_theme_screenshots(theme_name):
|
|||||||
|
|
||||||
def load_theme_fonts(theme_name):
|
def load_theme_fonts(theme_name):
|
||||||
"""
|
"""
|
||||||
Загружает все шрифты выбранной темы.
|
Загружает все шрифты выбранной темы, если они ещё не были загружены.
|
||||||
:param theme_name: Имя темы.
|
|
||||||
"""
|
"""
|
||||||
|
global _loaded_theme
|
||||||
|
if _loaded_theme == theme_name:
|
||||||
|
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
QFontDatabase.removeAllApplicationFonts()
|
QFontDatabase.removeAllApplicationFonts()
|
||||||
fonts_folder = None
|
fonts_folder = None
|
||||||
if theme_name == "standart":
|
if theme_name == "standart":
|
||||||
@@ -131,9 +136,13 @@ def load_theme_fonts(theme_name):
|
|||||||
else:
|
else:
|
||||||
logger.error(f"Error loading font: {filename}")
|
logger.error(f"Error loading font: {filename}")
|
||||||
|
|
||||||
def load_logo():
|
_loaded_theme = theme_name
|
||||||
logo_path = None
|
|
||||||
|
|
||||||
|
def load_logo():
|
||||||
|
"""
|
||||||
|
Загружает логотип темы из стандартной папки.
|
||||||
|
"""
|
||||||
|
logo_path = None
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
|
logo_path = os.path.join(base_dir, "themes", "standart", "images", "theme_logo.svg")
|
||||||
|
|
||||||
@@ -201,28 +210,34 @@ def load_theme(theme_name):
|
|||||||
class ThemeManager:
|
class ThemeManager:
|
||||||
"""
|
"""
|
||||||
Класс для управления темами приложения.
|
Класс для управления темами приложения.
|
||||||
|
Реализует паттерн Singleton для единого экземпляра.
|
||||||
Позволяет получить список доступных тем, загрузить и применить выбранную тему.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
_instance = None
|
||||||
self.current_theme_name = None
|
|
||||||
self.current_theme_module = None
|
|
||||||
|
|
||||||
def get_available_themes(self):
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.current_theme_name = None
|
||||||
|
cls._instance.current_theme_module = None
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def get_available_themes(self) -> list:
|
||||||
"""Возвращает список доступных тем."""
|
"""Возвращает список доступных тем."""
|
||||||
return list_themes()
|
return list_themes()
|
||||||
|
|
||||||
def get_theme_logo(self):
|
def get_theme_logo(self):
|
||||||
"""Возвращает логотип для текущей или указанной темы."""
|
"""Возвращает логотип текущей темы."""
|
||||||
return load_logo()
|
return load_logo()
|
||||||
|
|
||||||
def apply_theme(self, theme_name):
|
def apply_theme(self, theme_name: str):
|
||||||
"""
|
"""
|
||||||
Применяет выбранную тему: загружает модуль стилей, шрифты и логотип.
|
Применяет указанную тему, если она ещё не применена.
|
||||||
Если загрузка прошла успешно, сохраняет выбранную тему в конфигурации.
|
Возвращает модуль темы или обёртку.
|
||||||
:param theme_name: Имя темы.
|
|
||||||
:return: Загруженный модуль темы (или обёртка).
|
|
||||||
"""
|
"""
|
||||||
|
if self.current_theme_name == theme_name and self.current_theme_module is not None:
|
||||||
|
logger.debug(f"Theme '{theme_name}' is already applied, skipping")
|
||||||
|
return self.current_theme_module
|
||||||
|
|
||||||
try:
|
try:
|
||||||
theme_module = load_theme(theme_name)
|
theme_module = load_theme(theme_name)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -230,6 +245,7 @@ class ThemeManager:
|
|||||||
theme_module = load_theme("standart")
|
theme_module = load_theme("standart")
|
||||||
theme_name = "standart"
|
theme_name = "standart"
|
||||||
save_theme_to_config("standart")
|
save_theme_to_config("standart")
|
||||||
|
|
||||||
load_theme_fonts(theme_name)
|
load_theme_fonts(theme_name)
|
||||||
self.current_theme_name = theme_name
|
self.current_theme_name = theme_name
|
||||||
self.current_theme_module = theme_module
|
self.current_theme_module = theme_module
|
||||||
|
Reference in New Issue
Block a user