chore(input_manager): clean code

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2026-01-08 17:24:22 +05:00
parent bbfc51f908
commit 59aecbc6e8
5 changed files with 565 additions and 167 deletions

3
.gitignore vendored
View File

@@ -33,3 +33,6 @@ Thumbs.db
.vscode .vscode
.ropeproject .ropeproject
.zed .zed
# get_wine debug folder
proton_downloads

View File

@@ -135,6 +135,15 @@ def create_dialog_hints_widget(theme, main_window, input_manager, context='defau
("prev_tab", _("Prev Tab")), # LB / L1 ("prev_tab", _("Prev Tab")), # LB / L1
("next_tab", _("Next Tab")), # RB / R1 ("next_tab", _("Next Tab")), # RB / R1
] ]
elif context == 'proton_manager':
dialog_actions = [
("confirm", _("Toggle")), # A / Cross
("add_game", _("Download")), # X / Triangle
("prev_dir", _("Clear All")), # Y / Square
("back", _("Cancel")), # B / Circle
("prev_tab", _("Prev Tab")), # LB / L1
("next_tab", _("Next Tab")), # RB / R1
]
hints_labels = [] # Store for updates (returned for class storage) hints_labels = [] # Store for updates (returned for class storage)

View File

@@ -17,6 +17,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.theme_manager import ThemeManager from portprotonqt.theme_manager import ThemeManager
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.version_utils import version_sort_key from portprotonqt.version_utils import version_sort_key
from portprotonqt.dialogs import create_dialog_hints_widget, update_dialog_hints
logger = get_logger(__name__) logger = get_logger(__name__)
theme_manager = ThemeManager() theme_manager = ThemeManager()
@@ -330,7 +331,7 @@ class ExtractionThread(QThread):
class ProtonManager(QDialog): class ProtonManager(QDialog):
def __init__(self, parent=None, portproton_location=None, theme=None): def __init__(self, parent=None, portproton_location=None, theme=None, input_manager=None):
super().__init__(parent) super().__init__(parent)
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config()) self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.selected_assets = {} # {unique_id: asset_data} self.selected_assets = {} # {unique_id: asset_data}
@@ -340,9 +341,25 @@ class ProtonManager(QDialog):
self.assets_to_download = [] self.assets_to_download = []
self.current_download_index = 0 self.current_download_index = 0
self.portproton_location = portproton_location self.portproton_location = portproton_location
self.input_manager = input_manager # Input manager for gamepad support
self.initial_command_executed = False # Track if --initial command has been executed
# Find main window
self.main_window = None
parent_widget = self.parent()
while parent_widget:
if hasattr(parent_widget, 'input_manager'):
self.main_window = parent_widget
break
parent_widget = parent_widget.parent()
self.initUI() self.initUI()
self.load_proton_data_from_json() self.load_proton_data_from_json()
# Enable gamepad support if input manager is provided
if self.input_manager:
self.enable_proton_manager_mode()
def initUI(self): def initUI(self):
self.setWindowTitle(_('Get other Wine')) self.setWindowTitle(_('Get other Wine'))
self.resize(1100, 720) self.resize(1100, 720)
@@ -403,6 +420,14 @@ class ProtonManager(QDialog):
layout.addWidget(self.download_frame) layout.addWidget(self.download_frame)
# Create hints widget using common function
if self.input_manager and self.main_window:
self.current_theme_name = read_theme_from_config()
self.hints_widget, self.hints_labels = create_dialog_hints_widget(
self.theme, self.main_window, self.input_manager, context='proton_manager'
)
layout.addWidget(self.hints_widget)
# Кнопки управления # Кнопки управления
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.download_btn = QPushButton(_('Download Selected')) self.download_btn = QPushButton(_('Download Selected'))
@@ -416,6 +441,26 @@ class ProtonManager(QDialog):
button_layout.addWidget(self.clear_btn) button_layout.addWidget(self.clear_btn)
layout.addLayout(button_layout) layout.addLayout(button_layout)
# Connect signals for hints updates
if self.input_manager and self.main_window:
self.input_manager.button_event.connect(
lambda *args: update_dialog_hints(
self.hints_labels, self.main_window, self.input_manager,
theme_manager, self.current_theme_name
)
)
self.input_manager.dpad_moved.connect(
lambda *args: update_dialog_hints(
self.hints_labels, self.main_window, self.input_manager,
theme_manager, self.current_theme_name
)
)
# Initial update
update_dialog_hints(
self.hints_labels, self.main_window, self.input_manager,
theme_manager, self.current_theme_name
)
def load_proton_data_from_json(self): def load_proton_data_from_json(self):
"""Загружаем данные по Протонам из файла JSON""" """Загружаем данные по Протонам из файла JSON"""
json_url = "https://git.linux-gaming.ru/Boria138/PortProton-Wine-Metadata/raw/branch/main/wine_metadata.json" json_url = "https://git.linux-gaming.ru/Boria138/PortProton-Wine-Metadata/raw/branch/main/wine_metadata.json"
@@ -821,6 +866,26 @@ class ProtonManager(QDialog):
self.download_btn.setEnabled(True) self.download_btn.setEnabled(True)
self.clear_btn.setEnabled(True) self.clear_btn.setEnabled(True)
self.is_downloading = False self.is_downloading = False
# Run the initial command after all assets have been processed
import subprocess
try:
# Get the proper PortProton start command
start_cmd = get_portproton_start_command()
if start_cmd and not self.initial_command_executed:
result = subprocess.run(start_cmd + ["cli", "--initial"], timeout=10)
if result.returncode != 0:
logger.warning(f"Initial PortProton command returned non-zero exit code: {result.returncode}")
else:
logger.info("Initial PortProton command executed successfully after all assets processed")
self.initial_command_executed = True # Mark that command has been executed
elif self.initial_command_executed:
logger.debug("Initial PortProton command already executed, skipping")
except subprocess.TimeoutExpired:
logger.warning("Initial PortProton command timed out")
except Exception as e:
logger.error(f"Error running initial PortProton command: {e}")
QMessageBox.information(self, _("Downloading Complete"), _("All selected archives have been downloaded!")) QMessageBox.information(self, _("Downloading Complete"), _("All selected archives have been downloaded!"))
return return
@@ -954,22 +1019,6 @@ class ProtonManager(QDialog):
def extraction_finished(archive_path, success): def extraction_finished(archive_path, success):
if success: if success:
logger.info(f"Successfully extracted: {archive_path}") logger.info(f"Successfully extracted: {archive_path}")
# Run the initial command after successful extraction
import subprocess
try:
# Get the proper PortProton start command
start_cmd = get_portproton_start_command()
if start_cmd:
result = subprocess.run(start_cmd + ["cli", "--initial"], timeout=10)
if result.returncode != 0:
logger.warning(f"Initial PortProton command returned non-zero exit code: {result.returncode}")
else:
logger.warning("Could not determine PortProton start command, skipping initial command")
except subprocess.TimeoutExpired:
logger.warning("Initial PortProton command timed out")
except Exception as e:
logger.error(f"Error running initial PortProton command: {e}")
else: else:
logger.error(f"Failed to extract: {archive_path}") logger.error(f"Failed to extract: {archive_path}")
QMessageBox.critical(self, _("Extraction Error"), _("Failed to extract archive: {0}").format(archive_path)) QMessageBox.critical(self, _("Extraction Error"), _("Failed to extract archive: {0}").format(archive_path))
@@ -1043,6 +1092,15 @@ class ProtonManager(QDialog):
self.current_download_index += 1 self.current_download_index += 1
QTimer.singleShot(100, self.start_next_download) QTimer.singleShot(100, self.start_next_download)
def has_active_processes(self):
"""Check if there are active download or extraction processes"""
extraction_active = (self.current_extraction_thread and
self.current_extraction_thread.isRunning())
download_active = (self.current_download_thread and
hasattr(self.current_download_thread, 'isRunning') and
self.current_download_thread.isRunning())
return extraction_active or download_active
def cancel_current_download(self): def cancel_current_download(self):
"""Cancel current download or extraction""" """Cancel current download or extraction"""
# Stop extraction thread if running # Stop extraction thread if running
@@ -1068,6 +1126,8 @@ class ProtonManager(QDialog):
self.assets_to_download = [] self.assets_to_download = []
self.current_download_index = 0 self.current_download_index = 0
self.is_downloading = False self.is_downloading = False
# Сбрасываем флаг выполнения команды --initial, так как процесс отменен
self.initial_command_executed = False
# Сброс/перезапуск UI # Сброс/перезапуск UI
self.download_frame.setVisible(False) self.download_frame.setVisible(False)
@@ -1076,46 +1136,87 @@ class ProtonManager(QDialog):
QMessageBox.information(self, _("Operation Cancelled"), _("Download or extraction has been cancelled.")) QMessageBox.information(self, _("Operation Cancelled"), _("Download or extraction has been cancelled."))
def enable_proton_manager_mode(self):
"""Enable gamepad mode for ProtonManager"""
if self.input_manager:
self.input_manager.enable_proton_manager_mode(self)
def disable_proton_manager_mode(self):
"""Disable gamepad mode for ProtonManager"""
if self.input_manager:
self.input_manager.disable_proton_manager_mode()
def closeEvent(self, event): def closeEvent(self, event):
"""Проверка, что все потоки останавливаются при закрытии приложения""" """Проверка, что все потоки останавливаются при закрытии приложения"""
logger.debug("Closing ProtonManager dialog...") logger.debug("Closing ProtonManager dialog...")
# Stop extraction thread if running # Disable gamepad mode before closing
if self.current_extraction_thread and self.current_extraction_thread.isRunning(): if self.input_manager:
logger.debug("Stopping current extraction thread...") self.disable_proton_manager_mode()
self.current_extraction_thread.stop()
if not self.current_extraction_thread.wait(2000):
logger.warning("Extraction thread did not stop gracefully during close")
# Stop download thread if running # Check if there are active processes and cancel them
try: if self.has_active_processes():
if (self.current_download_thread and logger.debug("Active processes detected, cancelling before close...")
hasattr(self.current_download_thread, 'isRunning') and self.cancel_current_download()
self.current_download_thread.isRunning()): else:
logger.debug("Stopping current download thread...") # Stop extraction thread if running
if hasattr(self.current_download_thread, 'stop'): if self.current_extraction_thread and self.current_extraction_thread.isRunning():
self.current_download_thread.stop() logger.debug("Stopping current extraction thread...")
if not self.current_download_thread.wait(2000): self.current_extraction_thread.stop()
logger.warning("Download thread did not stop gracefully during close") if not self.current_extraction_thread.wait(2000):
except RuntimeError: logger.warning("Extraction thread did not stop gracefully during close")
# Object already deleted, which is fine
logger.debug("Download thread object already deleted during close") # Stop download thread if running
try:
if (self.current_download_thread and
hasattr(self.current_download_thread, 'isRunning') and
self.current_download_thread.isRunning()):
logger.debug("Stopping current download thread...")
if hasattr(self.current_download_thread, 'stop'):
self.current_download_thread.stop()
if not self.current_download_thread.wait(2000):
logger.warning("Download thread did not stop gracefully during close")
except RuntimeError:
# Object already deleted, which is fine
logger.debug("Download thread object already deleted during close")
# If we're closing without active processes but haven't completed all downloads,
# reset the initial command flag so it can run if the dialog is opened again
if self.is_downloading and self.current_download_index < len(self.assets_to_download):
self.initial_command_executed = False
event.accept() event.accept()
def reject(self):
"""Override reject to properly cancel active processes before closing"""
# Disable gamepad mode before rejecting
if self.input_manager:
self.disable_proton_manager_mode()
if self.has_active_processes():
logger.debug("Active processes detected, cancelling before reject...")
self.cancel_current_download()
else:
# If we're rejecting without active processes but haven't completed all downloads,
# reset the initial command flag so it can run if the dialog is opened again
if self.is_downloading and self.current_download_index < len(self.assets_to_download):
self.initial_command_executed = False
super().reject()
def show_proton_manager(parent=None, portproton_location=None):
def show_proton_manager(parent=None, portproton_location=None, input_manager=None):
""" """
Shows the Proton/WINE archive extractor dialog. Shows the Proton/WINE archive extractor dialog.
Args: Args:
parent: Parent widget for the dialog parent: Parent widget for the dialog
portproton_location: Location of PortProton installation portproton_location: Location of PortProton installation
input_manager: Input manager for gamepad support
Returns: Returns:
ProtonManager dialog instance ProtonManager dialog instance
""" """
dialog = ProtonManager(parent, portproton_location) dialog = ProtonManager(parent, portproton_location, input_manager=input_manager)
dialog.exec() # Use exec() for modal dialog dialog.exec() # Use exec() for modal dialog
return dialog return dialog

View File

@@ -6,7 +6,7 @@ from typing import Protocol, cast, Any
from evdev import InputDevice, InputEvent, UInput, ecodes, list_devices, ff from evdev import InputDevice, InputEvent, UInput, ecodes, list_devices, ff
from enum import Enum from enum import Enum
from pyudev import Context, Monitor, 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, QSlider from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QSlider, QCheckBox
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent, QMouseEvent from PySide6.QtGui import QKeyEvent, QMouseEvent
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
@@ -137,6 +137,16 @@ class InputManager(QObject):
self.deadzone_value = 15 # мёртвая зона из ядра (flat параметр) self.deadzone_value = 15 # мёртвая зона из ядра (flat параметр)
self.sensitivity = 8.0 self.sensitivity = 8.0
# Dynamic attributes for different modes (declared here to satisfy type checkers)
self.winetricks_dialog = None
self.settings_dialog = None
self.file_explorer = None
self.proton_manager_dialog = None
self.original_button_handler = None
self.original_dpad_handler = None
self.original_gamepad_state = None
self._original_handlers_saved = False
self.scroll_accumulator = 0.0 self.scroll_accumulator = 0.0
self.scroll_sensitivity = 0.15 self.scroll_sensitivity = 0.15
self.scroll_threshold = 0.2 self.scroll_threshold = 0.2
@@ -343,15 +353,12 @@ class InputManager(QObject):
def enable_file_explorer_mode(self, file_explorer): def enable_file_explorer_mode(self, file_explorer):
"""Настройка обработки геймпада для FileExplorer""" """Настройка обработки геймпада для FileExplorer"""
try: try:
self.file_explorer = file_explorer self._setup_mode_handlers(
self.original_button_handler = self.handle_button_slot file_explorer,
self.original_dpad_handler = self.handle_dpad_slot self.handle_file_explorer_button,
self.original_gamepad_state = self._gamepad_handling_enabled self.handle_file_explorer_dpad,
'file_explorer'
self.handle_button_slot = self.handle_file_explorer_button )
self.handle_dpad_slot = self.handle_file_explorer_dpad
self._gamepad_handling_enabled = True
logger.debug("Gamepad handling successfully connected for FileExplorer") logger.debug("Gamepad handling successfully connected for FileExplorer")
except Exception as e: except Exception as e:
logger.error(f"Error connecting gamepad handlers for FileExplorer: {e}") logger.error(f"Error connecting gamepad handlers for FileExplorer: {e}")
@@ -360,12 +367,9 @@ class InputManager(QObject):
"""Восстановление оригинальных обработчиков (дефолт возвращаем)""" """Восстановление оригинальных обработчиков (дефолт возвращаем)"""
try: try:
if self.file_explorer: if self.file_explorer:
self.handle_button_slot = self.original_button_handler # Additional cleanup for file explorer
self.handle_dpad_slot = self.original_dpad_handler
self._gamepad_handling_enabled = self.original_gamepad_state
self.file_explorer = None
self.nav_timer.stop() self.nav_timer.stop()
self._restore_original_handlers('file_explorer')
logger.debug("Gamepad handling successfully restored") logger.debug("Gamepad handling successfully restored")
except Exception as e: except Exception as e:
logger.error(f"Error restoring gamepad handlers: {e}") logger.error(f"Error restoring gamepad handlers: {e}")
@@ -557,20 +561,12 @@ class InputManager(QObject):
def enable_winetricks_mode(self, winetricks_dialog): def enable_winetricks_mode(self, winetricks_dialog):
"""Setup gamepad handling for WinetricksDialog""" """Setup gamepad handling for WinetricksDialog"""
try: try:
self.winetricks_dialog = winetricks_dialog self._setup_mode_handlers(
self.original_button_handler = self.handle_button_slot winetricks_dialog,
self.original_dpad_handler = self.handle_dpad_slot self.handle_winetricks_button,
self.original_gamepad_state = self._gamepad_handling_enabled self.handle_winetricks_dpad,
'winetricks_dialog'
self.handle_button_slot = self.handle_winetricks_button )
self.handle_dpad_slot = self.handle_winetricks_dpad
self._gamepad_handling_enabled = True
# Reset dpad timer for table nav
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
logger.debug("Gamepad handling successfully connected for WinetricksDialog") logger.debug("Gamepad handling successfully connected for WinetricksDialog")
except Exception as e: except Exception as e:
logger.error(f"Error connecting gamepad handlers for Winetricks: {e}") logger.error(f"Error connecting gamepad handlers for Winetricks: {e}")
@@ -579,15 +575,7 @@ class InputManager(QObject):
"""Restore original main window handlers""" """Restore original main window handlers"""
try: try:
if self.winetricks_dialog: if self.winetricks_dialog:
self.handle_button_slot = self.original_button_handler self._restore_original_handlers('winetricks_dialog')
self.handle_dpad_slot = self.original_dpad_handler
self._gamepad_handling_enabled = self.original_gamepad_state
self.winetricks_dialog = None
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
logger.debug("Gamepad handling successfully restored from Winetricks") logger.debug("Gamepad handling successfully restored from Winetricks")
except Exception as e: except Exception as e:
logger.error(f"Error restoring gamepad handlers from Winetricks: {e}") logger.error(f"Error restoring gamepad handlers from Winetricks: {e}")
@@ -606,12 +594,7 @@ class InputManager(QObject):
if button_code in BUTTONS['confirm']: # A: Toggle checkbox if button_code in BUTTONS['confirm']: # A: Toggle checkbox
if isinstance(focused, QTableWidget): if isinstance(focused, QTableWidget):
current_row = focused.currentRow() self.handle_table_confirm(focused)
if current_row >= 0:
checkbox_item = focused.item(current_row, 0)
if checkbox_item and isinstance(checkbox_item, QTableWidgetItem):
new_state = Qt.CheckState.Checked if checkbox_item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
checkbox_item.setCheckState(new_state)
return return
elif button_code in BUTTONS['add_game']: # X: Install elif button_code in BUTTONS['add_game']: # X: Install
@@ -710,23 +693,387 @@ class InputManager(QObject):
table.setCurrentCell(0, 0) table.setCurrentCell(0, 0)
table.setFocus(Qt.FocusReason.OtherFocusReason) table.setFocus(Qt.FocusReason.OtherFocusReason)
# TABLE NAVIGATION METHODS
def handle_table_navigation(self, table: QTableWidget, code: int, value: int):
"""
Обрабатывает навигацию по таблице
Args:
table: QTableWidget для обработки навигации
code: Код события (обычно ABS_HAT0X или ABS_HAT0Y)
value: Значение события (направление)
"""
row_count = table.rowCount()
if row_count <= 0:
return
current_row = table.currentRow()
if current_row < 0:
current_row = 0
table.setCurrentCell(0, 0)
if code == ecodes.ABS_HAT0Y and value != 0:
# Vertical navigation
if value > 0: # Down
new_row = min(current_row + 1, row_count - 1)
elif value < 0: # Up
new_row = max(current_row - 1, 0)
else:
return
table.setCurrentCell(new_row, table.currentColumn())
item = table.item(new_row, table.currentColumn())
if item:
table.scrollToItem(
item,
QAbstractItemView.ScrollHint.PositionAtCenter
)
table.setFocus(Qt.FocusReason.OtherFocusReason)
return
elif code == ecodes.ABS_HAT0X and value != 0:
# Horizontal navigation
col_count = table.columnCount()
current_col = table.currentColumn()
if current_col < 0:
current_col = 0
if value < 0: # Left
new_col = max(current_col - 1, 0)
elif value > 0: # Right
new_col = min(current_col + 1, col_count - 1)
else:
return
table.setCurrentCell(table.currentRow(), new_col)
table.setFocus(Qt.FocusReason.OtherFocusReason)
return
def handle_table_confirm(self, table: QTableWidget):
"""
Обрабатывает подтверждение (например, нажатие A) для таблицы
Args:
table: QTableWidget для обработки подтверждения
"""
current_row = table.currentRow()
current_col = table.currentColumn()
if current_row >= 0 and current_col >= 0:
# Check if the cell contains a checkbox
item = table.item(current_row, current_col)
if item and (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
# Toggle the checkbox state
new_state = Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
item.setCheckState(new_state)
return True
# Call custom confirm callback if exists
callback = getattr(table, '_on_confirm_callback', None) # type: ignore
if callback and callable(callback):
callback(table, current_row, current_col)
return True
# WIDGET NAVIGATION METHODS
def setup_widget_navigation(self, widget: QWidget, navigation_type: str = "default", **kwargs):
"""
Устанавливает навигацию для виджета
Args:
widget: QWidget для настройки навигации
navigation_type: Тип навигации ('table', 'list', 'combo', 'default')
**kwargs: Дополнительные параметры для навигации
"""
widget.installEventFilter(self)
# Use direct assignment for custom navigation properties, with type ignore for pyright
widget._navigation_type = navigation_type # type: ignore
for key, value in kwargs.items():
setattr(widget, f'_{key}', value)
def handle_widget_navigation(self, widget: QWidget, code: int, value: int):
"""
Обрабатывает навигацию по виджету
Args:
widget: QWidget для обработки навигации
code: Код события (обычно ABS_HAT0X или ABS_HAT0Y)
value: Значение события (направление)
"""
nav_type = getattr(widget, '_navigation_type', 'default') # type: ignore
if nav_type == 'table' and isinstance(widget, QTableWidget):
self.handle_table_navigation(widget, code, value)
elif nav_type == 'list' and isinstance(widget, QListWidget):
self.handle_list_navigation(widget, code, value)
elif nav_type == 'combo' and isinstance(widget, QComboBox):
self.handle_combo_navigation(widget, code, value)
else:
# Default navigation behavior
if isinstance(widget, QTableWidget):
self.handle_table_navigation(widget, code, value)
elif isinstance(widget, QListWidget):
self.handle_list_navigation(widget, code, value)
elif isinstance(widget, QComboBox):
self.handle_combo_navigation(widget, code, value)
def handle_list_navigation(self, list_widget: QListWidget, code: int, value: int):
"""
Обрабатывает навигацию по списку
Args:
list_widget: QListWidget для обработки навигации
code: Код события (обычно ABS_HAT0X или ABS_HAT0Y)
value: Значение события (направление)
"""
if code == ecodes.ABS_HAT0Y and value != 0:
model = list_widget.model()
current_index = list_widget.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)
list_widget.setCurrentIndex(model.index(next_row, current_index.column()))
elif value < 0: # Up
prev_row = max(current_row - 1, 0)
list_widget.setCurrentIndex(model.index(prev_row, current_index.column()))
list_widget.scrollTo(list_widget.currentIndex(), QListView.ScrollHint.PositionAtCenter)
def handle_combo_navigation(self, combo_widget: QComboBox, code: int, value: int):
"""
Обрабатывает навигацию по комбинированному виджету
Args:
combo_widget: QComboBox для обработки навигации
code: Код события (обычно ABS_HAT0X или ABS_HAT0Y)
value: Значение события (направление)
"""
if code == ecodes.ABS_HAT0Y and value != 0:
current_index = combo_widget.currentIndex()
if value > 0: # Down
new_index = min(current_index + 1, combo_widget.count() - 1)
elif value < 0: # Up
new_index = max(current_index - 1, 0)
else:
return
if new_index != current_index:
combo_widget.setCurrentIndex(new_index)
def _setup_mode_handlers(self, dialog_instance, button_handler, dpad_handler, dialog_attr_name):
"""Common method to setup mode handlers"""
# Save original handlers if not already saved
if not hasattr(self, '_original_handlers_saved') or not self._original_handlers_saved:
self.original_button_handler = self.handle_button_slot
self.original_dpad_handler = self.handle_dpad_slot
self.original_gamepad_state = self._gamepad_handling_enabled
self._original_handlers_saved = True
# Set the dialog instance
if dialog_attr_name == 'winetricks_dialog':
self.winetricks_dialog = dialog_instance
elif dialog_attr_name == 'settings_dialog':
self.settings_dialog = dialog_instance
elif dialog_attr_name == 'file_explorer':
self.file_explorer = dialog_instance
elif dialog_attr_name == 'proton_manager_dialog':
self.proton_manager_dialog = dialog_instance
# Set new handlers
self.handle_button_slot = button_handler
self.handle_dpad_slot = dpad_handler
self._gamepad_handling_enabled = True
# Reset dpad timer
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
def _restore_original_handlers(self, dialog_attr_name):
"""Common method to restore original handlers"""
# Restore original handlers
self.handle_button_slot = self.original_button_handler
self.handle_dpad_slot = self.original_dpad_handler
self._gamepad_handling_enabled = self.original_gamepad_state
# Reset dpad timer
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
# Clear the dialog reference
if dialog_attr_name == 'winetricks_dialog':
self.winetricks_dialog = None
elif dialog_attr_name == 'settings_dialog':
self.settings_dialog = None
elif dialog_attr_name == 'file_explorer':
self.file_explorer = None
elif dialog_attr_name == 'proton_manager_dialog':
self.proton_manager_dialog = None
# Reset the flag so original handlers can be saved again on next enable
if hasattr(self, '_original_handlers_saved'):
self._original_handlers_saved = False
# PROTON MANAGER SUPPORT
def enable_proton_manager_mode(self, proton_manager_dialog):
"""Setup gamepad handling for ProtonManagerDialog"""
try:
self._setup_mode_handlers(
proton_manager_dialog,
self.handle_proton_manager_button,
self.handle_proton_manager_dpad,
'proton_manager_dialog'
)
logger.debug("Gamepad handling successfully connected for ProtonManager")
except Exception as e:
logger.error(f"Error connecting gamepad handlers for ProtonManager: {e}")
def disable_proton_manager_mode(self):
"""Restore original main window handlers"""
try:
if self.proton_manager_dialog:
self._restore_original_handlers('proton_manager_dialog')
logger.debug("Gamepad handling successfully restored from ProtonManager")
except Exception as e:
logger.error(f"Error restoring gamepad handlers from ProtonManager: {e}")
def handle_proton_manager_button(self, button_code, value):
if self.proton_manager_dialog is None or value == 0:
return
try:
# Handle common UI elements like QMessageBox, QMenu, etc.
if self._handle_common_ui_elements(button_code):
return
# ProtonManager-specific button handling
focused = QApplication.focusWidget()
if button_code in BUTTONS['confirm']: # A: Toggle checkbox
if isinstance(focused, QTableWidget):
current_row = focused.currentRow()
if current_row >= 0:
checkbox_widget = focused.cellWidget(current_row, 0)
if checkbox_widget:
checkbox = checkbox_widget.findChild(QCheckBox)
if checkbox and checkbox.isEnabled():
checkbox.setChecked(not checkbox.isChecked())
return
elif button_code in BUTTONS['add_game']: # X: Download
self.proton_manager_dialog.download_selected()
elif button_code in BUTTONS['prev_dir']: # Y: Clear
self.proton_manager_dialog.clear_selection()
elif button_code in BUTTONS['back']: # B: Cancel/Close
# Cancel any active downloads/extractions before closing
if (self.proton_manager_dialog.current_extraction_thread and
self.proton_manager_dialog.current_extraction_thread.isRunning()) or \
(self.proton_manager_dialog.current_download_thread and
hasattr(self.proton_manager_dialog.current_download_thread, 'isRunning') and
self.proton_manager_dialog.current_download_thread.isRunning()):
# If there's an active download/extraction, cancel it
self.proton_manager_dialog.cancel_current_download()
else:
# If no active processes, just close the dialog
self.proton_manager_dialog.reject()
elif button_code in BUTTONS['prev_tab']: # LB: Previous tab
new_index = max(0, self.proton_manager_dialog.tab_widget.currentIndex() - 1)
self.proton_manager_dialog.tab_widget.setCurrentIndex(new_index)
self._focus_first_row_in_current_proton_manager_table()
elif button_code in BUTTONS['next_tab']: # RB: Next tab
new_index = min(self.proton_manager_dialog.tab_widget.count() - 1, self.proton_manager_dialog.tab_widget.currentIndex() + 1)
self.proton_manager_dialog.tab_widget.setCurrentIndex(new_index)
self._focus_first_row_in_current_proton_manager_table()
else:
self._parent.activateFocusedWidget()
except Exception as e:
logger.error(f"Error in handle_proton_manager_button: {e}")
def handle_proton_manager_dpad(self, code, value, now):
if self.proton_manager_dialog is None:
return
try:
if value == 0: # Release
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
return
# Timer setup
if self.current_dpad_code != code or self.current_dpad_value != value:
self.dpad_timer.stop()
self.dpad_timer.setInterval(150 if self.dpad_timer.isActive() else 300)
self.dpad_timer.start()
self.current_dpad_code = code
self.current_dpad_value = value
table = self._get_current_proton_manager_table()
if not table or table.rowCount() == 0:
return
current_row = table.currentRow()
if code == ecodes.ABS_HAT0Y: # Up/Down
step = -1 if value < 0 else 1
new_row = current_row + step
# Skip hidden rows
while 0 <= new_row < table.rowCount() and table.isRowHidden(new_row):
new_row += step
# Bounds check
if new_row < 0:
new_row = current_row
if new_row >= table.rowCount():
new_row = current_row
if new_row != current_row:
table.setCurrentCell(new_row, 0)
table.setFocus(Qt.FocusReason.OtherFocusReason)
elif code == ecodes.ABS_HAT0X: # Left/Right (Tabs)
current_index = self.proton_manager_dialog.tab_widget.currentIndex()
if value < 0: # Left
new_index = max(0, current_index - 1)
else: # Right
new_index = min(self.proton_manager_dialog.tab_widget.count() - 1, current_index + 1)
if new_index != current_index:
self.proton_manager_dialog.tab_widget.setCurrentIndex(new_index)
self._focus_first_row_in_current_proton_manager_table()
except Exception as e:
logger.error(f"Error in handle_proton_manager_dpad: {e}")
def _get_current_proton_manager_table(self):
if self.proton_manager_dialog:
current_container = self.proton_manager_dialog.tab_widget.currentWidget()
if current_container:
table = current_container.findChild(QTableWidget)
return table
return None
def _focus_first_row_in_current_proton_manager_table(self):
table = self._get_current_proton_manager_table()
if table and table.rowCount() > 0:
table.setCurrentCell(0, 0)
table.setFocus(Qt.FocusReason.OtherFocusReason)
# SETTINGS MODE # SETTINGS MODE
def enable_settings_mode(self, settings_dialog): def enable_settings_mode(self, settings_dialog):
"""Setup gamepad handling for ExeSettingsDialog""" """Setup gamepad handling for ExeSettingsDialog"""
try: try:
self.settings_dialog = settings_dialog self._setup_mode_handlers(
self.original_button_handler = self.handle_button_slot settings_dialog,
self.original_dpad_handler = self.handle_dpad_slot self.handle_settings_button,
self.original_gamepad_state = self._gamepad_handling_enabled self.handle_settings_dpad,
'settings_dialog'
self.handle_button_slot = self.handle_settings_button )
self.handle_dpad_slot = self.handle_settings_dpad
self._gamepad_handling_enabled = True
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
logger.debug("Gamepad handling successfully connected for SettingsDialog") logger.debug("Gamepad handling successfully connected for SettingsDialog")
except Exception as e: except Exception as e:
logger.error(f"Error connecting gamepad handlers for SettingsDialog: {e}") logger.error(f"Error connecting gamepad handlers for SettingsDialog: {e}")
@@ -735,15 +1082,7 @@ class InputManager(QObject):
"""Restore original main window handlers""" """Restore original main window handlers"""
try: try:
if self.settings_dialog: if self.settings_dialog:
self.handle_button_slot = self.original_button_handler self._restore_original_handlers('settings_dialog')
self.handle_dpad_slot = self.original_dpad_handler
self._gamepad_handling_enabled = self.original_gamepad_state
self.settings_dialog = None
self.dpad_timer.stop()
self.current_dpad_code = None
self.current_dpad_value = 0
logger.debug("Gamepad handling successfully restored from Settings") logger.debug("Gamepad handling successfully restored from Settings")
except Exception as e: except Exception as e:
logger.error(f"Error restoring gamepad handlers from Settings: {e}") logger.error(f"Error restoring gamepad handlers from Settings: {e}")
@@ -829,18 +1168,13 @@ class InputManager(QObject):
# Standard interaction # Standard interaction
focused = QApplication.focusWidget() focused = QApplication.focusWidget()
if isinstance(focused, QTableWidget) and table and focused.currentRow() >= 0: if isinstance(focused, QTableWidget) and table and focused.currentRow() >= 0:
row = focused.currentRow()
cell = focused.cellWidget(row, 1)
# Main settings (checkboxes) # Main settings (checkboxes)
if self.settings_dialog and table == self.settings_dialog.settings_table: if self.settings_dialog and table == self.settings_dialog.settings_table:
item = focused.item(row, 1) self.handle_table_confirm(focused)
if item and (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
new_state = Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
item.setCheckState(new_state)
return return
# Advanced settings # Advanced settings
cell = focused.cellWidget(focused.currentRow(), 1)
if isinstance(cell, QComboBox) and cell.isEnabled(): if isinstance(cell, QComboBox) and cell.isEnabled():
cell.showPopup() cell.showPopup()
cell.setFocus() cell.setFocus()
@@ -1635,51 +1969,10 @@ class InputManager(QObject):
return return
# Table navigation # Table navigation using generalized methods
if isinstance(focused, QTableWidget): if isinstance(focused, QTableWidget):
row_count = focused.rowCount() self.handle_table_navigation(focused, code, value)
if row_count <= 0: return
return
current_row = focused.currentRow()
if current_row < 0:
current_row = 0
focused.setCurrentCell(0, 0)
if code == ecodes.ABS_HAT0Y and value != 0:
# Vertical navigation
if value > 0: # Down
new_row = min(current_row + 1, row_count - 1)
elif value < 0: # Up
new_row = max(current_row - 1, 0)
else:
return
focused.setCurrentCell(new_row, focused.currentColumn())
item = focused.item(new_row, focused.currentColumn())
if item:
focused.scrollToItem(
item,
QAbstractItemView.ScrollHint.PositionAtCenter
)
focused.setFocus(Qt.FocusReason.OtherFocusReason)
return
elif code == ecodes.ABS_HAT0X and value != 0:
# Horizontal navigation
col_count = focused.columnCount()
current_col = focused.currentColumn()
if current_col < 0:
current_col = 0
if value < 0: # Left
new_col = max(current_col - 1, 0)
elif value > 0: # Right
new_col = min(current_col + 1, col_count - 1)
else:
return
focused.setCurrentCell(focused.currentRow(), new_col)
focused.setFocus(Qt.FocusReason.OtherFocusReason)
return
# Search focus logic for tabs 0 and 1 # Search focus logic for tabs 0 and 1
if code == ecodes.ABS_HAT0Y and value < 0: if code == ecodes.ABS_HAT0Y and value < 0:
@@ -2009,18 +2302,10 @@ class InputManager(QObject):
# General actions: Activate, Back, Add # General actions: Activate, Back, Add
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
# Special handling for table widgets with checkboxes # Special handling for table widgets
if isinstance(focused, QTableWidget): if isinstance(focused, QTableWidget):
current_row = focused.currentRow() self.handle_table_confirm(focused)
current_col = focused.currentColumn() return True
if current_row >= 0 and current_col >= 0:
# Check if the cell contains a checkbox
item = focused.item(current_row, current_col)
if item and (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
# Toggle the checkbox state
new_state = Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
item.setCheckState(new_state)
return True
self._parent.activateFocusedWidget() self._parent.activateFocusedWidget()
return True return True
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace): elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):

View File

@@ -1889,7 +1889,7 @@ class MainWindow(QMainWindow):
def show_proton_manager(self): def show_proton_manager(self):
"""Shows the Proton/WINE manager for downloading other WINE versions""" """Shows the Proton/WINE manager for downloading other WINE versions"""
show_proton_manager(self, self.portproton_location) show_proton_manager(self, self.portproton_location, input_manager=self.input_manager)
def clear_prefix(self): def clear_prefix(self):
"""Очищает префикс""" """Очищает префикс"""