aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Uebernickel <lars.uebernickel@canonical.com>2013-02-14 22:25:39 +0000
committerTarmac <Unknown>2013-02-14 22:25:39 +0000
commitd05835eae4e47417fdd41a1d4fcaf6a2bcfd96ea (patch)
tree43995c9938a1039e251512d6f9ed71971b27a476
parente7966480ecd14bd6cb4c4ec5f527cfe10a1a0b0f (diff)
parent40d7c42d5212dc97ce6b07f05828fb62440d0694 (diff)
downloadlibayatana-indicator-d05835eae4e47417fdd41a1d4fcaf6a2bcfd96ea.tar.gz
libayatana-indicator-d05835eae4e47417fdd41a1d4fcaf6a2bcfd96ea.tar.bz2
libayatana-indicator-d05835eae4e47417fdd41a1d4fcaf6a2bcfd96ea.zip
Add IndicatorNg.
IndicatorNg is an indicator object that reads an indicator service file and watches the bus for a corresponding service to appear. It turns the menus and actions exported by the service into an indicator entry. I think this is a good solution for the transition period in which we support both styles of indicators. (It means we don't need to copy templates around.) An indicator service file must have an ".indicator" extension and contents simlilar to this: [Indicator Service] Name=indicator-test BusName=com.canonical.indicator.test ObjectPath=/com/canonical/indicator/test For unity-panel-service, these files will be installed somewhere. The indicator-loader in this branch accepts a path to such a file as the first command line argument (instead of the .so file). This can be tested with the example indicator in lp:~larsu/libunity/add-indicator (examples/indicator.vala). Approved by Charles Kerr, Ted Gould.
-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