Compare commits
1 Commits
virtual-ke
...
renovate/a
Author | SHA1 | Date | |
---|---|---|---|
|
87020c37bb |
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:e459af116e0cb6c7d5094c0dd4c999d4335d948324192902125b7aff91601a00
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
|
@@ -20,7 +20,6 @@
|
||||
- Исправлено зависание при добавлении или удалении игры в Wayland
|
||||
- Исправлено зависание при поиске игр
|
||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
||||
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
|
||||
|
||||
### Contributors
|
||||
|
||||
|
@@ -17,7 +17,6 @@ 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
|
||||
from portprotonqt.preloader import Preloader
|
||||
import psutil
|
||||
|
||||
@@ -511,8 +510,8 @@ class FileExplorer(QDialog):
|
||||
"""Update the list of mounted drives and favorite folders."""
|
||||
for i in reversed(range(self.drives_layout.count())):
|
||||
item = self.drives_layout.itemAt(i)
|
||||
widget = item.widget() if item else None
|
||||
if widget:
|
||||
if item and item.widget():
|
||||
widget = item.widget()
|
||||
self.drives_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
@@ -818,64 +817,6 @@ 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)
|
||||
|
||||
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:
|
||||
|
@@ -12,7 +12,6 @@ from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.animations import GameCardAnimations
|
||||
from typing import cast
|
||||
|
||||
|
||||
class GameCard(QFrame):
|
||||
borderWidthChanged = Signal()
|
||||
gradientAngleChanged = Signal()
|
||||
@@ -448,7 +447,6 @@ class GameCard(QFrame):
|
||||
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
||||
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
||||
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
self.animations.paint_border(QPainter(self))
|
||||
|
@@ -14,7 +14,6 @@ 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__)
|
||||
|
||||
@@ -444,30 +443,9 @@ 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()
|
||||
@@ -476,21 +454,6 @@ 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):
|
||||
@@ -587,7 +550,6 @@ 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()
|
||||
@@ -650,6 +612,7 @@ 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)
|
||||
|
||||
@@ -664,76 +627,6 @@ 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:
|
||||
@@ -748,32 +641,6 @@ 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
|
||||
@@ -806,7 +673,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): # Keep up/down for other dialogs
|
||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0 and not isinstance(focused, QTableWidget): # Skip if focused on table
|
||||
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)
|
||||
@@ -859,7 +726,6 @@ class InputManager(QObject):
|
||||
active.show_next()
|
||||
return
|
||||
|
||||
|
||||
# Table navigation
|
||||
if isinstance(focused, QTableWidget):
|
||||
row_count = focused.rowCount()
|
||||
@@ -1413,40 +1279,3 @@ 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()
|
||||
|
@@ -1,73 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
||||
@@ -206,13 +206,8 @@ class MainWindow(QMainWindow):
|
||||
self.controlHintsWidget = self.createControlHintsWidget()
|
||||
mainLayout.addWidget(self.controlHintsWidget)
|
||||
|
||||
self.updateControlHints()
|
||||
|
||||
self.restore_state()
|
||||
|
||||
self.keyboard = VirtualKeyboard(self)
|
||||
mainLayout.addWidget(self.keyboard)
|
||||
|
||||
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
@@ -435,7 +430,6 @@ class MainWindow(QMainWindow):
|
||||
# Update navigation buttons
|
||||
self.updateNavButtons()
|
||||
|
||||
|
||||
@Slot(list)
|
||||
def on_games_loaded(self, games: list[tuple]):
|
||||
self.game_library_manager.set_games(games)
|
||||
@@ -2072,19 +2066,10 @@ class MainWindow(QMainWindow):
|
||||
completionist_time = hltb.format_game_time(game, "completionist")
|
||||
|
||||
# Очищаем layout перед добавлением новых элементов
|
||||
def clear_layout(layout):
|
||||
while layout.count():
|
||||
item = layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
sublayout = item.layout()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
elif sublayout:
|
||||
clear_layout(sublayout)
|
||||
|
||||
clear_layout(hltbLayout)
|
||||
|
||||
|
||||
while hltbLayout.count():
|
||||
child = hltbLayout.takeAt(0)
|
||||
if child.widget():
|
||||
child.widget().deleteLater()
|
||||
|
||||
has_data = False
|
||||
|
||||
@@ -2552,7 +2537,7 @@ class MainWindow(QMainWindow):
|
||||
self.settingsDebounceTimer.stop()
|
||||
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer.isActive():
|
||||
self.searchDebounceTimer.stop()
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer and self.checkProcessTimer.isActive():
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
|
@@ -1,542 +0,0 @@
|
||||
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 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
|
@@ -27,19 +27,19 @@ classifiers = [
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"babel>=2.17.0",
|
||||
"beautifulsoup4>=4.14.2",
|
||||
"beautifulsoup4>=4.13.5",
|
||||
"evdev>=1.9.2",
|
||||
"icoextract>=0.2.0",
|
||||
"numpy>=2.2.4",
|
||||
"orjson>=3.11.3",
|
||||
"orjson>=3.11.2",
|
||||
"pillow>=11.3.0",
|
||||
"psutil>=7.1.0",
|
||||
"pyside6==6.9.1",
|
||||
"psutil>=7.0.0",
|
||||
"pyside6>=6.9.1",
|
||||
"pyudev>=0.24.3",
|
||||
"requests>=2.32.5",
|
||||
"tqdm>=4.67.1",
|
||||
"vdf>=3.4",
|
||||
"websocket-client>=1.9.0",
|
||||
"websocket-client>=1.8.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -105,5 +105,5 @@ ignore = [
|
||||
dev = [
|
||||
"pre-commit>=4.3.0",
|
||||
"pyaspeller>=2.0.2",
|
||||
"pyright>=1.1.406",
|
||||
"pyright>=1.1.404",
|
||||
]
|
||||
|
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices"
|
||||
],
|
||||
"extends": ["config:best-practices"],
|
||||
"rebaseWhen": "never",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
@@ -11,23 +9,6 @@
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Update renovate only weekly",
|
||||
"matchDepNames": ["ghcr.io/renovatebot/renovate"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"description": "Automerge renovate updates",
|
||||
"matchPackageNames": [
|
||||
"ghcr.io/renovatebot/renovate"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch",
|
||||
"digest"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
@@ -52,7 +33,7 @@
|
||||
"groupName": "Python dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["numpy", "setuptools", "python", "pyside6"],
|
||||
"matchPackageNames": ["numpy", "setuptools", "python"],
|
||||
"enabled": false,
|
||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
||||
},
|
||||
|
Reference in New Issue
Block a user