Compare commits
47 Commits
d05f2fccd6
...
main
Author | SHA1 | Date | |
---|---|---|---|
ef3f2d6e96
|
|||
657d7728a6
|
|||
9452bfda2e | |||
7eb2db0d68 | |||
6ef7a03366 | |||
e5af354b56 | |||
e6e5f6c8ea | |||
84306bb31b | |||
60af4d1482 | |||
692e11b21d | |||
b1a804811e | |||
9a30cfaea7 | |||
5dd2f71f5e | |||
dba172361b
|
|||
a9c70b8818
|
|||
135ace732f
|
|||
8b727f64e1
|
|||
a8eb591da5
|
|||
fe4ca1ee87
|
|||
ffe3e9d3d6
|
|||
49d39b5d61
|
|||
|
03566da704
|
||
|
7f996ab6a0 | ||
|
9e17978155 | ||
5d0185b1b4
|
|||
5c134be04e
|
|||
8c66695192
|
|||
7a141d8e46
|
|||
abb2377fb7
|
|||
75f4f346de
|
|||
87a9f85272
|
|||
240f685ece
|
|||
af4e3e95bb
|
|||
017d9a42cf
|
|||
18b7c4054b
|
|||
dd7f71b70a
|
|||
8fd44c575b
|
|||
65b43c1572
|
|||
f35276abfe
|
|||
6fea9a9a7e
|
|||
5189474631
|
|||
|
416cc6a268 | ||
|
3b44ed5252 | ||
c8c45dda06
|
|||
3f9f794e6f
|
|||
ba9d8b76d8
|
|||
e99c71c1f8
|
@@ -94,7 +94,7 @@ jobs:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
|
@@ -138,7 +138,7 @@ jobs:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
|
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
|
10
CHANGELOG.md
@@ -7,13 +7,22 @@
|
||||
|
||||
### Added
|
||||
- Возможность скроллинга библиотеки мышью или пальцем
|
||||
- Импорт и экспорт бекапа префикса
|
||||
- Диалог для управление Winetricks
|
||||
- Кнопки для удаления префикса, wine или proton
|
||||
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке
|
||||
|
||||
### Changed
|
||||
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
||||
- В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку
|
||||
|
||||
### Fixed
|
||||
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
||||
- Исправлено зависание при добавлении или удалении игры в Wayland
|
||||
- Исправлено зависание при поиске игр
|
||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
||||
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
|
||||
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
|
||||
|
||||
### Contributors
|
||||
|
||||
@@ -38,6 +47,7 @@
|
||||
|
||||
### Contributors
|
||||
- @wmigor (Igor Akulov)
|
||||
- @Vector_null
|
||||
|
||||
---
|
||||
|
||||
|
@@ -54,8 +54,6 @@ PortProtonQt использует код и зависимости от след
|
||||
- [Legendary](https://github.com/derrod/legendary) — инструмент для работы с Epic Games Store, лицензия [GPL-3.0](https://github.com/derrod/legendary/blob/master/LICENSE).
|
||||
- [Icoextract](https://github.com/jlu5/icoextract) — библиотека для извлечения иконок, лицензия [MIT](https://github.com/jlu5/icoextract/blob/master/LICENSE).
|
||||
- [HowLongToBeat Python API](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI) — библиотека для взаимодействия с HowLongToBeat, лицензия [MIT](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/LICENSE.md).
|
||||
- [Those Awesome Guys: Gamepad prompts images](https://thoseawesomeguys.com/prompts/) - Набор подсказок для геймпада и клавиатур, лицензия [CC0](https://creativecommons.org/public-domain/cc0/)
|
||||
|
||||
Полный текст лицензий см. в файле [LICENSE](LICENSE).
|
||||
|
||||
> [!WARNING]
|
||||
|
@@ -54,6 +54,11 @@ AppDir:
|
||||
- libxcb-cursor0
|
||||
- libimage-exiftool-perl
|
||||
- xdg-utils
|
||||
- cabextract
|
||||
- curl
|
||||
- 7zip
|
||||
- unzip
|
||||
- unrar
|
||||
exclude:
|
||||
- "*-doc"
|
||||
- "*-man"
|
||||
|
@@ -6,7 +6,7 @@ arch=('any')
|
||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||
license=('GPL-3.0')
|
||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
||||
sha256sums=('SKIP')
|
||||
|
@@ -6,7 +6,7 @@ arch=('any')
|
||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||
license=('GPL-3.0')
|
||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||
sha256sums=('SKIP')
|
||||
|
@@ -46,6 +46,11 @@ Requires: python3-pillow
|
||||
Requires: perl-Image-ExifTool
|
||||
Requires: xdg-utils
|
||||
Requires: python3-beautifulsoup4
|
||||
Requires: cabextract
|
||||
Requires: gzip
|
||||
Requires: unzip
|
||||
Requires: curl
|
||||
Requires: unrar
|
||||
|
||||
%description -n python3-%{pypi_name}-git
|
||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||
|
@@ -43,6 +43,11 @@ Requires: python3-pillow
|
||||
Requires: perl-Image-ExifTool
|
||||
Requires: xdg-utils
|
||||
Requires: python3-beautifulsoup4
|
||||
Requires: cabextract
|
||||
Requires: gzip
|
||||
Requires: unzip
|
||||
Requires: curl
|
||||
Requires: unrar
|
||||
|
||||
%description -n python3-%{pypi_name}
|
||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||
|
@@ -21,9 +21,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 of 193 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 233 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 233 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 233 of 233 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -21,9 +21,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 из 193 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 233 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 233 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 233 из 233 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -126,7 +126,21 @@ class FlowLayout(QLayout):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
return self.doLayout(QRect(0, 0, width, 0), True)
|
||||
# Аналогично фильтруем видимые для тестового расчёта высоты
|
||||
visible_items = []
|
||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
||||
for item in self.itemList:
|
||||
if item.widget() and item.widget().isVisible():
|
||||
visible_items.append(item)
|
||||
s = item.sizeHint()
|
||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
||||
|
||||
if len(visible_items) == 0:
|
||||
return 0
|
||||
|
||||
_, total_height = compute_layout(nat_sizes, width, self._spacing, self._max_scale)
|
||||
return total_height
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super().setGeometry(rect)
|
||||
@@ -145,26 +159,46 @@ class FlowLayout(QLayout):
|
||||
return size
|
||||
|
||||
def doLayout(self, rect, testOnly):
|
||||
N = len(self.itemList)
|
||||
if N == 0:
|
||||
N_total = len(self.itemList)
|
||||
if N_total == 0:
|
||||
return 0
|
||||
|
||||
nat_sizes = np.empty((N, 2), dtype=np.int32)
|
||||
# Фильтруем только видимые элементы
|
||||
visible_items = []
|
||||
visible_indices = [] # Индексы в оригинальном itemList для установки геометрии
|
||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
||||
for i, item in enumerate(self.itemList):
|
||||
s = item.sizeHint()
|
||||
nat_sizes[i, 0] = s.width()
|
||||
nat_sizes[i, 1] = s.height()
|
||||
if item.widget() and item.widget().isVisible():
|
||||
visible_items.append(item)
|
||||
visible_indices.append(i)
|
||||
s = item.sizeHint()
|
||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
||||
|
||||
N = len(visible_items)
|
||||
if N == 0:
|
||||
# Если все скрыты, устанавливаем нулевые геометрии для всех
|
||||
if not testOnly:
|
||||
for item in self.itemList:
|
||||
item.setGeometry(QRect())
|
||||
return 0
|
||||
|
||||
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
||||
|
||||
if not testOnly:
|
||||
for i, item in enumerate(self.itemList):
|
||||
x = geom_array[i, 0] + rect.x()
|
||||
y = geom_array[i, 1] + rect.y()
|
||||
w = geom_array[i, 2]
|
||||
h = geom_array[i, 3]
|
||||
# Устанавливаем геометрии только для видимых
|
||||
for idx, (_vis_idx, item) in enumerate(zip(visible_indices, visible_items, strict=True)):
|
||||
x = geom_array[idx, 0] + rect.x()
|
||||
y = geom_array[idx, 1] + rect.y()
|
||||
w = geom_array[idx, 2]
|
||||
h = geom_array[idx, 3]
|
||||
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
||||
|
||||
# Для невидимых — нулевая геометрия
|
||||
for i in range(N_total):
|
||||
if i not in visible_indices:
|
||||
self.itemList[i].setGeometry(QRect())
|
||||
|
||||
return total_height
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
|
@@ -2,11 +2,13 @@ import os
|
||||
import tempfile
|
||||
import re
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
from PySide6.QtGui import QPixmap, QIcon, QTextCursor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller
|
||||
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
|
||||
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
|
||||
)
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
|
||||
|
||||
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
|
||||
from icoextract import IconExtractor, IconExtractorError
|
||||
from PIL import Image
|
||||
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
||||
@@ -15,6 +17,8 @@ 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -507,8 +511,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)
|
||||
if item and item.widget():
|
||||
widget = item.widget()
|
||||
widget = item.widget() if item else None
|
||||
if widget:
|
||||
self.drives_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
@@ -597,6 +601,16 @@ class FileExplorer(QDialog):
|
||||
self.thumbnail_cache.clear() # Clear cache when changing directories
|
||||
self.pending_thumbnails.clear() # Clear pending thumbnails
|
||||
try:
|
||||
if self.directory_only:
|
||||
item = QListWidgetItem("./")
|
||||
folder_icon = theme_manager.get_icon("folder")
|
||||
# Ensure the icon is a QIcon
|
||||
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
|
||||
folder_icon = QIcon(folder_icon)
|
||||
elif not isinstance(folder_icon, QIcon):
|
||||
folder_icon = QIcon() # Fallback to empty icon
|
||||
item.setIcon(folder_icon)
|
||||
self.file_list.addItem(item)
|
||||
if self.current_path != "/":
|
||||
item = QListWidgetItem("../")
|
||||
folder_icon = theme_manager.get_icon("folder")
|
||||
@@ -804,6 +818,60 @@ 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, theme=self.theme, button_width=40)
|
||||
self.keyboard.hide()
|
||||
|
||||
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:
|
||||
@@ -967,3 +1035,465 @@ Icon={icon_path}
|
||||
"""
|
||||
|
||||
return desktop_entry, desktop_path
|
||||
|
||||
class WinetricksDialog(QDialog):
|
||||
"""Dialog for managing Winetricks components in a prefix."""
|
||||
|
||||
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
|
||||
super().__init__(parent)
|
||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||
self.prefix_path: str | None = prefix_path
|
||||
self.wine_use: str | None = wine_use
|
||||
self.portproton_path = get_portproton_location()
|
||||
if self.portproton_path is None:
|
||||
logger.error("PortProton location not found")
|
||||
return
|
||||
self.tmp_path = os.path.join(self.portproton_path, "data", "tmp")
|
||||
os.makedirs(self.tmp_path, exist_ok=True)
|
||||
self.winetricks_path = os.path.join(self.tmp_path, "winetricks")
|
||||
if self.prefix_path is None:
|
||||
logger.error("Prefix path not provided")
|
||||
return
|
||||
self.log_path = os.path.join(self.prefix_path, "winetricks.log")
|
||||
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
||||
if not os.path.exists(self.log_path):
|
||||
open(self.log_path, 'a').close()
|
||||
|
||||
self.downloader = Downloader(max_workers=4)
|
||||
self.apply_process: QProcess | None = None
|
||||
|
||||
self.setWindowTitle(_("Prefix Manager"))
|
||||
self.setModal(True)
|
||||
self.resize(700, 700)
|
||||
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||
|
||||
self.update_winetricks()
|
||||
self.setup_ui()
|
||||
self.load_lists()
|
||||
|
||||
def update_winetricks(self):
|
||||
"""Update the winetricks script."""
|
||||
if not self.downloader.has_internet():
|
||||
logger.warning("No internet connection, skipping winetricks update")
|
||||
return
|
||||
|
||||
url = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"
|
||||
temp_path = os.path.join(self.tmp_path, "winetricks_temp")
|
||||
|
||||
try:
|
||||
self.downloader.download(url, temp_path)
|
||||
with open(temp_path) as f:
|
||||
ext_content = f.read()
|
||||
ext_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', ext_content)
|
||||
ext_ver = ext_ver_match.group(1) if ext_ver_match else None
|
||||
logger.info(f"External winetricks version: {ext_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get external version: {e}")
|
||||
ext_ver = None
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
return
|
||||
|
||||
int_ver = None
|
||||
if os.path.exists(self.winetricks_path):
|
||||
try:
|
||||
with open(self.winetricks_path) as f:
|
||||
int_content = f.read()
|
||||
int_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', int_content)
|
||||
int_ver = int_ver_match.group(1) if int_ver_match else None
|
||||
logger.info(f"Internal winetricks version: {int_ver}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read internal winetricks version: {e}")
|
||||
|
||||
update_needed = not os.path.exists(self.winetricks_path) or (int_ver != ext_ver and ext_ver)
|
||||
|
||||
if update_needed:
|
||||
try:
|
||||
self.downloader.download(url, self.winetricks_path)
|
||||
os.chmod(self.winetricks_path, 0o755)
|
||||
logger.info(f"Winetricks updated to version {ext_ver}")
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update winetricks: {e}")
|
||||
elif os.path.exists(self.winetricks_path):
|
||||
self.apply_modifications(self.winetricks_path)
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
def apply_modifications(self, file_path):
|
||||
"""Apply custom modifications to the winetricks script."""
|
||||
if not os.path.exists(file_path):
|
||||
return
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Apply sed-like replacements
|
||||
content = re.sub(r'w_metadata vcrun2015 dlls \\', r'w_metadata !dont_use_2015! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2017 dlls \\', r'w_metadata !dont_use_2017! dlls \\', content)
|
||||
content = re.sub(r'w_metadata vcrun2019 dlls \\', r'w_metadata !dont_use_2019! dlls \\', content)
|
||||
content = re.sub(r'w_set_winver win2k3', r'w_set_winver win7', content)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
logger.info("Winetricks modifications applied")
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying modifications to winetricks: {e}")
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the user interface with tabs and tables."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
# Log output
|
||||
self.log_output = QTextEdit()
|
||||
self.log_output.setReadOnly(True)
|
||||
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
|
||||
main_layout.addWidget(self.log_output)
|
||||
|
||||
# Tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
|
||||
|
||||
table_base_style = self.theme.WINETRICKS_TABBLE_STYLE
|
||||
|
||||
# DLLs tab
|
||||
self.dll_table = QTableWidget()
|
||||
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.dll_table.setColumnCount(3)
|
||||
self.dll_table.setHorizontalHeaderLabels([_("Set"), _("Libraries"), _("Information")])
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.dll_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.dll_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.dll_preloader = Preloader()
|
||||
dll_preloader_container = QWidget()
|
||||
dll_preloader_layout = QVBoxLayout(dll_preloader_container)
|
||||
dll_preloader_layout.addStretch()
|
||||
dll_preloader_hlayout = QHBoxLayout()
|
||||
dll_preloader_hlayout.addStretch()
|
||||
dll_preloader_hlayout.addWidget(self.dll_preloader)
|
||||
dll_preloader_hlayout.addStretch()
|
||||
dll_preloader_layout.addLayout(dll_preloader_hlayout)
|
||||
dll_preloader_layout.addStretch()
|
||||
dll_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
dll_preloader_layout.setSpacing(0)
|
||||
|
||||
self.dll_container = QStackedWidget()
|
||||
self.dll_container.addWidget(dll_preloader_container)
|
||||
self.dll_container.addWidget(self.dll_table)
|
||||
self.tab_widget.addTab(self.dll_container, "DLLs")
|
||||
|
||||
# Fonts tab
|
||||
self.fonts_table = QTableWidget()
|
||||
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.fonts_table.setColumnCount(3)
|
||||
self.fonts_table.setHorizontalHeaderLabels([_("Set"), _("Fonts"), _("Information")])
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.fonts_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.fonts_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.fonts_preloader = Preloader()
|
||||
fonts_preloader_container = QWidget()
|
||||
fonts_preloader_layout = QVBoxLayout(fonts_preloader_container)
|
||||
fonts_preloader_layout.addStretch()
|
||||
fonts_preloader_hlayout = QHBoxLayout()
|
||||
fonts_preloader_hlayout.addStretch()
|
||||
fonts_preloader_hlayout.addWidget(self.fonts_preloader)
|
||||
fonts_preloader_hlayout.addStretch()
|
||||
fonts_preloader_layout.addLayout(fonts_preloader_hlayout)
|
||||
fonts_preloader_layout.addStretch()
|
||||
fonts_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
fonts_preloader_layout.setSpacing(0)
|
||||
|
||||
self.fonts_container = QStackedWidget()
|
||||
self.fonts_container.addWidget(fonts_preloader_container)
|
||||
self.fonts_container.addWidget(self.fonts_table)
|
||||
self.tab_widget.addTab(self.fonts_container, _("Fonts"))
|
||||
|
||||
# Settings tab
|
||||
self.settings_table = QTableWidget()
|
||||
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.settings_table.setColumnCount(3)
|
||||
self.settings_table.setHorizontalHeaderLabels([_("Set"), _("Settings"), _("Information")])
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.settings_table.horizontalHeader().resizeSection(0, 50)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
self.settings_table.setStyleSheet(table_base_style)
|
||||
|
||||
self.settings_preloader = Preloader()
|
||||
settings_preloader_container = QWidget()
|
||||
settings_preloader_layout = QVBoxLayout(settings_preloader_container)
|
||||
settings_preloader_layout.addStretch()
|
||||
settings_preloader_hlayout = QHBoxLayout()
|
||||
settings_preloader_hlayout.addStretch()
|
||||
settings_preloader_hlayout.addWidget(self.settings_preloader)
|
||||
settings_preloader_hlayout.addStretch()
|
||||
settings_preloader_layout.addLayout(settings_preloader_hlayout)
|
||||
settings_preloader_layout.addStretch()
|
||||
settings_preloader_layout.setContentsMargins(0, 0, 0, 0)
|
||||
settings_preloader_layout.setSpacing(0)
|
||||
|
||||
self.settings_container = QStackedWidget()
|
||||
self.settings_container.addWidget(settings_preloader_container)
|
||||
self.settings_container.addWidget(self.settings_table)
|
||||
self.tab_widget.addTab(self.settings_container, _("Settings"))
|
||||
|
||||
self.containers = {
|
||||
"dlls": self.dll_container,
|
||||
"fonts": self.fonts_container,
|
||||
"settings": self.settings_container
|
||||
}
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||
self.force_button = AutoSizeButton(_("Force Install"), icon=theme_manager.get_icon("apply"))
|
||||
self.install_button = AutoSizeButton(_("Install"), icon=theme_manager.get_icon("apply"))
|
||||
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.force_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
self.install_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
button_layout.addWidget(self.force_button)
|
||||
button_layout.addWidget(self.install_button)
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
|
||||
self.install_button.clicked.connect(lambda: self.install_selected(force=False))
|
||||
|
||||
def load_lists(self):
|
||||
"""Load and populate the lists for DLLs, Fonts, and Settings"""
|
||||
if not os.path.exists(self.winetricks_path):
|
||||
QMessageBox.warning(self, _("Error"), _("Winetricks not found. Please try again."))
|
||||
self.reject()
|
||||
return
|
||||
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
env.insert("WINETRICKS_DOWNLOADER", "curl")
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
cwd = os.path.dirname(self.winetricks_path)
|
||||
|
||||
# DLLs
|
||||
self.containers["dlls"].setCurrentIndex(0)
|
||||
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
|
||||
|
||||
# Fonts
|
||||
self.containers["fonts"].setCurrentIndex(0)
|
||||
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
|
||||
|
||||
# Settings
|
||||
self.containers["settings"].setCurrentIndex(0)
|
||||
self._start_list_process("settings", self.settings_table, self.get_settings_exclusions(), env, cwd)
|
||||
|
||||
def _start_list_process(self, category, table, exclusion_pattern, env, cwd):
|
||||
"""Запускает QProcess для списка."""
|
||||
process = QProcess(self)
|
||||
process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
process.setProcessEnvironment(env)
|
||||
process.finished.connect(lambda exit_code, exit_status: self._on_list_finished(category, table, exclusion_pattern, process, exit_code, exit_status))
|
||||
process.start(self.winetricks_path, [category, "list"])
|
||||
|
||||
def _on_list_finished(self, category, table, exclusion_pattern, process: QProcess | None, exit_code, exit_status):
|
||||
"""Обработчик завершения списка."""
|
||||
if process is None:
|
||||
logger.error(f"Process is None for {category}")
|
||||
self.containers[category].setCurrentIndex(1)
|
||||
return
|
||||
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||
self.populate_table(table, output, exclusion_pattern, self.log_path)
|
||||
# Restore focus after populating
|
||||
if table.rowCount() > 0:
|
||||
table.setCurrentCell(0, 0)
|
||||
table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
else:
|
||||
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||
logger.error(f"Failed to list {category}: {error_output}")
|
||||
|
||||
self.containers[category].setCurrentIndex(1)
|
||||
|
||||
def get_dll_exclusions(self):
|
||||
"""Get regex pattern for DLL exclusions."""
|
||||
return r'(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio1|Foundation)'
|
||||
|
||||
def get_fonts_exclusions(self):
|
||||
"""Get regex pattern for Fonts exclusions."""
|
||||
return r'dont_use'
|
||||
|
||||
def get_settings_exclusions(self):
|
||||
"""Get regex pattern for Settings exclusions."""
|
||||
return r'(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)'
|
||||
|
||||
def populate_table(self, table, output, exclusion_pattern, log_path):
|
||||
"""Populate the table with items from output, checking installation status."""
|
||||
table.setRowCount(0)
|
||||
table.verticalHeader().setVisible(False)
|
||||
lines = output.strip().split('\n')
|
||||
installed = set()
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# regex-парсинг (имя - первое слово, остальное - описание)
|
||||
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or re.search(exclusion_pattern, line, re.I):
|
||||
continue
|
||||
|
||||
line = line.split('(', 1)[0].strip()
|
||||
|
||||
match = line_re.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
_status, name, info = match.groups()
|
||||
# Очищаем info от мусора
|
||||
info = re.sub(r'\[.*?\]', '', info).strip() # Удаляем [скачивания] и т.п.
|
||||
|
||||
# To match bash desc extraction: after name, substr(2) to trim leading space
|
||||
if info.startswith(' '):
|
||||
info = info[1:].lstrip()
|
||||
|
||||
# Фильтр служебных строк
|
||||
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
|
||||
continue
|
||||
|
||||
checked = Qt.CheckState.Checked if name in installed else Qt.CheckState.Unchecked
|
||||
|
||||
row = table.rowCount()
|
||||
table.insertRow(row)
|
||||
|
||||
# Checkbox
|
||||
checkbox = QTableWidgetItem()
|
||||
checkbox.setCheckState(checked)
|
||||
table.setItem(row, 0, checkbox)
|
||||
|
||||
# Name
|
||||
name_item = QTableWidgetItem(name)
|
||||
table.setItem(row, 1, name_item)
|
||||
|
||||
# Info
|
||||
info_item = QTableWidgetItem(info)
|
||||
table.setItem(row, 2, info_item)
|
||||
|
||||
def install_selected(self, force=False):
|
||||
"""Install selected components."""
|
||||
selected = []
|
||||
for table in [self.dll_table, self.fonts_table, self.settings_table]:
|
||||
for row in range(table.rowCount()):
|
||||
checkbox = table.item(row, 0)
|
||||
if checkbox is not None and checkbox.checkState() == Qt.CheckState.Checked:
|
||||
name_item = table.item(row, 1)
|
||||
if name_item is not None:
|
||||
name = name_item.text()
|
||||
if name and name not in selected:
|
||||
selected.append(name)
|
||||
|
||||
# Load installed
|
||||
installed = set()
|
||||
if os.path.exists(self.log_path):
|
||||
with open(self.log_path) as f:
|
||||
for line in f:
|
||||
installed.add(line.strip())
|
||||
|
||||
# Filter to new selected
|
||||
new_selected = [name for name in selected if name not in installed]
|
||||
|
||||
if not new_selected:
|
||||
QMessageBox.information(self, _("Warning"), _("No components selected."))
|
||||
return
|
||||
|
||||
self.install_button.setEnabled(False)
|
||||
self.force_button.setEnabled(False)
|
||||
self.cancel_button.setEnabled(False)
|
||||
|
||||
self._start_install_process(new_selected, force)
|
||||
|
||||
def _start_install_process(self, selected, force):
|
||||
"""Запускает QProcess для установки."""
|
||||
assert self.prefix_path is not None
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
if self.wine_use is not None:
|
||||
env.insert("WINE", self.wine_use)
|
||||
|
||||
self.apply_process = QProcess(self)
|
||||
self.apply_process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
|
||||
self.apply_process.setProcessEnvironment(env)
|
||||
self.apply_process.readyReadStandardOutput.connect(self._on_ready_read)
|
||||
self.apply_process.finished.connect(lambda exit_code, exit_status: self._on_install_finished(exit_code, exit_status, selected))
|
||||
args = ["--unattended"] + (["--force"] if force else []) + selected
|
||||
self.apply_process.start(self.winetricks_path, args)
|
||||
|
||||
def _on_ready_read(self):
|
||||
"""Handle ready read for install process."""
|
||||
if self.apply_process is None:
|
||||
return
|
||||
data = self.apply_process.readAllStandardOutput().data()
|
||||
message = bytes(data).decode('utf-8', 'ignore').strip()
|
||||
self._log(message)
|
||||
|
||||
def _on_install_finished(self, exit_code, exit_status, selected):
|
||||
"""Обработчик завершения установки."""
|
||||
error_message = ""
|
||||
if self.apply_process is not None:
|
||||
# Читаем вывод в зависимости от режима каналов
|
||||
if self.apply_process.processChannelMode() == QProcess.ProcessChannelMode.MergedChannels:
|
||||
# Если каналы объединены, читаем из StandardOutput
|
||||
output_data = self.apply_process.readAllStandardOutput().data()
|
||||
error_message = bytes(output_data).decode('utf-8', 'ignore')
|
||||
else:
|
||||
# Если каналы разделены, читаем из StandardError
|
||||
error_data = self.apply_process.readAllStandardError().data()
|
||||
error_message = bytes(error_data).decode('utf-8', 'ignore')
|
||||
|
||||
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||
logger.error(f"Winetricks install failed: {error_message}")
|
||||
QMessageBox.warning(self, _("Error"), _("Installation failed. Check logs."))
|
||||
else:
|
||||
if os.path.exists(self.log_path):
|
||||
with open(self.log_path) as f:
|
||||
existing = {line.strip() for line in f if line.strip()}
|
||||
else:
|
||||
existing = set()
|
||||
with open(self.log_path, 'a') as f:
|
||||
for name in selected:
|
||||
if name not in existing:
|
||||
f.write(f"{name}\n")
|
||||
logger.info("Winetricks installation completed successfully.")
|
||||
QMessageBox.information(self, _("Success"), _("Components installed successfully."))
|
||||
self.load_lists()
|
||||
|
||||
# Разблокировка
|
||||
self.install_button.setEnabled(True)
|
||||
self.force_button.setEnabled(True)
|
||||
self.cancel_button.setEnabled(True)
|
||||
|
||||
def _log(self, message):
|
||||
"""Добавляет в лог."""
|
||||
self.log_output.append(message)
|
||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||
|
@@ -12,6 +12,7 @@ from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.animations import GameCardAnimations
|
||||
from typing import cast
|
||||
|
||||
|
||||
class GameCard(QFrame):
|
||||
borderWidthChanged = Signal()
|
||||
gradientAngleChanged = Signal()
|
||||
@@ -447,6 +448,7 @@ 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))
|
||||
|
@@ -35,6 +35,7 @@ class MainWindowProtocol(Protocol):
|
||||
_last_card_width: int
|
||||
current_hovered_card: GameCard | None
|
||||
current_focused_card: GameCard | None
|
||||
gamesListWidget: QWidget | None
|
||||
|
||||
class GameLibraryManager:
|
||||
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
||||
@@ -51,8 +52,9 @@ class GameLibraryManager:
|
||||
self.sizeSlider: QSlider | None = None
|
||||
self._update_timer: QTimer | None = None
|
||||
self._pending_update = False
|
||||
self.pending_deletions = deque() # Queue for deferred widget deletion
|
||||
self.dirty = False # Flag for when full resort is needed
|
||||
self.pending_deletions = deque()
|
||||
self.is_filtering = False
|
||||
self.dirty = False
|
||||
|
||||
def create_games_library_widget(self):
|
||||
"""Creates the games library widget with search, grid, and slider."""
|
||||
@@ -201,10 +203,13 @@ class GameLibraryManager:
|
||||
self._pending_update = False
|
||||
self._update_game_grid_immediate()
|
||||
|
||||
def update_game_grid(self, games_list: list[tuple] | None = None):
|
||||
def update_game_grid(self, games_list: list[tuple] | None = None, is_filter: bool = False):
|
||||
"""Schedules a game grid update with debouncing."""
|
||||
if games_list is not None:
|
||||
self.filtered_games = games_list
|
||||
if not is_filter:
|
||||
if games_list is not None:
|
||||
self.filtered_games = games_list
|
||||
self.dirty = True # Full rebuild only for non-filter
|
||||
self.is_filtering = is_filter
|
||||
self._pending_update = True
|
||||
|
||||
if self._update_timer is not None:
|
||||
@@ -217,123 +222,158 @@ class GameLibraryManager:
|
||||
if self.gamesListLayout is None or self.gamesListWidget is None:
|
||||
return
|
||||
|
||||
games_list = self.filtered_games if self.filtered_games else self.games
|
||||
search_text = self.main_window.searchEdit.text().strip().lower()
|
||||
favorites = read_favorites()
|
||||
sort_method = read_sort_method()
|
||||
|
||||
# Batch layout updates (extended scope)
|
||||
self.gamesListWidget.setUpdatesEnabled(False)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.setEnabled(False) # Disable layout during batch
|
||||
if self.is_filtering:
|
||||
# Filter mode: do not change layout, only hide/show cards
|
||||
self._apply_filter_visibility(search_text)
|
||||
else:
|
||||
# Full update: sorting, removal/addition, reorganization
|
||||
games_list = self.filtered_games if self.filtered_games else self.games
|
||||
favorites = read_favorites()
|
||||
sort_method = read_sort_method()
|
||||
|
||||
try:
|
||||
# Optimized sorting: Partition favorites first, then sort subgroups
|
||||
def partition_sort_key(game):
|
||||
name = game[0]
|
||||
is_fav = name in favorites
|
||||
fav_order = 0 if is_fav else 1
|
||||
if sort_method == "playtime":
|
||||
return (fav_order, -game[11] if game[11] else 0, -game[10] if game[10] else 0)
|
||||
elif sort_method == "alphabetical":
|
||||
return (fav_order, name.lower())
|
||||
elif sort_method == "favorites":
|
||||
return (fav_order,)
|
||||
# Batch layout updates (extended scope)
|
||||
self.gamesListWidget.setUpdatesEnabled(False)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.setEnabled(False) # Disable layout during batch
|
||||
|
||||
try:
|
||||
# Optimized sorting: Partition favorites first, then sort subgroups
|
||||
def partition_sort_key(game):
|
||||
name = game[0]
|
||||
is_fav = name in favorites
|
||||
fav_order = 0 if is_fav else 1
|
||||
if sort_method == "playtime":
|
||||
return (fav_order, -game[11] if game[11] else 0, -game[10] if game[10] else 0)
|
||||
elif sort_method == "alphabetical":
|
||||
return (fav_order, name.lower())
|
||||
elif sort_method == "favorites":
|
||||
return (fav_order,)
|
||||
else:
|
||||
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
|
||||
|
||||
# Quick partition: Sort favorites and non-favorites separately, then merge
|
||||
fav_games = [g for g in games_list if g[0] in favorites]
|
||||
non_fav_games = [g for g in games_list if g[0] not in favorites]
|
||||
sorted_fav = sorted(fav_games, key=partition_sort_key)
|
||||
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
||||
sorted_games = sorted_fav + sorted_non_fav
|
||||
|
||||
# Build set of current game keys for faster lookup
|
||||
current_game_keys = {(game[0], game[4]) for game in sorted_games}
|
||||
|
||||
# Remove cards that no longer exist (batch)
|
||||
cards_to_remove = []
|
||||
for card_key in list(self.game_card_cache.keys()):
|
||||
if card_key not in current_game_keys:
|
||||
cards_to_remove.append(card_key)
|
||||
|
||||
for card_key in cards_to_remove:
|
||||
card = self.game_card_cache.pop(card_key)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.removeWidget(card)
|
||||
self.pending_deletions.append(card) # Defer
|
||||
if card_key in self.pending_images:
|
||||
del self.pending_images[card_key]
|
||||
|
||||
# Track current layout order (only if dirty/full update needed)
|
||||
if self.dirty and self.gamesListLayout is not None:
|
||||
current_layout_order = []
|
||||
for i in range(self.gamesListLayout.count()):
|
||||
item = self.gamesListLayout.itemAt(i)
|
||||
if item is not None:
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
for key, card in self.game_card_cache.items():
|
||||
if card == widget:
|
||||
current_layout_order.append(key)
|
||||
break
|
||||
else:
|
||||
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
|
||||
current_layout_order = None # Skip reorg if not dirty
|
||||
|
||||
# Quick partition: Sort favorites and non-favorites separately, then merge
|
||||
fav_games = [g for g in games_list if g[0] in favorites]
|
||||
non_fav_games = [g for g in games_list if g[0] not in favorites]
|
||||
sorted_fav = sorted(fav_games, key=partition_sort_key)
|
||||
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
||||
sorted_games = sorted_fav + sorted_non_fav
|
||||
new_card_order = []
|
||||
cards_to_add = []
|
||||
|
||||
# Build set of current game keys for faster lookup
|
||||
current_game_keys = {(game[0], game[4]) for game in sorted_games}
|
||||
for game_data in sorted_games:
|
||||
game_name = game_data[0]
|
||||
exec_line = game_data[4]
|
||||
game_key = (game_name, exec_line)
|
||||
should_be_visible = not search_text or search_text in game_name.lower()
|
||||
|
||||
# Remove cards that no longer exist (batch)
|
||||
cards_to_remove = []
|
||||
for card_key in list(self.game_card_cache.keys()):
|
||||
if card_key not in current_game_keys:
|
||||
cards_to_remove.append(card_key)
|
||||
if game_key in self.game_card_cache:
|
||||
card = self.game_card_cache[game_key]
|
||||
if card.isVisible() != should_be_visible:
|
||||
card.setVisible(should_be_visible)
|
||||
new_card_order.append(game_key)
|
||||
else:
|
||||
if self.context_menu_manager is None:
|
||||
continue
|
||||
|
||||
for card_key in cards_to_remove:
|
||||
card = self.game_card_cache.pop(card_key)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.removeWidget(card)
|
||||
self.pending_deletions.append(card) # Defer
|
||||
if card_key in self.pending_images:
|
||||
del self.pending_images[card_key]
|
||||
|
||||
# Track current layout order (only if dirty/full update needed)
|
||||
if self.dirty and self.gamesListLayout is not None:
|
||||
current_layout_order = []
|
||||
for i in range(self.gamesListLayout.count()):
|
||||
item = self.gamesListLayout.itemAt(i)
|
||||
if item is not None:
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
for key, card in self.game_card_cache.items():
|
||||
if card == widget:
|
||||
current_layout_order.append(key)
|
||||
break
|
||||
else:
|
||||
current_layout_order = None # Skip reorg if not dirty
|
||||
|
||||
new_card_order = []
|
||||
cards_to_add = []
|
||||
|
||||
for game_data in sorted_games:
|
||||
game_name = game_data[0]
|
||||
exec_line = game_data[4]
|
||||
game_key = (game_name, exec_line)
|
||||
should_be_visible = not search_text or search_text in game_name.lower()
|
||||
|
||||
if game_key in self.game_card_cache:
|
||||
card = self.game_card_cache[game_key]
|
||||
if card.isVisible() != should_be_visible:
|
||||
card = self._create_game_card(game_data)
|
||||
self.game_card_cache[game_key] = card
|
||||
card.setVisible(should_be_visible)
|
||||
new_card_order.append(game_key)
|
||||
else:
|
||||
if self.context_menu_manager is None:
|
||||
continue
|
||||
new_card_order.append(game_key)
|
||||
cards_to_add.append((game_key, card))
|
||||
|
||||
card = self._create_game_card(game_data)
|
||||
self.game_card_cache[game_key] = card
|
||||
card.setVisible(should_be_visible)
|
||||
new_card_order.append(game_key)
|
||||
cards_to_add.append((game_key, card))
|
||||
# Only reorganize if order changed AND dirty
|
||||
if self.dirty and self.gamesListLayout is not None and (current_layout_order is None or new_card_order != current_layout_order):
|
||||
# Remove all widgets from layout (batch)
|
||||
while self.gamesListLayout.count():
|
||||
self.gamesListLayout.takeAt(0)
|
||||
|
||||
# Only reorganize if order changed AND dirty
|
||||
if self.dirty and self.gamesListLayout is not None and (current_layout_order is None or new_card_order != current_layout_order):
|
||||
# Remove all widgets from layout (batch)
|
||||
while self.gamesListLayout.count():
|
||||
self.gamesListLayout.takeAt(0)
|
||||
# Add widgets in new order (batch)
|
||||
for game_key in new_card_order:
|
||||
card = self.game_card_cache[game_key]
|
||||
self.gamesListLayout.addWidget(card)
|
||||
|
||||
# Add widgets in new order (batch)
|
||||
for game_key in new_card_order:
|
||||
card = self.game_card_cache[game_key]
|
||||
self.gamesListLayout.addWidget(card)
|
||||
self.dirty = False # Reset flag
|
||||
|
||||
self.dirty = False # Reset flag
|
||||
# Deferred deletions (run in timer to avoid stack overflow)
|
||||
if self.pending_deletions:
|
||||
QTimer.singleShot(0, lambda: self._flush_deletions())
|
||||
|
||||
# Deferred deletions (run in timer to avoid stack overflow)
|
||||
if self.pending_deletions:
|
||||
QTimer.singleShot(0, lambda: self._flush_deletions())
|
||||
# Load visible images for new cards only
|
||||
if cards_to_add:
|
||||
self.load_visible_images()
|
||||
|
||||
# Load visible images for new cards only
|
||||
if cards_to_add:
|
||||
self.load_visible_images()
|
||||
finally:
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.setEnabled(True)
|
||||
self.gamesListWidget.setUpdatesEnabled(True)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.update()
|
||||
self.gamesListWidget.updateGeometry()
|
||||
self.main_window._last_card_width = self.card_width
|
||||
|
||||
finally:
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.setEnabled(True)
|
||||
self.gamesListWidget.setUpdatesEnabled(True)
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.update()
|
||||
self.is_filtering = False # Reset flag in any case
|
||||
|
||||
def _apply_filter_visibility(self, search_text: str):
|
||||
"""Applies visibility to cards based on search, without changing the layout."""
|
||||
visible_count = 0
|
||||
for game_key, card in self.game_card_cache.items():
|
||||
game_name = card.name # Assume GameCard has 'name' attribute
|
||||
should_be_visible = not search_text or search_text in game_name.lower()
|
||||
if card.isVisible() != should_be_visible:
|
||||
card.setVisible(should_be_visible)
|
||||
if should_be_visible:
|
||||
visible_count += 1
|
||||
# Load image only for newly visible cards
|
||||
if game_key in self.pending_images:
|
||||
cover_path, width, height, callback = self.pending_images.pop(game_key)
|
||||
load_pixmap_async(cover_path, width, height, callback)
|
||||
|
||||
# Force full relayout after visibility changes
|
||||
if self.gamesListLayout is not None:
|
||||
self.gamesListLayout.invalidate() # Принудительно инвалидируем для пересчёта
|
||||
self.gamesListLayout.update()
|
||||
if self.gamesListWidget is not None:
|
||||
self.gamesListWidget.updateGeometry()
|
||||
self.main_window._last_card_width = self.card_width
|
||||
self.main_window._last_card_width = self.card_width
|
||||
|
||||
# If search is empty, load images for visible ones
|
||||
if not search_text:
|
||||
self.load_visible_images()
|
||||
|
||||
def _create_game_card(self, game_data: tuple) -> GameCard:
|
||||
"""Creates a new game card with all necessary connections."""
|
||||
@@ -412,9 +452,4 @@ class GameLibraryManager:
|
||||
|
||||
def filter_games_delayed(self):
|
||||
"""Filters games based on search text and updates the grid."""
|
||||
text = self.main_window.searchEdit.text().strip().lower()
|
||||
if text == "":
|
||||
self.filtered_games = self.games
|
||||
else:
|
||||
self.filtered_games = [game for game in self.games if text in game[0].lower()]
|
||||
self.update_game_grid(self.filtered_games)
|
||||
self.update_game_grid(is_filter=True)
|
||||
|
@@ -5,7 +5,7 @@ from typing import Protocol, cast
|
||||
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||
from enum import Enum
|
||||
from pyudev import Context, Monitor, MonitorObserver, Device
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView
|
||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
||||
from PySide6.QtGui import QKeyEvent, QMouseEvent
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -13,7 +13,8 @@ from portprotonqt.image_utils import FullscreenDialog
|
||||
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
|
||||
from portprotonqt.dialogs import AddGameDialog, WinetricksDialog
|
||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -71,7 +72,7 @@ class InputManager(QObject):
|
||||
for seamless UI interaction.
|
||||
"""
|
||||
# Signals for gamepad events
|
||||
button_pressed = Signal(int) # Signal for button presses
|
||||
button_event = Signal(int, int) # Signal for button events: (code, value) where value=1 (press), 0 (release)
|
||||
dpad_moved = Signal(int, int, float) # Signal for D-pad movements
|
||||
toggle_fullscreen = Signal(bool) # Signal for toggling fullscreen mode (True for fullscreen, False for normal)
|
||||
|
||||
@@ -130,7 +131,7 @@ class InputManager(QObject):
|
||||
self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1)
|
||||
|
||||
# Connect signals to slots
|
||||
self.button_pressed.connect(self.handle_button_slot)
|
||||
self.button_event.connect(self.handle_button_slot)
|
||||
self.dpad_moved.connect(self.handle_dpad_slot)
|
||||
self.toggle_fullscreen.connect(self.handle_fullscreen_slot)
|
||||
|
||||
@@ -201,7 +202,9 @@ class InputManager(QObject):
|
||||
except Exception as e:
|
||||
logger.error(f"Error restoring gamepad handlers: {e}")
|
||||
|
||||
def handle_file_explorer_button(self, button_code):
|
||||
def handle_file_explorer_button(self, button_code, value):
|
||||
if value == 0: # Ignore releases
|
||||
return
|
||||
try:
|
||||
popup = QApplication.activePopupWidget()
|
||||
if isinstance(popup, QMenu):
|
||||
@@ -441,8 +444,33 @@ class InputManager(QObject):
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping rumble: {e}", exc_info=True)
|
||||
|
||||
@Slot(int)
|
||||
def handle_button_slot(self, button_code: int) -> None:
|
||||
@Slot(int, int)
|
||||
def handle_button_slot(self, button_code: int, value: int) -> None:
|
||||
active_window = QApplication.activeWindow()
|
||||
|
||||
# Обработка виртуальной клавиатуры в AddGameDialog (handle both press and release)
|
||||
if isinstance(active_window, AddGameDialog):
|
||||
focused = QApplication.focusWidget()
|
||||
if button_code in BUTTONS['confirm'] and value == 1 and isinstance(focused, QLineEdit):
|
||||
# Показываем клавиатуру при нажатии A на поле ввода (only on press)
|
||||
active_window.show_keyboard_for_widget(focused)
|
||||
return
|
||||
|
||||
# Если клавиатура видима, обрабатываем её кнопки (including release)
|
||||
if hasattr(active_window, 'keyboard') and active_window.keyboard.isVisible():
|
||||
self.handle_virtual_keyboard(button_code, value)
|
||||
return
|
||||
|
||||
# Main window keyboard handling (including release)
|
||||
keyboard = getattr(self._parent, 'keyboard', None)
|
||||
if keyboard and keyboard.isVisible():
|
||||
self.handle_virtual_keyboard(button_code, value)
|
||||
return
|
||||
|
||||
# Ignore releases for all other (non-keyboard) button handling
|
||||
if value == 0:
|
||||
return
|
||||
|
||||
if not self._gamepad_handling_enabled:
|
||||
return
|
||||
try:
|
||||
@@ -455,6 +483,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):
|
||||
@@ -551,6 +594,39 @@ 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()
|
||||
if isinstance(current_table, QTableWidget):
|
||||
current_row = current_table.currentRow()
|
||||
if current_row >= 0:
|
||||
checkbox = current_table.item(current_row, 0)
|
||||
if checkbox:
|
||||
checkbox.setCheckState(
|
||||
Qt.CheckState.Unchecked if checkbox.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked
|
||||
)
|
||||
return
|
||||
elif button_code in BUTTONS['add_game']: # X button - install
|
||||
active.install_selected(force=False)
|
||||
return
|
||||
elif button_code in BUTTONS['prev_dir']: # Y button - force install
|
||||
active.install_selected(force=True)
|
||||
return
|
||||
elif button_code in BUTTONS['back']: # B button - close dialog
|
||||
active.reject()
|
||||
return
|
||||
elif button_code in BUTTONS['prev_tab']: # LB - previous tab
|
||||
current_idx = active.tab_widget.currentIndex()
|
||||
new_idx = (current_idx - 1) % active.tab_widget.count()
|
||||
active.tab_widget.setCurrentIndex(new_idx)
|
||||
return
|
||||
elif button_code in BUTTONS['next_tab']: # RB - next tab
|
||||
current_idx = active.tab_widget.currentIndex()
|
||||
new_idx = (current_idx + 1) % active.tab_widget.count()
|
||||
active.tab_widget.setCurrentIndex(new_idx)
|
||||
return
|
||||
|
||||
# Standard navigation
|
||||
if button_code in BUTTONS['confirm']:
|
||||
self._parent.activateFocusedWidget()
|
||||
@@ -595,8 +671,83 @@ 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)
|
||||
|
||||
# Handle release early
|
||||
if value == 0:
|
||||
self.current_dpad_code = None
|
||||
self.current_dpad_value = 0
|
||||
self.axis_moving = False
|
||||
self.current_axis_delay = self.initial_axis_move_delay
|
||||
self.dpad_timer.stop()
|
||||
return
|
||||
|
||||
# Update D-pad state for continuous movement
|
||||
self.current_dpad_code = code
|
||||
self.current_dpad_value = value
|
||||
if not self.axis_moving:
|
||||
self.axis_moving = True
|
||||
self.last_move_time = current_time
|
||||
self.current_axis_delay = self.initial_axis_move_delay
|
||||
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000))
|
||||
|
||||
if keyboard and keyboard.isVisible():
|
||||
# Обработка горизонтального перемещения (LEFT/RIGHT)
|
||||
if code in (ecodes.ABS_HAT0X, ecodes.ABS_X):
|
||||
normalized_value = 0
|
||||
if code == ecodes.ABS_X: # Левый стик
|
||||
# Применяем мертвую зону
|
||||
if abs(value) < self.dead_zone:
|
||||
self.current_dpad_code = None
|
||||
self.current_dpad_value = 0
|
||||
self.axis_moving = False
|
||||
self.dpad_timer.stop()
|
||||
return
|
||||
normalized_value = 1 if value > self.dead_zone else -1
|
||||
else: # D-pad
|
||||
normalized_value = value # D-pad уже дает -1, 0, 1
|
||||
|
||||
if normalized_value != 0:
|
||||
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):
|
||||
normalized_value = 0
|
||||
if code == ecodes.ABS_Y: # Левый стик
|
||||
# Применяем мертвую зону
|
||||
if abs(value) < self.dead_zone:
|
||||
self.current_dpad_code = None
|
||||
self.current_dpad_value = 0
|
||||
self.axis_moving = False
|
||||
self.dpad_timer.stop()
|
||||
return
|
||||
normalized_value = 1 if value > self.dead_zone else -1
|
||||
else: # D-pad
|
||||
normalized_value = value # D-pad уже дает -1, 0, 1
|
||||
|
||||
if normalized_value != 0:
|
||||
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:
|
||||
logger.error("gamesListWidget not available yet, skipping D-pad navigation")
|
||||
return
|
||||
try:
|
||||
|
||||
app = QApplication.instance()
|
||||
@@ -606,22 +757,31 @@ class InputManager(QObject):
|
||||
if not app or not active:
|
||||
return
|
||||
|
||||
# Update D-pad state
|
||||
if value != 0:
|
||||
self.current_dpad_code = code
|
||||
self.current_dpad_value = value
|
||||
if not self.axis_moving:
|
||||
self.axis_moving = True
|
||||
self.last_move_time = current_time
|
||||
self.current_axis_delay = self.initial_axis_move_delay
|
||||
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
|
||||
else:
|
||||
self.current_dpad_code = None
|
||||
self.current_dpad_value = 0
|
||||
self.axis_moving = False
|
||||
self.current_axis_delay = self.initial_axis_move_delay
|
||||
self.dpad_timer.stop() # Stop timer when D-pad is released
|
||||
return
|
||||
# Новый код: обработка перехода на поле поиска
|
||||
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
|
||||
|
||||
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
||||
@@ -638,7 +798,7 @@ class InputManager(QObject):
|
||||
elif value < 0: # Left
|
||||
active.focusPreviousChild()
|
||||
return
|
||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
|
||||
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)
|
||||
@@ -691,6 +851,53 @@ class InputManager(QObject):
|
||||
active.show_next()
|
||||
return
|
||||
|
||||
|
||||
# Table navigation
|
||||
if isinstance(focused, QTableWidget):
|
||||
row_count = focused.rowCount()
|
||||
if row_count <= 0:
|
||||
return
|
||||
current_row = focused.currentRow()
|
||||
if current_row < 0:
|
||||
current_row = 0
|
||||
focused.setCurrentCell(0, 0)
|
||||
|
||||
if code == ecodes.ABS_HAT0Y and value != 0:
|
||||
# Vertical navigation
|
||||
if value > 0: # Down
|
||||
new_row = min(current_row + 1, row_count - 1)
|
||||
elif value < 0: # Up
|
||||
new_row = max(current_row - 1, 0)
|
||||
else:
|
||||
return
|
||||
|
||||
focused.setCurrentCell(new_row, focused.currentColumn())
|
||||
item = focused.item(new_row, focused.currentColumn())
|
||||
if item:
|
||||
focused.scrollToItem(
|
||||
item,
|
||||
QAbstractItemView.ScrollHint.PositionAtCenter
|
||||
)
|
||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
elif code == ecodes.ABS_HAT0X and value != 0:
|
||||
# Horizontal navigation
|
||||
col_count = focused.columnCount()
|
||||
current_col = focused.currentColumn()
|
||||
if current_col < 0:
|
||||
current_col = 0
|
||||
|
||||
if value < 0: # Left
|
||||
new_col = max(current_col - 1, 0)
|
||||
elif value > 0: # Right
|
||||
new_col = min(current_col + 1, col_count - 1)
|
||||
else:
|
||||
return
|
||||
|
||||
focused.setCurrentCell(focused.currentRow(), new_col)
|
||||
focused.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
|
||||
# Library tab navigation (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||
focused = QApplication.focusWidget()
|
||||
@@ -836,6 +1043,52 @@ class InputManager(QObject):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_dpad_slot: {e}", exc_info=True)
|
||||
|
||||
def handle_virtual_keyboard(self, button_code: int, value: 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 - подтверждение
|
||||
if value == 1:
|
||||
keyboard.activateFocusedKey()
|
||||
elif button_code in BUTTONS['back']: # Кнопка B/Circle - скрыть клавиатуру
|
||||
if value == 1:
|
||||
keyboard.hide()
|
||||
# Возвращаем фокус на поле ввода
|
||||
if keyboard.current_input_widget:
|
||||
keyboard.current_input_widget.setFocus()
|
||||
elif button_code in BUTTONS['prev_tab']: # LB/L1 - переключение раскладки
|
||||
if value == 1:
|
||||
keyboard.on_lang_click()
|
||||
elif button_code in BUTTONS['next_tab']: # RB/R1 - переключение Shift
|
||||
if value == 1:
|
||||
keyboard.on_shift_click(not keyboard.shift_pressed)
|
||||
elif button_code in BUTTONS['context_menu']: # Кнопка Start - подтверждение
|
||||
if value == 1:
|
||||
keyboard.activateFocusedKey()
|
||||
elif button_code in BUTTONS['menu']: # Кнопка Select - скрыть клавиатуру
|
||||
if value == 1:
|
||||
keyboard.hide()
|
||||
# Возвращаем фокус на поле ввода
|
||||
if keyboard.current_input_widget:
|
||||
keyboard.current_input_widget.setFocus()
|
||||
elif button_code in BUTTONS['add_game']: # Кнопка X - Backspace (now holdable)
|
||||
if value == 1:
|
||||
keyboard.on_backspace_pressed()
|
||||
elif value == 0:
|
||||
keyboard.stop_backspace_repeat()
|
||||
|
||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
@@ -1083,8 +1336,8 @@ class InputManager(QObject):
|
||||
self.gamepad = None
|
||||
if self.gamepad_thread:
|
||||
self.gamepad_thread.join()
|
||||
# Signal to exit fullscreen mode
|
||||
self.toggle_fullscreen.emit(False)
|
||||
if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
|
||||
self.toggle_fullscreen.emit(False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling udev event: {e}", exc_info=True)
|
||||
|
||||
@@ -1142,11 +1395,12 @@ class InputManager(QObject):
|
||||
if not app or not active:
|
||||
continue
|
||||
|
||||
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||
if event.type == ecodes.EV_KEY:
|
||||
# Emit on both press (1) and release (0)
|
||||
self.button_event.emit(event.code, event.value)
|
||||
# Special handling for menu on press only
|
||||
if event.value == 1 and event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
else:
|
||||
self.button_pressed.emit(event.code)
|
||||
elif event.type == ecodes.EV_ABS:
|
||||
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
|
||||
# Проверяем, достаточно ли времени прошло с последнего срабатывания
|
||||
@@ -1155,17 +1409,19 @@ class InputManager(QObject):
|
||||
if event.code == ecodes.ABS_Z: # LT/L2
|
||||
if event.value > 128 and not self.lt_pressed:
|
||||
self.lt_pressed = True
|
||||
self.button_pressed.emit(event.code)
|
||||
self.button_event.emit(event.code, 1) # Emit as press
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.lt_pressed:
|
||||
self.lt_pressed = False
|
||||
self.button_event.emit(event.code, 0) # Emit as release
|
||||
elif event.code == ecodes.ABS_RZ: # RT/R2
|
||||
if event.value > 128 and not self.rt_pressed:
|
||||
self.rt_pressed = True
|
||||
self.button_pressed.emit(event.code)
|
||||
self.button_event.emit(event.code, 1) # Emit as press
|
||||
self.last_trigger_time = now
|
||||
elif event.value <= 128 and self.rt_pressed:
|
||||
self.rt_pressed = False
|
||||
self.button_event.emit(event.code, 0) # Emit as release
|
||||
else:
|
||||
self.dpad_moved.emit(event.code, event.value, now)
|
||||
except OSError as e:
|
||||
|
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', ';', ':', '_']
|
||||
]
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -191,6 +191,10 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -304,6 +308,45 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -370,6 +413,9 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -382,13 +428,95 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -191,6 +191,10 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -304,6 +308,45 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -370,6 +413,9 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -382,13 +428,95 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -189,6 +189,10 @@ msgstr ""
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr ""
|
||||
|
||||
@@ -302,6 +306,45 @@ msgstr ""
|
||||
msgid "No cover selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set"
|
||||
msgstr ""
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -368,6 +411,9 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -380,13 +426,95 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr ""
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgid "Compatibility tool:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
|
@@ -9,18 +9,17 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||
"PO-Revision-Date: 2025-09-23 22:23+0500\n"
|
||||
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
|
||||
"PO-Revision-Date: 2025-10-09 16:37+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
|
||||
"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
"X-Generator: Poedit 3.6\n"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
@@ -87,11 +86,11 @@ msgstr "Успешно"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"'{game_name}' was added to Steam. Please restart Steam for changes to take "
|
||||
"effect."
|
||||
"'{game_name}' was added to Steam. Please restart Steam for changes to "
|
||||
"take effect."
|
||||
msgstr ""
|
||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, "
|
||||
"чтобы изменения вступили в силу."
|
||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите "
|
||||
"Steam, чтобы изменения вступили в силу."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found for game: {game_name}"
|
||||
@@ -179,11 +178,11 @@ msgstr "Подтвердите удаление"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Are you sure you want to delete '{game_name}'? This will remove the .desktop "
|
||||
"file and custom data."
|
||||
"Are you sure you want to delete '{game_name}'? This will remove the "
|
||||
".desktop file and custom data."
|
||||
msgstr ""
|
||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ."
|
||||
"desktop и пользовательских данных."
|
||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению "
|
||||
"файла .desktop и пользовательских данных."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete .desktop file: {error}"
|
||||
@@ -197,6 +196,10 @@ msgstr "'{game_name}' был(а) успешно удалён(а)"
|
||||
msgid "Failed to delete custom data: {error}"
|
||||
msgstr "Не удалось удалить пользовательские данные: {error}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{game_name}' successfully"
|
||||
msgstr "'{game_name}' успешно добавлен(а)"
|
||||
|
||||
msgid "Game name and executable path are required"
|
||||
msgstr "Требуются название игры и путь к исполняемому файлу"
|
||||
|
||||
@@ -225,11 +228,11 @@ msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to take "
|
||||
"effect."
|
||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to"
|
||||
" take effect."
|
||||
msgstr ""
|
||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы "
|
||||
"изменения вступили в силу."
|
||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam,"
|
||||
" чтобы изменения вступили в силу."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
||||
@@ -274,7 +277,7 @@ msgstr "Путь: "
|
||||
|
||||
#, python-format
|
||||
msgid "Access denied: %s"
|
||||
msgstr "Доступ запрещен: %s"
|
||||
msgstr "Доступ запрещён: %s"
|
||||
|
||||
msgid "Edit Game"
|
||||
msgstr "Редактировать игру"
|
||||
@@ -312,6 +315,45 @@ msgstr "Скачивание обложки..."
|
||||
msgid "No cover selected"
|
||||
msgstr "Обложка не выбрана"
|
||||
|
||||
msgid "Prefix Manager"
|
||||
msgstr "Менеджер префиксов"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Выбор"
|
||||
|
||||
msgid "Libraries"
|
||||
msgstr "Библиотеки"
|
||||
|
||||
msgid "Information"
|
||||
msgstr "Описание"
|
||||
|
||||
msgid "Fonts"
|
||||
msgstr "Шрифты"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr "Принудительно установить"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr "Winetricks не найден. Повторите попытку."
|
||||
|
||||
msgid "Warning"
|
||||
msgstr "Предупреждение"
|
||||
|
||||
msgid "No components selected."
|
||||
msgstr "Не выбрано ни одного компонента."
|
||||
|
||||
msgid "Installation failed. Check logs."
|
||||
msgstr "Установка не удалась. Проверьте журналы."
|
||||
|
||||
msgid "Components installed successfully."
|
||||
msgstr "Компоненты успешно установлены."
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr "Загрузка игр из Epic Games Store..."
|
||||
|
||||
@@ -378,6 +420,9 @@ msgstr "Назад"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Полный экран"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr "Загрузка игр из Steam..."
|
||||
|
||||
@@ -390,14 +435,96 @@ msgstr "Игровая библиотека"
|
||||
msgid "Find Games ..."
|
||||
msgstr "Найти игры..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr "'{name}' добавлен(а)"
|
||||
|
||||
msgid "Here you can configure automatic game installation..."
|
||||
msgstr "Здесь можно настроить автоматическую установку игр..."
|
||||
|
||||
msgid "List of available emulators and their configuration..."
|
||||
msgstr "Список доступных эмуляторов и их настройка..."
|
||||
|
||||
msgid "Various Wine parameters and versions..."
|
||||
msgstr "Различные параметры и версии wine..."
|
||||
msgid "Compatibility tool:"
|
||||
msgstr "Инструмент совместимости:"
|
||||
|
||||
msgid "Prefix:"
|
||||
msgstr "Префикс:"
|
||||
|
||||
msgid "Wine Configuration"
|
||||
msgstr "Конфигурация Wine"
|
||||
|
||||
msgid "Registry Editor"
|
||||
msgstr "Редактор реестра"
|
||||
|
||||
msgid "Control Panel"
|
||||
msgstr "Панель управления"
|
||||
|
||||
msgid "Task Manager"
|
||||
msgstr "Диспетчер задач"
|
||||
|
||||
msgid "Command Prompt"
|
||||
msgstr "Командная строка"
|
||||
|
||||
msgid "Uninstaller"
|
||||
msgstr "Удаление программ"
|
||||
|
||||
msgid "Create Prefix Backup"
|
||||
msgstr "Создать резервную копию префикса"
|
||||
|
||||
msgid "Load Prefix Backup"
|
||||
msgstr "Загрузить резервную копию префикса"
|
||||
|
||||
msgid "Delete Compatibility Tool"
|
||||
msgstr "Удалить Инструмент совместимости"
|
||||
|
||||
msgid "Delete Prefix"
|
||||
msgstr "Удалить Префикс"
|
||||
|
||||
msgid "Clear Prefix"
|
||||
msgstr "Очистить Префикс"
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr "Не удалось запустить процесс резервного копирования."
|
||||
|
||||
msgid "Failed to start restore process."
|
||||
msgstr "Не удалось запустить процесс восстановления."
|
||||
|
||||
msgid "Prefix backup completed."
|
||||
msgstr "Резервное копирование префикса завершено."
|
||||
|
||||
msgid "Prefix backup failed."
|
||||
msgstr "Сбой резервного копирования префикса."
|
||||
|
||||
msgid "Prefix restore completed."
|
||||
msgstr "Восстановление префикса завершено."
|
||||
|
||||
msgid "Prefix restore failed."
|
||||
msgstr "Восстановление префикса не удалось."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete prefix '{}'?"
|
||||
msgstr "Вы уверены, что хотите удалить префикс «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' deleted."
|
||||
msgstr "Префикс «{}» удален."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete prefix: {}"
|
||||
msgstr "Не удалось удалить префикс: {}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
||||
msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Compatibility tool '{}' deleted."
|
||||
msgstr "Инструмент совместимости «{}» удален."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to delete compatibility tool: {}"
|
||||
msgstr "Не удалось удалить инструмент совместимости: {}"
|
||||
|
||||
msgid "Main PortProton parameters..."
|
||||
msgstr "Основные параметры PortProton..."
|
||||
@@ -482,7 +609,8 @@ msgstr "Подтвердите удаление"
|
||||
|
||||
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
||||
msgstr ""
|
||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить."
|
||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя "
|
||||
"отменить."
|
||||
|
||||
msgid "Settings reset. Restarting..."
|
||||
msgstr "Настройки сброшены. Перезапуск..."
|
||||
@@ -654,3 +782,4 @@ msgstr "Нет избранных"
|
||||
|
||||
msgid "No recent games"
|
||||
msgstr "Нет недавних игр"
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import sys
|
||||
import psutil
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
||||
@@ -35,11 +35,11 @@ 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)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||
from typing import cast
|
||||
from collections.abc import Callable
|
||||
@@ -145,7 +145,7 @@ class MainWindow(QMainWindow):
|
||||
headerLayout.addStretch()
|
||||
|
||||
self.input_manager = InputManager(self) # type: ignore
|
||||
self.input_manager.button_pressed.connect(self.updateControlHints)
|
||||
self.input_manager.button_event.connect(self.updateControlHints)
|
||||
self.input_manager.dpad_moved.connect(self.updateControlHints)
|
||||
|
||||
# 2. НАВИГАЦИЯ (КНОПКИ ВКЛАДОК)
|
||||
@@ -206,8 +206,12 @@ class MainWindow(QMainWindow):
|
||||
self.controlHintsWidget = self.createControlHintsWidget()
|
||||
mainLayout.addWidget(self.controlHintsWidget)
|
||||
|
||||
self.updateControlHints()
|
||||
|
||||
self.restore_state()
|
||||
|
||||
self.keyboard = VirtualKeyboard(self, self.theme)
|
||||
|
||||
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
@@ -248,6 +252,10 @@ class MainWindow(QMainWindow):
|
||||
GamepadType.XBOX: "xbox_view",
|
||||
GamepadType.PLAYSTATION: "ps_share",
|
||||
},
|
||||
'search': {
|
||||
GamepadType.XBOX: "xbox_y",
|
||||
GamepadType.PLAYSTATION: "ps_square",
|
||||
},
|
||||
}
|
||||
return mappings.get(action, {}).get(gtype, "placeholder")
|
||||
|
||||
@@ -286,6 +294,7 @@ class MainWindow(QMainWindow):
|
||||
("add_game", _("Add Game")),
|
||||
("context_menu", _("Menu")),
|
||||
("menu", _("Fullscreen")),
|
||||
("search", _("Search")),
|
||||
]
|
||||
|
||||
keyboard_hints = [
|
||||
@@ -301,12 +310,12 @@ class MainWindow(QMainWindow):
|
||||
def makeHint(icon_name: str, action_text: str, is_gamepad: bool, action: str | None = None,):
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setContentsMargins(0, 5, 0, 0)
|
||||
layout.setSpacing(6)
|
||||
|
||||
# иконка кнопки
|
||||
icon_label = QLabel()
|
||||
icon_label.setFixedSize(32, 32)
|
||||
icon_label.setFixedSize(26, 26)
|
||||
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
pixmap = QPixmap()
|
||||
@@ -319,7 +328,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if not pixmap.isNull():
|
||||
icon_label.setPixmap(pixmap.scaled(
|
||||
32, 32,
|
||||
26, 26,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
))
|
||||
@@ -398,7 +407,7 @@ class MainWindow(QMainWindow):
|
||||
gtype = self.input_manager.gamepad_type
|
||||
logger.debug("Updating control hints, gamepad connected: %s, type: %s", is_gamepad_connected, gtype.value)
|
||||
|
||||
gamepad_actions = ['confirm', 'back', 'add_game', 'context_menu', 'menu']
|
||||
gamepad_actions = ['confirm', 'back', 'add_game', 'context_menu', 'menu', 'search']
|
||||
|
||||
for container, icon_label, action in self.hintsLabels:
|
||||
if action in gamepad_actions: # Gamepad hint
|
||||
@@ -412,7 +421,7 @@ class MainWindow(QMainWindow):
|
||||
pixmap.load(str(icon_path))
|
||||
if not pixmap.isNull():
|
||||
icon_label.setPixmap(pixmap.scaled(
|
||||
32, 32,
|
||||
26, 26,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
))
|
||||
@@ -421,7 +430,7 @@ class MainWindow(QMainWindow):
|
||||
placeholder = self.theme_manager.get_theme_image("placeholder", self.current_theme_name)
|
||||
if placeholder:
|
||||
pixmap.load(str(placeholder))
|
||||
icon_label.setPixmap(pixmap.scaled(32, 32, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||
else:
|
||||
container.setVisible(False)
|
||||
else: # Keyboard hint
|
||||
@@ -747,17 +756,29 @@ class MainWindow(QMainWindow):
|
||||
self.searchDebounceTimer = QTimer(self)
|
||||
self.searchDebounceTimer.setSingleShot(True)
|
||||
self.searchDebounceTimer.setInterval(300)
|
||||
self.searchDebounceTimer.timeout.connect(self.game_library_manager.filter_games_delayed)
|
||||
self.searchDebounceTimer.timeout.connect(self.on_search_changed)
|
||||
|
||||
layout.addWidget(self.searchEdit)
|
||||
return self.container, self.searchEdit
|
||||
|
||||
def on_search_text_changed(self, text: str):
|
||||
"""Search text change handler with debounce."""
|
||||
self.searchDebounceTimer.stop()
|
||||
self.searchDebounceTimer.start()
|
||||
|
||||
@Slot()
|
||||
def on_search_changed(self):
|
||||
"""Triggers filtering with delay."""
|
||||
if hasattr(self, 'game_library_manager'):
|
||||
self.game_library_manager.filter_games_delayed()
|
||||
|
||||
def startSearchDebounce(self, text):
|
||||
self.searchDebounceTimer.start()
|
||||
|
||||
def createInstalledTab(self):
|
||||
self.gamesLibraryWidget = self.game_library_manager.create_games_library_widget()
|
||||
self.stackedWidget.addWidget(self.gamesLibraryWidget)
|
||||
self.gamesListWidget = self.game_library_manager.gamesListWidget
|
||||
self.game_library_manager.update_game_grid()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
@@ -935,7 +956,6 @@ class MainWindow(QMainWindow):
|
||||
# Trigger visible images load
|
||||
QTimer.singleShot(200, self.game_library_manager.load_visible_images)
|
||||
|
||||
self.update_status_message.emit(_("Enriching from Steam..."), 3000)
|
||||
from portprotonqt.steam_api import get_steam_game_info_async
|
||||
get_steam_game_info_async(final_name, exec_line, on_steam_info)
|
||||
|
||||
@@ -994,14 +1014,245 @@ class MainWindow(QMainWindow):
|
||||
self.wineTitle.setObjectName("tabTitle")
|
||||
layout.addWidget(self.wineTitle)
|
||||
|
||||
self.wineContent = QLabel(_("Various Wine parameters and versions..."))
|
||||
self.wineContent.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
self.wineContent.setObjectName("tabContent")
|
||||
layout.addWidget(self.wineContent)
|
||||
if self.portproton_location is None:
|
||||
return
|
||||
|
||||
dist_path = os.path.join(self.portproton_location, "data", "dist")
|
||||
prefixes_path = os.path.join(self.portproton_location, "data", "prefixes")
|
||||
|
||||
if not os.path.exists(dist_path):
|
||||
return
|
||||
|
||||
formLayout = QFormLayout()
|
||||
formLayout.setContentsMargins(0, 10, 0, 0)
|
||||
formLayout.setSpacing(10)
|
||||
formLayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
self.wine_versions = [d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))]
|
||||
self.wineCombo = QComboBox()
|
||||
self.wineCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.wineCombo.addItems(self.wine_versions)
|
||||
self.wineCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||
self.wineCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.wineTitleLabel = QLabel(_("Compatibility tool:"))
|
||||
self.wineTitleLabel.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.wineTitleLabel.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
if self.wine_versions:
|
||||
self.wineCombo.setCurrentIndex(0)
|
||||
formLayout.addRow(self.wineTitleLabel, self.wineCombo)
|
||||
|
||||
self.prefixes = [d for d in os.listdir(prefixes_path) if os.path.isdir(os.path.join(prefixes_path, d))] if os.path.exists(prefixes_path) else []
|
||||
self.prefixCombo = QComboBox()
|
||||
self.prefixCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.prefixCombo.addItems(self.prefixes)
|
||||
self.prefixCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||
self.prefixCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.prefixTitleLabel = QLabel(_("Prefix:"))
|
||||
self.prefixTitleLabel.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.prefixTitleLabel.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
if self.prefixes:
|
||||
self.prefixCombo.setCurrentIndex(0)
|
||||
formLayout.addRow(self.prefixTitleLabel, self.prefixCombo)
|
||||
|
||||
layout.addLayout(formLayout)
|
||||
|
||||
# --- Wine Tools ---
|
||||
tools_grid = QGridLayout()
|
||||
tools_grid.setSpacing(6)
|
||||
|
||||
tools = [
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("control", _("Control Panel")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("explorer", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
]
|
||||
|
||||
for i, (_tool_cmd, tool_name) in enumerate(tools):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = AutoSizeButton(tool_name, update_size=False)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
tools_grid.addWidget(btn, row, col)
|
||||
|
||||
for col in range(3):
|
||||
tools_grid.setColumnStretch(col, 1)
|
||||
|
||||
layout.addLayout(tools_grid)
|
||||
|
||||
# --- Additional Tools ---
|
||||
additional_grid = QGridLayout()
|
||||
additional_grid.setSpacing(6)
|
||||
|
||||
additional_buttons = [
|
||||
("Winetricks", self.open_winetricks),
|
||||
(_("Create Prefix Backup"), self.create_prefix_backup),
|
||||
(_("Load Prefix Backup"), self.load_prefix_backup),
|
||||
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
||||
(_("Delete Prefix"), self.delete_prefix),
|
||||
(_("Clear Prefix"), None),
|
||||
]
|
||||
|
||||
for i, (text, callback) in enumerate(additional_buttons):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = AutoSizeButton(text, update_size=False)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
if callback:
|
||||
btn.clicked.connect(callback)
|
||||
additional_grid.addWidget(btn, row, col)
|
||||
|
||||
for col in range(3):
|
||||
additional_grid.setColumnStretch(col, 1)
|
||||
|
||||
layout.addLayout(additional_grid)
|
||||
tools_grid.setContentsMargins(10, 4, 10, 0)
|
||||
additional_grid.setContentsMargins(10, 6, 10, 0)
|
||||
layout.addStretch(1)
|
||||
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
|
||||
def create_prefix_backup(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
return
|
||||
file_explorer = FileExplorer(self, directory_only=True)
|
||||
file_explorer.file_signal.file_selected.connect(lambda path: self._perform_backup(path, selected_prefix))
|
||||
file_explorer.exec()
|
||||
|
||||
def _perform_backup(self, backup_dir, prefix_name):
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
self.backup_process = QProcess(self)
|
||||
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
||||
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir]
|
||||
self.backup_process.start(cmd[0], cmd[1:])
|
||||
if not self.backup_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
||||
|
||||
def load_prefix_backup(self):
|
||||
file_explorer = FileExplorer(self, file_filter='.ppack')
|
||||
file_explorer.file_signal.file_selected.connect(self._perform_restore)
|
||||
file_explorer.exec()
|
||||
|
||||
def _perform_restore(self, file_path):
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
self.restore_process = QProcess(self)
|
||||
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
||||
cmd = [start_sh, "--restore-prefix", file_path]
|
||||
self.restore_process.start(cmd[0], cmd[1:])
|
||||
if not self.restore_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
||||
|
||||
def _on_backup_finished(self, exitCode):
|
||||
if exitCode == 0:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix backup completed."))
|
||||
else:
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix backup failed."))
|
||||
|
||||
def _on_restore_finished(self, exitCode):
|
||||
if exitCode == 0:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix restore completed."))
|
||||
else:
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix restore failed."))
|
||||
|
||||
def delete_prefix(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not self.portproton_location:
|
||||
return
|
||||
|
||||
if not selected_prefix:
|
||||
return
|
||||
|
||||
prefix_path = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
if not os.path.exists(prefix_path):
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
_("Confirm Deletion"),
|
||||
_("Are you sure you want to delete prefix '{}'?").format(selected_prefix),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
shutil.rmtree(prefix_path)
|
||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' deleted.").format(selected_prefix))
|
||||
# обновляем список
|
||||
self.prefixCombo.clear()
|
||||
self.prefixes = [d for d in os.listdir(os.path.join(self.portproton_location, "data", "prefixes"))
|
||||
if os.path.isdir(os.path.join(self.portproton_location, "data", "prefixes", d))]
|
||||
self.prefixCombo.addItems(self.prefixes)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete prefix: {}").format(str(e)))
|
||||
|
||||
def delete_compat_tool(self):
|
||||
"""Удаляет выбранный Wine/Proton дистрибутив из каталога dist."""
|
||||
if not self.portproton_location:
|
||||
return
|
||||
|
||||
selected_tool = self.wineCombo.currentText()
|
||||
if not selected_tool:
|
||||
return
|
||||
|
||||
tool_path = os.path.join(self.portproton_location, "data", "dist", selected_tool)
|
||||
if not os.path.exists(tool_path):
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
_("Confirm Deletion"),
|
||||
_("Are you sure you want to delete compatibility tool '{}'?").format(selected_tool),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
shutil.rmtree(tool_path)
|
||||
QMessageBox.information(self, _("Success"), _("Compatibility tool '{}' deleted.").format(selected_tool))
|
||||
# обновляем список
|
||||
self.wineCombo.clear()
|
||||
self.wine_versions = [d for d in os.listdir(os.path.join(self.portproton_location, "data", "dist"))
|
||||
if os.path.isdir(os.path.join(self.portproton_location, "data", "dist", d))]
|
||||
self.wineCombo.addItems(self.wine_versions)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to delete compatibility tool: {}").format(str(e)))
|
||||
|
||||
def open_winetricks(self):
|
||||
"""Open the Winetricks dialog for the selected prefix and wine."""
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
if not selected_prefix:
|
||||
return
|
||||
|
||||
selected_wine = self.wineCombo.currentText()
|
||||
if not selected_wine:
|
||||
return
|
||||
|
||||
assert self.portproton_location is not None
|
||||
prefix_path = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
wine_path = os.path.join(self.portproton_location, "data", "dist", selected_wine, "bin", "wine")
|
||||
|
||||
# Open Winetricks dialog
|
||||
dialog = WinetricksDialog(self, self.theme, prefix_path, wine_path)
|
||||
dialog.exec()
|
||||
|
||||
def createPortProtonTab(self):
|
||||
"""Вкладка 'PortProton Settings'."""
|
||||
self.portProtonWidget = QWidget()
|
||||
@@ -1824,10 +2075,19 @@ class MainWindow(QMainWindow):
|
||||
completionist_time = hltb.format_game_time(game, "completionist")
|
||||
|
||||
# Очищаем layout перед добавлением новых элементов
|
||||
while hltbLayout.count():
|
||||
child = hltbLayout.takeAt(0)
|
||||
if child.widget():
|
||||
child.widget().deleteLater()
|
||||
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)
|
||||
|
||||
|
||||
|
||||
has_data = False
|
||||
|
||||
@@ -2295,7 +2555,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 and self.checkProcessTimer.isActive():
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
|
49
portprotonqt/preloader.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import time
|
||||
|
||||
from PySide6.QtCore import QRect
|
||||
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
class Preloader(QWidget):
|
||||
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(150, 150)
|
||||
self._speed = speed
|
||||
self._line_width = line_line_width
|
||||
self._color1 = color
|
||||
self._color2 = QColor(color.red(), color.green(), color.blue(), 0)
|
||||
self._start_time = time.time()
|
||||
|
||||
def showEvent(self, event):
|
||||
self._start_time = time.time()
|
||||
|
||||
def paintEvent(self, event):
|
||||
rect = self._get_preloader_rect()
|
||||
center = rect.center()
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.setPen(self._get_pen())
|
||||
painter.translate(center)
|
||||
painter.rotate(self._get_angle())
|
||||
painter.translate(-center)
|
||||
painter.drawArc(rect, 0, 270 * 16)
|
||||
self.update()
|
||||
|
||||
def _get_pen(self) -> QPen:
|
||||
gradient = QConicalGradient()
|
||||
gradient.setCenter(self.rect().center())
|
||||
gradient.setColorAt(0, self._color1)
|
||||
gradient.setColorAt(1, self._color2)
|
||||
pen = QPen(QBrush(gradient), self._line_width)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
return pen
|
||||
|
||||
def _get_angle(self) -> float:
|
||||
duration = time.time() - self._start_time
|
||||
return (self._speed * duration) % 360.0
|
||||
|
||||
def _get_preloader_rect(self) -> QRect:
|
||||
size = self._line_width // 2
|
||||
rect = self.rect()
|
||||
rect.adjust(size, size, -size, -size)
|
||||
return rect
|
@@ -211,14 +211,28 @@ def normalize_name(s):
|
||||
|
||||
def is_valid_candidate(candidate):
|
||||
"""
|
||||
Checks if a candidate contains forbidden substrings:
|
||||
- win32
|
||||
- win64
|
||||
- gamelauncher
|
||||
Additionally checks the string without spaces.
|
||||
Returns True if the candidate is valid, otherwise False.
|
||||
Determines whether a given candidate string is valid for use as a game name.
|
||||
|
||||
The function performs the following checks:
|
||||
1. Normalizes the candidate using `normalize_name()`.
|
||||
2. Rejects the candidate if the normalized name is exactly "game"
|
||||
(to avoid overly generic names).
|
||||
3. Removes spaces and checks for forbidden substrings:
|
||||
- "win32"
|
||||
- "win64"
|
||||
- "gamelauncher"
|
||||
These are checked in the space-free version of the string.
|
||||
4. Returns True only if none of the forbidden conditions are met.
|
||||
|
||||
Args:
|
||||
candidate (str): The candidate string to validate.
|
||||
|
||||
Returns:
|
||||
bool: True if the candidate is valid, False otherwise.
|
||||
"""
|
||||
normalized_candidate = normalize_name(candidate)
|
||||
if normalized_candidate == "game":
|
||||
return False
|
||||
normalized_no_space = normalized_candidate.replace(" ", "")
|
||||
forbidden = ["win32", "win64", "gamelauncher"]
|
||||
for token in forbidden:
|
||||
|
Before Width: | Height: | Size: 880 B |
1
portprotonqt/themes/standart/images/key_backspace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g><rect x="1" y="6" width="46" height="36" rx="5" ry="5" fill="#3f424d" stroke-width="1.1506"/><rect x="4.2329" y="8.5301" width="39.534" height="30.94" rx="4.2972" ry="4.2972" fill="#fff" stroke-width=".98888"/><path d="m23.24 22.785c-0.67917 0.69059-0.67818 1.807 0 2.4913l8.0309 8.1037c1.8756 1.8787 4.6892-0.93962 2.8136-2.8183l-3.5038-3.5097c-0.58434-0.58533-0.39618-1.0598 0.44066-1.0598h9.6139c1.0992 0 1.9895-0.89179 1.9895-1.9928 0-1.1005-0.89028-1.9928-1.9895-1.9928h-9.6139c-0.82771 0-1.0277-0.47176-0.44066-1.0597l3.5038-3.5093c1.8756-1.8787-0.93803-4.6971-2.8136-2.8183z" fill="#3f424d" fill-rule="evenodd"/></g></svg>
|
After Width: | Height: | Size: 751 B |
Before Width: | Height: | Size: 2.0 KiB |
48
portprotonqt/themes/standart/images/key_context.svg
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
version="1.1"
|
||||
viewBox="0 0 48 48"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
sodipodi:docname="key_context.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs2" /><sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="8.6915209"
|
||||
inkscape:cx="72.311855"
|
||||
inkscape:cy="22.780823"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1406"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" /><path
|
||||
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.554217;enable-background:accumulate;stop-color:#000000"
|
||||
d="m 17.400964,38.281601 -0.04068,-15.381724 c -0.0087,-3.288656 2.401967,-6.020242 5.542168,-6.550475 V 7.4098472 C 11.174091,7.9874382 1.8422139,17.678792 1.8422139,29.550445 v 8.911269 c 3.429133,2.844892 11.5678151,2.890776 15.5587501,-0.180113 z"
|
||||
id="path10"
|
||||
sodipodi:nodetypes="csccscc" /><path
|
||||
fill="#000000"
|
||||
d="m 23.956256,40.5905 h -9e-6 c -2.438553,0 -4.433731,-1.995178 -4.433731,-4.43373 V 25.072424 c 0,-2.438552 1.995178,-4.433731 4.433731,-4.433731 h 9e-6 c 2.438552,0 4.43373,1.995179 4.43373,4.433731 V 36.15677 c 0,2.438552 -1.995178,4.43373 -4.43373,4.43373 z"
|
||||
id="path2"
|
||||
style="fill:#686e7e;fill-opacity:1;stroke-width:0.554217" /><g
|
||||
id="g15"
|
||||
transform="matrix(0.97480136,0,0,0.99852328,1.4840752,1.6593149)"><path
|
||||
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000"
|
||||
d="m 30.231637,35.990171 0.03878,-14.663865 c 0.0083,-3.135176 -2.289868,-5.73928 -5.283518,-6.244767 V 6.5591888 C 36.167905,7.1098239 45.209208,16.349815 45.064267,27.666494 l -0.109685,8.563937 c -3.269097,2.712122 -10.918265,2.687312 -14.722945,-0.24026 z"
|
||||
id="path14" /><path
|
||||
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000"
|
||||
d="m 24.224126,5.7586892 v 9.9671448 l 0.634933,0.107994 c 2.632815,0.444559 4.656653,2.729598 4.649348,5.490959 l -0.04096,15.03916 0.299778,0.230885 c 2.097287,1.613791 5.093143,2.357986 8.017658,2.392636 2.924514,0.03465 5.796042,-0.625772 7.656435,-2.169199 l 0.271848,-0.2253 0.113581,-8.91699 C 45.976953,15.94787 36.604257,6.3680498 25.024774,5.7977906 Z m 1.524956,1.6795 C 36.150995,8.3658717 44.437912,17.028984 44.301786,27.65736 l -0.104271,8.114479 c -1.445908,1.069255 -3.851487,1.720797 -6.394017,1.690673 -2.543438,-0.03013 -5.090881,-0.734663 -6.807375,-1.934591 l 0.03724,-14.199409 c 0.0087,-3.271088 -2.263607,-5.953645 -5.284281,-6.771998 z"
|
||||
id="path15" /></g></svg>
|
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 874 B |
1
portprotonqt/themes/standart/images/key_e.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m17.977 16.26h11.807v2.6476h-8.086v3.554h7.2989v2.6476h-7.2989v3.9834h8.3245v2.6476h-12.046z" fill="#3f424d" stroke-width=".4977" aria-label="E"/></svg>
|
After Width: | Height: | Size: 726 B |
Before Width: | Height: | Size: 1.3 KiB |
1
portprotonqt/themes/standart/images/key_enter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6 6h36c2.77 0 5 2.23 5 5v26c0 2.77-2.23 5-5 5h-36c-2.77 0-5-2.23-5-5v-26c0-2.77 2.23-5 5-5z" fill="#3f424d" stroke-width="1.1506"/><path d="m8.5301 8.5301h30.94c2.3806 0 4.2972 1.9166 4.2972 4.2972v22.346c0 2.3806-1.9166 4.2972-4.2972 4.2972h-30.94c-2.3806 0-4.2972-1.9166-4.2972-4.2972v-22.346c0-2.3806 1.9166-4.2972 4.2972-4.2972z" fill="#fff" stroke-width=".98888"/><path d="m8.2952 18.538h8.3321v1.8684h-5.7063v2.5081h5.1508v1.8684h-5.1508v2.811h5.8746v1.8684h-8.5005zm10.268 0h2.6596l5.2854 7.4568v-7.4568h2.3397v10.924h-2.6596l-5.2854-7.5747v7.5747h-2.3397zm15.166 1.8684h-3.3665v-1.8684h9.3421v1.8684h-3.3497v9.0559h-2.6259z" fill="#3f424d" stroke-width=".35123" aria-label="ENT"/></svg>
|
After Width: | Height: | Size: 823 B |
Before Width: | Height: | Size: 943 B |
1
portprotonqt/themes/standart/images/key_f11.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m11.139 18.538h8.5005v1.8684h-5.8746v2.6764h5.3191v1.8684h-5.3191v4.5111h-2.6259zm13.5 2.5754-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576zm9.7629 0-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576z" fill="#3f424d" stroke-width=".35123" aria-label="F11"/></svg>
|
After Width: | Height: | Size: 857 B |
Before Width: | Height: | Size: 933 B |
1
portprotonqt/themes/standart/images/key_left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m26.619 34a1.9874 1.9874 0 0 1-1.3812-0.55623l-7.5143-7.2497a3.0457 3.0457 0 0 1 0-4.3873l7.5143-7.2497a1.9882 1.9882 0 0 1 2.7603 2.8624l-6.8226 6.581 6.8226 6.581a1.9874 1.9874 0 0 1-1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg>
|
After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 956 B |
1
portprotonqt/themes/standart/images/key_right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m20.778 34a1.9874 1.9874 0 0 0 1.3812-0.55623l7.5143-7.2497a3.0457 3.0457 0 0 0 0-4.3873l-7.5143-7.2497a1.9882 1.9882 0 0 0-2.7603 2.8624l6.8226 6.581-6.8226 6.581a1.9874 1.9874 0 0 0 1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg>
|
After Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 1.9 KiB |
1
portprotonqt/themes/standart/images/ps_circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m24 13.476c-5.7918 0-10.524 4.7162-10.524 10.524 0 5.7918 4.7162 10.524 10.524 10.524 5.7918 0 10.524-4.7162 10.524-10.524 0-5.7918-4.7162-10.524-10.524-10.524zm0 18.037c-4.137 0-7.5128-3.3758-7.5128-7.5128s3.3758-7.5128 7.5128-7.5128 7.5128 3.3758 7.5128 7.5128-3.3592 7.5128-7.5128 7.5128z" fill="#3f424d" stroke-width="1.6548"/></svg>
|
After Width: | Height: | Size: 736 B |
Before Width: | Height: | Size: 1.7 KiB |
1
portprotonqt/themes/standart/images/ps_cross.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m34.076 13.91c-0.57906-0.57906-1.5387-0.57906-2.1177 0l-7.958 7.958-7.958-7.958c-0.57906-0.57906-1.5387-0.57906-2.1177 0-0.57906 0.57906-0.57906 1.5387 0 2.1177l7.958 7.958-7.958 7.958c-0.57906 0.57906-0.57906 1.5387 0 2.1177 0.2978 0.2978 0.67833 0.44671 1.0589 0.44671 0.38053 0 0.76106-0.1489 1.0589-0.44671l7.958-7.9415 7.958 7.958c0.2978 0.2978 0.67833 0.44671 1.0589 0.44671s0.76106-0.1489 1.0589-0.44671c0.57906-0.57906 0.57906-1.5387 0-2.1177l-7.958-7.958 7.958-7.958c0.57906-0.59561 0.57906-1.5387 0-2.1343z" fill="#3f424d" stroke-width="1.6545"/></svg>
|
After Width: | Height: | Size: 961 B |
Before Width: | Height: | Size: 1.7 KiB |
1
portprotonqt/themes/standart/images/ps_l1.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m10.465 39.437c4.1391 1.4258 20.596 4.9156 31.79 2.551 2.7034-0.57104 4.7508-3.32 4.744-6.0831l-0.057386-23.467c-0.009676-3.9677-4.6895-7.2319-7.5124-7.2255-12.075 0.0276-22.278-0.0068827-33.557 1.5493-2.7371 0.37765-4.8753 4.0033-4.8727 6.7663l0.016807 17.988c0.00451 4.8315 6.0288 6.743 9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m12.394 37.236c3.5492 1.2226 17.661 4.2149 27.259 2.1874 2.3181-0.48964 4.0736-2.8468 4.0678-5.216l-0.049207-20.123c-0.008279-3.4022-4.0211-6.2011-6.4416-6.1956-10.354 0.023666-19.103-0.0059052-28.774 1.3285-2.347 0.32383-4.1804 3.4327-4.1782 5.802l0.014412 15.424c0.00387 4.1428 5.1694 5.7819 8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m13.833 16.812h3.4556v11.917h7.0662v2.4588h-10.522zm17.101 3.3891-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="L1"/></svg>
|
After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 1.3 KiB |
1
portprotonqt/themes/standart/images/ps_options.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m18.047 46.216-2.1e-5 -5e-6c-5.4306-1.4551-8.6833-7.089-7.2282-12.52l6.6143-24.685c1.4551-5.4306 7.089-8.6833 12.52-7.2282l2.1e-5 5.5e-6c5.4306 1.4551 8.6833 7.089 7.2282 12.52l-6.6143 24.685c-1.4551 5.4306-7.089 8.6833-12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m19.229 41.807-1.7e-5 -4e-6c-4.3529-1.1664-6.9601-5.6821-5.7937-10.035l5.3016-19.786c1.1664-4.3529 5.6821-6.9601 10.035-5.7937l1.7e-5 4.4e-6c4.3529 1.1664 6.9601 5.6821 5.7937 10.035l-5.3016 19.786c-1.1664 4.3529-5.6821 6.9601-10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m19.502 18.291c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114s0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114s-0.69187-1.114-1.5459-1.114z" fill="#3f424d" fill-rule="evenodd" stroke-width=".11455"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
1
portprotonqt/themes/standart/images/ps_r1.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m37.535 39.437c-4.1391 1.4258-20.596 4.9156-31.79 2.551-2.7034-0.57104-4.7508-3.32-4.744-6.0831l0.057386-23.467c0.00968-3.9677 4.6895-7.2319 7.5124-7.2255 12.075 0.0276 22.278-0.00688 33.557 1.5493 2.7371 0.37765 4.8753 4.0033 4.8727 6.7663l-0.01681 17.988c-0.0045 4.8315-6.0288 6.743-9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m35.606 37.236c-3.5492 1.2226-17.661 4.2149-27.259 2.1874-2.3181-0.48964-4.0736-2.8468-4.0678-5.216l0.049207-20.123c0.00828-3.4022 4.0211-6.2011 6.4416-6.1956 10.354 0.023666 19.103-0.00591 28.774 1.3285 2.347 0.32383 4.1804 3.4327 4.1782 5.802l-0.01441 15.424c-0.0039 4.1428-5.1694 5.7819-8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m12.858 16.812h6.4681q2.8796 0 4.1644 0.70883 1.2848 0.68668 1.2848 2.3259v2.5252q0 1.2626-0.90819 1.9936-0.88604 0.70883-2.3702 0.90819l4.1644 5.9143h-3.9872l-3.7657-5.6485h-1.5949v5.6485h-3.4556zm6.4238 6.4459q1.2183 0 1.6613-0.31011 0.44302-0.33226 0.44302-1.2626v-1.0189q0-0.79744-0.48732-1.0854-0.46517-0.31011-1.617-0.31011h-2.9682v3.9872zm12.626-3.0568-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="R1"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB |
1
portprotonqt/themes/standart/images/ps_share.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m29.953 46.216 2.1e-5 -5e-6c5.4306-1.4551 8.6833-7.089 7.2282-12.52l-6.6143-24.685c-1.4551-5.4306-7.089-8.6833-12.52-7.2282l-2.1e-5 5.5e-6c-5.4306 1.4551-8.6833 7.089-7.2282 12.52l6.6143 24.685c1.4551 5.4306 7.089 8.6833 12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m28.771 41.807 1.7e-5 -4e-6c4.3529-1.1664 6.9601-5.6821 5.7937-10.035l-5.3016-19.786c-1.1664-4.3529-5.6821-6.9601-10.035-5.7937l-1.7e-5 4.4e-6c-4.3529 1.1664-6.9601 5.6821-5.7937 10.035l5.3016 19.786c1.1664 4.3529 5.6821 6.9601 10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m24.034 20.416c-0.54232 0-0.98296 0.41005-0.98296 0.91636v5.3348c0 0.50632 0.44064 0.91636 0.98296 0.91636s0.98124-0.41005 0.98124-0.91636v-5.3348c0-0.50632-0.43892-0.91636-0.98124-0.91636zm-5.9615 0.72033c-0.15955 0.0017-0.31975 0.03855-0.46652 0.11513-0.46966 0.24506-0.62269 0.79993-0.34257 1.2384l2.9506 4.6191c0.28012 0.43848 0.88858 0.59512 1.3582 0.35005 0.46966-0.24506 0.62269-0.79837 0.34257-1.2369l-2.9506-4.6192c-0.19258-0.30146-0.5407-0.4705-0.89172-0.46674zm11.856 0c-0.35102-0.0037-0.69914 0.16528-0.89172 0.46674l-2.9506 4.6191c-0.28011 0.43848-0.12709 0.99179 0.34257 1.2369 0.46967 0.24506 1.0781 0.08843 1.3582-0.35005l2.9506-4.6191c0.28011-0.43848 0.12709-0.99335-0.34257-1.2384-0.14677-0.07658-0.30696-0.11342-0.46652-0.11513z" fill="#3f424d" fill-rule="evenodd" stroke-width=".082805"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
portprotonqt/themes/standart/images/ps_square.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m33.019 13.476h-18.037c-0.8274 0-1.5059 0.67847-1.5059 1.5059v18.037c0 0.8274 0.67847 1.5059 1.5059 1.5059h18.037c0.8274 0 1.5059-0.67847 1.5059-1.5059v-18.037c0-0.8274-0.66192-1.5059-1.5059-1.5059zm-1.4893 18.037h-15.026v-15.026h15.026z" fill="#3f424d" stroke-width="1.6548"/></svg>
|
After Width: | Height: | Size: 682 B |
Before Width: | Height: | Size: 1.8 KiB |
1
portprotonqt/themes/standart/images/ps_triangle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m13.766 32.511h20.449c0.60033 0 1.1631-0.31892 1.4821-0.84421 0.30016-0.52529 0.30016-1.1819 0-1.7072l-10.224-17.71c-0.60033-1.0506-2.345-1.0506-2.9454 0l-10.224 17.71c-0.30016 0.52529-0.30016 1.1819 0 1.7072s0.86297 0.84421 1.4633 0.84421zm10.224-15.984 7.2602 12.588h-14.539z" fill="#3f424d" stroke-width="1.876"/></svg>
|
After Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 1.7 KiB |
1
portprotonqt/themes/standart/images/xbox_a.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.016 13.475h6.1623l7.5893 21.049h-5.1244l-1.8811-5.546h-7.6866l-1.8487 5.546h-4.9947zm5.6433 12.13-2.6595-7.9137h-0.12973l-2.6595 7.9137z" fill="#3f424d" stroke-width=".67675" aria-label="A"/></svg>
|
After Width: | Height: | Size: 600 B |
Before Width: | Height: | Size: 1.7 KiB |
1
portprotonqt/themes/standart/images/xbox_b.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m15.973 13.476h8.5299q3.0163 0 4.6379 0.45406 1.6541 0.42163 2.3352 1.3946 0.68109 0.94056 0.68109 2.6595v2.5946q0 0.87569-0.71353 1.6541-0.68109 0.77839-1.6216 1.0703v0.16216q1.2325 0.12973 2.2379 1.0703 1.0379 0.90812 1.0379 2.0433v3.2433q0 2.5622-2.0433 3.6325t-6.3244 1.0703h-8.7569zm8.5299 8.5623q1.2 0 1.7838-0.1946t0.77839-0.61623q0.22703-0.45406 0.22703-1.2973v-1.0379q0-0.74596-0.1946-1.1027-0.1946-0.3892-0.81082-0.55136-0.58379-0.16216-1.8811-0.16216h-3.373v4.9622zm0.12973 8.8866q1.8487 0 2.6271-0.42163t0.77839-1.3622v-1.6865q0-1.1676-0.61623-1.6541-0.58379-0.4865-2.1081-0.4865h-4.2812v5.6109z" fill="#3f424d" stroke-width=".67675" aria-label="B"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
1
portprotonqt/themes/standart/images/xbox_lb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.3645 12.915h35.271c2.9719 0 5.3645 2.3926 5.3645 5.3645v11.442c0 2.9719-2.3926 5.3645-5.3645 5.3645h-35.271c-2.9719 0-5.3645-2.3926-5.3645-5.3645v-11.442c0-2.9719 2.3926-5.3645 5.3645-5.3645z" fill="#3f424d"/><path d="m9.2118 14.704h29.576c2.4921 0 4.4983 2.0062 4.4983 4.4983v9.5944c0 2.4921-2.0062 4.4983-4.4983 4.4983h-29.576c-2.4921 0-4.4983-2.0062-4.4983-4.4983v-9.5944c0-2.4921 2.0062-4.4983 4.4983-4.4983z" fill="#fff"/><path d="m13.757 18h2.8844v9.9476h5.8983v2.0524h-8.7827zm10.724 0h4.8629q1.7196 0 2.6441 0.25886 0.94299 0.24037 1.3313 0.79507 0.38829 0.53621 0.38829 1.5162v1.4792q0 0.49923-0.40678 0.94299-0.38829 0.44376-0.9245 0.61017v0.09245q0.70262 0.07396 1.2758 0.61017 0.59168 0.51772 0.59168 1.1649v1.849q0 1.4607-1.1649 2.0709-1.1649 0.61017-3.6055 0.61017h-4.9923zm4.8629 4.8814q0.68413 0 1.0169-0.11094 0.33282-0.11094 0.44376-0.35131 0.12943-0.25886 0.12943-0.7396v-0.59168q0-0.42527-0.11094-0.62866-0.11094-0.22188-0.46225-0.31433-0.33282-0.09245-1.0724-0.09245h-1.923v2.829zm0.07396 5.0663q1.0539 0 1.4977-0.24037 0.44376-0.24037 0.44376-0.77658v-0.96148q0-0.66564-0.35131-0.94299-0.33282-0.27735-1.2018-0.27735h-2.4407v3.1988z" fill="#3f424d" stroke-width=".38581" aria-label="LB"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.8 KiB |
1
portprotonqt/themes/standart/images/xbox_rb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.3645 12.915h35.271c2.9719 0 5.3645 2.3926 5.3645 5.3645v11.442c0 2.9719-2.3926 5.3645-5.3645 5.3645h-35.271c-2.9719 0-5.3645-2.3926-5.3645-5.3645v-11.442c0-2.9719 2.3926-5.3645 5.3645-5.3645z" fill="#3f424d"/><path d="m9.2118 14.704h29.576c2.4921 0 4.4983 2.0062 4.4983 4.4983v9.5944c0 2.4921-2.0062 4.4983-4.4983 4.4983h-29.576c-2.4921 0-4.4983-2.0062-4.4983-4.4983v-9.5944c0-2.4921 2.0062-4.4983 4.4983-4.4983z" fill="#fff"/><path d="m12.943 18h5.3991q2.4037 0 3.4761 0.59168 1.0724 0.57319 1.0724 1.9414v2.1079q0 1.0539-0.75809 1.6641-0.7396 0.59168-1.9784 0.75809l3.4761 4.9368h-3.3282l-3.1433-4.7149h-1.3313v4.7149h-2.8844zm5.3621 5.3806q1.0169 0 1.3867-0.25886 0.3698-0.27735 0.3698-1.0539v-0.85054q0-0.66564-0.40678-0.90601-0.38829-0.25886-1.3498-0.25886h-2.4777v3.3282zm6.9892-5.3806h4.8629q1.7196 0 2.6441 0.25886 0.94299 0.24037 1.3313 0.79507 0.38829 0.53621 0.38829 1.5162v1.4792q0 0.49923-0.40678 0.94299-0.38829 0.44376-0.9245 0.61017v0.09245q0.70262 0.07396 1.2758 0.61017 0.59168 0.51772 0.59168 1.1649v1.849q0 1.4607-1.1649 2.0709-1.1649 0.61017-3.6055 0.61017h-4.9923zm4.8629 4.8814q0.68413 0 1.017-0.11094 0.33282-0.11094 0.44376-0.35131 0.12943-0.25886 0.12943-0.7396v-0.59168q0-0.42527-0.11094-0.62866-0.11094-0.22188-0.46225-0.31433-0.33282-0.09245-1.0724-0.09245h-1.923v2.829zm0.07396 5.0663q1.0539 0 1.4977-0.24037 0.44376-0.24037 0.44376-0.77658v-0.96148q0-0.66564-0.35131-0.94299-0.33282-0.27735-1.2018-0.27735h-2.4407v3.1988z" fill="#3f424d" stroke-width=".38581" aria-label="RB"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.1 KiB |
1
portprotonqt/themes/standart/images/xbox_start.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m16.169 14.061c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395zm0 8c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395zm0 8c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395z" fill="#3f424d" fill-rule="evenodd" stroke-width=".19943"/></svg>
|
After Width: | Height: | Size: 958 B |
Before Width: | Height: | Size: 2.2 KiB |
1
portprotonqt/themes/standart/images/xbox_view.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m12.75 14.492c-0.62128 0-1.1257 0.38721-1.1257 0.86456v12.1c0 0.47737 0.50442 0.86456 1.1257 0.86456h3.3753v-1.7274h-2.2496v-10.373h13.498v1.7291h2.2496v-2.5937c0-0.47735-0.50268-0.86456-1.1239-0.86456zm6.7489 5.1874c-0.62128 0-1.1239 0.38721-1.1239 0.86456v12.1c0 0.47737 0.50266 0.86456 1.1239 0.86456h15.749c0.62125 0 1.1239-0.3872 1.1239-0.86456v-12.1c0-0.47735-0.50268-0.86456-1.1239-0.86456zm1.1257 1.7291h13.498v10.371h-13.498z" clip-rule="evenodd" fill="#3f424d" fill-rule="evenodd" stroke-width=".98604"/></svg>
|
After Width: | Height: | Size: 919 B |
Before Width: | Height: | Size: 1.8 KiB |
1
portprotonqt/themes/standart/images/xbox_x.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.097 23.789-7.4272-10.314h5.8379l4.5082 7.0055 4.4758-7.0055h5.8379l-7.4272 10.314 7.7839 10.735h-5.8704l-4.8001-7.4596-4.8325 7.4596h-5.8379z" fill="#3f424d" stroke-width=".67675" aria-label="X"/></svg>
|
After Width: | Height: | Size: 605 B |
1
portprotonqt/themes/standart/images/xbox_y.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.438 26.092-7.6218-12.616h5.7406l4.4433 8.238 4.4109-8.238h5.7731l-7.6866 12.552v8.4974h-5.0595z" fill="#3f424d" stroke-width="1.0811" aria-label="Y"/></svg>
|
After Width: | Height: | Size: 559 B |
@@ -217,6 +217,56 @@ CONTEXT_MENU_STYLE = f"""
|
||||
}}
|
||||
"""
|
||||
|
||||
VIRTUAL_KEYBOARD_STYLE = """
|
||||
VirtualKeyboard {
|
||||
background-color: rgba(30, 30, 30, 200);
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
}
|
||||
QPushButton {
|
||||
font-size: 14px;
|
||||
border: 1px solid #555;
|
||||
border-top-color: #666;
|
||||
border-left-color: #666;
|
||||
border-radius: 3px;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
padding: 4px;
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505050, stop:1 #404040);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #606060, stop:1 #505050);
|
||||
border: 1px solid #666;
|
||||
border-top-color: #777;
|
||||
border-left-color: #777;
|
||||
}
|
||||
QPushButton:focus {
|
||||
border: 2px solid #4a90e2;
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a5a5a, stop:1 #454545);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3a3a3a, stop:1 #303030);
|
||||
border: 1px solid #444;
|
||||
border-bottom-color: #555;
|
||||
border-right-color: #555;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 3px;
|
||||
padding-left: 5px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
QPushButton[checked="true"] {
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a90e2, stop:1 #3a7ad2);
|
||||
color: white;
|
||||
border: 1px solid #2a6ac2;
|
||||
border-top-color: #5aa0f2;
|
||||
border-left-color: #5aa0f2;
|
||||
}
|
||||
QPushButton[checked="true"]:focus {
|
||||
border: 2px solid #6aa3f5;
|
||||
}
|
||||
"""
|
||||
|
||||
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
|
||||
MAIN_WINDOW_STYLE = f"""
|
||||
QWidget {{
|
||||
@@ -916,6 +966,96 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TAB_STYLE = f"""
|
||||
QTabWidget::pane {{
|
||||
border: 1px solid {color_d};
|
||||
background: {color_b};
|
||||
border-radius: {border_radius_a};
|
||||
}}
|
||||
QTabBar::tab {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
padding: 8px 16px;
|
||||
border-top-left-radius: {border_radius_a};
|
||||
border-top-right-radius: {border_radius_a};
|
||||
margin-right: 2px;
|
||||
}}
|
||||
QTabBar::tab:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTabBar::tab:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TABBLE_STYLE = f"""
|
||||
QTableWidget {{
|
||||
background: {color_c};
|
||||
color: {color_f};
|
||||
gridline-color: {color_d};
|
||||
alternate-background-color: {color_d};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
}}
|
||||
QHeaderView::section {{
|
||||
background: {color_d};
|
||||
color: {color_f};
|
||||
padding: 5px;
|
||||
border: {border_a};
|
||||
font-weight: bold;
|
||||
}}
|
||||
QTableWidget::item {{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid {color_d};
|
||||
}}
|
||||
QTableWidget::item:selected {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QTableWidget::item:hover {{
|
||||
background: {color_e};
|
||||
}}
|
||||
QTableWidget::indicator {{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: {border_b} {color_a};
|
||||
border-radius: {border_radius_a};
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}}
|
||||
QTableWidget::indicator:unchecked {{
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
image: none;
|
||||
}}
|
||||
QTableWidget::indicator:checked {{
|
||||
background: {color_a};
|
||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
||||
border: {border_b} {color_f};
|
||||
}}
|
||||
QTableWidget::indicator:hover {{
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: {border_b} {color_a};
|
||||
}}
|
||||
QTableWidget::indicator:focus {{
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
{SCROLL_AREA_STYLE}
|
||||
"""
|
||||
|
||||
WINETRICKS_LOG_STYLE = f"""
|
||||
QTextEdit {{
|
||||
background: {color_c};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
color: {color_f};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
padding: 5px;
|
||||
}}
|
||||
"""
|
||||
|
||||
FILE_EXPLORER_STYLE = f"""
|
||||
QListView {{
|
||||
font-size: {font_size_a};
|
||||
|
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
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from portprotonqt.config_utils import read_theme_from_config
|
||||
|
||||
theme_manager = ThemeManager()
|
||||
|
||||
class VirtualKeyboard(QFrame):
|
||||
keyPressed = Signal(str)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, theme=None, button_width: int = 80):
|
||||
super().__init__(parent)
|
||||
self._parent: QWidget | None = parent
|
||||
self.available_layouts: list[str] = self.get_layouts_setxkbmap()
|
||||
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||
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.base_button_width = 40
|
||||
self.base_min_width = 574
|
||||
self.button_width = button_width
|
||||
self.button_height = 40
|
||||
self.spacing = 4
|
||||
self.margins = 10
|
||||
self.num_cols = 14
|
||||
|
||||
self.initUI()
|
||||
self.hide()
|
||||
|
||||
self.setStyleSheet(self.theme.VIRTUAL_KEYBOARD_STYLE)
|
||||
|
||||
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(self.spacing)
|
||||
self.keyboard_layout.setContentsMargins(self.margins // 2, self.margins // 2, self.margins // 2, self.margins // 2)
|
||||
self.create_keyboard()
|
||||
|
||||
self.keyboard_container = QWidget()
|
||||
self.keyboard_container.setLayout(self.keyboard_layout)
|
||||
ratio = self.button_width / self.base_button_width
|
||||
self.keyboard_container.setMinimumWidth(int(self.base_min_width * ratio))
|
||||
self.keyboard_container.setMinimumHeight(220)
|
||||
self.keyboard_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
layout.addWidget(self.keyboard_container, 0, Qt.AlignmentFlag.AlignHCenter)
|
||||
self.setLayout(layout)
|
||||
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 = self.button_width
|
||||
fixed_h = self.button_height
|
||||
|
||||
# Выбираем текущую раскладку (обычная или с 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 * self.spacing, 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.setFixedSize(fixed_w * 5 + 4 * self.spacing, fixed_h)
|
||||
space.clicked.connect(lambda: self.on_button_click(' '))
|
||||
self.keyboard_layout.addWidget(space, 4, 1, 1, 5)
|
||||
|
||||
backspace = QPushButton('⌫')
|
||||
backspace.setFixedSize(fixed_w, fixed_h)
|
||||
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_w * 2 + self.spacing, fixed_h)
|
||||
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.setFixedSize(fixed_w * 2 + self.spacing, 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)
|
||||
|
||||
hide_button = QPushButton('Hide')
|
||||
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
||||
hide_button.clicked.connect(self.hide)
|
||||
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
||||
|
||||
if coords:
|
||||
row, col = coords
|
||||
item = self.keyboard_layout.itemAtPosition(row, col)
|
||||
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
|
||||
self.update_keyboard()
|
||||
if key_to_restore and key_to_restore in self.buttons:
|
||||
self.buttons[key_to_restore].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()
|
||||
|
||||
def on_shift_click(self, checked):
|
||||
self.shift_pressed = checked
|
||||
if not checked and self.caps_lock:
|
||||
self.caps_lock = False
|
||||
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.animateClick()
|
||||
|
||||
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
|
||||
|
||||
num_rows = self.keyboard_layout.rowCount()
|
||||
num_cols = self.keyboard_layout.columnCount()
|
||||
|
||||
found = False
|
||||
|
||||
if direction == "right":
|
||||
# Сначала ищем в той же строке вправо
|
||||
search_row = current_row
|
||||
search_col = current_col + col_span
|
||||
while search_col < num_cols:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_col += 1
|
||||
|
||||
if not found:
|
||||
# Переходим к следующей строке, начиная с col 0
|
||||
search_row = (current_row + 1) % num_rows
|
||||
search_col = 0
|
||||
# Ищем первую кнопку в этой строке
|
||||
while search_col < num_cols:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_col += 1
|
||||
|
||||
elif direction == "left":
|
||||
# Сначала ищем в той же строке влево
|
||||
search_row = current_row
|
||||
search_col = current_col - 1
|
||||
while search_col >= 0:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_col -= 1
|
||||
|
||||
if not found:
|
||||
# Переходим к предыдущей строке, начиная с последнего столбца
|
||||
search_row = (current_row - 1) % num_rows
|
||||
search_col = num_cols - 1
|
||||
# Ищем последнюю кнопку в этой строке
|
||||
while search_col >= 0:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_col -= 1
|
||||
|
||||
elif direction == "down":
|
||||
# Сначала ищем в том же столбце вниз
|
||||
search_col = current_col
|
||||
search_row = current_row + row_span
|
||||
while search_row < num_rows:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_row += 1
|
||||
|
||||
if not found:
|
||||
# Переходим к следующему столбцу, начиная с row 0
|
||||
search_col = (current_col + col_span) % num_cols
|
||||
search_row = 0
|
||||
# Ищем первую кнопку в этом столбце
|
||||
while search_row < num_rows:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_row += 1
|
||||
|
||||
elif direction == "up":
|
||||
# Сначала ищем в том же столбце вверх
|
||||
search_col = current_col
|
||||
search_row = current_row - 1
|
||||
while search_row >= 0:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_row -= 1
|
||||
|
||||
if not found:
|
||||
# Переходим к предыдущему столбцу, начиная с последней строки
|
||||
search_col = (current_col - 1) % num_cols
|
||||
search_row = num_rows - 1
|
||||
# Ищем последнюю кнопку в этом столбце
|
||||
while search_row >= 0:
|
||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||
if item and item.widget() and item.widget().isEnabled():
|
||||
next_button = cast(QPushButton, item.widget())
|
||||
next_button.setFocus()
|
||||
found = True
|
||||
break
|
||||
search_row -= 1
|
||||
|
||||
def findFirstFocusableButton(self) -> QPushButton | None:
|
||||
"""Находит первую фокусируемую кнопку на клавиатуре"""
|
||||
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.13.5",
|
||||
"beautifulsoup4>=4.14.2",
|
||||
"evdev>=1.9.2",
|
||||
"icoextract>=0.2.0",
|
||||
"numpy>=2.2.4",
|
||||
"orjson>=3.11.2",
|
||||
"orjson>=3.11.3",
|
||||
"pillow>=11.3.0",
|
||||
"psutil>=7.0.0",
|
||||
"pyside6>=6.9.1",
|
||||
"psutil>=7.1.0",
|
||||
"pyside6==6.9.1",
|
||||
"pyudev>=0.24.3",
|
||||
"requests>=2.32.5",
|
||||
"tqdm>=4.67.1",
|
||||
"vdf>=3.4",
|
||||
"websocket-client>=1.8.0",
|
||||
"websocket-client>=1.9.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -105,5 +105,5 @@ ignore = [
|
||||
dev = [
|
||||
"pre-commit>=4.3.0",
|
||||
"pyaspeller>=2.0.2",
|
||||
"pyright>=1.1.404",
|
||||
"pyright>=1.1.406",
|
||||
]
|
||||
|
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:best-practices"],
|
||||
"extends": [
|
||||
"config:best-practices"
|
||||
],
|
||||
"rebaseWhen": "never",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
@@ -9,6 +11,23 @@
|
||||
"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
|
||||
@@ -33,7 +52,7 @@
|
||||
"groupName": "Python dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["numpy", "setuptools", "python"],
|
||||
"matchPackageNames": ["numpy", "setuptools", "python", "pyside6"],
|
||||
"enabled": false,
|
||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
||||
},
|
||||
|