aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am.coverage4
-rw-r--r--configure.ac2
-rw-r--r--libindicator/Makefile.am8
-rw-r--r--libindicator/indicator-image-helper.c24
-rw-r--r--libindicator/indicator-image-helper.h8
-rw-r--r--libindicator/indicator-ng.c566
-rw-r--r--libindicator/indicator-ng.h48
-rw-r--r--libindicator/indicator-object.c7
-rw-r--r--libindicator/indicator-object.h1
-rw-r--r--tests/Makefile.am43
-rw-r--r--tests/com.canonical.indicator.test.service.in3
-rw-r--r--tests/com.canonical.test.indicator4
-rw-r--r--tests/com.canonical.test.nosuchservice.indicator4
-rw-r--r--tests/indicator-test-service.c108
-rw-r--r--tests/test-indicator-ng.c167
-rw-r--r--tools/indicator-loader.c27
-rwxr-xr-xtrim-lcov.py69
17 files changed, 1078 insertions, 15 deletions
diff --git a/Makefile.am.coverage b/Makefile.am.coverage
index fb97747..dc3b9c8 100644
--- a/Makefile.am.coverage
+++ b/Makefile.am.coverage
@@ -1,6 +1,8 @@
# Coverage targets
+EXTRA_DIST = trim-lcov.py
+
.PHONY: clean-gcno clean-gcda \
coverage-html generate-coverage-html clean-coverage-html \
coverage-gcovr generate-coverage-gcovr clean-coverage-gcovr
@@ -23,7 +25,7 @@ coverage-html: clean-gcda
generate-coverage-html:
@echo Collecting coverage data
- $(LCOV) --directory $(top_builddir) --capture --output-file coverage.info --no-checksum --compat-libtool
+ $(LCOV) --directory $(top_builddir) --capture --no-checksum --compat-libtool | $(top_srcdir)/trim-lcov.py > coverage.info
LANG=C $(GENHTML) --prefix $(top_builddir) --output-directory coveragereport --title "Code Coverage" --legend --show-details coverage.info
clean-coverage-html: clean-gcda
diff --git a/configure.ac b/configure.ac
index 6c34ad0..48f759f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -43,7 +43,7 @@ AS_IF([test "x$enable_deprecations" = xno],
##############################
GTK_REQUIRED_VERSION=2.18
-GTK3_REQUIRED_VERSION=2.91
+GTK3_REQUIRED_VERSION=3.6
GIO_UNIX_REQUIRED_VERSION=2.22
AC_ARG_WITH([gtk],
diff --git a/libindicator/Makefile.am b/libindicator/Makefile.am
index ef6c6c7..7bd37ef 100644
--- a/libindicator/Makefile.am
+++ b/libindicator/Makefile.am
@@ -36,6 +36,10 @@ indicator_headers = \
indicator-service.h \
indicator-service-manager.h
+if USE_GTK3
+indicator_headers += indicator-ng.h
+endif
+
libindicatorinclude_HEADERS = \
$(indicator_headers)
@@ -53,6 +57,10 @@ libindicator_la_SOURCES = \
indicator-service.c \
indicator-service-manager.c
+if USE_GTK3
+libindicator_la_SOURCES += indicator-ng.c
+endif
+
libindicator_la_CFLAGS = \
$(LIBINDICATOR_CFLAGS) \
$(COVERAGE_CFLAGS) \
diff --git a/libindicator/indicator-image-helper.c b/libindicator/indicator-image-helper.c
index 63488a1..382d0c9 100644
--- a/libindicator/indicator-image-helper.c
+++ b/libindicator/indicator-image-helper.c
@@ -62,7 +62,12 @@ refresh_image (GtkImage * image)
/* Grab the filename */
icon_filename = gtk_icon_info_get_filename(icon_info);
}
- g_return_if_fail(icon_filename != NULL); /* An error because we don't have a filename */
+
+ /* show a broken image if we don't have a filename */
+ if (icon_filename == NULL) {
+ gtk_image_set_from_stock (image, GTK_STOCK_MISSING_IMAGE, GTK_ICON_SIZE_LARGE_TOOLBAR);
+ return;
+ }
/* Build a pixbuf */
GError * error = NULL;
@@ -132,7 +137,8 @@ indicator_image_helper (const gchar * name)
/* Build us an image */
GtkImage * image = GTK_IMAGE(gtk_image_new());
- indicator_image_helper_update(image, name);
+ if (name)
+ indicator_image_helper_update(image, name);
return image;
}
@@ -144,17 +150,27 @@ indicator_image_helper_update (GtkImage * image, const gchar * name)
g_return_if_fail(name != NULL);
g_return_if_fail(name[0] != '\0');
g_return_if_fail(GTK_IS_IMAGE(image));
- gboolean seen_previously = FALSE;
/* Build us a GIcon */
GIcon * icon_names = g_themed_icon_new_with_default_fallbacks(name);
g_warn_if_fail(icon_names != NULL);
g_return_if_fail(icon_names != NULL);
+ indicator_image_helper_update_from_gicon (image, icon_names);
+
+ g_object_unref (icon_names);
+ return;
+}
+
+void
+indicator_image_helper_update_from_gicon (GtkImage *image, GIcon *icon)
+{
+ gboolean seen_previously = FALSE;
+
seen_previously = (g_object_get_data(G_OBJECT(image), INDICATOR_NAMES_DATA) != NULL);
/* Attach our names to the image */
- g_object_set_data_full(G_OBJECT(image), INDICATOR_NAMES_DATA, icon_names, g_object_unref);
+ g_object_set_data_full(G_OBJECT(image), INDICATOR_NAMES_DATA, g_object_ref (icon), g_object_unref);
/* Put the pixbuf in */
refresh_image(image);
diff --git a/libindicator/indicator-image-helper.h b/libindicator/indicator-image-helper.h
index 77e2f0a..290f4e2 100644
--- a/libindicator/indicator-image-helper.h
+++ b/libindicator/indicator-image-helper.h
@@ -26,8 +26,10 @@ License along with this library. If not, see
#include <gtk/gtk.h>
-GtkImage * indicator_image_helper (const gchar * name);
-void indicator_image_helper_update (GtkImage * image,
- const gchar * name);
+GtkImage * indicator_image_helper (const gchar * name);
+void indicator_image_helper_update (GtkImage * image,
+ const gchar * name);
+void indicator_image_helper_update_from_gicon (GtkImage * image,
+ GIcon * icon);
#endif /* __INDICATOR_IMAGE_HELPER_H__ */
diff --git a/libindicator/indicator-ng.c b/libindicator/indicator-ng.c
new file mode 100644
index 0000000..f2b957b
--- /dev/null
+++ b/libindicator/indicator-ng.c
@@ -0,0 +1,566 @@
+/*
+ * 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 <string.h>
+
+struct _IndicatorNg
+{
+ IndicatorObject parent;
+
+ gchar *service_file;
+ gchar *name;
+ gchar *object_path;
+ gchar *bus_name;
+ gchar *profile;
+ gchar *header_action;
+
+ guint name_watch_id;
+
+ GDBusConnection *session_bus;
+ GActionGroup *actions;
+ GMenuModel *menu;
+
+ IndicatorObjectEntry entry;
+ gchar *accessible_desc;
+
+ gint64 last_service_restart;
+};
+
+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 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)
+ {
+ 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->bus_name);
+ g_free (self->accessible_desc);
+ g_free (self->header_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 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_string (IndicatorNg *self,
+ const gchar *str)
+{
+ GIcon *icon;
+ GError *error = NULL;
+
+ if (str == NULL || *str == '\0')
+ {
+ if (self->entry.image)
+ {
+ gtk_image_clear (self->entry.image);
+ gtk_widget_hide (GTK_WIDGET (self->entry.image));
+ }
+ return;
+ }
+
+ if (!self->entry.image)
+ self->entry.image = g_object_ref_sink (gtk_image_new ());
+
+ gtk_widget_show (GTK_WIDGET (self->entry.image));
+
+ icon = g_icon_new_for_string (str, &error);
+ if (icon)
+ {
+ indicator_image_helper_update_from_gicon (self->entry.image, icon);
+ g_object_unref (icon);
+ }
+ else
+ {
+ g_warning ("invalid icon string '%s': %s", str, error->message);
+ gtk_image_set_from_stock (self->entry.image, GTK_STOCK_MISSING_IMAGE, GTK_ICON_SIZE_LARGE_TOOLBAR);
+ g_error_free (error);
+ }
+}
+
+static void
+indicator_ng_set_label (IndicatorNg *self,
+ const gchar *label)
+{
+ if (label == NULL || *label == '\0')
+ {
+ if (self->entry.label)
+ gtk_widget_hide (GTK_WIDGET (self->entry.label));
+ return;
+ }
+
+ if (!self->entry.label)
+ self->entry.label = g_object_ref_sink (gtk_label_new (NULL));
+
+ gtk_label_set_label (GTK_LABEL (self->entry.label), label);
+ gtk_widget_show (GTK_WIDGET (self->entry.label));
+}
+
+static void
+indicator_ng_update_entry (IndicatorNg *self)
+{
+ GVariant *state;
+
+ 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 *label;
+ const gchar *iconstr;
+ const gchar *accessible_desc;
+ gboolean visible;
+
+ g_variant_get (state, "(&s&s&sb)", &label, &iconstr, &accessible_desc, &visible);
+
+ indicator_ng_set_label (self, label);
+ indicator_ng_set_icon_from_string (self, iconstr);
+ indicator_ng_set_accessible_desc (self, accessible_desc);
+ indicator_object_set_visible (INDICATOR_OBJECT (self), visible);
+ }
+ else
+ g_warning ("the action of the indicator menu item must have state with type (sssb)");
+
+ 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-canonical-type", "s", &type))
+ {
+ has_type = g_str_equal (type, expected_type);
+ g_free (type);
+ }
+
+ return has_type;
+}
+
+static void
+indicator_ng_menu_changed (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);
+
+ if (indicator_ng_menu_item_is_of_type (self->menu, 0, "com.canonical.indicator.root"))
+ {
+ GMenuModel *popup;
+ gchar *action;
+
+ if (g_menu_model_get_item_attribute (self->menu, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action) &&
+ g_str_has_prefix (action, "indicator."))
+ {
+ self->header_action = g_strdup (action + strlen ("indicator."));
+ }
+
+ 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);
+
+ g_free (action);
+ }
+ else
+ g_warning ("indicator menu item must be of type 'com.canonical.indicator.root'");
+ }
+}
+
+static void
+indicator_ng_service_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+ gchar *menu_object_path;
+
+ g_assert (!self->actions);
+ g_assert (!self->menu);
+
+ 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);
+
+ menu_object_path = g_strconcat (self->object_path, "/", self->profile, NULL);
+ self->menu = G_MENU_MODEL (g_dbus_menu_model_get (connection, name_owner, 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);
+
+ g_free (menu_object_path);
+}
+
+static void
+indicator_ng_service_started (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IndicatorNg *self = user_data;
+ GError *error = NULL;
+ GVariant *reply;
+
+ reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &error);
+ if (!reply)
+ {
+ 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;
+ }
+
+ switch (g_variant_get_uint32 (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 (reply);
+}
+
+static void
+indicator_ng_service_vanished (GDBusConnection *connection,
+ 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)
+ 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);
+ }
+}
+
+static gboolean
+indicator_ng_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ IndicatorNg *self = INDICATOR_NG (initable);
+ GKeyFile *keyfile;
+
+ keyfile = g_key_file_new ();
+ if (!g_key_file_load_from_file (keyfile, self->service_file, G_KEY_FILE_NONE, error))
+ goto out;
+
+ if (!(self->name = g_key_file_get_string (keyfile, "Indicator Service", "Name", error)))
+ goto out;
+
+ self->entry.name_hint = self->name;
+
+ if (!(self->bus_name = g_key_file_get_string (keyfile, "Indicator Service", "BusName", error)))
+ goto out;
+
+ if (!(self->object_path = g_key_file_get_string (keyfile, "Indicator Service", "ObjectPath", error)))
+ goto out;
+
+ 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);
+
+out:
+ g_key_file_free (keyfile);
+
+ return self->name_watch_id > 0;
+}
+
+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;
+
+ 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)
+{
+ self->entry.menu = g_object_ref_sink (gtk_menu_new ());
+
+ /* 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;
+
+ 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;
+}
diff --git a/libindicator/indicator-ng.h b/libindicator/indicator-ng.h
new file mode 100644
index 0000000..f074a47
--- /dev/null
+++ b/libindicator/indicator-ng.h
@@ -0,0 +1,48 @@
+/*
+ * 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>
+ */
+
+#ifndef __INDICATOR_NG_H__
+#define __INDICATOR_NG_H__
+
+#include "indicator-object.h"
+
+#define INDICATOR_TYPE_NG (indicator_ng_get_type ())
+#define INDICATOR_NG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), INDICATOR_TYPE_NG, IndicatorNg))
+#define INDICATOR_NG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), INDICATOR_TYPE_NG, IndicatorNgClass))
+#define INDICATOR_IS_NG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), INDICATOR_TYPE_NG))
+#define INDICATOR_IS_NG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), INDICATOR_TYPE_NG))
+#define INDICATOR_NG_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), INDICATOR_TYPE_NG, IndicatorNgClass))
+
+typedef struct _IndicatorNg IndicatorNg;
+typedef IndicatorObjectClass IndicatorNgClass;
+
+GType indicator_ng_get_type (void);
+
+IndicatorNg * indicator_ng_new (const gchar *service_file,
+ GError **error);
+
+IndicatorNg * indicator_ng_new_for_profile (const gchar *service_file,
+ const gchar *profile,
+ GError **error);
+
+const gchar * indicator_ng_get_service_file (IndicatorNg *indicator);
+
+const gchar * indicator_ng_get_profile (IndicatorNg *indicator);
+
+#endif
diff --git a/libindicator/indicator-object.c b/libindicator/indicator-object.c
index ba2d377..30a6543 100644
--- a/libindicator/indicator-object.c
+++ b/libindicator/indicator-object.c
@@ -935,3 +935,10 @@ set_property (GObject * object,
}
}
+gboolean
+indicator_object_entry_is_visible (IndicatorObject * io, IndicatorObjectEntry * entry)
+{
+ g_return_val_if_fail (INDICATOR_IS_OBJECT (io), FALSE);
+
+ return entry_get_private (io, entry)->visibility == ENTRY_VISIBLE;
+}
diff --git a/libindicator/indicator-object.h b/libindicator/indicator-object.h
index d8551f9..3c1a203 100644
--- a/libindicator/indicator-object.h
+++ b/libindicator/indicator-object.h
@@ -199,6 +199,7 @@ GList * indicator_object_get_entries (IndicatorObject * io);
guint indicator_object_get_location (IndicatorObject * io, IndicatorObjectEntry * entry);
guint indicator_object_get_show_now (IndicatorObject * io, IndicatorObjectEntry * entry);
void indicator_object_set_visible (IndicatorObject * io, gboolean visible);
+gboolean indicator_object_entry_is_visible (IndicatorObject * io, IndicatorObjectEntry * entry);
void indicator_object_entry_activate (IndicatorObject * io, IndicatorObjectEntry * entry, guint timestamp);
void indicator_object_entry_activate_window (IndicatorObject * io, IndicatorObjectEntry * entry, guint windowid, guint timestamp);
void indicator_object_entry_close (IndicatorObject * io, IndicatorObjectEntry * entry, guint timestamp);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index c98bdbf..96825f3 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -26,7 +26,8 @@ EXTRA_DIST = \
session.conf.in \
service-manager-connect.service.in \
service-version-bad.service.in \
- service-version-good.service.in
+ service-version-good.service.in \
+ com.canonical.indicator.test.service.in
#############################
# Test Loader
@@ -466,3 +467,43 @@ DISTCLEANFILES += loader-tester
DISTCLEANFILES += $(XML_REPORT) $(HTML_REPORT)
+#############################
+# Indicator-ng
+#############################
+
+if USE_GTK3
+
+com.canonical.indicator.test.service: com.canonical.indicator.test.service.in Makefile.am
+ sed -e "s|\@builddir\@|$(abspath $(builddir))|" $< > $@
+
+check_PROGRAMS += test-indicator-ng
+
+test_indicator_ng_SOURCES = test-indicator-ng.c
+
+test_indicator_ng_CFLAGS = \
+ $(LIBINDICATOR_CFLAGS) \
+ -I$(top_srcdir) \
+ -DSRCDIR="\"$(srcdir)\"" \
+ -DBUILD_DIR="\"$(abs_builddir)\"" \
+ -Wall
+
+test_indicator_ng_LDADD = \
+ $(LIBINDICATOR_LIBS) \
+ -L$(top_builddir)/libindicator/.libs \
+ $(INDICATOR_LIB)
+
+TESTS += test-indicator-ng
+DISTCLEANFILES += test-indicator-ng
+
+# Mock indicator service
+check_PROGRAMS += indicator-test-service
+
+indicator_test_service_SOURCES = indicator-test-service.c
+indicator_test_service_CFLAGS = $(LIBINDICATOR_CFLAGS)
+indicator_test_service_LDADD = $(LIBINDICATOR_LIBS)
+
+EXTRA_indicator_test_service_DEPENDENCIES = com.canonical.indicator.test.service
+
+DISTCLEANFILES += indicator-test-service com.canonical.indicator.test.service
+
+endif
diff --git a/tests/com.canonical.indicator.test.service.in b/tests/com.canonical.indicator.test.service.in
new file mode 100644
index 0000000..3fdbe76
--- /dev/null
+++ b/tests/com.canonical.indicator.test.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=com.canonical.indicator.test
+Exec=@builddir@/indicator-test-service
diff --git a/tests/com.canonical.test.indicator b/tests/com.canonical.test.indicator
new file mode 100644
index 0000000..dad4c94
--- /dev/null
+++ b/tests/com.canonical.test.indicator
@@ -0,0 +1,4 @@
+[Indicator Service]
+Name=indicator-test
+BusName=com.canonical.indicator.test
+ObjectPath=/com/canonical/indicator/test
diff --git a/tests/com.canonical.test.nosuchservice.indicator b/tests/com.canonical.test.nosuchservice.indicator
new file mode 100644
index 0000000..8464749
--- /dev/null
+++ b/tests/com.canonical.test.nosuchservice.indicator
@@ -0,0 +1,4 @@
+[Indicator Service]
+Name=indicator-test
+BusName=com.canonical.indicator.test.nosuchservice
+ObjectPath=/com/canonical/indicator/test
diff --git a/tests/indicator-test-service.c b/tests/indicator-test-service.c
new file mode 100644
index 0000000..0393677
--- /dev/null
+++ b/tests/indicator-test-service.c
@@ -0,0 +1,108 @@
+
+#include <gio/gio.h>
+
+typedef struct
+{
+ GSimpleActionGroup *actions;
+ GMenu *menu;
+
+ guint actions_export_id;
+ guint menu_export_id;
+} IndicatorTestService;
+
+static void
+bus_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ IndicatorTestService *indicator = user_data;
+ GError *error = NULL;
+
+ indicator->actions_export_id = g_dbus_connection_export_action_group (connection,
+ "/com/canonical/indicator/test",
+ G_ACTION_GROUP (indicator->actions),
+ &error);
+ if (indicator->actions_export_id == 0)
+ {
+ g_warning ("cannot export action group: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ indicator->menu_export_id = g_dbus_connection_export_menu_model (connection,
+ "/com/canonical/indicator/test/desktop",
+ G_MENU_MODEL (indicator->menu),
+ &error);
+ if (indicator->menu_export_id == 0)
+ {
+ g_warning ("cannot export menu: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+}
+
+static void
+name_lost (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ IndicatorTestService *indicator = user_data;
+
+ if (indicator->actions_export_id)
+ g_dbus_connection_unexport_action_group (connection, indicator->actions_export_id);
+
+ if (indicator->menu_export_id)
+ g_dbus_connection_unexport_menu_model (connection, indicator->menu_export_id);
+}
+
+static void
+activate_show (GSimpleAction *action,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ g_message ("showing");
+}
+
+int
+main (int argc, char **argv)
+{
+ IndicatorTestService indicator = { 0 };
+ GMenuItem *item;
+ GMenu *submenu;
+ GActionEntry entries[] = {
+ { "_header", NULL, NULL, "('Test', 'indicator-test', 'Test indicator', true)", NULL },
+ { "show", activate_show, NULL, NULL, NULL }
+ };
+ GMainLoop *loop;
+
+ indicator.actions = g_simple_action_group_new ();
+ g_simple_action_group_add_entries (indicator.actions, entries, G_N_ELEMENTS (entries), NULL);
+
+ submenu = g_menu_new ();
+ g_menu_append (submenu, "Show", "indicator.show");
+ item = g_menu_item_new (NULL, "indicator._header");
+ g_menu_item_set_attribute (item, "x-canonical-type", "s", "com.canonical.indicator.root");
+ g_menu_item_set_submenu (item, G_MENU_MODEL (submenu));
+ indicator.menu = g_menu_new ();
+ g_menu_append_item (indicator.menu, item);
+
+ g_bus_own_name (G_BUS_TYPE_SESSION,
+ "com.canonical.indicator.test",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ bus_acquired,
+ NULL,
+ name_lost,
+ &indicator,
+ NULL);
+
+ loop = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (loop);
+
+ g_object_unref (submenu);
+ g_object_unref (item);
+ g_object_unref (indicator.actions);
+ g_object_unref (indicator.menu);
+ g_object_unref (loop);
+
+ return 0;
+}
diff --git a/tests/test-indicator-ng.c b/tests/test-indicator-ng.c
new file mode 100644
index 0000000..150cc37
--- /dev/null
+++ b/tests/test-indicator-ng.c
@@ -0,0 +1,167 @@
+
+#include <libindicator/indicator-ng.h>
+
+static void
+indicator_ng_test_func (gconstpointer user_data)
+{
+ GTestFunc test_func = user_data;
+ GTestDBus *bus;
+
+ bus = g_test_dbus_new (G_TEST_DBUS_NONE);
+ g_test_dbus_add_service_dir (bus, BUILD_DIR);
+ g_test_dbus_up (bus);
+
+ test_func ();
+
+ g_test_dbus_down (bus);
+ g_object_unref (bus);
+}
+
+#define indicator_ng_test_add(name, test_func) \
+ g_test_add_data_func ("/indicator-ng/" name, test_func, indicator_ng_test_func)
+
+static gboolean
+stop_main_loop (gpointer user_data)
+{
+ GMainLoop *loop = user_data;
+
+ g_main_loop_quit (loop);
+
+ return FALSE;
+}
+
+static void
+test_non_existing (void)
+{
+ IndicatorNg *indicator;
+ GError *error = NULL;
+
+ indicator = indicator_ng_new (SRCDIR "/com.canonical.does.not.exist.indicator", &error);
+ g_assert (indicator == NULL);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
+
+ g_clear_error (&error);
+}
+
+static void
+test_instantiation (void)
+{
+ IndicatorNg *indicator;
+ GError *error = NULL;
+ GMainLoop *loop;
+
+ indicator = indicator_ng_new (SRCDIR "/com.canonical.test.nosuchservice.indicator", &error);
+ g_assert (indicator);
+ g_assert (error == NULL);
+
+ g_assert_cmpstr (indicator_ng_get_service_file (indicator), ==, SRCDIR "/com.canonical.test.nosuchservice.indicator");
+ g_assert_cmpstr (indicator_ng_get_profile (indicator), ==, "desktop");
+
+ {
+ gchar *service_file;
+ gchar *profile;
+
+ g_object_get (indicator, "service-file", &service_file,
+ "profile", &profile,
+ NULL);
+
+ g_assert_cmpstr (service_file, ==, SRCDIR "/com.canonical.test.nosuchservice.indicator");
+ g_assert_cmpstr (profile, ==, "desktop");
+
+ g_free (service_file);
+ g_free (profile);
+ }
+
+ loop = g_main_loop_new (NULL, FALSE);
+ g_timeout_add (200, stop_main_loop, loop);
+ g_main_loop_run (loop);
+
+ /* no such service, so there shouldn't be any indicators */
+ g_assert (indicator_object_get_entries (INDICATOR_OBJECT (indicator)) == NULL);
+
+ g_main_loop_unref (loop);
+ g_object_unref (indicator);
+}
+
+static void
+test_instantiation_with_profile (void)
+{
+ IndicatorNg *indicator;
+ GError *error = NULL;
+
+ indicator = indicator_ng_new_for_profile (SRCDIR "/com.canonical.test.indicator", "greeter", &error);
+ g_assert (indicator);
+ g_assert (error == NULL);
+
+ g_assert_cmpstr (indicator_ng_get_profile (indicator), ==, "greeter");
+
+ g_object_unref (indicator);
+}
+
+static void
+test_menu (void)
+{
+ IndicatorNg *indicator;
+ GError *error = NULL;
+ GMainLoop *loop;
+ GList *entries;
+ IndicatorObjectEntry *entry;
+
+ indicator = indicator_ng_new (SRCDIR "/com.canonical.test.indicator", &error);
+ g_assert (indicator);
+ g_assert (error == NULL);
+
+ loop = g_main_loop_new (NULL, FALSE);
+ g_timeout_add (500, stop_main_loop, loop);
+ g_main_loop_run (loop);
+
+ entries = indicator_object_get_entries (INDICATOR_OBJECT (indicator));
+ g_assert_cmpint (g_list_length (entries), ==, 1);
+
+ entry = entries->data;
+ g_assert_cmpstr (entry->name_hint, ==, "indicator-test");
+ g_assert_cmpstr (entry->accessible_desc, ==, "Test indicator");
+ g_assert_cmpstr (gtk_label_get_label (entry->label), ==, "Test");
+ g_assert (gtk_image_get_storage_type (entry->image) == GTK_IMAGE_STOCK);
+ {
+ GList *children;
+ GtkMenuItem *item;
+
+ g_assert (entry->menu != NULL);
+
+ children = gtk_container_get_children (GTK_CONTAINER (entry->menu));
+ g_assert_cmpint (g_list_length (children), ==, 1);
+
+ item = children->data;
+ g_assert (GTK_IS_MENU_ITEM (item));
+ g_assert (gtk_widget_is_sensitive (GTK_WIDGET (item)));
+ g_assert_cmpstr (gtk_menu_item_get_label (item), ==, "Show");
+
+ g_list_free (children);
+ }
+
+ g_list_free (entries);
+ g_main_loop_unref (loop);
+ g_object_unref (indicator);
+}
+
+int
+main (int argc, char **argv)
+{
+ /* gvfs, dconf, and appmenu-gtk leak GDbusConnections, which confuses
+ * g_test_dbus_down. Make sure we're not using any of those.
+ */
+ g_setenv ("GIO_USE_VFS", "local", TRUE);
+ g_setenv ("GSETTINGS_BACKEND", "memory", TRUE);
+ g_unsetenv ("UBUNTU_MENUPROXY");
+
+ g_test_init (&argc, &argv, NULL);
+ gtk_init (&argc, &argv);
+
+ indicator_ng_test_add ("non-existing", test_non_existing);
+ indicator_ng_test_add ("instantiation", test_instantiation);
+ indicator_ng_test_add ("instantiation-with-profile", test_instantiation_with_profile);
+ indicator_ng_test_add ("menu", test_menu);
+
+ return g_test_run ();
+}
diff --git a/tools/indicator-loader.c b/tools/indicator-loader.c
index 560fc45..d275c43 100644
--- a/tools/indicator-loader.c
+++ b/tools/indicator-loader.c
@@ -25,6 +25,10 @@ License along with this library. If not, see
#include <gtk/gtk.h>
#include <libindicator/indicator-object.h>
+#if GTK_MAJOR_VERSION == 3
+#include <libindicator/indicator-ng.h>
+#endif
+
static GHashTable * entry_to_menuitem = NULL;
#define ENTRY_DATA_NAME "indicator-custom-entry-data"
@@ -129,14 +133,27 @@ load_module (const gchar * name, GtkWidget * menu)
g_debug("Looking at Module: %s", name);
g_return_val_if_fail(name != NULL, FALSE);
- if (!g_str_has_suffix(name, G_MODULE_SUFFIX)) {
- return FALSE;
- }
-
g_debug("Loading Module: %s", name);
/* Build the object for the module */
- IndicatorObject * io = indicator_object_new_from_file(name);
+ IndicatorObject *io;
+ if (g_str_has_suffix(name, G_MODULE_SUFFIX)) {
+ io = indicator_object_new_from_file(name);
+ }
+#if GTK_MAJOR_VERSION == 3
+ else if (g_str_has_suffix(name, ".indicator")) {
+ GError *error = NULL;
+
+ io = INDICATOR_OBJECT(indicator_ng_new(name, &error));
+ if (!io) {
+ g_warning ("could not load indicator from '%s': %s", name, error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+ }
+#endif
+ else
+ return FALSE;
/* Connect to it's signals */
g_signal_connect(G_OBJECT(io), INDICATOR_OBJECT_SIGNAL_ENTRY_ADDED, G_CALLBACK(entry_added), menu);
diff --git a/trim-lcov.py b/trim-lcov.py
new file mode 100755
index 0000000..8465e95
--- /dev/null
+++ b/trim-lcov.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+
+# Copyright 2013 Canonical Ltd.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.0 as published by the Free Software Foundation.
+#
+# This library 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 version 3.0 for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this library. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+# Author: Ryan Lortie <desrt@desrt.ca>
+
+
+# This script removes branch and/or line coverage data for lines that
+# contain a particular substring.
+#
+# In the interest of "fairness" it removes all branch or coverage data
+# when a match is found -- not just negative data. It is therefore
+# likely that running this script will actually reduce the total number
+# of lines and branches that are marked as covered (in absolute terms).
+#
+# This script intentionally avoids checking for errors. Any exceptions
+# will trigger make to fail.
+
+import sys
+
+line_suppress = ['g_assert_not_reached']
+branch_suppress = ['g_assert', 'g_return_if_fail', 'g_return_val_if_fail', 'G_DEFINE_TYPE']
+
+def check_suppress(suppressions, source, data):
+ line, _, rest = data.partition(',')
+ line = int(line) - 1
+
+ assert line < len(source)
+
+ for suppression in suppressions:
+ if suppression in source[line]:
+ return True
+
+ return False
+
+source = []
+for line in sys.stdin:
+ line = line[:-1]
+
+ keyword, _, rest = line.partition(':')
+
+ # Source file
+ if keyword == 'SF':
+ source = file(rest).readlines()
+
+ # Branch coverage data
+ elif keyword == 'BRDA':
+ if check_suppress(branch_suppress, source, rest):
+ continue
+
+ # Line coverage data
+ elif keyword == 'DA':
+ if check_suppress(line_suppress, source, rest):
+ continue
+
+ print line