forked from Boria138/PortProtonQt
feat(wine settings): initial introdouce
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -5,6 +5,9 @@ import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import psutil
|
||||
import tarfile
|
||||
import glob
|
||||
import re
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||
@@ -1005,14 +1008,335 @@ 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)
|
||||
# Путь к дистрибутивам Wine/Proton
|
||||
if self.portproton_location is None:
|
||||
content = QLabel(_("PortProton location not set"))
|
||||
content.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
content.setObjectName("tabContent")
|
||||
layout.addWidget(content)
|
||||
layout.addStretch(1)
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
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):
|
||||
content = QLabel(_("PortProton data/dist not found"))
|
||||
content.setStyleSheet(self.theme.CONTENT_STYLE)
|
||||
content.setObjectName("tabContent")
|
||||
layout.addWidget(content)
|
||||
layout.addStretch(1)
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
return
|
||||
|
||||
# Форма с настройками
|
||||
formLayout = QFormLayout()
|
||||
formLayout.setContentsMargins(0, 10, 0, 0)
|
||||
formLayout.setSpacing(10)
|
||||
formLayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Выбор версии Wine/Proton
|
||||
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(_("Wine/Proton Version:"))
|
||||
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(_("Wine 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
|
||||
toolsLayout = QHBoxLayout()
|
||||
toolsLayout.setSpacing(10)
|
||||
|
||||
tools = [
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("control", _("Control Panel")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("explorer", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
]
|
||||
|
||||
for tool_cmd, tool_name in tools:
|
||||
btn = AutoSizeButton(tool_name)
|
||||
btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
btn.clicked.connect(lambda checked, t=tool_cmd: self.run_wine_tool(t))
|
||||
toolsLayout.addWidget(btn)
|
||||
|
||||
layout.addLayout(toolsLayout)
|
||||
layout.addStretch(1)
|
||||
|
||||
self.stackedWidget.addWidget(self.wineWidget)
|
||||
|
||||
def prepare_wine(self, version):
|
||||
"""Подготавливает окружение Wine/Proton для выбранной версии."""
|
||||
if not version:
|
||||
return
|
||||
|
||||
if self.portproton_location is None:
|
||||
logger.warning("PortProton location not set")
|
||||
return
|
||||
|
||||
dist_path = os.path.join(self.portproton_location, "data", "dist")
|
||||
winendir = os.path.join(dist_path, version)
|
||||
if not os.path.exists(winendir):
|
||||
logger.warning(f"Wine directory not found: {winendir}")
|
||||
return
|
||||
|
||||
files_dir = os.path.join(winendir, "files")
|
||||
dist_dir = os.path.join(winendir, "dist")
|
||||
proton_tar = os.path.join(winendir, "proton_dist.tar")
|
||||
|
||||
if os.path.isdir(files_dir) and not os.path.isdir(dist_dir):
|
||||
for item in os.listdir(winendir):
|
||||
if item not in ["files", "version"]:
|
||||
item_path = os.path.join(winendir, item)
|
||||
if os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
os.remove(item_path)
|
||||
if os.path.exists(files_dir):
|
||||
for item in os.listdir(files_dir):
|
||||
shutil.move(os.path.join(files_dir, item), winendir)
|
||||
os.rmdir(files_dir)
|
||||
elif not os.path.isdir(files_dir) and os.path.isdir(dist_dir):
|
||||
for item in os.listdir(winendir):
|
||||
if item not in ["dist", "version"]:
|
||||
item_path = os.path.join(winendir, item)
|
||||
if os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
os.remove(item_path)
|
||||
if os.path.exists(dist_dir):
|
||||
for item in os.listdir(dist_dir):
|
||||
shutil.move(os.path.join(dist_dir, item), winendir)
|
||||
os.rmdir(dist_dir)
|
||||
elif os.path.isfile(proton_tar):
|
||||
with tarfile.open(proton_tar) as tar:
|
||||
tar.extractall(winendir)
|
||||
os.remove(proton_tar)
|
||||
for item in os.listdir(winendir):
|
||||
if item not in ["bin", "lib", "lib64", "share", "version"]:
|
||||
item_path = os.path.join(winendir, item)
|
||||
if os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
os.remove(item_path)
|
||||
|
||||
if os.path.exists(winendir):
|
||||
# Создать файл version
|
||||
version_file = os.path.join(winendir, "version")
|
||||
if not os.path.exists(version_file):
|
||||
with open(version_file, "w") as f:
|
||||
f.write(version)
|
||||
|
||||
# Симлинк lib64/wine
|
||||
lib_wine = os.path.join(winendir, "lib", "wine", "x86_64-unix")
|
||||
lib64_wine = os.path.join(winendir, "lib64", "wine")
|
||||
if not os.path.lexists(lib64_wine) and os.path.exists(lib_wine):
|
||||
os.makedirs(os.path.join(winendir, "lib64"), exist_ok=True)
|
||||
self.safe_symlink(os.path.join(winendir, "lib", "wine"), lib64_wine)
|
||||
|
||||
# Обработка mono и gecko
|
||||
tmp_path = os.path.join(self.portproton_location, "tmp")
|
||||
os.makedirs(tmp_path, exist_ok=True)
|
||||
for component in ["mono", "gecko"]:
|
||||
share_wine_comp = os.path.join(winendir, "share", "wine", component)
|
||||
tmp_comp = os.path.join(tmp_path, component)
|
||||
if os.path.lexists(share_wine_comp) and os.path.islink(share_wine_comp):
|
||||
logger.info(f"{share_wine_comp} is symlink. OK.")
|
||||
elif os.path.isdir(share_wine_comp):
|
||||
self.safe_copytree(share_wine_comp, tmp_comp)
|
||||
self.safe_rmtree(share_wine_comp)
|
||||
self.safe_symlink(tmp_comp, share_wine_comp)
|
||||
logger.info(f"Copied {component} to tmp and created symlink. OK.")
|
||||
else:
|
||||
self.safe_rmtree(share_wine_comp)
|
||||
if os.path.exists(tmp_comp):
|
||||
self.safe_symlink(tmp_comp, share_wine_comp)
|
||||
logger.warning(f"{share_wine_comp} is broken symlink. Repaired.")
|
||||
|
||||
# Модификация wine.inf
|
||||
wine_inf = os.path.join(winendir, "share", "wine", "wine.inf")
|
||||
if os.path.exists(wine_inf):
|
||||
with open(wine_inf) as f:
|
||||
lines = f.readlines()
|
||||
|
||||
nvidia_uuid = 'Global,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001'
|
||||
has_nvidia = any(nvidia_uuid in line for line in lines)
|
||||
if not has_nvidia:
|
||||
lines.append('HKLM,Software\\NVIDIA Corporation\\Global,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001\n')
|
||||
lines.append('HKLM,System\\ControlSet001\\Services\\nvlddmkm,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001\n')
|
||||
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if 'Steam.exe' in line or r'\\Valve\\Steam' in line or 'winemenubuilder' in line:
|
||||
continue
|
||||
new_lines.append(line)
|
||||
lines = new_lines
|
||||
|
||||
with open(wine_inf, "w") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
# Удаление steam и winemenubuilder файлов
|
||||
for libdir in ["lib", "lib64"]:
|
||||
lib_path = os.path.join(winendir, libdir)
|
||||
if os.path.exists(lib_path):
|
||||
# *steam*
|
||||
for pattern in [
|
||||
os.path.join(lib_path, "*steam*"),
|
||||
os.path.join(lib_path, "wine", "*", "*steam*"),
|
||||
os.path.join(lib_path, "wine", "*-windows", "winemenubuilder.exe")
|
||||
]:
|
||||
for file_path in glob.glob(pattern, recursive=True):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def safe_symlink(self, src, dst):
|
||||
"""Создает симлинк, удаляя dst если существует."""
|
||||
if os.path.exists(dst):
|
||||
if os.path.islink(dst):
|
||||
os.remove(dst)
|
||||
else:
|
||||
shutil.rmtree(dst)
|
||||
os.symlink(src, dst)
|
||||
|
||||
def safe_copytree(self, src, dst):
|
||||
"""Копирует директорию, удаляя dst если существует."""
|
||||
if os.path.exists(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
|
||||
def safe_rmtree(self, path):
|
||||
"""Удаляет директорию если существует."""
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
def clean_wine_dist_dirs(self):
|
||||
"""Normalizes Wine dist directory names to uppercase with underscores."""
|
||||
if self.portproton_location is None:
|
||||
return
|
||||
|
||||
dist_path = os.path.join(self.portproton_location, "data", "dist")
|
||||
if not os.path.exists(dist_path):
|
||||
return
|
||||
for entry in os.scandir(dist_path):
|
||||
if entry.is_dir():
|
||||
dist_dir = entry.name
|
||||
dist_dir_stripped = re.sub(r'\s+', ' ', dist_dir.strip())
|
||||
dist_dir_new = dist_dir_stripped.replace(' ', '_').upper()
|
||||
if dist_dir_new != dist_dir:
|
||||
new_path = os.path.join(dist_path, dist_dir_new)
|
||||
if not os.path.exists(new_path):
|
||||
try:
|
||||
os.rename(entry.path, new_path)
|
||||
logger.info(f"Renamed {dist_dir} to {dist_dir_new}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to rename {dist_dir} to {dist_dir_new}: {e}")
|
||||
|
||||
def run_wine_tool(self, tool_cmd: str):
|
||||
"""Запускает инструмент Wine с выбранной версией и префиксом."""
|
||||
version = self.wineCombo.currentText()
|
||||
prefix = self.prefixCombo.currentText()
|
||||
|
||||
if not version:
|
||||
QMessageBox.warning(self, _("Error"), _("Please select a Wine/Proton version"))
|
||||
return
|
||||
|
||||
if not prefix:
|
||||
QMessageBox.warning(self, _("Error"), _("Please select a prefix"))
|
||||
return
|
||||
|
||||
if self.portproton_location is None:
|
||||
QMessageBox.warning(self, _("Error"), _("PortProton location not set"))
|
||||
return
|
||||
|
||||
# Clean and normalize dist directories
|
||||
self.clean_wine_dist_dirs()
|
||||
|
||||
# Repopulate wineCombo with normalized names
|
||||
dist_path = os.path.join(self.portproton_location, "data", "dist")
|
||||
self.wine_versions = sorted([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))])
|
||||
self.wineCombo.clear()
|
||||
self.wineCombo.addItems(self.wine_versions)
|
||||
|
||||
# Try to select the normalized original version
|
||||
version_normalized = version.strip().replace(' ', '_').upper()
|
||||
index = self.wineCombo.findText(version_normalized)
|
||||
if index != -1:
|
||||
self.wineCombo.setCurrentIndex(index)
|
||||
version = version_normalized
|
||||
elif self.wine_versions:
|
||||
self.wineCombo.setCurrentIndex(0)
|
||||
version = self.wine_versions[0]
|
||||
else:
|
||||
QMessageBox.warning(self, _("Error"), _("No Wine versions found after cleaning"))
|
||||
return
|
||||
|
||||
# Prepare Wine for the (possibly updated) version
|
||||
self.prepare_wine(version)
|
||||
|
||||
prefixes_path = os.path.join(self.portproton_location, "data", "prefixes")
|
||||
winendir = os.path.join(dist_path, version)
|
||||
wine_bin = os.path.join(winendir, "bin", "wine")
|
||||
wineserver_bin = os.path.join(winendir, "bin", "wineserver")
|
||||
|
||||
if not os.path.exists(wine_bin):
|
||||
QMessageBox.warning(self, _("Error"), _("Wine binary not found: {}").format(wine_bin))
|
||||
return
|
||||
|
||||
prefix_dir = os.path.join(prefixes_path, prefix)
|
||||
if not os.path.exists(prefix_dir):
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix not found: {}").format(prefix_dir))
|
||||
return
|
||||
|
||||
env = os.environ.copy()
|
||||
env['WINEPREFIX'] = prefix_dir
|
||||
env['WINEDIR'] = winendir
|
||||
env['WINE'] = wine_bin
|
||||
env['WINELOADER'] = wine_bin
|
||||
env['WINESERVER'] = wineserver_bin
|
||||
env['WINEDEBUG'] = '-all'
|
||||
env['WINEDLLOVERRIDES'] = "steam_api,steam_api64,steamclient,steamclient64=n;dotnetfx35.exe,dotnetfx35setup.exe=b;winemenubuilder.exe=;mscoree="
|
||||
|
||||
try:
|
||||
if tool_cmd == "cmd":
|
||||
# Open Command Prompt in a separate terminal
|
||||
term_cmd = ["x-terminal-emulator", "-e", wine_bin, tool_cmd]
|
||||
subprocess.Popen(term_cmd, env=env, start_new_session=True)
|
||||
else:
|
||||
cmd = [wine_bin, tool_cmd]
|
||||
subprocess.Popen(cmd, env=env, start_new_session=True)
|
||||
self.statusBar().showMessage(_("Launched {} for prefix {}").format(tool_cmd, prefix), 3000)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to launch {tool_cmd}: {e}")
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to launch {}: {}").format(tool_cmd, str(e)))
|
||||
|
||||
def createPortProtonTab(self):
|
||||
"""Вкладка 'PortProton Settings'."""
|
||||
self.portProtonWidget = QWidget()
|
||||
|
Reference in New Issue
Block a user