16 Commits

Author SHA1 Message Date
647394ca92 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 09:34:24 +05:00
14dc44d4f7 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 09:25:31 +05:00
34e70d05f3 feat: add continuous D-pad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 09:20:53 +05:00
a21705da15 feat: hide the games from EGS until after the workout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 09:11:52 +05:00
1ea5fd710c feat: added --fullscreen cli argument
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 09:07:18 +05:00
4de4bdb99d feat: added system overlay to guide button
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-08 07:16:02 +05:00
bcf319c024 feat: optimize slider code
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 22:11:36 +05:00
83455bc33f fix: restore correct badge positioning on visibility change in GameCard
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 21:40:27 +05:00
2377426b27 fix: correct badge positioning in GameCard on display filter change (again)
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 21:31:07 +05:00
a5977f0f59 fix: correct badge positioning in GameCard.update_badge_visibility
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 19:08:53 +05:00
3e49357152 feat(build): add appstream metainfo files
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 19:02:24 +05:00
9c4ad0b7ba chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 15:28:41 +05:00
0f59c46d36 fix(input_manager): handle AddGameDialog navigation with D-pad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 15:26:37 +05:00
364e1dd02a feat(input_manager): Added QComboBox and QListView handler for Gamepad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 15:16:42 +05:00
c037af4314 feat(input_manager): Added QMenu handler for Gamepad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 11:21:51 +05:00
2ae3831662 fix(input_manager): restore keyboard navigation with Up/Down keys
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-07 10:39:50 +05:00
21 changed files with 536 additions and 297 deletions

View File

@@ -7,8 +7,6 @@
### Added
- Кнопки сброса настроек и очистки кэша
- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary)
- Бейдж EGS
- Бейдж PortProton
- Зависимость на `xdg-utils`
- Интеграция статуса WeAntiCheatYet в карточку
@@ -24,6 +22,7 @@
- Пункт в контекстное меню "Удалить из Steam”
- Метод сортировки сначала избранное
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
- Обработчики для QMenu и QComboBox на геймпаде
### Changed
- Обновлены все иконки
@@ -37,8 +36,12 @@
- Установка ширины бейджа в две трети ширины карточки
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
- Теперь D-pad можно зажимать для переключения карточек
- D-pad больше не переключает вкладки только RB и LB
- Кнопка добавления игры больше не фокусируется
- Диалог добавления игры теперь открывается только в библиотеке
- Аргумент --fullscreen для открытия приложения в режиме полноэкранного отображения
- Оверлей на кнопку Xbox / PS для закрытия приложения, выключения, перезагрузки и ухода в сон
### Fixed
- Обработка несуществующей темы с возвратом к “standart”

View File

@@ -7,7 +7,7 @@
## В планах
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
- [ ] Добавить возможность управление с геймпада
- [X] Добавить возможность управление с геймпада
- [ ] Добавить возможность управление с тачскрина
- [X] Добавить возможность управление с мыши и клавиатуры
- [X] Добавить систему тем [Документация](documentation/theme_guide)
@@ -16,6 +16,7 @@
- [ ] Продумать систему вкладок вместо той что есть сейчас
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
- [X] Брать описание и названия игр с базы данных Steam
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска

View File

@@ -45,7 +45,7 @@ Requires: perl-Image-ExifTool
Requires: xdg-utils
%description -n python3-%{pypi_name}-git
PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
%prep
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
@@ -62,6 +62,8 @@ cp -r build-aux/share %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/*
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%changelog

View File

@@ -42,7 +42,7 @@ Requires: perl-Image-ExifTool
Requires: xdg-utils
%description -n python3-%{pypi_name}
PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
%prep
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
@@ -61,6 +61,8 @@ cp -r build-aux/share %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/*
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%changelog

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<name>PortProtonQt</name>
<id>ru.linux_gaming.PortProtonQt</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<summary>Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store</summary>
<summary xml:lang="ru">Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store</summary>
<description>
<p>This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.</p>
</description>
<launchable type="desktop-id">ru.linux_gaming.PortProtonQt.desktop</launchable>
<developer id="ru.linux_gaming">
<name>Boria138</name>
</developer>
<recommends>
<control>keyboard</control>
<control>pointing</control>
<control>touch</control>
<control>gamepad</control>
</recommends>
<branding>
<color type="primary" scheme_preference="light">#007AFF</color>
<color type="primary" scheme_preference="dark">#09BEC8</color>
</branding>
<categories>
<category>Game</category>
<category>Utility</category>
</categories>
<url type="homepage">https://git.linux-gaming.ru/Boria138/PortProtonQt</url>
<url type="bugtracker">https://git.linux-gaming.ru/Boria138/PortProtonQt/issues</url>
<screenshots>
<screenshot type="default">
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0.png</image>
<caption>Library</caption>
<caption xml:lang="ru">Библиотека</caption>
</screenshot>
<screenshot>
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0.png</image>
<caption>Card detail page</caption>
<caption xml:lang="ru">Детали игры</caption>
</screenshot>
<screenshot>
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/src/commit/9c4ad0b7bacac08849aff9036561de7b88a9bad2/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png</image>
<caption>Settings</caption>
<caption xml:lang="ru">Настройки</caption>
</screenshot>
</screenshots>
<keywords>
<keyword translate="no">wine</keyword>
<keyword translate="no">proton</keyword>
<keyword translate="no">steam</keyword>
<keyword translate="no">windows</keyword>
<keyword translate="no">epic games store</keyword>
<keyword translate="no">egs</keyword>
<keyword translate="no">qt</keyword>
<keyword translate="no">portproton</keyword>
<keyword>games</keyword>
</keywords>
<content_rating type="oars-1.1" />
</component>

View File

@@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 of 154 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 153 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 153 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 of 153 |
---

View File

@@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 из 154 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 из 153 |
---

View File

@@ -4,8 +4,9 @@ from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
from portprotonqt.main_window import MainWindow
from portprotonqt.tray import SystemTray
from portprotonqt.config_utils import read_theme_from_config
from portprotonqt.config_utils import read_theme_from_config, save_fullscreen_config
from portprotonqt.logger import get_logger
from portprotonqt.cli import parse_args
logger = get_logger(__name__)
@@ -28,7 +29,17 @@ def main():
else:
logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
# Парсинг аргументов командной строки
args = parse_args()
window = MainWindow()
# Обработка флага --fullscreen
if args.fullscreen:
logger.info("Запуск в полноэкранном режиме по флагу --fullscreen")
save_fullscreen_config(True)
window.showFullScreen()
current_theme_name = read_theme_from_config()
tray = SystemTray(app, current_theme_name)
tray.show_action.triggered.connect(window.show)
@@ -43,7 +54,9 @@ def main():
tray.hide_action.triggered.connect(window.hide)
window.settings_saved.connect(recreate_tray)
window.show()
sys.exit(app.exec())
if __name__ == '__main__':

16
portprotonqt/cli.py Normal file
View File

@@ -0,0 +1,16 @@
import argparse
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
def parse_args():
"""
Парсит аргументы командной строки.
"""
parser = argparse.ArgumentParser(description="PortProtonQT CLI")
parser.add_argument(
"--fullscreen",
action="store_true",
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
)
return parser.parse_args()

View File

@@ -9,6 +9,7 @@ from PySide6.QtGui import QDesktopServices
from portprotonqt.config_utils import parse_desktop_entry
from portprotonqt.localization import _
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
from portprotonqt.dialogs import AddGameDialog
class ContextMenuManager:
"""Manages context menu actions for game management in PortProtonQT."""
@@ -321,7 +322,6 @@ class ContextMenuManager:
def edit_game_shortcut(self, game_name, exec_line, cover_path):
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
from portprotonqt.dialogs import AddGameDialog # Local import to avoid circular dependency
if not self._check_portproton():
return

View File

@@ -261,41 +261,40 @@ class GameCard(QFrame):
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites"))
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
# Обновляем видимость бейджей
self.steamLabel.setVisible(self.steam_visible)
self.egsLabel.setVisible(self.egs_visible)
self.portprotonLabel.setVisible(self.portproton_visible)
self.protondbLabel.setVisible(protondb_visible)
self.anticheatLabel.setVisible(anticheat_visible)
# Reposition badges
# Подготавливаем список всех бейджей с их текущей видимостью
badges = [
(self.steam_visible, self.steamLabel),
(self.egs_visible, self.egsLabel),
(self.portproton_visible, self.portprotonLabel),
(protondb_visible, self.protondbLabel),
(anticheat_visible, self.anticheatLabel),
]
# Пересчитываем позиции бейджей
right_margin = 8
badge_spacing = 5
top_y = 10
badge_y_positions = []
badge_width = int(self.coverLabel.width() * 2/3)
if self.steam_visible:
steam_x = self.coverLabel.width() - badge_width - right_margin
self.steamLabel.move(steam_x, top_y)
badge_y_positions.append(top_y + self.steamLabel.height())
if self.egs_visible:
egs_x = self.coverLabel.width() - badge_width - right_margin
egs_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
self.egsLabel.move(egs_x, egs_y)
badge_y_positions.append(egs_y + self.egsLabel.height())
if self.portproton_visible:
portproton_x = self.coverLabel.width() - badge_width - right_margin
portproton_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
self.portprotonLabel.move(portproton_x, portproton_y)
badge_y_positions.append(portproton_y + self.portprotonLabel.height())
if self.protondbLabel.isVisible():
protondb_x = self.coverLabel.width() - badge_width - right_margin
protondb_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
self.protondbLabel.move(protondb_x, protondb_y)
badge_y_positions.append(protondb_y + self.protondbLabel.height())
if self.anticheatLabel.isVisible():
anticheat_x = self.coverLabel.width() - badge_width - right_margin
anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
self.anticheatLabel.move(anticheat_x, anticheat_y)
for is_visible, badge in badges:
if is_visible:
badge_x = self.coverLabel.width() - badge_width - right_margin
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
badge.move(badge_x, badge_y)
badge_y_positions.append(badge_y + badge.height())
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
self.anticheatLabel.raise_()
self.protondbLabel.raise_()
self.portprotonLabel.raise_()

View File

@@ -3,8 +3,8 @@ import threading
from typing import Protocol, cast
from evdev import InputDevice, ecodes, list_devices
import pyudev
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent
from portprotonqt.logger import get_logger
from portprotonqt.image_utils import FullscreenDialog
@@ -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
@@ -37,11 +39,12 @@ BUTTONS = {
'confirm': {ecodes.BTN_A},
'back': {ecodes.BTN_B},
'add_game': {ecodes.BTN_Y},
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
'prev_tab': {ecodes.BTN_TL},
'next_tab': {ecodes.BTN_TR},
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
'context_menu': {ecodes.BTN_START},
'menu': {ecodes.BTN_SELECT},
'guide': {ecodes.BTN_MODE},
}
class InputManager(QObject):
@@ -69,7 +72,6 @@ class InputManager(QObject):
self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
self.axis_deadzone = axis_deadzone
self.initial_axis_move_delay = initial_axis_move_delay
self.repeat_axis_move_delay = repeat_axis_move_delay
@@ -81,6 +83,12 @@ class InputManager(QObject):
self.running = True
self._is_fullscreen = read_fullscreen_config()
# Add variables for continuous D-pad movement
self.dpad_timer = QTimer(self)
self.dpad_timer.timeout.connect(self.handle_dpad_repeat)
self.current_dpad_code = None # Tracks the current D-pad axis (e.g., ABS_HAT0X, ABS_HAT0Y)
self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1)
# Connect signals to slots
self.button_pressed.connect(self.handle_button_slot)
self.dpad_moved.connect(self.handle_dpad_slot)
@@ -129,10 +137,66 @@ class InputManager(QObject):
return
active = QApplication.activeWindow()
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']:
if popup.activeAction():
popup.activeAction().trigger()
popup.close()
return
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
popup.close()
return
return
# Handle QComboBox
if isinstance(focused, QComboBox):
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
focused.showPopup()
return
# Handle QListView
if isinstance(focused, QListView):
combo = None
parent = focused.parentWidget()
while parent:
if isinstance(parent, QComboBox):
combo = parent
break
parent = parent.parentWidget()
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
idx = focused.currentIndex()
if idx.isValid():
if combo:
combo.setCurrentIndex(idx.row())
combo.hidePopup()
combo.setFocus(Qt.FocusReason.OtherFocusReason)
else:
focused.activated.emit(idx)
focused.clicked.emit(idx)
focused.hide()
return
if button_code in BUTTONS['back']:
if combo:
combo.hidePopup()
combo.setFocus(Qt.FocusReason.OtherFocusReason)
else:
focused.clearSelection()
focused.hide()
# Закрытие AddGameDialog на кнопку B
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
active.reject() # Закрываем диалог
active.reject()
return
# FullscreenDialog
@@ -149,7 +213,9 @@ class InputManager(QObject):
if isinstance(focused, GameCard):
if button_code in BUTTONS['context_menu']:
pos = QPoint(focused.width() // 2, focused.height() // 2)
focused._show_context_menu(pos)
menu = focused._show_context_menu(pos)
if menu:
menu.setFocus(Qt.FocusReason.OtherFocusReason)
return
# Game launch on detail page
@@ -164,6 +230,8 @@ class InputManager(QObject):
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
elif button_code in BUTTONS['add_game']:
# Only open AddGameDialog if in library tab (index 0)
if self._parent.stackedWidget.currentIndex() == 0:
self._parent.openAddGameDialog()
elif button_code in BUTTONS['prev_tab']:
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
@@ -176,6 +244,14 @@ class InputManager(QObject):
except Exception as e:
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
def handle_dpad_repeat(self) -> None:
"""Handle repeated D-pad input while the D-pad is held."""
if self.current_dpad_code is not None and self.current_dpad_value != 0:
now = time.time()
if (now - self.last_move_time) >= self.current_axis_delay:
self.handle_dpad_slot(self.current_dpad_code, self.current_dpad_value, now)
self.last_move_time = now
self.current_axis_delay = self.repeat_axis_move_delay
@Slot(int, int, float)
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
@@ -188,6 +264,71 @@ class InputManager(QObject):
if not app:
return
active = QApplication.activeWindow()
focused = QApplication.focusWidget()
popup = QApplication.activePopupWidget()
# Update D-pad state
if value != 0:
self.current_dpad_code = code
self.current_dpad_value = value
if not self.axis_moving:
self.axis_moving = True
self.last_move_time = current_time
self.current_axis_delay = self.initial_axis_move_delay
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
else:
self.current_dpad_code = None
self.current_dpad_value = 0
self.axis_moving = False
self.current_axis_delay = self.initial_axis_move_delay
self.dpad_timer.stop() # Stop timer when D-pad is released
return
# 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
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
if focusables:
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
return
if value > 0: # Down
active.focusNextChild()
elif value < 0: # Up
active.focusPreviousChild()
return
# Handle QMenu navigation with D-pad
if isinstance(popup, QMenu):
if code == ecodes.ABS_HAT0Y and value != 0:
actions = popup.actions()
if actions:
current_idx = actions.index(popup.activeAction()) if popup.activeAction() in actions else 0
if value < 0: # Up
next_idx = (current_idx - 1) % len(actions)
popup.setActiveAction(actions[next_idx])
elif value > 0: # Down
next_idx = (current_idx + 1) % len(actions)
popup.setActiveAction(actions[next_idx])
return
return
# Handle QListView navigation with D-pad
if isinstance(focused, QListView) and code == ecodes.ABS_HAT0Y and value != 0:
model = focused.model()
current_index = focused.currentIndex()
if model and current_index.isValid():
row_count = model.rowCount()
current_row = current_index.row()
if value > 0: # Down
next_row = min(current_row + 1, row_count - 1)
focused.setCurrentIndex(model.index(next_row, current_index.column()))
elif value < 0: # Up
prev_row = max(current_row - 1, 0)
focused.setCurrentIndex(model.index(prev_row, current_index.column()))
focused.scrollTo(focused.currentIndex(), QListView.ScrollHint.PositionAtCenter)
return
# Fullscreen horizontal navigation
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
@@ -197,19 +338,6 @@ class InputManager(QObject):
active.show_next()
return
# Handle repeated D-pad movement
if value != 0:
if not self.axis_moving:
self.axis_moving = True
elif (current_time - self.last_move_time) < self.current_axis_delay:
return
self.last_move_time = current_time
self.current_axis_delay = self.repeat_axis_move_delay
else:
self.axis_moving = False
self.current_axis_delay = self.initial_axis_move_delay
return
# Library tab navigation (index 0)
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
focused = QApplication.focusWidget()
@@ -280,7 +408,6 @@ class InputManager(QObject):
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
if value > 0: # Down
next_row_idx = current_row_idx + 1
@@ -390,6 +517,23 @@ class InputManager(QObject):
focused._show_context_menu(pos)
return True
# Handle Up/Down keys for non-GameCard tabs
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not isinstance(focused, GameCard):
page = self._parent.stackedWidget.currentWidget()
if key == Qt.Key.Key_Down:
if isinstance(focused, NavLabel):
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
if focusables:
focusables[0].setFocus()
return True
elif focused:
focused.focusNextChild()
return True
elif key == Qt.Key.Key_Up and focused:
focused.focusPreviousChild()
return True
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
@@ -520,6 +664,9 @@ class InputManager(QObject):
if focusables:
focusables[0].setFocus()
return True
elif focused:
focused.focusNextChild()
return True
# Navigate up through tab content
if key == Qt.Key.Key_Up:
if isinstance(focused, NavLabel):
@@ -540,6 +687,8 @@ class InputManager(QObject):
elif key == Qt.Key.Key_E:
if isinstance(focused, QLineEdit):
return False
# Only open AddGameDialog if in library tab (index 0)
if self._parent.stackedWidget.currentIndex() == 0:
self._parent.openAddGameDialog()
return True
@@ -652,6 +801,7 @@ class InputManager(QObject):
def cleanup(self) -> None:
try:
self.running = False
self.dpad_timer.stop()
if self.gamepad_thread:
self.gamepad_thread.join()
if self.gamepad:

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
"POT-Creation-Date: 2025-06-08 09:31+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -362,21 +362,6 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@@ -392,22 +377,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@@ -505,6 +474,33 @@ msgstr ""
msgid "Launching"
msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot"
msgstr ""
msgid "Shutdown"
msgstr ""
msgid "Suspend"
msgstr ""
msgid "Exit Application"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Failed to reboot the system"
msgstr ""
msgid "Failed to shutdown the system"
msgstr ""
msgid "Failed to suspend the system"
msgstr ""
msgid "just now"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
"POT-Creation-Date: 2025-06-08 09:31+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -362,21 +362,6 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@@ -392,22 +377,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@@ -505,6 +474,33 @@ msgstr ""
msgid "Launching"
msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot"
msgstr ""
msgid "Shutdown"
msgstr ""
msgid "Suspend"
msgstr ""
msgid "Exit Application"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Failed to reboot the system"
msgstr ""
msgid "Failed to shutdown the system"
msgstr ""
msgid "Failed to suspend the system"
msgstr ""
msgid "just now"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQT 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
"POT-Creation-Date: 2025-06-08 09:31+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -360,21 +360,6 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
msgid "Open Legendary Login"
msgstr ""
msgid "Legendary Authentication:"
msgstr ""
msgid "Enter Legendary Authorization Code"
msgstr ""
msgid "Authorization Code:"
msgstr ""
msgid "Submit Code"
msgstr ""
msgid "Save Settings"
msgstr ""
@@ -390,22 +375,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
msgid "Please enter an authorization code"
msgstr ""
msgid "Successfully authenticated with Legendary"
msgstr ""
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr ""
msgid "Legendary executable not found"
msgstr ""
msgid "Unexpected error during authentication"
msgstr ""
msgid "Confirm Reset"
msgstr ""
@@ -503,6 +472,33 @@ msgstr ""
msgid "Launching"
msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot"
msgstr ""
msgid "Shutdown"
msgstr ""
msgid "Suspend"
msgstr ""
msgid "Exit Application"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Failed to reboot the system"
msgstr ""
msgid "Failed to shutdown the system"
msgstr ""
msgid "Failed to suspend the system"
msgstr ""
msgid "just now"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
"PO-Revision-Date: 2025-06-06 20:01+0500\n"
"POT-Creation-Date: 2025-06-08 09:31+0500\n"
"PO-Revision-Date: 2025-06-08 09:31+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -369,21 +369,6 @@ msgstr "Режим полноэкранного отображения прил
msgid "Auto Fullscreen on Gamepad connected:"
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
msgid "Open Legendary Login"
msgstr "Открыть браузер для входа в Legendary"
msgid "Legendary Authentication:"
msgstr "Авторизация в Legendary:"
msgid "Enter Legendary Authorization Code"
msgstr "Введите код авторизации Legendary"
msgid "Authorization Code:"
msgstr "Код авторизации:"
msgid "Submit Code"
msgstr "Отправить код"
msgid "Save Settings"
msgstr "Сохранить настройки"
@@ -399,22 +384,6 @@ msgstr "Открытие страницы входа в Legendary в брауз
msgid "Failed to open Legendary login page"
msgstr "Не удалось открыть страницу входа в Legendary"
msgid "Please enter an authorization code"
msgstr "Пожалуйста, введите код авторизации"
msgid "Successfully authenticated with Legendary"
msgstr "Успешная аутентификация с Legendary"
#, python-brace-format
msgid "Legendary authentication failed: {0}"
msgstr "Сбой аутентификации в Legendary: {0}"
msgid "Legendary executable not found"
msgstr "Не найден исполняемый файл Legendary"
msgid "Unexpected error during authentication"
msgstr "Неожиданная ошибка при аутентификации"
msgid "Confirm Reset"
msgstr "Подтвердите удаление"
@@ -514,6 +483,33 @@ msgstr "Невозможно запустить игру пока запущен
msgid "Launching"
msgstr "Идёт запуск"
msgid "System Overlay"
msgstr "Системный оверлей"
msgid "Reboot"
msgstr "Перезагрузить"
msgid "Shutdown"
msgstr "Выключить"
msgid "Suspend"
msgstr "Перейти в ждущий режим"
msgid "Exit Application"
msgstr "Выйти из приложения"
msgid "Cancel"
msgstr "Отмена"
msgid "Failed to reboot the system"
msgstr "Не удалось перезагрузить систему"
msgid "Failed to shutdown the system"
msgstr "Не удалось завершить работу системы"
msgid "Failed to suspend the system"
msgstr "Не удалось перейти в ждущий режим"
msgid "just now"
msgstr "только что"

View File

@@ -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
@@ -259,25 +260,19 @@ class MainWindow(QMainWindow):
self.update_status_message.emit
)
elif display_filter == "favorites":
def on_all_games(portproton_games, steam_games, epic_games):
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
def on_all_games(portproton_games, steam_games):
games = [game for game in portproton_games + steam_games if game[0] in favorites]
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
lambda sg: load_egs_games_async(
self.legendary_path,
lambda eg: on_all_games(pg, sg, eg),
self.downloader,
self.update_progress.emit,
self.update_status_message.emit
)
lambda sg: on_all_games(pg, sg)
)
)
else:
def on_all_games(portproton_games, steam_games, epic_games):
def on_all_games(portproton_games, steam_games):
seen = set()
games = []
for game in portproton_games + steam_games + epic_games:
for game in portproton_games + steam_games:
name = game[0]
if name not in seen:
seen.add(name)
@@ -285,13 +280,7 @@ class MainWindow(QMainWindow):
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
lambda sg: load_egs_games_async(
self.legendary_path,
lambda eg: on_all_games(pg, sg, eg),
self.downloader,
self.update_progress.emit,
self.update_status_message.emit
)
lambda sg: on_all_games(pg, sg)
)
)
return []
@@ -500,6 +489,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)
@@ -539,6 +533,12 @@ class MainWindow(QMainWindow):
def startSearchDebounce(self, text):
self.searchDebounceTimer.start()
def on_slider_value_changed(self, value: int):
self.card_width = value
self.sizeSlider.setToolTip(f"{value} px")
save_card_size(value)
self.updateGameGrid()
def filterGamesDelayed(self):
"""Filters games based on search text and updates the grid."""
text = self.searchEdit.text().strip().lower()
@@ -579,33 +579,16 @@ class MainWindow(QMainWindow):
self.sizeSlider.setFixedWidth(150)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout)
self.sliderDebounceTimer = QTimer(self)
self.sliderDebounceTimer.setSingleShot(True)
self.sliderDebounceTimer.setInterval(40)
def on_slider_value_changed():
self.setUpdatesEnabled(False)
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.updateGameGrid()
self.setUpdatesEnabled(True)
self.sizeSlider.valueChanged.connect(lambda val: self.sliderDebounceTimer.start())
self.sliderDebounceTimer.timeout.connect(on_slider_value_changed)
def calculate_card_width():
available_width = scrollArea.width() - 20
spacing = self.gamesListLayout._spacing
target_cards_per_row = 8
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
calculated_width = max(200, min(calculated_width, 250))
if not self.sizeSlider.value() == self.card_width:
self.card_width = calculated_width
self.sizeSlider.setValue(self.card_width)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.updateGameGrid()
QTimer.singleShot(0, calculate_card_width)
@@ -621,7 +604,6 @@ class MainWindow(QMainWindow):
self._last_width = self.width()
if abs(self.width() - self._last_width) > 10:
self._last_width = self.width()
self.sliderDebounceTimer.start()
def loadVisibleImages(self):
visible_region = self.gamesListWidget.visibleRegion()
@@ -742,6 +724,7 @@ class MainWindow(QMainWindow):
return
dialog = AddGameDialog(self, self.theme)
dialog.setFocus(Qt.FocusReason.OtherFocusReason)
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
# Предзаполняем путь к .exe при drag-and-drop
@@ -920,7 +903,7 @@ class MainWindow(QMainWindow):
# 3. Games display_filter
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
self.gamesDisplayCombo = QComboBox()
self.gamesDisplayCombo.addItems(self.filter_labels)
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
@@ -989,37 +972,6 @@ class MainWindow(QMainWindow):
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
# 7. Legendary Authentication
self.legendaryAuthButton = AutoSizeButton(
_("Open Legendary Login"),
icon=self.theme_manager.get_icon("login")
)
self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
self.legendaryCodeEdit = QLineEdit()
self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
self.submitCodeButton = AutoSizeButton(
_("Submit Code"),
icon=self.theme_manager.get_icon("save")
)
self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
formLayout.addRow(QLabel(""), self.submitCodeButton)
layout.addLayout(formLayout)
# Кнопки
@@ -1070,37 +1022,6 @@ class MainWindow(QMainWindow):
logger.error(f"Failed to open Legendary login page: {e}")
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
def submitLegendaryCode(self):
"""Submits the Legendary authorization code using the legendary CLI."""
auth_code = self.legendaryCodeEdit.text().strip()
if not auth_code:
QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
return
try:
# Execute legendary auth command
result = subprocess.run(
[self.legendary_path, "auth", "--code", auth_code],
capture_output=True,
text=True,
check=True
)
logger.info("Legendary authentication successful: %s", result.stdout)
self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
self.legendaryCodeEdit.clear()
# Reload Epic Games Store games after successful authentication
self.games = self.loadGames()
self.updateGameGrid()
except subprocess.CalledProcessError as e:
logger.error("Legendary authentication failed: %s", e.stderr)
self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
except FileNotFoundError:
logger.error("Legendary executable not found at %s", self.legendary_path)
self.statusBar().showMessage(_("Legendary executable not found"), 5000)
except Exception as e:
logger.error("Unexpected error during Legendary authentication: %s", str(e))
self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
def resetSettings(self):
"""Сбрасывает настройки и перезапускает приложение."""
reply = QMessageBox.question(

View File

@@ -0,0 +1,87 @@
import subprocess
from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, 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)
# 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 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()