feat: added virtual keyboard
All checks were successful
Code check / Check code (pull_request) Successful in 1m11s
All checks were successful
Code check / Check code (pull_request) Successful in 1m11s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -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