From f3325ca35f918d6bd1137bbf81d6c266cca01320 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 7 Sep 2025 22:54:25 +0500 Subject: [PATCH] feat(theme-manager): implement singleton and caching for improved theme handling Signed-off-by: Boris Yumankulov --- portprotonqt/dialogs.py | 32 +++++++++++------------ portprotonqt/theme_manager.py | 48 +++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/portprotonqt/dialogs.py b/portprotonqt/dialogs.py index c9e3da7..698d318 100644 --- a/portprotonqt/dialogs.py +++ b/portprotonqt/dialogs.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from portprotonqt.main_window import MainWindow logger = get_logger(__name__) +theme_manager = ThemeManager() 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.""" def __init__(self, parent=None, game_name=None, theme=None, target_exe=None): super().__init__(parent) - self.theme_manager = ThemeManager() - self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config()) + self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config()) self.game_name = game_name self.target_exe = target_exe # Store the target executable name self.setWindowTitle(_("Launching {0}").format(self.game_name)) @@ -122,7 +122,7 @@ class GameLaunchDialog(QDialog): layout.addWidget(self.progress_bar) # 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.clicked.connect(self.reject) layout.addWidget(self.cancel_button, alignment=Qt.AlignmentFlag.AlignCenter) @@ -172,8 +172,7 @@ class GameLaunchDialog(QDialog): class FileExplorer(QDialog): def __init__(self, parent=None, theme=None, file_filter=None, initial_path=None, directory_only=False): super().__init__(parent) - self.theme_manager = ThemeManager() - self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config()) + self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config()) self.file_signal = FileSelectedSignal() self.file_filter = file_filter # Store the file filter self.directory_only = directory_only # Store the directory_only flag @@ -271,8 +270,8 @@ class FileExplorer(QDialog): # Кнопки self.button_layout = QHBoxLayout() self.button_layout.setSpacing(10) - self.select_button = AutoSizeButton(_("Select"), icon=self.theme_manager.get_icon("apply")) - self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel")) + self.select_button = AutoSizeButton(_("Select"), icon=theme_manager.get_icon("apply")) + self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel")) self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) self.button_layout.addWidget(self.select_button) @@ -405,7 +404,7 @@ class FileExplorer(QDialog): # Добавляем смонтированные диски for drive in drives: 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.setFocusPolicy(Qt.FocusPolicy.StrongFocus) button.clicked.connect(lambda checked, path=drive: self.change_drive(path)) @@ -415,7 +414,7 @@ class FileExplorer(QDialog): # Добавляем избранные папки for folder in favorite_folders: 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.setFocusPolicy(Qt.FocusPolicy.StrongFocus) button.clicked.connect(lambda checked, path=folder: self.change_drive(path)) @@ -483,7 +482,7 @@ class FileExplorer(QDialog): try: if self.current_path != "/": item = QListWidgetItem("../") - folder_icon = self.theme_manager.get_icon("folder") + folder_icon = theme_manager.get_icon("folder") # Ensure the icon is a QIcon if isinstance(folder_icon, str) and os.path.isfile(folder_icon): folder_icon = QIcon(folder_icon) @@ -498,7 +497,7 @@ class FileExplorer(QDialog): # Добавляем директории for d in sorted(dirs): 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 if isinstance(folder_icon, str) and os.path.isfile(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): super().__init__(parent) from portprotonqt.context_menu_manager import CustomLineEdit # Локальный импорт - self.theme_manager = ThemeManager() - self.theme = theme if theme else self.theme_manager.apply_theme(read_theme_from_config()) + self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config()) self.edit_mode = edit_mode self.original_name = game_name self.last_exe_path = exe_path # Store last selected exe path @@ -626,7 +624,7 @@ class AddGameDialog(QDialog): if 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.clicked.connect(self.browseExe) exeBrowseButton.setObjectName("exeBrowseButton") # Для поиска кнопки @@ -648,7 +646,7 @@ class AddGameDialog(QDialog): if 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.clicked.connect(self.browseCover) coverBrowseButton.setObjectName("coverBrowseButton") # Для поиска кнопки @@ -677,8 +675,8 @@ class AddGameDialog(QDialog): # Dialog buttons self.button_layout = QHBoxLayout() self.button_layout.setSpacing(10) - self.select_button = AutoSizeButton(_("Apply"), icon=self.theme_manager.get_icon("apply")) - self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.theme_manager.get_icon("cancel")) + self.select_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply")) + self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel")) self.select_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) self.button_layout.addWidget(self.select_button) diff --git a/portprotonqt/theme_manager.py b/portprotonqt/theme_manager.py index a13d10d..4f36b2e 100644 --- a/portprotonqt/theme_manager.py +++ b/portprotonqt/theme_manager.py @@ -14,6 +14,7 @@ THEMES_DIRS = [ os.path.join(xdg_data_home, "PortProtonQt", "themes"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes") ] +_loaded_theme = None # Запрещенные модули и функции FORBIDDEN_MODULES = { @@ -101,9 +102,13 @@ def load_theme_screenshots(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() fonts_folder = None if theme_name == "standart": @@ -131,9 +136,13 @@ def load_theme_fonts(theme_name): else: logger.error(f"Error loading font: {filename}") -def load_logo(): - logo_path = None + _loaded_theme = theme_name +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") @@ -201,28 +210,34 @@ def load_theme(theme_name): class ThemeManager: """ Класс для управления темами приложения. - - Позволяет получить список доступных тем, загрузить и применить выбранную тему. + Реализует паттерн Singleton для единого экземпляра. """ - def __init__(self): - self.current_theme_name = None - self.current_theme_module = None + _instance = 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() def get_theme_logo(self): - """Возвращает логотип для текущей или указанной темы.""" + """Возвращает логотип текущей темы.""" 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: theme_module = load_theme(theme_name) except FileNotFoundError: @@ -230,6 +245,7 @@ class ThemeManager: theme_module = load_theme("standart") theme_name = "standart" save_theme_to_config("standart") + load_theme_fonts(theme_name) self.current_theme_name = theme_name self.current_theme_module = theme_module