From d4b498cb1f8970aed0d1c8c9afc36a04cc3463b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Kuchci=C5=84ski?= Date: Sun, 2 Nov 2025 22:39:27 +0100 Subject: [PATCH] Add support for dynamic launcher portal Add support for the dynamic launcher portal, replacing the previous manual creating of desktop files. Closes: https://github.com/bottlesdevs/Bottles/issues/1366 --- bottles/backend/state.py | 3 + bottles/backend/utils/imagemagick.py | 2 +- bottles/backend/utils/manager.py | 122 +++++++++------------------ bottles/frontend/widgets/program.py | 24 +++--- data/data.gresource.xml.in | 3 + 5 files changed, 56 insertions(+), 98 deletions(-) diff --git a/bottles/backend/state.py b/bottles/backend/state.py index faa0ac1ff9..5bb7fcb082 100644 --- a/bottles/backend/state.py +++ b/bottles/backend/state.py @@ -57,6 +57,9 @@ class Signals(Enum): EagleStep = "Eagle.step" # data(Result): msg(str) EagleFinished = "Eagle.finished" # data(Result): results(dict) + # Dynamic launcher portal + DesktopEntryCreated = "DesktopEntry.created" + class Status(Enum): RUNNING = "running" diff --git a/bottles/backend/utils/imagemagick.py b/bottles/backend/utils/imagemagick.py index bdeda075e9..d44ed6f956 100644 --- a/bottles/backend/utils/imagemagick.py +++ b/bottles/backend/utils/imagemagick.py @@ -85,4 +85,4 @@ def convert( cmd += " -flatten" cmd += f" '{dest}'" - subprocess.Popen(["bash", "-c", cmd]) + subprocess.run(["bash", "-c", cmd]) diff --git a/bottles/backend/utils/manager.py b/bottles/backend/utils/manager.py index 3c879c7186..c3b9ecfad5 100644 --- a/bottles/backend/utils/manager.py +++ b/bottles/backend/utils/manager.py @@ -17,13 +17,13 @@ import os import shlex import shutil -from datetime import datetime from gettext import gettext as _ -from glob import glob from typing import Optional import icoextract # type: ignore [import-untyped] +from bottles.backend.params import APP_ID + from bottles.backend.globals import Paths from bottles.backend.logger import Logger from bottles.backend.models.config import BottleConfig @@ -32,6 +32,10 @@ from bottles.backend.utils.generic import get_mime from bottles.backend.utils.imagemagick import ImageMagickUtils +from gi.repository import GLib, Gio, Xdp + +portal = Xdp.Portal() + logging = Logger() @@ -223,22 +227,9 @@ def create_desktop_entry( program: dict, skip_icon: bool = False, custom_icon: str = "", - use_xdp: bool = False, - ) -> bool: - if not use_xdp: - try: - os.makedirs(Paths.applications, exist_ok=True) - except OSError: - return False - - cmd_legacy = "bottles" - cmd_cli = "bottles-cli" + ): icon = "com.usebottles.bottles-program" - if "FLATPAK_ID" in os.environ: - cmd_legacy = "flatpak run com.usebottles.bottles" - cmd_cli = "flatpak run --command=bottles-cli com.usebottles.bottles" - if not skip_icon and not custom_icon: icon = ManagerUtils.extract_icon( config, program.get("name"), program.get("path") @@ -246,78 +237,43 @@ def create_desktop_entry( elif custom_icon: icon = custom_icon - if not use_xdp: - file_name_template = "%s/%s--%s--%s.desktop" - existing_files = glob( - file_name_template - % (Paths.applications, config.Name, program.get("name"), "*") + def prepare_install_cb (self, result): + ret = portal.dynamic_launcher_prepare_install_finish(result) + id = f"{config.get('Name')}.{program.get('name')}" + sum_type = GLib.ChecksumType.SHA1 + exec = "bottles-cli run -p {} -b '{}' -- %u".format( + shlex.quote(program.get('name')), config.get('Name') ) - desktop_file = file_name_template % ( - Paths.applications, - config.Name, - program.get("name"), - datetime.now().timestamp(), + portal.dynamic_launcher_install( + ret["token"], + "{}.App_{}.desktop".format( + APP_ID, GLib.compute_checksum_for_string(sum_type, id, -1) + ), + """[Desktop Entry] + Exec={} + Type=Application + Terminal=false + Categories=Application; + Comment=Launch {} using Bottles. + StartupWMClass={}""".format( + exec, program.get("name"), program.get("name") + ) ) + SignalManager.send(Signals.DesktopEntryCreated) - if existing_files: - for file in existing_files: - os.remove(file) - - # [Bug-]issue #4247 (single- to double-quotes in Desktop Entry spec -> "The Exec key"): - with open(desktop_file, "w") as f: - f.write("[Desktop Entry]\n") - f.write(f"Name={program.get('name')}\n") - f.write( - f"Exec={cmd_cli} run -p \"{program.get('name')}\" -b \"{config.get('Name')}\" -- %u\n" - ) - f.write("Type=Application\n") - f.write("Terminal=false\n") - f.write("Categories=Application;\n") - f.write(f"Icon={icon}\n") - f.write(f"Comment=Launch {program.get('name')} using Bottles.\n") - f.write(f"StartupWMClass={program.get('name')}\n") - # Actions - f.write("Actions=Configure;\n") - f.write("[Desktop Action Configure]\n") - f.write("Name=Configure in Bottles\n") - f.write(f"Exec={cmd_legacy} -b \"{config.get('Name')}\"\n") - - return True - ''' - WIP: the following code is not working yet, it raises an error: - GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod - import uuid - from gi.repository import Gio, Xdp - - portal = Xdp.Portal() if icon == "com.usebottles.bottles-program": - _icon = Gio.BytesIcon.new(icon.encode("utf-8")) + icon += ".svg" + _icon = Gio.File.new_for_uri( + f"resource:/com/usebottles/bottles/icons/scalable/apps/{icon}" + ) else: - _icon = Gio.FileIcon.new(Gio.File.new_for_path(icon)) - icon_v = _icon.serialize() - token = portal.dynamic_launcher_request_install_token(program.get("name"), icon_v) - portal.dynamic_launcher_install( - token, - f"com.usebottles.bottles.{config.get('Name')}.{program.get('name')}.{str(uuid.uuid4())}.desktop", - """ - [Desktop Entry] - Exec={} - Type=Application - Terminal=false - Categories=Application; - Comment=Launch {} using Bottles. - Actions=Configure; - [Desktop Action Configure] - Name=Configure in Bottles - Exec={} - """.format( - f"{cmd_cli} run -p {shlex.quote(program.get('name'))} -b '{config.get('Path')}'", - program.get("name"), - f"{cmd_legacy} -b '{config.get('Name')}'" - ).encode("utf-8") - ) - ''' - return False + _icon = Gio.File.new_for_path(icon) + icon_v = Gio.BytesIcon.new(_icon.load_bytes()[0]).serialize() + portal.dynamic_launcher_prepare_install(None, + program.get("name"), icon_v, + Xdp.LauncherType.APPLICATION, + None, True, False, None, + prepare_install_cb) @staticmethod def browse_wineprefix(wineprefix: dict): diff --git a/bottles/frontend/widgets/program.py b/bottles/frontend/widgets/program.py index 61542da9d0..5778029d51 100644 --- a/bottles/frontend/widgets/program.py +++ b/bottles/frontend/widgets/program.py @@ -23,6 +23,7 @@ from bottles.backend.managers.library import LibraryManager from bottles.backend.managers.steam import SteamManager from bottles.backend.models.result import Result +from bottles.backend.state import SignalManager, Signals from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.threading import RunAsync from bottles.backend.wine.executor import WineExecutor @@ -34,6 +35,7 @@ from bottles.frontend.windows.playtimegraph import PlaytimeGraphDialog from bottles.frontend.windows.rename import RenameDialog +from typing import Optional # noinspection PyUnusedLocal @Gtk.Template(resource_path="/com/usebottles/bottles/program-entry.ui") @@ -348,27 +350,21 @@ def browse_program_folder(self, _widget): self.pop_actions.popdown() # workaround #1640 def add_entry(self, _widget): - @GtkUtils.run_in_main_loop - def update(result, _error=False): - if not result: - webbrowser.open("https://docs.usebottles.com/bottles/programs#flatpak") - return - - self.window.show_toast( - _('Desktop Entry created for "{0}"').format(self.program["name"]) - ) - - RunAsync( - ManagerUtils.create_desktop_entry, - callback=update, + ManagerUtils.create_desktop_entry( config=self.config, program={ "name": self.program["name"], "executable": self.program["executable"], "path": self.program["path"], - }, + } ) + def _on_desktop_entry_created(data: Optional[Result] = None) -> None: + self.window.show_toast( + _('Desktop Entry created for "{0}"').format(self.program["name"]) + ) + SignalManager.connect(Signals.DesktopEntryCreated, _on_desktop_entry_created) + def add_to_library(self, _widget): def update(_result, _error=False): self.window.update_library() diff --git a/data/data.gresource.xml.in b/data/data.gresource.xml.in index 13b85cd35f..c200be6c3d 100644 --- a/data/data.gresource.xml.in +++ b/data/data.gresource.xml.in @@ -3,6 +3,9 @@ @APP_ID@.metainfo.xml + + icons/hicolor/scalable/apps/com.usebottles.bottles-program.svg + icons/hicolor/symbolic/apps/bottles-steam-symbolic.svg icons/hicolor/symbolic/actions/external-link-symbolic.svg