feat: added virtual keyboard #57
| @@ -17,6 +17,7 @@ from portprotonqt.logger import get_logger | ||||
| from portprotonqt.theme_manager import ThemeManager | ||||
| from portprotonqt.custom_widgets import AutoSizeButton | ||||
| from portprotonqt.downloader import Downloader | ||||
| from portprotonqt.virtual_keyboard import VirtualKeyboard, connect_keyboard_to_lineedit | ||||
| from portprotonqt.preloader import Preloader | ||||
| import psutil | ||||
|  | ||||
| @@ -817,6 +818,69 @@ class AddGameDialog(QDialog): | ||||
|         if edit_mode: | ||||
|             self.updatePreview() | ||||
|  | ||||
|         # Инициализация клавиатуры (отдельным методом вроде лучше) | ||||
|         self.init_keyboard() | ||||
|  | ||||
|         # Устанавливаем фокус на первое поле при открытии | ||||
|         QTimer.singleShot(0, self.nameEdit.setFocus) | ||||
|  | ||||
|     def init_keyboard(self): | ||||
|         """Инициализация виртуальной клавиатуры""" | ||||
|         self.keyboard = VirtualKeyboard(self) | ||||
|         self.keyboard.hide() | ||||
|  | ||||
|         # Устанавливаем минимальные размеры | ||||
|         self.keyboard.setMinimumWidth(574) | ||||
|         self.keyboard.setMinimumHeight(220) | ||||
|  | ||||
|         # Подключаем клавиатуру к полям ввода | ||||
|         connect_keyboard_to_lineedit(self.keyboard, self.nameEdit) | ||||
|         connect_keyboard_to_lineedit(self.keyboard, self.exeEdit) | ||||
|         connect_keyboard_to_lineedit(self.keyboard, self.coverEdit) | ||||
|  | ||||
|     def show_keyboard_for_widget(self, widget): | ||||
|         """Показывает клавиатуру для указанного виджета""" | ||||
|         if not widget or not widget.isVisible(): | ||||
|             return | ||||
|  | ||||
|         # Устанавливаем текущий виджет ввода | ||||
|         self.keyboard.current_input_widget = widget | ||||
|  | ||||
|         # Позиционирование клавиатуры | ||||
|         keyboard_height = 220 | ||||
|         self.keyboard.setFixedWidth(self.width()) | ||||
|         self.keyboard.setFixedHeight(keyboard_height) | ||||
|         self.keyboard.move(0, self.height() - keyboard_height) | ||||
|  | ||||
|         # Показываем и поднимаем клавиатуру | ||||
|         self.keyboard.setParent(self) | ||||
|         self.keyboard.show() | ||||
|         self.keyboard.raise_() | ||||
|  | ||||
|         # TODO: доработать. | ||||
|         # Устанавливаем фокус на первую кнопку клавиатуры | ||||
|         first_button = self.keyboard.findFirstFocusableButton() | ||||
|         if first_button: | ||||
|             QTimer.singleShot(50, lambda: first_button.setFocus()) | ||||
|  | ||||
|     def closeEvent(self, event): | ||||
|         """Обработчик закрытия окна""" | ||||
|         if hasattr(self, 'keyboard'): | ||||
|             self.keyboard.hide() | ||||
|         super().closeEvent(event) | ||||
|  | ||||
|     def reject(self): | ||||
|         """Обработчик кнопки Cancel""" | ||||
|         if hasattr(self, 'keyboard'): | ||||
|             self.keyboard.hide() | ||||
|         super().reject() | ||||
|  | ||||
|     def accept(self): | ||||
|         """Обработчик кнопки Apply""" | ||||
|         if hasattr(self, 'keyboard'): | ||||
|             self.keyboard.hide() | ||||
|         super().accept() | ||||
|  | ||||
|     def browseExe(self): | ||||
|         """Открывает файловый менеджер для выбора exe-файла""" | ||||
|         try: | ||||
|   | ||||
| @@ -14,6 +14,7 @@ from portprotonqt.custom_widgets import NavLabel, AutoSizeButton | ||||
| from portprotonqt.game_card import GameCard | ||||
| from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config | ||||
| from portprotonqt.dialogs import AddGameDialog, WinetricksDialog | ||||
| from portprotonqt.virtual_keyboard import VirtualKeyboard | ||||
|  | ||||
| logger = get_logger(__name__) | ||||
|  | ||||
| @@ -443,9 +444,30 @@ class InputManager(QObject): | ||||
|  | ||||
|     @Slot(int) | ||||
|     def handle_button_slot(self, button_code: int) -> None: | ||||
|         active_window = QApplication.activeWindow() | ||||
|  | ||||
|         # Обработка виртуальной клавиатуры в AddGameDialog | ||||
|         if isinstance(active_window, AddGameDialog): | ||||
|             focused = QApplication.focusWidget() | ||||
|             if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit): | ||||
|                 # Показываем клавиатуру при нажатии A на поле ввода | ||||
|                 active_window.show_keyboard_for_widget(focused) | ||||
|                 return | ||||
|  | ||||
|             # Если клавиатура видима, обрабатываем её кнопки | ||||
|             if hasattr(active_window, 'keyboard') and active_window.keyboard.isVisible(): | ||||
|                 self.handle_virtual_keyboard(button_code) | ||||
|                 return | ||||
|  | ||||
|         keyboard = getattr(self._parent, 'keyboard', None) | ||||
|         if keyboard and keyboard.isVisible(): | ||||
|             self.handle_virtual_keyboard(button_code) | ||||
|             return | ||||
|  | ||||
|         if not self._gamepad_handling_enabled: | ||||
|             return | ||||
|         try: | ||||
|  | ||||
|             app = QApplication.instance() | ||||
|             active = QApplication.activeWindow() | ||||
|             focused = QApplication.focusWidget() | ||||
| @@ -454,6 +476,21 @@ class InputManager(QObject): | ||||
|             if not app or not active: | ||||
|                 return | ||||
|  | ||||
|             if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit): | ||||
|                 search_edit = getattr(self._parent, 'searchEdit', None) | ||||
|                 if focused == search_edit: | ||||
|                     keyboard = getattr(self._parent, 'keyboard', None) | ||||
|                     if keyboard: | ||||
|                         keyboard.show_for_widget(focused) | ||||
|                         return | ||||
|  | ||||
|                 # Handle Y button to focus search | ||||
|             if button_code in BUTTONS['prev_dir']:  # Y button | ||||
|                 search_edit = getattr(self._parent, 'searchEdit', None) | ||||
|                 if search_edit: | ||||
|                     search_edit.setFocus() | ||||
|                     return | ||||
|  | ||||
|             # Handle Guide button to open system overlay | ||||
|             if button_code in BUTTONS['guide']: | ||||
|                 if not popup and not isinstance(active, QDialog): | ||||
| @@ -550,6 +587,7 @@ class InputManager(QObject): | ||||
|                     self._parent.toggleGame(self._parent.current_exec_line, None) | ||||
|                     return | ||||
|  | ||||
|  | ||||
|             if isinstance(active, WinetricksDialog): | ||||
|                 if button_code in BUTTONS['confirm']:  # A button - toggle checkbox | ||||
|                     current_table = active.tab_widget.currentWidget() | ||||
| @@ -612,7 +650,6 @@ class InputManager(QObject): | ||||
|                     new_value = max(size_slider.value() - 10, size_slider.minimum()) | ||||
|                     size_slider.setValue(new_value) | ||||
|                     self._parent.on_slider_released() | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error in handle_button_slot: {e}", exc_info=True) | ||||
|  | ||||
| @@ -627,6 +664,76 @@ class InputManager(QObject): | ||||
|  | ||||
|     @Slot(int, int, float) | ||||
|     def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None: | ||||
|         keyboard = None | ||||
|         active_window = QApplication.activeWindow() | ||||
|  | ||||
|         # Проверяем клавиатуру в активном окне (AddGameDialog или главном окне) | ||||
|         if isinstance(active_window, AddGameDialog): | ||||
|             keyboard = getattr(active_window, 'keyboard', None) | ||||
|         else: | ||||
|             keyboard = getattr(self._parent, 'keyboard', None) | ||||
|  | ||||
|         if keyboard and keyboard.isVisible(): | ||||
|             # Обработка горизонтального перемещения (LEFT/RIGHT) | ||||
|             if code in (ecodes.ABS_HAT0X, ecodes.ABS_X): | ||||
|                 if code == ecodes.ABS_X:  # Левый стик | ||||
|                     # Применяем мертвую зону | ||||
|                     if abs(value) < self.dead_zone: | ||||
|                         self.current_dpad_code = None | ||||
|                         self.current_dpad_value = 0 | ||||
|                         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) | ||||
|                 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:  # Влево | ||||
|                         keyboard.move_focus_left() | ||||
|                 return | ||||
|  | ||||
|             # Обработка вертикального перемещения (UP/DOWN) | ||||
|             elif code in (ecodes.ABS_HAT0Y, ecodes.ABS_Y): | ||||
|                 if code == ecodes.ABS_Y:  # Левый стик | ||||
|                     # Применяем мертвую зону | ||||
|                     if abs(value) < self.dead_zone: | ||||
|                         self.current_dpad_code = None | ||||
|                         self.current_dpad_value = 0 | ||||
|                         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) | ||||
|                 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:  # Вверх | ||||
|                         keyboard.move_focus_up() | ||||
|                 return | ||||
|  | ||||
|         if not self._gamepad_handling_enabled: | ||||
|             return | ||||
|         if not hasattr(self._parent, 'gamesListWidget') or self._parent.gamesListWidget is None: | ||||
| @@ -641,6 +748,32 @@ class InputManager(QObject): | ||||
|             if not app or not active: | ||||
|                 return | ||||
|  | ||||
|             # Новый код: обработка перехода на поле поиска | ||||
|             if code == ecodes.ABS_HAT0Y and value < 0:  # Only D-pad up | ||||
|                 if isinstance(focused, GameCard): | ||||
|                     # Get all visible game cards | ||||
|                     game_cards = self._parent.gamesListWidget.findChildren(GameCard) | ||||
|                     if not game_cards: | ||||
|                         return | ||||
|  | ||||
|                     # Find the current card's position | ||||
|                     current_card_pos = focused.pos() | ||||
|                     current_row_y = current_card_pos.y() | ||||
|  | ||||
|                     # Check if this is the first row (no cards above) | ||||
|                     is_first_row = True | ||||
|                     for card in game_cards: | ||||
|                         if card.pos().y() < current_row_y and card.isVisible(): | ||||
|                             is_first_row = False | ||||
|                             break | ||||
|  | ||||
|                     # Only move to search if on first row | ||||
|                     if is_first_row: | ||||
|                         search_edit = getattr(self._parent, 'searchEdit', None) | ||||
|                         if search_edit: | ||||
|                             search_edit.setFocus() | ||||
|                             return | ||||
|  | ||||
|             # Update D-pad state | ||||
|             if value != 0: | ||||
|                 self.current_dpad_code = code | ||||
| @@ -673,7 +806,7 @@ class InputManager(QObject): | ||||
|                     elif value < 0:  # Left | ||||
|                         active.focusPreviousChild() | ||||
|                     return | ||||
|             elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0 and not isinstance(focused, QTableWidget):  # Skip if focused on table | ||||
|             elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0 and not isinstance(focused, QTableWidget):  # Keep up/down for other dialogs | ||||
|                 if not focused or not active.focusWidget(): | ||||
|                     # If no widget is focused, focus the first focusable widget | ||||
|                     focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively) | ||||
| @@ -726,6 +859,7 @@ class InputManager(QObject): | ||||
|                     active.show_next() | ||||
|                 return | ||||
|  | ||||
|  | ||||
|             # Table navigation | ||||
|             if isinstance(focused, QTableWidget): | ||||
|                 row_count = focused.rowCount() | ||||
| @@ -1279,3 +1413,40 @@ class InputManager(QObject): | ||||
|             self.gamepad_type = GamepadType.UNKNOWN | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error during cleanup: {e}", exc_info=True) | ||||
|  | ||||
|     def handle_virtual_keyboard(self, button_code: int) -> None: | ||||
|         # Проверяем клавиатуру в активном окне | ||||
|         active_window = QApplication.activeWindow() | ||||
|         keyboard = None | ||||
|  | ||||
|         # Сначала проверяем AddGameDialog | ||||
|         if isinstance(active_window, AddGameDialog): | ||||
|             keyboard = getattr(active_window, 'keyboard', None) | ||||
|         else: | ||||
|             # Если это не AddGameDialog, проверяем клавиатуру в главном окне | ||||
|             keyboard = getattr(self._parent, 'keyboard', None) | ||||
|  | ||||
|         if not keyboard or not isinstance(keyboard, VirtualKeyboard) or not keyboard.isVisible(): | ||||
|             return | ||||
|  | ||||
|         # Обработка кнопок геймпада | ||||
|         if button_code in BUTTONS['confirm']:  # Кнопка A/Cross - подтверждение | ||||
|             keyboard.activateFocusedKey() | ||||
|         elif button_code in BUTTONS['back']:  # Кнопка B/Circle - скрыть клавиатуру | ||||
|             keyboard.hide() | ||||
|             # Возвращаем фокус на поле ввода | ||||
|             if keyboard.current_input_widget: | ||||
|                 keyboard.current_input_widget.setFocus() | ||||
|         elif button_code in BUTTONS['prev_tab']:  # LB/L1 - переключение раскладки | ||||
|             keyboard.on_lang_click() | ||||
|         elif button_code in BUTTONS['next_tab']:  # RB/R1 - переключение Shift | ||||
|             keyboard.on_shift_click(not keyboard.shift_pressed) | ||||
|         elif button_code in BUTTONS['context_menu']:  # Кнопка Start - подтверждение | ||||
|             keyboard.activateFocusedKey() | ||||
|         elif button_code in BUTTONS['menu']:  # Кнопка Select - скрыть клавиатуру | ||||
|             keyboard.hide() | ||||
|             # Возвращаем фокус на поле ввода | ||||
|             if keyboard.current_input_widget: | ||||
|                 keyboard.current_input_widget.setFocus() | ||||
|         elif button_code in BUTTONS['add_game']:  # Кнопка X - Backspace | ||||
|             keyboard.on_backspace_click() | ||||
|   | ||||
							
								
								
									
										73
									
								
								portprotonqt/keyboard_layouts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								portprotonqt/keyboard_layouts.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| # keyboard_layouts.py | ||||
| keyboard_layouts = { | ||||
|     'en': { | ||||
|         'normal': [ | ||||
|             ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"], | ||||
|             ['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['~', '!', '@', '#', '$', '%', '^', '&&', '*', '(', ')', '_', '+'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'], | ||||
|             ['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?'] | ||||
|         ] | ||||
|     }, | ||||
|     'ru': { | ||||
|         'normal': [ | ||||
|             ['ё', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], | ||||
|             ['TAB', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', '\\'], | ||||
|             ['CAPS', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'], | ||||
|             ['⬆', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['Ё', '!', '"', '№', ';', '%', ':', '?', '*', '(', ')', '_', '+'], | ||||
|             ['TAB', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', '/'], | ||||
|             ['CAPS', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э'], | ||||
|             ['⬆', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ','] | ||||
|         ] | ||||
|     }, | ||||
|     'fr': { | ||||
|         'normal': [ | ||||
|             ['²', '&', 'é', '"', "'", '(', '-', 'è', '_', 'ç', 'à', ')', '='], | ||||
|             ['TAB', 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '^', '$', '*'], | ||||
|             ['CAPS', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'], | ||||
|             ['⬆', 'w', 'x', 'c', 'v', 'b', 'n', ',', ';', ':', '!'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '°', '+'], | ||||
|             ['TAB', 'A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '¨', '£', 'µ'], | ||||
|             ['CAPS', 'Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', '%'], | ||||
|             ['⬆', 'W', 'X', 'C', 'V', 'B', 'N', '?', '.', '/', '§'] | ||||
|         ] | ||||
|     }, | ||||
|     'es': { | ||||
|         'normal': [ | ||||
|             ['º', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', "'", '¡'], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '`', '+', '\\'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', "'", 'ç'], | ||||
|             ['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['ª', '!', '"', '·', '$', '%', '&', '/', '(', ')', '=', '?', '¿'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '^', '*', '|'], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ñ', '"', 'Ç'], | ||||
|             ['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_'] | ||||
|         ] | ||||
|     }, | ||||
|     'de': { | ||||
|         'normal': [ | ||||
|             ['^', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß', '´'], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü', '+', '#'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'], | ||||
|             ['⬆', '<', 'y', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['°', '!', '"', '§', '$', '%', '&', '/', '(', ')', '=', '?', '`'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', 'O', 'P', 'Ü', '*', '\''], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ö', 'Ä'], | ||||
|             ['⬆', '>', 'Y', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_'] | ||||
|         ] | ||||
|     } | ||||
| } | ||||
| @@ -35,7 +35,7 @@ from portprotonqt.howlongtobeat_api import HowLongToBeat | ||||
| from portprotonqt.downloader import Downloader | ||||
| from portprotonqt.tray_manager import TrayManager | ||||
| from portprotonqt.game_library_manager import GameLibraryManager | ||||
|  | ||||
| from portprotonqt.virtual_keyboard import VirtualKeyboard, connect_keyboard_to_lineedit | ||||
|  | ||||
| from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, | ||||
|                                QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout) | ||||
| @@ -210,6 +210,10 @@ class MainWindow(QMainWindow): | ||||
|  | ||||
|         self.restore_state() | ||||
|  | ||||
|         self.keyboard = VirtualKeyboard(self) | ||||
|         mainLayout.addWidget(self.keyboard) | ||||
|         connect_keyboard_to_lineedit(self.keyboard, self.searchEdit) | ||||
|  | ||||
|         self.detail_animations = DetailPageAnimations(self, self.theme) | ||||
|         QTimer.singleShot(0, self.loadGames) | ||||
|  | ||||
|   | ||||
							
								
								
									
										586
									
								
								portprotonqt/virtual_keyboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										586
									
								
								portprotonqt/virtual_keyboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,586 @@ | ||||
| from typing import cast | ||||
| from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout, | ||||
|                                QSizePolicy, QWidget, QLineEdit) | ||||
| from PySide6.QtCore import Qt, Signal, QProcess | ||||
| from portprotonqt.keyboard_layouts import keyboard_layouts  # Импортируем раскладки | ||||
|  | ||||
| class VirtualKeyboard(QFrame): | ||||
|     keyPressed = Signal(str) | ||||
|  | ||||
|     def __init__(self, parent: QWidget | None = None): | ||||
|         super().__init__(parent) | ||||
|         self._parent: QWidget | None = parent | ||||
|         self.available_layouts: list[str] = self.get_layouts_setxkbmap() | ||||
|         if not self.available_layouts: | ||||
|             self.available_layouts.append('en') | ||||
|         self.current_layout: str = self.available_layouts[0] | ||||
|  | ||||
|         self.focus_timer = None | ||||
|         self.focus_delay = 150  # Задержка между перемещениями в мс | ||||
|         self.last_focus_time = 0 | ||||
|  | ||||
|         self.backspace_pressed = False | ||||
|         self.backspace_timer = None | ||||
|         self.backspace_initial_delay = 500 | ||||
|         self.backspace_repeat_delay = 50 | ||||
|         self.gamepad_x_pressed = False | ||||
|         self.caps_lock = False | ||||
|         self.shift_pressed = False | ||||
|         self.current_input_widget = None | ||||
|         self.cursor_visible = True | ||||
|         self.last_focused_button = None | ||||
|         self.initUI() | ||||
|         self.hide() | ||||
|  | ||||
|         self.setStyleSheet(""" | ||||
|             VirtualKeyboard { | ||||
|                 background-color: rgba(0, 0, 0, 200);  /* Полупрозрачный серый */ | ||||
|                 border-radius: 5px; | ||||
|                 border: 1px solid #ccc; | ||||
|             } | ||||
|             QPushButton { | ||||
|                 font-size: 14px; | ||||
|                 border: 1px solid #888; | ||||
|                 border-radius: 3px; | ||||
|                 min-width: 30px; | ||||
|                 min-height: 30px; | ||||
|                 padding: 0px; | ||||
|             } | ||||
|             QPushButton:pressed { | ||||
|                 background-color: #d0d0d0; | ||||
|             } | ||||
|             QPushButton[checked="true"] { | ||||
|                 background-color: #a0c4ff; | ||||
|                 border: 1px solid #4a90e2; | ||||
|             } | ||||
|             QPushButton[checked="true"] { | ||||
|                 background-color: #4a90e2; | ||||
|                 color: white; | ||||
|                 border: 2px solid #1a73e8; | ||||
|             } | ||||
|         """) | ||||
|  | ||||
|     def keyPressEvent(self, event): | ||||
|         if event.key() == Qt.Key.Key_B or event.key() == Qt.Key.Key_Escape: | ||||
|             # Закрываем клавиатуру по нажатию B/Back | ||||
|             if isinstance(self._parent, QWidget): | ||||
|                 self._parent.hide() | ||||
|             # Возвращаем фокус на диалог | ||||
|             if self._parent and self._parent.parent(): | ||||
|                 dialog_parent = self._parent.parent() | ||||
|                 current_focused_field = getattr(dialog_parent, 'current_focused_field', None) | ||||
|                 if current_focused_field: | ||||
|                     current_focused_field.setFocus() | ||||
|             return | ||||
|  | ||||
|         elif event.key() == Qt.Key.Key_X and not self.gamepad_x_pressed: | ||||
|             self.gamepad_x_pressed = True | ||||
|             self.on_backspace_pressed() | ||||
|             return | ||||
|  | ||||
|         super().keyPressEvent(event) | ||||
|  | ||||
|     def keyReleaseEvent(self, event): | ||||
|         # Обработка отпускания кнопки X геймпада | ||||
|         if event.key() == Qt.Key.Key_X: | ||||
|             self.gamepad_x_pressed = False | ||||
|             self.stop_backspace_repeat() | ||||
|             return | ||||
|  | ||||
|         super().keyReleaseEvent(event) | ||||
|  | ||||
|     def highlight_cursor_position(self): | ||||
|         """Подсвечиваем текущую позицию курсора""" | ||||
|         if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit): | ||||
|             return | ||||
|  | ||||
|         # Просто устанавливаем курсор на нужную позицию без выделения | ||||
|         self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition()) | ||||
|  | ||||
|     def initUI(self): | ||||
|         layout = QVBoxLayout() | ||||
|         layout.setContentsMargins(0, 0, 0, 0) | ||||
|         layout.setSpacing(0) | ||||
|  | ||||
|         self.keyboard_layout = QGridLayout() | ||||
|         self.keyboard_layout.setSpacing(1) | ||||
|         self.keyboard_layout.setContentsMargins(0, 0, 0, 0) | ||||
|         self.create_keyboard() | ||||
|  | ||||
|         keyboard_container = QWidget() | ||||
|         keyboard_container.setLayout(self.keyboard_layout) | ||||
|         # keyboard_container.setFixedSize(660, 220) | ||||
|         keyboard_container.setMinimumWidth(574) | ||||
|         keyboard_container.setMinimumHeight(220) | ||||
|         keyboard_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) | ||||
|  | ||||
|         layout.addWidget(keyboard_container, 0, Qt.AlignmentFlag.AlignHCenter) | ||||
|         self.setLayout(layout) | ||||
|         # self.setMinimumHeight(240) | ||||
|         self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) | ||||
|  | ||||
|     def run_shell_command(self, cmd: str) -> str | None: | ||||
|         process = QProcess(self) | ||||
|         process.start("sh", ["-c", cmd]) | ||||
|         process.waitForFinished(-1) | ||||
|         if process.exitCode() == 0: | ||||
|             output_bytes = process.readAllStandardOutput().data() | ||||
|             if isinstance(output_bytes, memoryview): | ||||
|                 output_str = output_bytes.tobytes().decode('utf-8').strip() | ||||
|             else: | ||||
|                 output_str = output_bytes.decode('utf-8').strip() | ||||
|             return output_str | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|     def get_layouts_setxkbmap(self) -> list[str]: | ||||
|         """Получаем раскладки, которые используются в системе, возвращаем список вида ['us', 'ru'] и т.п.""" | ||||
|         cmd = r'''localectl status | awk -F: '/X11 Layout/ {gsub(/^[ \t]+/, "", $2); print $2}' ''' | ||||
|         output = self.run_shell_command(cmd) | ||||
|         if output: | ||||
|             layouts = [lang.strip() for lang in output.split(',') if lang.strip()] | ||||
|             return layouts if layouts else ['en'] | ||||
|         else: | ||||
|             return ['en'] | ||||
|  | ||||
|     def create_keyboard(self): | ||||
|         # TODO: сделать нормальное описание (сейчас лень) | ||||
|         # Основные раскладки с учетом Shift | ||||
|         # Фильтруем доступные раскладки | ||||
|  | ||||
|         LAYOUT_MAP = {'us': 'en'} | ||||
|  | ||||
|         # Assume keyboard_layouts is dict[str, dict[str, list[list[str]]]] | ||||
|         self.layouts: dict[str, dict[str, list[list[str]]]] = { | ||||
|             lang: keyboard_layouts.get(LAYOUT_MAP.get(lang, lang), keyboard_layouts['en']) | ||||
|             for lang in self.available_layouts | ||||
|         } | ||||
|  | ||||
|         self.current_layout = (self.current_layout if self.current_layout in self.layouts else next(iter(self.layouts.keys()), None) or 'en') | ||||
|  | ||||
|         self.buttons: dict[str, QPushButton] = {} | ||||
|         self.update_keyboard() | ||||
|  | ||||
|     def update_keyboard(self): | ||||
|         coords = self._save_focused_coords() | ||||
|  | ||||
|         # Очищаем предыдущие кнопки | ||||
|         while self.keyboard_layout.count(): | ||||
|             item = self.keyboard_layout.takeAt(0) | ||||
|             if item.widget(): | ||||
|                 item.widget().deleteLater() | ||||
|  | ||||
|         fixed_w = 40 | ||||
|         fixed_h = 40 | ||||
|  | ||||
|         # Выбираем текущую раскладку (обычная или с shift) | ||||
|         layout_mode = 'shift' if self.shift_pressed else 'normal' | ||||
|         layout_data = self.layouts.get(self.current_layout, {}) | ||||
|         buttons: list[list[str]] = layout_data.get(layout_mode, []) | ||||
|  | ||||
|         # Добавляем основные кнопки | ||||
|         for row_idx, row in enumerate(buttons): | ||||
|             for col_idx, key in enumerate(row): | ||||
|                 button = QPushButton(key) | ||||
|                 button.setFixedSize(fixed_w, fixed_h) | ||||
|  | ||||
|                 # Обработчики для CAPS и левого Shift | ||||
|                 if key == 'CAPS': | ||||
|                     button.setCheckable(True) | ||||
|                     button.setChecked(self.caps_lock) | ||||
|                     button.clicked.connect(self.on_caps_click) | ||||
|                 elif key == '⬆':  # Левый Shift | ||||
|                     button.setCheckable(True) | ||||
|                     button.setChecked(self.shift_pressed) | ||||
|                     button.clicked.connect(lambda checked: self.on_shift_click(checked)) | ||||
|                 else: | ||||
|                     button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k)) | ||||
|  | ||||
|                 self.keyboard_layout.addWidget(button, row_idx, col_idx) | ||||
|                 self.buttons[key] = button | ||||
|  | ||||
|         # Нижний ряд (специальные кнопки) | ||||
|         shift = QPushButton('⬆') | ||||
|         shift.setFixedSize((fixed_w * 3) + 2, fixed_h) | ||||
|         shift.setCheckable(True) | ||||
|         shift.setChecked(self.shift_pressed) | ||||
|         shift.clicked.connect(lambda checked: self.on_shift_click(checked)) | ||||
|         self.keyboard_layout.addWidget(shift, 3, 11, 1, 3) | ||||
|  | ||||
|         button = QPushButton('CAPS') | ||||
|         button.setCheckable(True) | ||||
|         button.setChecked(self.caps_lock) | ||||
|         button.clicked.connect(self.on_caps_click) | ||||
|  | ||||
|         space = QPushButton('Space') | ||||
|         space.setFixedHeight(fixed_h) | ||||
|         space.clicked.connect(lambda: self.on_button_click(' ')) | ||||
|         self.keyboard_layout.addWidget(space, 4, 1, 1, 5) | ||||
|  | ||||
|         backspace = QPushButton('⌫') | ||||
|         backspace.setFixedSize(fixed_h, fixed_w) | ||||
|         # backspace.clicked.connect(self.on_backspace_click) | ||||
|         backspace.pressed.connect(self.on_backspace_pressed) | ||||
|         backspace.released.connect(self.stop_backspace_repeat) | ||||
|         self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1) | ||||
|  | ||||
|         enter = QPushButton('Enter') | ||||
|         enter.setFixedSize((fixed_h*2) + 2, fixed_w) | ||||
|         enter.clicked.connect(self.on_enter_click) | ||||
|         self.keyboard_layout.addWidget(enter, 2, 12, 1, 2) | ||||
|  | ||||
|         lang = QPushButton('🌐') | ||||
|         lang.setFixedSize(fixed_w, fixed_h) | ||||
|         lang.clicked.connect(self.on_lang_click) | ||||
|         self.keyboard_layout.addWidget(lang, 4, 0, 1, 1) | ||||
|  | ||||
|         clear = QPushButton('Clear') | ||||
|         clear.setFixedHeight(fixed_h) | ||||
|         clear.clicked.connect(self.on_clear_click) | ||||
|         self.keyboard_layout.addWidget(clear, 4, 10, 1, 2) | ||||
|  | ||||
|         up = QPushButton('▲') | ||||
|         up.setFixedSize(fixed_w, fixed_h) | ||||
|         up.clicked.connect(self.up_key)  # Обработка клика мышью - управление курсором | ||||
|         self.keyboard_layout.addWidget(up, 4, 6, 1, 1) | ||||
|  | ||||
|         down = QPushButton('▼') | ||||
|         down.setFixedSize(fixed_w, fixed_h) | ||||
|         down.clicked.connect(self.down_key) | ||||
|         self.keyboard_layout.addWidget(down, 4, 7, 1, 1) | ||||
|  | ||||
|         left = QPushButton('◄') | ||||
|         left.setFixedSize(fixed_w, fixed_h) | ||||
|         left.clicked.connect(self.left_key) | ||||
|         self.keyboard_layout.addWidget(left, 4, 8, 1, 1) | ||||
|  | ||||
|         right = QPushButton('►') | ||||
|         right.setFixedSize(fixed_w, fixed_h) | ||||
|         right.clicked.connect(self.right_key) | ||||
|         self.keyboard_layout.addWidget(right, 4, 9, 1, 1) | ||||
|  | ||||
|         right = QPushButton('Hide') | ||||
|         right.setFixedSize((fixed_w*2) + 2, fixed_h) | ||||
|         right.clicked.connect(self.hide) | ||||
|         self.keyboard_layout.addWidget(right, 4, 12, 1, 2) | ||||
|  | ||||
|         if coords: | ||||
|             # print('изменились координаты') | ||||
|             row, col = coords | ||||
|             # print(row, col) | ||||
|             item = self.keyboard_layout.itemAtPosition(row, col) | ||||
|             # print(item) | ||||
|             if item and item.widget(): | ||||
|                 item.widget().setFocus() | ||||
|  | ||||
|     def up_key(self): | ||||
|         """Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима""" | ||||
|         if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): | ||||
|             self.current_input_widget.setCursorPosition(0) | ||||
|             self.current_input_widget.setFocus() | ||||
|  | ||||
|     def down_key(self): | ||||
|         """Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима""" | ||||
|         if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): | ||||
|             self.current_input_widget.setCursorPosition(len(self.current_input_widget.text())) | ||||
|             self.current_input_widget.setFocus() | ||||
|  | ||||
|     def left_key(self): | ||||
|         """Перемещает курсор в QLineEdit влево, если клавиатура видима""" | ||||
|         if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): | ||||
|             pos = self.current_input_widget.cursorPosition() | ||||
|             if pos > 0: | ||||
|                 self.current_input_widget.setCursorPosition(pos - 1) | ||||
|             self.current_input_widget.setFocus() | ||||
|  | ||||
|     def right_key(self): | ||||
|         """Перемещает курсор в QLineEdit вправо, если клавиатура видима""" | ||||
|         if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): | ||||
|             pos = self.current_input_widget.cursorPosition() | ||||
|             text_len = len(self.current_input_widget.text()) | ||||
|             if pos < text_len: | ||||
|                 self.current_input_widget.setCursorPosition(pos + 1) | ||||
|             self.current_input_widget.setFocus() | ||||
|  | ||||
|     def move_focus_up(self): | ||||
|         """Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью""" | ||||
|         current_time = self.get_current_time() | ||||
|         if current_time - self.last_focus_time >= self.focus_delay: | ||||
|             self.focusNextKey("up") | ||||
|             self.last_focus_time = current_time | ||||
|  | ||||
|     def move_focus_down(self): | ||||
|         """Перемещает фокус по кнопкам клавиатуры вниз с фиксированной скоростью""" | ||||
|         current_time = self.get_current_time() | ||||
|         if current_time - self.last_focus_time >= self.focus_delay: | ||||
|             self.focusNextKey("down") | ||||
|             self.last_focus_time = current_time | ||||
|  | ||||
|     def move_focus_left(self): | ||||
|         """Перемещает фокус по кнопкам клавиатуры влево с фиксированной скоростью""" | ||||
|         current_time = self.get_current_time() | ||||
|         if current_time - self.last_focus_time >= self.focus_delay: | ||||
|             self.focusNextKey("left") | ||||
|             self.last_focus_time = current_time | ||||
|  | ||||
|     def move_focus_right(self): | ||||
|         """Перемещает фокус по кнопкам клавиатуры вправо с фиксированной скоростью""" | ||||
|         current_time = self.get_current_time() | ||||
|         if current_time - self.last_focus_time >= self.focus_delay: | ||||
|             self.focusNextKey("right") | ||||
|             self.last_focus_time = current_time | ||||
|  | ||||
|     def get_current_time(self): | ||||
|         """Возвращает текущее время в миллисекундах""" | ||||
|         from time import time | ||||
|         return int(time() * 1000) | ||||
|  | ||||
|     def _save_focused_coords(self) -> tuple[int, int] | None: | ||||
|         """Возвращает (row, col) кнопки с фокусом или None""" | ||||
|         current = self.focusWidget() | ||||
|         if not current: | ||||
|             return None | ||||
|         idx = self.keyboard_layout.indexOf(current) | ||||
|         if idx == -1: | ||||
|             return None | ||||
|         position = cast(tuple[int, int, int, int], self.keyboard_layout.getItemPosition(idx)) | ||||
|         return position[:2]  # row, col | ||||
|  | ||||
|     def on_button_click(self, key): | ||||
|         if key in ['TAB', 'CAPS', '⬆']: | ||||
|             if key == 'TAB': | ||||
|                 self.on_tab_click() | ||||
|             elif key == 'CAPS': | ||||
|                 self.on_caps_click() | ||||
|             elif key == '⬆': | ||||
|                 self.on_shift_click(not self.shift_pressed) | ||||
|             self.highlight_cursor_position() | ||||
|         elif self.current_input_widget is not None: | ||||
|             # Сохраняем текущую кнопку с фокусом | ||||
|             focused_button = self.focusWidget() | ||||
|             key_to_restore = None | ||||
|             if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values(): | ||||
|                 key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None) | ||||
|  | ||||
|             key = "&" if key == "&&" else key | ||||
|             cursor_pos = self.current_input_widget.cursorPosition() | ||||
|             text = self.current_input_widget.text() | ||||
|             new_text = text[:cursor_pos] + key + text[cursor_pos:] | ||||
|             self.current_input_widget.setText(new_text) | ||||
|             self.current_input_widget.setCursorPosition(cursor_pos + len(key)) | ||||
|             self.keyPressed.emit(key) | ||||
|             self.highlight_cursor_position() | ||||
|  | ||||
|             # Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа | ||||
|             if self.shift_pressed and not self.caps_lock: | ||||
|                 self.shift_pressed = False | ||||
|                 # Сохраняем состояние перед обновлением | ||||
|                 # was_shift = self.shift_pressed | ||||
|                 self.update_keyboard() | ||||
|                 if key_to_restore and key_to_restore in self.buttons: | ||||
|                     self.buttons[key_to_restore].setFocus() | ||||
|                 # Восстанавливаем фокус | ||||
|                 # if focused_button and focused_button in self.buttons.values(): | ||||
|                 #     focused_button.setFocus() | ||||
|  | ||||
|     def on_tab_click(self): | ||||
|         if self.current_input_widget is not None: | ||||
|             self.current_input_widget.insert('\t') | ||||
|             self.keyPressed.emit('Tab') | ||||
|             self.current_input_widget.setFocus() | ||||
|             self.highlight_cursor_position() | ||||
|  | ||||
|     def on_caps_click(self): | ||||
|         """Включаем/выключаем CapsLock""" | ||||
|         self.caps_lock = not self.caps_lock | ||||
|         self.shift_pressed = self.caps_lock | ||||
|         self.update_keyboard() | ||||
|  | ||||
|     # ---------- таймерное событие ---------- | ||||
|     def timerEvent(self, event): | ||||
|         if event.timerId() == self.backspace_timer: | ||||
|             self.on_backspace_click()  # стираем ещё один символ | ||||
|             # первое срабатывание прошло – ускоряем | ||||
|             if self.backspace_timer: | ||||
|                 self.killTimer(self.backspace_timer) | ||||
|                 self.backspace_timer = self.startTimer(self.backspace_repeat_delay) | ||||
|     def on_backspace_click(self): | ||||
|         """Обработка одного нажатия Backspace""" | ||||
|         if self.current_input_widget is not None: | ||||
|             cursor_pos = self.current_input_widget.cursorPosition() | ||||
|             text = self.current_input_widget.text() | ||||
|  | ||||
|             if cursor_pos > 0: | ||||
|                 new_text = text[:cursor_pos - 1] + text[cursor_pos:] | ||||
|                 self.current_input_widget.setText(new_text) | ||||
|                 self.current_input_widget.setCursorPosition(cursor_pos - 1) | ||||
|                 self.keyPressed.emit('Backspace') | ||||
|                 self.highlight_cursor_position() | ||||
|  | ||||
|     def on_backspace_pressed(self): | ||||
|         """Обработка зажатого Backspace""" | ||||
|         self.backspace_pressed = True | ||||
|         self.start_backspace_repeat() | ||||
|  | ||||
|     def start_backspace_repeat(self): | ||||
|         """Запуск автоповтора нажатия Backspace""" | ||||
|         self.on_backspace_click()  # Первое нажатие | ||||
|         self.backspace_timer = self.startTimer(self.backspace_initial_delay) | ||||
|  | ||||
|     def stop_backspace_repeat(self): | ||||
|         """Остановка автоповтора нажатия Backspace""" | ||||
|         if self.backspace_timer: | ||||
|             self.killTimer(self.backspace_timer) | ||||
|             self.backspace_timer = None | ||||
|         self.backspace_pressed = False | ||||
|  | ||||
|     def on_enter_click(self): | ||||
|         """Обработка действия кнопки Enter""" | ||||
|         # TODO: тут подумать, как обрабатывать нажатие. | ||||
|         #  Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела | ||||
|         if self.current_input_widget is not None: | ||||
|             self.current_input_widget.insert('\n') | ||||
|             self.keyPressed.emit('Enter') | ||||
|  | ||||
|     def on_clear_click(self): | ||||
|         """Чистим строку от введённого текста""" | ||||
|         if self.current_input_widget is not None: | ||||
|             self.current_input_widget.clear() | ||||
|             self.keyPressed.emit('Clear') | ||||
|             self.highlight_cursor_position() | ||||
|  | ||||
|     def on_lang_click(self): | ||||
|         """Переключение раскладки""" | ||||
|         if not self.available_layouts: | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             current_index = self.available_layouts.index(self.current_layout) | ||||
|             next_index = (current_index + 1) % len(self.available_layouts) | ||||
|             self.current_layout = self.available_layouts[next_index] | ||||
|         except ValueError: | ||||
|             # Если текущей раскладки нет в available_layouts | ||||
|             self.current_layout = self.available_layouts[0] if self.available_layouts else 'en' | ||||
|  | ||||
|         self.update_keyboard() | ||||
|  | ||||
|         # available_layouts = list(keyboard_layouts.keys()) | ||||
|         # current_index = self.available_layouts.index(self.current_layout) | ||||
|         # next_index = (current_index + 1) % len(self.available_layouts) | ||||
|         # self.current_layout = self.available_layouts[next_index] | ||||
|         # self.update_keyboard() | ||||
|  | ||||
|     def on_shift_click(self, checked): | ||||
|         self.shift_pressed = checked | ||||
|         self.update_keyboard() | ||||
|  | ||||
|     def show_for_widget(self, widget): | ||||
|         self.current_input_widget = widget | ||||
|         if widget: | ||||
|             widget.setFocus() | ||||
|             self.highlight_cursor_position() | ||||
|  | ||||
|         # Позиционирование клавиатуры внизу родительского виджета | ||||
|         if self._parent and isinstance(self._parent, QWidget): | ||||
|             keyboard_height = 220 | ||||
|             self.setFixedWidth(self._parent.width()) | ||||
|             self.setFixedHeight(keyboard_height) | ||||
|             self.move(0, self._parent.height() - keyboard_height) | ||||
|  | ||||
|         self.show() | ||||
|         self.raise_() | ||||
|  | ||||
|         # Установить фокус на первую кнопку, если нет фокуса на виджете ввода | ||||
|         if not widget: | ||||
|             first_button: QPushButton | None = next((cast(QPushButton, btn) for btn in self.buttons.values()), None) | ||||
|             if first_button: | ||||
|                 first_button.setFocus() | ||||
|  | ||||
|     def activateFocusedKey(self): | ||||
|         """Активирует текущую выделенную кнопку на клавиатуре""" | ||||
|         focused = self.focusWidget() | ||||
|         if isinstance(focused, QPushButton): | ||||
|             focused.click() | ||||
|  | ||||
|     def focusNextKey(self, direction: str): | ||||
|         """Перемещает фокус на следующую кнопку в указанном направлении""" | ||||
|         current = self.focusWidget() | ||||
|         if not current: | ||||
|             first_button = self.findFirstFocusableButton() | ||||
|             if first_button: | ||||
|                 first_button.setFocus() | ||||
|             return | ||||
|  | ||||
|         current_idx = self.keyboard_layout.indexOf(current) | ||||
|         if current_idx == -1: | ||||
|             return | ||||
|  | ||||
|         position = cast(tuple[int, int, int, int], self.keyboard_layout.getItemPosition(current_idx)) | ||||
|         current_row, current_col, row_span, col_span = position | ||||
|  | ||||
|         # Поиск следующей кнопки | ||||
|         if direction == "right": | ||||
|             next_col = current_col + col_span | ||||
|             next_row = current_row | ||||
|             max_attempts = self.keyboard_layout.columnCount() - next_col | ||||
|         elif direction == "left": | ||||
|             next_col = current_col - 1 | ||||
|             next_row = current_row | ||||
|             max_attempts = next_col + 1 | ||||
|         elif direction == "down": | ||||
|             next_col = current_col | ||||
|             next_row = current_row + row_span | ||||
|             max_attempts = self.keyboard_layout.rowCount() - next_row | ||||
|         elif direction == "up": | ||||
|             next_col = current_col | ||||
|             next_row = current_row - 1 | ||||
|             max_attempts = next_row + 1 | ||||
|         else: | ||||
|             return | ||||
|  | ||||
|         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() | ||||
|  | ||||
|     def findFirstFocusableButton(self) -> QPushButton | None: | ||||
|         """Находит первую фокусируемую кнопку на клавиатуре""" | ||||
|         for row in range(self.keyboard_layout.rowCount()): | ||||
|             for col in range(self.keyboard_layout.columnCount()): | ||||
|                 item = self.keyboard_layout.itemAtPosition(row, col) | ||||
|                 if item and item.widget() and item.widget().isEnabled(): | ||||
|                     return cast(QPushButton, item.widget()) | ||||
|         return None | ||||
|  | ||||
| def connect_keyboard_to_lineedit(keyboard: VirtualKeyboard, line_edit: QLineEdit): | ||||
|     """Подключаем виртуальную клавиатуру к QLineEdit""" | ||||
|     original_mouse_press = line_edit.mousePressEvent | ||||
|  | ||||
|     def custom_mouse_press(event): | ||||
|         # Сначала вызываем оригинальный обработчик | ||||
|         if original_mouse_press: | ||||
|             original_mouse_press(event) | ||||
|         # Затем показываем клавиатуру | ||||
|         keyboard.show_for_widget(line_edit) | ||||
|         line_edit.setFocus() | ||||
|  | ||||
|     line_edit.mousePressEvent = custom_mouse_press | ||||
|     line_edit.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||
		Reference in New Issue
	
	Block a user