diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index 74abfc1bcb2..21ec474fc20 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -725,6 +725,7 @@ def get_programs(self, config: BottleConfig) -> List[dict]: "pre_script": _program.get("pre_script"), "post_script": _program.get("post_script"), "folder": _program.get("folder", program_folder), + "disc_image": _program.get("disc_image"), "dxvk": _program.get("dxvk"), "vkd3d": _program.get("vkd3d"), "dxvk_nvapi": _program.get("dxvk_nvapi"), diff --git a/bottles/backend/wine/drives.py b/bottles/backend/wine/drives.py index a87b3dc9400..22178bf5d0d 100644 --- a/bottles/backend/wine/drives.py +++ b/bottles/backend/wine/drives.py @@ -32,7 +32,7 @@ def get_drive(self, letter: str): return None def set_drive_path(self, letter: str, path: str): - """Change a drives path in the bottle""" + """Change a drive's path in the bottle""" letter = f"{letter}:".lower() drive_sym_path = os.path.join(self.dosdevices_path, letter) if not os.path.exists(self.dosdevices_path): diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index 2c4480421d0..12bb65d7d70 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -34,6 +34,7 @@ def __init__( pre_script: Optional[str] = None, post_script: Optional[str] = None, cwd: Optional[str] = None, + disc_image: Optional[str] = None, monitoring: Optional[list] = None, program_dxvk: Optional[bool] = None, program_vkd3d: Optional[bool] = None, @@ -63,6 +64,7 @@ def __init__( self.pre_script = pre_script self.post_script = post_script self.cwd = self.__get_cwd(cwd) + self.disc_image = disc_image self.monitoring = monitoring self.use_gamescope = program_gamescope self.use_virt_desktop = program_virt_desktop @@ -123,6 +125,7 @@ def run_program(cls, config: BottleConfig, program: dict, terminal: bool = False pre_script=program.get("pre_script"), post_script=program.get("post_script"), cwd=program.get("folder"), + disc_image=program.get("disc_image"), terminal=terminal, program_dxvk=program.get("dxvk"), program_vkd3d=program.get("vkd3d"), @@ -213,6 +216,7 @@ def run_cli(self): pre_script=self.pre_script, post_script=self.post_script, cwd=self.cwd, + disc_image=self.disc_image, ) return Result(status=True, data={"output": res}) @@ -279,6 +283,7 @@ def __launch_exe(self): pre_script=self.pre_script, post_script=self.post_script, cwd=self.cwd, + disc_image=self.disc_image, ) res = winecmd.run() self.__set_monitors() @@ -317,6 +322,7 @@ def __launch_with_starter(self): pre_script=self.pre_script, post_script=self.post_script, cwd=self.cwd, + disc_image=self.disc_image, ) self.__set_monitors() return Result(status=True, data={"output": res}) diff --git a/bottles/backend/wine/start.py b/bottles/backend/wine/start.py index b88738ec11c..2729d5746ae 100644 --- a/bottles/backend/wine/start.py +++ b/bottles/backend/wine/start.py @@ -20,6 +20,7 @@ def run( pre_script: Optional[str] = None, post_script: Optional[str] = None, cwd: Optional[str] = None, + disc_image: Optional[str] = None, ): winepath = WinePath(self.config) @@ -42,6 +43,7 @@ def run( pre_script=pre_script, post_script=post_script, cwd=cwd, + disc_image=disc_image, minimal=False, action_name="run", ) diff --git a/bottles/backend/wine/winecommand.py b/bottles/backend/wine/winecommand.py index c481e17cff7..52eeeebf808 100644 --- a/bottles/backend/wine/winecommand.py +++ b/bottles/backend/wine/winecommand.py @@ -1,6 +1,7 @@ import os import shutil import stat +import string import subprocess import tempfile import shlex @@ -25,6 +26,7 @@ from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.terminal import TerminalUtils from bottles.backend.utils.steam import SteamUtils +from bottles.backend.wine.drives import Drives logging = Logger() @@ -100,6 +102,7 @@ def __init__( pre_script: Optional[str] = None, post_script: Optional[str] = None, cwd: Optional[str] = None, + disc_image: Optional[str] = None, ): _environment = environment.copy() self.config = self._get_config(config) @@ -113,7 +116,7 @@ def __init__( else self.config.Parameters.gamescope ) self.command = self.get_cmd( - command, pre_script, post_script, environment=_environment + command, pre_script, post_script, disc_image, environment=_environment ) self.terminal = terminal self.env = self.get_env(_environment) @@ -489,6 +492,7 @@ def get_cmd( command, pre_script: Optional[str] = None, post_script: Optional[str] = None, + disc_image: Optional[str] = None, return_steam_cmd: bool = False, return_clean_cmd: bool = False, environment: Optional[dict] = None, @@ -603,6 +607,22 @@ def get_cmd( if pre_script not in (None, ""): command = f"sh '{pre_script}' ; {command}" + if disc_image is not None: + # Mount/unmount disc image on temp mount point + mount_point = "/tmp/bottles/disc" + command = f""" + flatpak-spawn --host mkdir -p '{mount_point}' + flatpak-spawn --host fuseiso '{disc_image}' '{mount_point}' + {command} + flatpak-spawn --host fusermount -uz '{mount_point}' # -z for lazy unmount + """ + + # Assign path to first free drive letter + drives = Drives(self.config) + alphabet = string.ascii_uppercase + letter = next(c for c in alphabet if c >= "D" and not drives.get_drive(c)) + drives.set_drive_path(letter, mount_point) + return command def _get_gamescope_cmd(self, return_steam_cmd: bool = False) -> str: diff --git a/bottles/backend/wine/wineprogram.py b/bottles/backend/wine/wineprogram.py index 86166eca48c..c546b706d2c 100644 --- a/bottles/backend/wine/wineprogram.py +++ b/bottles/backend/wine/wineprogram.py @@ -46,6 +46,7 @@ def launch( pre_script: Optional[str] = None, post_script: Optional[str] = None, cwd: Optional[str] = None, + disc_image: Optional[str] = None, action_name: str = "launch", ): if environment is None: @@ -73,6 +74,7 @@ def launch( pre_script=pre_script, post_script=post_script, cwd=cwd, + disc_image=disc_image, arguments=program_args, ) diff --git a/bottles/frontend/cli/cli.py b/bottles/frontend/cli/cli.py index e08d75a158f..b58573c951c 100644 --- a/bottles/frontend/cli/cli.py +++ b/bottles/frontend/cli/cli.py @@ -669,6 +669,7 @@ def run_program(self): program.get("pre_script", None) program.get("post_script", None) program.get("folder", None) + program.get("disc_image", None) program.get("dxvk") program.get("vkd3d") diff --git a/bottles/frontend/ui/dialog-launch-options.blp b/bottles/frontend/ui/dialog-launch-options.blp index ebd35d227ec..63e508990af 100644 --- a/bottles/frontend/ui/dialog-launch-options.blp +++ b/bottles/frontend/ui/dialog-launch-options.blp @@ -140,6 +140,37 @@ template $LaunchOptionsDialog: Adw.Window { } } } + + Adw.ActionRow action_disc_image { + activatable-widget: btn_disc_image; + title: _("CD/DVD Image"); + subtitle: _("Choose an optical disc image to be mounted."); + + Box { + spacing: 6; + + Button btn_disc_image_reset { + tooltip-text: _("Reset to Default"); + valign: center; + visible: false; + icon-name: "edit-undo-symbolic"; + + styles [ + "flat", + ] + } + + Button btn_disc_image { + tooltip-text: _("Choose a Disc Image"); + valign: center; + icon-name: "document-open-symbolic"; + + styles [ + "flat", + ] + } + } + } } Adw.PreferencesGroup { diff --git a/bottles/frontend/utils/filters.py b/bottles/frontend/utils/filters.py index 081e1ad4a4a..c60b37a1475 100644 --- a/bottles/frontend/utils/filters.py +++ b/bottles/frontend/utils/filters.py @@ -42,6 +42,18 @@ def add_yaml_filters(dialog): dialog.add_filter(filter) +def add_disc_image_filters(dialog): + filter = Gtk.FileFilter() + filter.set_name(_("Optical Disc Image")) + filter.add_pattern("*.bin") + filter.add_pattern("*.cue") + filter.add_pattern("*.img") + filter.add_pattern("*.iso") + filter.add_pattern("*.mds") + + dialog.add_filter(filter) + + def add_all_filters(dialog): filter = Gtk.FileFilter() filter.set_name(_("All Files")) diff --git a/bottles/frontend/windows/launchoptions.py b/bottles/frontend/windows/launchoptions.py index 1b1550d0133..cd96cbbbe5f 100644 --- a/bottles/frontend/windows/launchoptions.py +++ b/bottles/frontend/windows/launchoptions.py @@ -19,6 +19,7 @@ from bottles.backend.utils.manager import ManagerUtils from bottles.backend.logger import Logger +from bottles.frontend.utils.filters import add_disc_image_filters, add_all_filters from gettext import gettext as _ logging = Logger() @@ -40,9 +41,13 @@ class LaunchOptionsDialog(Adw.Window): btn_post_script_reset = Gtk.Template.Child() btn_cwd = Gtk.Template.Child() btn_cwd_reset = Gtk.Template.Child() + btn_disc_image = Gtk.Template.Child() + btn_disc_image_reset = Gtk.Template.Child() btn_reset_defaults = Gtk.Template.Child() action_pre_script = Gtk.Template.Child() action_post_script = Gtk.Template.Child() + action_cwd = Gtk.Template.Child() + action_disc_image = Gtk.Template.Child() switch_dxvk = Gtk.Template.Child() switch_vkd3d = Gtk.Template.Child() switch_nvapi = Gtk.Template.Child() @@ -54,13 +59,13 @@ class LaunchOptionsDialog(Adw.Window): action_nvapi = Gtk.Template.Child() action_fsr = Gtk.Template.Child() action_gamescope = Gtk.Template.Child() - action_cwd = Gtk.Template.Child() action_virt_desktop = Gtk.Template.Child() # endregion __default_pre_script_msg = _("Choose a script which should be executed before run.") __default_post_script_msg = _("Choose a script which should be executed after run.") __default_cwd_msg = _("Choose from where start the program.") + __default_disc_image_msg = _("Choose an optical disc image to be mounted.") __msg_disabled = _("{0} is disabled globally for this bottle.") __msg_override = _("This setting overrides the bottle's global setting.") @@ -108,6 +113,8 @@ def __init__(self, parent, config, program, **kwargs): self.btn_post_script_reset.connect("clicked", self.__reset_post_script) self.btn_cwd.connect("clicked", self.__choose_cwd) self.btn_cwd_reset.connect("clicked", self.__reset_cwd) + self.btn_disc_image.connect("clicked", self.__choose_disc_image) + self.btn_disc_image_reset.connect("clicked", self.__reset_disc_image) self.btn_reset_defaults.connect("clicked", self.__reset_defaults) self.entry_arguments.connect("activate", self.__save) @@ -185,6 +192,10 @@ def __init__(self, parent, config, program, **kwargs): self.action_cwd.set_subtitle(program["folder"]) self.btn_cwd_reset.set_visible(True) + if program.get("disc_image") not in ["", None]: + self.action_disc_image.set_subtitle(program["disc_image"]) + self.btn_disc_image_reset.set_visible(True) + self.__set_disabled_switches() def __check_override(self, widget, state, action, name): @@ -344,6 +355,35 @@ def __reset_cwd(self, *_args): self.action_cwd.set_subtitle(self.__default_cwd_msg) self.btn_cwd_reset.set_visible(False) + def __choose_disc_image(self, *_args): + def set_path(dialog, response): + if response != Gtk.ResponseType.ACCEPT: + self.action_disc_image.set_subtitle(self.__default_disc_image_msg) + return + + disc_image = dialog.get_file().get_path() + self.program["disc_image"] = disc_image + self.action_disc_image.set_subtitle(disc_image) + self.btn_disc_image_reset.set_visible(True) + + dialog = Gtk.FileChooserNative.new( + title=_("Select a CD/DVD Image"), + action=Gtk.FileChooserAction.OPEN, + parent=self.window, + accept_label=_("Select"), + ) + + add_disc_image_filters(dialog) + add_all_filters(dialog) + dialog.set_modal(True) + dialog.connect("response", set_path) + dialog.show() + + def __reset_disc_image(self, *_args): + self.program["disc_image"] = None + self.action_disc_image.set_subtitle(self.__default_disc_image_msg) + self.btn_disc_image_reset.set_visible(False) + def __reset_defaults(self, *_args): self.switch_dxvk.set_active(self.global_dxvk) self.switch_vkd3d.set_active(self.global_vkd3d)