From 84306bb31b7f16c9ec305fdc3d9332fc3c2d450b Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 9 Oct 2025 15:48:55 +0500 Subject: [PATCH] feat(virtual_keyboard): added dpad reapeat movement Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 63 +++++-------- portprotonqt/virtual_keyboard.py | 148 +++++++++++++++++++++++-------- 2 files changed, 134 insertions(+), 77 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 58afc35..76a446c 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -680,31 +680,41 @@ class InputManager(QObject): else: keyboard = getattr(self._parent, 'keyboard', None) + # Handle release early + if value == 0: + self.current_dpad_code = None + self.current_dpad_value = 0 + self.axis_moving = False + self.current_axis_delay = self.initial_axis_move_delay + self.dpad_timer.stop() + return + + # Update D-pad state for continuous movement + self.current_dpad_code = code + self.current_dpad_value = value + if not self.axis_moving: + self.axis_moving = True + self.last_move_time = current_time + self.current_axis_delay = self.initial_axis_move_delay + self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) + if keyboard and keyboard.isVisible(): # Обработка горизонтального перемещения (LEFT/RIGHT) if code in (ecodes.ABS_HAT0X, ecodes.ABS_X): + normalized_value = 0 if code == ecodes.ABS_X: # Левый стик # Применяем мертвую зону if abs(value) < self.dead_zone: self.current_dpad_code = None self.current_dpad_value = 0 + self.axis_moving = False self.dpad_timer.stop() return - - # Нормализуем значение стика (-1, 0, 1) - normalized_value = 1 if value > self.dead_zone else (-1 if value < -self.dead_zone else 0) + normalized_value = 1 if value > self.dead_zone else -1 else: # D-pad normalized_value = value # D-pad уже дает -1, 0, 1 if normalized_value != 0: - # Ограничиваем частоту перемещений - now = time.time() - if now - self.last_move_time < self.current_axis_delay: - return - - self.last_move_time = now - self.current_axis_delay = self.repeat_axis_move_delay # Уменьшаем задержку после первого перемещения - if normalized_value > 0: # Вправо keyboard.move_focus_right() elif normalized_value < 0: # Влево @@ -713,28 +723,20 @@ class InputManager(QObject): # Обработка вертикального перемещения (UP/DOWN) elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y): + normalized_value = 0 if code == ecodes.ABS_Y: # Левый стик # Применяем мертвую зону if abs(value) < self.dead_zone: self.current_dpad_code = None self.current_dpad_value = 0 + self.axis_moving = False self.dpad_timer.stop() return - - # Нормализуем значение стика (-1, 0, 1) - normalized_value = 1 if value > self.dead_zone else (-1 if value < -self.dead_zone else 0) + normalized_value = 1 if value > self.dead_zone else -1 else: # D-pad normalized_value = value # D-pad уже дает -1, 0, 1 if normalized_value != 0: - # Ограничиваем частоту перемещений - now = time.time() - if now - self.last_move_time < self.current_axis_delay: - return - - self.last_move_time = now - self.current_axis_delay = self.repeat_axis_move_delay # Уменьшаем задержку после первого перемещения - if normalized_value > 0: # Вниз keyboard.move_focus_down() elif normalized_value < 0: # Вверх @@ -781,23 +783,6 @@ class InputManager(QObject): search_edit.setFocus() return - # Update D-pad state - if value != 0: - self.current_dpad_code = code - self.current_dpad_value = value - if not self.axis_moving: - self.axis_moving = True - self.last_move_time = current_time - self.current_axis_delay = self.initial_axis_move_delay - self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds) - else: - self.current_dpad_code = None - self.current_dpad_value = 0 - self.axis_moving = False - self.current_axis_delay = self.initial_axis_move_delay - self.dpad_timer.stop() # Stop timer when D-pad is released - return - # Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0: if isinstance(active, QMessageBox): # Specific handling for QMessageBox diff --git a/portprotonqt/virtual_keyboard.py b/portprotonqt/virtual_keyboard.py index e85e481..996fab9 100644 --- a/portprotonqt/virtual_keyboard.py +++ b/portprotonqt/virtual_keyboard.py @@ -451,7 +451,7 @@ class VirtualKeyboard(QFrame): focused.animateClick() def focusNextKey(self, direction: str): - """Перемещает фокус на следующую кнопку в указанном направлении""" + """Перемещает фокус на следующую кнопку в указанном направлении с обертыванием""" current = self.focusWidget() if not current: first_button = self.findFirstFocusableButton() @@ -466,48 +466,120 @@ class VirtualKeyboard(QFrame): position = cast(tuple[int, int, int, int], self.keyboard_layout.getItemPosition(current_idx)) current_row, current_col, row_span, col_span = position - # Поиск следующей кнопки + num_rows = self.keyboard_layout.rowCount() + num_cols = self.keyboard_layout.columnCount() + + found = False + if direction == "right": - next_col = current_col + col_span - next_row = current_row - max_attempts = self.keyboard_layout.columnCount() - next_col + # Сначала ищем в той же строке вправо + search_row = current_row + search_col = current_col + col_span + while search_col < num_cols: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_col += 1 + + if not found: + # Переходим к следующей строке, начиная с col 0 + search_row = (current_row + 1) % num_rows + search_col = 0 + # Ищем первую кнопку в этой строке + while search_col < num_cols: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_col += 1 + # Если не нашли в этой строке, продолжаем к следующей, но для простоты останавливаемся + # (можно добавить полный цикл, но предполагаем, что строки не пустые) + elif direction == "left": - next_col = current_col - 1 - next_row = current_row - max_attempts = next_col + 1 + # Сначала ищем в той же строке влево + search_row = current_row + search_col = current_col - 1 + while search_col >= 0: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_col -= 1 + + if not found: + # Переходим к предыдущей строке, начиная с последнего столбца + search_row = (current_row - 1) % num_rows + search_col = num_cols - 1 + # Ищем последнюю кнопку в этой строке + while search_col >= 0: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_col -= 1 + elif direction == "down": - next_col = current_col - next_row = current_row + row_span - max_attempts = self.keyboard_layout.rowCount() - next_row + # Сначала ищем в том же столбце вниз + search_col = current_col + search_row = current_row + row_span + while search_row < num_rows: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_row += 1 + + if not found: + # Переходим к следующему столбцу, начиная с row 0 + search_col = (current_col + col_span) % num_cols + search_row = 0 + # Ищем первую кнопку в этом столбце + while search_row < num_rows: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_row += 1 + elif direction == "up": - next_col = current_col - next_row = current_row - 1 - max_attempts = next_row + 1 - else: - return + # Сначала ищем в том же столбце вверх + search_col = current_col + search_row = current_row - 1 + while search_row >= 0: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_row -= 1 - next_button = None - attempts = 0 - - while attempts < max_attempts: - item = self.keyboard_layout.itemAtPosition(next_row, next_col) - if item and item.widget() and item.widget().isEnabled(): - next_button = cast(QPushButton, item.widget()) - break - - if direction == "right": - next_col += 1 - elif direction == "left": - next_col -= 1 - elif direction == "down": - next_row += 1 - elif direction == "up": - next_row -= 1 - - attempts += 1 - - if next_button: - next_button.setFocus() + if not found: + # Переходим к предыдущему столбцу, начиная с последней строки + search_col = (current_col - 1) % num_cols + search_row = num_rows - 1 + # Ищем последнюю кнопку в этом столбце + while search_row >= 0: + item = self.keyboard_layout.itemAtPosition(search_row, search_col) + if item and item.widget() and item.widget().isEnabled(): + next_button = cast(QPushButton, item.widget()) + next_button.setFocus() + found = True + break + search_row -= 1 def findFirstFocusableButton(self) -> QPushButton | None: """Находит первую фокусируемую кнопку на клавиатуре"""