From 4de4bdb99ddda46a28b7381616fb6c22b0d70645 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 07:16:02 +0500 Subject: [PATCH] feat: added system overlay to guide button Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 11 +++- portprotonqt/main_window.py | 6 ++ portprotonqt/system_overlay.py | 100 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 portprotonqt/system_overlay.py diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 2f0caff..a9e29d5 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -25,6 +25,8 @@ class MainWindowProtocol(Protocol): ... def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None: ... + def openSystemOverlay(self) -> None: + ... stackedWidget: QStackedWidget tabButtons: dict[int, QWidget] gamesListWidget: QWidget @@ -42,6 +44,7 @@ BUTTONS = { 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, 'context_menu': {ecodes.BTN_START}, 'menu': {ecodes.BTN_SELECT}, + 'guide': {ecodes.BTN_MODE}, } class InputManager(QObject): @@ -131,6 +134,12 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() + # Handle Guide button to open system overlay + if button_code in BUTTONS['guide']: + if not popup and not isinstance(active, QDialog): + self._parent.openSystemOverlay() + return + # Handle QMenu (context menu) if isinstance(popup, QMenu): if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: @@ -244,7 +253,7 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() - # Handle AddGameDialog navigation with D-pad + # Handle SystemOverlay or AddGameDialog navigation with D-pad if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: if not focused or not active.focusWidget(): # If no widget is focused, focus the first focusable widget diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index df5a9fa..90a5f6b 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -13,6 +13,7 @@ from portprotonqt.game_card import GameCard from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel from portprotonqt.input_manager import InputManager from portprotonqt.context_menu_manager import ContextMenuManager +from portprotonqt.system_overlay import SystemOverlay from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games @@ -500,6 +501,11 @@ class MainWindow(QMainWindow): btn.setChecked(i == index) self.stackedWidget.setCurrentIndex(index) + def openSystemOverlay(self): + """Opens the system overlay dialog.""" + overlay = SystemOverlay(self, self.theme) + overlay.exec() + def createSearchWidget(self) -> tuple[QWidget, QLineEdit]: self.container = QWidget() self.container.setStyleSheet(self.theme.CONTAINER_STYLE) diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py new file mode 100644 index 0000000..a64b45e --- /dev/null +++ b/portprotonqt/system_overlay.py @@ -0,0 +1,100 @@ +import subprocess +from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QLabel, QMessageBox +from PySide6.QtWidgets import QApplication +from PySide6.QtCore import Qt +from portprotonqt.logger import get_logger +from portprotonqt.localization import _ + +logger = get_logger(__name__) + +class SystemOverlay(QDialog): + """Overlay dialog for system actions like reboot, sleep, shutdown, suspend, and exit.""" + def __init__(self, parent, theme): + super().__init__(parent) + self.theme = theme + self.setWindowTitle(_("System Overlay")) + self.setModal(True) + self.setFixedSize(400, 300) + + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(10) + + title = QLabel(_("System Actions")) + title.setStyleSheet(self.theme.TAB_TITLE_STYLE) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # Reboot button + reboot_button = QPushButton(_("Reboot")) + #reboot_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + reboot_button.clicked.connect(self.reboot) + layout.addWidget(reboot_button) + + # Shutdown button + shutdown_button = QPushButton(_("Shutdown")) + #shutdown_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + shutdown_button.clicked.connect(self.shutdown) + layout.addWidget(shutdown_button) + + # Suspend button + suspend_button = QPushButton(_("Suspend")) + #suspend_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + suspend_button.clicked.connect(self.suspend) + layout.addWidget(suspend_button) + + # Exit application button + exit_button = QPushButton(_("Exit Application")) + #exit_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + exit_button.clicked.connect(self.exit_application) + layout.addWidget(exit_button) + + # Cancel button + cancel_button = QPushButton(_("Cancel")) + #cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + cancel_button.clicked.connect(self.reject) + layout.addWidget(cancel_button) + + # Set focus to the first button + reboot_button.setFocus() + + def reboot(self): + try: + subprocess.run(["systemctl", "reboot"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to reboot: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to reboot the system")) + self.accept() + + def sleep(self): + try: + subprocess.run(["systemctl", "suspend-then-hibernate"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to sleep: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to put the system to sleep")) + self.accept() + + def shutdown(self): + try: + subprocess.run(["systemctl", "poweroff"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to shutdown: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to shutdown the system")) + self.accept() + + def suspend(self): + try: + subprocess.run(["systemctl", "suspend"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to suspend: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to suspend the system")) + self.accept() + + def exit_application(self): + QApplication.quit() + self.accept()