forked from Boria138/PortProtonQt
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			7df6ad3b80
			...
			10d3fe8ab4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 10d3fe8ab4 | |||
| a568ad9ef8 | |||
| f074843fc8 | |||
| 4ab078b93e | 
| @@ -7,12 +7,19 @@ | |||||||
|  |  | ||||||
| ### Added | ### Added | ||||||
| - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению | - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению | ||||||
|  | - В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет | ||||||
|  | - К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада | ||||||
|  | - Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы) | ||||||
|  |  | ||||||
| ### Changed | ### Changed | ||||||
| - При завершении автоустановки приложение больше не перезапускается | - При завершении автоустановки приложение больше не перезапускается | ||||||
|  | - Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название | ||||||
|  |  | ||||||
| ### Fixed | ### Fixed | ||||||
| - Исправлено наложение карточек при смене фильтра игр | - Исправлено наложение карточек при смене фильтра игр | ||||||
|  | - Исправлена невозможность запуска приложения без подключёного геймпада | ||||||
|  | - Исправлена невозможность установки компонентов Winetricks через геймпад | ||||||
|  | - Ресиверы и виртуальные устройства больше не считаются за геймпад | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Contributors | ### Contributors | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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() | ||||||
|   | |||||||
| @@ -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: | ||||||
|         """ |         """ | ||||||
|         Быстрая проверка: является ли устройство джойстиком. |         Быстрая проверка: является ли устройство джойстиком. | ||||||
|   | |||||||
| @@ -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: | ||||||
|             # Сворачиваем в трей вместо закрытия |             # Сворачиваем в трей вместо закрытия | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user