forked from Boria138/PortProtonQt
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4a758f3b3c
|
|||
|
0853dd1579
|
|||
|
bbb87c0455
|
|||
|
b32a71a125
|
|||
|
|
bddf9f850a | ||
|
|
a9c3cfa167 | ||
|
7675bc4cdc
|
|||
|
ffa203f019
|
|||
|
3eed25ecee
|
@@ -11,12 +11,12 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
rev: 0.8.22
|
rev: 0.9.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.1
|
rev: v0.14.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
|
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location
|
from portprotonqt.config_utils import (
|
||||||
|
save_fullscreen_config,
|
||||||
|
read_fullscreen_config,
|
||||||
|
get_portproton_location,
|
||||||
|
)
|
||||||
from portprotonqt.logger import get_logger, setup_logger
|
from portprotonqt.logger import get_logger, setup_logger
|
||||||
from portprotonqt.cli import parse_args
|
from portprotonqt.cli import parse_args
|
||||||
|
|
||||||
@@ -16,25 +22,23 @@ __app_version__ = "0.1.8"
|
|||||||
def get_version():
|
def get_version():
|
||||||
try:
|
try:
|
||||||
commit = subprocess.check_output(
|
commit = subprocess.check_output(
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
stderr=subprocess.DEVNULL
|
stderr=subprocess.DEVNULL,
|
||||||
).decode('utf-8').strip()
|
).decode("utf-8").strip()
|
||||||
return f"{__app_version__} ({commit})"
|
return f"{__app_version__} ({commit})"
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||||
return __app_version__
|
return __app_version__
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ['PW_CLI'] = '1'
|
os.environ["PW_CLI"] = "1"
|
||||||
os.environ['PROCESS_LOG'] = '1'
|
os.environ["PROCESS_LOG"] = "1"
|
||||||
os.environ['START_FROM_STEAM'] = '1'
|
os.environ["START_FROM_STEAM"] = "1"
|
||||||
|
|
||||||
portproton_path = get_portproton_location()
|
portproton_path, start_sh = get_portproton_location()
|
||||||
|
if portproton_path is None or start_sh is None:
|
||||||
if portproton_path is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
subprocess.run(start_sh + ["cli", "--initial"])
|
||||||
subprocess.run([script_path, 'cli', '--initial'])
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||||
@@ -43,41 +47,116 @@ def main():
|
|||||||
app.setApplicationVersion(__app_version__)
|
app.setApplicationVersion(__app_version__)
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
# Setup logger with specified debug level
|
|
||||||
setup_logger(args.debug_level)
|
setup_logger(args.debug_level)
|
||||||
|
|
||||||
# Reinitialize logger after setup to ensure it uses the new configuration
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# --- Single-instance logic ---
|
||||||
|
server_name = __app_id__
|
||||||
|
socket = QLocalSocket()
|
||||||
|
socket.connectToServer(server_name)
|
||||||
|
|
||||||
|
if socket.waitForConnected(200):
|
||||||
|
# Второй экземпляр — передаём команду первому
|
||||||
|
fullscreen = args.fullscreen or read_fullscreen_config()
|
||||||
|
msg = b"show:fullscreen" if fullscreen else b"show"
|
||||||
|
socket.write(msg)
|
||||||
|
socket.flush()
|
||||||
|
socket.waitForBytesWritten(500)
|
||||||
|
socket.disconnectFromServer()
|
||||||
|
logger.info("Restored existing instance from tray")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Если старый сокет остался — удалить
|
||||||
|
QLocalServer.removeServer(server_name)
|
||||||
|
|
||||||
|
local_server = QLocalServer()
|
||||||
|
if not local_server.listen(server_name):
|
||||||
|
logger.warning(f"Failed to start local server: {local_server.errorString()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Qt translations ---
|
||||||
system_locale = QLocale.system()
|
system_locale = QLocale.system()
|
||||||
qt_translator = QTranslator()
|
qt_translator = QTranslator()
|
||||||
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||||
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
||||||
app.installTranslator(qt_translator)
|
app.installTranslator(qt_translator)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
|
logger.warning(
|
||||||
|
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Main Window ---
|
||||||
version = get_version()
|
version = get_version()
|
||||||
window = MainWindow(app_name=__app_name__, version=version)
|
window = MainWindow(app_name=__app_name__, version=version)
|
||||||
|
|
||||||
if args.fullscreen:
|
# --- Handle incoming connections ---
|
||||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
def handle_new_connection():
|
||||||
|
conn = local_server.nextPendingConnection()
|
||||||
|
if not conn:
|
||||||
|
return
|
||||||
|
|
||||||
|
if conn.waitForReadyRead(1000):
|
||||||
|
data = conn.readAll().data()
|
||||||
|
msg = bytes(data).decode("utf-8", errors="ignore")
|
||||||
|
logger.info(f"IPC message received: {msg}")
|
||||||
|
|
||||||
|
def restore_window():
|
||||||
|
try:
|
||||||
|
if msg.startswith("show"):
|
||||||
|
if hasattr(window, "restore_from_tray"):
|
||||||
|
window.restore_from_tray() # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
window.showNormal()
|
||||||
|
window.raise_()
|
||||||
|
window.activateWindow()
|
||||||
|
window.setWindowState(
|
||||||
|
window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive
|
||||||
|
)
|
||||||
|
|
||||||
|
if ":fullscreen" in msg:
|
||||||
|
logger.info("Switching to fullscreen via IPC")
|
||||||
|
save_fullscreen_config(True)
|
||||||
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
logger.info("Switching to normal window via IPC")
|
||||||
|
save_fullscreen_config(False)
|
||||||
|
window.showNormal()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to restore window: {e}")
|
||||||
|
|
||||||
|
# Выполняем в основном потоке
|
||||||
|
QTimer.singleShot(0, restore_window)
|
||||||
|
|
||||||
|
conn.disconnectFromServer()
|
||||||
|
|
||||||
|
local_server.newConnection.connect(handle_new_connection)
|
||||||
|
|
||||||
|
# --- Initial fullscreen state ---
|
||||||
|
launch_fullscreen = args.fullscreen or read_fullscreen_config()
|
||||||
|
if launch_fullscreen:
|
||||||
|
logger.info(
|
||||||
|
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
|
||||||
|
)
|
||||||
save_fullscreen_config(True)
|
save_fullscreen_config(True)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
logger.info("Launching in normal mode")
|
||||||
|
save_fullscreen_config(False)
|
||||||
|
window.showNormal()
|
||||||
|
|
||||||
|
# --- Cleanup ---
|
||||||
def cleanup_on_exit():
|
def cleanup_on_exit():
|
||||||
nonlocal window
|
try:
|
||||||
app.aboutToQuit.disconnect()
|
local_server.close()
|
||||||
if window:
|
QLocalServer.removeServer(server_name)
|
||||||
window.close()
|
if window:
|
||||||
app.quit()
|
window.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Cleanup error: {e}")
|
||||||
|
|
||||||
app.aboutToQuit.connect(cleanup_on_exit)
|
app.aboutToQuit.connect(cleanup_on_exit)
|
||||||
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
_portproton_location = None
|
_portproton_location = None
|
||||||
|
_portproton_start_sh = None
|
||||||
|
|
||||||
# Paths to configuration files
|
# Paths to configuration files
|
||||||
CONFIG_FILE = os.path.join(
|
CONFIG_FILE = os.path.join(
|
||||||
@@ -101,33 +103,65 @@ def read_file_content(file_path):
|
|||||||
return f.read().strip()
|
return f.read().strip()
|
||||||
|
|
||||||
def get_portproton_location():
|
def get_portproton_location():
|
||||||
"""Returns the path to the PortProton directory.
|
"""Returns the path to the PortProton directory and start.sh command list.
|
||||||
|
|
||||||
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
|
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
|
||||||
If the path is invalid, uses the default directory.
|
If the path is invalid, uses the default flatpak directory.
|
||||||
|
If Flatpak package ru.linux_gaming.PortProton is installed, returns the Flatpak run command list.
|
||||||
"""
|
"""
|
||||||
global _portproton_location
|
global _portproton_location, _portproton_start_sh
|
||||||
if _portproton_location is not None:
|
|
||||||
return _portproton_location
|
if '_portproton_location' in globals() and _portproton_location is not None:
|
||||||
|
return _portproton_location, _portproton_start_sh
|
||||||
|
|
||||||
|
location = None
|
||||||
|
|
||||||
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
||||||
try:
|
try:
|
||||||
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
||||||
if location and os.path.isdir(location):
|
if location and os.path.isdir(location):
|
||||||
_portproton_location = location
|
|
||||||
logger.info(f"PortProton path from configuration: {location}")
|
logger.info(f"PortProton path from configuration: {location}")
|
||||||
return _portproton_location
|
else:
|
||||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path")
|
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
||||||
|
location = None
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path")
|
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
||||||
|
location = None
|
||||||
|
|
||||||
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||||
if os.path.isdir(default_dir):
|
is_flatpak_installed = False
|
||||||
_portproton_location = default_dir
|
try:
|
||||||
logger.info(f"Using flatpak PortProton directory: {default_dir}")
|
result = subprocess.run(
|
||||||
return _portproton_location
|
["flatpak", "list"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||||
|
is_flatpak_installed = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
logger.warning("PortProton configuration and flatpak directory not found")
|
if is_flatpak_installed:
|
||||||
return None
|
_portproton_location = default_flatpak_dir if os.path.isdir(default_flatpak_dir) else None
|
||||||
|
_portproton_start_sh = ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||||
|
logger.info("Detected Flatpak installation: using 'flatpak run ru.linux_gaming.PortProton'")
|
||||||
|
elif location:
|
||||||
|
_portproton_location = location
|
||||||
|
start_sh_path = os.path.join(location, "data", "scripts", "start.sh")
|
||||||
|
_portproton_start_sh = [start_sh_path] if os.path.exists(start_sh_path) else None
|
||||||
|
logger.info(f"Using PortProton installation from config path: {_portproton_location}")
|
||||||
|
elif os.path.isdir(default_flatpak_dir):
|
||||||
|
_portproton_location = default_flatpak_dir
|
||||||
|
start_sh_path = os.path.join(default_flatpak_dir, "data", "scripts", "start.sh")
|
||||||
|
_portproton_start_sh = [start_sh_path] if os.path.exists(start_sh_path) else None
|
||||||
|
logger.info(f"Using Flatpak PortProton directory: {_portproton_location}")
|
||||||
|
else:
|
||||||
|
logger.warning("PortProton configuration and Flatpak directory not found")
|
||||||
|
_portproton_location = None
|
||||||
|
_portproton_start_sh = None
|
||||||
|
|
||||||
|
return _portproton_location, _portproton_start_sh
|
||||||
|
|
||||||
def parse_desktop_entry(file_path):
|
def parse_desktop_entry(file_path):
|
||||||
"""Reads and parses a .desktop file using configparser.
|
"""Reads and parses a .desktop file using configparser.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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, QTextCursor
|
from PySide6.QtGui import QPixmap, QIcon, QTextCursor, QColor
|
||||||
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, QAbstractItemView, QStackedWidget
|
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
|
||||||
@@ -1674,3 +1674,308 @@ class WinetricksDialog(QDialog):
|
|||||||
if self.input_manager:
|
if self.input_manager:
|
||||||
self.input_manager.disable_winetricks_mode()
|
self.input_manager.disable_winetricks_mode()
|
||||||
super().reject()
|
super().reject()
|
||||||
|
|
||||||
|
class ExeSettingsDialog(QDialog):
|
||||||
|
def __init__(self, parent=None, theme=None, exe_path=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
|
||||||
|
self.exe_path = exe_path
|
||||||
|
if not self.exe_path:
|
||||||
|
logger.error("Exe path not provided")
|
||||||
|
return
|
||||||
|
self.portproton_path = get_portproton_location()
|
||||||
|
if self.portproton_path is None:
|
||||||
|
logger.error("PortProton location not found")
|
||||||
|
return
|
||||||
|
base_path = os.path.join(self.portproton_path, "data")
|
||||||
|
self.start_sh = os.path.join(base_path, "scripts", "start.sh")
|
||||||
|
self.ppdb_path = self.exe_path + ".ppdb" if not self.exe_path.endswith('.ppdb') else self.exe_path
|
||||||
|
self.current_settings = {}
|
||||||
|
self.value_widgets = {}
|
||||||
|
self.original_values = {}
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
self.branch_name = _("Unknown")
|
||||||
|
|
||||||
|
self.setWindowTitle(_("Exe Settings"))
|
||||||
|
self.setModal(True)
|
||||||
|
self.resize(900, 600)
|
||||||
|
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
|
||||||
|
|
||||||
|
self.init_toggle_settings()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# Find input_manager and main_window
|
||||||
|
self.input_manager = None
|
||||||
|
self.main_window = None
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'input_manager'):
|
||||||
|
self.input_manager = cast("MainWindow", parent).input_manager
|
||||||
|
self.main_window = parent
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
|
# Create hints widget using common function
|
||||||
|
self.hints_widget, self.hints_labels = create_dialog_hints_widget(
|
||||||
|
self.theme, self.main_window, self.input_manager, context='winetricks'
|
||||||
|
)
|
||||||
|
self.main_layout.addWidget(self.hints_widget)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
if self.input_manager:
|
||||||
|
self.input_manager.button_event.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
self.input_manager.dpad_moved.connect(
|
||||||
|
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
)
|
||||||
|
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
|
||||||
|
|
||||||
|
# Load current settings (includes list-db)
|
||||||
|
self.load_current_settings()
|
||||||
|
|
||||||
|
def init_toggle_settings(self):
|
||||||
|
"""Initialize predefined toggle settings with descriptions."""
|
||||||
|
self.toggle_settings = {
|
||||||
|
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
|
||||||
|
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
|
||||||
|
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
|
||||||
|
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
|
||||||
|
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
|
||||||
|
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
|
||||||
|
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
|
||||||
|
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
|
||||||
|
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
|
||||||
|
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
|
||||||
|
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
|
||||||
|
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
|
||||||
|
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
|
||||||
|
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
|
||||||
|
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
|
||||||
|
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
|
||||||
|
'PW_USE_TERMINAL': _("Run the application in a terminal"),
|
||||||
|
'PW_GUI_DISABLED_CS': _("Disable startup mode and WINE version selector window"),
|
||||||
|
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
|
||||||
|
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
|
||||||
|
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
|
||||||
|
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
|
||||||
|
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
|
||||||
|
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
|
||||||
|
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
|
||||||
|
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
|
||||||
|
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
|
||||||
|
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
|
||||||
|
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
|
||||||
|
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
|
||||||
|
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
|
||||||
|
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
|
||||||
|
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
|
||||||
|
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
|
||||||
|
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
|
||||||
|
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
|
||||||
|
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
|
||||||
|
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
|
||||||
|
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Set up the user interface."""
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.main_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Метка с текущей веткой (STABLE / DEVEL)
|
||||||
|
self.branch_label = QLabel(_("Detected branch: Unknown"))
|
||||||
|
self.branch_label.setStyleSheet("font-weight: bold;")
|
||||||
|
self.main_layout.addWidget(self.branch_label)
|
||||||
|
|
||||||
|
# Таблица настроек
|
||||||
|
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([_("Setting"), _("Value"), _("Description")])
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.settings_table.horizontalHeader().resizeSection(1, 100)
|
||||||
|
self.settings_table.setWordWrap(True)
|
||||||
|
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
|
||||||
|
self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
|
||||||
|
self.main_layout.addWidget(self.settings_table)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.apply_button = AutoSizeButton(_("Apply"), icon=theme_manager.get_icon("apply"))
|
||||||
|
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
|
||||||
|
self.apply_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||||
|
button_layout.addWidget(self.apply_button)
|
||||||
|
button_layout.addWidget(self.cancel_button)
|
||||||
|
self.main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
self.apply_button.clicked.connect(self.apply_changes)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
def load_current_settings(self):
|
||||||
|
"""Load available toggles first, then current settings."""
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_list_db_finished)
|
||||||
|
process.start(self.start_sh, ["cli", "--list-db"])
|
||||||
|
|
||||||
|
def on_list_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --list-db output and extract available keys."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.available_keys = set()
|
||||||
|
self.blocked_keys = set()
|
||||||
|
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
|
||||||
|
for line in output.splitlines():
|
||||||
|
if "Branch in used:" in line:
|
||||||
|
self.branch_name = line.split(":", 1)[1].strip()
|
||||||
|
self.branch_label.setText(_("Detected branch: ") + self.branch_name)
|
||||||
|
continue
|
||||||
|
stripped_line = line.strip()
|
||||||
|
if stripped_line.startswith("PW_"):
|
||||||
|
parts = stripped_line.split(maxsplit=1)
|
||||||
|
key = parts[0]
|
||||||
|
self.available_keys.add(key)
|
||||||
|
if len(parts) > 1 and parts[1] == "blocked":
|
||||||
|
self.blocked_keys.add(key)
|
||||||
|
|
||||||
|
# Показываем только пересечение
|
||||||
|
self.available_keys &= set(self.toggle_settings.keys())
|
||||||
|
logger.debug(f"Filtered available keys (intersection): {self.available_keys}")
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to get --list-db output; showing all toggles")
|
||||||
|
self.available_keys = set(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
# Загружаем текущие настройки
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_show_ppdb_finished)
|
||||||
|
process.start(self.start_sh, ["cli", "--show-ppdb", self.ppdb_path])
|
||||||
|
|
||||||
|
def on_show_ppdb_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --show-ppdb output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
logger.warning("Failed to load settings, using defaults")
|
||||||
|
else:
|
||||||
|
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore').strip()
|
||||||
|
self.current_settings = {}
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if '=' in line and line.strip().startswith('PW_'):
|
||||||
|
key, val = line.split('=', 1)
|
||||||
|
self.current_settings[key.strip()] = val.strip()
|
||||||
|
logger.debug(f"Loaded current settings: {self.current_settings}")
|
||||||
|
|
||||||
|
# Force blocked settings to '0'
|
||||||
|
for key in self.blocked_keys:
|
||||||
|
self.current_settings[key] = '0'
|
||||||
|
|
||||||
|
self.populate_table()
|
||||||
|
|
||||||
|
def populate_table(self):
|
||||||
|
"""Populate the table with settings that are available in both lists."""
|
||||||
|
self.settings_table.setRowCount(0)
|
||||||
|
self.value_widgets.clear()
|
||||||
|
self.original_values.clear()
|
||||||
|
self.settings_table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
visible_keys = sorted(self.available_keys) if self.available_keys else sorted(self.toggle_settings.keys())
|
||||||
|
|
||||||
|
for toggle in visible_keys:
|
||||||
|
description = self.toggle_settings.get(toggle)
|
||||||
|
if not description:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = self.settings_table.rowCount()
|
||||||
|
self.settings_table.insertRow(row)
|
||||||
|
|
||||||
|
name_item = QTableWidgetItem(toggle)
|
||||||
|
name_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
|
||||||
|
current_val = self.current_settings.get(toggle, '0')
|
||||||
|
is_blocked = toggle in self.blocked_keys
|
||||||
|
checkbox = QTableWidgetItem()
|
||||||
|
checkbox.setFlags(checkbox.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
check_state = Qt.CheckState.Checked if current_val == '1' and not is_blocked else Qt.CheckState.Unchecked
|
||||||
|
checkbox.setCheckState(check_state)
|
||||||
|
checkbox.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
if is_blocked:
|
||||||
|
checkbox.setFlags(checkbox.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
checkbox.setBackground(QColor(240, 240, 240))
|
||||||
|
name_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 1, checkbox)
|
||||||
|
|
||||||
|
desc_item = QTableWidgetItem(description)
|
||||||
|
desc_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
desc_item.setToolTip(description)
|
||||||
|
desc_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
if is_blocked:
|
||||||
|
desc_item.setForeground(QColor(128, 128, 128))
|
||||||
|
self.settings_table.setItem(row, 2, desc_item)
|
||||||
|
|
||||||
|
self.settings_table.setItem(row, 0, name_item)
|
||||||
|
self.value_widgets[(row, 1)] = checkbox
|
||||||
|
self.original_values[toggle] = current_val
|
||||||
|
|
||||||
|
self.settings_table.resizeRowsToContents()
|
||||||
|
if self.settings_table.rowCount() > 0:
|
||||||
|
self.settings_table.setCurrentCell(0, 0)
|
||||||
|
self.settings_table.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
"""Apply changes by collecting diffs and running --edit-db."""
|
||||||
|
changes = []
|
||||||
|
for key, orig_val in self.original_values.items():
|
||||||
|
if key in self.blocked_keys:
|
||||||
|
continue # Skip blocked keys
|
||||||
|
row = -1
|
||||||
|
for r in range(self.settings_table.rowCount()):
|
||||||
|
item0 = self.settings_table.item(r, 0)
|
||||||
|
if item0 and item0.text() == key:
|
||||||
|
row = r
|
||||||
|
break
|
||||||
|
if row == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = self.settings_table.item(row, 1)
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_val = '1' if item.checkState() == Qt.CheckState.Checked else '0'
|
||||||
|
if new_val != orig_val:
|
||||||
|
changes.append(f"{key}={new_val}")
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
QMessageBox.information(self, _("Info"), _("No changes to apply."))
|
||||||
|
return
|
||||||
|
|
||||||
|
process = QProcess(self)
|
||||||
|
process.finished.connect(self.on_edit_db_finished)
|
||||||
|
args = ["cli", "--edit-db", self.exe_path] + changes
|
||||||
|
process.start(self.start_sh, args)
|
||||||
|
self.apply_button.setEnabled(False)
|
||||||
|
|
||||||
|
def on_edit_db_finished(self, exit_code, exit_status):
|
||||||
|
"""Handle --edit-db output."""
|
||||||
|
process = cast(QProcess, self.sender())
|
||||||
|
self.apply_button.setEnabled(True)
|
||||||
|
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
|
||||||
|
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to apply changes. Check logs."))
|
||||||
|
logger.error(f"Failed to apply changes: {error_output}")
|
||||||
|
else:
|
||||||
|
self.load_current_settings()
|
||||||
|
QMessageBox.information(self, _("Success"), _("Settings updated successfully."))
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
super().reject()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
||||||
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
|
||||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||||
@@ -404,6 +404,13 @@ class GameCard(QFrame):
|
|||||||
self.favoriteLabel.setText("☆")
|
self.favoriteLabel.setText("☆")
|
||||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||||
|
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'game_library_manager'):
|
||||||
|
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
|
||||||
|
break
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
def toggle_favorite(self):
|
def toggle_favorite(self):
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
if self.is_favorite:
|
if self.is_favorite:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import psutil
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
|
||||||
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, FlowLayout
|
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
|
||||||
@@ -73,7 +73,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.game_processes = []
|
self.game_processes = []
|
||||||
self.target_exe = None
|
self.target_exe = None
|
||||||
self.current_running_button = None
|
self.current_running_button = None
|
||||||
self.portproton_location = get_portproton_location()
|
self.portproton_location, self.start_sh = get_portproton_location()
|
||||||
|
|
||||||
self.game_library_manager = GameLibraryManager(self, self.theme, None)
|
self.game_library_manager = GameLibraryManager(self, self.theme, None)
|
||||||
|
|
||||||
@@ -458,11 +458,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.current_install_script = script_name
|
self.current_install_script = script_name
|
||||||
self.seen_progress = False
|
self.seen_progress = False
|
||||||
self.current_percent = 0.0
|
self.current_percent = 0.0
|
||||||
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else ""
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(start_sh):
|
if not start_sh:
|
||||||
self.installing = False
|
self.installing = False
|
||||||
return
|
return
|
||||||
cmd = [start_sh, "cli", "--autoinstall", script_name]
|
cmd = start_sh + ["cli", "--autoinstall", script_name]
|
||||||
self.install_process = QProcess(self)
|
self.install_process = QProcess(self)
|
||||||
self.install_process.finished.connect(self.on_install_finished)
|
self.install_process.finished.connect(self.on_install_finished)
|
||||||
self.install_process.errorOccurred.connect(self.on_install_error)
|
self.install_process.errorOccurred.connect(self.on_install_error)
|
||||||
@@ -1424,12 +1424,10 @@ class MainWindow(QMainWindow):
|
|||||||
prefix = self.prefixCombo.currentText()
|
prefix = self.prefixCombo.currentText()
|
||||||
if not wine or not prefix:
|
if not wine or not prefix:
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
return
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(start_sh):
|
cmd = start_sh + ["cli", cli_arg, wine, prefix]
|
||||||
return
|
|
||||||
cmd = [start_sh, "cli", cli_arg, wine, prefix]
|
|
||||||
|
|
||||||
# Показываем прогресс-бар перед запуском
|
# Показываем прогресс-бар перед запуском
|
||||||
self.wine_progress_bar.setVisible(True)
|
self.wine_progress_bar.setVisible(True)
|
||||||
@@ -1508,12 +1506,13 @@ class MainWindow(QMainWindow):
|
|||||||
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
||||||
|
|
||||||
def clear_prefix(self):
|
def clear_prefix(self):
|
||||||
"""Очистка префикса (позже удалить)."""
|
"""Очищает префикс"""
|
||||||
selected_prefix = self.prefixCombo.currentText()
|
selected_prefix = self.prefixCombo.currentText()
|
||||||
selected_wine = self.wineCombo.currentText()
|
selected_wine = self.wineCombo.currentText()
|
||||||
|
|
||||||
if not selected_prefix or not selected_wine:
|
if not selected_prefix or not selected_wine:
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
return
|
||||||
|
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@@ -1526,98 +1525,35 @@ class MainWindow(QMainWindow):
|
|||||||
if reply != QMessageBox.StandardButton.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
start_sh = self.start_sh
|
||||||
if not os.path.exists(prefix_dir):
|
|
||||||
|
self.wine_progress_bar.setVisible(True)
|
||||||
|
self.update_status_message.emit(_("Clearing prefix..."), 0)
|
||||||
|
|
||||||
|
self.clear_process = QProcess(self)
|
||||||
|
self.clear_process.finished.connect(lambda exitCode, exitStatus: self._on_clear_prefix_finished(exitCode))
|
||||||
|
self.clear_process.errorOccurred.connect(lambda error: self._on_clear_prefix_error(error))
|
||||||
|
cmd = start_sh + ["cli", "--clear_pfx", selected_wine, selected_prefix]
|
||||||
|
self.clear_process.start(cmd[0], cmd[1:])
|
||||||
|
|
||||||
|
if not self.clear_process.waitForStarted(5000):
|
||||||
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to start prefix clear process."))
|
||||||
return
|
return
|
||||||
|
|
||||||
success = True
|
def _on_clear_prefix_finished(self, exitCode):
|
||||||
errors = []
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
# Удаление файлов
|
if exitCode == 0:
|
||||||
files_to_remove = [
|
QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
|
||||||
os.path.join(prefix_dir, "*.dot*"),
|
|
||||||
os.path.join(prefix_dir, "*.prog*"),
|
|
||||||
os.path.join(prefix_dir, ".wine_ver"),
|
|
||||||
os.path.join(prefix_dir, "system.reg"),
|
|
||||||
os.path.join(prefix_dir, "user.reg"),
|
|
||||||
os.path.join(prefix_dir, "userdef.reg"),
|
|
||||||
os.path.join(prefix_dir, "winetricks.log"),
|
|
||||||
os.path.join(prefix_dir, ".update-timestamp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
import glob
|
|
||||||
for pattern in files_to_remove:
|
|
||||||
if "*" in pattern: # Глобальный паттерн
|
|
||||||
matches = glob.glob(pattern)
|
|
||||||
for file_path in matches:
|
|
||||||
try:
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
os.remove(file_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
else: # Конкретный файл
|
|
||||||
try:
|
|
||||||
if os.path.exists(pattern):
|
|
||||||
os.remove(pattern)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
# Удаление директорий
|
|
||||||
dirs_to_remove = [
|
|
||||||
os.path.join(prefix_dir, "drive_c", "windows"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
|
|
||||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
|
|
||||||
]
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
for dir_path in dirs_to_remove:
|
|
||||||
try:
|
|
||||||
if os.path.exists(dir_path):
|
|
||||||
shutil.rmtree(dir_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
|
|
||||||
if os.path.exists(tmp_path):
|
|
||||||
import glob
|
|
||||||
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
|
|
||||||
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
|
|
||||||
for file_path in bin_files + foz_files:
|
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
except Exception as e:
|
|
||||||
success = False
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
if success:
|
|
||||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
|
|
||||||
else:
|
else:
|
||||||
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5]))
|
QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
|
||||||
QMessageBox.warning(self, _("Warning"), error_msg)
|
|
||||||
|
def _on_clear_prefix_error(self, error):
|
||||||
|
self.wine_progress_bar.setVisible(False)
|
||||||
|
self.update_status_message.emit("", 0)
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Failed to run clear prefix command: {}").format(error))
|
||||||
|
|
||||||
def create_prefix_backup(self):
|
def create_prefix_backup(self):
|
||||||
selected_prefix = self.prefixCombo.currentText()
|
selected_prefix = self.prefixCombo.currentText()
|
||||||
@@ -1629,14 +1565,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _perform_backup(self, backup_dir, prefix_name):
|
def _perform_backup(self, backup_dir, prefix_name):
|
||||||
os.makedirs(backup_dir, exist_ok=True)
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
if not os.path.exists(start_sh):
|
|
||||||
return
|
return
|
||||||
|
start_sh = self.start_sh
|
||||||
self.backup_process = QProcess(self)
|
self.backup_process = QProcess(self)
|
||||||
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
||||||
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir]
|
cmd = start_sh + ["--backup-prefix", prefix_name, backup_dir]
|
||||||
self.backup_process.start(cmd[0], cmd[1:])
|
self.backup_process.start(cmd[0], cmd[1:])
|
||||||
if not self.backup_process.waitForStarted():
|
if not self.backup_process.waitForStarted():
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
||||||
@@ -1649,14 +1583,12 @@ class MainWindow(QMainWindow):
|
|||||||
def _perform_restore(self, file_path):
|
def _perform_restore(self, file_path):
|
||||||
if not file_path or not os.path.exists(file_path):
|
if not file_path or not os.path.exists(file_path):
|
||||||
return
|
return
|
||||||
if not self.portproton_location:
|
if not self.portproton_location or not self.start_sh:
|
||||||
return
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
if not os.path.exists(start_sh):
|
|
||||||
return
|
return
|
||||||
|
start_sh = self.start_sh
|
||||||
self.restore_process = QProcess(self)
|
self.restore_process = QProcess(self)
|
||||||
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
||||||
cmd = [start_sh, "--restore-prefix", file_path]
|
cmd = start_sh + ["--restore-prefix", file_path]
|
||||||
self.restore_process.start(cmd[0], cmd[1:])
|
self.restore_process.start(cmd[0], cmd[1:])
|
||||||
if not self.restore_process.waitForStarted():
|
if not self.restore_process.waitForStarted():
|
||||||
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
||||||
@@ -2326,6 +2258,14 @@ class MainWindow(QMainWindow):
|
|||||||
def darkenColor(self, color, factor=200):
|
def darkenColor(self, color, factor=200):
|
||||||
return color.darker(factor)
|
return color.darker(factor)
|
||||||
|
|
||||||
|
def open_exe_settings(self, exe_path):
|
||||||
|
"""Open the ExeSettingsDialog for the given executable."""
|
||||||
|
if not os.path.exists(exe_path):
|
||||||
|
QMessageBox.warning(self, _("Error"), _("Executable not found: {0}").format(exe_path))
|
||||||
|
return
|
||||||
|
dialog = ExeSettingsDialog(self, self.theme, exe_path)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
|
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
|
||||||
detailPage = QWidget()
|
detailPage = QWidget()
|
||||||
self._animations = {}
|
self._animations = {}
|
||||||
@@ -2628,8 +2568,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
clear_layout(hltbLayout)
|
clear_layout(hltbLayout)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
has_data = False
|
has_data = False
|
||||||
|
|
||||||
if main_story_time is not None:
|
if main_story_time is not None:
|
||||||
@@ -2713,6 +2651,14 @@ class MainWindow(QMainWindow):
|
|||||||
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
|
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
|
||||||
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
|
# Settings button
|
||||||
|
settings_icon = self.theme_manager.get_icon("settings")
|
||||||
|
settings_button = AutoSizeButton(_("Settings"), icon=settings_icon)
|
||||||
|
settings_button.setFixedSize(120, 40)
|
||||||
|
settings_button.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
|
||||||
|
settings_button.clicked.connect(lambda: self.open_exe_settings(file_to_check))
|
||||||
|
detailsLayout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
contentFrameLayout.addWidget(detailsWidget)
|
contentFrameLayout.addWidget(detailsWidget)
|
||||||
mainLayout.addStretch()
|
mainLayout.addStretch()
|
||||||
|
|
||||||
@@ -2935,10 +2881,7 @@ class MainWindow(QMainWindow):
|
|||||||
env_vars = os.environ.copy()
|
env_vars = os.environ.copy()
|
||||||
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
||||||
|
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = self.start_sh or ""
|
||||||
if self.portproton_location is not None and ".var" not in self.portproton_location:
|
|
||||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
|
||||||
wrapper = start_sh
|
|
||||||
|
|
||||||
cmd = [wrapper, game_exe]
|
cmd = [wrapper, game_exe]
|
||||||
|
|
||||||
@@ -3032,13 +2975,6 @@ class MainWindow(QMainWindow):
|
|||||||
exe_name = os.path.splitext(current_exe)[0]
|
exe_name = os.path.splitext(current_exe)[0]
|
||||||
env_vars = os.environ.copy()
|
env_vars = os.environ.copy()
|
||||||
|
|
||||||
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
|
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
|
||||||
env_vars['PROCESS_LOG'] = '1'
|
|
||||||
elif entry_exec_split[0] == "flatpak":
|
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
|
||||||
env_vars['PROCESS_LOG'] = '1'
|
|
||||||
|
|
||||||
# Запускаем игру
|
# Запускаем игру
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class PortProtonAPI:
|
|||||||
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||||
self.portproton_location = get_portproton_location()
|
self.portproton_location, self.portproton_start_sh = get_portproton_location()
|
||||||
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
||||||
self._topics_data = None
|
self._topics_data = None
|
||||||
|
|||||||
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.0005 1c-0.38761 0-0.77522 0.0327-1.1588 0.0979-0.16351 0.0281-0.30273 0.13627-0.37209 0.28935l-0.39088 0.86264c-0.49378 0.16682-0.96454 0.39759-1.4007 0.68616 2.5e-4 0-0.90672-0.2272-0.90672-0.2272-0.161-0.0403-0.33098 3e-3 -0.45442 0.11569-0.57867 0.5285-1.0672 1.1514-1.4451 1.8432-0.0804 0.14721-0.0841 0.32549-0.01 0.47628l0.41938 0.84865c-0.17954 0.49666-0.29567 1.0147-0.346 1.5417l-0.73995 0.57946c-0.13121 0.10289-0.20407 0.26514-0.19431 0.4335 0.0453 0.78981 0.21961 1.5666 0.51558 2.2983 0.0631 0.15587 0.1978 0.27003 0.36005 0.30467l0.91397 0.19559c0.26993 0.45234 0.59572 0.86802 0.96931 1.2363l-0.0161 0.94973c-3e-3 0.16861 0.0766 0.32755 0.21183 0.42484 0.63551 0.45642 1.3414 0.80207 2.0884 1.0229 0.15926 0.0471 0.33077 0.0109 0.45872-0.0963l0.72016-0.60485c0.51582 0.0674 1.0384 0.0674 1.5544 0l0.72016 0.60485c0.12796 0.10722 0.29946 0.14343 0.45872 0.0963 0.74693-0.22083 1.4528-0.56648 2.0883-1.0229 0.13521-0.0973 0.21465-0.25623 0.21189-0.42484l-0.0161-0.94973c0.37359-0.36829 0.69939-0.78372 0.96932-1.2363l0.91396-0.19559c0.16226-0.0347 0.29695-0.1488 0.36005-0.30467 0.29597-0.73174 0.47026-1.5085 0.51558-2.2983 0.01-0.16836-0.0631-0.33061-0.1943-0.4335l-0.73996-0.57946c-0.0501-0.52671-0.16652-1.045-0.34606-1.5417l0.41944-0.84865c0.0746-0.15079 0.0709-0.32907-0.01-0.47628-0.37785-0.69176-0.86638-1.3147-1.445-1.8432-0.12345-0.11258-0.29343-0.15594-0.45443-0.11569l-0.90697 0.2272c-0.43594-0.28857-0.9067-0.51908-1.4005-0.68616l-0.39088-0.86264c-0.0694-0.15308-0.20858-0.26132-0.37209-0.28935-0.38361-0.0653-0.77121-0.0979-1.1588-0.0979zm0 4.1365a2.8152 2.8635 0 0 1 2.8152 2.8636 2.8152 2.8635 0 0 1-2.8152 2.8635 2.8152 2.8635 0 0 1-2.8152-2.8635 2.8152 2.8635 0 0 1 2.8152-2.8636z" fill="#fff" stroke-width=".25254"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user