@crossbind/extension-menu 0.0.0-bootstrap.0 → 2.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ #include "menu.h"
2
+
3
+ #include "crossbind/app.h"
4
+
5
+ #include <gio/gio.h>
6
+
7
+ #include <memory>
8
+ #include <optional>
9
+ #include <set>
10
+ #include <string>
11
+
12
+ // A notification goes to the desktop's notification server, which tells of a click on it as its default action.
13
+ namespace crossbind::menu {
14
+ namespace {
15
+
16
+ constexpr const char *NOTIFICATIONS = "org.freedesktop.Notifications";
17
+ constexpr const char *NOTIFICATIONS_PATH = "/org/freedesktop/Notifications";
18
+ // The server's own time for a notification to show.
19
+ constexpr gint32 SERVER_EXPIRY = -1;
20
+
21
+ // The notifications the server showed for the app, which another app's clicks are not.
22
+ std::set<guint32> shown;
23
+ // Whether the server reads markup in a notification's body, which then takes the page's text escaped.
24
+ std::optional<bool> isMarkupRead;
25
+
26
+ void actionInvoked(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *, GVariant *parameters, gpointer) {
27
+ guint32 id = 0;
28
+ const gchar *action = nullptr;
29
+ g_variant_get(parameters, "(u&s)", &id, &action);
30
+ if (shown.count(id) && g_strcmp0(action, "default") == 0) app::emitEventToFront("notification");
31
+ }
32
+
33
+ void notificationClosed(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *, GVariant *parameters, gpointer) {
34
+ guint32 id = 0;
35
+ g_variant_get(parameters, "(uu)", &id, nullptr);
36
+ shown.erase(id);
37
+ }
38
+
39
+ // Null without a session bus.
40
+ GDBusConnection *sessionBus() {
41
+ static GDBusConnection *bus = [] {
42
+ GDBusConnection *made = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
43
+ if (!made) return made;
44
+ // GDBus ends the app by default when the bus goes away.
45
+ g_dbus_connection_set_exit_on_close(made, FALSE);
46
+ g_dbus_connection_signal_subscribe(made, NOTIFICATIONS, NOTIFICATIONS, "ActionInvoked", NOTIFICATIONS_PATH, nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
47
+ actionInvoked, nullptr, nullptr);
48
+ g_dbus_connection_signal_subscribe(made, NOTIFICATIONS, NOTIFICATIONS, "NotificationClosed", NOTIFICATIONS_PATH, nullptr,
49
+ G_DBUS_SIGNAL_FLAGS_NONE, notificationClosed, nullptr, nullptr);
50
+ return made;
51
+ }();
52
+ return bus;
53
+ }
54
+
55
+ struct Pending {
56
+ Notification notification;
57
+ std::function<void(Failure)> done;
58
+ };
59
+
60
+ void notified(GObject *source, GAsyncResult *result, gpointer data) {
61
+ const std::unique_ptr<Pending> pending(static_cast<Pending *>(data));
62
+ GError *error = nullptr;
63
+ GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
64
+ if (!reply) {
65
+ const std::string why = std::string("crossbind: the desktop did not show the notification: ") + error->message;
66
+ g_error_free(error);
67
+ pending->done(why);
68
+ return;
69
+ }
70
+ guint32 id = 0;
71
+ g_variant_get(reply, "(u)", &id);
72
+ g_variant_unref(reply);
73
+ shown.insert(id);
74
+ pending->done(std::nullopt);
75
+ }
76
+
77
+ // Its default action is what a click on it does, which most servers show no button for.
78
+ void notify(std::unique_ptr<Pending> pending, bool isMarkup) {
79
+ const Notification &notification = pending->notification;
80
+ gchar *body = isMarkup ? g_markup_escape_text(notification.body.c_str(), -1) : g_strdup(notification.body.c_str());
81
+ const std::string file = resourceFolder() + "/icon.png";
82
+ gchar *icon = g_file_test(file.c_str(), G_FILE_TEST_EXISTS) ? g_filename_to_uri(file.c_str(), nullptr, nullptr) : nullptr;
83
+ const gchar *actions[] = {"default", "Open", nullptr};
84
+ const std::string name = appName();
85
+ GVariant *parameters = g_variant_new("(susss^as@a{sv}i)", name.c_str(), 0, icon ? icon : "", notification.title.c_str(), body, actions,
86
+ g_variant_new_array(G_VARIANT_TYPE("{sv}"), nullptr, 0), SERVER_EXPIRY);
87
+ g_free(icon);
88
+ g_free(body);
89
+ g_dbus_connection_call(sessionBus(), NOTIFICATIONS, NOTIFICATIONS_PATH, NOTIFICATIONS, "Notify", parameters, G_VARIANT_TYPE("(u)"),
90
+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, notified, pending.release());
91
+ }
92
+
93
+ // A server without the capability body-markup shows markup as it is, and one with it reads an & as markup's.
94
+ void capabilitiesRead(GObject *source, GAsyncResult *result, gpointer data) {
95
+ std::unique_ptr<Pending> pending(static_cast<Pending *>(data));
96
+ GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, nullptr);
97
+ // Notify then tells why the server cannot be reached.
98
+ if (!reply) return notify(std::move(pending), false);
99
+ const gchar **capabilities = nullptr;
100
+ g_variant_get(reply, "(^a&s)", &capabilities);
101
+ bool isMarkup = false;
102
+ for (const gchar **capability = capabilities; capability && *capability; capability += 1) {
103
+ isMarkup = isMarkup || g_strcmp0(*capability, "body-markup") == 0;
104
+ }
105
+ g_free(capabilities);
106
+ g_variant_unref(reply);
107
+ isMarkupRead = isMarkup;
108
+ notify(std::move(pending), isMarkup);
109
+ }
110
+
111
+ } // namespace
112
+
113
+ void showNotification(Notification notification, std::function<void(Failure)> done) {
114
+ onMainThread([notification = std::move(notification), done = std::move(done)] {
115
+ GDBusConnection *bus = sessionBus();
116
+ if (!bus) {
117
+ done("crossbind: the app has no session bus to show its notifications on");
118
+ return;
119
+ }
120
+ auto pending = std::make_unique<Pending>(Pending{notification, done});
121
+ if (isMarkupRead) {
122
+ notify(std::move(pending), *isMarkupRead);
123
+ return;
124
+ }
125
+ g_dbus_connection_call(bus, NOTIFICATIONS, NOTIFICATIONS_PATH, NOTIFICATIONS, "GetCapabilities", nullptr, G_VARIANT_TYPE("(as)"),
126
+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, capabilitiesRead, pending.release());
127
+ });
128
+ }
129
+
130
+ } // namespace crossbind::menu
@@ -0,0 +1,470 @@
1
+ #include "menu.h"
2
+
3
+ #include "crossbind/app.h"
4
+
5
+ #include <gtk/gtk.h>
6
+
7
+ #include <map>
8
+ #include <string>
9
+ #include <utility>
10
+ #include <vector>
11
+
12
+ // The tray's icon is a StatusNotifierItem, which the trays of KDE, Xfce and others show, and GNOME's with its
13
+ // AppIndicator extension, and its menu a com.canonical.dbusmenu, which the tray shows itself.
14
+ namespace crossbind::menu {
15
+ namespace {
16
+
17
+ constexpr const char *ITEM_PATH = "/StatusNotifierItem";
18
+ constexpr const char *ITEM_INTERFACE = "org.kde.StatusNotifierItem";
19
+ constexpr const char *MENU_PATH = "/MenuBar";
20
+ constexpr const char *MENU_INTERFACE = "com.canonical.dbusmenu";
21
+ // What an item without a menu names as its menu, as Qt's and KDE's trays read it.
22
+ constexpr const char *NO_MENU_PATH = "/NO_DBUSMENU";
23
+ constexpr const char *WATCHER = "org.kde.StatusNotifierWatcher";
24
+ constexpr const char *WATCHER_PATH = "/StatusNotifierWatcher";
25
+ constexpr guint32 DBUSMENU_VERSION = 3;
26
+ const char *const NO_TRAY = "crossbind: the desktop shows no status tray, which GNOME shows with its AppIndicator extension";
27
+
28
+ const char *const ITEM_XML = R"xml(<node>
29
+ <interface name="org.kde.StatusNotifierItem">
30
+ <property name="Category" type="s" access="read"/>
31
+ <property name="Id" type="s" access="read"/>
32
+ <property name="Title" type="s" access="read"/>
33
+ <property name="Status" type="s" access="read"/>
34
+ <property name="IconName" type="s" access="read"/>
35
+ <property name="IconThemePath" type="s" access="read"/>
36
+ <property name="ToolTip" type="(sa(iiay)ss)" access="read"/>
37
+ <property name="ItemIsMenu" type="b" access="read"/>
38
+ <property name="Menu" type="o" access="read"/>
39
+ <method name="Activate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
40
+ <method name="SecondaryActivate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
41
+ <method name="ContextMenu"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
42
+ <method name="Scroll"><arg type="i" direction="in"/><arg type="s" direction="in"/></method>
43
+ <signal name="NewTitle"/>
44
+ <signal name="NewIcon"/>
45
+ <signal name="NewToolTip"/>
46
+ <signal name="NewStatus"><arg type="s"/></signal>
47
+ </interface>
48
+ </node>)xml";
49
+
50
+ const char *const MENU_XML = R"xml(<node>
51
+ <interface name="com.canonical.dbusmenu">
52
+ <property name="Version" type="u" access="read"/>
53
+ <property name="TextDirection" type="s" access="read"/>
54
+ <property name="Status" type="s" access="read"/>
55
+ <property name="IconThemePath" type="as" access="read"/>
56
+ <method name="GetLayout">
57
+ <arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="as" direction="in"/>
58
+ <arg type="u" direction="out"/><arg type="(ia{sv}av)" direction="out"/>
59
+ </method>
60
+ <method name="GetGroupProperties">
61
+ <arg type="ai" direction="in"/><arg type="as" direction="in"/><arg type="a(ia{sv})" direction="out"/>
62
+ </method>
63
+ <method name="GetProperty">
64
+ <arg type="i" direction="in"/><arg type="s" direction="in"/><arg type="v" direction="out"/>
65
+ </method>
66
+ <method name="Event">
67
+ <arg type="i" direction="in"/><arg type="s" direction="in"/><arg type="v" direction="in"/><arg type="u" direction="in"/>
68
+ </method>
69
+ <method name="EventGroup"><arg type="a(isvu)" direction="in"/><arg type="ai" direction="out"/></method>
70
+ <method name="AboutToShow"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
71
+ <method name="AboutToShowGroup">
72
+ <arg type="ai" direction="in"/><arg type="ai" direction="out"/><arg type="ai" direction="out"/>
73
+ </method>
74
+ <signal name="ItemsPropertiesUpdated"><arg type="a(ia{sv})"/><arg type="a(ias)"/></signal>
75
+ <signal name="LayoutUpdated"><arg type="u"/><arg type="i"/></signal>
76
+ </interface>
77
+ </node>)xml";
78
+
79
+ // An item of the tray's menu by its id, which each change of the menu gives anew, so a click on one of the menu before
80
+ // finds none; the menu itself is 0.
81
+ struct Entry {
82
+ MenuItem item;
83
+ std::vector<gint32> children;
84
+ };
85
+
86
+ std::map<gint32, Entry> entries;
87
+ gint32 nextId = 1;
88
+ guint32 revision = 0;
89
+ std::string tooltip;
90
+ MenuClicked onClick;
91
+
92
+ // The tray's own connection to the session bus: a tray takes the icon away once its owner leaves the bus, as when the
93
+ // connection closes.
94
+ GDBusConnection *connection = nullptr;
95
+ guint itemObject = 0;
96
+ guint menuObject = 0;
97
+ guint watcherWatch = 0;
98
+ bool isWatcherSeen = false;
99
+ // Moves on as the connection goes, so the tray's answer for an icon taken away tells nothing.
100
+ guint generation = 0;
101
+
102
+ // The sets of the tray waiting for it to take the icon, which the first of them asked for.
103
+ std::vector<std::function<void(Failure)>> waitingSets;
104
+
105
+ Failure failure(const std::string &what, const GError *error) { return error ? what + ": " + error->message : what; }
106
+
107
+ gint32 addEntry(const MenuItem &item) {
108
+ const gint32 id = nextId++;
109
+ Entry entry{item, {}};
110
+ entry.item.items.clear();
111
+ for (const auto &child : item.items) entry.children.push_back(addEntry(child));
112
+ entries[id] = std::move(entry);
113
+ return id;
114
+ }
115
+
116
+ void setEntries(const std::vector<MenuItem> &menu) {
117
+ entries.clear();
118
+ MenuItem root;
119
+ root.isSubmenu = true;
120
+ Entry top{root, {}};
121
+ for (const auto &item : menu) top.children.push_back(addEntry(item));
122
+ entries[0] = std::move(top);
123
+ revision += 1;
124
+ }
125
+
126
+ bool hasMenu() { return !entries.at(0).children.empty(); }
127
+
128
+ // dbusmenu reads an underscore as the start of a key's name.
129
+ std::string labelOf(const std::string &label) {
130
+ std::string escaped;
131
+ for (const char c : label) escaped += c == '_' ? std::string("__") : std::string(1, c);
132
+ return escaped;
133
+ }
134
+
135
+ // The properties dbusmenu leaves out are at their defaults: a standard item, enabled and unchecked.
136
+ GVariant *propertiesOf(const Entry &entry) {
137
+ GVariantBuilder properties;
138
+ g_variant_builder_init(&properties, G_VARIANT_TYPE_VARDICT);
139
+ const MenuItem &item = entry.item;
140
+ if (item.isSeparator) {
141
+ g_variant_builder_add(&properties, "{sv}", "type", g_variant_new_string("separator"));
142
+ return g_variant_builder_end(&properties);
143
+ }
144
+ if (!item.label.empty()) g_variant_builder_add(&properties, "{sv}", "label", g_variant_new_string(labelOf(item.label).c_str()));
145
+ if (!item.isEnabled) g_variant_builder_add(&properties, "{sv}", "enabled", g_variant_new_boolean(FALSE));
146
+ if (item.checked) {
147
+ g_variant_builder_add(&properties, "{sv}", "toggle-type", g_variant_new_string("checkmark"));
148
+ g_variant_builder_add(&properties, "{sv}", "toggle-state", g_variant_new_int32(*item.checked ? 1 : 0));
149
+ }
150
+ if (item.isSubmenu) g_variant_builder_add(&properties, "{sv}", "children-display", g_variant_new_string("submenu"));
151
+ return g_variant_builder_end(&properties);
152
+ }
153
+
154
+ // A depth of -1 reaches every item below.
155
+ GVariant *layoutOf(gint32 id, gint32 depth) {
156
+ const Entry &entry = entries.at(id);
157
+ GVariantBuilder children;
158
+ g_variant_builder_init(&children, G_VARIANT_TYPE("av"));
159
+ if (depth != 0) {
160
+ for (const gint32 child : entry.children) g_variant_builder_add(&children, "v", layoutOf(child, depth - 1));
161
+ }
162
+ return g_variant_new("(i@a{sv}@av)", id, propertiesOf(entry), g_variant_builder_end(&children));
163
+ }
164
+
165
+ // A click toggles an item with checked first, which the tray hears of.
166
+ void clicked(gint32 id) {
167
+ const auto found = entries.find(id);
168
+ if (found == entries.end()) return;
169
+ MenuItem &item = found->second.item;
170
+ if (item.isSeparator || item.isSubmenu || !item.isEnabled) return;
171
+ if (item.checked) {
172
+ item.checked = !*item.checked;
173
+ GVariantBuilder updated;
174
+ g_variant_builder_init(&updated, G_VARIANT_TYPE("a(ia{sv})"));
175
+ g_variant_builder_add(&updated, "(i@a{sv})", id, propertiesOf(found->second));
176
+ g_dbus_connection_emit_signal(connection, nullptr, MENU_PATH, MENU_INTERFACE, "ItemsPropertiesUpdated",
177
+ g_variant_new("(@a(ia{sv})@a(ias))", g_variant_builder_end(&updated),
178
+ g_variant_new_array(G_VARIANT_TYPE("(ias)"), nullptr, 0)),
179
+ nullptr);
180
+ }
181
+ if (onClick && !item.id.empty()) onClick(0, item.id, item.checked);
182
+ }
183
+
184
+ void returnNoEntry(GDBusMethodInvocation *invocation, gint32 id) {
185
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "the tray's menu has no item %d", id);
186
+ }
187
+
188
+ GVariant *noIds() { return g_variant_new_array(G_VARIANT_TYPE_INT32, nullptr, 0); }
189
+
190
+ // GDBus answers a call of a method the XML has not.
191
+ void menuCalled(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *method, GVariant *parameters,
192
+ GDBusMethodInvocation *invocation, gpointer) {
193
+ const std::string name = method;
194
+ if (name == "GetLayout") {
195
+ gint32 parent = 0;
196
+ gint32 depth = 0;
197
+ g_variant_get(parameters, "(ii@as)", &parent, &depth, nullptr);
198
+ if (!entries.count(parent)) return returnNoEntry(invocation, parent);
199
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(u@(ia{sv}av))", revision, layoutOf(parent, depth)));
200
+ } else if (name == "GetGroupProperties") {
201
+ GVariantIter *ids = nullptr;
202
+ g_variant_get(parameters, "(ai@as)", &ids, nullptr);
203
+ GVariantBuilder found;
204
+ g_variant_builder_init(&found, G_VARIANT_TYPE("a(ia{sv})"));
205
+ gint32 id = 0;
206
+ while (g_variant_iter_next(ids, "i", &id)) {
207
+ if (const auto entry = entries.find(id); entry != entries.end()) {
208
+ g_variant_builder_add(&found, "(i@a{sv})", id, propertiesOf(entry->second));
209
+ }
210
+ }
211
+ g_variant_iter_free(ids);
212
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(@a(ia{sv}))", g_variant_builder_end(&found)));
213
+ } else if (name == "GetProperty") {
214
+ gint32 id = 0;
215
+ const gchar *property = nullptr;
216
+ g_variant_get(parameters, "(i&s)", &id, &property);
217
+ const auto entry = entries.find(id);
218
+ if (entry == entries.end()) return returnNoEntry(invocation, id);
219
+ GVariant *properties = g_variant_ref_sink(propertiesOf(entry->second));
220
+ GVariant *value = g_variant_lookup_value(properties, property, nullptr);
221
+ g_variant_unref(properties);
222
+ if (!value) {
223
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "the item has no %s", property);
224
+ return;
225
+ }
226
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(@v)", g_variant_new_variant(value)));
227
+ g_variant_unref(value);
228
+ } else if (name == "Event") {
229
+ gint32 id = 0;
230
+ const gchar *event = nullptr;
231
+ g_variant_get(parameters, "(i&svu)", &id, &event, nullptr, nullptr);
232
+ if (!entries.count(id)) return returnNoEntry(invocation, id);
233
+ if (g_strcmp0(event, "clicked") == 0) clicked(id);
234
+ g_dbus_method_invocation_return_value(invocation, nullptr);
235
+ } else if (name == "EventGroup") {
236
+ GVariantIter *events = nullptr;
237
+ g_variant_get(parameters, "(a(isvu))", &events);
238
+ GVariantBuilder missing;
239
+ g_variant_builder_init(&missing, G_VARIANT_TYPE("ai"));
240
+ gint32 id = 0;
241
+ const gchar *event = nullptr;
242
+ while (g_variant_iter_next(events, "(i&svu)", &id, &event, nullptr, nullptr)) {
243
+ if (!entries.count(id)) {
244
+ g_variant_builder_add(&missing, "i", id);
245
+ } else if (g_strcmp0(event, "clicked") == 0) {
246
+ clicked(id);
247
+ }
248
+ }
249
+ g_variant_iter_free(events);
250
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ai)", g_variant_builder_end(&missing)));
251
+ } else if (name == "AboutToShow") {
252
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", FALSE));
253
+ } else if (name == "AboutToShowGroup") {
254
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(@ai@ai)", noIds(), noIds()));
255
+ }
256
+ }
257
+
258
+ GVariant *menuProperty(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *property, GError **, gpointer) {
259
+ const std::string name = property;
260
+ if (name == "Version") return g_variant_new_uint32(DBUSMENU_VERSION);
261
+ if (name == "TextDirection") return g_variant_new_string(gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL ? "rtl" : "ltr");
262
+ if (name == "Status") return g_variant_new_string("normal");
263
+ return g_variant_new_strv(nullptr, 0);
264
+ }
265
+
266
+ // The app's icon, which a Linux app's build puts in its files.
267
+ bool hasIcon() {
268
+ static const bool has = g_file_test((resourceFolder() + "/icon.png").c_str(), G_FILE_TEST_EXISTS);
269
+ return has;
270
+ }
271
+
272
+ GVariant *toolTip() {
273
+ return g_variant_new("(s@a(iiay)ss)", "", g_variant_new_array(G_VARIANT_TYPE("(iiay)"), nullptr, 0), tooltip.c_str(), "");
274
+ }
275
+
276
+ GVariant *menuPath() { return g_variant_new_object_path(hasMenu() ? MENU_PATH : NO_MENU_PATH); }
277
+
278
+ GVariant *itemProperty(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *property, GError **, gpointer) {
279
+ const std::string name = property;
280
+ if (name == "Category") return g_variant_new_string("ApplicationStatus");
281
+ if (name == "Id") return g_variant_new_string(appId().c_str());
282
+ if (name == "Title") return g_variant_new_string(appName().c_str());
283
+ if (name == "Status") return g_variant_new_string("Active");
284
+ // A tray finds the app's icon by its name in the app's files, as an AppIndicator's, or one of its theme for an app
285
+ // without an icon.
286
+ if (name == "IconName") return g_variant_new_string(hasIcon() ? "icon" : "application-x-executable");
287
+ if (name == "IconThemePath") return g_variant_new_string(hasIcon() ? resourceFolder().c_str() : "");
288
+ if (name == "ToolTip") return toolTip();
289
+ if (name == "ItemIsMenu") return g_variant_new_boolean(FALSE);
290
+ return menuPath();
291
+ }
292
+
293
+ // A tray calls ContextMenu on a right click of an item without a menu, which the item would show itself.
294
+ void itemCalled(GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *method, GVariant *,
295
+ GDBusMethodInvocation *invocation, gpointer) {
296
+ const std::string name = method;
297
+ if (name == "Activate" || (name == "ContextMenu" && !hasMenu())) app::emitEventToFront("tray");
298
+ g_dbus_method_invocation_return_value(invocation, nullptr);
299
+ }
300
+
301
+ const GDBusInterfaceVTable ITEM_CALLS{itemCalled, itemProperty, nullptr, {}};
302
+ const GDBusInterfaceVTable MENU_CALLS{menuCalled, menuProperty, nullptr, {}};
303
+
304
+ void disconnect() {
305
+ generation += 1;
306
+ if (!connection) return;
307
+ if (watcherWatch) g_bus_unwatch_name(watcherWatch);
308
+ if (menuObject) g_dbus_connection_unregister_object(connection, menuObject);
309
+ if (itemObject) g_dbus_connection_unregister_object(connection, itemObject);
310
+ g_dbus_connection_close(connection, nullptr, nullptr, nullptr);
311
+ g_object_unref(connection);
312
+ connection = nullptr;
313
+ watcherWatch = 0;
314
+ menuObject = 0;
315
+ itemObject = 0;
316
+ }
317
+
318
+ void answerSets(const Failure &failure) {
319
+ for (auto &done : std::exchange(waitingSets, {})) done(failure);
320
+ }
321
+
322
+ bool isMissing(const GError *error) {
323
+ return g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER);
324
+ }
325
+
326
+ // The answer of a call made for an icon the page took away meanwhile tells nothing, as removeTray answered its sets.
327
+ bool isCurrent(gpointer generationAsked) { return GPOINTER_TO_UINT(generationAsked) == generation; }
328
+
329
+ void registered(GObject *source, GAsyncResult *result, gpointer data) {
330
+ GError *error = nullptr;
331
+ if (GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error)) g_variant_unref(reply);
332
+ if (!isCurrent(data)) {
333
+ g_clear_error(&error);
334
+ return;
335
+ }
336
+ if (!error) {
337
+ answerSets(std::nullopt);
338
+ return;
339
+ }
340
+ const Failure why = isMissing(error) ? Failure(NO_TRAY) : failure("crossbind: the desktop's status tray did not take the app's icon", error);
341
+ g_error_free(error);
342
+ disconnect();
343
+ answerSets(why);
344
+ }
345
+
346
+ // A tray that started anew and did not take the icon back leaves it to the next start.
347
+ void registeredAgain(GObject *source, GAsyncResult *result, gpointer) {
348
+ GError *error = nullptr;
349
+ if (GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error)) g_variant_unref(reply);
350
+ g_clear_error(&error);
351
+ }
352
+
353
+ // The tray takes the item by its path, the connection's own name being the bus's.
354
+ void registerItem(GAsyncReadyCallback answered) {
355
+ g_dbus_connection_call(connection, WATCHER, WATCHER_PATH, WATCHER, "RegisterStatusNotifierItem", g_variant_new("(s)", ITEM_PATH), nullptr,
356
+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, answered, GUINT_TO_POINTER(generation));
357
+ }
358
+
359
+ // A watcher without a tray, as KDE's outside Plasma, would take the icon and show it nowhere. One that cannot tell is
360
+ // taken at its word.
361
+ void hostChecked(GObject *source, GAsyncResult *result, gpointer data) {
362
+ GError *error = nullptr;
363
+ GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
364
+ bool hasTray = !isMissing(error);
365
+ if (reply) {
366
+ GVariant *value = nullptr;
367
+ g_variant_get(reply, "(v)", &value);
368
+ hasTray = !g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN) || g_variant_get_boolean(value);
369
+ g_variant_unref(value);
370
+ g_variant_unref(reply);
371
+ }
372
+ g_clear_error(&error);
373
+ if (!isCurrent(data)) return;
374
+ if (!hasTray) {
375
+ disconnect();
376
+ answerSets(NO_TRAY);
377
+ return;
378
+ }
379
+ registerItem(registered);
380
+ }
381
+
382
+ void checkHost() {
383
+ g_dbus_connection_call(connection, WATCHER, WATCHER_PATH, "org.freedesktop.DBus.Properties", "Get",
384
+ g_variant_new("(ss)", WATCHER, "IsStatusNotifierHostRegistered"), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, -1,
385
+ nullptr, hostChecked, GUINT_TO_POINTER(generation));
386
+ }
387
+
388
+ // A tray that starts anew, as when the desktop's shell restarts, takes the icon again. The watch tells of the one there
389
+ // at first as well, which takes it.
390
+ void watcherAppeared(GDBusConnection *, const gchar *, const gchar *, gpointer) {
391
+ if (std::exchange(isWatcherSeen, true)) registerItem(registeredAgain);
392
+ }
393
+
394
+ Failure connect() {
395
+ GError *error = nullptr;
396
+ gchar *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error);
397
+ if (address) {
398
+ const auto flags = static_cast<GDBusConnectionFlags>(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION);
399
+ connection = g_dbus_connection_new_for_address_sync(address, flags, nullptr, nullptr, &error);
400
+ g_free(address);
401
+ }
402
+ if (!connection) {
403
+ const Failure why = failure("crossbind: the app has no session bus to show its tray icon on", error);
404
+ g_clear_error(&error);
405
+ return why;
406
+ }
407
+ static GDBusNodeInfo *item = g_dbus_node_info_new_for_xml(ITEM_XML, nullptr);
408
+ static GDBusNodeInfo *menu = g_dbus_node_info_new_for_xml(MENU_XML, nullptr);
409
+ itemObject = g_dbus_connection_register_object(connection, ITEM_PATH, item->interfaces[0], &ITEM_CALLS, nullptr, nullptr, &error);
410
+ if (itemObject) menuObject = g_dbus_connection_register_object(connection, MENU_PATH, menu->interfaces[0], &MENU_CALLS, nullptr, nullptr, &error);
411
+ if (!menuObject) {
412
+ const Failure why = failure("crossbind: the tray's icon cannot go on the session bus", error);
413
+ g_clear_error(&error);
414
+ disconnect();
415
+ return why;
416
+ }
417
+ isWatcherSeen = false;
418
+ watcherWatch = g_bus_watch_name_on_connection(connection, WATCHER, G_BUS_NAME_WATCHER_FLAGS_NONE, watcherAppeared, nullptr, nullptr, nullptr);
419
+ return std::nullopt;
420
+ }
421
+
422
+ // A tray reads its item's properties anew on a signal of the item's, and the menu on LayoutUpdated.
423
+ void announceChanges() {
424
+ g_dbus_connection_emit_signal(connection, nullptr, MENU_PATH, MENU_INTERFACE, "LayoutUpdated", g_variant_new("(ui)", revision, 0), nullptr);
425
+ g_dbus_connection_emit_signal(connection, nullptr, ITEM_PATH, ITEM_INTERFACE, "NewToolTip", nullptr, nullptr);
426
+ GVariantBuilder changed;
427
+ g_variant_builder_init(&changed, G_VARIANT_TYPE_VARDICT);
428
+ g_variant_builder_add(&changed, "{sv}", "ToolTip", toolTip());
429
+ g_variant_builder_add(&changed, "{sv}", "Menu", menuPath());
430
+ g_dbus_connection_emit_signal(connection, nullptr, ITEM_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged",
431
+ g_variant_new("(s@a{sv}@as)", ITEM_INTERFACE, g_variant_builder_end(&changed), g_variant_new_strv(nullptr, 0)),
432
+ nullptr);
433
+ }
434
+
435
+ } // namespace
436
+
437
+ // The tray answers once it took the icon, and rejects when the desktop has none; a set before it answered answers with
438
+ // the first.
439
+ void setTray(TrayOptions options, MenuClicked clicked, std::function<void(Failure)> done) {
440
+ onMainThread([options = std::move(options), clicked = std::move(clicked), done = std::move(done)] {
441
+ setEntries(options.menu);
442
+ tooltip = options.tooltip;
443
+ onClick = clicked;
444
+ if (connection) {
445
+ announceChanges();
446
+ if (waitingSets.empty()) {
447
+ done(std::nullopt);
448
+ } else {
449
+ waitingSets.push_back(done);
450
+ }
451
+ return;
452
+ }
453
+ if (const Failure failed = connect()) {
454
+ done(failed);
455
+ return;
456
+ }
457
+ waitingSets.push_back(done);
458
+ checkHost();
459
+ });
460
+ }
461
+
462
+ void removeTray(std::function<void(Failure)> done) {
463
+ onMainThread([done = std::move(done)] {
464
+ disconnect();
465
+ answerSets(std::nullopt);
466
+ done(std::nullopt);
467
+ });
468
+ }
469
+
470
+ } // namespace crossbind::menu