forked from Boria138/PortProtonQt
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
bcf319c024 | |||
83455bc33f | |||
2377426b27 | |||
a5977f0f59 | |||
3e49357152 | |||
9c4ad0b7ba | |||
0f59c46d36 | |||
364e1dd02a | |||
c037af4314 | |||
2ae3831662 |
@ -24,6 +24,7 @@
|
|||||||
- Пункт в контекстное меню "Удалить из Steam”
|
- Пункт в контекстное меню "Удалить из Steam”
|
||||||
- Метод сортировки сначала избранное
|
- Метод сортировки сначала избранное
|
||||||
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
|
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
|
||||||
|
- Обработчики для QMenu и QComboBox на геймпаде
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Обновлены все иконки
|
- Обновлены все иконки
|
||||||
@ -39,6 +40,7 @@
|
|||||||
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
|
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
|
||||||
- D-pad больше не переключает вкладки только RB и LB
|
- D-pad больше не переключает вкладки только RB и LB
|
||||||
- Кнопка добавления игры больше не фокусируется
|
- Кнопка добавления игры больше не фокусируется
|
||||||
|
- Диалог добавления игры теперь открывается только в библиотеке
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Обработка несуществующей темы с возвратом к “standart”
|
- Обработка несуществующей темы с возвратом к “standart”
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
## В планах
|
## В планах
|
||||||
|
|
||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
- [ ] Добавить возможность управление с геймпада
|
- [X] Добавить возможность управление с геймпада
|
||||||
- [ ] Добавить возможность управление с тачскрина
|
- [ ] Добавить возможность управление с тачскрина
|
||||||
- [X] Добавить возможность управление с мыши и клавиатуры
|
- [X] Добавить возможность управление с мыши и клавиатуры
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
@ -16,6 +16,7 @@
|
|||||||
- [ ] Продумать систему вкладок вместо той что есть сейчас
|
- [ ] Продумать систему вкладок вместо той что есть сейчас
|
||||||
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
|
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
|
||||||
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
|
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
|
||||||
|
- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
- [X] Брать описание и названия игр с базы данных Steam
|
- [X] Брать описание и названия игр с базы данных Steam
|
||||||
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
|
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
|
||||||
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
|
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
|
||||||
|
@ -45,7 +45,7 @@ Requires: perl-Image-ExifTool
|
|||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}-git
|
%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
|
%prep
|
||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
|
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}
|
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_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
|
%changelog
|
||||||
|
@ -42,7 +42,7 @@ Requires: perl-Image-ExifTool
|
|||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}
|
%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
|
%prep
|
||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
|
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}
|
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_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
|
%changelog
|
||||||
|
@ -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>
|
@ -9,6 +9,7 @@ from PySide6.QtGui import QDesktopServices
|
|||||||
from portprotonqt.config_utils import parse_desktop_entry
|
from portprotonqt.config_utils import parse_desktop_entry
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||||
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
|
|
||||||
class ContextMenuManager:
|
class ContextMenuManager:
|
||||||
"""Manages context menu actions for game management in PortProtonQT."""
|
"""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):
|
def edit_game_shortcut(self, game_name, exec_line, cover_path):
|
||||||
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
|
"""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():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
|
@ -261,46 +261,45 @@ class GameCard(QFrame):
|
|||||||
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
|
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.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"))
|
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.steamLabel.setVisible(self.steam_visible)
|
||||||
self.egsLabel.setVisible(self.egs_visible)
|
self.egsLabel.setVisible(self.egs_visible)
|
||||||
self.portprotonLabel.setVisible(self.portproton_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
|
right_margin = 8
|
||||||
badge_spacing = 5
|
badge_spacing = 5
|
||||||
top_y = 10
|
top_y = 10
|
||||||
badge_y_positions = []
|
badge_y_positions = []
|
||||||
badge_width = int(self.coverLabel.width() * 2/3)
|
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)
|
|
||||||
|
|
||||||
self.anticheatLabel.raise_()
|
for is_visible, badge in badges:
|
||||||
self.protondbLabel.raise_()
|
if is_visible:
|
||||||
self.portprotonLabel.raise_()
|
badge_x = self.coverLabel.width() - badge_width - right_margin
|
||||||
self.egsLabel.raise_()
|
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||||
self.steamLabel.raise_()
|
badge.move(badge_x, badge_y)
|
||||||
|
badge_y_positions.append(badge_y + badge.height())
|
||||||
|
|
||||||
|
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
|
||||||
|
self.anticheatLabel.raise_()
|
||||||
|
self.protondbLabel.raise_()
|
||||||
|
self.portprotonLabel.raise_()
|
||||||
|
self.egsLabel.raise_()
|
||||||
|
self.steamLabel.raise_()
|
||||||
|
|
||||||
def _show_context_menu(self, pos):
|
def _show_context_menu(self, pos):
|
||||||
"""Delegate context menu display to ContextMenuManager."""
|
"""Delegate context menu display to ContextMenuManager."""
|
||||||
|
@ -3,7 +3,7 @@ import threading
|
|||||||
from typing import Protocol, cast
|
from typing import Protocol, cast
|
||||||
from evdev import InputDevice, ecodes, list_devices
|
from evdev import InputDevice, ecodes, list_devices
|
||||||
import pyudev
|
import pyudev
|
||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
|
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView
|
||||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
|
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
|
||||||
from PySide6.QtGui import QKeyEvent
|
from PySide6.QtGui import QKeyEvent
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@ -37,8 +37,8 @@ BUTTONS = {
|
|||||||
'confirm': {ecodes.BTN_A},
|
'confirm': {ecodes.BTN_A},
|
||||||
'back': {ecodes.BTN_B},
|
'back': {ecodes.BTN_B},
|
||||||
'add_game': {ecodes.BTN_Y},
|
'add_game': {ecodes.BTN_Y},
|
||||||
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
|
'prev_tab': {ecodes.BTN_TL},
|
||||||
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
|
'next_tab': {ecodes.BTN_TR},
|
||||||
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
||||||
'context_menu': {ecodes.BTN_START},
|
'context_menu': {ecodes.BTN_START},
|
||||||
'menu': {ecodes.BTN_SELECT},
|
'menu': {ecodes.BTN_SELECT},
|
||||||
@ -129,10 +129,60 @@ class InputManager(QObject):
|
|||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
|
# 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
|
# Закрытие AddGameDialog на кнопку B
|
||||||
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
|
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
|
||||||
active.reject() # Закрываем диалог
|
active.reject()
|
||||||
return
|
return
|
||||||
|
|
||||||
# FullscreenDialog
|
# FullscreenDialog
|
||||||
@ -149,7 +199,9 @@ class InputManager(QObject):
|
|||||||
if isinstance(focused, GameCard):
|
if isinstance(focused, GameCard):
|
||||||
if button_code in BUTTONS['context_menu']:
|
if button_code in BUTTONS['context_menu']:
|
||||||
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
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
|
return
|
||||||
|
|
||||||
# Game launch on detail page
|
# Game launch on detail page
|
||||||
@ -164,7 +216,9 @@ class InputManager(QObject):
|
|||||||
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
|
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
|
||||||
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
|
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
|
||||||
elif button_code in BUTTONS['add_game']:
|
elif button_code in BUTTONS['add_game']:
|
||||||
self._parent.openAddGameDialog()
|
# 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']:
|
elif button_code in BUTTONS['prev_tab']:
|
||||||
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
|
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
|
||||||
self._parent.switchTab(idx)
|
self._parent.switchTab(idx)
|
||||||
@ -176,7 +230,6 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
@Slot(int, int, float)
|
@Slot(int, int, float)
|
||||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||||
try:
|
try:
|
||||||
@ -188,6 +241,54 @@ class InputManager(QObject):
|
|||||||
if not app:
|
if not app:
|
||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
|
# Handle 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
|
# Fullscreen horizontal navigation
|
||||||
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
|
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
|
||||||
@ -280,7 +381,6 @@ class InputManager(QObject):
|
|||||||
next_card.setFocus()
|
next_card.setFocus()
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
||||||
if value > 0: # Down
|
if value > 0: # Down
|
||||||
next_row_idx = current_row_idx + 1
|
next_row_idx = current_row_idx + 1
|
||||||
@ -390,6 +490,23 @@ class InputManager(QObject):
|
|||||||
focused._show_context_menu(pos)
|
focused._show_context_menu(pos)
|
||||||
return True
|
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)
|
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
|
||||||
idx = self._parent.stackedWidget.currentIndex()
|
idx = self._parent.stackedWidget.currentIndex()
|
||||||
total = len(self._parent.tabButtons)
|
total = len(self._parent.tabButtons)
|
||||||
@ -520,6 +637,9 @@ class InputManager(QObject):
|
|||||||
if focusables:
|
if focusables:
|
||||||
focusables[0].setFocus()
|
focusables[0].setFocus()
|
||||||
return True
|
return True
|
||||||
|
elif focused:
|
||||||
|
focused.focusNextChild()
|
||||||
|
return True
|
||||||
# Navigate up through tab content
|
# Navigate up through tab content
|
||||||
if key == Qt.Key.Key_Up:
|
if key == Qt.Key.Key_Up:
|
||||||
if isinstance(focused, NavLabel):
|
if isinstance(focused, NavLabel):
|
||||||
@ -540,8 +660,10 @@ class InputManager(QObject):
|
|||||||
elif key == Qt.Key.Key_E:
|
elif key == Qt.Key.Key_E:
|
||||||
if isinstance(focused, QLineEdit):
|
if isinstance(focused, QLineEdit):
|
||||||
return False
|
return False
|
||||||
self._parent.openAddGameDialog()
|
# Only open AddGameDialog if in library tab (index 0)
|
||||||
return True
|
if self._parent.stackedWidget.currentIndex() == 0:
|
||||||
|
self._parent.openAddGameDialog()
|
||||||
|
return True
|
||||||
|
|
||||||
# Toggle fullscreen with F11
|
# Toggle fullscreen with F11
|
||||||
if key == Qt.Key.Key_F11:
|
if key == Qt.Key.Key_F11:
|
||||||
|
@ -539,6 +539,12 @@ class MainWindow(QMainWindow):
|
|||||||
def startSearchDebounce(self, text):
|
def startSearchDebounce(self, text):
|
||||||
self.searchDebounceTimer.start()
|
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):
|
def filterGamesDelayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""Filters games based on search text and updates the grid."""
|
||||||
text = self.searchEdit.text().strip().lower()
|
text = self.searchEdit.text().strip().lower()
|
||||||
@ -579,33 +585,16 @@ class MainWindow(QMainWindow):
|
|||||||
self.sizeSlider.setFixedWidth(150)
|
self.sizeSlider.setFixedWidth(150)
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||||
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
||||||
|
self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
|
||||||
sliderLayout.addWidget(self.sizeSlider)
|
sliderLayout.addWidget(self.sizeSlider)
|
||||||
layout.addLayout(sliderLayout)
|
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():
|
def calculate_card_width():
|
||||||
available_width = scrollArea.width() - 20
|
available_width = scrollArea.width() - 20
|
||||||
spacing = self.gamesListLayout._spacing
|
spacing = self.gamesListLayout._spacing
|
||||||
target_cards_per_row = 8
|
target_cards_per_row = 8
|
||||||
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
|
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
|
||||||
calculated_width = max(200, min(calculated_width, 250))
|
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)
|
QTimer.singleShot(0, calculate_card_width)
|
||||||
|
|
||||||
@ -621,7 +610,6 @@ class MainWindow(QMainWindow):
|
|||||||
self._last_width = self.width()
|
self._last_width = self.width()
|
||||||
if abs(self.width() - self._last_width) > 10:
|
if abs(self.width() - self._last_width) > 10:
|
||||||
self._last_width = self.width()
|
self._last_width = self.width()
|
||||||
self.sliderDebounceTimer.start()
|
|
||||||
|
|
||||||
def loadVisibleImages(self):
|
def loadVisibleImages(self):
|
||||||
visible_region = self.gamesListWidget.visibleRegion()
|
visible_region = self.gamesListWidget.visibleRegion()
|
||||||
@ -742,6 +730,7 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
dialog = AddGameDialog(self, self.theme)
|
dialog = AddGameDialog(self, self.theme)
|
||||||
|
dialog.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
|
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
|
||||||
|
|
||||||
# Предзаполняем путь к .exe при drag-and-drop
|
# Предзаполняем путь к .exe при drag-and-drop
|
||||||
|
Loading…
x
Reference in New Issue
Block a user