feat(tray): add modal game launch dialog with process detection and cancellation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -4,7 +4,7 @@ import re
|
|||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from PySide6.QtGui import QPixmap, QIcon
|
from PySide6.QtGui import QPixmap, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication
|
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer
|
||||||
from icoextract import IconExtractor, IconExtractorError
|
from icoextract import IconExtractor, IconExtractorError
|
||||||
@@ -16,6 +16,7 @@ import portprotonqt.themes.standart.styles as default_styles
|
|||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.custom_widgets import AutoSizeButton
|
from portprotonqt.custom_widgets import AutoSizeButton
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
|
import psutil
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
@@ -89,6 +90,86 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
|
|||||||
class FileSelectedSignal(QObject):
|
class FileSelectedSignal(QObject):
|
||||||
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
file_selected = Signal(str) # Сигнал с путем к выбранному файлу
|
||||||
|
|
||||||
|
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 = theme if theme else default_styles
|
||||||
|
self.theme_manager = ThemeManager()
|
||||||
|
self.game_name = game_name if game_name else _("Game")
|
||||||
|
self.target_exe = target_exe # Store the target executable name
|
||||||
|
self.setWindowTitle(_("Launching {0}").format(self.game_name))
|
||||||
|
self.setModal(True)
|
||||||
|
self.setFixedSize(400, 200)
|
||||||
|
self.setStyleSheet(self.theme.MESSAGE_BOX_STYLE)
|
||||||
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Game name label
|
||||||
|
label = QLabel(_("Launching {0}...").format(self.game_name))
|
||||||
|
label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Progress bar (indeterminate)
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setStyleSheet(self.theme.PROGRESS_BAR_STYLE)
|
||||||
|
self.progress_bar.setRange(0, 0) # Indeterminate mode
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=self.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)
|
||||||
|
|
||||||
|
# Center dialog on parent
|
||||||
|
if parent:
|
||||||
|
parent_geometry = parent.geometry()
|
||||||
|
center_point = parent_geometry.center()
|
||||||
|
dialog_geometry = self.geometry()
|
||||||
|
dialog_geometry.moveCenter(center_point)
|
||||||
|
self.setGeometry(dialog_geometry)
|
||||||
|
|
||||||
|
# Timer to check if the game process is running
|
||||||
|
self.check_process_timer = QTimer(self)
|
||||||
|
self.check_process_timer.timeout.connect(self.check_target_exe)
|
||||||
|
self.check_process_timer.start(500)
|
||||||
|
|
||||||
|
def is_target_exe_running(self):
|
||||||
|
"""Check if the target executable is running using psutil."""
|
||||||
|
if not self.target_exe:
|
||||||
|
return False
|
||||||
|
for proc in psutil.process_iter(attrs=["name"]):
|
||||||
|
if proc.info["name"].lower() == self.target_exe.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_target_exe(self):
|
||||||
|
"""Check if the game process is running and close the dialog if it is."""
|
||||||
|
if self.is_target_exe_running():
|
||||||
|
logger.info(f"Game {self.game_name} process detected as running, closing launch dialog")
|
||||||
|
self.accept() # Close dialog when game is running
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
elif not hasattr(self.parent(), 'game_processes') or not any(proc.poll() is None for proc in cast("MainWindow", self.parent()).game_processes):
|
||||||
|
# If no child processes are running, stop the timer but keep dialog open
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""Handle dialog cancellation."""
|
||||||
|
logger.info(f"Game launch cancelled for {self.game_name}")
|
||||||
|
self.check_process_timer.stop()
|
||||||
|
self.check_process_timer.deleteLater()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
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)
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
|
import shlex
|
||||||
|
import signal
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication, QMessageBox
|
||||||
from PySide6.QtGui import QIcon, QAction
|
from PySide6.QtGui import QIcon, QAction
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@@ -8,6 +12,7 @@ from portprotonqt.theme_manager import ThemeManager
|
|||||||
import portprotonqt.themes.standart.styles as default_styles
|
import portprotonqt.themes.standart.styles as default_styles
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
from portprotonqt.config_utils import read_favorites, read_theme_from_config, save_theme_to_config
|
||||||
|
from portprotonqt.dialogs import GameLaunchDialog
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -16,18 +21,25 @@ class TrayManager:
|
|||||||
|
|
||||||
Обеспечивает:
|
Обеспечивает:
|
||||||
- Показ/скрытие главного окна по двойному клику на иконку трея.
|
- Показ/скрытие главного окна по двойному клику на иконку трея.
|
||||||
- Контекстное меню с опциями: Show/Hide (переключается в зависимости от состояния окна),
|
- Контекстное меню с опциями: Show/Hide, Favorites, Recent Games, Themes, Exit.
|
||||||
Favorites (быстрый запуск избранных игр), Recent Games (быстрый запуск недавних игр), Themes (быстрая смена тем), Exit.
|
- Динамическое заполнение меню Favorites, Recent Games и Themes.
|
||||||
- Меню Favorites, Recent Games и Themes динамически заполняются при показе (через aboutToShow).
|
- Сворачивание в трей при закрытии окна, полное закрытие через Exit.
|
||||||
- При закрытии окна (крестик) приложение сворачивается в трей, а не закрывается.
|
|
||||||
Полное закрытие только через Exit в меню.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, main_window, app_name: str | None = None, theme=None):
|
def __init__(self, main_window, app_name: str | None = None, theme=None):
|
||||||
self.app_name = app_name if app_name is not None else "PortProtonQt"
|
self.app_name = app_name if app_name is not None else "PortProtonQt"
|
||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
self.theme = theme if theme is not None else default_styles
|
selected_theme = read_theme_from_config()
|
||||||
self.current_theme_name = read_theme_from_config()
|
self.current_theme_name = selected_theme
|
||||||
|
try:
|
||||||
|
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning(f"Тема '{selected_theme}' не найдена, применяется стандартная тема 'standart'")
|
||||||
|
self.theme = self.theme_manager.apply_theme("standart")
|
||||||
|
self.current_theme_name = "standart"
|
||||||
|
save_theme_to_config("standart")
|
||||||
|
if not self.theme:
|
||||||
|
self.theme = default_styles
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.tray_icon = QSystemTrayIcon(self.main_window)
|
self.tray_icon = QSystemTrayIcon(self.main_window)
|
||||||
|
|
||||||
@@ -41,24 +53,19 @@ class TrayManager:
|
|||||||
self.tray_icon.activated.connect(self.handle_tray_click)
|
self.tray_icon.activated.connect(self.handle_tray_click)
|
||||||
self.tray_icon.setToolTip(self.app_name)
|
self.tray_icon.setToolTip(self.app_name)
|
||||||
|
|
||||||
# Контекстное меню
|
|
||||||
self.tray_menu = QMenu()
|
self.tray_menu = QMenu()
|
||||||
self.toggle_action = QAction(_("Show"), self.main_window)
|
self.toggle_action = QAction(_("Show"), self.main_window)
|
||||||
self.toggle_action.triggered.connect(self.toggle_window_action)
|
self.toggle_action.triggered.connect(self.toggle_window_action)
|
||||||
|
|
||||||
# Подменю для избранных игр
|
|
||||||
self.favorites_menu = QMenu(_("Favorites"))
|
self.favorites_menu = QMenu(_("Favorites"))
|
||||||
self.favorites_menu.aboutToShow.connect(self.populate_favorites_menu)
|
self.favorites_menu.aboutToShow.connect(self.populate_favorites_menu)
|
||||||
|
|
||||||
# Подменю для недавних игр
|
|
||||||
self.recent_menu = QMenu(_("Recent Games"))
|
self.recent_menu = QMenu(_("Recent Games"))
|
||||||
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
|
self.recent_menu.aboutToShow.connect(self.populate_recent_menu)
|
||||||
|
|
||||||
# Подменю для тем
|
|
||||||
self.themes_menu = QMenu(_("Themes"))
|
self.themes_menu = QMenu(_("Themes"))
|
||||||
self.themes_menu.aboutToShow.connect(self.populate_themes_menu)
|
self.themes_menu.aboutToShow.connect(self.populate_themes_menu)
|
||||||
|
|
||||||
# Добавляем действия в меню
|
|
||||||
self.tray_menu.addAction(self.toggle_action)
|
self.tray_menu.addAction(self.toggle_action)
|
||||||
self.tray_menu.addSeparator()
|
self.tray_menu.addSeparator()
|
||||||
self.tray_menu.addMenu(self.favorites_menu)
|
self.tray_menu.addMenu(self.favorites_menu)
|
||||||
@@ -74,41 +81,35 @@ class TrayManager:
|
|||||||
self.tray_icon.setContextMenu(self.tray_menu)
|
self.tray_icon.setContextMenu(self.tray_menu)
|
||||||
self.tray_icon.show()
|
self.tray_icon.show()
|
||||||
|
|
||||||
# Флаг для принудительного выхода
|
|
||||||
self.main_window.is_exiting = False
|
self.main_window.is_exiting = False
|
||||||
|
|
||||||
# Переменные для отслеживания двойного клика
|
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
self.click_timer = QTimer()
|
self.click_timer = QTimer()
|
||||||
self.click_timer.setSingleShot(True)
|
self.click_timer.setSingleShot(True)
|
||||||
self.click_timer.timeout.connect(self.reset_click_count)
|
self.click_timer.timeout.connect(self.reset_click_count)
|
||||||
|
|
||||||
|
self.launch_dialog = None
|
||||||
|
|
||||||
def update_toggle_action(self):
|
def update_toggle_action(self):
|
||||||
"""Update toggle_action text based on window visibility."""
|
|
||||||
if self.main_window.isVisible():
|
if self.main_window.isVisible():
|
||||||
self.toggle_action.setText(_("Hide"))
|
self.toggle_action.setText(_("Hide"))
|
||||||
else:
|
else:
|
||||||
self.toggle_action.setText(_("Show"))
|
self.toggle_action.setText(_("Show"))
|
||||||
|
|
||||||
def handle_tray_click(self, reason):
|
def handle_tray_click(self, reason):
|
||||||
"""Обрабатывает клики по иконке трея, отслеживая двойной клик."""
|
|
||||||
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||||||
self.click_count += 1
|
self.click_count += 1
|
||||||
if self.click_count == 1:
|
if self.click_count == 1:
|
||||||
# Запускаем таймер для ожидания второго клика (300 мс - стандартное время для двойного клика)
|
|
||||||
self.click_timer.start(300)
|
self.click_timer.start(300)
|
||||||
elif self.click_count == 2:
|
elif self.click_count == 2:
|
||||||
# Двойной клик зафиксирован
|
|
||||||
self.click_timer.stop()
|
self.click_timer.stop()
|
||||||
self.toggle_window_action()
|
self.toggle_window_action()
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
|
|
||||||
def reset_click_count(self):
|
def reset_click_count(self):
|
||||||
"""Сбрасывает счетчик кликов, если таймер истек."""
|
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
|
|
||||||
def toggle_window_action(self):
|
def toggle_window_action(self):
|
||||||
"""Toggle window visibility and update action text."""
|
|
||||||
if self.main_window.isVisible():
|
if self.main_window.isVisible():
|
||||||
self.main_window.hide()
|
self.main_window.hide()
|
||||||
else:
|
else:
|
||||||
@@ -117,7 +118,6 @@ class TrayManager:
|
|||||||
self.main_window.activateWindow()
|
self.main_window.activateWindow()
|
||||||
|
|
||||||
def populate_favorites_menu(self):
|
def populate_favorites_menu(self):
|
||||||
"""Динамически заполняет меню избранных игр с указанием источника."""
|
|
||||||
self.favorites_menu.clear()
|
self.favorites_menu.clear()
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
if not favorites:
|
if not favorites:
|
||||||
@@ -134,13 +134,12 @@ class TrayManager:
|
|||||||
exec_line, source = game_data
|
exec_line, source = game_data
|
||||||
action_text = f"{fav} ({source})"
|
action_text = f"{fav} ({source})"
|
||||||
action = QAction(action_text, self.main_window)
|
action = QAction(action_text, self.main_window)
|
||||||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
action.triggered.connect(lambda checked=False, el=exec_line, name=fav: self.launch_game_with_dialog(el, name))
|
||||||
self.favorites_menu.addAction(action)
|
self.favorites_menu.addAction(action)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Exec line not found for favorite: {fav}")
|
logger.warning(f"Exec line not found for favorite: {fav}")
|
||||||
|
|
||||||
def populate_recent_menu(self):
|
def populate_recent_menu(self):
|
||||||
"""Динамически заполняет меню недавних игр (топ-5 по timestamp) с указанием источника."""
|
|
||||||
self.recent_menu.clear()
|
self.recent_menu.clear()
|
||||||
if not self.main_window.games:
|
if not self.main_window.games:
|
||||||
no_recent_action = QAction(_("No recent games"), self.main_window)
|
no_recent_action = QAction(_("No recent games"), self.main_window)
|
||||||
@@ -156,45 +155,99 @@ class TrayManager:
|
|||||||
source = game[12]
|
source = game[12]
|
||||||
action_text = f"{game_name} ({source})"
|
action_text = f"{game_name} ({source})"
|
||||||
action = QAction(action_text, self.main_window)
|
action = QAction(action_text, self.main_window)
|
||||||
action.triggered.connect(lambda checked=False, el=exec_line: self.main_window.toggleGame(el))
|
action.triggered.connect(lambda checked=False, el=exec_line, name=game_name: self.launch_game_with_dialog(el, name))
|
||||||
self.recent_menu.addAction(action)
|
self.recent_menu.addAction(action)
|
||||||
|
|
||||||
|
def launch_game_with_dialog(self, exec_line, game_name):
|
||||||
|
"""Launch a game with a modal dialog indicating progress."""
|
||||||
|
try:
|
||||||
|
# Determine target executable
|
||||||
|
target_exe = None
|
||||||
|
if exec_line.startswith("steam://"):
|
||||||
|
# Steam games are handled differently, no target_exe needed
|
||||||
|
self.launch_dialog = GameLaunchDialog(self.main_window, game_name=game_name, theme=self.theme)
|
||||||
|
else:
|
||||||
|
# Extract target executable from exec_line
|
||||||
|
entry_exec_split = shlex.split(exec_line)
|
||||||
|
if entry_exec_split[0] == "env" and len(entry_exec_split) > 2:
|
||||||
|
file_to_check = entry_exec_split[2]
|
||||||
|
elif entry_exec_split[0] == "flatpak" and len(entry_exec_split) > 3:
|
||||||
|
file_to_check = entry_exec_split[3]
|
||||||
|
else:
|
||||||
|
file_to_check = entry_exec_split[0]
|
||||||
|
|
||||||
|
if not os.path.exists(file_to_check):
|
||||||
|
logger.error(f"File not found: {file_to_check}")
|
||||||
|
QMessageBox.warning(self.main_window, _("Error"), _("File not found: {0}").format(file_to_check))
|
||||||
|
return
|
||||||
|
|
||||||
|
target_exe = os.path.basename(file_to_check)
|
||||||
|
self.launch_dialog = GameLaunchDialog(self.main_window, game_name=game_name, theme=self.theme, target_exe=target_exe)
|
||||||
|
|
||||||
|
self.launch_dialog.rejected.connect(lambda: self.cancel_game_launch(exec_line))
|
||||||
|
self.launch_dialog.show()
|
||||||
|
|
||||||
|
self.main_window.toggleGame(exec_line)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to launch game {game_name}: {e}")
|
||||||
|
if self.launch_dialog:
|
||||||
|
self.launch_dialog.reject()
|
||||||
|
self.launch_dialog = None
|
||||||
|
QMessageBox.warning(self.main_window, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||||
|
|
||||||
|
def cancel_game_launch(self, exec_line):
|
||||||
|
"""Cancel the game launch and terminate the process, using MainWindow's stop logic."""
|
||||||
|
if self.main_window.game_processes and self.main_window.target_exe:
|
||||||
|
for proc in self.main_window.game_processes:
|
||||||
|
try:
|
||||||
|
parent = psutil.Process(proc.pid)
|
||||||
|
children = parent.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
child.terminate()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
psutil.wait_procs(children, timeout=5)
|
||||||
|
for child in children:
|
||||||
|
if child.is_running():
|
||||||
|
child.kill()
|
||||||
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
self.main_window.game_processes = []
|
||||||
|
self.main_window.resetPlayButton()
|
||||||
|
if self.launch_dialog:
|
||||||
|
self.launch_dialog.reject()
|
||||||
|
self.launch_dialog = None
|
||||||
|
logger.info(f"Game launch cancelled for exec line: {exec_line}")
|
||||||
|
|
||||||
def populate_themes_menu(self):
|
def populate_themes_menu(self):
|
||||||
"""Динамически заполняет меню тем, позволяя переключать доступные темы."""
|
|
||||||
self.themes_menu.clear()
|
self.themes_menu.clear()
|
||||||
available_themes = self.theme_manager.get_available_themes()
|
available_themes = self.theme_manager.get_available_themes()
|
||||||
current_theme = read_theme_from_config()
|
|
||||||
|
|
||||||
for theme_name in sorted(available_themes):
|
for theme_name in sorted(available_themes):
|
||||||
action = QAction(theme_name, self.main_window)
|
action = QAction(theme_name, self.main_window)
|
||||||
action.setCheckable(True)
|
action.setCheckable(True)
|
||||||
action.setChecked(theme_name == current_theme)
|
action.setChecked(theme_name == self.current_theme_name)
|
||||||
action.triggered.connect(lambda checked=False, tn=theme_name: self.switch_theme(tn))
|
action.triggered.connect(lambda checked=False, tn=theme_name: self.switch_theme(tn))
|
||||||
self.themes_menu.addAction(action)
|
self.themes_menu.addAction(action)
|
||||||
|
|
||||||
def switch_theme(self, theme_name: str):
|
def switch_theme(self, theme_name: str):
|
||||||
"""Сохраняет выбранную тему и перезапускает приложение для применения изменений."""
|
|
||||||
try:
|
try:
|
||||||
# Сохраняем новую тему в конфигурации
|
|
||||||
save_theme_to_config(theme_name)
|
save_theme_to_config(theme_name)
|
||||||
logger.info(f"Saved theme {theme_name}, restarting application to apply changes")
|
logger.info(f"Saved theme {theme_name}, restarting application to apply changes")
|
||||||
|
|
||||||
# Получаем текущий исполняемый файл и аргументы
|
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
args = sys.argv
|
args = sys.argv
|
||||||
|
|
||||||
# Закрываем текущее приложение
|
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
# Перезапускаем приложение
|
|
||||||
subprocess.Popen([executable] + args)
|
subprocess.Popen([executable] + args)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to switch theme to {theme_name}: {e}")
|
logger.error(f"Failed to switch theme to {theme_name}: {e}")
|
||||||
# В случае ошибки сохраняем стандартную тему
|
|
||||||
save_theme_to_config("standart")
|
save_theme_to_config("standart")
|
||||||
# Перезапускаем приложение с дефолтной темой
|
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
args = sys.argv
|
args = sys.argv
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
@@ -202,7 +255,6 @@ class TrayManager:
|
|||||||
subprocess.Popen([executable] + args)
|
subprocess.Popen([executable] + args)
|
||||||
|
|
||||||
def force_exit(self):
|
def force_exit(self):
|
||||||
"""Принудительно закрывает приложение."""
|
|
||||||
self.main_window.is_exiting = True
|
self.main_window.is_exiting = True
|
||||||
self.main_window.close()
|
self.main_window.close()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
Reference in New Issue
Block a user