4 Commits

Author SHA1 Message Date
10d3fe8ab4 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:40:56 +05:00
a568ad9ef8 fix(add_game_dialog): prevent overwriting manually entered game name
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:09:58 +05:00
f074843fc8 fix: prevent udev monitor hang by using non-blocking poll with timeout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:53:47 +05:00
4ab078b93e fix: sync card_width between GameLibraryManager and MainWindow to prevent config overwrite
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:17:17 +05:00
5 changed files with 58 additions and 32 deletions

View File

@@ -7,12 +7,19 @@
### Added ### Added
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
### Changed ### Changed
- При завершении автоустановки приложение больше не перезапускается - При завершении автоустановки приложение больше не перезапускается
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
### Fixed ### Fixed
- Исправлено наложение карточек при смене фильтра игр - Исправлено наложение карточек при смене фильтра игр
- Исправлена невозможность запуска приложения без подключёного геймпада
- Исправлена невозможность установки компонентов Winetricks через геймпад
- Ресиверы и виртуальные устройства больше не считаются за геймпад
### Contributors ### Contributors

View File

@@ -1034,8 +1034,8 @@ class AddGameDialog(QDialog):
"""Обработчик выбора файла в FileExplorer""" """Обработчик выбора файла в FileExplorer"""
self.exeEdit.setText(file_path) self.exeEdit.setText(file_path)
self.last_exe_path = file_path # Update last selected exe path self.last_exe_path = file_path # Update last selected exe path
if not self.edit_mode: if not self.edit_mode and not self.nameEdit.text().strip():
# Автоматически заполняем имя игры, если не в режиме редактирования # Автоматически заполняем имя игры, если не в режиме редактирования или если оно не введено вручную
game_name = os.path.splitext(os.path.basename(file_path))[0] game_name = os.path.splitext(os.path.basename(file_path))[0]
self.nameEdit.setText(game_name) self.nameEdit.setText(game_name)

View File

@@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol):
# Required attributes # Required attributes
searchEdit: CustomLineEdit searchEdit: CustomLineEdit
_last_card_width: int _last_card_width: int
card_width: int
current_hovered_card: GameCard | None current_hovered_card: GameCard | None
current_focused_card: GameCard | None current_focused_card: GameCard | None
gamesListWidget: QWidget | None gamesListWidget: QWidget | None
@@ -128,6 +129,8 @@ class GameLibraryManager:
self.card_width = self.sizeSlider.value() self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px") self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width) save_card_size(self.card_width)
self.main_window.card_width = self.card_width
self.main_window._last_card_width = self.card_width
for card in self.game_card_cache.values(): for card in self.game_card_cache.values():
card.update_card_size(self.card_width) card.update_card_size(self.card_width)
self.update_game_grid() self.update_game_grid()

View File

@@ -1456,52 +1456,69 @@ class InputManager(QObject):
threading.Thread(target=self.run_udev_monitor, daemon=True).start() threading.Thread(target=self.run_udev_monitor, daemon=True).start()
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)") logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
def run_udev_monitor(self) -> None: def run_udev_monitor(self) -> None:
""" """
Неблокирующий опрос udev событий без MonitorObserver. Безопасный неблокирующий udev monitor для геймпадов.
Использует monitor.poll() с таймаутом для корректного завершения. Использует select.poll() вместо блокирующего monitor.poll().
""" """
try: try:
logger.info("Starting udev monitor...") logger.info("Starting udev monitor...")
monitor = Monitor.from_netlink(self.udev_context) monitor = Monitor.from_netlink(self.udev_context)
monitor.filter_by(subsystem='input') monitor.filter_by(subsystem='input')
monitor.start()
logger.info("Monitor started, draining initial events...")
# КРИТИЧНО: При старте udev отправляет события о ВСЕХ существующих устройствах try:
# Это может быть 10-50+ событий, которые блокируют инициализацию monitor.start()
# Решение: дренируем (игнорируем) все события за первые 500ms except Exception as e:
logger.error(f"Failed to start udev monitor: {e}")
return
import select
fd = monitor.fileno()
poller = select.poll()
poller.register(fd, select.POLLIN)
# Короткий дренаж событий при запуске (0.5 сек)
drain_start = time.time() drain_start = time.time()
drained_count = 0 drained_count = 0
while time.time() - drain_start < 0.5: while time.time() - drain_start < 0.5:
device = monitor.poll(timeout=0.1) events = poller.poll(100)
if device is not None: if not events:
continue
try:
_ = monitor.poll(timeout=0) # просто читаем, не обрабатываем
drained_count += 1 drained_count += 1
except Exception:
break
self.monitor_ready = True self.monitor_ready = True
logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...") logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...")
# Основной цикл опроса с таймаутом 1 секунда # Основной цикл
while self.running: while self.running:
# poll() возвращает None при таймауте - не блокирует навсегда events = poller.poll(1000) # 1 сек таймаут
device = monitor.poll(timeout=1.0) if not events:
continue # просто ждём, не блокируем
if device is not None: try:
action = device.action device = monitor.poll(timeout=0)
except Exception as e:
logger.debug(f"Monitor poll failed: {e}")
continue
# Фильтруем только джойстики на уровне callback if not device:
# Это предотвращает обработку мышей/клавиатур/и т.д. continue
if action and self._is_joystick_device(device):
logger.info(f"Joystick hotplug event: {action} for {device.sys_name}") action = device.action
self.handle_udev_event(action, device) if action and self._is_joystick_device(device):
logger.info(f"Joystick hotplug event: {action} for {device.sys_name}")
# отправляем сигнал в Qt-поток
self.handle_udev_event(action, device)
logger.info("udev monitor stopped gracefully") logger.info("udev monitor stopped gracefully")
except Exception as e: except Exception as e:
logger.error(f"Error in udev monitor: {e}", exc_info=True) logger.error(f"Error in udev monitor: {e}", exc_info=True)
def _is_joystick_device(self, device: Device) -> bool: def _is_joystick_device(self, device: Device) -> bool:
""" """
Быстрая проверка: является ли устройство джойстиком. Быстрая проверка: является ли устройство джойстиком.

View File

@@ -3056,7 +3056,13 @@ class MainWindow(QMainWindow):
"""Обработчик закрытия окна: проверяет настройку minimize_to_tray. """Обработчик закрытия окна: проверяет настройку minimize_to_tray.
Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем. Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем.
""" """
minimize_to_tray = read_minimize_to_tray() # Импорт read_minimize_to_tray из config_utils minimize_to_tray = read_minimize_to_tray()
save_card_size(self.card_width)
save_auto_card_size(self.auto_card_width)
# Сохраняем настройки окна
if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height())
if hasattr(self, 'is_exiting') and self.is_exiting or not minimize_to_tray: if hasattr(self, 'is_exiting') and self.is_exiting or not minimize_to_tray:
# Принудительное закрытие: завершаем процессы и приложение # Принудительное закрытие: завершаем процессы и приложение
for proc in self.game_processes: for proc in self.game_processes:
@@ -3097,13 +3103,6 @@ class MainWindow(QMainWindow):
self.wine_monitor_timer.deleteLater() self.wine_monitor_timer.deleteLater()
self.wine_monitor_timer = None self.wine_monitor_timer = None
# Сохраняем настройки окна
if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height())
save_card_size(self.card_width)
save_auto_card_size(self.auto_card_width)
event.accept() event.accept()
else: else:
# Сворачиваем в трей вместо закрытия # Сворачиваем в трей вместо закрытия