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.theme_manager import ThemeManager
|
||||||
from portprotonqt.custom_widgets import AutoSizeButton
|
from portprotonqt.custom_widgets import AutoSizeButton
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
|
from portprotonqt.virtual_keyboard import VirtualKeyboard, connect_keyboard_to_lineedit
|
||||||
from portprotonqt.preloader import Preloader
|
from portprotonqt.preloader import Preloader
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
@@ -817,6 +818,69 @@ class AddGameDialog(QDialog):
|
|||||||
if edit_mode:
|
if edit_mode:
|
||||||
self.updatePreview()
|
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):
|
def browseExe(self):
|
||||||
"""Открывает файловый менеджер для выбора exe-файла"""
|
"""Открывает файловый менеджер для выбора exe-файла"""
|
||||||
try:
|
try:
|
||||||
|
@@ -14,6 +14,7 @@ from portprotonqt.custom_widgets import NavLabel, AutoSizeButton
|
|||||||
from portprotonqt.game_card import GameCard
|
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.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.dialogs import AddGameDialog, WinetricksDialog
|
||||||
|
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -443,9 +444,30 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
@Slot(int)
|
@Slot(int)
|
||||||
def handle_button_slot(self, button_code: int) -> None:
|
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:
|
if not self._gamepad_handling_enabled:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
@@ -454,6 +476,21 @@ class InputManager(QObject):
|
|||||||
if not app or not active:
|
if not app or not active:
|
||||||
return
|
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
|
# Handle Guide button to open system overlay
|
||||||
if button_code in BUTTONS['guide']:
|
if button_code in BUTTONS['guide']:
|
||||||
if not popup and not isinstance(active, QDialog):
|
if not popup and not isinstance(active, QDialog):
|
||||||
@@ -550,6 +587,7 @@ class InputManager(QObject):
|
|||||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
if isinstance(active, WinetricksDialog):
|
if isinstance(active, WinetricksDialog):
|
||||||
if button_code in BUTTONS['confirm']: # A button - toggle checkbox
|
if button_code in BUTTONS['confirm']: # A button - toggle checkbox
|
||||||
current_table = active.tab_widget.currentWidget()
|
current_table = active.tab_widget.currentWidget()
|
||||||
@@ -612,7 +650,6 @@ class InputManager(QObject):
|
|||||||
new_value = max(size_slider.value() - 10, size_slider.minimum())
|
new_value = max(size_slider.value() - 10, size_slider.minimum())
|
||||||
size_slider.setValue(new_value)
|
size_slider.setValue(new_value)
|
||||||
self._parent.on_slider_released()
|
self._parent.on_slider_released()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
@@ -627,6 +664,76 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
@Slot(int, int, float)
|
@Slot(int, int, float)
|
||||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
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:
|
if not self._gamepad_handling_enabled:
|
||||||
return
|
return
|
||||||
if not hasattr(self._parent, 'gamesListWidget') or self._parent.gamesListWidget is None:
|
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:
|
if not app or not active:
|
||||||
return
|
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
|
# Update D-pad state
|
||||||
if value != 0:
|
if value != 0:
|
||||||
self.current_dpad_code = code
|
self.current_dpad_code = code
|
||||||
@@ -673,7 +806,7 @@ class InputManager(QObject):
|
|||||||
elif value < 0: # Left
|
elif value < 0: # Left
|
||||||
active.focusPreviousChild()
|
active.focusPreviousChild()
|
||||||
return
|
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 not focused or not active.focusWidget():
|
||||||
# If no widget is focused, focus the first focusable widget
|
# If no widget is focused, focus the first focusable widget
|
||||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
@@ -726,6 +859,7 @@ class InputManager(QObject):
|
|||||||
active.show_next()
|
active.show_next()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# Table navigation
|
# Table navigation
|
||||||
if isinstance(focused, QTableWidget):
|
if isinstance(focused, QTableWidget):
|
||||||
row_count = focused.rowCount()
|
row_count = focused.rowCount()
|
||||||
@@ -1279,3 +1413,40 @@ class InputManager(QObject):
|
|||||||
self.gamepad_type = GamepadType.UNKNOWN
|
self.gamepad_type = GamepadType.UNKNOWN
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
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.downloader import Downloader
|
||||||
from portprotonqt.tray_manager import TrayManager
|
from portprotonqt.tray_manager import TrayManager
|
||||||
from portprotonqt.game_library_manager import GameLibraryManager
|
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,
|
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
||||||
@@ -210,6 +210,10 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.restore_state()
|
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)
|
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||||
QTimer.singleShot(0, self.loadGames)
|
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