1 /*
2  * Copyright (C) 2016 Alberts Muktupāvels
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include <config.h>
19 
20 #include "sn-dbus-menu.h"
21 #include "sn-dbus-menu-gen.h"
22 #include "sn-dbus-menu-item.h"
23 
24 struct _SnDBusMenu
25 {
26   GtkMenu        parent;
27 
28   GHashTable    *items;
29 
30   GCancellable  *cancellable;
31 
32   gchar         *bus_name;
33   gchar         *object_path;
34   guint          name_id;
35 
36   SnDBusMenuGen *proxy;
37 };
38 
39 enum
40 {
41   PROP_0,
42 
43   PROP_BUS_NAME,
44   PROP_OBJECT_PATH,
45 
46   LAST_PROP
47 };
48 
49 static GParamSpec *properties[LAST_PROP] = { NULL };
50 
51 static const gchar *property_names[] =
52 {
53   "accessible-desc",
54   "children-display",
55   "disposition",
56   "enabled",
57   "icon-data",
58   "icon-name",
59   "label",
60   "shortcut",
61   "toggle-type",
62   "toggle-state",
63   "type",
64   "visible",
65   NULL
66 };
67 
G_DEFINE_TYPE(SnDBusMenu,sn_dbus_menu,GTK_TYPE_MENU)68 G_DEFINE_TYPE (SnDBusMenu, sn_dbus_menu, GTK_TYPE_MENU)
69 
70 static void
71 activate_cb (GtkWidget  *widget,
72              SnDBusMenu *menu)
73 {
74   guint id;
75 
76   if (gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget)) != NULL)
77     return;
78 
79   id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "item-id"));
80   sn_dbus_menu_gen_call_event_sync (menu->proxy, id, "clicked",
81                                     g_variant_new ("v", g_variant_new_int32 (0)),
82                                     gtk_get_current_event_time (), NULL, NULL);
83 }
84 
85 static GtkMenu *
layout_update_item(SnDBusMenu * menu,GtkMenu * gtk_menu,guint id,GVariant * props)86 layout_update_item (SnDBusMenu *menu,
87                     GtkMenu    *gtk_menu,
88                     guint       id,
89                     GVariant   *props)
90 {
91   SnDBusMenuItem *item;
92 
93   if (id == 0)
94     return gtk_menu;
95 
96   item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
97 
98   if (item == NULL)
99     {
100       item = sn_dbus_menu_item_new (props);
101 
102       g_object_set_data (G_OBJECT (item->item), "item-id", GUINT_TO_POINTER (id));
103       gtk_menu_shell_append (GTK_MENU_SHELL (gtk_menu), item->item);
104 
105       item->activate_id = g_signal_connect (item->item, "activate",
106                                             G_CALLBACK (activate_cb), menu);
107 
108       g_hash_table_replace (menu->items, GUINT_TO_POINTER (id), item);
109     }
110   else
111     {
112       sn_dbus_menu_item_update_props (item, props);
113     }
114   return item->submenu;
115 }
116 
117 static void
layout_parse(SnDBusMenu * menu,GVariant * layout,GtkMenu * gtk_menu)118 layout_parse (SnDBusMenu *menu,
119               GVariant   *layout,
120               GtkMenu    *gtk_menu)
121 {
122   guint id;
123   GVariant *props;
124   GVariant *items;
125   GtkMenu *submenu;
126   GVariantIter iter;
127   GVariant *child;
128 
129   if (!g_variant_is_of_type (layout, G_VARIANT_TYPE ("(ia{sv}av)")))
130     {
131       g_warning ("Type of return value for 'layout' property in "
132                  "'GetLayout' call should be '(ia{sv}av)' but got '%s'",
133                  g_variant_get_type_string (layout));
134 
135       return;
136     }
137 
138   g_variant_get (layout, "(i@a{sv}@av)", &id, &props, &items);
139 
140   submenu = layout_update_item (menu, gtk_menu, id, props);
141   g_variant_unref (props);
142 
143   g_variant_iter_init (&iter, items);
144   while ((child = g_variant_iter_next_value (&iter)))
145     {
146       GVariant *value;
147 
148       value = g_variant_get_variant (child);
149 
150       layout_parse (menu, value, submenu);
151       g_variant_unref (value);
152 
153       g_variant_unref (child);
154     }
155 
156   g_variant_unref (items);
157 }
158 
159 static void
get_layout_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)160 get_layout_cb (GObject      *source_object,
161                GAsyncResult *res,
162                gpointer      user_data)
163 {
164   GVariant *layout;
165   guint revision;
166   GError *error;
167   SnDBusMenu *menu;
168 
169   error = NULL;
170   sn_dbus_menu_gen_call_get_layout_finish (SN_DBUS_MENU_GEN (source_object),
171                                            &revision, &layout, res, &error);
172 
173   if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
174     {
175       g_error_free (error);
176       return;
177     }
178 
179   menu = SN_DBUS_MENU (user_data);
180 
181   if (error != NULL)
182     {
183       g_warning ("%s", error->message);
184       g_error_free (error);
185       return;
186     }
187 
188   g_hash_table_remove_all (menu->items);
189   layout_parse (menu, layout, GTK_MENU (menu));
190 
191   /* Reposition menu to accomodate any size changes   */
192   /* Menu size never changes with GTK 3.20 or earlier */
193   gtk_menu_reposition(GTK_MENU(menu));
194 
195   g_variant_unref (layout);
196 }
197 
198 static void
update_layout(SnDBusMenu * menu,gint parent)199 update_layout (SnDBusMenu *menu,
200                gint        parent)
201 {
202   gint depth;
203 
204   parent = 0;
205   depth = -1;
206 
207   sn_dbus_menu_gen_call_get_layout (menu->proxy, parent, depth,
208                                     property_names, menu->cancellable,
209                                     get_layout_cb, menu);
210 }
211 
212 static void
items_properties_updated_cb(SnDBusMenuGen * proxy,GVariant * updated_props,GVariant * removed_props,SnDBusMenu * menu)213 items_properties_updated_cb (SnDBusMenuGen *proxy,
214                              GVariant      *updated_props,
215                              GVariant      *removed_props,
216                              SnDBusMenu    *menu)
217 {
218   GVariantIter iter;
219   guint id;
220   GVariant *props;
221   SnDBusMenuItem *item;
222 
223   g_variant_iter_init (&iter, updated_props);
224   while (g_variant_iter_next (&iter, "(i@a{sv})", &id, &props))
225     {
226       item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
227 
228       if (item != NULL)
229         sn_dbus_menu_item_update_props (item, props);
230 
231       g_variant_unref (props);
232     }
233 
234   g_variant_iter_init (&iter, removed_props);
235   while (g_variant_iter_next (&iter, "(i@as)", &id, &props))
236     {
237       item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
238 
239       if (item != NULL)
240         sn_dbus_menu_item_remove_props (item, props);
241 
242       g_variant_unref (props);
243     }
244 }
245 
246 static void
layout_updated_cb(SnDBusMenuGen * proxy,guint revision,gint parent,SnDBusMenu * menu)247 layout_updated_cb (SnDBusMenuGen *proxy,
248                    guint          revision,
249                    gint           parent,
250                    SnDBusMenu    *menu)
251 {
252   update_layout (menu, parent);
253 }
254 
255 static void
item_activation_requested_cb(SnDBusMenuGen * proxy,gint id,guint timestamp,SnDBusMenu * menu)256 item_activation_requested_cb (SnDBusMenuGen *proxy,
257                               gint           id,
258                               guint          timestamp,
259                               SnDBusMenu    *menu)
260 {
261   g_debug ("activation requested: id - %d, timestamp - %d", id, timestamp);
262 }
263 
264 static void
map_cb(GtkWidget * widget,SnDBusMenu * menu)265 map_cb (GtkWidget  *widget,
266         SnDBusMenu *menu)
267 {
268   gboolean need_update;
269 
270   sn_dbus_menu_gen_call_event_sync (menu->proxy, 0, "opened",
271                                     g_variant_new ("v", g_variant_new_int32 (0)),
272                                     gtk_get_current_event_time (),
273                                     NULL, NULL);
274 
275   sn_dbus_menu_gen_call_about_to_show_sync (menu->proxy, 0, &need_update,
276                                             NULL, NULL);
277 
278   if (need_update)
279     {
280       update_layout (menu, 0);
281     }
282 }
283 
284 static void
unmap_cb(GtkWidget * widget,SnDBusMenu * menu)285 unmap_cb (GtkWidget  *widget,
286           SnDBusMenu *menu)
287 {
288   sn_dbus_menu_gen_call_event_sync (menu->proxy, 0, "closed",
289                                     g_variant_new ("v", g_variant_new_int32 (0)),
290                                     gtk_get_current_event_time (),
291                                     NULL, NULL);
292 }
293 
294 static void
proxy_ready_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)295 proxy_ready_cb (GObject      *source_object,
296                 GAsyncResult *res,
297                 gpointer      user_data)
298 {
299   SnDBusMenuGen *proxy;
300   GError *error;
301   SnDBusMenu *menu;
302 
303   error = NULL;
304   proxy = sn_dbus_menu_gen_proxy_new_finish (res, &error);
305 
306   if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
307     {
308       g_error_free (error);
309       return;
310     }
311 
312   menu = SN_DBUS_MENU (user_data);
313   menu->proxy = proxy;
314 
315   if (error)
316     {
317       g_warning ("%s", error->message);
318       g_error_free (error);
319       return;
320     }
321 
322   g_signal_connect (proxy, "items-properties-updated",
323                     G_CALLBACK (items_properties_updated_cb), menu);
324 
325   g_signal_connect (proxy, "layout-updated",
326                     G_CALLBACK (layout_updated_cb), menu);
327 
328   g_signal_connect (proxy, "item-activation-requested",
329                     G_CALLBACK (item_activation_requested_cb), menu);
330 
331   g_signal_connect (menu, "map", G_CALLBACK (map_cb), menu);
332   g_signal_connect (menu, "unmap", G_CALLBACK (unmap_cb), menu);
333 
334   update_layout (menu, 0);
335 }
336 
337 static void
name_appeared_cb(GDBusConnection * connection,const gchar * name,const gchar * name_owner,gpointer user_data)338 name_appeared_cb (GDBusConnection *connection,
339                   const gchar     *name,
340                   const gchar     *name_owner,
341                   gpointer         user_data)
342 {
343   SnDBusMenu *menu;
344 
345   menu = SN_DBUS_MENU (user_data);
346 
347   sn_dbus_menu_gen_proxy_new (connection, G_DBUS_PROXY_FLAGS_NONE,
348                               menu->bus_name, menu->object_path,
349                               menu->cancellable, proxy_ready_cb, menu);
350 }
351 
352 static void
name_vanished_cb(GDBusConnection * connection,const gchar * name,gpointer user_data)353 name_vanished_cb (GDBusConnection *connection,
354                   const gchar     *name,
355                   gpointer         user_data)
356 {
357   SnDBusMenu *menu;
358 
359   menu = SN_DBUS_MENU (user_data);
360 
361   g_clear_object (&menu->proxy);
362 }
363 
364 static void
sn_dbus_menu_constructed(GObject * object)365 sn_dbus_menu_constructed (GObject *object)
366 {
367   SnDBusMenu *menu;
368   GtkWidget *toplevel;
369   GdkScreen *screen;
370   GdkVisual *visual;
371   GtkStyleContext *context;
372 
373   G_OBJECT_CLASS (sn_dbus_menu_parent_class)->constructed (object);
374   menu = SN_DBUS_MENU (object);
375 
376   /*Set up theme and transparency support*/
377   toplevel = gtk_widget_get_toplevel(GTK_WIDGET(menu));
378   /* Fix any failures of compiz/other wm's to communicate with gtk for transparency */
379   screen = gtk_widget_get_screen(GTK_WIDGET(toplevel));
380   visual = gdk_screen_get_rgba_visual(screen);
381   gtk_widget_set_visual(GTK_WIDGET(toplevel), visual);
382   /* Set menu and it's toplevel window to follow panel theme */
383   context = gtk_widget_get_style_context (GTK_WIDGET(toplevel));
384   gtk_style_context_add_class(context,"gnome-panel-menu-bar");
385   gtk_style_context_add_class(context,"mate-panel-menu-bar");
386 
387   menu->name_id = g_bus_watch_name (G_BUS_TYPE_SESSION, menu->bus_name,
388                                     G_BUS_NAME_WATCHER_FLAGS_NONE,
389                                     name_appeared_cb, name_vanished_cb,
390                                     menu, NULL);
391 }
392 
393 static void
sn_dbus_menu_dispose(GObject * object)394 sn_dbus_menu_dispose (GObject *object)
395 {
396   SnDBusMenu *menu;
397 
398   menu = SN_DBUS_MENU (object);
399 
400   if (menu->name_id > 0)
401     {
402       g_bus_unwatch_name (menu->name_id);
403       menu->name_id = 0;
404     }
405 
406   g_clear_pointer (&menu->items, g_hash_table_destroy);
407 
408   g_cancellable_cancel (menu->cancellable);
409   g_clear_object (&menu->cancellable);
410   g_clear_object (&menu->proxy);
411 
412   G_OBJECT_CLASS (sn_dbus_menu_parent_class)->dispose (object);
413 }
414 
415 static void
sn_dbus_menu_finalize(GObject * object)416 sn_dbus_menu_finalize (GObject *object)
417 {
418   SnDBusMenu *menu;
419 
420   menu = SN_DBUS_MENU (object);
421 
422   g_free (menu->bus_name);
423   g_free (menu->object_path);
424 
425   G_OBJECT_CLASS (sn_dbus_menu_parent_class)->finalize (object);
426 }
427 
428 static void
sn_dbus_menu_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)429 sn_dbus_menu_set_property (GObject      *object,
430                            guint         property_id,
431                            const GValue *value,
432                            GParamSpec   *pspec)
433 {
434   SnDBusMenu *menu;
435 
436   menu = SN_DBUS_MENU (object);
437 
438   switch (property_id)
439     {
440       case PROP_BUS_NAME:
441         menu->bus_name = g_value_dup_string (value);
442         break;
443 
444       case PROP_OBJECT_PATH:
445         menu->object_path = g_value_dup_string (value);
446         break;
447 
448       default:
449         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
450         break;
451     }
452 }
453 
454 static void
install_properties(GObjectClass * object_class)455 install_properties (GObjectClass *object_class)
456 {
457   properties[PROP_BUS_NAME] =
458     g_param_spec_string ("bus-name", "bus-name", "bus-name", NULL,
459                          G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
460                          G_PARAM_STATIC_STRINGS);
461 
462   properties[PROP_OBJECT_PATH] =
463     g_param_spec_string ("object-path", "object-path", "object-path", NULL,
464                          G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
465                          G_PARAM_STATIC_STRINGS);
466 
467   g_object_class_install_properties (object_class, LAST_PROP, properties);
468 }
469 
470 static void
sn_dbus_menu_class_init(SnDBusMenuClass * menu_class)471 sn_dbus_menu_class_init (SnDBusMenuClass *menu_class)
472 {
473   GObjectClass *object_class;
474 
475   object_class = G_OBJECT_CLASS (menu_class);
476 
477   object_class->constructed = sn_dbus_menu_constructed;
478   object_class->dispose = sn_dbus_menu_dispose;
479   object_class->finalize = sn_dbus_menu_finalize;
480   object_class->set_property = sn_dbus_menu_set_property;
481 
482   install_properties (object_class);
483 }
484 
485 static void
sn_dbus_menu_init(SnDBusMenu * menu)486 sn_dbus_menu_init (SnDBusMenu *menu)
487 {
488   menu->items = g_hash_table_new_full (NULL, NULL, NULL, sn_dubs_menu_item_free);
489   menu->cancellable = g_cancellable_new ();
490 }
491 
492 GtkMenu *
sn_dbus_menu_new(const gchar * bus_name,const gchar * object_path)493 sn_dbus_menu_new (const gchar *bus_name,
494                   const gchar *object_path)
495 {
496   return g_object_new (SN_TYPE_DBUS_MENU,
497                        "bus-name", bus_name,
498                        "object-path", object_path,
499                        NULL);
500 }
501