/* * Copyright 2012 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 . * * Authors: * Lars Uebernickel */ #include "im-application-list.h" #include "indicator-messages-application.h" #include "gactionmuxer.h" #include "indicator-desktop-shortcuts.h" #include "im-accounts-service.h" #include #include #include "glib/gi18n.h" typedef GObjectClass ImApplicationListClass; struct _ImApplicationList { GObject parent; GHashTable *applications; GActionMuxer *muxer; GSimpleActionGroup * globalactions; GSimpleAction * statusaction; GHashTable *app_status; ImAccountsService * as; }; G_DEFINE_TYPE (ImApplicationList, im_application_list, G_TYPE_OBJECT); G_DEFINE_QUARK (draws_attention, message_action_draws_attention); enum { SOURCE_ADDED, SOURCE_CHANGED, SOURCE_REMOVED, MESSAGE_ADDED, MESSAGE_REMOVED, APP_ADDED, APP_STOPPED, REMOVE_ALL, STATUS_SET, N_SIGNALS }; static guint signals[N_SIGNALS]; typedef struct { ImApplicationList *list; GDesktopAppInfo *info; gchar *id; IndicatorMessagesApplication *proxy; GActionMuxer *muxer; GSimpleActionGroup *source_actions; GSimpleActionGroup *message_actions; GActionMuxer *message_sub_actions; GCancellable *cancellable; gboolean draws_attention; IndicatorDesktopShortcuts * shortcuts; } Application; /* Prototypes */ static void status_activated (GSimpleAction * action, GVariant * param, gpointer user_data); static void application_free (gpointer data) { Application *app = data; if (!app) return; g_object_unref (app->info); g_free (app->id); if (app->cancellable) { g_cancellable_cancel (app->cancellable); g_clear_object (&app->cancellable); } if (app->proxy) g_object_unref (app->proxy); if (app->muxer) { g_object_unref (app->muxer); g_object_unref (app->source_actions); g_object_unref (app->message_actions); g_object_unref (app->message_sub_actions); } g_clear_object (&app->shortcuts); g_slice_free (Application, app); } /* Check to see if we have actions by getting the full list of names and see if there is one. Not exactly efficient :-/ */ static gboolean _g_action_group_has_actions (GActionGroup * ag) { gchar ** list = NULL; gboolean retval = FALSE; list = g_action_group_list_actions(ag); retval = (list[0] != NULL); g_strfreev(list); return retval; } static gchar * escape_action_name (const gchar *name) { static const gchar *xdigits = "0123456789abcdef"; GString *escaped; gchar c; g_return_val_if_fail (name != NULL, NULL); escaped = g_string_new (NULL); while ((c = *name++)) { if (g_ascii_isalnum (c) || c == '.') { g_string_append_c (escaped, c); } else { g_string_append_c (escaped, '-'); g_string_append_c (escaped, xdigits[c >> 4]); g_string_append_c (escaped, xdigits[c & 0xf]); } } return g_string_free (escaped, FALSE); } static gchar * unescape_action_name (const gchar *name) { GString *unescaped; gint i; g_return_val_if_fail (name != NULL, NULL); unescaped = g_string_new (NULL); for (i = 0; name[i]; i++) { gint one, two; if (name[i] == '-' && (one = g_ascii_xdigit_value (name[i + 1])) >= 0 && (two = g_ascii_xdigit_value (name[i + 2])) >= 0) { g_string_append_c (unescaped, (one << 4) | two); i += 2; } else { g_string_append_c (unescaped, name[i]); } } return g_string_free (unescaped, FALSE); } /* Check to see if either of our action groups has any actions, if so return TRUE so we get chosen! */ static gboolean application_has_items (gpointer key, gpointer value, gpointer user_data) { Application *app = value; return _g_action_group_has_actions(G_ACTION_GROUP(app->source_actions)) || _g_action_group_has_actions(G_ACTION_GROUP(app->message_actions)); } static gboolean application_draws_attention (gpointer key, gpointer value, gpointer user_data) { Application *app = value; return app->draws_attention; } static void im_application_list_update_root_action (ImApplicationList *list) { const gchar *base_icon_name; const gchar *accessible_name; gchar *icon_name; GIcon * icon; GVariant *serialized_icon; GVariantBuilder builder; GVariant *state; guint n_applications; /* Figure out what type of icon we should be drawing */ if (g_hash_table_find (list->applications, application_draws_attention, NULL)) { base_icon_name = "indicator-messages-new-%s"; accessible_name = _("New Messages"); im_accounts_service_set_draws_attention(list->as, TRUE); } else { base_icon_name = "indicator-messages-%s"; accessible_name = _("Messages"); im_accounts_service_set_draws_attention(list->as, FALSE); } /* Include the IM state in the icon */ state = g_action_group_get_action_state(G_ACTION_GROUP(list->globalactions), "status"); icon_name = g_strdup_printf(base_icon_name, g_variant_get_string(state, NULL)); g_variant_unref(state); /* Build up the dictionary of values for the state */ g_variant_builder_init(&builder, G_VARIANT_TYPE_DICTIONARY); /* icon */ icon = g_themed_icon_new_with_default_fallbacks(icon_name); g_free(icon_name); if ((serialized_icon = g_icon_serialize(icon))) { g_variant_builder_add (&builder, "{sv}", "icon", serialized_icon); g_variant_unref (serialized_icon); } g_object_unref(icon); /* title */ g_variant_builder_add (&builder, "{sv}", "title", g_variant_new_string (_("Notifications"))); g_variant_builder_add (&builder, "{sv}", "tooltip", g_variant_new_string (_("Quick access to newly received messages"))); /* accessible description */ g_variant_builder_open(&builder, G_VARIANT_TYPE_DICT_ENTRY); g_variant_builder_add_value(&builder, g_variant_new_string("accessible-desc")); g_variant_builder_add_value(&builder, g_variant_new_variant(g_variant_new_string(accessible_name))); g_variant_builder_close(&builder); /* visibility */ n_applications = g_hash_table_size (list->applications); g_variant_builder_add (&builder, "{sv}", "visible", g_variant_new_boolean (n_applications > 0)); /* Set the state */ g_action_group_change_action_state (G_ACTION_GROUP(list->globalactions), "messages", g_variant_builder_end(&builder)); GAction * remove_action = g_action_map_lookup_action (G_ACTION_MAP (list->globalactions), "remove-all"); if (g_hash_table_find (list->applications, application_has_items, NULL)) { g_debug("Enabling remove-all"); g_simple_action_set_enabled(G_SIMPLE_ACTION(remove_action), TRUE); } else { g_debug("Disabling remove-all"); g_simple_action_set_enabled(G_SIMPLE_ACTION(remove_action), FALSE); } } /* Check a source action to see if it draws */ static gboolean app_source_action_check_draw (Application * app, const gchar * action_name) { GVariant *state; guint32 count; gint64 time; const gchar *string; gboolean draws_attention; state = g_action_group_get_action_state (G_ACTION_GROUP(app->source_actions), action_name); if (state == NULL) return FALSE; if (!g_variant_is_of_type(state, G_VARIANT_TYPE("(uxsb)"))) return FALSE; g_variant_get (state, "(ux&sb)", &count, &time, &string, &draws_attention); /* invisible sources do not draw attention */ if (count == 0 && time == 0 && (string == NULL || string[0] == '\0')) draws_attention = FALSE; g_variant_unref(state); return draws_attention; } /* Check a message action to see if it draws */ static gboolean app_message_action_check_draw (Application * app, const gchar * action_name) { GAction * action = NULL; action = g_action_map_lookup_action (G_ACTION_MAP (app->message_actions), action_name); return GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(action), message_action_draws_attention_quark())); } /* Regenerate the draw attention flag based on the sources and messages * that we have in the action groups. * * Returns TRUE if app->draws_attention has changed */ static gboolean application_update_draws_attention (Application * app) { gchar **source_actions = NULL; gchar **message_actions = NULL; gchar **it; gboolean was_drawing_attention = app->draws_attention; app->draws_attention = FALSE; source_actions = g_action_group_list_actions (G_ACTION_GROUP (app->source_actions)); for (it = source_actions; *it && !app->draws_attention; it++) app->draws_attention = app_source_action_check_draw (app, *it); message_actions = g_action_group_list_actions (G_ACTION_GROUP (app->message_actions)); for (it = message_actions; *it && !app->draws_attention; it++) app->draws_attention = app_message_action_check_draw (app, *it); g_strfreev (source_actions); g_strfreev (message_actions); return was_drawing_attention != app->draws_attention; } static void im_application_list_source_removed_action (Application *app, const gchar *action_name) { g_action_map_remove_action (G_ACTION_MAP(app->source_actions), action_name); g_signal_emit (app->list, signals[SOURCE_REMOVED], 0, app->id, action_name); application_update_draws_attention (app); im_application_list_update_root_action (app->list); } /* Remove a source from an application, signal up and update the status of the draws attention flag. */ static void im_application_list_source_removed (Application *app, const gchar *id) { gchar *action_name; action_name = escape_action_name (id); im_application_list_source_removed_action (app, action_name); g_free (action_name); } static void im_application_list_source_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) { Application *app = user_data; const gchar *action_name; gchar *source_id; action_name = g_action_get_name (G_ACTION (action)); source_id = unescape_action_name (action_name); if (g_variant_get_boolean (parameter)) { indicator_messages_application_call_activate_source (app->proxy, source_id, app->cancellable, NULL, NULL); } else { const gchar *sources[] = { source_id, NULL }; const gchar *messages[] = { NULL }; indicator_messages_application_call_dismiss (app->proxy, sources, messages, app->cancellable, NULL, NULL); } im_application_list_source_removed_action (app, action_name); g_free (source_id); } static void im_application_list_message_removed_action (Application *app, const gchar *action_name) { g_action_map_remove_action (G_ACTION_MAP(app->message_actions), action_name); g_action_muxer_remove (app->message_sub_actions, action_name); application_update_draws_attention (app); im_application_list_update_root_action (app->list); g_signal_emit (app->list, signals[MESSAGE_REMOVED], 0, app->id, action_name); } static void im_application_list_message_removed (Application *app, const gchar *id) { gchar *action_name; action_name = escape_action_name (id); im_application_list_message_removed_action(app, action_name); g_free (action_name); } static void im_application_list_message_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) { Application *app = user_data; const gchar *action_name; gchar *message_id; action_name = g_action_get_name (G_ACTION (action)); message_id = unescape_action_name (action_name); if (g_variant_get_boolean (parameter)) { indicator_messages_application_call_activate_message (app->proxy, message_id, "", g_variant_new_array (G_VARIANT_TYPE_VARIANT, NULL, 0), app->cancellable, NULL, NULL); } else { const gchar *sources[] = { NULL }; const gchar *messages[] = { message_id, NULL }; indicator_messages_application_call_dismiss (app->proxy, sources, messages, app->cancellable, NULL, NULL); } im_application_list_message_removed_action (app, action_name); g_free (message_id); } static void im_application_list_sub_message_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) { Application *app = user_data; const gchar *message_id; gchar *action_id; GVariantBuilder builder; message_id = g_object_get_data (G_OBJECT (action), "message"); action_id = unescape_action_name (g_action_get_name (G_ACTION (action))); g_variant_builder_init (&builder, G_VARIANT_TYPE ("av")); if (parameter) g_variant_builder_add (&builder, "v", parameter); indicator_messages_application_call_activate_message (app->proxy, message_id, action_id, g_variant_builder_end (&builder), app->cancellable, NULL, NULL); im_application_list_message_removed (app, message_id); g_free (action_id); } static void im_application_list_remove_all (GSimpleAction *action, GVariant *parameter, gpointer user_data) { ImApplicationList *list = user_data; GHashTableIter iter; Application *app; g_signal_emit (list, signals[REMOVE_ALL], 0); g_hash_table_iter_init (&iter, list->applications); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &app)) { gchar **source_actions; gchar **message_actions; gchar **it; app->draws_attention = FALSE; source_actions = g_action_group_list_actions (G_ACTION_GROUP (app->source_actions)); for (it = source_actions; *it; it++) im_application_list_source_removed_action (app, *it); message_actions = g_action_group_list_actions (G_ACTION_GROUP (app->message_actions)); for (it = message_actions; *it; it++) im_application_list_message_removed_action (app, *it); if (app->proxy != NULL) /* If it is remote, we tell the app we've cleared */ { guint i; gchar **unescaped_source_actions; gchar **unescaped_message_actions; /* Unescape action names */ unescaped_source_actions = g_new0 (gchar *, g_strv_length (source_actions) + 1); for (i = 0; source_actions[i]; i++) unescaped_source_actions[i] = unescape_action_name (source_actions[i]); unescaped_message_actions = g_new0 (gchar *, g_strv_length (message_actions) + 1); for (i = 0; message_actions[i]; i++) unescaped_message_actions[i] = unescape_action_name (message_actions[i]); indicator_messages_application_call_dismiss (app->proxy, (const gchar * const *) unescaped_source_actions, (const gchar * const *) unescaped_message_actions, app->cancellable, NULL, NULL); g_strfreev (unescaped_source_actions); g_strfreev (unescaped_message_actions); } g_strfreev (source_actions); g_strfreev (message_actions); } im_application_list_update_root_action (list); } static void im_application_list_dispose (GObject *object) { ImApplicationList *list = IM_APPLICATION_LIST (object); g_clear_object (&list->statusaction); g_clear_object (&list->globalactions); g_clear_pointer (&list->app_status, g_hash_table_unref); g_clear_pointer (&list->applications, g_hash_table_unref); g_clear_object (&list->muxer); g_clear_object (&list->as); G_OBJECT_CLASS (im_application_list_parent_class)->dispose (object); } static void im_application_list_finalize (GObject *object) { G_OBJECT_CLASS (im_application_list_parent_class)->finalize (object); } static void im_application_list_class_init (ImApplicationListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = im_application_list_dispose; object_class->finalize = im_application_list_finalize; signals[SOURCE_ADDED] = g_signal_new ("source-added", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_BOOLEAN); signals[SOURCE_CHANGED] = g_signal_new ("source-changed", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_BOOLEAN); signals[SOURCE_REMOVED] = g_signal_new ("source-removed", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); signals[MESSAGE_ADDED] = g_signal_new ("message-added", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 10, G_TYPE_STRING, G_TYPE_ICON, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_INT64, G_TYPE_BOOLEAN); signals[MESSAGE_REMOVED] = g_signal_new ("message-removed", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); signals[APP_ADDED] = g_signal_new ("app-added", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_DESKTOP_APP_INFO); signals[APP_STOPPED] = g_signal_new ("app-stopped", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); signals[REMOVE_ALL] = g_signal_new ("remove-all", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[STATUS_SET] = g_signal_new ("status-set", IM_TYPE_APPLICATION_LIST, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, G_TYPE_STRING); } static void im_application_list_init (ImApplicationList *list) { const GActionEntry action_entries[] = { { "remove-all", im_application_list_remove_all } }; list->applications = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, application_free); list->app_status = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); list->globalactions = g_simple_action_group_new (); { GSimpleAction * messages = g_simple_action_new_stateful("messages", NULL, g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0)); g_action_map_add_action(G_ACTION_MAP(list->globalactions), G_ACTION(messages)); } g_action_map_add_action_entries (G_ACTION_MAP (list->globalactions), action_entries, G_N_ELEMENTS (action_entries), list); list->statusaction = g_simple_action_new_stateful("status", G_VARIANT_TYPE_STRING, g_variant_new_string("offline")); g_signal_connect(list->statusaction, "activate", G_CALLBACK(status_activated), list); g_action_map_add_action(G_ACTION_MAP(list->globalactions), G_ACTION(list->statusaction)); list->muxer = g_action_muxer_new (); g_action_muxer_insert (list->muxer, NULL, G_ACTION_GROUP (list->globalactions)); list->as = im_accounts_service_ref_default(); im_application_list_update_root_action (list); } ImApplicationList * im_application_list_new (void) { return g_object_new (IM_TYPE_APPLICATION_LIST, NULL); } static gchar * im_application_list_canonical_id (const gchar *id) { gchar *str; gchar *p; int len; len = strlen (id); if (g_str_has_suffix (id, ".desktop")) len -= 8; str = g_strndup (id, len); for (p = str; *p; p++) { if (*p == '.') *p = '_'; } return str; } static Application * im_application_list_lookup (ImApplicationList *list, const gchar *desktop_id) { gchar *id; Application *app; id = im_application_list_canonical_id (desktop_id); app = g_hash_table_lookup (list->applications, id); g_free (id); return app; } void im_application_list_activate_launch (GSimpleAction *action, GVariant *parameter, gpointer user_data) { Application *app = user_data; GError *error = NULL; if (!g_app_info_launch (G_APP_INFO (app->info), NULL, NULL, &error)) { g_warning ("unable to launch application: %s", error->message); g_error_free (error); } } void im_application_list_activate_app_action (GSimpleAction *action, GVariant *parameter, gpointer user_data) { Application *app = user_data; indicator_desktop_shortcuts_nick_exec_with_context (app->shortcuts, g_action_get_name (G_ACTION (action)), NULL); } gboolean im_application_list_add (ImApplicationList *list, const gchar *desktop_id) { GDesktopAppInfo *info; Application *app; const gchar *id; GSimpleActionGroup *actions; GSimpleAction *launch_action; IndicatorDesktopShortcuts * shortcuts = NULL; g_return_val_if_fail (IM_IS_APPLICATION_LIST (list), FALSE); g_return_val_if_fail (desktop_id != NULL, FALSE); if (im_application_list_lookup (list, desktop_id)) return TRUE; info = g_desktop_app_info_new (desktop_id); if (!info) { g_warning ("an application with id '%s' is not installed", desktop_id); return FALSE; } id = g_app_info_get_id (G_APP_INFO (info)); g_return_val_if_fail (id != NULL, FALSE); { const char * filename = g_desktop_app_info_get_filename(info); if (filename != NULL) shortcuts = indicator_desktop_shortcuts_new(filename, "Messaging Menu"); } app = g_slice_new0 (Application); app->info = info; app->id = im_application_list_canonical_id (id); app->list = list; app->muxer = g_action_muxer_new (); app->source_actions = g_simple_action_group_new (); app->message_actions = g_simple_action_group_new (); app->message_sub_actions = g_action_muxer_new (); app->draws_attention = FALSE; app->shortcuts = shortcuts; actions = g_simple_action_group_new (); launch_action = g_simple_action_new_stateful ("launch", NULL, g_variant_new_boolean (FALSE)); g_signal_connect (launch_action, "activate", G_CALLBACK (im_application_list_activate_launch), app); g_action_map_add_action (G_ACTION_MAP (actions), G_ACTION (launch_action)); if (app->shortcuts != NULL) { const gchar ** nicks; for (nicks = indicator_desktop_shortcuts_get_nicks (app->shortcuts); *nicks; nicks++) { GSimpleAction *action; action = g_simple_action_new (*nicks, NULL); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_activate_app_action), app); g_action_map_add_action (G_ACTION_MAP (actions), G_ACTION (action)); g_object_unref (action); } } g_action_muxer_insert (app->muxer, NULL, G_ACTION_GROUP (actions)); g_action_muxer_insert (app->muxer, "src", G_ACTION_GROUP (app->source_actions)); g_action_muxer_insert (app->muxer, "msg", G_ACTION_GROUP (app->message_actions)); g_action_muxer_insert (app->muxer, "msg-actions", G_ACTION_GROUP (app->message_sub_actions)); g_hash_table_insert (list->applications, (gpointer) app->id, app); g_action_muxer_insert (list->muxer, app->id, G_ACTION_GROUP (app->muxer)); im_application_list_update_root_action (list); g_signal_emit (app->list, signals[APP_ADDED], 0, app->id, app->info); g_object_unref (launch_action); g_object_unref (actions); return TRUE; } void im_application_list_remove (ImApplicationList *list, const gchar *id) { Application *app; g_return_if_fail (IM_IS_APPLICATION_LIST (list)); app = im_application_list_lookup (list, id); if (app) { if (app->proxy || app->cancellable) g_signal_emit (app->list, signals[APP_STOPPED], 0, app->id); g_hash_table_remove (list->applications, id); g_action_muxer_remove (list->muxer, id); im_application_list_update_root_action (list); } } static void im_application_list_source_added (Application *app, guint position, GVariant *source) { const gchar *id; const gchar *label; GVariant *maybe_serialized_icon; guint32 count; gint64 time; const gchar *string; gboolean draws_attention; gboolean visible; GVariant *serialized_icon = NULL; GVariant *state; GSimpleAction *action; gchar *action_name; g_variant_get (source, "(&s&s@avux&sb)", &id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention); if (g_variant_n_children (maybe_serialized_icon) == 1) g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon); visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0'); state = g_variant_new ("(uxsb)", count, time, string, draws_attention); action_name = escape_action_name (id); action = g_simple_action_new_stateful (action_name, G_VARIANT_TYPE_BOOLEAN, state); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_source_activated), app); g_action_map_add_action (G_ACTION_MAP(app->source_actions), G_ACTION (action)); g_signal_emit (app->list, signals[SOURCE_ADDED], 0, app->id, action_name, label, serialized_icon, visible); if (visible && draws_attention && app->draws_attention == FALSE) app->draws_attention = TRUE; im_application_list_update_root_action (app->list); g_free (action_name); g_object_unref (action); if (serialized_icon) g_variant_unref (serialized_icon); g_variant_unref (maybe_serialized_icon); } static void im_application_list_source_changed (Application *app, GVariant *source) { const gchar *id; const gchar *label; GVariant *maybe_serialized_icon; guint32 count; gint64 time; const gchar *string; gboolean draws_attention; GVariant *serialized_icon = NULL; gboolean visible; gchar *action_name; g_variant_get (source, "(&s&s@avux&sb)", &id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention); if (g_variant_n_children (maybe_serialized_icon) == 1) g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon); action_name = escape_action_name (id); g_action_group_change_action_state (G_ACTION_GROUP (app->source_actions), action_name, g_variant_new ("(uxsb)", count, time, string, draws_attention)); visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0'); g_signal_emit (app->list, signals[SOURCE_CHANGED], 0, app->id, action_name, label, serialized_icon, visible); application_update_draws_attention (app); im_application_list_update_root_action (app->list); if (serialized_icon) g_variant_unref (serialized_icon); g_variant_unref (maybe_serialized_icon); g_free (action_name); } static void im_application_list_sources_listed (GObject *source_object, GAsyncResult *result, gpointer user_data) { Application *app = user_data; GVariant *sources; GError *error = NULL; if (indicator_messages_application_call_list_sources_finish (app->proxy, &sources, result, &error)) { GVariantIter iter; GVariant *source; guint i = 0; g_variant_iter_init (&iter, sources); while ((source = g_variant_iter_next_value (&iter))) { im_application_list_source_added (app, i++, source); g_variant_unref (source); } g_variant_unref (sources); } else { g_warning ("could not fetch the list of sources: %s", error->message); g_error_free (error); } } static GIcon * get_symbolic_app_icon (GDesktopAppInfo *info) { gchar *x_symbolic_icon; GIcon *icon; const gchar * const *names; gchar *symbolic_name; GIcon *symbolic_icon; /* If X-Ubuntu-SymbolicIcon exists, use that. It is always interpreted * as a filename. * * Simply appending -symbolic to the normal icon doesn't work for * icons specified by file name, because the symbolic icon might be in * a different format (symbolic icons tend to be svg and normal icons * png). Also, icons specified by file names don't allow for fallbacks * (without stating the file from this process), and we'd want to fall * back to the normal app icon. * * See lp: #1365408 */ if ((x_symbolic_icon = g_desktop_app_info_get_string (info, "X-Ubuntu-SymbolicIcon"))) { GFile *file; file = g_file_new_for_path (x_symbolic_icon); symbolic_icon = g_file_icon_new (file); g_object_unref (file); g_free (x_symbolic_icon); return symbolic_icon; } icon = g_app_info_get_icon (G_APP_INFO (info)); if (icon == NULL) return NULL; if (!G_IS_THEMED_ICON (icon)) return g_object_ref (icon); names = g_themed_icon_get_names (G_THEMED_ICON (icon)); if (!names || !names[0]) return g_object_ref (icon); symbolic_name = g_strconcat (names[0], "-symbolic", NULL); symbolic_icon = g_themed_icon_new_from_names ((gchar **) names, -1); g_themed_icon_prepend_name (G_THEMED_ICON (symbolic_icon), symbolic_name); g_free (symbolic_name); return symbolic_icon; } static void im_application_list_message_added (Application *app, GVariant *message) { const gchar *id; GVariant *maybe_serialized_icon; const gchar *title; const gchar *subtitle; const gchar *body; gint64 time; GVariantIter *action_iter; gboolean draws_attention; GVariant *serialized_icon = NULL; GSimpleAction *action; GIcon *app_icon; GVariant *actions = NULL; gchar *action_name; g_variant_get (message, "(&s@av&s&s&sxaa{sv}b)", &id, &maybe_serialized_icon, &title, &subtitle, &body, &time, &action_iter, &draws_attention); if (g_variant_n_children (maybe_serialized_icon) == 1) g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon); action_name = escape_action_name (id); action = g_simple_action_new (action_name, G_VARIANT_TYPE_BOOLEAN); g_object_set_qdata(G_OBJECT(action), message_action_draws_attention_quark(), GINT_TO_POINTER(draws_attention)); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_message_activated), app); g_action_map_add_action (G_ACTION_MAP(app->message_actions), G_ACTION (action)); { GVariant *entry; GSimpleActionGroup *action_group; GVariantBuilder actions_builder; g_variant_builder_init (&actions_builder, G_VARIANT_TYPE ("aa{sv}")); action_group = g_simple_action_group_new (); while ((entry = g_variant_iter_next_value (action_iter))) { const gchar *name; GSimpleAction *action; GVariant *label; const gchar *type = NULL; GVariant *hint; GVariantBuilder dict_builder; gchar *prefixed_name; gchar *escaped_name; if (!g_variant_lookup (entry, "name", "&s", &name)) { g_warning ("action dictionary for message '%s' is missing 'name' key", id); continue; } label = g_variant_lookup_value (entry, "label", G_VARIANT_TYPE_STRING); g_variant_lookup (entry, "parameter-type", "&g", &type); hint = g_variant_lookup_value (entry, "parameter-hint", NULL); escaped_name = escape_action_name (name); action = g_simple_action_new (escaped_name, type ? G_VARIANT_TYPE (type) : NULL); g_object_set_data_full (G_OBJECT (action), "message", g_strdup (id), g_free); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_sub_message_activated), app); g_action_map_add_action (G_ACTION_MAP(action_group), G_ACTION (action)); g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); prefixed_name = g_strjoin (".", app->id, "msg-actions", action_name, escaped_name, NULL); g_variant_builder_add (&dict_builder, "{sv}", "name", g_variant_new_string (prefixed_name)); if (label) { g_variant_builder_add (&dict_builder, "{sv}", "label", label); g_variant_unref (label); } if (type) g_variant_builder_add (&dict_builder, "{sv}", "parameter-type", g_variant_new_string (type)); if (hint) { g_variant_builder_add (&dict_builder, "{sv}", "parameter-hint", hint); g_variant_unref (hint); } g_variant_builder_add (&actions_builder, "a{sv}", &dict_builder); g_object_unref (action); g_variant_unref (entry); g_free (prefixed_name); g_free (escaped_name); } g_action_muxer_insert (app->message_sub_actions, action_name, G_ACTION_GROUP (action_group)); actions = g_variant_builder_end (&actions_builder); g_object_unref (action_group); } if (draws_attention && !app->draws_attention) { app->draws_attention = TRUE; im_application_list_update_root_action (app->list); } app_icon = get_symbolic_app_icon (app->info); g_signal_emit (app->list, signals[MESSAGE_ADDED], 0, app->id, app_icon, action_name, serialized_icon, title, subtitle, body, actions, time, draws_attention); g_free (action_name); g_variant_iter_free (action_iter); g_object_unref (action); if (serialized_icon) g_variant_unref (serialized_icon); g_variant_unref (maybe_serialized_icon); g_object_unref (app_icon); } static void im_application_list_messages_listed (GObject *source_object, GAsyncResult *result, gpointer user_data) { Application *app = user_data; GVariant *messages; GError *error = NULL; if (indicator_messages_application_call_list_messages_finish (app->proxy, &messages, result, &error)) { GVariantIter iter; GVariant *message; g_variant_iter_init (&iter, messages); while ((message = g_variant_iter_next_value (&iter))) { im_application_list_message_added (app, message); g_variant_unref (message); } g_variant_unref (messages); } else { g_warning ("could not fetch the list of messages: %s", error->message); g_error_free (error); } } static void im_application_list_unset_remote (Application *app) { gboolean was_running; was_running = app->proxy || app->cancellable; if (app->cancellable) { g_cancellable_cancel (app->cancellable); g_clear_object (&app->cancellable); } g_clear_object (&app->proxy); /* clear actions by creating a new action group and overriding it in * the muxer */ g_object_unref (app->source_actions); g_object_unref (app->message_actions); g_object_unref (app->message_sub_actions); app->source_actions = g_simple_action_group_new (); app->message_actions = g_simple_action_group_new (); app->message_sub_actions = g_action_muxer_new (); g_action_muxer_insert (app->muxer, "src", G_ACTION_GROUP (app->source_actions)); g_action_muxer_insert (app->muxer, "msg", G_ACTION_GROUP (app->message_actions)); g_action_muxer_insert (app->muxer, "msg-actions", G_ACTION_GROUP (app->message_sub_actions)); app->draws_attention = FALSE; im_application_list_update_root_action (app->list); g_action_group_change_action_state (G_ACTION_GROUP (app->muxer), "launch", g_variant_new_boolean (FALSE)); if (was_running) g_signal_emit (app->list, signals[APP_STOPPED], 0, app->id); } static void im_application_list_app_vanished (GDBusConnection *connection, const gchar *name, gpointer user_data) { Application *app = user_data; im_application_list_unset_remote (app); } static void im_application_list_proxy_created (GObject *source_object, GAsyncResult *result, gpointer user_data) { Application *app = user_data; GError *error = NULL; app->proxy = indicator_messages_application_proxy_new_finish (result, &error); if (!app->proxy) { if (error->code != G_IO_ERROR_CANCELLED) g_warning ("could not create application proxy: %s", error->message); g_error_free (error); return; } indicator_messages_application_call_list_sources (app->proxy, app->cancellable, im_application_list_sources_listed, app); indicator_messages_application_call_list_messages (app->proxy, app->cancellable, im_application_list_messages_listed, app); g_signal_connect_swapped (app->proxy, "source-added", G_CALLBACK (im_application_list_source_added), app); g_signal_connect_swapped (app->proxy, "source-changed", G_CALLBACK (im_application_list_source_changed), app); g_signal_connect_swapped (app->proxy, "source-removed", G_CALLBACK (im_application_list_source_removed), app); g_signal_connect_swapped (app->proxy, "message-added", G_CALLBACK (im_application_list_message_added), app); g_signal_connect_swapped (app->proxy, "message-removed", G_CALLBACK (im_application_list_message_removed), app); g_action_group_change_action_state (G_ACTION_GROUP (app->muxer), "launch", g_variant_new_boolean (TRUE)); g_bus_watch_name_on_connection (g_dbus_proxy_get_connection (G_DBUS_PROXY (app->proxy)), g_dbus_proxy_get_name (G_DBUS_PROXY (app->proxy)), G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, im_application_list_app_vanished, app, NULL); } void im_application_list_set_remote (ImApplicationList *list, const gchar *id, GDBusConnection *connection, const gchar *unique_bus_name, const gchar *object_path) { Application *app; g_return_if_fail (IM_IS_APPLICATION_LIST (list)); app = im_application_list_lookup (list, id); if (!app) { g_warning ("'%s' is not a registered application", id); return; } if (!connection && !unique_bus_name && !object_path) { im_application_list_unset_remote (app); return; } if (app->cancellable) { gchar *name_owner = NULL; if (app->proxy) name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (app->proxy)); if (g_strcmp0 (name_owner, unique_bus_name) != 0) { g_warning ("replacing '%s' at %s with %s", id, name_owner, unique_bus_name); im_application_list_unset_remote (app); } g_free (name_owner); } app->cancellable = g_cancellable_new (); indicator_messages_application_proxy_new (connection, G_DBUS_PROXY_FLAGS_NONE, unique_bus_name, object_path, app->cancellable, im_application_list_proxy_created, app); } GActionGroup * im_application_list_get_action_group (ImApplicationList *list) { g_return_val_if_fail (IM_IS_APPLICATION_LIST (list), NULL); return G_ACTION_GROUP (list->muxer); } GList * im_application_list_get_applications (ImApplicationList *list) { g_return_val_if_fail (IM_IS_APPLICATION_LIST (list), NULL); return g_hash_table_get_keys (list->applications); } GDesktopAppInfo * im_application_list_get_application (ImApplicationList *list, const gchar *id) { Application *app; g_return_val_if_fail (IM_IS_APPLICATION_LIST (list), NULL); app = g_hash_table_lookup (list->applications, id); return app ? app->info : NULL; } static void status_activated (GSimpleAction * action, GVariant * param, gpointer user_data) { g_return_if_fail (IM_IS_APPLICATION_LIST(user_data)); ImApplicationList * list = IM_APPLICATION_LIST(user_data); const gchar * status = g_variant_get_string(param, NULL); g_simple_action_set_state(action, param); GList * appshash = g_hash_table_get_keys(list->app_status); GList * appsfree = g_list_copy_deep(appshash, (GCopyFunc)g_strdup, NULL); GList * app; for (app = appsfree; app != NULL; app = g_list_next(app)) { g_hash_table_insert(list->app_status, app->data, g_strdup(status)); } g_list_free(appshash); g_list_free(appsfree); g_signal_emit (list, signals[STATUS_SET], 0, status); im_application_list_update_root_action(list); return; } #define STATUS_ID_OFFLINE (G_N_ELEMENTS(status_ids) - 1) static const gchar *status_ids[] = { "available", "away", "busy", "invisible", "offline" }; static guint status2val (const gchar * string) { if (string == NULL) return STATUS_ID_OFFLINE; guint i; for (i = 0; i < G_N_ELEMENTS(status_ids); i++) { if (g_strcmp0(status_ids[i], string) == 0) { break; } } if (i > STATUS_ID_OFFLINE) i = STATUS_ID_OFFLINE; return i; } void im_application_list_set_status (ImApplicationList * list, const gchar * id, const gchar *status) { g_return_if_fail (IM_IS_APPLICATION_LIST (list)); g_hash_table_insert(list->app_status, im_application_list_canonical_id(id), g_strdup(status)); guint final_status = STATUS_ID_OFFLINE; GList * statuses = g_hash_table_get_values(list->app_status); GList * statusentry; for (statusentry = statuses; statusentry != NULL; statusentry = g_list_next(statusentry)) { guint statusval = status2val((gchar *)statusentry->data); final_status = MIN(final_status, statusval); } g_list_free(statuses); g_simple_action_set_state(list->statusaction, g_variant_new_string(status_ids[final_status])); im_application_list_update_root_action(list); return; }