aboutsummaryrefslogtreecommitdiff
path: root/src/indicator-ng.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/indicator-ng.c')
-rw-r--r--src/indicator-ng.c1031
1 files changed, 1031 insertions, 0 deletions
diff --git a/src/indicator-ng.c b/src/indicator-ng.c
new file mode 100644
index 0000000..f057600
--- /dev/null
+++ b/src/indicator-ng.c
@@ -0,0 +1,1031 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.
+ *
+ * Authors:
+ * Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+#include "indicator-ng.h"
+#include "indicator-image-helper.h"
+#include <libayatana-ido/ayatanamenuitemfactory.h>
+#include <string.h>
+
+#define MENU_SECTIONS 20
+
+struct _IndicatorNg
+{
+ IndicatorObject parent;
+
+ gchar *service_file;
+ gchar *name;
+ gchar *object_path;
+ gchar *menu_object_path;
+ gchar *bus_name;
+ gchar *profile;
+ gchar *header_action;
+ gchar *scroll_action;
+ gchar *secondary_action;
+ gchar *submenu_action;
+ gint position;
+
+ guint name_watch_id;
+
+ GDBusConnection *session_bus;
+ GActionGroup *actions;
+ GMenuModel *menu;
+
+ IndicatorObjectEntry entry;
+ gchar *accessible_desc;
+
+ gint64 last_service_restart;
+ GMenuModel *lMenuSections[MENU_SECTIONS];
+};
+
+static void indicator_ng_initable_iface_init (GInitableIface *initable);
+G_DEFINE_TYPE_WITH_CODE (IndicatorNg, indicator_ng, INDICATOR_OBJECT_TYPE,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, indicator_ng_initable_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_SERVICE_FILE,
+ PROP_PROFILE,
+ N_PROPERTIES
+};
+
+static GQuark m_pActionMuxer = 0;
+static GParamSpec *properties[N_PROPERTIES];
+
+static void
+indicator_ng_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IndicatorNg *self = INDICATOR_NG (object);
+
+ switch (property_id)
+ {
+ case PROP_SERVICE_FILE:
+ g_value_set_string (value, self->service_file);
+ break;
+
+ case PROP_PROFILE:
+ g_value_set_string (value, self->profile);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+indicator_ng_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IndicatorNg *self = INDICATOR_NG (object);
+
+ switch (property_id)
+ {
+ case PROP_SERVICE_FILE: /* construct-only */
+ self->service_file = g_strdup (g_value_get_string (value));
+ break;
+
+ case PROP_PROFILE: /* construct-only */
+ self->profile = g_strdup (g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+indicator_ng_free_actions_and_menu (IndicatorNg *self)
+{
+ if (self->actions)
+ {
+ gtk_widget_insert_action_group (GTK_WIDGET (self->entry.menu), "indicator", NULL);
+ g_signal_handlers_disconnect_by_data (self->actions, self);
+ g_clear_object (&self->actions);
+ }
+
+ if (self->menu)
+ {
+ for (guint nMenuSection = 0; nMenuSection < MENU_SECTIONS; nMenuSection++)
+ {
+ if (self->lMenuSections[nMenuSection])
+ {
+ g_object_unref(self->lMenuSections[nMenuSection]);
+ self->lMenuSections[nMenuSection] = NULL;
+ }
+ }
+
+ g_signal_handlers_disconnect_by_data (self->menu, self);
+ g_clear_object (&self->menu);
+ }
+}
+
+static void
+indicator_ng_dispose (GObject *object)
+{
+ IndicatorNg *self = INDICATOR_NG (object);
+
+ if (self->name_watch_id)
+ {
+ g_bus_unwatch_name (self->name_watch_id);
+ self->name_watch_id = 0;
+ }
+
+ g_clear_object (&self->session_bus);
+
+ indicator_ng_free_actions_and_menu (self);
+
+ g_clear_object (&self->entry.label);
+ g_clear_object (&self->entry.image);
+ g_clear_object (&self->entry.menu);
+
+ G_OBJECT_CLASS (indicator_ng_parent_class)->dispose (object);
+}
+
+static void
+indicator_ng_finalize (GObject *object)
+{
+ IndicatorNg *self = INDICATOR_NG (object);
+
+ g_free (self->service_file);
+ g_free (self->name);
+ g_free (self->object_path);
+ g_free (self->menu_object_path);
+ g_free (self->bus_name);
+ g_free (self->accessible_desc);
+ g_free (self->header_action);
+ g_free (self->scroll_action);
+ g_free (self->secondary_action);
+ g_free (self->submenu_action);
+
+ G_OBJECT_CLASS (indicator_ng_parent_class)->finalize (object);
+}
+
+static GList *
+indicator_ng_get_entries (IndicatorObject *io)
+{
+ IndicatorNg *self = INDICATOR_NG (io);
+
+ return g_list_append (NULL, &self->entry);
+}
+
+static gint
+indicator_ng_get_position (IndicatorObject *io)
+{
+ IndicatorNg *self = INDICATOR_NG (io);
+
+ return self->position;
+}
+
+static void
+indicator_ng_entry_scrolled (IndicatorObject *io,
+ __attribute__((unused)) IndicatorObjectEntry *entry,
+ gint delta,
+ IndicatorScrollDirection direction)
+{
+ IndicatorNg *self = INDICATOR_NG (io);
+
+ if (self->actions && self->scroll_action)
+ {
+ if (direction == INDICATOR_OBJECT_SCROLL_DOWN ||
+ direction == INDICATOR_OBJECT_SCROLL_LEFT)
+ {
+ delta *= -1;
+ }
+
+ g_action_group_activate_action (self->actions, self->scroll_action,
+ g_variant_new_int32 (delta));
+ }
+}
+
+void
+indicator_ng_secondary_activate (IndicatorObject *io,
+ __attribute__((unused)) IndicatorObjectEntry *entry,
+ __attribute__((unused)) guint timestamp,
+ __attribute__((unused)) gpointer user_data)
+{
+ IndicatorNg *self = INDICATOR_NG (io);
+
+ if (self->actions && self->secondary_action)
+ {
+ g_action_group_activate_action (self->actions, self->secondary_action, NULL);
+ }
+}
+
+static gboolean indicator_ng_menu_insert_idos(IndicatorNg *self, GMenuModel *pSection, guint nModelItem, guint nMenuItem, gboolean bNamespace, gchar *sNamespace)
+{
+ gboolean bChanged = FALSE;
+ gchar *sType;
+ gboolean bHasType = g_menu_model_get_item_attribute(pSection, nModelItem, "x-ayatana-type", "s", &sType);
+
+ if (bHasType)
+ {
+ GList *lMenuItems = gtk_container_get_children(GTK_CONTAINER(self->entry.menu));
+ GtkWidget *pMenuItemOld = GTK_WIDGET(g_list_nth_data(lMenuItems, nMenuItem));
+ const gchar *sName = gtk_widget_get_name(pMenuItemOld);
+
+ if (!g_str_equal(sName, sType))
+ {
+ GActionGroup *pActionGroup = (GActionGroup*)g_object_get_qdata(G_OBJECT(self->entry.menu), m_pActionMuxer);
+ GMenuItem *pMenuModelItem = g_menu_item_new_from_model(pSection, nModelItem);
+ GtkMenuItem* pMenuItemNew = NULL;
+ gchar *sAction;
+
+ if (bNamespace && g_menu_item_get_attribute(pMenuModelItem, G_MENU_ATTRIBUTE_ACTION, "s", &sAction))
+ {
+ gchar *sNamespacedAction = g_strconcat(sNamespace, ".", sAction, NULL);
+ g_menu_item_set_attribute(pMenuModelItem, G_MENU_ATTRIBUTE_ACTION, "s", sNamespacedAction);
+ g_free (sNamespacedAction);
+ g_free (sAction);
+ }
+
+ for (GList *pFactory = ayatana_menu_item_factory_get_all(); pFactory != NULL && pMenuItemNew == NULL; pFactory = pFactory->next)
+ {
+ pMenuItemNew = ayatana_menu_item_factory_create_menu_item(pFactory->data, sType, pMenuModelItem, pActionGroup);
+ bChanged = TRUE;
+ }
+
+ if (pMenuItemNew == NULL)
+ {
+ pMenuItemNew = GTK_MENU_ITEM(gtk_menu_item_new_with_label("Failed to create IDO object"));
+ }
+
+ gtk_widget_set_name(GTK_WIDGET(pMenuItemNew), sType);
+ gtk_widget_show(GTK_WIDGET(pMenuItemNew));
+ gtk_container_remove(GTK_CONTAINER(self->entry.menu), pMenuItemOld);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(self->entry.menu), GTK_WIDGET(pMenuItemNew), nMenuItem);
+ g_object_unref(pMenuModelItem);
+ }
+
+ g_list_free(lMenuItems);
+ g_free(sType);
+ }
+
+ return bChanged;
+}
+
+static void indicator_ng_menu_size_allocate(__attribute__((unused)) GtkWidget *pWidget, __attribute__((unused)) GtkAllocation *pAllocation, gpointer pUserData)
+{
+ IndicatorNg *self = pUserData;
+ GList *pMenuItem = gtk_container_get_children(GTK_CONTAINER(self->entry.menu));
+ guint nWidth = 0;
+ guint nHeight = 0;
+ GdkWindow *pWindowBin = NULL;
+
+ while (pMenuItem)
+ {
+ if (!pWindowBin)
+ {
+ pWindowBin = gtk_widget_get_parent_window(pMenuItem->data);
+ }
+
+ gint nWidthNat;
+ gint nHeightNat;
+ gtk_widget_get_preferred_width(pMenuItem->data, NULL, &nWidthNat);
+ gtk_widget_get_preferred_height(pMenuItem->data, NULL, &nHeightNat);
+ nWidth = MAX((gint)nWidth, nWidthNat);
+ nHeight += nHeightNat;
+ GtkBorder cPadding;
+ GtkStyleContext *pContext = gtk_widget_get_style_context(GTK_WIDGET(pMenuItem->data));
+ gtk_style_context_get_padding(pContext, gtk_style_context_get_state(pContext), &cPadding);
+ nWidth += cPadding.left + cPadding.right;
+ pMenuItem = g_list_next(pMenuItem);
+ }
+
+ g_list_free(pMenuItem);
+ GtkBorder cPadding;
+ GtkStyleContext *pContext = gtk_widget_get_style_context(GTK_WIDGET(self->entry.menu));
+ gtk_style_context_get_padding(pContext, gtk_style_context_get_state(pContext), &cPadding);
+ gint nBorderWidth = gtk_container_get_border_width(GTK_CONTAINER(self->entry.menu));
+ gint nIconWidth;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &nIconWidth, NULL);
+ nWidth += (2 * nBorderWidth) + cPadding.left + cPadding.right + (nIconWidth * 3) / 2;
+ nHeight += (2 * nBorderWidth) + cPadding.top + cPadding.bottom + (nIconWidth * 3) / 4;
+
+ GdkRectangle cRectangle = {0};
+ GdkDisplay *pDisplay = gdk_display_get_default();
+ GdkMonitor *pMonitor = gdk_display_get_primary_monitor(pDisplay);
+ gdk_monitor_get_workarea(pMonitor, &cRectangle);
+
+ if ((gint)nHeight <= cRectangle.height)
+ {
+ gdk_window_move_resize(pWindowBin, 0, 0, nWidth, nHeight);
+ }
+
+ nHeight = MIN((gint)nHeight, cRectangle.height);
+
+ GdkWindow *pWindow = gtk_widget_get_parent_window(GTK_WIDGET(self->entry.menu));
+ gdk_window_resize(pWindow, nWidth, nHeight);
+
+ gtk_menu_reposition(self->entry.menu);
+}
+
+static void indicator_ng_menu_section_changed(__attribute__((unused)) GMenuModel *pMenuSection, __attribute__((unused)) gint nPosition, __attribute__((unused)) gint nRemoved, __attribute__((unused)) gint nAdded, gpointer pUserData)
+{
+ IndicatorNg *self = pUserData;
+ GMenuModel *pModel = g_menu_model_get_item_link(self->menu, 0, G_MENU_LINK_SUBMENU);
+ guint nMenuItem = 0;
+ gboolean bChanged = FALSE;
+
+ if (pModel)
+ {
+ guint nSections = g_menu_model_get_n_items(pModel);
+
+ for (guint nSection = 0; nSection < nSections; nSection++)
+ {
+ GMenuModel *pSection = g_menu_model_get_item_link(pModel, nSection, G_MENU_LINK_SECTION);
+ guint nSubsections = 0;
+
+ if (pSection)
+ {
+ gchar *sNamespace;
+ gboolean bNamespace = g_menu_model_get_item_attribute(pModel, nSection, G_MENU_ATTRIBUTE_ACTION_NAMESPACE, "s", &sNamespace);
+ nSubsections = g_menu_model_get_n_items(pSection);
+
+ for (guint nSubsection = 0; nSubsection < nSubsections; nSubsection++)
+ {
+ GMenuModel *pSubsection = g_menu_model_get_item_link(pSection, nSubsection, G_MENU_LINK_SECTION);
+
+ if (pSubsection)
+ {
+ guint nItems = g_menu_model_get_n_items(pSubsection);
+
+ // Skip the subsection separator (if there is one)
+ GList *lMenuItems = gtk_container_get_children(GTK_CONTAINER(self->entry.menu));
+ GtkWidget *pMenuItem = GTK_WIDGET(g_list_nth_data(lMenuItems, nMenuItem));
+
+ if (GTK_IS_SEPARATOR_MENU_ITEM(pMenuItem))
+ {
+ nMenuItem++;
+ }
+
+ g_list_free(lMenuItems);
+
+ for (guint nItem = 0; nItem < nItems; nItem++)
+ {
+ bChanged = indicator_ng_menu_insert_idos(self, pSubsection, nItem, nMenuItem, bNamespace, sNamespace) || bChanged;
+ nMenuItem++;
+ }
+ }
+
+ g_object_unref(pSubsection);
+
+ bChanged = indicator_ng_menu_insert_idos(self, pSection, nSubsection, nMenuItem, bNamespace, sNamespace) || bChanged;
+
+ if (!g_str_equal(self->name, "ayatana-indicator-messages"))
+ {
+ nMenuItem++;
+ }
+ }
+
+ if (bNamespace)
+ {
+ g_free(sNamespace);
+ }
+
+ g_object_unref(pSection);
+ }
+
+ if (pSection && nSubsections)
+ {
+ nMenuItem++;
+ }
+ }
+
+ g_object_unref(pModel);
+ }
+
+ if (bChanged)
+ {
+ indicator_ng_menu_size_allocate(NULL, NULL, self);
+ }
+}
+
+static void indicator_ng_menu_shown(__attribute__((unused)) GtkWidget *pWidget, gpointer pUserData)
+{
+ IndicatorNg *self = pUserData;
+ guint nSectionCount = 0;
+
+ if (!self->lMenuSections[0])
+ {
+ self->lMenuSections[0] = g_menu_model_get_item_link(self->menu, 0, G_MENU_LINK_SUBMENU);
+
+ if (self->lMenuSections[0])
+ {
+ guint nSections = g_menu_model_get_n_items(self->lMenuSections[0]);
+
+ for (guint nSection = 0; nSection < nSections; nSection++)
+ {
+ self->lMenuSections[++nSectionCount] = g_menu_model_get_item_link(self->lMenuSections[0], nSection, G_MENU_LINK_SECTION);
+
+ if (self->lMenuSections[nSectionCount])
+ {
+ g_signal_connect(self->lMenuSections[nSectionCount], "items-changed", G_CALLBACK(indicator_ng_menu_section_changed), self);
+ guint nSubsections = g_menu_model_get_n_items(self->lMenuSections[nSectionCount]);
+ guint nParent = nSectionCount;
+
+ for (guint nSubsection = 0; nSubsection < nSubsections; nSubsection++)
+ {
+ self->lMenuSections[++nSectionCount] = g_menu_model_get_item_link(self->lMenuSections[nParent], nSubsection, G_MENU_LINK_SECTION);
+
+ if (self->lMenuSections[nSectionCount])
+ {
+ g_signal_connect(self->lMenuSections[nSectionCount], "items-changed", G_CALLBACK(indicator_ng_menu_section_changed), self);
+ }
+ }
+ }
+ }
+
+ g_signal_connect(self->lMenuSections[0], "items-changed", G_CALLBACK(indicator_ng_menu_section_changed), self);
+ indicator_ng_menu_section_changed(self->lMenuSections[0], 0, 0, 1, self);
+ }
+ }
+
+ if (self->submenu_action)
+ {
+ g_action_group_change_action_state(self->actions, self->submenu_action, g_variant_new_boolean(TRUE));
+ }
+}
+
+static void
+indicator_ng_menu_hidden (__attribute__((unused)) GtkWidget *widget,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+
+ if (self->submenu_action)
+ g_action_group_change_action_state (self->actions, self->submenu_action,
+ g_variant_new_boolean (FALSE));
+}
+
+static void
+indicator_ng_set_accessible_desc (IndicatorNg *self,
+ const gchar *accessible_desc)
+{
+ g_free (self->accessible_desc);
+ self->accessible_desc = g_strdup (accessible_desc);
+
+ self->entry.accessible_desc = self->accessible_desc;
+ g_signal_emit_by_name (self, INDICATOR_OBJECT_SIGNAL_ACCESSIBLE_DESC_UPDATE, &self->entry);
+}
+
+static void
+indicator_ng_set_icon_from_variant (IndicatorNg *self,
+ GVariant *variant)
+{
+ GIcon *icon;
+
+ if (variant == NULL)
+ {
+ if (self->entry.image)
+ {
+ gtk_image_clear (self->entry.image);
+ gtk_widget_hide (GTK_WIDGET (self->entry.image));
+ }
+ return;
+ }
+
+ gtk_widget_show (GTK_WIDGET (self->entry.image));
+
+ icon = g_icon_deserialize (variant);
+ if (icon)
+ {
+ indicator_image_helper_update_from_gicon (self->entry.image, icon);
+ g_object_unref (icon);
+ }
+ else
+ {
+ gchar *text = g_variant_print (variant, TRUE);
+ g_warning ("invalid icon variant '%s'", text);
+ gtk_image_set_from_icon_name (self->entry.image, "image-missing", GTK_ICON_SIZE_LARGE_TOOLBAR);
+ g_free (text);
+ }
+}
+
+static void indicator_ng_set_label(IndicatorNg *self, const gchar *label)
+{
+ if (!self->entry.label)
+ {
+ return;
+ }
+
+ const gchar *sLabel = label;
+ guint nSpacing = 3;
+ guint nPadding = 6;
+
+ if (label == NULL || *label == '\0' || !self->entry.image || !gtk_widget_get_visible(GTK_WIDGET(self->entry.image)))
+ {
+ nSpacing = 0;
+ nPadding = 0;
+ }
+
+ GtkWidget *pParent = gtk_widget_get_parent(GTK_WIDGET(self->entry.label));
+ GtkCssProvider *pCssProvider = gtk_css_provider_new();
+ GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(self->entry.label));
+ gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(pCssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ gchar *sCss = g_strdup_printf("label{padding-left: %ipx;}", nPadding);
+ gtk_css_provider_load_from_data(pCssProvider, sCss, -1, NULL);
+ g_free(sCss);
+ g_object_unref(pCssProvider);
+ if (GTK_IS_BOX(pParent))
+ {
+ gtk_box_set_spacing(GTK_BOX(pParent), nSpacing);
+ }
+ gtk_label_set_label(GTK_LABEL (self->entry.label), sLabel);
+
+ if (label)
+ {
+ gtk_widget_show(GTK_WIDGET (self->entry.label));
+ }
+}
+
+static void
+indicator_ng_update_entry (IndicatorNg *self)
+{
+ GVariant *state;
+ const gchar *label = NULL;
+ GVariant *icon = NULL;
+ const gchar *accessible_desc = NULL;
+ gboolean visible = TRUE;
+
+ g_return_if_fail (self->menu != NULL);
+ g_return_if_fail (self->actions != NULL);
+
+ if (!self->header_action ||
+ !g_action_group_has_action (self->actions, self->header_action))
+ {
+ indicator_object_set_visible (INDICATOR_OBJECT (self), FALSE);
+ return;
+ }
+
+ state = g_action_group_get_action_state (self->actions, self->header_action);
+ if (state && g_variant_is_of_type (state, G_VARIANT_TYPE ("(sssb)")))
+ {
+ const gchar *iconstr = NULL;
+
+ g_variant_get (state, "(&s&s&sb)", &label, &iconstr, &accessible_desc, &visible);
+
+ if (iconstr)
+ icon = g_variant_ref_sink (g_variant_new_string (iconstr));
+ }
+ else if (state && g_variant_is_of_type (state, G_VARIANT_TYPE ("a{sv}")))
+ {
+ g_variant_lookup (state, "label", "&s", &label);
+ g_variant_lookup (state, "icon", "*", &icon);
+ g_variant_lookup (state, "accessible-desc", "&s", &accessible_desc);
+ g_variant_lookup (state, "visible", "b", &visible);
+ }
+ else
+ g_warning ("the action of the indicator menu item must have state with type (sssb) or a{sv}");
+
+ indicator_ng_set_label (self, label);
+ indicator_ng_set_icon_from_variant (self, icon);
+ indicator_ng_set_accessible_desc (self, accessible_desc);
+ indicator_object_set_visible (INDICATOR_OBJECT (self), visible);
+
+ if (icon)
+ g_variant_unref (icon);
+ if (state)
+ g_variant_unref (state);
+}
+
+static gboolean
+indicator_ng_menu_item_is_of_type (GMenuModel *menu,
+ gint index,
+ const gchar *expected_type)
+{
+ gchar *type;
+ gboolean has_type = FALSE;
+
+ if (g_menu_model_get_item_attribute (menu, index, "x-ayatana-type", "s", &type))
+ {
+ has_type = g_str_equal (type, expected_type);
+ g_free (type);
+ }
+
+ return has_type;
+}
+
+static void
+indicator_ng_menu_changed (__attribute__((unused)) GMenuModel *menu,
+ gint position,
+ gint removed,
+ gint added,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+
+ /* The menu may only contain one item (the indicator title menu).
+ * Thus, the position is always 0, and there is either exactly one
+ * item added or exactly one item removed.
+ */
+ g_return_if_fail (position == 0);
+ g_return_if_fail (added < 2 && removed < 2 && added ^ removed);
+
+ if (removed)
+ indicator_object_set_visible (INDICATOR_OBJECT (self), FALSE);
+
+ if (added)
+ {
+ g_clear_pointer (&self->header_action, g_free);
+ g_clear_pointer (&self->scroll_action, g_free);
+ g_clear_pointer (&self->secondary_action, g_free);
+
+ if (indicator_ng_menu_item_is_of_type (self->menu, 0, "org.ayatana.indicator.root"))
+ {
+ GMenuModel *popup;
+ gchar *action;
+
+ if (g_menu_model_get_item_attribute (self->menu, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action))
+ {
+ if (g_str_has_prefix (action, "indicator."))
+ self->header_action = g_strdup (action + strlen ("indicator."));
+ g_free (action);
+ }
+
+ if (g_menu_model_get_item_attribute (self->menu, 0, "x-ayatana-scroll-action", "s", &action))
+ {
+ if (g_str_has_prefix (action, "indicator."))
+ self->scroll_action = g_strdup (action + strlen ("indicator."));
+ g_free (action);
+ }
+
+ if (g_menu_model_get_item_attribute (self->menu, 0, "x-ayatana-secondary-action", "s", &action))
+ {
+ if (g_str_has_prefix (action, "indicator."))
+ self->secondary_action = g_strdup (action + strlen ("indicator."));
+ g_free (action);
+ }
+
+ if (g_menu_model_get_item_attribute (self->menu, 0, "submenu-action", "s", &action))
+ {
+ if (g_str_has_prefix (action, "indicator."))
+ self->submenu_action = g_strdup (action + strlen ("indicator."));
+ g_free (action);
+ }
+
+ for (guint nMenuSection = 0; nMenuSection < MENU_SECTIONS; nMenuSection++)
+ {
+ if (self->lMenuSections[nMenuSection])
+ {
+ g_object_unref(self->lMenuSections[nMenuSection]);
+ }
+ }
+
+ popup = g_menu_model_get_item_link (self->menu, 0, G_MENU_LINK_SUBMENU);
+ if (popup)
+ {
+ gtk_menu_shell_bind_model (GTK_MENU_SHELL (self->entry.menu), popup, NULL, TRUE);
+ g_object_unref (popup);
+ }
+
+ indicator_ng_update_entry (self);
+ }
+ else
+ g_warning ("indicator menu item must be of type 'org.ayatana.indicator.root'");
+ }
+}
+
+static void
+indicator_ng_service_appeared (GDBusConnection *connection,
+ __attribute__((unused)) const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+
+ g_assert (!self->actions);
+ g_assert (!self->menu);
+
+ /* watch is not established when menu_object_path == NULL */
+ g_assert (self->menu_object_path);
+
+ self->session_bus = g_object_ref (connection);
+
+ self->actions = G_ACTION_GROUP (g_dbus_action_group_get (connection, name_owner, self->object_path));
+ gtk_widget_insert_action_group (GTK_WIDGET (self->entry.menu), "indicator", self->actions);
+ g_signal_connect_swapped (self->actions, "action-added", G_CALLBACK (indicator_ng_update_entry), self);
+ g_signal_connect_swapped (self->actions, "action-removed", G_CALLBACK (indicator_ng_update_entry), self);
+ g_signal_connect_swapped (self->actions, "action-state-changed", G_CALLBACK (indicator_ng_update_entry), self);
+
+ self->menu = G_MENU_MODEL (g_dbus_menu_model_get (connection, name_owner, self->menu_object_path));
+ g_signal_connect (self->menu, "items-changed", G_CALLBACK (indicator_ng_menu_changed), self);
+ if (g_menu_model_get_n_items (self->menu))
+ indicator_ng_menu_changed (self->menu, 0, 0, 1, self);
+
+ indicator_ng_update_entry (self);
+}
+
+static void
+indicator_ng_service_started (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+ GError *error = NULL;
+ GVariant *result;
+ guint32 start_service_reply;
+
+ result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);
+ if (!result)
+ {
+ g_warning ("Could not activate service '%s': %s", self->name, error->message);
+ indicator_object_set_visible (INDICATOR_OBJECT (self), FALSE);
+ g_error_free (error);
+ return;
+ }
+
+ start_service_reply = 0;
+ g_variant_get (result, "(u)", &start_service_reply);
+
+ switch (start_service_reply)
+ {
+ case 1: /* DBUS_START_REPLY_SUCCESS */
+ break;
+
+ case 2: /* DBUS_START_REPLY_ALREADY_RUNNING */
+ g_warning ("could not start service '%s': it is already running", self->name);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ g_variant_unref (result);
+}
+
+static void
+indicator_ng_service_vanished (__attribute__((unused)) GDBusConnection *connection,
+ __attribute__((unused)) const gchar *name,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+
+ indicator_ng_free_actions_and_menu (self);
+
+ /* Names may vanish because the service decided it doesn't need to
+ * show its indicator anymore, or because it crashed. Let's assume it
+ * crashes and restart it unless it explicitly hid its indicator. */
+
+ if (indicator_object_entry_is_visible (INDICATOR_OBJECT (self), &self->entry))
+ {
+ gint64 now;
+
+ /* take care not to start it if it repeatedly crashes */
+ now = g_get_monotonic_time ();
+ if (now - self->last_service_restart < 1 * G_USEC_PER_SEC)
+ {
+ g_warning ("The indicator '%s' vanished too quickly after appearing. It won't "
+ "be respawned anymore, as it could be crashing repeatedly.", self->name);
+ return;
+ }
+
+ self->last_service_restart = now;
+
+ g_dbus_connection_call (self->session_bus,
+ "org.freedesktop.DBus",
+ "/",
+ "org.freedesktop.DBus",
+ "StartServiceByName",
+ g_variant_new ("(su)", self->bus_name, 0),
+ G_VARIANT_TYPE ("(u)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ indicator_ng_service_started,
+ self);
+ }
+}
+
+/* Get an integer from a keyfile. Returns @default_value if the key
+ * doesn't exist exists or is not an integer */
+static gint
+g_key_file_maybe_get_integer (GKeyFile *keyfile,
+ const gchar *group,
+ const gchar *key,
+ gint default_value)
+{
+ GError *error = NULL;
+ gint i;
+
+ i = g_key_file_get_integer (keyfile, group, key, &error);
+ if (error)
+ {
+ g_error_free (error);
+ return default_value;
+ }
+
+ return i;
+}
+
+static gboolean
+indicator_ng_load_from_keyfile (IndicatorNg *self,
+ GKeyFile *keyfile,
+ GError **error)
+{
+ g_assert (self->name == NULL);
+ g_assert (self->object_path == NULL);
+ g_assert (self->menu_object_path == NULL);
+
+ self->name = g_key_file_get_string (keyfile, "Indicator Service", "Name", error);
+ if (self->name == NULL)
+ return FALSE;
+
+ self->object_path = g_key_file_get_string (keyfile, "Indicator Service", "ObjectPath", error);
+ if (self->object_path == NULL)
+ return FALSE;
+
+ self->position = g_key_file_maybe_get_integer (keyfile, "Indicator Service", "Position", -1);
+
+ /*
+ * Don't throw an error when the profile doesn't exist. Non-existant
+ * profiles are silently ignored by not showing an indicator at all.
+ */
+ if (g_key_file_has_group (keyfile, self->profile))
+ {
+ /* however, if the profile exists, it must have "ObjectPath" */
+ self->menu_object_path = g_key_file_get_string (keyfile, self->profile, "ObjectPath", error);
+ if (self->menu_object_path == NULL)
+ return FALSE;
+
+ /* a position in the profile overrides the global one */
+ self->position = g_key_file_maybe_get_integer (keyfile, self->profile, "Position", self->position);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+indicator_ng_initable_init (GInitable *initable,
+ __attribute__((unused)) GCancellable *cancellable,
+ GError **error)
+{
+ IndicatorNg *self = INDICATOR_NG (initable);
+ GKeyFile *keyfile;
+ gboolean success = FALSE;
+
+ self->bus_name = g_path_get_basename (self->service_file);
+
+ keyfile = g_key_file_new ();
+ if (g_key_file_load_from_file (keyfile, self->service_file, G_KEY_FILE_NONE, error) &&
+ indicator_ng_load_from_keyfile (self, keyfile, error))
+ {
+ self->entry.name_hint = self->name;
+
+ /* only watch the service when it supports the proile we're interested in */
+ if (self->menu_object_path)
+ {
+ self->name_watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
+ self->bus_name,
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ indicator_ng_service_appeared,
+ indicator_ng_service_vanished,
+ self, NULL);
+ }
+
+ success = TRUE;
+ }
+
+ g_key_file_free (keyfile);
+ return success;
+}
+
+static void
+indicator_ng_class_init (IndicatorNgClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+ IndicatorObjectClass *io_class = INDICATOR_OBJECT_CLASS (class);
+
+ object_class->get_property = indicator_ng_get_property;
+ object_class->set_property = indicator_ng_set_property;
+ object_class->dispose = indicator_ng_dispose;
+ object_class->finalize = indicator_ng_finalize;
+
+ io_class->get_entries = indicator_ng_get_entries;
+ io_class->get_position = indicator_ng_get_position;
+ io_class->entry_scrolled = indicator_ng_entry_scrolled;
+ io_class->secondary_activate = indicator_ng_secondary_activate;
+
+ properties[PROP_SERVICE_FILE] = g_param_spec_string ("service-file",
+ "Service file",
+ "Path of the service file",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_PROFILE] = g_param_spec_string ("profile",
+ "Profile",
+ "Indicator profile",
+ "desktop",
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties(object_class, N_PROPERTIES, properties);
+}
+
+static void
+indicator_ng_initable_iface_init (GInitableIface *initable)
+{
+ initable->init = indicator_ng_initable_init;
+}
+
+static void
+indicator_ng_init (IndicatorNg *self)
+{
+ m_pActionMuxer = g_quark_from_static_string ("gtk-widget-action-muxer");
+
+ for (guint nMenuSection = 0; nMenuSection < MENU_SECTIONS; nMenuSection++)
+ {
+ self->lMenuSections[nMenuSection] = NULL;
+ }
+
+ self->entry.label = (GtkLabel*)g_object_ref_sink (gtk_label_new (NULL));
+ self->entry.image = (GtkImage*)g_object_ref_sink (gtk_image_new ());
+
+ self->entry.menu = (GtkMenu*)g_object_ref_sink (gtk_menu_new ());
+
+ g_signal_connect (self->entry.menu, "show", G_CALLBACK (indicator_ng_menu_shown), self);
+ g_signal_connect (self->entry.menu, "hide", G_CALLBACK (indicator_ng_menu_hidden), self);
+ g_signal_connect (self->entry.menu, "size-allocate", G_CALLBACK (indicator_ng_menu_size_allocate), self);
+
+ GtkCssProvider *pCssProvider = gtk_css_provider_new();
+ GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(self->entry.menu));
+ gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(pCssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ gtk_css_provider_load_from_data(pCssProvider, "menu > arrow{min-height: 0; padding: 0; margin: 0;}", -1, NULL);
+
+ GtkWidget *pWindow = gtk_widget_get_parent(GTK_WIDGET(self->entry.menu));
+ pStyleContext = gtk_widget_get_style_context(pWindow);
+ gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(pCssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ gtk_css_provider_load_from_data(pCssProvider, "window > decoration {box-shadow: 0 1px 2px rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.13);}", -1, NULL);
+
+ g_object_unref(pCssProvider);
+
+ /* work around IndicatorObject's warning that the accessible
+ * description is missing. We never set it on construction, but when
+ * the menu model has arrived on the bus.
+ */
+ self->accessible_desc = g_strdup ("");
+ self->entry.accessible_desc = self->accessible_desc;
+
+ self->position = -1;
+
+ indicator_object_set_visible (INDICATOR_OBJECT (self), FALSE);
+}
+
+IndicatorNg *
+indicator_ng_new (const gchar *service_file,
+ GError **error)
+{
+ return g_initable_new (INDICATOR_TYPE_NG, NULL, error,
+ "service-file", service_file,
+ NULL);
+}
+
+IndicatorNg *
+indicator_ng_new_for_profile (const gchar *service_file,
+ const gchar *profile,
+ GError **error)
+{
+ return g_initable_new (INDICATOR_TYPE_NG, NULL, error,
+ "service-file", service_file,
+ "profile", profile,
+ NULL);
+}
+
+const gchar *
+indicator_ng_get_service_file (IndicatorNg *self)
+{
+ g_return_val_if_fail (INDICATOR_IS_NG (self), NULL);
+
+ return self->service_file;
+}
+
+const gchar *
+indicator_ng_get_profile (IndicatorNg *self)
+{
+ g_return_val_if_fail (INDICATOR_IS_NG (self), NULL);
+
+ return self->profile;
+}