diff --git a/bottles/backend/dlls/dll.py b/bottles/backend/dlls/dll.py index ab7563f2ea..7320173b5b 100644 --- a/bottles/backend/dlls/dll.py +++ b/bottles/backend/dlls/dll.py @@ -45,6 +45,11 @@ def __init__(self, version: str): def get_base_path(version: str) -> str: pass + @staticmethod + @abstractmethod + def get_override_keys() -> str: + pass + def check(self) -> bool: found = deepcopy(self.dlls) diff --git a/bottles/backend/dlls/dxvk.py b/bottles/backend/dlls/dxvk.py index b4beb54b23..bebd357a05 100644 --- a/bottles/backend/dlls/dxvk.py +++ b/bottles/backend/dlls/dxvk.py @@ -35,6 +35,10 @@ class DXVKComponent(DLLComponent): ] } + @staticmethod + def get_override_keys() -> str: + return "d3d9,d3d10core,d3d11,dxgi" + @staticmethod def get_base_path(version: str) -> str: return ManagerUtils.get_dxvk_path(version) diff --git a/bottles/backend/dlls/latencyflex.py b/bottles/backend/dlls/latencyflex.py index 87c5fb0f93..447b2da6f6 100644 --- a/bottles/backend/dlls/latencyflex.py +++ b/bottles/backend/dlls/latencyflex.py @@ -27,6 +27,10 @@ class LatencyFleXComponent(DLLComponent): ] } + @staticmethod + def get_override_keys() -> str: + return "latencyflex_layer,latencyflex_wine" + @staticmethod def get_base_path(version: str) -> str: return ManagerUtils.get_latencyflex_path(version) diff --git a/bottles/backend/dlls/nvapi.py b/bottles/backend/dlls/nvapi.py index af630f001e..85fa298892 100644 --- a/bottles/backend/dlls/nvapi.py +++ b/bottles/backend/dlls/nvapi.py @@ -42,6 +42,11 @@ class NVAPIComponent(DLLComponent): ] } + @staticmethod + def get_override_keys() -> str: + # Bottles does not override (_)nvngx + return "nvapi,nvapi64" + @staticmethod def get_base_path(version: str) -> str: return ManagerUtils.get_nvapi_path(version) diff --git a/bottles/backend/dlls/vkd3d.py b/bottles/backend/dlls/vkd3d.py index 4f545b1160..0118c6ea26 100644 --- a/bottles/backend/dlls/vkd3d.py +++ b/bottles/backend/dlls/vkd3d.py @@ -31,6 +31,10 @@ class VKD3DComponent(DLLComponent): ] } + @staticmethod + def get_override_keys() -> str: + return "d3d12,d3d12core" + @staticmethod def get_base_path(version: str) -> str: return ManagerUtils.get_vkd3d_path(version) diff --git a/bottles/backend/managers/epicgamesstore.py b/bottles/backend/managers/epicgamesstore.py index 1a4a6a17a6..1db5bb249f 100644 --- a/bottles/backend/managers/epicgamesstore.py +++ b/bottles/backend/managers/epicgamesstore.py @@ -78,12 +78,6 @@ def get_installed_games(config: BottleConfig) -> list: "path": _path, "folder": _folder, "icon": "com.usebottles.bottles-program", - "dxvk": config.Parameters.dxvk, - "vkd3d": config.Parameters.vkd3d, - "dxvk_nvapi": config.Parameters.dxvk_nvapi, - "fsr": config.Parameters.fsr, - "virtual_desktop": config.Parameters.virtual_desktop, - "pulseaudio_latency": config.Parameters.pulseaudio_latency, "id": str(uuid.uuid4()), }) return games diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index 97e0174c50..a87db8adc7 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -691,19 +691,19 @@ def get_programs(self, config: BottleConfig) -> List[dict]: else: program_folder = os.path.dirname(_program["path"]) installed_programs.append({ - "executable": _program["executable"], - "arguments": _program.get("arguments", ""), - "name": _program["name"], - "path": _program["path"], + "executable": _program.get("executable"), + "arguments": _program.get("arguments"), + "name": _program.get("name"), + "path": _program.get("path"), "folder": _program.get("folder", program_folder), "icon": "com.usebottles.bottles-program", "script": _program.get("script"), - "dxvk": _program.get("dxvk", config.Parameters.dxvk), - "vkd3d": _program.get("vkd3d", config.Parameters.vkd3d), - "dxvk_nvapi": _program.get("dxvk_nvapi", config.Parameters.dxvk_nvapi), - "fsr": _program.get("fsr", config.Parameters.fsr), - "pulseaudio_latency": _program.get("pulseaudio_latency", config.Parameters.pulseaudio_latency), - "virtual_desktop": _program.get("virtual_desktop", config.Parameters.virtual_desktop), + "dxvk": _program.get("dxvk"), + "vkd3d": _program.get("vkd3d"), + "dxvk_nvapi": _program.get("dxvk_nvapi"), + "fsr": _program.get("fsr"), + "pulseaudio_latency": _program.get("pulseaudio_latency"), + "virtual_desktop": _program.get("virtual_desktop"), "removed": _program.get("removed"), "id": _program.get("id") }) @@ -745,13 +745,6 @@ def get_programs(self, config: BottleConfig) -> List[dict]: "folder": program_folder, "icon": "com.usebottles.bottles-program", "id": str(uuid.uuid4()), - "script": "", - "dxvk": config.Parameters.dxvk, - "vkd3d": config.Parameters.vkd3d, - "dxvk_nvapi": config.Parameters.dxvk_nvapi, - "fsr": config.Parameters.fsr, - "pulseaudio_latency": config.Parameters.pulseaudio_latency, - "virtual_desktop": config.Parameters.virtual_desktop, "auto_discovered": True }) found.append(executable_name) diff --git a/bottles/backend/managers/steam.py b/bottles/backend/managers/steam.py index 28ea0f704e..1c389a4b9f 100644 --- a/bottles/backend/managers/steam.py +++ b/bottles/backend/managers/steam.py @@ -244,12 +244,6 @@ def get_installed_apps_as_programs(self) -> list: "path": _path, "folder": _folder, "icon": "com.usebottles.bottles-program", - "dxvk": self.config.Parameters.dxvk, - "vkd3d": self.config.Parameters.vkd3d, - "dxvk_nvapi": self.config.Parameters.dxvk_nvapi, - "fsr": self.config.Parameters.fsr, - "virtual_desktop": self.config.Parameters.virtual_desktop, - "pulseaudio_latency": self.config.Parameters.pulseaudio_latency, "id": str(uuid.uuid4()), }) @@ -365,8 +359,6 @@ def get_launch_options(self, prefix: str, app_conf: Optional[dict] = None) -> {} launch_options = app_conf.get("LaunchOptions", "") _fail_msg = f"Fail to get launch options from Steam for: {prefix}" - prefix, args = "", "" - env_vars = {} res = { "command": "", "args": "", @@ -378,26 +370,7 @@ def get_launch_options(self, prefix: str, app_conf: Optional[dict] = None) -> {} logging.debug(_fail_msg) return res - if "%command%" in launch_options: - _c = launch_options.split("%command%") - prefix = _c[0] if len(_c) > 0 else "" - args = _c[1] if len(_c) > 1 else "" - else: - args = launch_options - - try: - prefix_list = shlex.split(prefix.strip()) - except ValueError: - prefix_list = prefix.split(shlex.quote(prefix.strip())) - - for p in prefix_list.copy(): - if "=" in p: - k, v = p.split("=", 1) - v = shlex.quote(v) if " " in v else v - env_vars[k] = v - prefix_list.remove(p) - - command = " ".join(prefix_list) + command, args, env_vars = SteamUtils.handle_launch_options(launch_options) res = { "command": command, "args": args, diff --git a/bottles/backend/managers/ubisoftconnect.py b/bottles/backend/managers/ubisoftconnect.py index f9310ae635..7b252781be 100644 --- a/bottles/backend/managers/ubisoftconnect.py +++ b/bottles/backend/managers/ubisoftconnect.py @@ -120,12 +120,6 @@ def get_installed_games(config: BottleConfig) -> list: "path": _path, "folder": _folder, "icon": "com.usebottles.bottles-program", - "dxvk": config.Parameters.dxvk, - "vkd3d": config.Parameters.vkd3d, - "dxvk_nvapi": config.Parameters.dxvk_nvapi, - "fsr": config.Parameters.fsr, - "virtual_desktop": config.Parameters.virtual_desktop, - "pulseaudio_latency": config.Parameters.pulseaudio_latency, "id": str(uuid.uuid4()), }) return games diff --git a/bottles/backend/utils/steam.py b/bottles/backend/utils/steam.py index bafad3b912..17eabbb827 100644 --- a/bottles/backend/utils/steam.py +++ b/bottles/backend/utils/steam.py @@ -15,7 +15,7 @@ # along with this program. If not, see . # -import os, subprocess +import os, subprocess, shlex from typing import Union, TextIO from typing import TextIO @@ -104,3 +104,33 @@ def get_dist_directory(path: str) -> str: logging.warning(f"No /dist or /files sub-directory was found under this Proton directory: {path}") return dist_directory + + @staticmethod + def handle_launch_options(launch_options: str) -> tuple[str, str, str]: + """ + Handle launch options. Supports the %command% pattern. + Return prefix, arguments, and environment variables. + """ + env_vars = {} + prefix, args = "", "" + if "%command%" in launch_options: + _c = launch_options.split("%command%") + prefix = _c[0] if len(_c) > 0 else "" + args = _c[1] if len(_c) > 1 else "" + else: + args = launch_options + + try: + prefix_list = shlex.split(prefix.strip()) + except ValueError: + prefix_list = prefix.split(shlex.quote(prefix.strip())) + + for p in prefix_list.copy(): + if "=" in p: + k, v = p.split("=", 1) + v = shlex.quote(v) if " " in v else v + env_vars[k] = v + prefix_list.remove(p) + + prefix = " ".join(prefix_list) + return prefix, args, env_vars \ No newline at end of file diff --git a/bottles/backend/utils/terminal.py b/bottles/backend/utils/terminal.py index 103b61de3b..a7e60c8576 100644 --- a/bottles/backend/utils/terminal.py +++ b/bottles/backend/utils/terminal.py @@ -17,6 +17,7 @@ import os import subprocess +import shlex from bottles.backend.logger import Logger @@ -87,17 +88,20 @@ def execute(self, command, env=None, colors="default", cwd=None): colors = "default" colors = self.colors[colors] + command = shlex.quote(command) if self.terminal[0] == 'easyterm.py': - command = ' '.join(self.terminal) % (colors, f'bash -c "{command}"') + command = ' '.join(self.terminal) % (colors, shlex.quote(f'bash -c {command}')) if "ENABLE_BASH" in os.environ: command = ' '.join(self.terminal) % (colors, f"bash") elif self.terminal[0] in ['kgx', 'xfce4-terminal']: - command = ' '.join(self.terminal) % "'sh -c %s'" % f'"{command}"' + command = ' '.join(self.terminal) % "'sh -c %s'" % f'{command}' elif self.terminal[0] in ['kitty', 'foot', 'konsole', 'gnome-terminal']: - command = ' '.join(self.terminal) % "sh -c %s" % f'"{command}"' + command = ' '.join(self.terminal) % "sh -c %s" % f'{command}' else: - command = ' '.join(self.terminal) % "bash -c %s" % f'"{command}"' + command = ' '.join(self.terminal) % "bash -c %s" % f'{command}' + + logging.info(f"Command: {command}") subprocess.Popen( command, diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index 2a734eb143..1bf5b01179 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -3,6 +3,9 @@ import uuid from typing import Union, Optional +from bottles.backend.dlls.dxvk import DXVKComponent +from bottles.backend.dlls.nvapi import NVAPIComponent +from bottles.backend.dlls.vkd3d import VKD3DComponent from bottles.backend.logger import Logger from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result @@ -32,11 +35,11 @@ def __init__( move_upd_fn: callable = None, post_script: Optional[str] = None, monitoring: Optional[list] = None, - override_dxvk: Optional[bool] = None, - override_vkd3d: Optional[bool] = None, - override_nvapi: Optional[bool] = None, - override_fsr: Optional[bool] = None, - override_virt_desktop: Optional[bool] = None + program_dxvk: Optional[bool] = None, + program_vkd3d: Optional[bool] = None, + program_nvapi: Optional[bool] = None, + program_fsr: Optional[bool] = None, + program_virt_desktop: Optional[bool] = None ): logging.info("Launching an executable…") self.config = config @@ -59,67 +62,57 @@ def __init__( self.environment = environment self.post_script = post_script self.monitoring = monitoring - self.use_virt_desktop = override_virt_desktop + self.use_virt_desktop = program_virt_desktop env_dll_overrides = [] - if override_dxvk is not None \ - and not override_dxvk \ - and self.config.Parameters.dxvk: - env_dll_overrides.append("d3d9,d3d11,d3d10core,dxgi=n") - - if override_vkd3d is not None \ - and not override_vkd3d \ - and self.config.Parameters.vkd3d: - env_dll_overrides.append("d3d12,d3d12core=n") - - if override_nvapi is not None \ - and not override_nvapi \ - and self.config.Parameters.dxvk_nvapi: - env_dll_overrides.append("nvapi,nvapi64=n") - - if override_fsr is not None and override_fsr: - self.environment["WINE_FULLSCREEN_FSR"] = "1" + + # None = use global DXVK value + if program_dxvk is not None: + # DXVK is globally activated, but disabled for the program + if not program_dxvk and self.config.Parameters.dxvk: + # Disable DXVK for the program + override_dxvk = DXVKComponent.get_override_keys() + "=b" + env_dll_overrides.append(override_dxvk) + + if program_vkd3d is not None: + if not program_vkd3d and self.config.Parameters.vkd3d: + override_vkd3d = VKD3DComponent.get_override_keys() + "=b" + env_dll_overrides.append(override_vkd3d) + + if program_nvapi is not None: + if not program_nvapi and self.config.Parameters.dxvk_nvapi: + override_nvapi = NVAPIComponent.get_override_keys() + "=b" + env_dll_overrides.append(override_nvapi) + + if program_fsr is not None and program_fsr != self.config.Parameters.fsr: + self.environment["WINE_FULLSCREEN_FSR"] = "1" if program_fsr else "0" self.environment["WINE_FULLSCREEN_FSR_STRENGTH"] = str(self.config.Parameters.fsr_sharpening_strength) + if self.config.Parameters.fsr_quality_mode: + self.environment["WINE_FULLSCREEN_FSR_MODE"] = str(self.config.Parameters.fsr_quality_mode) - if "WINEDLLOVERRIDES" in self.environment: - self.environment["WINEDLLOVERRIDES"] += "," + ",".join(env_dll_overrides) - else: - self.environment["WINEDLLOVERRIDES"] = ",".join(env_dll_overrides) + if env_dll_overrides: + if "WINEDLLOVERRIDES" in self.environment: + self.environment["WINEDLLOVERRIDES"] += ";" + ";".join(env_dll_overrides) + else: + self.environment["WINEDLLOVERRIDES"] = ";".join(env_dll_overrides) @classmethod def run_program(cls, config: BottleConfig, program: dict, terminal: bool = False): if program is None: logging.warning("The program entry is not well formatted.") - dxvk = config.Parameters.dxvk - vkd3d = config.Parameters.vkd3d - nvapi = config.Parameters.dxvk_nvapi - fsr = config.Parameters.fsr - virt_desktop = config.Parameters.virtual_desktop - - if program.get("dxvk") != dxvk: - dxvk = program.get("dxvk") - if program.get("vkd3d") != vkd3d: - vkd3d = program.get("vkd3d") - if program.get("dxvk_nvapi") != nvapi: - nvapi = program.get("dxvk_nvapi") - if program.get("fsr") != fsr: - fsr = program.get("fsr") - if program.get("virtual_desktop") != virt_desktop: - virt_desktop = program.get("virtual_desktop") - return cls( config=config, - exec_path=program["path"], - args=program.get("arguments", ""), - cwd=program.get("folder", None), - post_script=program.get("script", None), + exec_path=program.get("path"), + args=program.get("arguments"), + cwd=program.get("folder"), + post_script=program.get("script"), terminal=terminal, - override_dxvk=dxvk, - override_vkd3d=vkd3d, - override_nvapi=nvapi, - override_fsr=fsr, - override_virt_desktop=virt_desktop + program_dxvk=program.get("dxvk"), + program_vkd3d=program.get("vkd3d"), + program_nvapi=program.get("dxvk_nvapi"), + program_fsr=program.get("fsr"), + program_virt_desktop=program.get("virtual_desktop") ).run() def __get_cwd(self, cwd: str) -> Union[str, None]: diff --git a/bottles/backend/wine/winecommand.py b/bottles/backend/wine/winecommand.py index bdad95d628..e332a45ecb 100644 --- a/bottles/backend/wine/winecommand.py +++ b/bottles/backend/wine/winecommand.py @@ -3,6 +3,7 @@ import stat import subprocess import tempfile +import shlex from typing import Optional from bottles.backend.globals import Paths, gamemode_available, gamescope_available, mangohud_available, \ @@ -88,21 +89,22 @@ def __init__( command: str, terminal: bool = False, arguments: str = False, - environment: dict = False, + environment: dict = {}, communicate: bool = False, cwd: Optional[str] = None, colors: str = "default", minimal: bool = False, # avoid gamemode/gamescope usage post_script: Optional[str] = None ): + _environment = environment.copy() self.config = self._get_config(config) self.minimal = minimal self.arguments = arguments self.cwd = self._get_cwd(cwd) self.runner, self.runner_runtime = self._get_runner_info() - self.command = self.get_cmd(command, post_script) + self.command = self.get_cmd(command, post_script, environment=_environment) self.terminal = terminal - self.env = self.get_env(environment) + self.env = self.get_env(_environment) self.communicate = communicate self.colors = colors self.vmtouch_files = None @@ -175,8 +177,6 @@ def get_env(self, environment: Optional[dict] = None, return_steam_env: bool = F if config.Environment_Variables: for key, value in config.Environment_Variables.items(): env.add(key, value, override=True) - if (key == "WINEDLLOVERRIDES") and value: - dll_overrides.extend(value.split(";")) # Environment variables from argument if environment: @@ -314,8 +314,6 @@ def get_env(self, environment: Optional[dict] = None, return_steam_env: bool = F # DXVK-Nvapi environment variables if params.dxvk_nvapi and not return_steam_env: - conf = self.__set_dxvk_nvapi_conf(bottle) - env.add("DXVK_CONFIG_FILE", conf) # NOTE: users reported that DXVK_ENABLE_NVAPI and DXVK_NVAPIHACK must be set to make # DLSS works. I don't have a GPU compatible with this tech, so I'll trust them env.add("DXVK_NVAPIHACK", "0") @@ -444,12 +442,16 @@ def get_cmd( command, post_script: Optional[str] = None, return_steam_cmd: bool = False, - return_clean_cmd: bool = False + return_clean_cmd: bool = False, + environment: Optional[dict] = None ) -> str: config = self.config params = config.Parameters runner = self.runner + if environment is None: + environment = {} + if return_clean_cmd: return_steam_cmd = True @@ -528,12 +530,16 @@ def get_cmd( logging.warning("Steam runtime was requested and found but there are no valid combinations") if self.arguments: - if "%command%" in self.arguments: - prefix = self.arguments.split("%command%")[0] - suffix = self.arguments.split("%command%")[1] - command = f"{prefix} {command} {suffix}" - else: - command = f"{command} {self.arguments}" + prefix, suffix, extracted_env = SteamUtils.handle_launch_options(self.arguments) + if prefix: + command = f"{prefix} {command}" + if suffix: + command = f"{command} {suffix}" + if extracted_env: + if extracted_env.get("WINEDLLOVERRIDES") and environment.get("WINEDLLOVERRIDES"): + environment["WINEDLLOVERRIDES"] += ";" + extracted_env.get("WINEDLLOVERRIDES") + del extracted_env["WINEDLLOVERRIDES"] + environment.update(extracted_env) if post_script is not None: command = f"{command} ; sh '{post_script}'" @@ -582,7 +588,7 @@ def _vmtouch_preload(self): s = (self.cwd + "/" + (self.command.split(" ")[-1].split('\\')[-1])).replace('\'', "") else: s = self.command.split(" ")[-1] - self.vmtouch_files = f"'{s}'" + self.vmtouch_files = shlex.quote(s) # if self.config.Parameters.vmtouch_cache_cwd: # self.vmtouch_files = "'"+self.vmtouch_files+"' '"+self.cwd+"/'" Commented out as fix for #1941 @@ -694,26 +700,3 @@ def run(self) -> Result[Optional[str]]: return Result(True, data=rv) - @staticmethod - def __set_dxvk_nvapi_conf(bottle: str): - """ - TODO: This should be moved to a dedicated DXVKConf class when - we will provide a way to set the DXVK configuration. - """ - dxvk_conf = f"{bottle}/dxvk.conf" - if not os.path.exists(dxvk_conf): - # create dxvk.conf if it doesn't exist - with open(dxvk_conf, "w") as f: - f.write("dxgi.nvapiHack = False") - else: - # check if dxvk.conf has the nvapiHack option, if not add it - with open(dxvk_conf, "r") as f: - lines = f.readlines() - with open(dxvk_conf, "w") as f: - for line in lines: - if "dxgi.nvapiHack" in line: - f.write("dxgi.nvapiHack = False\n") - else: - f.write(line) - - return dxvk_conf diff --git a/bottles/frontend/cli/cli.py b/bottles/frontend/cli/cli.py index 8a785946aa..b3f38627fa 100644 --- a/bottles/frontend/cli/cli.py +++ b/bottles/frontend/cli/cli.py @@ -543,6 +543,12 @@ def run_program(self): _executable = self.args.executable _cwd = None _script = None + _program_dxvk = None + _program_vkd3d = None + _program_dxvk_nvapi = None + _program_fsr = None + _program_virt_desktop = None + mng = Manager(g_settings=self.settings, is_cli=True) mng.checks() @@ -552,12 +558,6 @@ def run_program(self): bottle = mng.local_bottles[_bottle] programs = mng.get_programs(bottle) - _dxvk = bottle.Parameters.dxvk - _vkd3d = bottle.Parameters.vkd3d - _nvapi = bottle.Parameters.dxvk_nvapi - _fsr = bottle.Parameters.fsr - _pulse = bottle.Parameters.pulseaudio_latency - _virt_desktop = bottle.Parameters.virtual_desktop if _program is not None: if _executable is not None: @@ -575,18 +575,11 @@ def run_program(self): _cwd = program.get("folder", "") _script = program.get("script", None) - if program.get("dxvk") != _dxvk: - _dxvk = program.get("dxvk") - if program.get("vkd3d") != _vkd3d: - _vkd3d = program.get("vkd3d") - if program.get("dxvk_nvapi") != _nvapi: - _nvapi = program.get("dxvk_nvapi") - if program.get("fsr") != _fsr: - _fsr = program.get("fsr") - if program.get("pulseaudio_latency") != _pulse: - _pulse = program.get("pulseaudio_latency") - if program.get("virtual_desktop") != _virt_desktop: - _virt_desktop = program.get("virtual_desktop") + _program_dxvk = program.get("dxvk") + _program_vkd3d = program.get("vkd3d") + _program_dxvk_nvapi = program.get("dxvk_nvapi") + _program_fsr = program.get("fsr") + _program_virt_desktop = program.get("virtual_desktop") WineExecutor( bottle, @@ -594,11 +587,11 @@ def run_program(self): args=_args, cwd=_cwd, post_script=_script, - override_dxvk=_dxvk, - override_vkd3d=_vkd3d, - override_nvapi=_nvapi, - override_fsr=_fsr, - override_virt_desktop=_virt_desktop + program_dxvk=_program_dxvk, + program_vkd3d=_program_vkd3d, + program_nvapi=_program_dxvk_nvapi, + program_fsr=_program_fsr, + program_virt_desktop=_program_virt_desktop ).run_cli() # endregion diff --git a/bottles/frontend/ui/dialog-dll-overrides.blp b/bottles/frontend/ui/dialog-dll-overrides.blp index d387deff54..526503f20e 100644 --- a/bottles/frontend/ui/dialog-dll-overrides.blp +++ b/bottles/frontend/ui/dialog-dll-overrides.blp @@ -14,6 +14,19 @@ template DLLOverridesDialog : .AdwPreferencesWindow { .AdwEntryRow entry_row { title: _("New Override"); show-apply-button: true; + + [suffix] + MenuButton menu_invalid_override { + valign: center; + tooltip-text: _("Show Information"); + icon-name: "warning-symbolic"; + popover: popover_invalid_override; + visible: false; + + styles [ + "flat", + ] + } } } @@ -22,3 +35,20 @@ template DLLOverridesDialog : .AdwPreferencesWindow { } } } + +Popover popover_invalid_override { + Label { + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + xalign: 0; + wrap: true; + wrap-mode: word_char; + ellipsize: none; + lines: 4; + use-markup: true; + max-width-chars: 40; + label: _("This override is already managed by Bottles."); + } +} diff --git a/bottles/frontend/windows/dlloverrides.py b/bottles/frontend/windows/dlloverrides.py index 3db97747e3..7c1d324394 100644 --- a/bottles/frontend/windows/dlloverrides.py +++ b/bottles/frontend/windows/dlloverrides.py @@ -17,6 +17,7 @@ from gi.repository import Gtk, GLib, Adw +from bottles.backend.dlls.dll import DLLComponent @Gtk.Template(resource_path='/com/usebottles/bottles/dll-override-entry.ui') class DLLEntry(Adw.ComboRow): @@ -84,6 +85,7 @@ class DLLOverridesDialog(Adw.PreferencesWindow): # region Widgets entry_row = Gtk.Template.Child() group_overrides = Gtk.Template.Child() + menu_invalid_override = Gtk.Template.Child() # endregion @@ -99,8 +101,32 @@ def __init__(self, window, config, **kwargs): self.__populate_overrides_list() # connect signals + self.entry_row.connect('changed', self.__check_override) self.entry_row.connect("apply", self.__save_override) + def __check_override(self, *_args): + """ + This function check if the override name is valid + Overrides already managed by Bottles (e.g. DXVK, VKD3D...) are deemed invalid + """ + dll_name = self.entry_row.get_text() + invalid_dlls = [] + + for managed_component in DLLComponent.__subclasses__(): + invalid_dlls += managed_component.get_override_keys().split(",") + + is_invalid = dll_name in invalid_dlls + + self.__valid_name = not is_invalid + self.menu_invalid_override.set_visible(is_invalid) + if is_invalid: + self.entry_row.add_css_class("error") + self.entry_row.set_show_apply_button(False) + else: + self.entry_row.remove_css_class("error") + # Needs to be set to true immediately + self.entry_row.set_show_apply_button(True) + def __save_override(self, *_args): """ This function check if the override name is not empty, then @@ -109,7 +135,7 @@ def __save_override(self, *_args): """ dll_name = self.entry_row.get_text() - if dll_name != "": + if dll_name != "" and self.__valid_name: self.manager.update_config( config=self.config, key=dll_name, diff --git a/bottles/frontend/windows/envvars.py b/bottles/frontend/windows/envvars.py index 3ec5d6c656..3f8df1aae0 100644 --- a/bottles/frontend/windows/envvars.py +++ b/bottles/frontend/windows/envvars.py @@ -96,7 +96,7 @@ def __init__(self, window, config, **kwargs): self.entry_name.connect("apply", self.__save_var) def __validate(self, *_args): - self.__valid_name = GtkUtils.validate_entry(self.entry_name) + self.__valid_name = GtkUtils.validate_entry(self.entry_name, lambda envvar : envvar.startswith("WINEDLLOVERRIDES")) def __save_var(self, *_args): """ @@ -111,7 +111,7 @@ def __save_var(self, *_args): env_name = self.entry_name.get_text() env_value = "value" - split_value = env_name.rsplit('=', 1) + split_value = env_name.split('=', 1) if len(split_value) == 2: env_name = split_value[0] env_value = split_value[1] diff --git a/bottles/frontend/windows/launchoptions.py b/bottles/frontend/windows/launchoptions.py index 986cac9e8d..f06804dc9a 100644 --- a/bottles/frontend/windows/launchoptions.py +++ b/bottles/frontend/windows/launchoptions.py @@ -52,8 +52,19 @@ class LaunchOptionsDialog(Adw.Window): __default_script_msg = _("Choose a script which should be executed after run.") __default_cwd_msg = _("Choose from where start the program.") - __msg_disabled = _("{0} is already disabled for this bottle.") - __msg_override = _("This setting is different from the bottle's default.") + __msg_disabled = _("{0} is disabled globally for this bottle.") + __msg_override = _("This setting overrides the bottle's global setting.") + + def __set_disabled_switches(self): + if not self.global_dxvk: + self.action_dxvk.set_subtitle(self.__msg_disabled.format("DXVK")) + self.switch_dxvk.set_sensitive(False) + if not self.global_vkd3d: + self.action_vkd3d.set_subtitle(self.__msg_disabled.format("VKD3D")) + self.switch_vkd3d.set_sensitive(False) + if not self.global_nvapi: + self.action_nvapi.set_subtitle(self.__msg_disabled.format("DXVK-NVAPI")) + self.switch_nvapi.set_sensitive(False) def __init__(self, parent, config, program, **kwargs): super().__init__(**kwargs) @@ -70,6 +81,14 @@ def __init__(self, parent, config, program, **kwargs): # set widget defaults self.entry_arguments.set_text(program.get("arguments", "")) + # keeps track of toggled switches + self.toggled = {} + self.toggled["dxvk"] = False + self.toggled["vkd3d"] = False + self.toggled["dxvk_nvapi"] = False + self.toggled["fsr"] = False + self.toggled["virtual_desktop"] = False + # connect signals self.btn_save.connect("clicked", self.__save) self.btn_script.connect("clicked", self.__choose_script) @@ -78,35 +97,65 @@ def __init__(self, parent, config, program, **kwargs): self.btn_cwd_reset.connect("clicked", self.__reset_cwd) self.btn_reset_defaults.connect("clicked", self.__reset_defaults) self.entry_arguments.connect("activate", self.__save) + + # set overrides status + self.global_dxvk = program_dxvk = config.Parameters.dxvk + self.global_vkd3d = program_vkd3d = config.Parameters.vkd3d + self.global_nvapi = program_nvapi = config.Parameters.dxvk_nvapi + self.global_fsr = program_fsr = config.Parameters.fsr + self.global_virt_desktop = program_virt_desktop = config.Parameters.virtual_desktop + + if self.program.get("dxvk") is not None: + program_dxvk = self.program.get("dxvk") + self.action_dxvk.set_subtitle(self.__msg_override) + if self.program.get("vkd3d") is not None: + program_vkd3d = self.program.get("vkd3d") + self.action_vkd3d.set_subtitle(self.__msg_override) + if self.program.get("dxvk_nvapi") is not None: + program_nvapi = self.program.get("dxvk_nvapi") + self.action_nvapi.set_subtitle(self.__msg_override) + if self.program.get("fsr") is not None: + program_fsr = self.program.get("fsr") + self.action_fsr.set_subtitle(self.__msg_override) + if self.program.get("virtual_desktop") is not None: + program_virt_desktop = self.program.get("virtual_desktop") + self.action_virt_desktop.set_subtitle(self.__msg_override) + + self.switch_dxvk.set_active(program_dxvk) + self.switch_vkd3d.set_active(program_vkd3d) + self.switch_nvapi.set_active(program_nvapi) + self.switch_fsr.set_active(program_fsr) + self.switch_virt_desktop.set_active(program_virt_desktop) + self.switch_dxvk.connect( "state-set", self.__check_override, - config.Parameters.dxvk, - self.action_dxvk + self.action_dxvk, + "dxvk" ) self.switch_vkd3d.connect( "state-set", self.__check_override, - config.Parameters.vkd3d, - self.action_vkd3d + self.action_vkd3d, + "vkd3d" ) self.switch_nvapi.connect( "state-set", self.__check_override, - config.Parameters.dxvk_nvapi, - self.action_nvapi + self.action_nvapi, + "dxvk_nvapi" ) self.switch_fsr.connect( "state-set", self.__check_override, - config.Parameters.fsr, - self.action_fsr + self.action_fsr, + "fsr" ) self.switch_virt_desktop.connect( "state-set", self.__check_override, - config.Parameters.virtual_desktop, - self.action_virt_desktop + self.action_virt_desktop, + "virtual_desktop" ) if program.get("script") not in ["", None]: @@ -117,72 +166,34 @@ def __init__(self, parent, config, program, **kwargs): self.action_cwd.set_subtitle(program["folder"]) self.btn_cwd_reset.set_visible(True) - # set overrides status - dxvk = config.Parameters.dxvk - vkd3d = config.Parameters.vkd3d - nvapi = config.Parameters.dxvk_nvapi - fsr = config.Parameters.fsr - virt_desktop = config.Parameters.virtual_desktop + self.__set_disabled_switches() - if not dxvk: - self.action_dxvk.set_subtitle(self.__msg_disabled.format("DXVK")) - self.switch_dxvk.set_sensitive(False) - if not vkd3d: - self.action_vkd3d.set_subtitle(self.__msg_disabled.format("VKD3D")) - self.switch_vkd3d.set_sensitive(False) - if not nvapi: - self.action_nvapi.set_subtitle(self.__msg_disabled.format("DXVK-Nvapi")) - self.switch_nvapi.set_sensitive(False) - - if dxvk != self.program.get("dxvk"): - self.action_dxvk.set_subtitle(self.__msg_override) - if vkd3d != self.program.get("vkd3d"): - self.action_vkd3d.set_subtitle(self.__msg_override) - if nvapi != self.program.get("dxvk_nvapi"): - self.action_nvapi.set_subtitle(self.__msg_override) - if fsr != self.program.get("fsr"): - self.action_fsr.set_subtitle(self.__msg_override) - if virt_desktop != self.program.get("virtual_desktop"): - self.action_virt_desktop.set_subtitle(self.__msg_override) - - if "dxvk" in self.program: - dxvk = self.program["dxvk"] - if "vkd3d" in self.program: - vkd3d = self.program["vkd3d"] - if "dxvk_nvapi" in self.program: - nvapi = self.program["dxvk_nvapi"] - if "fsr" in self.program: - fsr = self.program["fsr"] - if "virtual_desktop" in self.program: - virt_desktop = self.program["virtual_desktop"] - - self.switch_dxvk.set_active(dxvk) - self.switch_vkd3d.set_active(vkd3d) - self.switch_nvapi.set_active(nvapi) - self.switch_fsr.set_active(fsr) - self.switch_virt_desktop.set_active(virt_desktop) - - def __check_override(self, widget, state, value, action): - if state != value: - action.set_subtitle(self.__msg_override) - else: - action.set_subtitle("") + def __check_override(self, widget, state, action, name): + self.toggled[name] = True + action.set_subtitle(self.__msg_override) def get_config(self): return self.config + def __set_override(self, name, program_value, global_value): + # Special reset value + if self.toggled[name] is None and name in self.program: + del self.program[name] + if self.toggled[name]: + self.program[name] = program_value + def __idle_save(self, *_args): - dxvk = self.switch_dxvk.get_state() - vkd3d = self.switch_vkd3d.get_state() - nvapi = self.switch_nvapi.get_state() - fsr = self.switch_fsr.get_state() - virt_desktop = self.switch_virt_desktop.get_state() - - self.program["dxvk"] = dxvk - self.program["vkd3d"] = vkd3d - self.program["dxvk_nvapi"] = nvapi - self.program["fsr"] = fsr - self.program["virtual_desktop"] = virt_desktop + program_dxvk = self.switch_dxvk.get_state() + program_vkd3d = self.switch_vkd3d.get_state() + program_nvapi = self.switch_nvapi.get_state() + program_fsr = self.switch_fsr.get_state() + program_virt_desktop = self.switch_virt_desktop.get_state() + + self.__set_override("dxvk", program_dxvk, self.global_dxvk) + self.__set_override("vkd3d", program_vkd3d, self.global_vkd3d) + self.__set_override("dxvk_nvapi", program_nvapi, self.global_nvapi) + self.__set_override("fsr", program_fsr, self.global_fsr) + self.__set_override("virtual_desktop", program_virt_desktop, self.global_virt_desktop) self.program["arguments"] = self.entry_arguments.get_text() self.config = self.manager.update_config( @@ -255,8 +266,16 @@ def __reset_cwd(self, *_args): self.btn_cwd_reset.set_visible(False) def __reset_defaults(self, *_args): - self.switch_dxvk.set_active(self.config.Parameters.dxvk) - self.switch_vkd3d.set_active(self.config.Parameters.vkd3d) - self.switch_nvapi.set_active(self.config.Parameters.dxvk_nvapi) - self.switch_fsr.set_active(self.config.Parameters.fsr) - self.switch_virt_desktop.set_active(self.config.Parameters.virtual_desktop) + self.switch_dxvk.set_active(self.global_dxvk) + self.switch_vkd3d.set_active(self.global_vkd3d) + self.switch_nvapi.set_active(self.global_nvapi) + self.switch_fsr.set_active(self.global_fsr) + self.switch_virt_desktop.set_active(self.global_virt_desktop) + self.action_dxvk.set_subtitle("") + self.action_vkd3d.set_subtitle("") + self.action_nvapi.set_subtitle("") + self.action_fsr.set_subtitle("") + self.action_virt_desktop.set_subtitle("") + self.__set_disabled_switches() + for name in self.toggled: + self.toggled[name] = None