forked from Boria138/PortProtonQt
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			cde92885d4
			...
			7df6ad3b80
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7df6ad3b80 | |||
| 464ad0fe9c | 
| @@ -177,6 +177,26 @@ def save_card_size(card_width): | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def read_auto_card_size(): | ||||
|     """Reads the card size (width) for Auto Install from the [Cards] section. | ||||
|     Returns 250 if the parameter is not set. | ||||
|     """ | ||||
|     cp = read_config_safely(CONFIG_FILE) | ||||
|     if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"): | ||||
|         save_auto_card_size(250) | ||||
|         return 250 | ||||
|     return cp.getint("Cards", "auto_card_width", fallback=250) | ||||
|  | ||||
| def save_auto_card_size(card_width): | ||||
|     """Saves the card size (width) for Auto Install to the [Cards] section.""" | ||||
|     cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser() | ||||
|     if "Cards" not in cp: | ||||
|         cp["Cards"] = {} | ||||
|     cp["Cards"]["auto_card_width"] = str(card_width) | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
|  | ||||
| def read_sort_method(): | ||||
|     """Reads the sort method from the [Games] section. | ||||
|     Returns 'last_launch' if the parameter is not set. | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import os | ||||
| from typing import Protocol, cast | ||||
| from evdev import InputDevice, InputEvent, ecodes, list_devices, ff | ||||
| from enum import Enum | ||||
| from pyudev import Context, Monitor, MonitorObserver, Device, Devices | ||||
| from pyudev import Context, Monitor, Device, Devices | ||||
| from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem | ||||
| from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer | ||||
| from PySide6.QtGui import QKeyEvent, QMouseEvent | ||||
| @@ -76,6 +76,7 @@ class InputManager(QObject): | ||||
|     button_event = Signal(int, int)  # Signal for button events: (code, value) where value=1 (press), 0 (release) | ||||
|     dpad_moved = Signal(int, int, float)  # Signal for D-pad movements | ||||
|     toggle_fullscreen = Signal(bool)  # Signal for toggling fullscreen mode (True for fullscreen, False for normal) | ||||
|     gamepad_hotplug = Signal(str)  # 'add' or 'remove' | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
| @@ -1436,106 +1437,241 @@ class InputManager(QObject): | ||||
|         return super().eventFilter(obj, event) | ||||
|  | ||||
|     def init_gamepad(self) -> None: | ||||
|         self.monitor_observer = None | ||||
|         self.udev_context = Context()  # Создаём context один раз | ||||
|         self.Devices = Devices  # Сохраняем класс для использования в других методах | ||||
|         self.udev_context = Context() | ||||
|         self.Devices = Devices | ||||
|         self.monitor_ready = False | ||||
|  | ||||
|         # Подключаем сигнал hotplug к обработчику в главном потоке | ||||
|         self.gamepad_hotplug.connect(self._on_gamepad_hotplug) | ||||
|  | ||||
|         # Debounce timer для отложенной проверки геймпада (в главном потоке Qt) | ||||
|         self.gamepad_check_timer = QTimer() | ||||
|         self.gamepad_check_timer.setSingleShot(True) | ||||
|         self.gamepad_check_timer.timeout.connect(self.check_gamepad) | ||||
|  | ||||
|         # Первоначальная проверка | ||||
|         self.check_gamepad() | ||||
|  | ||||
|         # Запускаем udev monitor в отдельном потоке | ||||
|         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() с таймаутом для корректного завершения. | ||||
|         """ | ||||
|         try: | ||||
|             logger.info("Starting udev monitor...") | ||||
|             monitor = Monitor.from_netlink(self.udev_context) | ||||
|             monitor.filter_by(subsystem='input') | ||||
|             logger.info("Monitor created and filtered") | ||||
|             monitor.start() | ||||
|             logger.info("Monitor started, draining initial events...") | ||||
|  | ||||
|             observer = MonitorObserver(monitor, self.handle_udev_event) | ||||
|             self.monitor_observer = observer | ||||
|             logger.info("MonitorObserver created") | ||||
|             # КРИТИЧНО: При старте udev отправляет события о ВСЕХ существующих устройствах | ||||
|             # Это может быть 10-50+ событий, которые блокируют инициализацию | ||||
|             # Решение: дренируем (игнорируем) все события за первые 500ms | ||||
|             drain_start = time.time() | ||||
|             drained_count = 0 | ||||
|  | ||||
|             observer.start() | ||||
|             logger.info("MonitorObserver started") | ||||
|             while time.time() - drain_start < 0.5: | ||||
|                 device = monitor.poll(timeout=0.1) | ||||
|                 if device is not None: | ||||
|                     drained_count += 1 | ||||
|  | ||||
|             # Держим поток живым, пока не получим сигнал остановки | ||||
|             self.monitor_ready = True | ||||
|             logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...") | ||||
|  | ||||
|             # Основной цикл опроса с таймаутом 1 секунда | ||||
|             while self.running: | ||||
|                 time.sleep(1) | ||||
|                 # poll() возвращает None при таймауте - не блокирует навсегда | ||||
|                 device = monitor.poll(timeout=1.0) | ||||
|  | ||||
|             logger.info("MonitorObserver stopped gracefully") | ||||
|                 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}") | ||||
|                         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: | ||||
|         """ | ||||
|         Быстрая проверка: является ли устройство джойстиком. | ||||
|         Проверяет ID_INPUT_JOYSTICK из udev базы данных. | ||||
|         """ | ||||
|         try: | ||||
|             # Проверяем свойство ID_INPUT_JOYSTICK | ||||
|             if device.get('ID_INPUT_JOYSTICK') == '1': | ||||
|                 return True | ||||
|  | ||||
|             # Дополнительно: проверяем родительские устройства | ||||
|             # (некоторые контроллеры имеют свойство только у родителя) | ||||
|             parent = device.parent | ||||
|             if parent and parent.get('ID_INPUT_JOYSTICK') == '1': | ||||
|                 return True | ||||
|  | ||||
|             return False | ||||
|         except Exception as e: | ||||
|             logger.debug(f"Error checking joystick device: {e}") | ||||
|             return False | ||||
|  | ||||
|  | ||||
|     def handle_udev_event(self, action: str, device: Device) -> None: | ||||
|         """ | ||||
|         Обработчик udev событий для джойстиков. | ||||
|         Отправляет сигнал в главный поток Qt вместо прямого вызова QTimer. | ||||
|         """ | ||||
|         try: | ||||
|             if action == 'add': | ||||
|                 time.sleep(0.1) | ||||
|                 self.check_gamepad() | ||||
|                 # Отправляем сигнал в главный поток Qt | ||||
|                 # QTimer будет запущен там безопасно | ||||
|                 logger.debug("Emitting gamepad add signal") | ||||
|                 self.gamepad_hotplug.emit('add') | ||||
|  | ||||
|             elif action == 'remove' and self.gamepad: | ||||
|                 if not any(self.gamepad.path == path for path in list_devices()): | ||||
|                     logger.info("Gamepad disconnected") | ||||
|                     self.stop_rumble() | ||||
|                     self.gamepad = None | ||||
|                     if self.gamepad_thread: | ||||
|                         self.gamepad_thread.join() | ||||
|                     if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): | ||||
|                         self.toggle_fullscreen.emit(False) | ||||
|                 # Проверяем конкретно наш геймпад по пути устройства | ||||
|                 device_node = device.device_node  # например, /dev/input/event3 | ||||
|  | ||||
|                 if device_node and self.gamepad.path == device_node: | ||||
|                     logger.info(f"Connected gamepad disconnected: {device_node}") | ||||
|                     # Отправляем сигнал в главный поток | ||||
|                     self.gamepad_hotplug.emit('remove') | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error handling udev event: {e}", exc_info=True) | ||||
|  | ||||
|  | ||||
|     def _on_gamepad_hotplug(self, action: str) -> None: | ||||
|         """ | ||||
|         Обработчик сигнала hotplug, выполняется в главном потоке Qt. | ||||
|         Безопасно работает с QTimer. | ||||
|         """ | ||||
|         try: | ||||
|             if action == 'add': | ||||
|                 # Debounce: откладываем проверку на 200ms | ||||
|                 # Множественные события за короткое время объединяются в один вызов | ||||
|                 logger.debug("Scheduling gamepad check (debounced)") | ||||
|                 self.gamepad_check_timer.start(200) | ||||
|  | ||||
|             elif action == 'remove': | ||||
|                 # Немедленная обработка отключения | ||||
|                 self.stop_rumble() | ||||
|                 self.gamepad = None | ||||
|  | ||||
|                 if self.gamepad_thread: | ||||
|                     self.gamepad_thread.join(timeout=2.0) | ||||
|  | ||||
|                 if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): | ||||
|                     self.toggle_fullscreen.emit(False) | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error in hotplug handler: {e}", exc_info=True) | ||||
|  | ||||
|  | ||||
|     def check_gamepad(self) -> None: | ||||
|         """ | ||||
|         Проверка и подключение геймпада. | ||||
|         Вызывается из главного потока Qt через QTimer (debounced). | ||||
|         """ | ||||
|         try: | ||||
|             new_gamepad = self.find_gamepad() | ||||
|             if new_gamepad and new_gamepad != self.gamepad: | ||||
|                 logger.info(f"Gamepad connected: {new_gamepad.name}") | ||||
|  | ||||
|             # Проверяем, действительно ли это новый геймпад | ||||
|             if new_gamepad: | ||||
|                 if not self.gamepad or new_gamepad.path != self.gamepad.path: | ||||
|                     logger.info(f"Gamepad connected: {new_gamepad.name} at {new_gamepad.path}") | ||||
|                     self.stop_rumble() | ||||
|                     self.gamepad = new_gamepad | ||||
|  | ||||
|                     if self.gamepad_thread: | ||||
|                         self.gamepad_thread.join(timeout=2.0) | ||||
|  | ||||
|                     self.gamepad_thread = threading.Thread( | ||||
|                         target=self.monitor_gamepad, | ||||
|                         daemon=True | ||||
|                     ) | ||||
|                     self.gamepad_thread.start() | ||||
|  | ||||
|                     # Автоматический фуллскрин при подключении геймпада | ||||
|                     if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): | ||||
|                         self.toggle_fullscreen.emit(True) | ||||
|  | ||||
|             elif self.gamepad and not any(self.gamepad.path == path for path in list_devices()): | ||||
|                 # Геймпад был подключён, но теперь его нет в системе | ||||
|                 logger.info("Gamepad no longer detected") | ||||
|                 self.stop_rumble() | ||||
|                 self.gamepad = new_gamepad | ||||
|                 self.gamepad = None | ||||
|  | ||||
|                 if self.gamepad_thread: | ||||
|                     self.gamepad_thread.join() | ||||
|                 self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True) | ||||
|                 self.gamepad_thread.start() | ||||
|                 # Send signal for fullscreen mode only if: | ||||
|                 # 1. auto_fullscreen_gamepad is enabled | ||||
|                 # 2. fullscreen is not already enabled (to avoid conflict) | ||||
|                     self.gamepad_thread.join(timeout=2.0) | ||||
|  | ||||
|                 if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): | ||||
|                     self.toggle_fullscreen.emit(True) | ||||
|                     self.toggle_fullscreen.emit(False) | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error checking gamepad: {e}", exc_info=True) | ||||
|  | ||||
|  | ||||
|     def find_gamepad(self) -> InputDevice | None: | ||||
|         """ | ||||
|         Находит первый доступный геймпад. | ||||
|         Оптимизирован: предварительная фильтрация по capabilities перед udev-запросами. | ||||
|         """ | ||||
|         try: | ||||
|             devices = [InputDevice(path) for path in list_devices()] | ||||
|             logger.info(f"Checking {len(devices)} devices for gamepad...") | ||||
|  | ||||
|             if not devices: | ||||
|                 return None | ||||
|  | ||||
|             logger.debug(f"Checking {len(devices)} devices for gamepad...") | ||||
|  | ||||
|             for device in devices: | ||||
|                 logger.debug(f"Checking device: {device.name} at {device.path}") | ||||
|  | ||||
|                 # Skip ASRock LED controller (vendor ID: 26ce, product ID: 01a2) | ||||
|                 # Skip ASRock LED controller (известная проблема) | ||||
|                 if device.info.vendor == 0x26ce and device.info.product == 0x01a2: | ||||
|                     logger.debug(f"Skipping ASRock LED controller: {device.name}") | ||||
|                     continue | ||||
|  | ||||
|                 # Получаем udev-устройство для проверки ID_INPUT_JOYSTICK | ||||
|                 try: | ||||
|                     udev_device = self.Devices.from_device_file(self.udev_context, device.path) | ||||
|                     is_joystick = udev_device.get('ID_INPUT_JOYSTICK') | ||||
|                 # Предварительная фильтрация: проверяем capabilities | ||||
|                 # Джойстик должен иметь хотя бы оси (ABS) или кнопки (KEY) | ||||
|                 # Это избегает udev-запросов для явно не-джойстиков | ||||
|                 caps = device.capabilities(verbose=False) | ||||
|                 has_abs_axes = ecodes.EV_ABS in caps | ||||
|                 has_buttons = ecodes.EV_KEY in caps | ||||
|  | ||||
|                     logger.debug(f"Device {device.name}: ID_INPUT_JOYSTICK = {is_joystick}") | ||||
|                 if not (has_abs_axes or has_buttons): | ||||
|                     continue | ||||
|  | ||||
|                 # Только для потенциальных джойстиков делаем udev-запрос | ||||
|                 try: | ||||
|                     udev_device = self.Devices.from_device_file( | ||||
|                         self.udev_context, | ||||
|                         device.path | ||||
|                     ) | ||||
|                     is_joystick = udev_device.get('ID_INPUT_JOYSTICK') | ||||
|  | ||||
|                     if is_joystick == '1': | ||||
|                         logger.info(f"Found gamepad: {device.name}") | ||||
|                         return device | ||||
|                     else: | ||||
|                         logger.debug(f"Skipping non-joystick device: {device.name}") | ||||
|  | ||||
|                 except Exception as e: | ||||
|                     logger.warning(f"Could not check udev properties for {device.path}: {e}") | ||||
|                     logger.debug(f"Could not check udev properties for {device.path}: {e}") | ||||
|                     continue | ||||
|  | ||||
|             logger.warning("No gamepad found") | ||||
|             logger.debug("No gamepad found") | ||||
|             return None | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error finding gamepad: {e}", exc_info=True) | ||||
|             return None | ||||
|  | ||||
|  | ||||
|     def monitor_gamepad(self) -> None: | ||||
|         try: | ||||
|             if not self.gamepad: | ||||
| @@ -1599,26 +1735,32 @@ class InputManager(QObject): | ||||
|             self.gamepad = None | ||||
|  | ||||
|     def cleanup(self) -> None: | ||||
|         """ | ||||
|         Корректное завершение работы с геймпадом и udev монитором. | ||||
|         """ | ||||
|         try: | ||||
|             # Флаг для остановки udev monitor loop | ||||
|             self.running = False | ||||
|  | ||||
|             # Останавливаем udev monitor | ||||
|             if self.monitor_observer: | ||||
|                 try: | ||||
|                     logger.info("Stopping udev monitor...") | ||||
|                     self.monitor_observer.send_stop() | ||||
|                 except Exception as e: | ||||
|                     logger.warning(f"Error stopping monitor observer: {e}") | ||||
|                 self.monitor_observer = None | ||||
|  | ||||
|             # Останавливаем все таймеры | ||||
|             if hasattr(self, 'gamepad_check_timer'): | ||||
|                 self.gamepad_check_timer.stop() | ||||
|             self.dpad_timer.stop() | ||||
|             self.nav_timer.stop() | ||||
|  | ||||
|             # Очистка геймпада | ||||
|             self.stop_rumble() | ||||
|  | ||||
|             if self.gamepad_thread: | ||||
|                 self.gamepad_thread.join(timeout=2.0)  # Добавлен таймаут | ||||
|                 self.gamepad_thread.join(timeout=2.0) | ||||
|  | ||||
|             if self.gamepad: | ||||
|                 self.gamepad.close() | ||||
|  | ||||
|             self.gamepad = None | ||||
|             self.gamepad_type = GamepadType.UNKNOWN | ||||
|  | ||||
|             logger.info("Gamepad cleanup completed") | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error during cleanup: {e}", exc_info=True) | ||||
|   | ||||
| @@ -29,7 +29,8 @@ from portprotonqt.config_utils import ( | ||||
|     read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method, | ||||
|     save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config, | ||||
|     save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config, | ||||
|     clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray | ||||
|     clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray, | ||||
|     read_auto_card_size, save_auto_card_size | ||||
| ) | ||||
| from portprotonqt.localization import _, get_egs_language, read_metadata_translations | ||||
| from portprotonqt.howlongtobeat_api import HowLongToBeat | ||||
| @@ -39,7 +40,7 @@ from portprotonqt.game_library_manager import GameLibraryManager | ||||
| from portprotonqt.virtual_keyboard import VirtualKeyboard | ||||
|  | ||||
| from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, | ||||
|                                QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller) | ||||
|                                QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider) | ||||
| from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess | ||||
| from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices | ||||
| from typing import cast | ||||
| @@ -63,6 +64,7 @@ class MainWindow(QMainWindow): | ||||
|         self.theme = self.theme_manager.apply_theme(selected_theme) | ||||
|         self.tray_manager = TrayManager(self, app_name, self.current_theme_name) | ||||
|         self.card_width = read_card_size() | ||||
|         self.auto_card_width = read_auto_card_size() | ||||
|         self._last_card_width = self.card_width | ||||
|         self.setWindowTitle(f"{app_name} {version}") | ||||
|         self.setMinimumSize(800, 600) | ||||
| @@ -1100,8 +1102,7 @@ class MainWindow(QMainWindow): | ||||
|         autoInstallPage = QWidget() | ||||
|         autoInstallPage.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE) | ||||
|         autoInstallLayout = QVBoxLayout(autoInstallPage) | ||||
|         autoInstallLayout.setContentsMargins(20, 0, 20, 0) | ||||
|         autoInstallLayout.setSpacing(0) | ||||
|         autoInstallLayout.setSpacing(15) | ||||
|  | ||||
|         # Верхняя панель с заголовком и поиском | ||||
|         headerWidget = QWidget() | ||||
| @@ -1150,6 +1151,25 @@ class MainWindow(QMainWindow): | ||||
|  | ||||
|         autoInstallLayout.addWidget(self.autoInstallScrollArea) | ||||
|  | ||||
|         # Slider for card size | ||||
|         sliderLayout = QHBoxLayout() | ||||
|         sliderLayout.setSpacing(0) | ||||
|         sliderLayout.setContentsMargins(0, 0, 0, 0) | ||||
|         sliderLayout.addStretch() | ||||
|  | ||||
|         self.auto_size_slider = QSlider(Qt.Orientation.Horizontal) | ||||
|         self.auto_size_slider.setMinimum(200) | ||||
|         self.auto_size_slider.setMaximum(250) | ||||
|         self.auto_size_slider.setValue(self.auto_card_width) | ||||
|         self.auto_size_slider.setTickInterval(10) | ||||
|         self.auto_size_slider.setFixedWidth(150) | ||||
|         self.auto_size_slider.setToolTip(f"{self.auto_card_width} px") | ||||
|         self.auto_size_slider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE) | ||||
|         self.auto_size_slider.sliderReleased.connect(self.on_auto_slider_released) | ||||
|         sliderLayout.addWidget(self.auto_size_slider) | ||||
|  | ||||
|         autoInstallLayout.addLayout(sliderLayout) | ||||
|  | ||||
|         # Хранение карточек | ||||
|         self.autoInstallGameCards = {} | ||||
|         self.allAutoInstallCards = [] | ||||
| @@ -1159,7 +1179,7 @@ class MainWindow(QMainWindow): | ||||
|             if exe_name in self.autoInstallGameCards and local_path: | ||||
|                 card = self.autoInstallGameCards[exe_name] | ||||
|                 card.cover_path = local_path | ||||
|                 load_pixmap_async(local_path, self.card_width, int(self.card_width * 1.5), card.on_cover_loaded) | ||||
|                 load_pixmap_async(local_path, self.auto_card_width, int(self.auto_card_width * 1.5), card.on_cover_loaded) | ||||
|  | ||||
|         # Загрузка игр | ||||
|         def on_autoinstall_games_loaded(games: list[tuple]): | ||||
| @@ -1195,7 +1215,7 @@ class MainWindow(QMainWindow): | ||||
|                     None, None, None, game_source, | ||||
|                     select_callback=select_callback, | ||||
|                     theme=self.theme, | ||||
|                     card_width=self.card_width, | ||||
|                     card_width=self.auto_card_width, | ||||
|                     parent=self.autoInstallContainer, | ||||
|                 ) | ||||
|  | ||||
| @@ -1237,6 +1257,18 @@ class MainWindow(QMainWindow): | ||||
|  | ||||
|         self.stackedWidget.addWidget(autoInstallPage) | ||||
|  | ||||
|     def on_auto_slider_released(self): | ||||
|         """Handles auto-install slider release to update card size.""" | ||||
|         if hasattr(self, 'auto_size_slider') and self.auto_size_slider: | ||||
|             self.auto_card_width = self.auto_size_slider.value() | ||||
|             self.auto_size_slider.setToolTip(f"{self.auto_card_width} px") | ||||
|             save_auto_card_size(self.auto_card_width) | ||||
|             for card in self.allAutoInstallCards: | ||||
|                 card.update_card_size(self.auto_card_width) | ||||
|             self.autoInstallContainerLayout.invalidate() | ||||
|             self.autoInstallContainer.updateGeometry() | ||||
|             self.autoInstallScrollArea.updateGeometry() | ||||
|  | ||||
|     def filterAutoInstallGames(self): | ||||
|         """Filter auto install game cards based on search text.""" | ||||
|         search_text = self.autoInstallSearchLineEdit.text().lower().strip() | ||||
| @@ -3070,6 +3102,7 @@ class MainWindow(QMainWindow): | ||||
|                 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: | ||||
|   | ||||
| @@ -54,8 +54,8 @@ class VirtualKeyboard(QFrame): | ||||
|                 self.main_window = cast(Any, parent_widget) | ||||
|             parent_widget = cast(QWidget | None, parent_widget.parent()) | ||||
|  | ||||
|         self.current_theme_name = read_theme_from_config() | ||||
|  | ||||
|         self.current_theme_name = read_theme_from_config() | ||||
|         self.initUI() | ||||
|         self.hide() | ||||
|  | ||||
| @@ -132,27 +132,33 @@ class VirtualKeyboard(QFrame): | ||||
|         self.buttons: dict[str, QPushButton] = {} | ||||
|         self.update_keyboard() | ||||
|  | ||||
|     def set_gamepad_icon(self, button, icon_type, gtype='default'): | ||||
|         """Set gamepad icon on button based on type. Now works even without gamepad by using 'default' gtype.""" | ||||
|     def set_gamepad_icon(self, button, icon_type, gtype=''): | ||||
|         """Set gamepad icon on button based on type""" | ||||
|         if icon_type in ['back', 'add_game']: | ||||
|             icon_name = self.main_window.get_button_icon(icon_type, gtype) if self.main_window else f"{icon_type}_default.png" | ||||
|             icon_name = self.main_window.get_button_icon(icon_type, gtype) | ||||
|         else:  # nav left/right | ||||
|             if icon_type in ['left', 'right']: | ||||
|                 direction = icon_type | ||||
|                 icon_name = self.main_window.get_nav_icon(direction, gtype) if self.main_window else f"{direction}_default.png" | ||||
|                 icon_name = self.main_window.get_nav_icon(direction, gtype) | ||||
|             else: | ||||
|                 direction = 'left' if icon_type == 'left' else 'right' | ||||
|                 icon_name = self.main_window.get_nav_icon(direction, gtype) if self.main_window else f"{direction}_default.png" | ||||
|                 icon_name = self.main_window.get_nav_icon(direction, gtype) | ||||
|  | ||||
|         icon_path = theme_manager.get_theme_image(icon_name, self.current_theme_name) | ||||
|         pixmap = QPixmap() | ||||
|         if icon_path: | ||||
|             pixmap = QPixmap(str(icon_path)) | ||||
|             if not pixmap.isNull(): | ||||
|                 button.setIcon(QIcon(pixmap)) | ||||
|             pixmap.load(str(icon_path)) | ||||
|         if not pixmap.isNull(): | ||||
|             button.setIcon(QIcon(pixmap)) | ||||
|             button.setIconSize(QSize(20, 20)) | ||||
|             return | ||||
|         else: | ||||
|             # Fallback to placeholder | ||||
|             placeholder = theme_manager.get_theme_image("placeholder", self.current_theme_name) | ||||
|             if placeholder: | ||||
|                 button.setIcon(QIcon(placeholder)) | ||||
|                 button.setIconSize(QSize(20, 20)) | ||||
|                 return | ||||
|         # Fallback: if no icon found, try standard Qt icon or leave empty | ||||
|         print(f"Warning: Icon {icon_name} not found for button {icon_type}") | ||||
|  | ||||
|     def update_keyboard(self): | ||||
|         coords = self._save_focused_coords() | ||||
| @@ -187,7 +193,7 @@ class VirtualKeyboard(QFrame): | ||||
|                     button.setChecked(self.shift_pressed) | ||||
|                     button.clicked.connect(lambda checked: self.on_shift_click(checked)) | ||||
|                     # Add gamepad icon for Shift (RB/R) | ||||
|                     gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default' | ||||
|                     gtype = self.input_manager.gamepad_type | ||||
|                     self.set_gamepad_icon(button, 'right', gtype) | ||||
|                 else: | ||||
|                     button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k)) | ||||
| @@ -202,7 +208,7 @@ class VirtualKeyboard(QFrame): | ||||
|         shift.setChecked(self.shift_pressed) | ||||
|         shift.clicked.connect(lambda checked: self.on_shift_click(checked)) | ||||
|         # Add gamepad icon for Shift (RB/R) | ||||
|         gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default' | ||||
|         gtype = self.input_manager.gamepad_type | ||||
|         self.set_gamepad_icon(shift, 'right', gtype) | ||||
|         self.keyboard_layout.addWidget(shift, 3, 11, 1, 3) | ||||
|  | ||||
| @@ -221,7 +227,7 @@ class VirtualKeyboard(QFrame): | ||||
|         backspace.pressed.connect(self.on_backspace_pressed) | ||||
|         backspace.released.connect(self.stop_backspace_repeat) | ||||
|         # Add gamepad icon for Backspace (X/Triangle) | ||||
|         gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default' | ||||
|         gtype = self.input_manager.gamepad_type | ||||
|         self.set_gamepad_icon(backspace, 'add_game', gtype) | ||||
|         self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1) | ||||
|  | ||||
| @@ -234,7 +240,7 @@ class VirtualKeyboard(QFrame): | ||||
|         lang.setFixedSize(fixed_w, fixed_h) | ||||
|         lang.clicked.connect(self.on_lang_click) | ||||
|         # Add gamepad icon for Lang (LB/L) | ||||
|         gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default' | ||||
|         gtype = self.input_manager.gamepad_type | ||||
|         self.set_gamepad_icon(lang, 'left', gtype) | ||||
|         self.keyboard_layout.addWidget(lang, 4, 0, 1, 1) | ||||
|  | ||||
| @@ -267,7 +273,7 @@ class VirtualKeyboard(QFrame): | ||||
|         hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h) | ||||
|         hide_button.clicked.connect(self.hide) | ||||
|         # Add gamepad icon for Hide (B/Circle) | ||||
|         gtype = self.input_manager.gamepad_type if self.input_manager and self.input_manager.gamepad else 'default' | ||||
|         gtype = self.input_manager.gamepad_type | ||||
|         self.set_gamepad_icon(hide_button, 'back', gtype) | ||||
|         self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user