summaryrefslogtreecommitdiff
path: root/applets/notification_area/status-notifier
diff options
context:
space:
mode:
authorColomban Wendling <[email protected]>2017-01-19 18:46:10 +0100
committerlukefromdc <[email protected]>2017-01-23 13:49:34 -0500
commit7d39b2e82f46777efa67224f078c1cec9e827654 (patch)
tree7a415a153f4a3afb52358ed8a34ced08c25bdc5c /applets/notification_area/status-notifier
parenta506150684ad2e71b1f70190ee70fe9eda7a4ba9 (diff)
downloadmate-panel-7d39b2e82f46777efa67224f078c1cec9e827654.tar.bz2
mate-panel-7d39b2e82f46777efa67224f078c1cec9e827654.tar.xz
Add StatusNotifier support to the Notification Area applet
The StatusNotifier part of the implementation is based off gnome-panel's status-notifier applet.
Diffstat (limited to 'applets/notification_area/status-notifier')
-rw-r--r--applets/notification_area/status-notifier/Makefile.am83
-rw-r--r--applets/notification_area/status-notifier/README2
-rw-r--r--applets/notification_area/status-notifier/com.canonical.dbusmenu.xml74
-rw-r--r--applets/notification_area/status-notifier/org.kde.StatusNotifierHost.xml8
-rw-r--r--applets/notification_area/status-notifier/org.kde.StatusNotifierItem.xml80
-rw-r--r--applets/notification_area/status-notifier/org.kde.StatusNotifierWatcher.xml42
-rw-r--r--applets/notification_area/status-notifier/sn-dbus-menu-item.c472
-rw-r--r--applets/notification_area/status-notifier/sn-dbus-menu-item.h64
-rw-r--r--applets/notification_area/status-notifier/sn-dbus-menu.c472
-rw-r--r--applets/notification_area/status-notifier/sn-dbus-menu.h33
-rw-r--r--applets/notification_area/status-notifier/sn-host-v0.c487
-rw-r--r--applets/notification_area/status-notifier/sn-host-v0.h33
-rw-r--r--applets/notification_area/status-notifier/sn-image-menu-item.c216
-rw-r--r--applets/notification_area/status-notifier/sn-image-menu-item.h41
-rw-r--r--applets/notification_area/status-notifier/sn-item-v0.c1355
-rw-r--r--applets/notification_area/status-notifier/sn-item-v0.h37
-rw-r--r--applets/notification_area/status-notifier/sn-item.c491
-rw-r--r--applets/notification_area/status-notifier/sn-item.h70
18 files changed, 4060 insertions, 0 deletions
diff --git a/applets/notification_area/status-notifier/Makefile.am b/applets/notification_area/status-notifier/Makefile.am
new file mode 100644
index 00000000..78d0a7fb
--- /dev/null
+++ b/applets/notification_area/status-notifier/Makefile.am
@@ -0,0 +1,83 @@
+NULL =
+
+noinst_LTLIBRARIES = libstatus-notifier.la
+
+AM_CPPFLAGS = \
+ $(NOTIFICATION_AREA_CFLAGS) \
+ $(LIBMATE_PANEL_APPLET_CFLAGS) \
+ -I$(srcdir) \
+ -I$(srcdir)/.. \
+ -DMATELOCALEDIR=\""$(datadir)/locale"\" \
+ -DG_LOG_DOMAIN=\""notification-area-applet"\" \
+ $(DISABLE_DEPRECATED_CFLAGS)
+
+AM_CFLAGS = $(WARN_CFLAGS)
+
+libstatus_notifier_la_SOURCES = \
+ sn-dbus-menu.c \
+ sn-dbus-menu.h \
+ sn-dbus-menu-item.c \
+ sn-dbus-menu-item.h \
+ sn-host-v0.c \
+ sn-host-v0.h \
+ sn-image-menu-item.c \
+ sn-image-menu-item.h \
+ sn-item.c \
+ sn-item.h \
+ sn-item-v0.c \
+ sn-item-v0.h \
+ $(BUILT_SOURCES) \
+ $(NULL)
+
+libstatus_notifier_la_LIBADD = \
+ $(LIBM) \
+ $(NOTIFICATION_AREA_LIBS) \
+ $(NULL)
+
+sn-dbus-menu-gen.h:
+sn-dbus-menu-gen.c: com.canonical.dbusmenu.xml
+ $(AM_V_GEN) $(GDBUS_CODEGEN) --c-namespace Sn \
+ --generate-c-code sn-dbus-menu-gen \
+ $(srcdir)/com.canonical.dbusmenu.xml
+
+sn-host-v0-gen.h:
+sn-host-v0-gen.c: org.kde.StatusNotifierHost.xml
+ $(AM_V_GEN) $(GDBUS_CODEGEN) --c-namespace Sn \
+ --generate-c-code sn-host-v0-gen \
+ $(srcdir)/org.kde.StatusNotifierHost.xml
+
+sn-item-v0-gen.h:
+sn-item-v0-gen.c: org.kde.StatusNotifierItem.xml
+ $(AM_V_GEN) $(GDBUS_CODEGEN) --c-namespace Sn \
+ --generate-c-code sn-item-v0-gen \
+ $(srcdir)/org.kde.StatusNotifierItem.xml
+
+sn-watcher-v0-gen.h:
+sn-watcher-v0-gen.c: org.kde.StatusNotifierWatcher.xml
+ $(AM_V_GEN) $(GDBUS_CODEGEN) --c-namespace Sn \
+ --generate-c-code sn-watcher-v0-gen \
+ $(srcdir)/org.kde.StatusNotifierWatcher.xml
+
+BUILT_SOURCES = \
+ sn-dbus-menu-gen.c \
+ sn-dbus-menu-gen.h \
+ sn-host-v0-gen.c \
+ sn-host-v0-gen.h \
+ sn-item-v0-gen.c \
+ sn-item-v0-gen.h \
+ sn-watcher-v0-gen.c \
+ sn-watcher-v0-gen.h \
+ $(NULL)
+
+EXTRA_DIST = \
+ com.canonical.dbusmenu.xml \
+ org.kde.StatusNotifierHost.xml \
+ org.kde.StatusNotifierItem.xml \
+ org.kde.StatusNotifierWatcher.xml \
+ $(NULL)
+
+CLEANFILES = \
+ $(BUILT_SOURCES) \
+ $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/applets/notification_area/status-notifier/README b/applets/notification_area/status-notifier/README
new file mode 100644
index 00000000..8b89ae98
--- /dev/null
+++ b/applets/notification_area/status-notifier/README
@@ -0,0 +1,2 @@
+Based off gnome-panel's status-notifier applet:
+https://git.gnome.org/browse/gnome-panel/tree/modules/external/status-notifier
diff --git a/applets/notification_area/status-notifier/com.canonical.dbusmenu.xml b/applets/notification_area/status-notifier/com.canonical.dbusmenu.xml
new file mode 100644
index 00000000..f8f72d10
--- /dev/null
+++ b/applets/notification_area/status-notifier/com.canonical.dbusmenu.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="com.canonical.dbusmenu">
+ <annotation name="org.gtk.GDBus.C.Name" value="DBusMenuGen" />
+
+ <method name="GetLayout">
+ <arg type="i" name="parentId" direction="in" />
+ <arg type="i" name="recursionDepth" direction="in" />
+ <arg type="as" name="propertyNames" direction="in" />
+ <arg type="u" name="revision" direction="out" />
+ <arg type="(ia{sv}av)" name="layout" direction="out" />
+ </method>
+
+ <method name="GetGroupProperties">
+ <arg type="ai" name="ids" direction="in" />
+ <arg type="as" name="propertyNames" direction="in" />
+ <arg type="a(ia{sv})" name="properties" direction="out" />
+ </method>
+
+ <method name="GetProperty">
+ <arg type="i" name="id" direction="in" />
+ <arg type="s" name="name" direction="in" />
+ <arg type="v" name="value" direction="out" />
+ </method>
+
+ <method name="Event">
+ <arg type="i" name="id" direction="in" />
+ <arg type="s" name="eventId" direction="in" />
+ <arg type="v" name="data" direction="in" />
+ <arg type="u" name="timestamp" direction="in" />
+ </method>
+
+ <method name="EventGroup">
+ <arg type="a(isvu)" name="events" direction="in" />
+ <arg type="ai" name="idErrors" direction="out" />
+ </method>
+
+ <method name="AboutToShow">
+ <arg type="i" name="id" direction="in" />
+ <arg type="b" name="needUpdate" direction="out" />
+ </method>
+
+ <method name="AboutToShowGroup">
+ <arg type="ai" name="ids" direction="in" />
+ <arg type="ai" name="updatesNeeded" direction="out" />
+ <arg type="ai" name="idErrors" direction="out" />
+ </method>
+
+ <property name="Version" type="u" access="read" />
+
+ <property name="TextDirection" type="s" access="read" />
+
+ <property name="Status" type="s" access="read" />
+
+ <property name="IconThemePath" type="as" access="read" />
+
+ <signal name="ItemsPropertiesUpdated">
+ <arg type="a(ia{sv})" name="updatedProps" direction="out" />
+ <arg type="a(ias)" name="removedProps" direction="out" />
+ </signal>
+
+ <signal name="LayoutUpdated">
+ <arg type="u" name="revision" direction="out" />
+ <arg type="i" name="parent" direction="out" />
+ </signal>
+
+ <signal name="ItemActivationRequested">
+ <arg type="i" name="id" direction="out" />
+ <arg type="u" name="timestamp" direction="out" />
+ </signal>
+ </interface>
+</node>
diff --git a/applets/notification_area/status-notifier/org.kde.StatusNotifierHost.xml b/applets/notification_area/status-notifier/org.kde.StatusNotifierHost.xml
new file mode 100644
index 00000000..3e8b5625
--- /dev/null
+++ b/applets/notification_area/status-notifier/org.kde.StatusNotifierHost.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.kde.StatusNotifierHost">
+ <annotation name="org.gtk.GDBus.C.Name" value="HostV0Gen" />
+ </interface>
+</node>
diff --git a/applets/notification_area/status-notifier/org.kde.StatusNotifierItem.xml b/applets/notification_area/status-notifier/org.kde.StatusNotifierItem.xml
new file mode 100644
index 00000000..609d3050
--- /dev/null
+++ b/applets/notification_area/status-notifier/org.kde.StatusNotifierItem.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.kde.StatusNotifierItem">
+ <annotation name="org.gtk.GDBus.C.Name" value="ItemV0Gen" />
+
+ <method name="ContextMenu">
+ <arg name="x" type="i" direction="in" />
+ <arg name="y" type="i" direction="in" />
+ </method>
+
+ <method name="Activate">
+ <arg name="x" type="i" direction="in" />
+ <arg name="y" type="i" direction="in" />
+ </method>
+
+ <method name="SecondaryActivate">
+ <arg name="x" type="i" direction="in" />
+ <arg name="y" type="i" direction="in" />
+ </method>
+
+ <method name="Scroll">
+ <arg name="delta" type="i" direction="in" />
+ <arg name="orientation" type="s" direction="in" />
+ </method>
+
+ <property name="Category" type="s" access="read" />
+
+ <property name="Id" type="s" access="read" />
+
+ <property name="Title" type="s" access="read" />
+
+ <property name="Status" type="s" access="read" />
+
+ <property name="WindowId" type="i" access="read" />
+
+ <property name="IconName" type="s" access="read"/>
+
+ <property name="IconPixmap" type="a(iiay)" access="read" />
+
+ <property name="OverlayIconName" type="s" access="read"/>
+
+ <property name="OverlayIconPixmap" type="a(iiay)" access="read" />
+
+ <property name="AttentionIconName" type="s" access="read"/>
+
+ <property name="AttentionIconPixmap" type="a(iiay)" access="read" />
+
+ <property name="AttentionMovieName" type="s" access="read" />
+
+ <property name="ToolTip" type="(sa(iiay)ss)" access="read" />
+
+ <signal name="NewTitle" />
+
+ <signal name="NewIcon" />
+
+ <signal name="NewAttentionIcon" />
+
+ <signal name="NewOverlayIcon" />
+
+ <signal name="NewToolTip" />
+
+ <signal name="NewStatus">
+ <arg name="status" type="s" />
+ </signal>
+
+ <!-- Properties and signals that is not part of specification -->
+
+ <property name="IconThemePath" type="s" access="read" />
+
+ <property name="Menu" type="o" access="read" />
+
+ <property name="ItemIsMenu" type="b" access="read" />
+
+ <signal name="NewIconThemePath">
+ <arg name="icon_theme_path" type="s" />
+ </signal>
+ </interface>
+</node>
diff --git a/applets/notification_area/status-notifier/org.kde.StatusNotifierWatcher.xml b/applets/notification_area/status-notifier/org.kde.StatusNotifierWatcher.xml
new file mode 100644
index 00000000..3b0f135a
--- /dev/null
+++ b/applets/notification_area/status-notifier/org.kde.StatusNotifierWatcher.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.kde.StatusNotifierWatcher">
+ <annotation name="org.gtk.GDBus.C.Name" value="WatcherV0Gen" />
+
+ <method name="RegisterStatusNotifierItem">
+ <annotation name="org.gtk.GDBus.C.Name" value="RegisterItem" />
+ <arg type="s" direction="in" name="service" />
+ </method>
+
+ <method name="RegisterStatusNotifierHost">
+ <annotation name="org.gtk.GDBus.C.Name" value="RegisterHost" />
+ <arg type="s" direction="in" name="service" />
+ </method>
+
+ <property name="RegisteredStatusNotifierItems" type="as" access="read">
+ <annotation name="org.gtk.GDBus.C.Name" value="RegisteredItems" />
+ </property>
+
+ <property name="IsStatusNotifierHostRegistered" type="b" access="read">
+ <annotation name="org.gtk.GDBus.C.Name" value="IsHostRegistered" />
+ </property>
+
+ <property name="ProtocolVersion" type="i" access="read" />
+
+ <signal name="StatusNotifierItemRegistered">
+ <annotation name="org.gtk.GDBus.C.Name" value="ItemRegistered" />
+ <arg type="s" direction="out" name="service" />
+ </signal>
+
+ <signal name="StatusNotifierItemUnregistered">
+ <annotation name="org.gtk.GDBus.C.Name" value="ItemUnregistered" />
+ <arg type="s" direction="out" name="service" />
+ </signal>
+
+ <signal name="StatusNotifierHostRegistered">
+ <annotation name="org.gtk.GDBus.C.Name" value="HostRegistered" />
+ </signal>
+ </interface>
+</node>
diff --git a/applets/notification_area/status-notifier/sn-dbus-menu-item.c b/applets/notification_area/status-notifier/sn-dbus-menu-item.c
new file mode 100644
index 00000000..3e740b1c
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-dbus-menu-item.c
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sn-dbus-menu-item.h"
+#include "sn-image-menu-item.h"
+
+static GdkPixbuf *
+pxibuf_new (GVariant *variant)
+{
+ gsize length;
+ const guchar *data;
+ GInputStream *stream;
+ GdkPixbuf *pixbuf;
+ GError *error;
+
+ data = g_variant_get_fixed_array (variant, &length, sizeof (guchar));
+
+ if (length == 0)
+ return NULL;
+
+ stream = g_memory_input_stream_new_from_data (data, length, NULL);
+
+ if (stream == NULL)
+ return NULL;
+
+ error = NULL;
+ pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, &error);
+ g_object_unref (stream);
+
+ if (error != NULL)
+ {
+ g_warning ("Unable to build GdkPixbuf from icon data: %s", error->message);
+ g_error_free (error);
+ }
+
+ return pixbuf;
+}
+
+static SnShortcut *
+sn_shortcut_new (guint key,
+ GdkModifierType mask)
+{
+ SnShortcut *shortcut;
+
+ shortcut = g_new0 (SnShortcut, 1);
+
+ shortcut->key = key;
+ shortcut->mask = mask;
+
+ return shortcut;
+}
+
+static SnShortcut **
+sn_shortcuts_new (GVariant *variant)
+{
+ GPtrArray *array;
+ GVariantIter shortcuts;
+ GVariantIter *shortcut;
+
+ if (variant == NULL || g_variant_iter_init (&shortcuts, variant) == 0)
+ return NULL;
+
+ array = g_ptr_array_new ();
+ while (g_variant_iter_next (&shortcuts, "as", &shortcut))
+ {
+ guint key;
+ GdkModifierType mask;
+ const gchar *string;
+
+ key = 0;
+ mask = 0;
+
+ while (g_variant_iter_next (shortcut, "&s", &string))
+ {
+ if (g_strcmp0 (string, "Control") == 0)
+ mask |= GDK_CONTROL_MASK;
+ else if (g_strcmp0 (string, "Alt") == 0)
+ mask |= GDK_MOD1_MASK;
+ else if (g_strcmp0 (string, "Shift") == 0)
+ mask |= GDK_SHIFT_MASK;
+ else if (g_strcmp0 (string, "Super") == 0)
+ mask |= GDK_SUPER_MASK;
+ else
+ gtk_accelerator_parse (string, &key, NULL);
+ }
+
+ g_ptr_array_add (array,sn_shortcut_new (key, mask));
+ g_variant_iter_free (shortcut);
+ }
+
+ g_ptr_array_add (array, NULL);
+ return (SnShortcut **) g_ptr_array_free (array, FALSE);
+}
+
+static void
+sn_shortcuts_free (SnShortcut **shortcuts)
+{
+ guint i;
+
+ if (shortcuts == NULL)
+ return;
+
+ for (i = 0; shortcuts[i] != NULL; i++)
+ g_free (shortcuts[i]);
+
+ g_free (shortcuts);
+}
+
+SnDBusMenuItem *
+sn_dbus_menu_item_new (GVariant *props)
+{
+ SnDBusMenuItem *item;
+ GVariantIter iter;
+ const gchar *prop;
+ GVariant *value;
+
+ item = g_new0 (SnDBusMenuItem, 1);
+
+ item->enabled = TRUE;
+ item->toggle_state = -1;
+ item->visible = TRUE;
+
+ g_variant_iter_init (&iter, props);
+ while (g_variant_iter_next (&iter, "{&sv}", &prop, &value))
+ {
+ if (g_strcmp0 (prop, "accessible-desc") == 0)
+ item->accessible_desc = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "children-display") == 0)
+ item->children_display = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "disposition") == 0)
+ item->disposition = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "enabled") == 0)
+ item->enabled = g_variant_get_boolean (value);
+ else if (g_strcmp0 (prop, "icon-name") == 0)
+ item->icon_name = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "icon-data") == 0)
+ item->icon_data = pxibuf_new (value);
+ else if (g_strcmp0 (prop, "label") == 0)
+ item->label = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "shortcut") == 0)
+ item->shortcuts = sn_shortcuts_new (value);
+ else if (g_strcmp0 (prop, "toggle-type") == 0)
+ item->toggle_type = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "toggle-state") == 0)
+ item->toggle_state = g_variant_get_int32 (value);
+ else if (g_strcmp0 (prop, "type") == 0)
+ item->type = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (prop, "visible") == 0)
+ item->visible = g_variant_get_boolean (value);
+ else
+ g_debug ("unknown property '%s'", prop);
+
+ g_variant_unref (value);
+ }
+
+ if (g_strcmp0 (item->type, "separator") == 0)
+ {
+ item->item = gtk_separator_menu_item_new ();
+ }
+ else
+ {
+ if (g_strcmp0 (item->toggle_type, "checkmark") == 0)
+ {
+ item->item = gtk_check_menu_item_new ();
+ gtk_menu_item_set_use_underline (GTK_MENU_ITEM (item->item), TRUE);
+ }
+ else if (g_strcmp0 (item->toggle_type, "radio") == 0)
+ {
+ item->item = gtk_radio_menu_item_new (NULL);
+ gtk_menu_item_set_use_underline (GTK_MENU_ITEM (item->item), TRUE);
+ }
+ else
+ {
+ SnImageMenuItem *image_item;
+
+ item->item = sn_image_menu_item_new ();
+ image_item = SN_IMAGE_MENU_ITEM (item->item);
+
+ if (item->icon_name)
+ {
+ sn_image_menu_item_set_image_from_icon_name (image_item,
+ item->icon_name);
+ }
+ else if (item->icon_data)
+ {
+ sn_image_menu_item_set_image_from_icon_pixbuf (image_item,
+ item->icon_data);
+ }
+ }
+
+ if (g_strcmp0 (item->children_display, "submenu") == 0)
+ {
+ GtkWidget *submenu;
+
+ submenu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (item->item), submenu);
+
+ item->submenu = GTK_MENU (submenu);
+ g_object_ref_sink (item->submenu);
+ }
+
+ gtk_menu_item_set_label (GTK_MENU_ITEM (item->item), item->label);
+
+ if (item->shortcuts)
+ {
+ guint i;
+
+ for (i = 0; item->shortcuts[i] != NULL; i++)
+ {
+ }
+ }
+
+ if (item->toggle_state != -1 && GTK_IS_CHECK_MENU_ITEM (item->item))
+ {
+ GtkCheckMenuItem *check;
+
+ check = GTK_CHECK_MENU_ITEM (item->item);
+
+ if (item->toggle_state == 1)
+ gtk_check_menu_item_set_active (check, TRUE);
+ else if (item->toggle_state == 0)
+ gtk_check_menu_item_set_active (check, FALSE);
+ }
+ }
+
+ gtk_widget_set_sensitive (item->item, item->enabled);
+ gtk_widget_set_visible (item->item, item->visible);
+
+ g_object_ref_sink (item->item);
+ return item;
+}
+
+void
+sn_dubs_menu_item_free (gpointer data)
+{
+ SnDBusMenuItem *item;
+
+ item = (SnDBusMenuItem *) data;
+ if (item == NULL)
+ return;
+
+ g_clear_pointer (&item->accessible_desc, g_free);
+ g_clear_pointer (&item->children_display, g_free);
+ g_clear_pointer (&item->disposition, g_free);
+ g_clear_pointer (&item->icon_name, g_free);
+ g_clear_object (&item->icon_data);
+ g_clear_pointer (&item->label, g_free);
+ g_clear_pointer (&item->shortcuts, sn_shortcuts_free);
+ g_clear_pointer (&item->toggle_type, g_free);
+ g_clear_pointer (&item->type, g_free);
+
+ gtk_widget_destroy (item->item);
+ g_clear_object (&item->item);
+ g_clear_object (&item->submenu);
+
+ g_free (item);
+}
+
+void
+sn_dbus_menu_item_update_props (SnDBusMenuItem *item,
+ GVariant *props)
+{
+ GVariantIter iter;
+ const gchar *prop;
+ GVariant *value;
+
+ g_variant_iter_init (&iter, props);
+ while (g_variant_iter_next (&iter, "{&sv}", &prop, &value))
+ {
+ if (g_strcmp0 (prop, "accessible-desc") == 0)
+ {
+ g_free (item->accessible_desc);
+ item->accessible_desc = g_variant_dup_string (value, NULL);
+ }
+ else if (g_strcmp0 (prop, "children-display") == 0)
+ {
+ g_free (item->children_display);
+ item->children_display = g_variant_dup_string (value, NULL);
+ }
+ else if (g_strcmp0 (prop, "disposition") == 0)
+ {
+ g_free (item->disposition);
+ item->disposition = g_variant_dup_string (value, NULL);
+ }
+ else if (g_strcmp0 (prop, "enabled") == 0)
+ {
+ item->enabled = g_variant_get_boolean (value);
+ gtk_widget_set_sensitive (item->item, item->enabled);
+ }
+ else if (g_strcmp0 (prop, "icon-name") == 0)
+ {
+ SnImageMenuItem *image_item;
+
+ g_free (item->icon_name);
+ item->icon_name = g_variant_dup_string (value, NULL);
+
+ image_item = SN_IMAGE_MENU_ITEM (item->item);
+
+ if (item->icon_name)
+ {
+ sn_image_menu_item_set_image_from_icon_name (image_item,
+ item->icon_name);
+ }
+ else
+ {
+ sn_image_menu_item_unset_image (image_item);
+ }
+ }
+ else if (g_strcmp0 (prop, "icon-data") == 0)
+ {
+ SnImageMenuItem *image_item;
+
+ g_clear_object (&item->icon_data);
+ item->icon_data = pxibuf_new (value);
+
+ image_item = SN_IMAGE_MENU_ITEM (item->item);
+
+ if (item->icon_data)
+ {
+ sn_image_menu_item_set_image_from_icon_pixbuf (image_item,
+ item->icon_data);
+ }
+ else
+ {
+ sn_image_menu_item_unset_image (image_item);
+ }
+ }
+ else if (g_strcmp0 (prop, "label") == 0)
+ {
+ g_free (item->label);
+ item->label = g_variant_dup_string (value, NULL);
+
+ if (!GTK_IS_SEPARATOR_MENU_ITEM (item->item))
+ gtk_menu_item_set_label (GTK_MENU_ITEM (item->item), item->label);
+ }
+ else if (g_strcmp0 (prop, "shortcut") == 0)
+ {
+ sn_shortcuts_free (item->shortcuts);
+ item->shortcuts = sn_shortcuts_new (value);
+ }
+ else if (g_strcmp0 (prop, "toggle-type") == 0)
+ {
+ g_free (item->toggle_type);
+ item->toggle_type = g_variant_dup_string (value, NULL);
+ }
+ else if (g_strcmp0 (prop, "toggle-state") == 0)
+ {
+ item->toggle_state = g_variant_get_int32 (value);
+
+ if (item->toggle_state != -1 && GTK_IS_CHECK_MENU_ITEM (item->item))
+ {
+ GtkCheckMenuItem *check;
+
+ check = GTK_CHECK_MENU_ITEM (item->item);
+
+ g_signal_handler_block (item->item, item->activate_id);
+
+ if (item->toggle_state == 1)
+ gtk_check_menu_item_set_active (check, TRUE);
+ else if (item->toggle_state == 0)
+ gtk_check_menu_item_set_active (check, FALSE);
+
+ g_signal_handler_unblock (item->item, item->activate_id);
+ }
+ }
+ else if (g_strcmp0 (prop, "type") == 0)
+ {
+ g_free (item->type);
+ item->type = g_variant_dup_string (value, NULL);
+ }
+ else if (g_strcmp0 (prop, "visible") == 0)
+ {
+ item->visible = g_variant_get_boolean (value);
+ gtk_widget_set_visible (item->item, item->visible);
+ }
+ else
+ {
+ g_debug ("updating unknown property - '%s'", prop);
+ }
+
+ g_variant_unref (value);
+ }
+}
+
+void
+sn_dbus_menu_item_remove_props (SnDBusMenuItem *item,
+ GVariant *props)
+{
+ GVariantIter iter;
+ const gchar *prop;
+
+ g_variant_iter_init (&iter, props);
+ while (g_variant_iter_next (&iter, "&s", &prop))
+ {
+ if (g_strcmp0 (prop, "accessible-desc") == 0)
+ {
+ g_clear_pointer (&item->accessible_desc, g_free);
+ }
+ else if (g_strcmp0 (prop, "children-display") == 0)
+ {
+ g_clear_pointer (&item->children_display, g_free);
+ }
+ else if (g_strcmp0 (prop, "disposition") == 0)
+ {
+ g_clear_pointer (&item->disposition, g_free);
+ }
+ else if (g_strcmp0 (prop, "enabled") == 0)
+ {
+ item->enabled = TRUE;
+ gtk_widget_set_sensitive (item->item, item->enabled);
+ }
+ else if (g_strcmp0 (prop, "icon-name") == 0)
+ {
+ g_clear_pointer (&item->icon_name, g_free);
+ if (SN_IS_IMAGE_MENU_ITEM (item->item))
+ sn_image_menu_item_unset_image (SN_IMAGE_MENU_ITEM (item->item));
+ }
+ else if (g_strcmp0 (prop, "icon-data") == 0)
+ {
+ g_clear_object (&item->icon_data);
+ if (SN_IS_IMAGE_MENU_ITEM (item->item))
+ sn_image_menu_item_unset_image (SN_IMAGE_MENU_ITEM (item->item));
+ }
+ else if (g_strcmp0 (prop, "label") == 0)
+ {
+ g_clear_pointer (&item->label, g_free);
+ if (!GTK_IS_SEPARATOR_MENU_ITEM (item->item))
+ gtk_menu_item_set_label (GTK_MENU_ITEM (item->item), item->label);
+ }
+ else if (g_strcmp0 (prop, "shortcut") == 0)
+ {
+ g_clear_pointer (&item->shortcuts, sn_shortcuts_free);
+ }
+ else if (g_strcmp0 (prop, "toggle-type") == 0)
+ {
+ g_clear_pointer (&item->toggle_type, g_free);
+ }
+ else if (g_strcmp0 (prop, "toggle-state") == 0)
+ {
+ item->toggle_state = -1;
+ }
+ else if (g_strcmp0 (prop, "type") == 0)
+ {
+ g_clear_pointer (&item->type, g_free);
+ }
+ else if (g_strcmp0 (prop, "visible") == 0)
+ {
+ item->visible = TRUE;
+ gtk_widget_set_visible (item->item, item->visible);
+ }
+ else
+ {
+ g_debug ("removing unknown property - '%s'", prop);
+ }
+ }
+}
diff --git a/applets/notification_area/status-notifier/sn-dbus-menu-item.h b/applets/notification_area/status-notifier/sn-dbus-menu-item.h
new file mode 100644
index 00000000..c89de5c3
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-dbus-menu-item.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_DBUS_MENU_ITEM_H
+#define SN_DUBS_MENU_ITEM_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+ guint key;
+ GdkModifierType mask;
+} SnShortcut;
+
+typedef struct
+{
+ gchar *accessible_desc;
+ gchar *children_display;
+ gchar *disposition;
+ gboolean enabled;
+ gchar *icon_name;
+ GdkPixbuf *icon_data;
+ gchar *label;
+ SnShortcut **shortcuts;
+ gchar *toggle_type;
+ gint32 toggle_state;
+ gchar *type;
+ gboolean visible;
+
+ GtkWidget *item;
+ GtkMenu *submenu;
+
+ gulong activate_id;
+} SnDBusMenuItem;
+
+SnDBusMenuItem *sn_dbus_menu_item_new (GVariant *props);
+
+void sn_dubs_menu_item_free (gpointer data);
+
+void sn_dbus_menu_item_update_props (SnDBusMenuItem *item,
+ GVariant *props);
+
+void sn_dbus_menu_item_remove_props (SnDBusMenuItem *item,
+ GVariant *props);
+
+G_END_DECLS
+
+#endif
diff --git a/applets/notification_area/status-notifier/sn-dbus-menu.c b/applets/notification_area/status-notifier/sn-dbus-menu.c
new file mode 100644
index 00000000..9cbab345
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-dbus-menu.c
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sn-dbus-menu.h"
+#include "sn-dbus-menu-gen.h"
+#include "sn-dbus-menu-item.h"
+
+struct _SnDBusMenu
+{
+ GtkMenu parent;
+
+ GHashTable *items;
+
+ GCancellable *cancellable;
+
+ gchar *bus_name;
+ gchar *object_path;
+ guint name_id;
+
+ SnDBusMenuGen *proxy;
+};
+
+enum
+{
+ PROP_0,
+
+ PROP_BUS_NAME,
+ PROP_OBJECT_PATH,
+
+ LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP] = { NULL };
+
+static const gchar *property_names[] =
+{
+ "accessible-desc",
+ "children-display",
+ "disposition",
+ "enabled",
+ "icon-data",
+ "icon-name",
+ "label",
+ "shortcut",
+ "toggle-type",
+ "toggle-state",
+ "type",
+ "visible",
+ NULL
+};
+
+G_DEFINE_TYPE (SnDBusMenu, sn_dbus_menu, GTK_TYPE_MENU)
+
+static void
+activate_cb (GtkWidget *widget,
+ SnDBusMenu *menu)
+{
+ guint id;
+
+ if (gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget)) != NULL)
+ return;
+
+ id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "item-id"));
+ sn_dbus_menu_gen_call_event_sync (menu->proxy, id, "clicked",
+ g_variant_new ("v", g_variant_new_int32 (0)),
+ gtk_get_current_event_time (), NULL, NULL);
+}
+
+static GtkMenu *
+layout_update_item (SnDBusMenu *menu,
+ GtkMenu *gtk_menu,
+ guint id,
+ GVariant *props)
+{
+ SnDBusMenuItem *item;
+
+ if (id == 0)
+ return gtk_menu;
+
+ item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
+
+ if (item == NULL)
+ {
+ item = sn_dbus_menu_item_new (props);
+
+ g_object_set_data (G_OBJECT (item->item), "item-id", GUINT_TO_POINTER (id));
+ gtk_menu_shell_append (GTK_MENU_SHELL (gtk_menu), item->item);
+
+ item->activate_id = g_signal_connect (item->item, "activate",
+ G_CALLBACK (activate_cb), menu);
+
+ g_hash_table_replace (menu->items, GUINT_TO_POINTER (id), item);
+ }
+ else
+ {
+ sn_dbus_menu_item_update_props (item, props);
+ }
+
+ return item->submenu;
+}
+
+static void
+layout_parse (SnDBusMenu *menu,
+ GVariant *layout,
+ GtkMenu *gtk_menu)
+{
+ guint id;
+ GVariant *props;
+ GVariant *items;
+ GtkMenu *submenu;
+ GVariantIter iter;
+ GVariant *child;
+
+ g_variant_get (layout, "(i@a{sv}@av)", &id, &props, &items);
+
+ submenu = layout_update_item (menu, gtk_menu, id, props);
+ g_variant_unref (props);
+
+ g_variant_iter_init (&iter, items);
+ while ((child = g_variant_iter_next_value (&iter)))
+ {
+ GVariant *value;
+
+ value = g_variant_get_variant (child);
+
+ layout_parse (menu, value, submenu);
+ g_variant_unref (value);
+
+ g_variant_unref (child);
+ }
+
+ g_variant_unref (items);
+}
+
+static void
+get_layout_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GVariant *layout;
+ guint revision;
+ GError *error;
+ SnDBusMenu *menu;
+
+ error = NULL;
+ sn_dbus_menu_gen_call_get_layout_finish (SN_DBUS_MENU_GEN (source_object),
+ &revision, &layout, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ menu = SN_DBUS_MENU (user_data);
+
+ if (error != NULL)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_hash_table_remove_all (menu->items);
+ layout_parse (menu, layout, GTK_MENU (menu));
+ g_variant_unref (layout);
+}
+
+static void
+update_layout (SnDBusMenu *menu,
+ gint parent)
+{
+ gint depth;
+
+ parent = 0;
+ depth = -1;
+
+ sn_dbus_menu_gen_call_get_layout (menu->proxy, parent, depth,
+ property_names, menu->cancellable,
+ get_layout_cb, menu);
+}
+
+static void
+items_properties_updated_cb (SnDBusMenuGen *proxy,
+ GVariant *updated_props,
+ GVariant *removed_props,
+ SnDBusMenu *menu)
+{
+ GVariantIter iter;
+ guint id;
+ GVariant *props;
+ SnDBusMenuItem *item;
+
+ g_variant_iter_init (&iter, updated_props);
+ while (g_variant_iter_next (&iter, "(i@a{sv})", &id, &props))
+ {
+ item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
+
+ if (item != NULL)
+ sn_dbus_menu_item_update_props (item, props);
+
+ g_variant_unref (props);
+ }
+
+ g_variant_iter_init (&iter, removed_props);
+ while (g_variant_iter_next (&iter, "(i@as)", &id, &props))
+ {
+ item = g_hash_table_lookup (menu->items, GUINT_TO_POINTER (id));
+
+ if (item != NULL)
+ sn_dbus_menu_item_remove_props (item, props);
+
+ g_variant_unref (props);
+ }
+}
+
+static void
+layout_updated_cb (SnDBusMenuGen *proxy,
+ guint revision,
+ gint parent,
+ SnDBusMenu *menu)
+{
+ update_layout (menu, parent);
+}
+
+static void
+item_activation_requested_cb (SnDBusMenuGen *proxy,
+ gint id,
+ guint timestamp,
+ SnDBusMenu *menu)
+{
+ g_debug ("activation requested: id - %d, timestamp - %d", id, timestamp);
+}
+
+static void
+map_cb (GtkWidget *widget,
+ SnDBusMenu *menu)
+{
+ gboolean need_update;
+
+ sn_dbus_menu_gen_call_event_sync (menu->proxy, 0, "opened",
+ g_variant_new ("v", g_variant_new_int32 (0)),
+ gtk_get_current_event_time (),
+ NULL, NULL);
+
+ sn_dbus_menu_gen_call_about_to_show_sync (menu->proxy, 0, &need_update,
+ NULL, NULL);
+
+ if (need_update)
+ {
+ update_layout (menu, 0);
+ }
+}
+
+static void
+unmap_cb (GtkWidget *widget,
+ SnDBusMenu *menu)
+{
+ sn_dbus_menu_gen_call_event_sync (menu->proxy, 0, "closed",
+ g_variant_new ("v", g_variant_new_int32 (0)),
+ gtk_get_current_event_time (),
+ NULL, NULL);
+}
+
+static void
+proxy_ready_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnDBusMenuGen *proxy;
+ GError *error;
+ SnDBusMenu *menu;
+
+ error = NULL;
+ proxy = sn_dbus_menu_gen_proxy_new_finish (res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ menu = SN_DBUS_MENU (user_data);
+ menu->proxy = proxy;
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_signal_connect (proxy, "items-properties-updated",
+ G_CALLBACK (items_properties_updated_cb), menu);
+
+ g_signal_connect (proxy, "layout-updated",
+ G_CALLBACK (layout_updated_cb), menu);
+
+ g_signal_connect (proxy, "item-activation-requested",
+ G_CALLBACK (item_activation_requested_cb), menu);
+
+ g_signal_connect (menu, "map", G_CALLBACK (map_cb), menu);
+ g_signal_connect (menu, "unmap", G_CALLBACK (unmap_cb), menu);
+
+ update_layout (menu, 0);
+}
+
+static void
+name_appeared_cb (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ SnDBusMenu *menu;
+
+ menu = SN_DBUS_MENU (user_data);
+
+ sn_dbus_menu_gen_proxy_new (connection, G_DBUS_PROXY_FLAGS_NONE,
+ menu->bus_name, menu->object_path,
+ menu->cancellable, proxy_ready_cb, menu);
+}
+
+static void
+name_vanished_cb (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ SnDBusMenu *menu;
+
+ menu = SN_DBUS_MENU (user_data);
+
+ g_clear_object (&menu->proxy);
+}
+
+static void
+sn_dbus_menu_constructed (GObject *object)
+{
+ SnDBusMenu *menu;
+
+ G_OBJECT_CLASS (sn_dbus_menu_parent_class)->constructed (object);
+ menu = SN_DBUS_MENU (object);
+
+ menu->name_id = g_bus_watch_name (G_BUS_TYPE_SESSION, menu->bus_name,
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ name_appeared_cb, name_vanished_cb,
+ menu, NULL);
+}
+
+static void
+sn_dbus_menu_dispose (GObject *object)
+{
+ SnDBusMenu *menu;
+
+ menu = SN_DBUS_MENU (object);
+
+ if (menu->name_id > 0)
+ {
+ g_bus_unwatch_name (menu->name_id);
+ menu->name_id = 0;
+ }
+
+ g_clear_pointer (&menu->items, g_hash_table_destroy);
+
+ g_cancellable_cancel (menu->cancellable);
+ g_clear_object (&menu->cancellable);
+ g_clear_object (&menu->proxy);
+
+ G_OBJECT_CLASS (sn_dbus_menu_parent_class)->dispose (object);
+}
+
+static void
+sn_dbus_menu_finalize (GObject *object)
+{
+ SnDBusMenu *menu;
+
+ menu = SN_DBUS_MENU (object);
+
+ g_free (menu->bus_name);
+ g_free (menu->object_path);
+
+ G_OBJECT_CLASS (sn_dbus_menu_parent_class)->finalize (object);
+}
+
+static void
+sn_dbus_menu_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnDBusMenu *menu;
+
+ menu = SN_DBUS_MENU (object);
+
+ switch (property_id)
+ {
+ case PROP_BUS_NAME:
+ menu->bus_name = g_value_dup_string (value);
+ break;
+
+ case PROP_OBJECT_PATH:
+ menu->object_path = g_value_dup_string (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+ properties[PROP_BUS_NAME] =
+ g_param_spec_string ("bus-name", "bus-name", "bus-name", NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_OBJECT_PATH] =
+ g_param_spec_string ("object-path", "object-path", "object-path", NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+sn_dbus_menu_class_init (SnDBusMenuClass *menu_class)
+{
+ GObjectClass *object_class;
+
+ object_class = G_OBJECT_CLASS (menu_class);
+
+ object_class->constructed = sn_dbus_menu_constructed;
+ object_class->dispose = sn_dbus_menu_dispose;
+ object_class->finalize = sn_dbus_menu_finalize;
+ object_class->set_property = sn_dbus_menu_set_property;
+
+ install_properties (object_class);
+}
+
+static void
+sn_dbus_menu_init (SnDBusMenu *menu)
+{
+ menu->items = g_hash_table_new_full (NULL, NULL, NULL, sn_dubs_menu_item_free);
+ menu->cancellable = g_cancellable_new ();
+}
+
+GtkMenu *
+sn_dbus_menu_new (const gchar *bus_name,
+ const gchar *object_path)
+{
+ return g_object_new (SN_TYPE_DBUS_MENU,
+ "bus-name", bus_name,
+ "object-path", object_path,
+ NULL);
+}
diff --git a/applets/notification_area/status-notifier/sn-dbus-menu.h b/applets/notification_area/status-notifier/sn-dbus-menu.h
new file mode 100644
index 00000000..39907d07
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-dbus-menu.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_DBUS_MENU_H
+#define SN_DBUS_MENU_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_DBUS_MENU sn_dbus_menu_get_type ()
+G_DECLARE_FINAL_TYPE (SnDBusMenu, sn_dbus_menu, SN, DBUS_MENU, GtkMenu)
+
+GtkMenu *sn_dbus_menu_new (const gchar *bus_name,
+ const gchar *object_path);
+
+G_END_DECLS
+
+#endif
diff --git a/applets/notification_area/status-notifier/sn-host-v0.c b/applets/notification_area/status-notifier/sn-host-v0.c
new file mode 100644
index 00000000..eeab0254
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-host-v0.c
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sn-host-v0.h"
+#include "sn-item-v0.h"
+#include "sn-watcher-v0-gen.h"
+
+#define SN_HOST_BUS_NAME "org.kde.StatusNotifierHost"
+#define SN_HOST_OBJECT_PATH "/StatusNotifierHost"
+#define SN_ITEM_OBJECT_PATH "/StatusNotifierItem"
+#define SN_WATCHER_BUS_NAME "org.kde.StatusNotifierWatcher"
+#define SN_WATCHER_OBJECT_PATH "/StatusNotifierWatcher"
+
+struct _SnHostV0
+{
+ SnHostV0GenSkeleton parent;
+
+ gchar *bus_name;
+ gchar *object_path;
+ guint bus_name_id;
+
+ GCancellable *cancellable;
+
+ guint watcher_id;
+ SnWatcherV0Gen *watcher;
+
+ GSList *items;
+
+ gint icon_padding;
+ gint icon_size;
+};
+
+enum
+{
+ PROP_0,
+
+ PROP_ICON_PADDING,
+ PROP_ICON_SIZE,
+
+ LAST_PROP
+};
+
+static void sn_host_v0_gen_init (SnHostV0GenIface *iface);
+static void na_host_init (NaHostInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (SnHostV0, sn_host_v0, SN_TYPE_HOST_V0_GEN_SKELETON,
+ G_IMPLEMENT_INTERFACE (SN_TYPE_HOST_V0_GEN, sn_host_v0_gen_init)
+ G_IMPLEMENT_INTERFACE (NA_TYPE_HOST, na_host_init))
+
+static void
+sn_host_v0_gen_init (SnHostV0GenIface *iface)
+{
+}
+
+static void
+na_host_init (NaHostInterface *iface)
+{
+}
+
+static void
+get_bus_name_and_object_path (const gchar *service,
+ gchar **bus_name,
+ gchar **object_path)
+{
+ gchar *tmp;
+
+ g_assert (*bus_name == NULL);
+ g_assert (*object_path == NULL);
+
+ tmp = g_strstr_len (service, -1, "/");
+ if (tmp != NULL)
+ {
+ gchar **strings;
+
+ strings = g_strsplit (service, "/", 2);
+
+ *bus_name = g_strdup (strings[0]);
+ *object_path = g_strdup (tmp);
+
+ g_strfreev (strings);
+ }
+ else
+ {
+ *bus_name = g_strdup (service);
+ *object_path = g_strdup (SN_ITEM_OBJECT_PATH);
+ }
+}
+
+static void
+ready_cb (SnItem *item,
+ SnHostV0 *v0)
+{
+ na_host_emit_item_added (NA_HOST (v0), NA_ITEM (item));
+}
+
+static void
+add_registered_item (SnHostV0 *v0,
+ const gchar *service)
+{
+ gchar *bus_name;
+ gchar *object_path;
+ SnItem *item;
+
+ bus_name = NULL;
+ object_path = NULL;
+
+ get_bus_name_and_object_path (service, &bus_name, &object_path);
+
+ item = sn_item_v0_new (bus_name, object_path);
+ g_object_ref_sink (item);
+
+ g_object_bind_property (v0, "icon-padding", item, "margin",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+ g_object_bind_property (v0, "icon-size", item, "icon-size",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+ v0->items = g_slist_prepend (v0->items, item);
+ g_signal_connect (item, "ready", G_CALLBACK (ready_cb), v0);
+
+ g_free (bus_name);
+ g_free (object_path);
+}
+
+static void
+item_registered_cb (SnWatcherV0Gen *watcher,
+ const gchar *service,
+ SnHostV0 *v0)
+{
+ add_registered_item (v0, service);
+}
+
+static void
+item_unregistered_cb (SnWatcherV0Gen *watcher,
+ const gchar *service,
+ SnHostV0 *v0)
+{
+ GSList *l;
+
+ for (l = v0->items; l != NULL; l = g_slist_next (l))
+ {
+ SnItem *item;
+ gboolean found;
+ gchar *bus_name;
+ gchar *object_path;
+
+ item = SN_ITEM (l->data);
+
+ found = FALSE;
+ bus_name = NULL;
+ object_path = NULL;
+
+ get_bus_name_and_object_path (service, &bus_name, &object_path);
+
+ if (g_strcmp0 (sn_item_get_bus_name (item), bus_name) == 0 &&
+ g_strcmp0 (sn_item_get_object_path (item), object_path) == 0)
+ {
+ v0->items = g_slist_remove (v0->items, item);
+ na_host_emit_item_removed (NA_HOST (v0), NA_ITEM (item));
+ g_object_unref (item);
+
+ found = TRUE;
+ }
+
+ g_free (bus_name);
+ g_free (object_path);
+
+ if (found)
+ break;
+ }
+}
+
+static void
+register_host_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error;
+ SnHostV0 *v0;
+ gchar **items;
+ gint i;
+
+ error = NULL;
+ sn_watcher_v0_gen_call_register_host_finish (SN_WATCHER_V0_GEN (source_object),
+ res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ v0 = SN_HOST_V0 (user_data);
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+
+ return;
+ }
+
+ g_signal_connect (v0->watcher, "item-registered",
+ G_CALLBACK (item_registered_cb), v0);
+
+ g_signal_connect (v0->watcher, "item-unregistered",
+ G_CALLBACK (item_unregistered_cb), v0);
+
+ items = sn_watcher_v0_gen_dup_registered_items (v0->watcher);
+
+ for (i = 0; items[i] != NULL; i++)
+ add_registered_item (v0, items[i]);
+
+ g_strfreev (items);
+}
+
+static void
+proxy_ready_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error;
+ SnWatcherV0Gen *watcher;
+ SnHostV0 *v0;
+
+ error = NULL;
+ watcher = sn_watcher_v0_gen_proxy_new_finish (res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ v0 = SN_HOST_V0 (user_data);
+ v0->watcher = watcher;
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+
+ return;
+ }
+
+ sn_watcher_v0_gen_call_register_host (v0->watcher, v0->object_path,
+ v0->cancellable, register_host_cb, v0);
+}
+
+static void
+name_appeared_cb (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (user_data);
+
+ g_assert (v0->cancellable == NULL);
+ v0->cancellable = g_cancellable_new ();
+
+ sn_watcher_v0_gen_proxy_new (connection, G_DBUS_PROXY_FLAGS_NONE,
+ SN_WATCHER_BUS_NAME, SN_WATCHER_OBJECT_PATH,
+ v0->cancellable, proxy_ready_cb, user_data);
+}
+
+static void
+emit_item_removed_signal (gpointer data,
+ gpointer user_data)
+{
+ na_host_emit_item_removed (NA_HOST (user_data), NA_ITEM (data));
+}
+
+static void
+name_vanished_cb (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (user_data);
+
+ g_cancellable_cancel (v0->cancellable);
+ g_clear_object (&v0->cancellable);
+
+ g_clear_object (&v0->watcher);
+
+ if (v0->items)
+ {
+ g_slist_foreach (v0->items, emit_item_removed_signal, v0);
+ g_slist_free_full (v0->items, g_object_unref);
+ v0->items = NULL;
+ }
+}
+
+static void
+bus_acquired_cb (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ SnHostV0 *v0;
+ GDBusInterfaceSkeleton *skeleton;
+ GError *error;
+
+ v0 = SN_HOST_V0 (user_data);
+ skeleton = G_DBUS_INTERFACE_SKELETON (v0);
+
+ error = NULL;
+ g_dbus_interface_skeleton_export (skeleton, connection,
+ v0->object_path, &error);
+
+ if (error != NULL)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+
+ return;
+ }
+
+ v0->watcher_id = g_bus_watch_name (G_BUS_TYPE_SESSION, SN_WATCHER_BUS_NAME,
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ name_appeared_cb, name_vanished_cb,
+ v0, NULL);
+}
+
+static void
+sn_host_v0_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (object);
+
+ switch (property_id)
+ {
+ case PROP_ICON_PADDING:
+ g_value_set_int (value, v0->icon_padding);
+ break;
+
+ case PROP_ICON_SIZE:
+ g_value_set_int (value, v0->icon_size);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_host_v0_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (object);
+
+ switch (property_id)
+ {
+ case PROP_ICON_PADDING:
+ v0->icon_padding = g_value_get_int (value);
+ break;
+
+ case PROP_ICON_SIZE:
+ v0->icon_size = g_value_get_int (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+ /* NaHost properties */
+ g_object_class_override_property (object_class, PROP_ICON_PADDING, "icon-padding");
+ g_object_class_override_property (object_class, PROP_ICON_SIZE, "icon-size");
+}
+
+static void
+sn_host_v0_dispose (GObject *object)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (object);
+
+ if (v0->bus_name_id > 0)
+ {
+ g_bus_unown_name (v0->bus_name_id);
+ v0->bus_name_id = 0;
+ }
+
+ if (v0->watcher_id > 0)
+ {
+ g_bus_unwatch_name (v0->watcher_id);
+ v0->watcher_id = 0;
+ }
+
+ g_cancellable_cancel (v0->cancellable);
+ g_clear_object (&v0->cancellable);
+
+ g_clear_object (&v0->watcher);
+
+ if (v0->items)
+ {
+ g_slist_foreach (v0->items, emit_item_removed_signal, v0);
+ g_slist_free_full (v0->items, g_object_unref);
+ v0->items = NULL;
+ }
+
+ G_OBJECT_CLASS (sn_host_v0_parent_class)->dispose (object);
+}
+
+static void
+sn_host_v0_finalize (GObject *object)
+{
+ SnHostV0 *v0;
+
+ v0 = SN_HOST_V0 (object);
+
+ g_clear_pointer (&v0->bus_name, g_free);
+ g_clear_pointer (&v0->object_path, g_free);
+
+ G_OBJECT_CLASS (sn_host_v0_parent_class)->finalize (object);
+}
+
+static void
+sn_host_v0_class_init (SnHostV0Class *v0_class)
+{
+ GObjectClass *object_class;
+
+ object_class = G_OBJECT_CLASS (v0_class);
+
+ object_class->dispose = sn_host_v0_dispose;
+ object_class->finalize = sn_host_v0_finalize;
+ object_class->get_property = sn_host_v0_get_property;
+ object_class->set_property = sn_host_v0_set_property;
+
+ install_properties (object_class);
+}
+
+static void
+sn_host_v0_init (SnHostV0 *v0)
+{
+ GBusNameOwnerFlags flags;
+ static guint id;
+
+ flags = G_BUS_NAME_OWNER_FLAGS_NONE;
+ id++;
+
+ v0->bus_name = g_strdup_printf ("%s-%d-%d", SN_HOST_BUS_NAME, getpid (), id);
+ v0->object_path = g_strdup_printf ("%s/%d", SN_HOST_OBJECT_PATH,id);
+
+ v0->bus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, v0->bus_name, flags,
+ bus_acquired_cb, NULL, NULL, v0, NULL);
+
+ v0->icon_size = 16;
+ v0->icon_padding = 0;
+}
+
+NaHost *
+sn_host_v0_new (void)
+{
+ return g_object_new (SN_TYPE_HOST_V0, NULL);
+}
diff --git a/applets/notification_area/status-notifier/sn-host-v0.h b/applets/notification_area/status-notifier/sn-host-v0.h
new file mode 100644
index 00000000..61c8b5c3
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-host-v0.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_HOST_V0_H
+#define SN_HOST_V0_H
+
+#include "na-host.h"
+#include "sn-host-v0-gen.h"
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_HOST_V0 sn_host_v0_get_type ()
+G_DECLARE_FINAL_TYPE (SnHostV0, sn_host_v0, SN, HOST_V0, SnHostV0GenSkeleton)
+
+NaHost *sn_host_v0_new (void);
+
+G_END_DECLS
+
+#endif
diff --git a/applets/notification_area/status-notifier/sn-image-menu-item.c b/applets/notification_area/status-notifier/sn-image-menu-item.c
new file mode 100644
index 00000000..2246b961
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-image-menu-item.c
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sn-image-menu-item.h"
+
+#define SPACING 6
+
+struct _SnImageMenuItem
+{
+ GtkMenuItem parent;
+
+ GtkWidget *box;
+ GtkWidget *image;
+ GtkWidget *accel_label;
+
+ gchar *label;
+};
+
+G_DEFINE_TYPE (SnImageMenuItem, sn_image_menu_item, GTK_TYPE_MENU_ITEM)
+
+static void
+sn_image_menu_item_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ GtkWidgetClass *widget_class;
+ SnImageMenuItem *item;
+ GtkRequisition image_requisition;
+
+ widget_class = GTK_WIDGET_CLASS (sn_image_menu_item_parent_class);
+ item = SN_IMAGE_MENU_ITEM (widget);
+
+ widget_class->get_preferred_width (widget, minimum, natural);
+
+ if (!gtk_widget_get_visible (item->image))
+ return;
+
+ gtk_widget_get_preferred_size (item->image, &image_requisition, NULL);
+
+ if (image_requisition.width > 0)
+ {
+ *minimum -= image_requisition.width + SPACING;
+ *natural -= image_requisition.width + SPACING;
+ }
+}
+
+static void
+sn_image_menu_item_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkWidgetClass *widget_class;
+ SnImageMenuItem *item;
+ GtkRequisition image_requisition;
+ GtkAllocation box_allocation;
+
+ widget_class = GTK_WIDGET_CLASS (sn_image_menu_item_parent_class);
+ item = SN_IMAGE_MENU_ITEM (widget);
+
+ widget_class->size_allocate (widget, allocation);
+
+ if (!gtk_widget_get_visible (item->image))
+ return;
+
+ gtk_widget_get_preferred_size (item->image, &image_requisition, NULL);
+ gtk_widget_get_allocation (item->box, &box_allocation);
+
+ if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_LTR)
+ {
+ if (image_requisition.width > 0)
+ box_allocation.x -= image_requisition.width + SPACING;
+ }
+ else
+ {
+ if (image_requisition.width > 0)
+ box_allocation.x += image_requisition.width + SPACING;
+ }
+
+ gtk_widget_size_allocate (item->box, &box_allocation);
+}
+
+static const gchar *
+sn_image_menu_item_get_label (GtkMenuItem *menu_item)
+{
+ SnImageMenuItem *item;
+
+ item = SN_IMAGE_MENU_ITEM (menu_item);
+
+ return item->label;
+}
+
+static void
+sn_image_menu_item_toggle_size_request (GtkMenuItem *menu_item,
+ gint *requisition)
+{
+ SnImageMenuItem *item;
+ GtkRequisition image_requisition;
+
+ item = SN_IMAGE_MENU_ITEM (menu_item);
+
+ *requisition = 0;
+
+ if (!gtk_widget_get_visible (item->image))
+ return;
+
+ gtk_widget_get_preferred_size (item->image, &image_requisition, NULL);
+
+ if (image_requisition.width > 0)
+ *requisition = image_requisition.width + SPACING;
+}
+
+static void
+sn_image_menu_item_set_label (GtkMenuItem *menu_item,
+ const gchar *label)
+{
+ SnImageMenuItem *item;
+
+ item = SN_IMAGE_MENU_ITEM (menu_item);
+
+ if (g_strcmp0 (item->label, label) != 0)
+ {
+ g_free (item->label);
+ item->label = g_strdup (label);
+
+ gtk_label_set_text_with_mnemonic (GTK_LABEL (item->accel_label), label);
+ g_object_notify (G_OBJECT (menu_item), "label");
+ }
+}
+
+static void
+sn_image_menu_item_class_init (SnImageMenuItemClass *item_class)
+{
+ GtkWidgetClass *widget_class;
+ GtkMenuItemClass *menu_item_class;
+
+ widget_class = GTK_WIDGET_CLASS (item_class);
+ menu_item_class = GTK_MENU_ITEM_CLASS (item_class);
+
+ widget_class->get_preferred_width = sn_image_menu_item_get_preferred_width;
+ widget_class->size_allocate = sn_image_menu_item_size_allocate;
+
+ menu_item_class->get_label = sn_image_menu_item_get_label;
+ menu_item_class->toggle_size_request = sn_image_menu_item_toggle_size_request;
+ menu_item_class->set_label = sn_image_menu_item_set_label;
+}
+
+static void
+sn_image_menu_item_init (SnImageMenuItem *item)
+{
+ GtkAccelLabel *accel_label;
+
+ item->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, SPACING);
+ gtk_container_add (GTK_CONTAINER (item), item->box);
+ gtk_widget_show (item->box);
+
+ item->image = gtk_image_new ();
+ gtk_box_pack_start (GTK_BOX (item->box), item->image, FALSE, FALSE, 0);
+
+ item->accel_label = gtk_accel_label_new ("");
+ gtk_box_pack_end (GTK_BOX (item->box), item->accel_label, TRUE, TRUE, 0);
+ gtk_label_set_xalign (GTK_LABEL (item->accel_label), 0.0);
+ gtk_widget_show (item->accel_label);
+
+ accel_label = GTK_ACCEL_LABEL (item->accel_label);
+ gtk_accel_label_set_accel_widget (accel_label, GTK_WIDGET (item));
+ gtk_label_set_use_underline (GTK_LABEL (accel_label), TRUE);
+}
+
+GtkWidget *
+sn_image_menu_item_new (void)
+{
+ return g_object_new (SN_TYPE_IMAGE_MENU_ITEM, NULL);
+}
+
+void
+sn_image_menu_item_set_image_from_icon_name (SnImageMenuItem *item,
+ const gchar *icon_name)
+{
+ GtkImage *image;
+
+ image = GTK_IMAGE (item->image);
+
+ gtk_image_set_from_icon_name (image, icon_name, GTK_ICON_SIZE_MENU);
+ gtk_image_set_pixel_size (image, 16);
+ gtk_widget_show (item->image);
+}
+
+void
+sn_image_menu_item_set_image_from_icon_pixbuf (SnImageMenuItem *item,
+ GdkPixbuf *pixbuf)
+{
+ gtk_image_set_from_pixbuf (GTK_IMAGE (item->image), pixbuf);
+ gtk_widget_show (item->image);
+}
+
+void
+sn_image_menu_item_unset_image (SnImageMenuItem *item)
+{
+ gtk_image_clear (GTK_IMAGE (item->image));
+ gtk_widget_hide (item->image);
+}
diff --git a/applets/notification_area/status-notifier/sn-image-menu-item.h b/applets/notification_area/status-notifier/sn-image-menu-item.h
new file mode 100644
index 00000000..b0fe8a90
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-image-menu-item.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_IMAGE_MENU_ITEM_H
+#define SN_IMAGE_MENU_ITEM_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_IMAGE_MENU_ITEM sn_image_menu_item_get_type ()
+G_DECLARE_FINAL_TYPE (SnImageMenuItem, sn_image_menu_item,
+ SN, IMAGE_MENU_ITEM, GtkMenuItem)
+
+GtkWidget *sn_image_menu_item_new (void);
+
+void sn_image_menu_item_set_image_from_icon_name (SnImageMenuItem *item,
+ const gchar *icon_name);
+
+void sn_image_menu_item_set_image_from_icon_pixbuf (SnImageMenuItem *item,
+ GdkPixbuf *pixbuf);
+
+void sn_image_menu_item_unset_image (SnImageMenuItem *item);
+
+G_END_DECLS
+
+#endif
diff --git a/applets/notification_area/status-notifier/sn-item-v0.c b/applets/notification_area/status-notifier/sn-item-v0.c
new file mode 100644
index 00000000..eb5a95c2
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-item-v0.c
@@ -0,0 +1,1355 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ * Copyright (C) 2017 Colomban Wendling <[email protected]>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <math.h>
+
+#include "sn-item.h"
+#include "sn-item-v0.h"
+#include "sn-item-v0-gen.h"
+
+#define SN_ITEM_INTERFACE "org.kde.StatusNotifierItem"
+
+typedef struct
+{
+ cairo_surface_t *surface;
+ gint width;
+ gint height;
+} SnIconPixmap;
+
+typedef struct
+{
+ gchar *icon_name;
+ SnIconPixmap **icon_pixmap;
+ gchar *title;
+ gchar *text;
+} SnTooltip;
+
+struct _SnItemV0
+{
+ SnItem parent;
+
+ GtkWidget *image;
+ gint icon_size;
+ gint effective_icon_size;
+
+ GCancellable *cancellable;
+ SnItemV0Gen *proxy;
+
+ gchar *id;
+ gchar *category;
+ gchar *status;
+
+ gchar *title;
+ gint32 window_id;
+ gchar *icon_name;
+ SnIconPixmap **icon_pixmap;
+ gchar *overlay_icon_name;
+ SnIconPixmap **overlay_icon_pixmap;
+ gchar *attention_icon_name;
+ SnIconPixmap **attention_icon_pixmap;
+ gchar *attention_movie_name;
+ SnTooltip *tooltip;
+ gchar *icon_theme_path;
+ gchar *menu;
+ gboolean item_is_menu;
+
+ guint update_id;
+};
+
+enum
+{
+ PROP_0,
+
+ PROP_ICON_SIZE,
+
+ LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP] = { NULL };
+
+G_DEFINE_TYPE (SnItemV0, sn_item_v0, SN_TYPE_ITEM)
+
+static cairo_surface_t *
+scale_surface (SnIconPixmap *pixmap,
+ GtkOrientation orientation,
+ gint size)
+{
+ gdouble ratio;
+ gdouble new_width;
+ gdouble new_height;
+ gdouble scale_x;
+ gdouble scale_y;
+ gint width;
+ gint height;
+ cairo_content_t content;
+ cairo_surface_t *scaled;
+ cairo_t *cr;
+
+ ratio = pixmap->width / (gdouble) pixmap->height;
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ new_height = (gdouble) size;
+ new_width = new_height * ratio;
+ }
+ else
+ {
+ new_width = (gdouble) size;
+ new_height = new_width * ratio;
+ }
+
+ scale_x = new_width / pixmap->width;
+ scale_y = new_height / pixmap->height;
+
+ width = ceil (new_width);
+ height = ceil (new_height);
+
+ content = CAIRO_CONTENT_COLOR_ALPHA;
+ scaled = cairo_surface_create_similar (pixmap->surface, content, width, height);
+ cr = cairo_create (scaled);
+
+ cairo_scale (cr, scale_x, scale_y);
+ cairo_set_source_surface (cr, pixmap->surface, 0, 0);
+ cairo_paint (cr);
+
+ cairo_destroy (cr);
+ return scaled;
+}
+
+static gint
+compare_size (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ SnIconPixmap *p1;
+ SnIconPixmap *p2;
+ GtkOrientation orientation;
+
+ p1 = (SnIconPixmap *) a;
+ p2 = (SnIconPixmap *) b;
+ orientation = GPOINTER_TO_UINT (user_data);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ return p1->height - p2->height;
+ else
+ return p1->width - p2->width;
+}
+
+static GList *
+get_pixmaps_sorted (SnItemV0 *v0,
+ GtkOrientation orientation,
+ gint size)
+{
+ GList *pixmaps;
+ guint i;
+
+ pixmaps = NULL;
+ for (i = 0; v0->icon_pixmap[i] != NULL; i++)
+ pixmaps = g_list_prepend (pixmaps, v0->icon_pixmap[i]);
+
+ return g_list_sort_with_data (pixmaps, compare_size,
+ GUINT_TO_POINTER (orientation));
+}
+
+static cairo_surface_t *
+get_surface (SnItemV0 *v0,
+ GtkOrientation orientation,
+ gint size)
+{
+ GList *pixmaps;
+ cairo_surface_t *surface;
+ SnIconPixmap *best;
+ GList *l;
+
+ g_assert (v0->icon_pixmap != NULL && v0->icon_pixmap[0] != NULL);
+
+ pixmaps = get_pixmaps_sorted (v0, orientation, size);
+ surface = NULL;
+
+ for (l = pixmaps; l != NULL; l = l->next)
+ {
+ SnIconPixmap *pixmap;
+
+ pixmap = (SnIconPixmap *) l->data;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ if (pixmap->height == size)
+ {
+ surface = pixmap->surface;
+ break;
+ }
+ else if (pixmap->height > size)
+ {
+ best = pixmap;
+ break;
+ }
+ }
+ else
+ {
+ if (pixmap->width == size)
+ {
+ surface = pixmap->surface;
+ break;
+ }
+ else if (pixmap->width > size)
+ {
+ best = pixmap;
+ break;
+ }
+ }
+
+ best = pixmap;
+ }
+
+ g_list_free (pixmaps);
+
+ if (surface != NULL)
+ return cairo_surface_reference (surface);
+
+ return scale_surface (best, orientation, size);
+}
+
+static void
+update (SnItemV0 *v0)
+{
+ AtkObject *accessible;
+ GtkImage *image;
+ SnTooltip *tip;
+ gboolean visible;
+ gint icon_size;
+
+ g_return_if_fail (SN_IS_ITEM_V0 (v0));
+
+ image = GTK_IMAGE (v0->image);
+
+ if (v0->icon_size > 0)
+ icon_size = v0->icon_size;
+ else
+ icon_size = MAX (1, v0->effective_icon_size);
+
+ if (v0->icon_name != NULL && *v0->icon_name != '\0')
+ {
+ GtkIconTheme *icon_theme;
+
+ icon_theme = gtk_icon_theme_get_default ();
+
+ gtk_icon_theme_rescan_if_needed (icon_theme);
+ gtk_image_set_from_icon_name (image, v0->icon_name, GTK_ICON_SIZE_MENU);
+ gtk_image_set_pixel_size (image, icon_size);
+ }
+ else if (v0->icon_pixmap != NULL && v0->icon_pixmap[0] != NULL)
+ {
+ cairo_surface_t *surface;
+
+ surface = get_surface (v0,
+ gtk_orientable_get_orientation (GTK_ORIENTABLE (v0)),
+ icon_size);
+ gtk_image_set_from_surface (image, surface);
+ cairo_surface_destroy (surface);
+ }
+ else
+ {
+ gtk_image_set_from_icon_name (image, "image-missing", GTK_ICON_SIZE_MENU);
+ gtk_image_set_pixel_size (image, icon_size);
+ }
+
+ tip = v0->tooltip;
+
+ if (tip != NULL)
+ {
+ gchar *markup;
+
+ markup = NULL;
+
+ if ((tip->title != NULL && *tip->title != '\0') &&
+ (tip->text != NULL && *tip->text != '\0'))
+ {
+ markup = g_strdup_printf ("%s\n%s", tip->title, tip->text);
+ }
+ else if (tip->title != NULL && *tip->title != '\0')
+ {
+ markup = g_strdup (tip->title);
+ }
+ else if (tip->text != NULL && *tip->text != '\0')
+ {
+ markup = g_strdup (tip->text);
+ }
+
+ gtk_widget_set_tooltip_markup (GTK_WIDGET (v0), markup);
+ g_free (markup);
+ }
+ else
+ {
+ gtk_widget_set_tooltip_markup (GTK_WIDGET (v0), NULL);
+ }
+
+ accessible = gtk_widget_get_accessible (GTK_WIDGET (v0));
+
+ if (v0->title != NULL && *v0->title != '\0')
+ atk_object_set_name (accessible, v0->title);
+ else
+ atk_object_set_name (accessible, v0->id);
+
+ visible = g_strcmp0 (v0->status, "Passive") != 0;
+ gtk_widget_set_visible (GTK_WIDGET (v0), visible);
+}
+
+static gboolean
+update_cb (gpointer user_data)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ v0->update_id = 0;
+ update (v0);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+queue_update (SnItemV0 *v0)
+{
+ if (v0->update_id != 0)
+ return;
+
+ v0->update_id = g_timeout_add (10, update_cb, v0);
+ g_source_set_name_by_id (v0->update_id, "[status-notifier] update_cb");
+}
+
+static cairo_surface_t *
+surface_from_variant (GVariant *variant,
+ gint width,
+ gint height)
+{
+ cairo_format_t format;
+ gint stride;
+ guint32 *data;
+
+ format = CAIRO_FORMAT_ARGB32;
+ stride = cairo_format_stride_for_width (format, width);
+ data = (guint32 *) g_variant_get_data (variant);
+
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ {
+ gint i;
+
+ for (i = 0; i < width * height; i++)
+ data[i] = GUINT32_FROM_BE (data[i]);
+ }
+#endif
+
+ return cairo_image_surface_create_for_data ((guchar *) data, format,
+ width, height, stride);
+}
+
+static cairo_surface_t *
+icon_surface_new (GVariant *variant,
+ gint width,
+ gint height)
+{
+ cairo_surface_t *surface;
+ cairo_surface_t *tmp;
+ cairo_t *cr;
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+ if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
+ return NULL;
+
+ tmp = surface_from_variant (variant, width, height);
+ if (cairo_surface_status (tmp) != CAIRO_STATUS_SUCCESS)
+ {
+ cairo_surface_destroy (surface);
+ return NULL;
+ }
+
+ cr = cairo_create (surface);
+ if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
+ {
+ cairo_surface_destroy (surface);
+ cairo_surface_destroy (tmp);
+ return NULL;
+ }
+
+ cairo_set_source_surface (cr, tmp, 0, 0);
+ cairo_paint (cr);
+
+ cairo_surface_destroy (tmp);
+ cairo_destroy (cr);
+
+ return surface;
+}
+
+static SnIconPixmap **
+icon_pixmap_new (GVariant *variant)
+{
+ GPtrArray *array;
+ GVariantIter iter;
+ gint width;
+ gint height;
+ GVariant *value;
+
+ if (variant == NULL || g_variant_iter_init (&iter, variant) == 0)
+ return NULL;
+
+ array = g_ptr_array_new ();
+ while (g_variant_iter_next (&iter, "(ii@ay)", &width, &height, &value))
+ {
+ cairo_surface_t *surface;
+
+ if (width == 0 || height == 0)
+ {
+ g_variant_unref (value);
+ continue;
+ }
+
+ surface = icon_surface_new (value, width, height);
+ g_variant_unref (value);
+
+ if (surface != NULL)
+ {
+ SnIconPixmap *pixmap;
+
+ pixmap = g_new0 (SnIconPixmap, 1);
+
+ pixmap->surface = surface;
+ pixmap->width = width;
+ pixmap->height = height;
+
+ g_ptr_array_add (array, pixmap);
+ }
+ }
+
+ g_ptr_array_add (array, NULL);
+ return (SnIconPixmap **) g_ptr_array_free (array, FALSE);
+}
+
+static void
+icon_pixmap_free (SnIconPixmap **data)
+{
+ gint i;
+
+ if (data == NULL)
+ return;
+
+ for (i = 0; data[i] != NULL; i++)
+ {
+ cairo_surface_destroy (data[i]->surface);
+ g_free (data[i]);
+ }
+
+ g_free (data);
+}
+
+static SnTooltip *
+sn_tooltip_new (GVariant *variant)
+{
+ const gchar *icon_name;
+ GVariant *icon_pixmap;
+ const gchar *title;
+ const gchar *text;
+ SnTooltip *tooltip;
+
+ if (variant == NULL)
+ return NULL;
+
+ g_variant_get (variant, "(&s@a(iiay)&s&s)",
+ &icon_name, &icon_pixmap,
+ &title, &text);
+
+ tooltip = g_new0 (SnTooltip, 1);
+
+ tooltip->icon_name = g_strdup (icon_name);
+ tooltip->icon_pixmap = icon_pixmap_new (icon_pixmap);
+ tooltip->title = g_strdup (title);
+ tooltip->text = g_strdup (text);
+
+ g_variant_unref (icon_pixmap);
+ return tooltip;
+}
+
+static void
+sn_tooltip_free (SnTooltip *tooltip)
+{
+ if (tooltip == NULL)
+ return;
+
+ g_free (tooltip->icon_name);
+ icon_pixmap_free (tooltip->icon_pixmap);
+ g_free (tooltip->title);
+ g_free (tooltip->text);
+
+ g_free (tooltip);
+}
+
+static GVariant *
+get_property (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data,
+ gboolean *cancelled)
+{
+ GVariant *variant;
+ GError *error;
+ GVariant *property;
+
+ error = NULL;
+ variant = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object),
+ res, &error);
+
+ *cancelled = FALSE;
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ *cancelled = TRUE;
+ g_error_free (error);
+ return NULL;
+ }
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ g_variant_get (variant, "(v)", &property);
+ g_variant_unref (variant);
+
+ return property;
+}
+
+static void
+update_property (SnItemV0 *v0,
+ const gchar *property,
+ GAsyncReadyCallback callback)
+{
+ GDBusProxy *proxy;
+ SnItem *item;
+
+ proxy = G_DBUS_PROXY (v0->proxy);
+ item = SN_ITEM (v0);
+
+ g_dbus_connection_call (g_dbus_proxy_get_connection (proxy),
+ sn_item_get_bus_name (item),
+ sn_item_get_object_path (item),
+ "org.freedesktop.DBus.Properties", "Get",
+ g_variant_new ("(ss)", SN_ITEM_INTERFACE, property),
+ G_VARIANT_TYPE ("(v)"),
+ G_DBUS_CALL_FLAGS_NONE, -1,
+ v0->cancellable, callback, v0);
+}
+
+static void
+update_title (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->title, g_free);
+ v0->title = g_variant_dup_string (variant, NULL);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+new_title_cb (SnItemV0 *v0)
+{
+ update_property (v0, "Title", update_title);
+}
+
+static void
+update_icon_name (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->icon_name, g_free);
+ v0->icon_name = g_variant_dup_string (variant, NULL);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+update_icon_pixmap (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->icon_pixmap, icon_pixmap_free);
+ v0->icon_pixmap = icon_pixmap_new (variant);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+new_icon_cb (SnItemV0 *v0)
+{
+ update_property (v0, "IconName", update_icon_name);
+ update_property (v0, "IconPixmap", update_icon_pixmap);
+}
+
+static void
+update_overlay_icon_name (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->overlay_icon_name, g_free);
+ v0->overlay_icon_name = g_variant_dup_string (variant, NULL);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+update_overlay_icon_pixmap (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->overlay_icon_pixmap, icon_pixmap_free);
+ v0->overlay_icon_pixmap = icon_pixmap_new (variant);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+new_overlay_icon_cb (SnItemV0 *v0)
+{
+ update_property (v0, "OverlayIconName", update_overlay_icon_name);
+ update_property (v0, "OverlayIconPixmap", update_overlay_icon_pixmap);
+}
+
+static void
+update_attention_icon_name (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->attention_icon_name, g_free);
+ v0->attention_icon_name = g_variant_dup_string (variant, NULL);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+update_attention_icon_pixmap (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->attention_icon_pixmap, icon_pixmap_free);
+ v0->attention_icon_pixmap = icon_pixmap_new (variant);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+new_attention_icon_cb (SnItemV0 *v0)
+{
+ update_property (v0, "AttentionIconName", update_attention_icon_name);
+ update_property (v0, "AttentionIconPixmap", update_attention_icon_pixmap);
+}
+
+static void
+update_tooltip (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *variant;
+ gboolean cancelled;
+
+ variant = get_property (source_object, res, user_data, &cancelled);
+ if (cancelled)
+ return;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ g_clear_pointer (&v0->tooltip, sn_tooltip_free);
+ v0->tooltip = sn_tooltip_new (variant);
+ g_clear_pointer (&variant, g_variant_unref);
+
+ queue_update (v0);
+}
+
+static void
+new_tooltip_cb (SnItemV0 *v0)
+{
+ update_property (v0, "ToolTip", update_tooltip);
+}
+
+static void
+new_status_cb (SnItemV0 *v0,
+ GVariant *parameters)
+{
+ GVariant *variant;
+
+ variant = g_variant_get_child_value (parameters, 0);
+
+ g_free (v0->status);
+ v0->status = g_variant_dup_string (variant, NULL);
+ g_variant_unref (variant);
+
+ queue_update (v0);
+}
+
+static void
+new_icon_theme_path_cb (SnItemV0 *v0,
+ GVariant *parameters)
+{
+ GVariant *variant;
+
+ variant = g_variant_get_child_value (parameters, 0);
+
+ g_free (v0->icon_theme_path);
+ v0->icon_theme_path = g_variant_dup_string (variant, NULL);
+ g_variant_unref (variant);
+
+ if (v0->icon_theme_path != NULL)
+ {
+ GtkIconTheme *icon_theme;
+
+ icon_theme = gtk_icon_theme_get_default ();
+
+ gtk_icon_theme_append_search_path (icon_theme, v0->icon_theme_path);
+ }
+
+ queue_update (v0);
+}
+
+static void
+g_properties_changed_cb (GDBusProxy *proxy,
+ GVariant *changed_properties,
+ GStrv invalidated_properties,
+ SnItemV0 *v0)
+{
+ gchar *debug;
+
+ debug = g_variant_print (changed_properties, FALSE);
+ g_debug ("g_properties_changed_cb: %s", debug);
+ g_free (debug);
+}
+
+static void
+g_signal_cb (GDBusProxy *proxy,
+ gchar *sender_name,
+ gchar *signal_name,
+ GVariant *parameters,
+ SnItemV0 *v0)
+{
+ if (g_strcmp0 (signal_name, "NewTitle") == 0)
+ new_title_cb (v0);
+ else if (g_strcmp0 (signal_name, "NewIcon") == 0)
+ new_icon_cb (v0);
+ else if (g_strcmp0 (signal_name, "NewOverlayIcon") == 0)
+ new_overlay_icon_cb (v0);
+ else if (g_strcmp0 (signal_name, "NewAttentionIcon") == 0)
+ new_attention_icon_cb (v0);
+ else if (g_strcmp0 (signal_name, "NewToolTip") == 0)
+ new_tooltip_cb (v0);
+ else if (g_strcmp0 (signal_name, "NewStatus") == 0)
+ new_status_cb (v0, parameters);
+ else if (g_strcmp0 (signal_name, "NewIconThemePath") == 0)
+ new_icon_theme_path_cb (v0, parameters);
+ else
+ g_assert_not_reached ();
+}
+
+static void
+get_all_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ GVariant *properties;
+ GError *error;
+ GVariantIter *iter;
+ gchar *key;
+ GVariant *value;
+
+ error = NULL;
+ properties = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object),
+ res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_variant_get (properties, "(a{sv})", &iter);
+ while (g_variant_iter_next (iter, "{sv}", &key, &value))
+ {
+ if (g_strcmp0 (key, "Category") == 0)
+ v0->category = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "Id") == 0)
+ v0->id = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "Title") == 0)
+ v0->title = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "Status") == 0)
+ v0->status = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "WindowId") == 0)
+ v0->window_id = g_variant_get_int32 (value);
+ else if (g_strcmp0 (key, "IconName") == 0)
+ v0->icon_name = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "IconPixmap") == 0)
+ v0->icon_pixmap = icon_pixmap_new (value);
+ else if (g_strcmp0 (key, "OverlayIconName") == 0)
+ v0->overlay_icon_name = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "OverlayIconPixmap") == 0)
+ v0->overlay_icon_pixmap = icon_pixmap_new (value);
+ else if (g_strcmp0 (key, "AttentionIconName") == 0)
+ v0->attention_icon_name = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "AttentionIconPixmap") == 0)
+ v0->attention_icon_pixmap = icon_pixmap_new (value);
+ else if (g_strcmp0 (key, "AttentionMovieName") == 0)
+ v0->attention_movie_name = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "ToolTip") == 0)
+ v0->tooltip = sn_tooltip_new (value);
+ else if (g_strcmp0 (key, "IconThemePath") == 0)
+ v0->icon_theme_path = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "Menu") == 0)
+ v0->menu = g_variant_dup_string (value, NULL);
+ else if (g_strcmp0 (key, "ItemIsMenu") == 0)
+ v0->item_is_menu = g_variant_get_boolean (value);
+ else
+ g_debug ("property '%s' not handled!", key);
+
+ g_variant_unref (value);
+ }
+
+ g_variant_iter_free (iter);
+ g_variant_unref (properties);
+
+ if (v0->id == NULL || v0->category == NULL || v0->status == NULL)
+ {
+ SnItem *item;
+ const gchar *bus_name;
+ const gchar *object_path;
+
+ item = SN_ITEM (v0);
+ bus_name = sn_item_get_bus_name (item);
+ object_path = sn_item_get_object_path (item);
+
+ g_warning ("Invalid Status Notifier Item (%s, %s)",
+ bus_name, object_path);
+
+ return;
+ }
+
+ if (v0->icon_theme_path != NULL)
+ {
+ GtkIconTheme *icon_theme;
+
+ icon_theme = gtk_icon_theme_get_default ();
+
+ gtk_icon_theme_append_search_path (icon_theme, v0->icon_theme_path);
+ }
+
+ g_signal_connect (v0->proxy, "g-properties-changed",
+ G_CALLBACK (g_properties_changed_cb), v0);
+
+ g_signal_connect (v0->proxy, "g-signal",
+ G_CALLBACK (g_signal_cb), v0);
+
+ update (v0);
+ sn_item_emit_ready (SN_ITEM (v0));
+}
+
+static void
+proxy_ready_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+ SnItemV0Gen *proxy;
+ GError *error;
+
+ error = NULL;
+ proxy = sn_item_v0_gen_proxy_new_for_bus_finish (res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ return;
+ }
+
+ v0 = SN_ITEM_V0 (user_data);
+ v0->proxy = proxy;
+
+ if (error)
+ {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_dbus_connection_call (g_dbus_proxy_get_connection (G_DBUS_PROXY (proxy)),
+ sn_item_get_bus_name (SN_ITEM (v0)),
+ sn_item_get_object_path (SN_ITEM (v0)),
+ "org.freedesktop.DBus.Properties", "GetAll",
+ g_variant_new ("(s)", SN_ITEM_INTERFACE),
+ G_VARIANT_TYPE ("(a{sv})"),
+ G_DBUS_CALL_FLAGS_NONE, -1,
+ v0->cancellable, get_all_cb, v0);
+}
+
+static void
+sn_item_v0_constructed (GObject *object)
+{
+ SnItemV0 *v0;
+ SnItem *item;
+
+ v0 = SN_ITEM_V0 (object);
+ item = SN_ITEM (v0);
+
+ G_OBJECT_CLASS (sn_item_v0_parent_class)->constructed (object);
+
+ v0->cancellable = g_cancellable_new ();
+ sn_item_v0_gen_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ sn_item_get_bus_name (item),
+ sn_item_get_object_path (item),
+ v0->cancellable,
+ proxy_ready_cb, v0);
+}
+
+static void
+sn_item_v0_dispose (GObject *object)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (object);
+
+ g_cancellable_cancel (v0->cancellable);
+ g_clear_object (&v0->cancellable);
+ g_clear_object (&v0->proxy);
+
+ if (v0->update_id != 0)
+ {
+ g_source_remove (v0->update_id);
+ v0->update_id = 0;
+ }
+
+ G_OBJECT_CLASS (sn_item_v0_parent_class)->dispose (object);
+}
+
+static void
+sn_item_v0_finalize (GObject *object)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (object);
+
+ g_clear_pointer (&v0->id, g_free);
+ g_clear_pointer (&v0->category, g_free);
+ g_clear_pointer (&v0->status, g_free);
+
+ g_clear_pointer (&v0->title, g_free);
+ g_clear_pointer (&v0->icon_name, g_free);
+ g_clear_pointer (&v0->icon_pixmap, icon_pixmap_free);
+ g_clear_pointer (&v0->overlay_icon_name, g_free);
+ g_clear_pointer (&v0->overlay_icon_pixmap, icon_pixmap_free);
+ g_clear_pointer (&v0->attention_icon_name, g_free);
+ g_clear_pointer (&v0->attention_icon_pixmap, icon_pixmap_free);
+ g_clear_pointer (&v0->attention_movie_name, g_free);
+ g_clear_pointer (&v0->tooltip, sn_tooltip_free);
+ g_clear_pointer (&v0->icon_theme_path, g_free);
+ g_clear_pointer (&v0->menu, g_free);
+
+ G_OBJECT_CLASS (sn_item_v0_parent_class)->finalize (object);
+}
+
+static const gchar *
+sn_item_v0_get_id (SnItem *item)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ return v0->id;
+}
+
+static const gchar *
+sn_item_v0_get_category (SnItem *item)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ return v0->category;
+}
+
+static const gchar *
+sn_item_v0_get_menu (SnItem *item)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ return v0->menu;
+}
+
+static void
+context_menu_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ sn_item_v0_gen_call_context_menu_finish (v0->proxy, res, NULL);
+}
+
+static void
+sn_item_v0_context_menu (SnItem *item,
+ gint x,
+ gint y)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ sn_item_v0_gen_call_context_menu (v0->proxy, x, y, NULL,
+ context_menu_cb, v0);
+}
+
+static void
+activate_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ sn_item_v0_gen_call_activate_finish (v0->proxy, res, NULL);
+}
+
+static void
+sn_item_v0_activate (SnItem *item,
+ gint x,
+ gint y)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ sn_item_v0_gen_call_activate (v0->proxy, x, y, NULL,
+ activate_cb, v0);
+}
+
+static void
+secondary_activate_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ sn_item_v0_gen_call_secondary_activate_finish (v0->proxy, res, NULL);
+}
+
+static void
+sn_item_v0_secondary_activate (SnItem *item,
+ gint x,
+ gint y)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (item);
+
+ sn_item_v0_gen_call_secondary_activate (v0->proxy, x, y, NULL,
+ secondary_activate_cb, v0);
+}
+
+static void
+scroll_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (user_data);
+
+ sn_item_v0_gen_call_scroll_finish (v0->proxy, res, NULL);
+}
+
+static void
+sn_item_v0_scroll (SnItem *item,
+ gint delta,
+ SnItemOrientation orientation)
+{
+ SnItemV0 *v0;
+ const gchar *tmp;
+
+ v0 = SN_ITEM_V0 (item);
+
+ switch (orientation)
+ {
+ case SN_ITEM_ORIENTATION_VERTICAL:
+ tmp = "Vertical";
+ break;
+
+ case SN_ITEM_ORIENTATION_HORIZONTAL:
+ default:
+ tmp = "Horizontal";
+ break;
+ }
+
+ sn_item_v0_gen_call_scroll (v0->proxy, delta, tmp, NULL, scroll_cb, v0);
+}
+
+static void
+sn_item_v0_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ SnItemV0 *v0 = SN_ITEM_V0 (widget);
+
+ GTK_WIDGET_CLASS (sn_item_v0_parent_class)->size_allocate (widget, allocation);
+
+ /* FIXME: this leads to grow-only size, unless there's underallocation.
+ * not a problem in the panel, but one in the test app.
+ * NOTE: actually, it *can* shrink, as we request a little less for padding
+ * around, but that only allows shrinking by steps of 2 */
+ if (v0->icon_size <= 0)
+ {
+ gint prev_effective_icon_size = v0->effective_icon_size;
+
+ if (gtk_orientable_get_orientation (GTK_ORIENTABLE (v0)) == GTK_ORIENTATION_HORIZONTAL)
+ v0->effective_icon_size = allocation->height;
+ else
+ v0->effective_icon_size = allocation->width;
+
+ /* leave some padding around */
+ if (v0->effective_icon_size > 2)
+ v0->effective_icon_size -= 2;
+
+ if (v0->effective_icon_size != prev_effective_icon_size)
+ queue_update (SN_ITEM_V0 (widget));
+ }
+}
+
+static void
+sn_item_v0_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (object);
+
+ switch (property_id)
+ {
+ case PROP_ICON_SIZE:
+ g_value_set_uint (value, v0->icon_size);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_item_v0_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnItemV0 *v0;
+
+ v0 = SN_ITEM_V0 (object);
+
+ switch (property_id)
+ {
+ case PROP_ICON_SIZE:
+ sn_item_v0_set_icon_size (v0, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+ properties[PROP_ICON_SIZE] =
+ g_param_spec_int ("icon-size", "Icon size", "Icon size", 0, G_MAXINT, 16,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+sn_item_v0_class_init (SnItemV0Class *v0_class)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+ SnItemClass *item_class;
+
+ object_class = G_OBJECT_CLASS (v0_class);
+ widget_class = GTK_WIDGET_CLASS (v0_class);
+ item_class = SN_ITEM_CLASS (v0_class);
+
+ object_class->constructed = sn_item_v0_constructed;
+ object_class->dispose = sn_item_v0_dispose;
+ object_class->finalize = sn_item_v0_finalize;
+ object_class->get_property = sn_item_v0_get_property;
+ object_class->set_property = sn_item_v0_set_property;
+
+ item_class->get_id = sn_item_v0_get_id;
+ item_class->get_category = sn_item_v0_get_category;
+ item_class->get_menu = sn_item_v0_get_menu;
+
+ item_class->context_menu = sn_item_v0_context_menu;
+ item_class->activate = sn_item_v0_activate;
+ item_class->secondary_activate = sn_item_v0_secondary_activate;
+ item_class->scroll = sn_item_v0_scroll;
+
+ widget_class->size_allocate = sn_item_v0_size_allocate;
+
+ gtk_widget_class_set_css_name (widget_class, "sn-item");
+
+ install_properties (object_class);
+}
+
+static void
+sn_item_v0_init (SnItemV0 *v0)
+{
+ v0->icon_size = 16;
+ v0->effective_icon_size = 0;
+ v0->image = gtk_image_new ();
+ gtk_container_add (GTK_CONTAINER (v0), v0->image);
+ gtk_widget_show (v0->image);
+}
+
+SnItem *
+sn_item_v0_new (const gchar *bus_name,
+ const gchar *object_path)
+{
+ return g_object_new (SN_TYPE_ITEM_V0,
+ "bus-name", bus_name,
+ "object-path", object_path,
+ NULL);
+}
+
+gint
+sn_item_v0_get_icon_size (SnItemV0 *v0)
+{
+ return v0->icon_size;
+}
+
+void
+sn_item_v0_set_icon_size (SnItemV0 *v0,
+ gint size)
+{
+ if (v0->icon_size != size)
+ {
+ v0->icon_size = size;
+ g_object_notify_by_pspec (G_OBJECT (v0), properties[PROP_ICON_SIZE]);
+
+ if (v0->id != NULL)
+ queue_update (v0);
+ }
+}
diff --git a/applets/notification_area/status-notifier/sn-item-v0.h b/applets/notification_area/status-notifier/sn-item-v0.h
new file mode 100644
index 00000000..4b1bd492
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-item-v0.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_ITEM_V0_H
+#define SN_ITEM_V0_H
+
+#include "sn-item.h"
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_ITEM_V0 sn_item_v0_get_type ()
+G_DECLARE_FINAL_TYPE (SnItemV0, sn_item_v0, SN, ITEM_V0, SnItem)
+
+SnItem *sn_item_v0_new (const gchar *bus_name,
+ const gchar *object_path);
+
+gint sn_item_v0_get_icon_size (SnItemV0 *v0);
+void sn_item_v0_set_icon_size (SnItemV0 *v0,
+ gint size);
+
+G_END_DECLS
+
+#endif
diff --git a/applets/notification_area/status-notifier/sn-item.c b/applets/notification_area/status-notifier/sn-item.c
new file mode 100644
index 00000000..13ee68f3
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-item.c
@@ -0,0 +1,491 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "sn-dbus-menu.h"
+#include "sn-item.h"
+
+#include "na-item.h"
+
+typedef struct
+{
+ gchar *bus_name;
+ gchar *object_path;
+
+ GtkOrientation orientation;
+
+ GtkMenu *menu;
+} SnItemPrivate;
+
+enum
+{
+ PROP_0,
+
+ PROP_BUS_NAME,
+ PROP_OBJECT_PATH,
+
+ PROP_ORIENTATION,
+
+ LAST_PROP
+};
+
+enum
+{
+ SIGNAL_READY,
+
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void na_item_init (NaItemInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (SnItem, sn_item, GTK_TYPE_BUTTON,
+ //~ G_ADD_PRIVATE (SnItem);
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE,
+ NULL)
+ G_IMPLEMENT_INTERFACE (NA_TYPE_ITEM,
+ na_item_init))
+
+/* FIXME: apparently if requesting < 2.38, G_ADD_PRIVATE() or the default
+ * sn_item_get_instance_private() does NOT work, and returns some garbage
+ * (either get_instance_private() works but G_ADD_PRIVATE() didn't do nothing,
+ * or the other way around, but it leads to using incorrect memory) */
+#define sn_item_get_instance_private(i) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((i), SN_TYPE_ITEM, SnItemPrivate))
+
+static void
+sn_item_dispose (GObject *object)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+
+ item = SN_ITEM (object);
+ priv = sn_item_get_instance_private (item);
+
+ g_clear_object (&priv->menu);
+
+ G_OBJECT_CLASS (sn_item_parent_class)->dispose (object);
+}
+
+static void
+sn_item_finalize (GObject *object)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+
+ item = SN_ITEM (object);
+ priv = sn_item_get_instance_private (item);
+
+ g_clear_pointer (&priv->bus_name, g_free);
+ g_clear_pointer (&priv->object_path, g_free);
+
+ G_OBJECT_CLASS (sn_item_parent_class)->finalize (object);
+}
+
+static void
+sn_item_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+
+ item = SN_ITEM (object);
+ priv = sn_item_get_instance_private (item);
+
+ switch (property_id)
+ {
+ case PROP_BUS_NAME:
+ g_value_set_string (value, priv->bus_name);
+ break;
+
+ case PROP_OBJECT_PATH:
+ g_value_set_string (value, priv->object_path);
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, priv->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_item_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+
+ item = SN_ITEM (object);
+ priv = sn_item_get_instance_private (item);
+
+ switch (property_id)
+ {
+ case PROP_BUS_NAME:
+ priv->bus_name = g_value_dup_string (value);
+ break;
+
+ case PROP_OBJECT_PATH:
+ priv->object_path = g_value_dup_string (value);
+ break;
+
+ case PROP_ORIENTATION:
+ priv->orientation = g_value_get_enum (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_item_get_action_coordinates (SnItem *item,
+ gint *x,
+ gint *y)
+{
+ GtkWidget *widget;
+ SnItemPrivate *priv;
+ GdkWindow *window;
+ GtkWidget *toplevel;
+ gint width;
+ gint height;
+
+ priv = sn_item_get_instance_private (item);
+ widget = GTK_WIDGET (item);
+ window = gtk_widget_get_window (widget);
+ toplevel = gtk_widget_get_toplevel (widget);
+
+ gdk_window_get_geometry (window, x, y, &width, &height);
+ gtk_widget_translate_coordinates (widget, toplevel, *x, *y, x, y);
+
+ if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
+ *y += height;
+ else
+ *x += width;
+}
+
+static gboolean
+sn_item_button_press_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+ GdkDisplay *display;
+ GdkSeat *seat;
+ gint x;
+ gint y;
+
+ if (event->button < 2 || event->button > 3)
+ return GTK_WIDGET_CLASS (sn_item_parent_class)->button_press_event (widget, event);
+
+ item = SN_ITEM (widget);
+ priv = sn_item_get_instance_private (item);
+ display = gdk_display_get_default ();
+ seat = gdk_display_get_default_seat (display);
+
+ sn_item_get_action_coordinates (item, &x, &y);
+
+ if (event->button == 2)
+ {
+ gdk_seat_ungrab (seat);
+ SN_ITEM_GET_CLASS (item)->secondary_activate (item, x, y);
+ }
+ else if (event->button == 3)
+ {
+ if (priv->menu != NULL)
+ {
+ gtk_menu_popup_at_widget (priv->menu, widget,
+ GDK_GRAVITY_SOUTH_WEST,
+ GDK_GRAVITY_NORTH_WEST,
+ (GdkEvent *) event);
+ }
+ else
+ {
+ gdk_seat_ungrab (seat);
+ SN_ITEM_GET_CLASS (item)->context_menu (item, x, y);
+ }
+ }
+ else
+ {
+ g_assert_not_reached ();
+ }
+
+ return GTK_WIDGET_CLASS (sn_item_parent_class)->button_press_event (widget, event);
+}
+
+static gboolean
+sn_item_popup_menu (GtkWidget *widget)
+{
+ SnItem *item;
+ SnItemPrivate *priv;
+
+ item = SN_ITEM (widget);
+ priv = sn_item_get_instance_private (item);
+
+ if (priv->menu != NULL)
+ {
+ gtk_menu_popup_at_widget (priv->menu, widget,
+ GDK_GRAVITY_SOUTH_WEST,
+ GDK_GRAVITY_NORTH_WEST,
+ NULL);
+ }
+ else
+ {
+ gint x;
+ gint y;
+
+ sn_item_get_action_coordinates (item, &x, &y);
+
+ SN_ITEM_GET_CLASS (item)->context_menu (item, x, y);
+ }
+
+ return TRUE;
+}
+
+static void
+sn_item_clicked (GtkButton *button)
+{
+ SnItem *item;
+ gint x;
+ gint y;
+
+ item = SN_ITEM (button);
+
+ sn_item_get_action_coordinates (item, &x, &y);
+
+ SN_ITEM_GET_CLASS (item)->activate (item, x, y);
+}
+
+static gboolean
+sn_item_scroll_event (GtkWidget *widget,
+ GdkEventScroll *event)
+{
+ SnItem *item;
+ GdkScrollDirection direction;
+ SnItemOrientation orientation;
+ gdouble dx;
+ gdouble dy;
+ gint delta;
+
+ item = SN_ITEM (widget);
+
+ if (!gdk_event_get_scroll_direction ((GdkEvent *) event, &direction))
+ {
+ g_assert_not_reached ();
+ }
+ else
+ {
+ switch (direction)
+ {
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_DOWN:
+ orientation = SN_ITEM_ORIENTATION_VERTICAL;
+ break;
+
+ case GDK_SCROLL_LEFT:
+ case GDK_SCROLL_RIGHT:
+ orientation = SN_ITEM_ORIENTATION_HORIZONTAL;
+ break;
+
+ case GDK_SCROLL_SMOOTH:
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+ }
+
+ if (!gdk_event_get_scroll_deltas ((GdkEvent *) event, &dx, &dy))
+ {
+ switch (direction)
+ {
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_LEFT:
+ delta = 1;
+ break;
+
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_RIGHT:
+ delta = -1;
+ break;
+
+ case GDK_SCROLL_SMOOTH:
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+ }
+ else
+ {
+ if (dy != 0)
+ delta = (gint) dy;
+ else
+ delta = (gint) dx;
+ }
+
+ SN_ITEM_GET_CLASS (item)->scroll (item, delta, orientation);
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+sn_item_ready (SnItem *item)
+{
+ const gchar *menu;
+ SnItemPrivate *priv;
+
+ menu = SN_ITEM_GET_CLASS (item)->get_menu (item);
+ if (menu == NULL)
+ return;
+
+ if (menu == NULL || *menu == '\0' || g_strcmp0 (menu, "/") == 0)
+ return;
+
+ priv = sn_item_get_instance_private (item);
+ priv->menu = sn_dbus_menu_new (priv->bus_name, menu);
+ g_object_ref_sink (priv->menu);
+}
+
+static const gchar *
+sn_item_get_id (NaItem *item)
+{
+ return SN_ITEM_GET_CLASS (item)->get_id (SN_ITEM (item));
+}
+
+static NaItemCategory
+sn_item_get_category (NaItem *item)
+{
+ const gchar *string;
+ NaItemCategory category;
+
+ string = SN_ITEM_GET_CLASS (item)->get_category (SN_ITEM (item));
+
+ if (g_strcmp0 (string, "Hardware") == 0)
+ category = NA_ITEM_CATEGORY_HARDWARE;
+ else if (g_strcmp0 (string, "SystemServices") == 0)
+ category = NA_ITEM_CATEGORY_SYSTEM_SERVICES;
+ else if (g_strcmp0 (string, "Communications") == 0)
+ category = NA_ITEM_CATEGORY_COMMUNICATIONS;
+ else
+ category = NA_ITEM_CATEGORY_APPLICATION_STATUS;
+
+ return category;
+}
+
+static void
+na_item_init (NaItemInterface *iface)
+{
+ iface->get_id = sn_item_get_id;
+ iface->get_category = sn_item_get_category;
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+ g_object_class_install_property (object_class, PROP_BUS_NAME,
+ g_param_spec_string ("bus-name", "bus-name", "bus-name", NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (object_class, PROP_OBJECT_PATH,
+ g_param_spec_string ("object-path", "object-path", "object-path", NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_override_property (object_class, PROP_ORIENTATION,
+ "orientation");
+}
+
+static void
+install_signals (SnItemClass *item_class)
+{
+ signals[SIGNAL_READY] =
+ g_signal_new ("ready", G_TYPE_FROM_CLASS (item_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SnItemClass, ready), NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+static void
+sn_item_class_init (SnItemClass *item_class)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+ GtkButtonClass *button_class;
+
+ object_class = G_OBJECT_CLASS (item_class);
+ widget_class = GTK_WIDGET_CLASS (item_class);
+ button_class = GTK_BUTTON_CLASS (item_class);
+
+ object_class->dispose = sn_item_dispose;
+ object_class->finalize = sn_item_finalize;
+ object_class->get_property = sn_item_get_property;
+ object_class->set_property = sn_item_set_property;
+
+ widget_class->button_press_event = sn_item_button_press_event;
+ widget_class->popup_menu = sn_item_popup_menu;
+ widget_class->scroll_event = sn_item_scroll_event;
+
+ button_class->clicked = sn_item_clicked;
+
+ item_class->ready = sn_item_ready;
+
+ install_properties (object_class);
+ install_signals (item_class);
+
+ g_type_class_add_private (item_class, sizeof (SnItemPrivate));
+}
+
+static void
+sn_item_init (SnItem *item)
+{
+ gtk_widget_add_events (GTK_WIDGET (item), GDK_SCROLL_MASK);
+}
+
+const gchar *
+sn_item_get_bus_name (SnItem *item)
+{
+ SnItemPrivate *priv;
+
+ priv = sn_item_get_instance_private (item);
+
+ return priv->bus_name;
+}
+
+const gchar *
+sn_item_get_object_path (SnItem *item)
+{
+ SnItemPrivate *priv;
+
+ priv = sn_item_get_instance_private (item);
+
+ return priv->object_path;
+}
+
+void
+sn_item_emit_ready (SnItem *item)
+{
+ g_signal_emit (item, signals[SIGNAL_READY], 0);
+}
diff --git a/applets/notification_area/status-notifier/sn-item.h b/applets/notification_area/status-notifier/sn-item.h
new file mode 100644
index 00000000..c414b942
--- /dev/null
+++ b/applets/notification_area/status-notifier/sn-item.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 Alberts Muktupāvels
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SN_ITEM_H
+#define SN_ITEM_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_ITEM sn_item_get_type ()
+G_DECLARE_DERIVABLE_TYPE (SnItem, sn_item, SN, ITEM, GtkButton)
+
+typedef enum
+{
+ SN_ITEM_ORIENTATION_HORIZONTAL,
+ SN_ITEM_ORIENTATION_VERTICAL
+} SnItemOrientation;
+
+struct _SnItemClass
+{
+ GtkButtonClass parent_class;
+
+ void (* ready) (SnItem *item);
+
+ const gchar * (* get_id) (SnItem *item);
+
+ const gchar * (* get_category) (SnItem *item);
+
+ const gchar * (* get_menu) (SnItem *item);
+
+ void (* context_menu) (SnItem *item,
+ gint x,
+ gint y);
+
+ void (* activate) (SnItem *item,
+ gint x,
+ gint y);
+
+ void (* secondary_activate) (SnItem *item,
+ gint x,
+ gint y);
+
+ void (* scroll) (SnItem *item,
+ gint delta,
+ SnItemOrientation orientation);
+};
+
+const gchar *sn_item_get_bus_name (SnItem *item);
+const gchar *sn_item_get_object_path (SnItem *item);
+
+void sn_item_emit_ready (SnItem *item);
+
+G_END_DECLS
+
+#endif