feat(wine settings): make winetricks work
All checks were successful
Code check / Check code (push) Successful in 1m10s
All checks were successful
Code check / Check code (push) Successful in 1m10s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -2,11 +2,13 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import re
|
import re
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from PySide6.QtGui import QPixmap, QIcon
|
from PySide6.QtGui import QPixmap, QIcon, QTextCursor
|
||||||
from PySide6.QtWidgets import (
|
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
|
||||||
)
|
)
|
||||||
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 icoextract import IconExtractor, IconExtractorError
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
|
||||||
@@ -977,3 +979,379 @@ Icon={icon_path}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return desktop_entry, desktop_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.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.tab_widget.addTab(self.dll_table, _("DLLs"))
|
||||||
|
|
||||||
|
# Fonts tab
|
||||||
|
self.fonts_table = QTableWidget()
|
||||||
|
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.tab_widget.addTab(self.fonts_table, _("Fonts"))
|
||||||
|
|
||||||
|
# Settings tab
|
||||||
|
self.settings_table = QTableWidget()
|
||||||
|
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.tab_widget.addTab(self.settings_table, _("Settings"))
|
||||||
|
|
||||||
|
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"), 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)
|
||||||
|
if self.wine_use is not None:
|
||||||
|
env.insert("WINE", self.wine_use)
|
||||||
|
|
||||||
|
cwd = os.path.dirname(self.winetricks_path)
|
||||||
|
|
||||||
|
# DLLs
|
||||||
|
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
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}")
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||||
|
logger.error(f"Failed to list {category}: {error_output}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
QMessageBox.information(self, _("Info"), _("No components selected."))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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, _("Info"), _("No new 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:
|
||||||
|
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:
|
||||||
|
with open(self.log_path, 'a') as f:
|
||||||
|
for name in selected:
|
||||||
|
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)
|
||||||
|
@@ -7,7 +7,7 @@ import sys
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from portprotonqt.logger import get_logger
|
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.game_card import GameCard
|
||||||
from portprotonqt.animations import DetailPageAnimations
|
from portprotonqt.animations import DetailPageAnimations
|
||||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
|
||||||
@@ -1079,7 +1079,7 @@ class MainWindow(QMainWindow):
|
|||||||
additional_grid.setSpacing(6)
|
additional_grid.setSpacing(6)
|
||||||
|
|
||||||
additional_buttons = [
|
additional_buttons = [
|
||||||
("Winetricks", None),
|
("Winetricks", self.open_winetricks),
|
||||||
(_("Create Prefix Backup"), self.create_prefix_backup),
|
(_("Create Prefix Backup"), self.create_prefix_backup),
|
||||||
(_("Load Prefix Backup"), self.load_prefix_backup),
|
(_("Load Prefix Backup"), self.load_prefix_backup),
|
||||||
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
(_("Delete Compatibility Tool"), self.delete_compat_tool),
|
||||||
@@ -1226,6 +1226,24 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to delete compatibility tool: {}").format(str(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):
|
def createPortProtonTab(self):
|
||||||
"""Вкладка 'PortProton Settings'."""
|
"""Вкладка 'PortProton Settings'."""
|
||||||
self.portProtonWidget = QWidget()
|
self.portProtonWidget = QWidget()
|
||||||
|
@@ -916,6 +916,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"""
|
FILE_EXPLORER_STYLE = f"""
|
||||||
QListView {{
|
QListView {{
|
||||||
font-size: {font_size_a};
|
font-size: {font_size_a};
|
||||||
|
Reference in New Issue
Block a user