By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 dbusmenu.py

View raw Download
text/x-script.python • 7.17 kiB
Python script, ASCII text executable
        
            
1
#!/usr/bin/env python3
2
"""
3
Translates the com.canonical.dbusmenu protocol to Gio menus.
4
Based on a C++ implementation.
5
Thanks to https://github.com/WayfireWM/wf-shell/blob/master/src/panel/widgets/tray/item.hpp
6
for the C++ version which is Copyright 2025, trigg <https://github.com/trigg>,
7
MIT/Expat Licence.
8
Python version: Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
9
10
This file (and no other files, unless otherwise stated) is licensed under the
11
MIT/Expat Licence:
12
13
Permission is hereby granted, free of charge, to any person obtaining a copy
14
of this software and associated documentation files (the "Software"), to deal
15
in the Software without restriction, including without limitation the rights
16
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
copies of the Software, and to permit persons to whom the Software is
18
furnished to do so, subject to the following conditions:
19
20
The above copyright notice and this permission notice shall be included in all
21
copies or substantial portions of the Software.
22
23
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
SOFTWARE.
30
"""
31
32
import typing
33
import uuid
34
35
from gi.repository import GLib, Gio, GObject, Dbusmenu
36
37
class CanonicalMenuModel(GObject.Object):
38
"""Implements com.canonical.dbusmenu protocol.
39
Provides a `menu` attribute to get a Gio.Menu for the specified DBus object, and an
40
`actions` attribute that is a Gio.ActionGroup with the menu actions to insert where needed.
41
"""
42
def __init__(self):
43
"""Generate a disconnected instance."""
44
super().__init__()
45
self.prefix = None
46
self.client = None
47
self.prefix = ""
48
self.actions = Gio.SimpleActionGroup()
49
self.menu: typing.Optional[Gio.Menu] = Gio.Menu()
50
51
def connect_dbus(self, dbus_name: str, menu_path: str, prefix: str) -> None:
52
"""Connect this menu with the specified DBus object.
53
:param dbus_name: The bus name to use, like ":1.360" or "org.gnome.Fractal"
54
:param menu_path: The DBus path to the menu. It can be found in the Menu attribute of an
55
org.kde.StatusNotifierItem.
56
:param prefix: The prefix to use for action names; use this name for inserting the
57
action group.
58
"""
59
self.prefix = prefix
60
self.client = Dbusmenu.Client.new(dbus_name, menu_path)
61
self.client.connect("layout-updated", self._layout_updated)
62
root = self.client.get_root()
63
self.reconstitute(root)
64
65
def reconstitute(self, root_item: Dbusmenu.Menuitem):
66
"""Regenerate the menu.
67
:param root_item: The item to recursively generate from.
68
"""
69
self.menu.remove_all()
70
for action_name in self.actions.list_actions():
71
self.actions.remove_action(action_name)
72
73
self._iterate_children(self.menu, root_item, 0)
74
75
def _iterate_children(self, parent_menu, parent, count):
76
if isinstance(parent, Dbusmenu.Menuitem):
77
current_section = Gio.Menu()
78
for child in parent.get_children():
79
label = child.property_get("label")
80
if not label:
81
parent_menu.append_section(None, current_section)
82
current_section = Gio.Menu()
83
continue
84
85
menu_type = ""
86
icon_name = ""
87
toggle_type = ""
88
89
if child.property_exist("type"):
90
menu_type = child.property_get("type")
91
enabled = child.property_get_bool("enabled")
92
if child.property_exist("icon-name"):
93
icon_name = child.property_get("icon-name")
94
toggle_state = child.property_get_int("toggle-state")
95
if child.property_exist("toggle-type"):
96
icon_name = child.property_get("toggle-type")
97
has_children = bool(child.get_children())
98
99
if has_children:
100
submenu = Gio.Menu()
101
count = self._iterate_children(submenu, child, count)
102
current_section.append_submenu(label, submenu)
103
else:
104
item = Gio.MenuItem.new(label, "a")
105
if enabled:
106
# Difference from C++ version: we don't use the label, to make it easier
107
# to translate
108
action_name = f"mi{count}"
109
action_string = f"{self.prefix}.{action_name}"
110
if toggle_type == "radio":
111
item.set_action_and_target_value(action_string, GLib.Variant.new_string(action_name))
112
boolean_action = Gio.SimpleAction(name=action_name, state=GLib.Variant.new_string(action_name if toggle_state else ""))
113
114
# A default argument is needed, otherwise the last item will be used
115
def on_activate(action, vb, item=child):
116
data = GLib.Variant.new_int32(0)
117
item.handle_event("clicked", data, 0)
118
119
boolean_action.connect("activate", on_activate)
120
self.actions.add_action(boolean_action)
121
elif toggle_type == "check":
122
item.set_action_and_target_value(action_string, GLib.Variant.new_boolean(toggle_state))
123
boolean_action = Gio.SimpleAction(name=action_name, state=GLib.Variant.new_boolean(toggle_state))
124
125
def on_activate(action, vb, item=child):
126
data = GLib.Variant.new_int32(0)
127
item.handle_event("clicked", data, 0)
128
129
boolean_action.connect("activate", on_activate)
130
self.actions.add_action(boolean_action)
131
else:
132
item.set_detailed_action(action_string)
133
if icon_name:
134
item.set_icon(Gio.ThemedIcon.new(icon_name))
135
action = Gio.SimpleAction(name=action_name)
136
137
def on_activate(action, vb, item=child):
138
data = GLib.Variant.new_int32(0)
139
item.handle_event("clicked", data, 0)
140
141
action.connect("activate", on_activate)
142
self.actions.add_action(action)
143
current_section.append_item(item)
144
count += 1
145
parent_menu.append_section(None, current_section)
146
return count
147
148
def _layout_updated(self, item):
149
if self.client is None:
150
return
151
152
root = self.client.get_root()
153
self.reconstitute(root)
154
self.emit("menu_changed")
155
156
@GObject.Signal
157
def menu_changed(self):
158
pass
159
160