(gui): adding type annotations

This commit is contained in:
Sergey Palcheh
2026-01-22 14:36:46 +06:00
parent 10ca3b3c98
commit 2a5003c15b

View File

@@ -16,22 +16,23 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QH
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve, pyqtSignal, QRect
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor, QTextCharFormat, QColor, QPalette
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from typing import Optional, Dict, Any, Callable, List, Set, Tuple, Union
class Var:
# Переменные определяемые в скрипте winehelper
SCRIPT_NAME = os.environ.get("SCRIPT_NAME")
USER_WORK_PATH = os.environ.get("USER_WORK_PATH")
RUN_SCRIPT = os.environ.get("RUN_SCRIPT")
DATA_PATH = os.environ.get("DATA_PATH")
CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE")
WH_ICON_PATH = os.environ.get("WH_ICON_PATH")
WH_ICON_TRAY = os.environ.get("WH_ICON_TRAY")
LICENSE_FILE = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE")
GENERAL = os.environ.get("GENERAL")
WH_WINETRICKS = os.environ.get("WH_WINETRICKS")
SCRIPT_NAME: Optional[str] = os.environ.get("SCRIPT_NAME")
USER_WORK_PATH: Optional[str] = os.environ.get("USER_WORK_PATH")
RUN_SCRIPT: Optional[str] = os.environ.get("RUN_SCRIPT")
DATA_PATH: Optional[str] = os.environ.get("DATA_PATH")
CHANGELOG_FILE: Optional[str] = os.environ.get("CHANGELOG_FILE")
WH_ICON_PATH: Optional[str] = os.environ.get("WH_ICON_PATH")
WH_ICON_TRAY: Optional[str] = os.environ.get("WH_ICON_TRAY")
LICENSE_FILE: Optional[str] = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE: Optional[str] = os.environ.get("AGREEMENT")
THIRD_PARTY_FILE: Optional[str] = os.environ.get("THIRD_PARTY_FILE")
GENERAL: Optional[str] = os.environ.get("GENERAL")
WH_WINETRICKS: Optional[str] = os.environ.get("WH_WINETRICKS")
class WinetricksManagerDialog(QDialog):
@@ -46,21 +47,21 @@ class WinetricksManagerDialog(QDialog):
installation_complete = pyqtSignal()
def __init__(self, prefix_path, winetricks_path, parent=None, wine_executable=None):
def __init__(self, prefix_path: str, winetricks_path: str, parent: Optional[QWidget] = None, wine_executable: Optional[str] = None):
super().__init__(parent)
self.prefix_path = prefix_path
self.winetricks_path = winetricks_path
self.wine_executable = wine_executable or 'wine'
self.initial_states = {}
self.apply_process = None
self.installation_finished = False
self.user_cancelled = False
self.processes = {}
self.category_statuses = {}
self.previous_tab_widget = None
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks")
self.prefix_path: str = prefix_path
self.winetricks_path: str = winetricks_path
self.wine_executable: str = wine_executable or 'wine'
self.initial_states: Dict[str, bool] = {}
self.apply_process: Optional[QProcess] = None
self.installation_finished: bool = False
self.user_cancelled: bool = False
self.processes: Dict[str, QProcess] = {}
self.category_statuses: Dict[str, str] = {}
self.previous_tab_widget: Optional[QWidget] = None
self.cache_dir: str = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks")
os.makedirs(self.cache_dir, exist_ok=True)
self.is_reloading_after_cache_clear = False
self.is_reloading_after_cache_clear: bool = False
self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
self.setMinimumSize(800, 500)
@@ -126,7 +127,7 @@ class WinetricksManagerDialog(QDialog):
# Устанавливаем начальное состояние для отслеживания покинутой вкладки
self.previous_tab_widget = self.tabs.currentWidget()
def on_tab_switched(self, index):
def on_tab_switched(self, index: int) -> None:
"""
Обрабатывает переключение вкладок.
Если установка только что завершилась, сбрасывает лог к информационному тексту.
@@ -144,7 +145,7 @@ class WinetricksManagerDialog(QDialog):
# Сохраняем текущую вкладку для следующего переключения
self.previous_tab_widget = self.tabs.widget(index)
def _create_category_tab(self, title):
def _create_category_tab(self, title: str) -> Tuple[QListWidget, QLineEdit]:
"""Создает вкладку с поиском и списком."""
tab = QWidget()
layout = QVBoxLayout(tab)
@@ -163,23 +164,23 @@ class WinetricksManagerDialog(QDialog):
self.tabs.addTab(tab, title)
return list_widget, search_edit
def filter_list(self, text, list_widget):
def filter_list(self, text: str, list_widget: QListWidget) -> None:
"""Фильтрует элементы в списке."""
for i in range(list_widget.count()):
item = list_widget.item(i)
item.setHidden(text.lower() not in item.text().lower())
def load_all_categories(self):
def load_all_categories(self) -> None:
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
for internal_name in self.categories.values():
self._start_load_process(internal_name)
def _get_cache_path(self, category):
def _get_cache_path(self, category: str) -> str:
"""Возвращает путь к файлу кэша для указанной категории."""
return os.path.join(self.cache_dir, f"{category}.json")
def _get_winetricks_hash(self):
def _get_winetricks_hash(self) -> Optional[str]:
"""Вычисляет хэш файла winetricks для проверки его обновления."""
try:
hasher = hashlib.sha256()
@@ -190,7 +191,7 @@ class WinetricksManagerDialog(QDialog):
except (IOError, OSError):
return None
def _start_load_process(self, category):
def _start_load_process(self, category: str) -> None:
"""Запускает QProcess для получения списка компонентов, используя кэш."""
cache_path = self._get_cache_path(category)
cache_ttl_seconds = 86400 # 24 часа
@@ -225,9 +226,9 @@ class WinetricksManagerDialog(QDialog):
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
def _parse_winetricks_log(self):
def _parse_winetricks_log(self) -> Set[str]:
"""Читает winetricks.log и возвращает множество установленных компонентов."""
installed_verbs = set()
installed_verbs: Set[str] = set()
log_path = os.path.join(self.prefix_path, "winetricks.log")
if not os.path.exists(log_path):
return installed_verbs
@@ -242,7 +243,7 @@ class WinetricksManagerDialog(QDialog):
self._log(f"--- Предупреждение: не удалось прочитать {log_path}: {e} ---")
return installed_verbs
def _parse_winetricks_list_output(self, output, installed_verbs, list_widget, category):
def _parse_winetricks_list_output(self, output: str, installed_verbs: Set[str], list_widget: QListWidget, category: str) -> bool:
"""Парсит вывод 'winetricks list' и заполняет QListWidget."""
# Regex, который обрабатывает строки как с префиксом статуса '[ ]', так и без него.
# 1. `(?:\[(.)]\s+)?` - опциональная группа для статуса (напр. '[x]').
@@ -250,9 +251,9 @@ class WinetricksManagerDialog(QDialog):
# 3. `(.*)` - оставшаяся часть строки (описание).
# Определяем шаблоны для фильтрации на основе категории
dlls_blacklist_pattern = None
fonts_blacklist_pattern = None
settings_blacklist_pattern = None
dlls_blacklist_pattern: Optional[re.Pattern] = None
fonts_blacklist_pattern: Optional[re.Pattern] = None
settings_blacklist_pattern: Optional[re.Pattern] = None
if category == 'dlls':
# Исключаем dont_use, dxvk*, vkd3d*, galliumnine, faudio*, Foundation
@@ -309,7 +310,7 @@ class WinetricksManagerDialog(QDialog):
return found_items
def _on_load_finished(self, category, exit_code, exit_status, from_cache=None):
def _on_load_finished(self, category: str, exit_code: int, exit_status: QProcess.ProcessState, from_cache: Optional[str] = None) -> None:
"""Обрабатывает завершение загрузки списка компонентов."""
if from_cache is not None:
output = from_cache
@@ -370,7 +371,7 @@ class WinetricksManagerDialog(QDialog):
self._log("\n=== Списки успешно обновлены ===")
self.is_reloading_after_cache_clear = False # Сбрасываем флаг
def _on_item_changed(self, item):
def _on_item_changed(self, item: QListWidgetItem) -> None:
"""Обрабатывает изменение состояния чекбокса, предотвращая снятие галочки с установленных."""
name = item.data(Qt.UserRole)
# Если компонент был изначально установлен и пользователь пытается его снять
@@ -385,7 +386,7 @@ class WinetricksManagerDialog(QDialog):
list_widget.blockSignals(False)
self._update_ui_state()
def _update_ui_state(self, *args):
def _update_ui_state(self, *args) -> None:
"""Централизованно обновляет состояние кнопок 'Применить' и 'Переустановить'."""
# 1. Проверяем, есть ли изменения в чекбоксах (установка новых или снятие галочек с новых)
has_changes = False
@@ -419,7 +420,7 @@ class WinetricksManagerDialog(QDialog):
self.reinstall_button.setEnabled(is_reinstallable)
def reinstall_selected(self):
def reinstall_selected(self) -> None:
"""Переустанавливает выбранный компонент."""
current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
if not current_list_widget:
@@ -442,7 +443,7 @@ class WinetricksManagerDialog(QDialog):
verbs_to_reinstall = [name]
self._start_install_process(verbs_to_reinstall)
def apply_changes(self):
def apply_changes(self) -> None:
"""Применяет выбранные изменения."""
# Собираем все компоненты, которые были отмечены для установки.
verbs_to_install = []
@@ -471,7 +472,7 @@ class WinetricksManagerDialog(QDialog):
self._start_install_process(verbs_to_install)
def _start_install_process(self, verbs_to_install):
def _start_install_process(self, verbs_to_install: List[str]) -> None:
"""Запускает процесс установки/переустановки winetricks."""
# Добавляем флаг --force, чтобы разрешить переустановку
self._log(f"Выполнение установки: winetricks --unattended --force {' '.join(verbs_to_install)}")
@@ -485,7 +486,7 @@ class WinetricksManagerDialog(QDialog):
self.apply_process.finished.connect(self.on_apply_finished)
self.apply_process.start(self.winetricks_path, ["--unattended", "--force"] + verbs_to_install)
def on_apply_finished(self, exit_code, exit_status):
def on_apply_finished(self, exit_code: int, exit_status: QProcess.ProcessState) -> None:
"""Обрабатывает завершение применения изменений."""
# 1. Проверяем, была ли отмена пользователем
if self.user_cancelled:
@@ -525,7 +526,7 @@ class WinetricksManagerDialog(QDialog):
self.installation_complete.emit()
self.installation_finished = True
def clear_winetricks_cache(self):
def clear_winetricks_cache(self) -> None:
"""Запускает очистку кэша Winetricks."""
reply = self._show_message_box(
"Очистка кэша Winetricks",
@@ -551,12 +552,12 @@ class WinetricksManagerDialog(QDialog):
self.cache_clear_process = QProcess(self)
self.cache_clear_process.setProcessChannelMode(QProcess.MergedChannels)
def handle_output():
def handle_output() -> None:
output = self.cache_clear_process.readAll().data().decode('utf-8', 'ignore').strip()
if output:
self._log(output)
def handle_finish(exit_code, exit_status):
def handle_finish(exit_code: int, exit_status: QProcess.ProcessState) -> None:
if exit_code == 0:
self.is_reloading_after_cache_clear = True # Устанавливаем флаг перед перезагрузкой
self.category_statuses.clear() # Очищаем статусы перед новой загрузкой
@@ -580,10 +581,10 @@ class WinetricksManagerDialog(QDialog):
winehelper_path = self.parent().winehelper_path if hasattr(self.parent(), 'winehelper_path') else Var.RUN_SCRIPT
args = ["clear-winetricks-cache", "--force"]
self._log(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(args)}\n")
self._log(f"Выполнение: {shlex.quote(winehelper_path or '')} {' '.join(args)}\n")
self.cache_clear_process.start(winehelper_path, args)
def closeEvent(self, event):
def closeEvent(self, event) -> None:
"""Обрабатывает закрытие окна, чтобы предотвратить выход во время установки."""
# Проверяем, запущен ли процесс установки/переустановки
if self.apply_process and self.apply_process.state() == QProcess.Running:
@@ -602,7 +603,7 @@ class WinetricksManagerDialog(QDialog):
else:
event.accept() # Процесс не запущен, можно закрывать
def _show_message_box(self, title, text, icon, config):
def _show_message_box(self, title: str, text: str, icon, config: dict) -> Optional[str]:
"""Централизованный метод для создания и показа QMessageBox."""
msg_box = QMessageBox(self)
msg_box.setWindowTitle(title)
@@ -622,7 +623,7 @@ class WinetricksManagerDialog(QDialog):
clicked_button = msg_box.clickedButton()
return clicked_button.text() if clicked_button else None
def _log(self, message, color=None):
def _log(self, message: str, color: Optional[str] = None) -> None:
"""Добавляет сообщение в лог с возможностью указания цвета."""
if color:
self.log_output.append(f'<span style="color:{color};">{message}</span>')
@@ -636,7 +637,7 @@ class ScriptParser:
"""Утилитарный класс для парсинга информации из скриптов установки."""
@staticmethod
def extract_icons_from_script(script_path):
def extract_icons_from_script(script_path: str) -> List[str]:
"""
Извлекает иконку для скрипта.
Сначала ищет переменную 'export PROG_ICON=', если не находит,
@@ -662,7 +663,7 @@ class ScriptParser:
return icon_names_str.split()
# 3. Если ничего не найдено, ищем все вызовы create_desktop
icon_names = []
icon_names: List[str] = []
for line in lines:
line = line.strip()
# Пропускаем закомментированные строки и пустые строки
@@ -688,7 +689,7 @@ class ScriptParser:
return []
@staticmethod
def extract_prog_name_from_script(script_path):
def extract_prog_name_from_script(script_path: str) -> Optional[str]:
"""Извлекает имя программы из строки PROG_NAME= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
@@ -703,7 +704,7 @@ class ScriptParser:
return None
@staticmethod
def extract_prog_url_from_script(script_path):
def extract_prog_url_from_script(script_path: str) -> Optional[str]:
"""Извлекает URL из строки export PROG_URL= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
@@ -716,7 +717,7 @@ class ScriptParser:
return None
@staticmethod
def extract_info_ru(script_path):
def extract_info_ru(script_path: str) -> str:
"""Извлекает информацию из строки # info_ru: в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
@@ -730,13 +731,13 @@ class ScriptParser:
class WineVersionSelectionDialog(QDialog):
"""Диалог для выбора версии Wine/Proton с группировкой."""
def __init__(self, architecture, parent=None):
def __init__(self, architecture: str, parent: Optional[QWidget] = None):
super().__init__(parent)
self.architecture = architecture
self.selected_version = None
self.wine_versions_data = {}
self.system_wine_display_name = "Системная версия"
self.selected_display_text = None
self.architecture: str = architecture
self.selected_version: Optional[str] = None
self.wine_versions_data: Dict[str, List[str]] = {}
self.system_wine_display_name: str = "Системная версия"
self.selected_display_text: Optional[str] = None
self.setWindowTitle(f"Выбор версии Wine/Proton для {architecture} префикса")
self.setMinimumSize(900, 500)
@@ -754,7 +755,7 @@ class WineVersionSelectionDialog(QDialog):
self.load_versions()
def load_versions(self):
def load_versions(self) -> None:
"""Запускает процесс получения списка версий Wine."""
self.main_tabs.clear()
loading_widget = QWidget()
@@ -772,16 +773,16 @@ class WineVersionSelectionDialog(QDialog):
self.main_tabs.setEnabled(True)
def _parse_sha256_list(self):
def _parse_sha256_list(self) -> None:
"""Парсит sha256sum.list для получения списка версий."""
sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list")
sha256_path = os.path.join(Var.DATA_PATH or "", "sha256sum.list")
if not os.path.exists(sha256_path):
QMessageBox.warning(self, "Ошибка", f"Файл с версиями не найден:\n{sha256_path}")
self.wine_versions_data = {}
return
self.wine_versions_data = {}
current_group = None
current_group: Optional[str] = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
@@ -814,9 +815,9 @@ class WineVersionSelectionDialog(QDialog):
QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать файл версий:\n{e}")
self.wine_versions_data = {}
def _get_installed_versions(self):
def _get_installed_versions(self) -> List[str]:
"""Возвращает список локально установленных версий Wine."""
dist_path = os.path.join(Var.USER_WORK_PATH, "dist")
dist_path = os.path.join(Var.USER_WORK_PATH or "", "dist")
if not os.path.isdir(dist_path):
return []
try:
@@ -827,7 +828,7 @@ class WineVersionSelectionDialog(QDialog):
except OSError:
return []
def populate_ui(self):
def populate_ui(self) -> None:
"""Заполняет UI отфильтрованными версиями."""
self.main_tabs.clear()
@@ -844,7 +845,7 @@ class WineVersionSelectionDialog(QDialog):
self.installed_grid_layout = QGridLayout(installed_content)
self.installed_grid_layout.setAlignment(Qt.AlignTop)
installed_versions_for_grid = []
installed_versions_for_grid: List[Union[Tuple[str, str], str]] = []
# Системная версия
if shutil.which('wine'):
@@ -902,7 +903,7 @@ class WineVersionSelectionDialog(QDialog):
self.filter_versions()
def _create_version_tab(self, title, versions_list, is_download_tab=True):
def _create_version_tab(self, title: str, versions_list: List[Union[Tuple[str, str], str]], is_download_tab: bool = True) -> None:
"""Создает вкладку с сеткой кнопок для переданного списка версий."""
tab_page = QWidget()
tab_layout = QVBoxLayout(tab_page)
@@ -922,7 +923,7 @@ class WineVersionSelectionDialog(QDialog):
self._create_grid_buttons(grid_layout, versions_list, is_download_tab=is_download_tab)
self.version_tabs.addTab(tab_page, title)
def _create_grid_buttons(self, grid_layout, versions_list, is_download_tab=False):
def _create_grid_buttons(self, grid_layout: QGridLayout, versions_list: List[Union[Tuple[str, str], str]], is_download_tab: bool = False) -> None:
is_win64_prefix = self.architecture == "win64"
is_win32_prefix = self.architecture == "win32"
re_32bit_version = re.compile(r'i[3-6]86|i586')
@@ -961,7 +962,7 @@ class WineVersionSelectionDialog(QDialog):
col = 0
row += 1
def filter_versions(self):
def filter_versions(self) -> None:
"""Фильтрует видимость кнопок версий на основе текста поиска."""
search_text = self.search_edit.text().lower()
@@ -978,7 +979,7 @@ class WineVersionSelectionDialog(QDialog):
any_visible = self._filter_grid(grid_layout, search_text)
self.version_tabs.setTabEnabled(i, any_visible)
def _filter_grid(self, grid_layout, search_text):
def _filter_grid(self, grid_layout: QGridLayout, search_text: str) -> bool:
"""Helper-функция для фильтрации кнопок в одной сетке."""
any_visible_in_grid = False
if not grid_layout:
@@ -996,7 +997,7 @@ class WineVersionSelectionDialog(QDialog):
return any_visible_in_grid
def on_version_selected(self, version_name):
def on_version_selected(self, version_name: str) -> None:
"""Обрабатывает выбор версии."""
self.selected_version = version_name
if version_name == 'system':
@@ -1008,20 +1009,20 @@ class WineVersionSelectionDialog(QDialog):
class CreatePrefixDialog(QDialog):
"""Диалог для создания нового префикса."""
def __init__(self, parent=None):
def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent)
self.parent_gui = parent # Сохранить ссылку на главное окно
self.parent_gui: Optional[QWidget] = parent # Сохранить ссылку на главное окно
self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(680, 250)
self.setModal(True)
# Attributes to store results
self.prefix_name = None
self.wine_arch = None
self.base_pfx = None
self.prepared_prefixes = {}
self.selected_wine_version_value = None
self.selected_wine_version_display = None
self.prefix_name: Optional[str] = None
self.wine_arch: Optional[str] = None
self.base_pfx: Optional[str] = None
self.prepared_prefixes: Dict[str, List[Tuple[str, str]]] = {'win32': [], 'win64': []}
self.selected_wine_version_value: Optional[str] = None
self.selected_wine_version_display: Optional[str] = None
layout = QVBoxLayout(self)
form_layout = QFormLayout()
@@ -1098,17 +1099,17 @@ class CreatePrefixDialog(QDialog):
self._load_prepared_prefixes()
self.on_architecture_changed()
def _load_prepared_prefixes(self):
def _load_prepared_prefixes(self) -> None:
"""Загружает и парсит шаблоны префиксов из sha256sum.list."""
self.prepared_prefixes = {'win32': [], 'win64': []}
sha256_file = os.path.join(Var.DATA_PATH, "sha256sum.list")
sha256_file = os.path.join(Var.DATA_PATH or "", "sha256sum.list")
if not os.path.exists(sha256_file):
QMessageBox.warning(self, "Ошибка", f"Файл с описаниями префиксов не найден: {sha256_file}")
return
in_prefix_section = False
current_description = ""
current_prefix_name = None
current_prefix_name: Optional[str] = None
try:
with open(sha256_file, 'r', encoding='utf-8') as f:
@@ -1166,7 +1167,7 @@ class CreatePrefixDialog(QDialog):
self.prepared_prefixes['win32'].insert(0, ('none', 'Создать чистый префикс без дополнительных библиотек'))
self.prepared_prefixes['win64'].insert(0, ('none', 'Создать чистый префикс без дополнительных библиотек'))
def open_wine_version_dialog(self):
def open_wine_version_dialog(self) -> None:
"""Открывает диалог выбора версии Wine."""
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
dialog = WineVersionSelectionDialog(architecture, self)
@@ -1174,7 +1175,7 @@ class CreatePrefixDialog(QDialog):
self.wine_version_edit.setText(dialog.selected_display_text)
self.selected_wine_version_value = dialog.selected_version
def on_architecture_changed(self):
def on_architecture_changed(self) -> None:
"""Обновляет список шаблонов и сбрасывает выбор версии Wine при смене архитектуры."""
# Сбрасываем выбор версии
self.wine_version_edit.clear()
@@ -1197,7 +1198,7 @@ class CreatePrefixDialog(QDialog):
self.prefix_template_selector.currentIndexChanged.connect(lambda i: self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(i)['tooltip']))
self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(0)['tooltip'])
def validate_prefix_name(self, text):
def validate_prefix_name(self, text: str) -> None:
"""Проверяет имя префикса в реальном времени и показывает/скрывает предупреждение."""
valid_pattern = r'^[a-zA-Z0-9_-]*$'
if re.match(valid_pattern, text):
@@ -1251,13 +1252,13 @@ class CreatePrefixDialog(QDialog):
class FileAssociationsDialog(QDialog):
"""Диалог для управления ассоциациями файлов (WH_XDG_OPEN)."""
def __init__(self, current_associations, parent=None):
def __init__(self, current_associations: str, parent: Optional[QWidget] = None):
super().__init__(parent)
self.setWindowTitle("Настройка ассоциаций файлов")
self.setMinimumWidth(450)
self.setModal(True)
self.new_associations = current_associations
self.new_associations: str = current_associations
layout = QVBoxLayout(self)
layout.setSpacing(10) # Добавляем вертикальный отступ между виджетами
@@ -1291,7 +1292,7 @@ class FileAssociationsDialog(QDialog):
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def validate_and_accept(self):
def validate_and_accept(self) -> None:
"""Проверяет введенные данные перед закрытием."""
forbidden_extensions = {"cpl", "dll", "exe", "lnk", "msi"}
@@ -1320,11 +1321,11 @@ class FileAssociationsDialog(QDialog):
class ComponentVersionSelectionDialog(QDialog):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
def __init__(self, component_group, title, parent=None, add_extra_options=True):
def __init__(self, component_group: str, title: str, parent: Optional[QWidget] = None, add_extra_options: bool = True):
super().__init__(parent)
self.component_group = component_group
self.selected_version = None
self.versions_data = []
self.component_group: str = component_group
self.selected_version: Optional[str] = None
self.versions_data: List[str] = []
self.setWindowTitle(title)
self.setMinimumSize(600, 400)
@@ -1348,7 +1349,7 @@ class ComponentVersionSelectionDialog(QDialog):
self.grid_layout = QGridLayout(scroll_content)
self.grid_layout.setAlignment(Qt.AlignTop)
self.buttons = []
self.buttons: List[QPushButton] = []
# Кнопка "Удалить" теперь находится вне сетки, поэтому начинаем с 0 строки.
self.load_versions(start_row=0)
@@ -1370,19 +1371,19 @@ class ComponentVersionSelectionDialog(QDialog):
main_layout.addLayout(button_layout)
def load_versions(self, start_row):
def load_versions(self, start_row: int) -> None:
"""Загружает и отображает версии."""
self._parse_sha256_list()
self.populate_ui(start_row)
def _parse_sha256_list(self):
def _parse_sha256_list(self) -> None:
"""Парсит sha256sum.list для получения списка версий."""
sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list")
sha256_path = os.path.join(Var.DATA_PATH or "", "sha256sum.list")
if not os.path.exists(sha256_path):
self.versions_data = []
return
current_group = None
current_group: Optional[str] = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
for line in f:
@@ -1403,7 +1404,7 @@ class ComponentVersionSelectionDialog(QDialog):
except IOError:
self.versions_data = []
def populate_ui(self, start_row):
def populate_ui(self, start_row: int) -> None:
"""Заполняет UI кнопками версий."""
versions = sorted(self.versions_data, reverse=True)
num_columns = 3
@@ -1418,7 +1419,7 @@ class ComponentVersionSelectionDialog(QDialog):
col = 0
row += 1
def filter_versions(self):
def filter_versions(self) -> None:
"""Фильтрует видимость кнопок версий и перестраивает сетку для плотного отображения."""
search_text = self.search_edit.text().lower()
@@ -1438,7 +1439,7 @@ class ComponentVersionSelectionDialog(QDialog):
self.grid_layout.addWidget(btn_widget, row, col)
btn_widget.setVisible(True)
def on_version_selected(self, version_name):
def on_version_selected(self, version_name: str) -> None:
"""Обрабатывает выбор версии."""
self.selected_version = version_name
self.accept()
@@ -1461,7 +1462,7 @@ class WineHelperGUI(QMainWindow):
)
# Стиль для кнопок в списках
self.BUTTON_LIST_STYLE = """
self.BUTTON_LIST_STYLE: str = """
QPushButton {
text-align: left;
padding-left: 10px;
@@ -1473,12 +1474,12 @@ class WineHelperGUI(QMainWindow):
}
"""
self.INSTALLED_BUTTON_LIST_STYLE = self.BUTTON_LIST_STYLE.replace(
self.INSTALLED_BUTTON_LIST_STYLE: str = self.BUTTON_LIST_STYLE.replace(
"padding-left: 10px;", "padding-left: 15px;"
)
# Стиль для кнопок тестовых программ
self.TEST_BUTTON_LIST_STYLE = """
self.TEST_BUTTON_LIST_STYLE: str = """
QPushButton {
background-color: #ffdc64; /* Более темный желтый фон */
color: black; /* Черный цвет текста для контраста */
@@ -1491,40 +1492,40 @@ class WineHelperGUI(QMainWindow):
"""
# Стили для оберток кнопок (для рамки выделения)
self.FRAME_STYLE_DEFAULT = "QFrame { border: 2px solid transparent; border-radius: 8px; padding: 0px; }"
self.FRAME_STYLE_SELECTED = "QFrame { border: 2px solid #0078d7; border-radius: 8px; padding: 0px; }"
self.FRAME_STYLE_DEFAULT: str = "QFrame { border: 2px solid transparent; border-radius: 8px; padding: 0px; }"
self.FRAME_STYLE_SELECTED: str = "QFrame { border: 2px solid #0078d7; border-radius: 8px; padding: 0px; }"
# Стили для кнопок Запустить/Остановить
self.RUN_BUTTON_STYLE = """
self.RUN_BUTTON_STYLE: str = """
QPushButton {
background-color: #4CAF50; color: white;
font-weight: bold;
}
"""
self.STOP_BUTTON_STYLE = """
self.STOP_BUTTON_STYLE: str = """
QPushButton { background-color: #d32f2f; color: white; font-weight: bold; }
"""
# Основные переменные
self.winehelper_path = Var.RUN_SCRIPT
self.process = None
self.current_script = None
self.install_process = None
self.current_display_name = None
self.install_dialog = None
self.current_active_button = None
self.installed_buttons = []
self.install_tabs_data = {}
self.running_apps = {} # {desktop_path: QProcess}
self.current_selected_app = None
self.icon_animators = {}
self.previous_tab_index = 0
self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке
self.prefixes_before_install = set()
self.winehelper_path: str = Var.RUN_SCRIPT
self.process: Optional[QProcess] = None
self.current_script: Optional[str] = None
self.install_process: Optional[QProcess] = None
self.current_display_name: Optional[str] = None
self.install_dialog: Optional[QDialog] = None
self.current_active_button: Optional[QPushButton] = None
self.installed_buttons: List[QPushButton] = []
self.install_tabs_data: Dict[str, Dict[str, Any]] = {}
self.running_apps: Dict[str, QProcess] = {} # {desktop_path: QProcess}
self.current_selected_app: Optional[Dict[str, Any]] = None
self.icon_animators: Dict[QPushButton, Dict[str, Any]] = {}
self.previous_tab_index: int = 0
self.current_managed_prefix_name: Optional[str] = None # Имя префикса, выбранного в выпадающем списке
self.prefixes_before_install: Set[str] = set()
self.is_quitting = False # Флаг для корректного выхода из приложения
self.command_output_buffer = ""
self.command_last_line_was_progress = False
self.is_quitting: bool = False # Флаг для корректного выхода из приложения
self.command_output_buffer: str = ""
self.command_last_line_was_progress: bool = False
# Создаем главный виджет и layout
self.main_widget = QWidget()
self.setCentralWidget(self.main_widget)
@@ -1998,7 +1999,7 @@ class WineHelperGUI(QMainWindow):
button_list.append(btn)
button_index += 1
def _create_searchable_grid_tab(self, placeholder_text, filter_slot, add_stretch=True):
def _create_searchable_grid_tab(self, placeholder_text: str, filter_slot: Callable, add_stretch: bool = True) -> Tuple[QWidget, QGridLayout, QLineEdit, QScrollArea]:
"""
Создает стандартную вкладку с полем поиска и сеточным макетом с прокруткой.
Возвращает кортеж (главный виджет вкладки, сеточный макет, поле поиска, область прокрутки).
@@ -2057,7 +2058,7 @@ class WineHelperGUI(QMainWindow):
return tab_widget, grid_layout, search_edit, scroll_area
def _create_and_populate_install_tab(self, tab_title, script_folders, search_placeholder, filter_slot):
def _create_and_populate_install_tab(self, tab_title: str, script_folders: List[str], search_placeholder: str, filter_slot: Callable) -> Tuple[List[str], List[QPushButton], QGridLayout, QLineEdit, QScrollArea]:
"""
Создает и заполняет вкладку для установки (автоматической или ручной).
Возвращает кортеж со скриптами, кнопками и виджетами.
@@ -2066,10 +2067,10 @@ class WineHelperGUI(QMainWindow):
search_placeholder, filter_slot
)
scripts = []
buttons_list = []
scripts: List[str] = []
buttons_list: List[QPushButton] = []
for folder in script_folders:
script_path = os.path.join(Var.DATA_PATH, folder)
script_path = os.path.join(Var.DATA_PATH or "", folder)
if os.path.isdir(script_path):
try:
folder_scripts = sorted(os.listdir(script_path))
@@ -2082,7 +2083,7 @@ class WineHelperGUI(QMainWindow):
return scripts, buttons_list, grid_layout, search_edit, scroll_area
def create_auto_install_tab(self):
def create_auto_install_tab(self) -> None:
"""Создает вкладку для автоматической установки программ"""
(
scripts, buttons, layout,
@@ -2090,7 +2091,7 @@ class WineHelperGUI(QMainWindow):
) = self._create_and_populate_install_tab(
"Автоматическая установка", ["autoinstall"], "Поиск скрипта автоматической установки...", partial(self.filter_buttons, 'auto')
)
self.autoinstall_scripts = scripts
self.autoinstall_scripts: List[str] = scripts
self.install_tabs_data['auto'] = {
'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area
}
@@ -2111,7 +2112,7 @@ class WineHelperGUI(QMainWindow):
# Сохраняем чекбокс для доступа в будущем
self.install_tabs_data['auto']['test_checkbox'] = test_checkbox
def create_manual_install_tab(self):
def create_manual_install_tab(self) -> None:
"""Создает вкладку для ручной установки программ"""
(
scripts, buttons, layout,
@@ -2119,12 +2120,12 @@ class WineHelperGUI(QMainWindow):
) = self._create_and_populate_install_tab(
"Ручная установка", ["manualinstall"], "Поиск скрипта ручной установки...", partial(self.filter_buttons, 'manual')
)
self.manualinstall_scripts = scripts
self.manualinstall_scripts: List[str] = scripts
self.install_tabs_data['manual'] = {
'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area
}
def update_auto_install_list(self):
def update_auto_install_list(self) -> None:
"""Обновляет список на вкладке 'Автоматическая установка' при изменении чекбокса."""
data = self.install_tabs_data.get('auto')
if not data:
@@ -2136,12 +2137,12 @@ class WineHelperGUI(QMainWindow):
# Если нужно показать тестовые версии и они еще не добавлены
if is_checked and not test_buttons:
test_script_folder = "testinstall"
script_path = os.path.join(Var.DATA_PATH, test_script_folder)
script_path = os.path.join(Var.DATA_PATH or "", test_script_folder)
if os.path.isdir(script_path):
try:
folder_scripts = sorted(os.listdir(script_path))
# Запоминаем, какие кнопки являются тестовыми
new_test_buttons = []
new_test_buttons: List[QPushButton] = []
self._populate_install_grid(data['layout'], folder_scripts, test_script_folder, new_test_buttons)
data['test_buttons'] = new_test_buttons
data['buttons'].extend(new_test_buttons)
@@ -2187,20 +2188,20 @@ class WineHelperGUI(QMainWindow):
# Очищаем список тестовых кнопок
data['test_buttons'].clear()
# Обновляем список скриптов
self.autoinstall_scripts = [s for s in self.autoinstall_scripts if not os.path.exists(os.path.join(Var.DATA_PATH, "testinstall", s))]
self.autoinstall_scripts = [s for s in self.autoinstall_scripts if not os.path.exists(os.path.join(Var.DATA_PATH or "", "testinstall", s))]
# В любом случае применяем фильтр, чтобы скрыть/показать кнопки в соответствии с поиском
if data['test_checkbox'].isChecked():
self.filter_buttons('auto')
def create_installed_tab(self):
def create_installed_tab(self) -> None:
"""Создает вкладку для отображения установленных программ в виде кнопок"""
installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab(
"Поиск установленной программы...", self.filter_installed_buttons, add_stretch=True
)
self.add_tab(installed_tab, "Установленные")
def create_prefix_tab(self):
def create_prefix_tab(self) -> None:
"""Создает вкладку 'Менеджер префиксов'"""
self.prefix_tab = QWidget()
layout = QVBoxLayout(self.prefix_tab)