forked from Boria138/PortProtonQt
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			7df6ad3b80
			...
			10d3fe8ab4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 10d3fe8ab4 | |||
| a568ad9ef8 | |||
| f074843fc8 | |||
| 4ab078b93e | 
| @@ -7,12 +7,19 @@ | ||||
|  | ||||
| ### Added | ||||
| - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению | ||||
| - В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет | ||||
| - К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада | ||||
| - Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы) | ||||
|  | ||||
| ### Changed | ||||
| - При завершении автоустановки приложение больше не перезапускается | ||||
| - Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название | ||||
|  | ||||
| ### Fixed | ||||
| - Исправлено наложение карточек при смене фильтра игр | ||||
| - Исправлена невозможность запуска приложения без подключёного геймпада | ||||
| - Исправлена невозможность установки компонентов Winetricks через геймпад | ||||
| - Ресиверы и виртуальные устройства больше не считаются за геймпад | ||||
|  | ||||
|  | ||||
| ### Contributors | ||||
|   | ||||
| @@ -1034,8 +1034,8 @@ class AddGameDialog(QDialog): | ||||
|         """Обработчик выбора файла в FileExplorer""" | ||||
|         self.exeEdit.setText(file_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] | ||||
|             self.nameEdit.setText(game_name) | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol): | ||||
|     # Required attributes | ||||
|     searchEdit: CustomLineEdit | ||||
|     _last_card_width: int | ||||
|     card_width: int | ||||
|     current_hovered_card: GameCard | None | ||||
|     current_focused_card: GameCard | None | ||||
|     gamesListWidget: QWidget | None | ||||
| @@ -128,6 +129,8 @@ class GameLibraryManager: | ||||
|         self.card_width = self.sizeSlider.value() | ||||
|         self.sizeSlider.setToolTip(f"{self.card_width} px") | ||||
|         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(): | ||||
|             card.update_card_size(self.card_width) | ||||
|         self.update_game_grid() | ||||
|   | ||||
| @@ -1456,52 +1456,69 @@ class InputManager(QObject): | ||||
|         threading.Thread(target=self.run_udev_monitor, daemon=True).start() | ||||
|         logger.info("Gamepad support initialized with hotplug (evdev + pyudev)") | ||||
|  | ||||
|  | ||||
|     def run_udev_monitor(self) -> None: | ||||
|         """ | ||||
|         Неблокирующий опрос udev событий без MonitorObserver. | ||||
|         Использует monitor.poll() с таймаутом для корректного завершения. | ||||
|         Безопасный неблокирующий udev monitor для геймпадов. | ||||
|         Использует select.poll() вместо блокирующего monitor.poll(). | ||||
|         """ | ||||
|         try: | ||||
|             logger.info("Starting udev monitor...") | ||||
|             monitor = Monitor.from_netlink(self.udev_context) | ||||
|             monitor.filter_by(subsystem='input') | ||||
|             monitor.start() | ||||
|             logger.info("Monitor started, draining initial events...") | ||||
|  | ||||
|             # КРИТИЧНО: При старте udev отправляет события о ВСЕХ существующих устройствах | ||||
|             # Это может быть 10-50+ событий, которые блокируют инициализацию | ||||
|             # Решение: дренируем (игнорируем) все события за первые 500ms | ||||
|             try: | ||||
|                 monitor.start() | ||||
|             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() | ||||
|             drained_count = 0 | ||||
|  | ||||
|             while time.time() - drain_start < 0.5: | ||||
|                 device = monitor.poll(timeout=0.1) | ||||
|                 if device is not None: | ||||
|                 events = poller.poll(100) | ||||
|                 if not events: | ||||
|                     continue | ||||
|                 try: | ||||
|                     _ = monitor.poll(timeout=0)  # просто читаем, не обрабатываем | ||||
|                     drained_count += 1 | ||||
|                 except Exception: | ||||
|                     break | ||||
|  | ||||
|             self.monitor_ready = True | ||||
|             logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...") | ||||
|  | ||||
|             # Основной цикл опроса с таймаутом 1 секунда | ||||
|             # Основной цикл | ||||
|             while self.running: | ||||
|                 # poll() возвращает None при таймауте - не блокирует навсегда | ||||
|                 device = monitor.poll(timeout=1.0) | ||||
|                 events = poller.poll(1000)  # 1 сек таймаут | ||||
|                 if not events: | ||||
|                     continue  # просто ждём, не блокируем | ||||
|  | ||||
|                 try: | ||||
|                     device = monitor.poll(timeout=0) | ||||
|                 except Exception as e: | ||||
|                     logger.debug(f"Monitor poll failed: {e}") | ||||
|                     continue | ||||
|  | ||||
|                 if not device: | ||||
|                     continue | ||||
|  | ||||
|                 if device is not None: | ||||
|                 action = device.action | ||||
|  | ||||
|                     # Фильтруем только джойстики на уровне callback | ||||
|                     # Это предотвращает обработку мышей/клавиатур/и т.д. | ||||
|                 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") | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error in udev monitor: {e}", exc_info=True) | ||||
|  | ||||
|  | ||||
|     def _is_joystick_device(self, device: Device) -> bool: | ||||
|         """ | ||||
|         Быстрая проверка: является ли устройство джойстиком. | ||||
|   | ||||
| @@ -3056,7 +3056,13 @@ class MainWindow(QMainWindow): | ||||
|         """Обработчик закрытия окна: проверяет настройку minimize_to_tray. | ||||
|         Если 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: | ||||
|             # Принудительное закрытие: завершаем процессы и приложение | ||||
|             for proc in self.game_processes: | ||||
| @@ -3097,13 +3103,6 @@ class MainWindow(QMainWindow): | ||||
|                 self.wine_monitor_timer.deleteLater() | ||||
|                 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() | ||||
|         else: | ||||
|             # Сворачиваем в трей вместо закрытия | ||||
|   | ||||
		Reference in New Issue
	
	Block a user