Skip to content

Commit d4b498c

Browse files
committed
Add support for dynamic launcher portal
Add support for the dynamic launcher portal, replacing the previous manual creating of desktop files. Closes: #1366
1 parent 8042ef1 commit d4b498c

File tree

5 files changed

+56
-98
lines changed

5 files changed

+56
-98
lines changed

bottles/backend/state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class Signals(Enum):
5757
EagleStep = "Eagle.step" # data(Result): msg(str)
5858
EagleFinished = "Eagle.finished" # data(Result): results(dict)
5959

60+
# Dynamic launcher portal
61+
DesktopEntryCreated = "DesktopEntry.created"
62+
6063

6164
class Status(Enum):
6265
RUNNING = "running"

bottles/backend/utils/imagemagick.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@ def convert(
8585
cmd += " -flatten"
8686

8787
cmd += f" '{dest}'"
88-
subprocess.Popen(["bash", "-c", cmd])
88+
subprocess.run(["bash", "-c", cmd])

bottles/backend/utils/manager.py

Lines changed: 39 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
import os
1818
import shlex
1919
import shutil
20-
from datetime import datetime
2120
from gettext import gettext as _
22-
from glob import glob
2321
from typing import Optional
2422

2523
import icoextract # type: ignore [import-untyped]
2624

25+
from bottles.backend.params import APP_ID
26+
2727
from bottles.backend.globals import Paths
2828
from bottles.backend.logger import Logger
2929
from bottles.backend.models.config import BottleConfig
@@ -32,6 +32,10 @@
3232
from bottles.backend.utils.generic import get_mime
3333
from bottles.backend.utils.imagemagick import ImageMagickUtils
3434

35+
from gi.repository import GLib, Gio, Xdp
36+
37+
portal = Xdp.Portal()
38+
3539
logging = Logger()
3640

3741

@@ -223,101 +227,53 @@ def create_desktop_entry(
223227
program: dict,
224228
skip_icon: bool = False,
225229
custom_icon: str = "",
226-
use_xdp: bool = False,
227-
) -> bool:
228-
if not use_xdp:
229-
try:
230-
os.makedirs(Paths.applications, exist_ok=True)
231-
except OSError:
232-
return False
233-
234-
cmd_legacy = "bottles"
235-
cmd_cli = "bottles-cli"
230+
):
236231
icon = "com.usebottles.bottles-program"
237232

238-
if "FLATPAK_ID" in os.environ:
239-
cmd_legacy = "flatpak run com.usebottles.bottles"
240-
cmd_cli = "flatpak run --command=bottles-cli com.usebottles.bottles"
241-
242233
if not skip_icon and not custom_icon:
243234
icon = ManagerUtils.extract_icon(
244235
config, program.get("name"), program.get("path")
245236
)
246237
elif custom_icon:
247238
icon = custom_icon
248239

249-
if not use_xdp:
250-
file_name_template = "%s/%s--%s--%s.desktop"
251-
existing_files = glob(
252-
file_name_template
253-
% (Paths.applications, config.Name, program.get("name"), "*")
240+
def prepare_install_cb (self, result):
241+
ret = portal.dynamic_launcher_prepare_install_finish(result)
242+
id = f"{config.get('Name')}.{program.get('name')}"
243+
sum_type = GLib.ChecksumType.SHA1
244+
exec = "bottles-cli run -p {} -b '{}' -- %u".format(
245+
shlex.quote(program.get('name')), config.get('Name')
254246
)
255-
desktop_file = file_name_template % (
256-
Paths.applications,
257-
config.Name,
258-
program.get("name"),
259-
datetime.now().timestamp(),
247+
portal.dynamic_launcher_install(
248+
ret["token"],
249+
"{}.App_{}.desktop".format(
250+
APP_ID, GLib.compute_checksum_for_string(sum_type, id, -1)
251+
),
252+
"""[Desktop Entry]
253+
Exec={}
254+
Type=Application
255+
Terminal=false
256+
Categories=Application;
257+
Comment=Launch {} using Bottles.
258+
StartupWMClass={}""".format(
259+
exec, program.get("name"), program.get("name")
260+
)
260261
)
262+
SignalManager.send(Signals.DesktopEntryCreated)
261263

262-
if existing_files:
263-
for file in existing_files:
264-
os.remove(file)
265-
266-
# [Bug-]issue #4247 (single- to double-quotes in Desktop Entry spec -> "The Exec key"):
267-
with open(desktop_file, "w") as f:
268-
f.write("[Desktop Entry]\n")
269-
f.write(f"Name={program.get('name')}\n")
270-
f.write(
271-
f"Exec={cmd_cli} run -p \"{program.get('name')}\" -b \"{config.get('Name')}\" -- %u\n"
272-
)
273-
f.write("Type=Application\n")
274-
f.write("Terminal=false\n")
275-
f.write("Categories=Application;\n")
276-
f.write(f"Icon={icon}\n")
277-
f.write(f"Comment=Launch {program.get('name')} using Bottles.\n")
278-
f.write(f"StartupWMClass={program.get('name')}\n")
279-
# Actions
280-
f.write("Actions=Configure;\n")
281-
f.write("[Desktop Action Configure]\n")
282-
f.write("Name=Configure in Bottles\n")
283-
f.write(f"Exec={cmd_legacy} -b \"{config.get('Name')}\"\n")
284-
285-
return True
286-
'''
287-
WIP: the following code is not working yet, it raises an error:
288-
GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod
289-
import uuid
290-
from gi.repository import Gio, Xdp
291-
292-
portal = Xdp.Portal()
293264
if icon == "com.usebottles.bottles-program":
294-
_icon = Gio.BytesIcon.new(icon.encode("utf-8"))
265+
icon += ".svg"
266+
_icon = Gio.File.new_for_uri(
267+
f"resource:/com/usebottles/bottles/icons/scalable/apps/{icon}"
268+
)
295269
else:
296-
_icon = Gio.FileIcon.new(Gio.File.new_for_path(icon))
297-
icon_v = _icon.serialize()
298-
token = portal.dynamic_launcher_request_install_token(program.get("name"), icon_v)
299-
portal.dynamic_launcher_install(
300-
token,
301-
f"com.usebottles.bottles.{config.get('Name')}.{program.get('name')}.{str(uuid.uuid4())}.desktop",
302-
"""
303-
[Desktop Entry]
304-
Exec={}
305-
Type=Application
306-
Terminal=false
307-
Categories=Application;
308-
Comment=Launch {} using Bottles.
309-
Actions=Configure;
310-
[Desktop Action Configure]
311-
Name=Configure in Bottles
312-
Exec={}
313-
""".format(
314-
f"{cmd_cli} run -p {shlex.quote(program.get('name'))} -b '{config.get('Path')}'",
315-
program.get("name"),
316-
f"{cmd_legacy} -b '{config.get('Name')}'"
317-
).encode("utf-8")
318-
)
319-
'''
320-
return False
270+
_icon = Gio.File.new_for_path(icon)
271+
icon_v = Gio.BytesIcon.new(_icon.load_bytes()[0]).serialize()
272+
portal.dynamic_launcher_prepare_install(None,
273+
program.get("name"), icon_v,
274+
Xdp.LauncherType.APPLICATION,
275+
None, True, False, None,
276+
prepare_install_cb)
321277

322278
@staticmethod
323279
def browse_wineprefix(wineprefix: dict):

bottles/frontend/widgets/program.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from bottles.backend.managers.library import LibraryManager
2424
from bottles.backend.managers.steam import SteamManager
2525
from bottles.backend.models.result import Result
26+
from bottles.backend.state import SignalManager, Signals
2627
from bottles.backend.utils.manager import ManagerUtils
2728
from bottles.backend.utils.threading import RunAsync
2829
from bottles.backend.wine.executor import WineExecutor
@@ -34,6 +35,7 @@
3435
from bottles.frontend.windows.playtimegraph import PlaytimeGraphDialog
3536
from bottles.frontend.windows.rename import RenameDialog
3637

38+
from typing import Optional
3739

3840
# noinspection PyUnusedLocal
3941
@Gtk.Template(resource_path="/com/usebottles/bottles/program-entry.ui")
@@ -348,27 +350,21 @@ def browse_program_folder(self, _widget):
348350
self.pop_actions.popdown() # workaround #1640
349351

350352
def add_entry(self, _widget):
351-
@GtkUtils.run_in_main_loop
352-
def update(result, _error=False):
353-
if not result:
354-
webbrowser.open("https://docs.usebottles.com/bottles/programs#flatpak")
355-
return
356-
357-
self.window.show_toast(
358-
_('Desktop Entry created for "{0}"').format(self.program["name"])
359-
)
360-
361-
RunAsync(
362-
ManagerUtils.create_desktop_entry,
363-
callback=update,
353+
ManagerUtils.create_desktop_entry(
364354
config=self.config,
365355
program={
366356
"name": self.program["name"],
367357
"executable": self.program["executable"],
368358
"path": self.program["path"],
369-
},
359+
}
370360
)
371361

362+
def _on_desktop_entry_created(data: Optional[Result] = None) -> None:
363+
self.window.show_toast(
364+
_('Desktop Entry created for "{0}"').format(self.program["name"])
365+
)
366+
SignalManager.connect(Signals.DesktopEntryCreated, _on_desktop_entry_created)
367+
372368
def add_to_library(self, _widget):
373369
def update(_result, _error=False):
374370
self.window.update_library()

data/data.gresource.xml.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<gresource prefix="/com/usebottles/bottles">
44
<file preprocess="xml-stripblanks" alias="appdata">@APP_ID@.metainfo.xml</file>
55
</gresource>
6+
<gresource prefix="/com/usebottles/bottles/icons/scalable/apps">
7+
<file preprocess="xml-stripblanks" alias="com.usebottles.bottles-program.svg">icons/hicolor/scalable/apps/com.usebottles.bottles-program.svg</file>
8+
</gresource>
69
<gresource prefix="/com/usebottles/bottles/icons/scalable/actions">
710
<file preprocess="xml-stripblanks" alias="bottles-steam-symbolic.svg">icons/hicolor/symbolic/apps/bottles-steam-symbolic.svg</file>
811
<file preprocess="xml-stripblanks" alias="external-link-symbolic.svg">icons/hicolor/symbolic/actions/external-link-symbolic.svg</file>

0 commit comments

Comments
 (0)