/* An indicator to show information that is in messaging applications that the user is using. Copyright 2009 Canonical Ltd. Authors: Ted Gould 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include "im-menu-item.h" #include "dbus-data.h" enum { TIME_CHANGED, ATTENTION_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; typedef struct _ImMenuItemPrivate ImMenuItemPrivate; struct _ImMenuItemPrivate { IndicateListener * listener; IndicateListenerServer * server; IndicateListenerIndicator * indicator; glong creation_seconds; glong seconds; gchar * count; gulong indicator_changed; gboolean attention; gboolean show; guint time_update_min; }; #define IM_MENU_ITEM_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), IM_MENU_ITEM_TYPE, ImMenuItemPrivate)) /* Prototypes */ static void im_menu_item_class_init (ImMenuItemClass *klass); static void im_menu_item_init (ImMenuItem *self); static void im_menu_item_dispose (GObject *object); static void im_menu_item_finalize (GObject *object); static void sender_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const gchar * propertydata, gpointer data); static void time_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const GTimeVal * propertydata, gpointer data); static void icon_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const gchar * propertydata, gpointer data); static void activate_cb (ImMenuItem * self, guint timestamp, gpointer data); static void indicator_modified_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, ImMenuItem * self); G_DEFINE_TYPE (ImMenuItem, im_menu_item, DBUSMENU_TYPE_MENUITEM); static void im_menu_item_class_init (ImMenuItemClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); g_type_class_add_private (klass, sizeof (ImMenuItemPrivate)); object_class->dispose = im_menu_item_dispose; object_class->finalize = im_menu_item_finalize; signals[TIME_CHANGED] = g_signal_new(IM_MENU_ITEM_SIGNAL_TIME_CHANGED, G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ImMenuItemClass, time_changed), NULL, NULL, g_cclosure_marshal_VOID__LONG, G_TYPE_NONE, 1, G_TYPE_LONG); signals[ATTENTION_CHANGED] = g_signal_new(IM_MENU_ITEM_SIGNAL_ATTENTION_CHANGED, G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ImMenuItemClass, attention_changed), NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN); return; } static void im_menu_item_init (ImMenuItem *self) { g_debug("Building new IM Menu Item"); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); /* Set the variables to NULL, but they should be configured further down the line. */ priv->listener = NULL; priv->server = NULL; priv->indicator = NULL; /* A sane default, but look below */ priv->creation_seconds = 0; priv->seconds = 0; /* Set the seconds to be the time when the item was created incase we're not given a better time. */ GTimeVal current_time; g_get_current_time(¤t_time); priv->creation_seconds = current_time.tv_sec; return; } static void im_menu_item_dispose (GObject *object) { G_OBJECT_CLASS (im_menu_item_parent_class)->dispose (object); ImMenuItem * self = IM_MENU_ITEM(object); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); if (priv->time_update_min != 0) { g_source_remove(priv->time_update_min); } g_signal_handler_disconnect(priv->listener, priv->indicator_changed); priv->indicator_changed = 0; return; } static void im_menu_item_finalize (GObject *object) { G_OBJECT_CLASS (im_menu_item_parent_class)->finalize (object); } /* Call back for getting icon data. It just passes it along to the indicator so that it can visualize it. Not our problem. */ static void icon_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const gchar * propertydata, gpointer data) { dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(data), INDICATOR_MENUITEM_PROP_ICON, propertydata); return; } /* This function takes the time and turns it into the appropriate string to put on the right side of the menu item. Of course it doesn't do that if there is a count set. If there's a count then it gets that space. */ static void update_time (ImMenuItem * self) { ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); /* Count has been set, so it takes priority. */ if (priv->count != NULL) { return; } /* Seconds hasn't been set, so we just want to keep the time area blank. */ if (priv->seconds == 0) { dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(self), INDICATOR_MENUITEM_PROP_RIGHT, ""); return; } gchar * timestring = NULL; GTimeVal current_time; g_get_current_time(¤t_time); guint elapsed_seconds = current_time.tv_sec - priv->seconds; guint elapsed_minutes = elapsed_seconds / 60; if (elapsed_seconds % 60 > 55) { /* We're using fuzzy timers, so we need fuzzy comparisons */ elapsed_minutes += 1; } if (elapsed_minutes < 60) { /* TRANSLATORS: This string is used to represent the number of minutes since an IM has occured. It is in the right column of a menu so being brief is desirable, but one character is not a requirement. */ timestring = g_strdup_printf(ngettext("%d m", "%d m", elapsed_minutes), elapsed_minutes); } else { guint elapsed_hours = elapsed_minutes / 60; if (elapsed_minutes % 60 > 55) { /* We're using fuzzy timers, so we need fuzzy comparisons */ elapsed_hours += 1; } /* TRANSLATORS: This string is used to represent the number of hours since an IM has occured. It is in the right column of a menu so being brief is desirable, but one character is not a requirement. */ timestring = g_strdup_printf(ngettext("%d h", "%d h", elapsed_hours), elapsed_hours); } if (timestring != NULL) { dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(self), INDICATOR_MENUITEM_PROP_RIGHT, timestring); g_free(timestring); } return; } /* This is a wrapper around update_time that matches the prototype needed to make this a timer callback. Silly. */ static gboolean time_update_cb (gpointer data) { ImMenuItem * self = IM_MENU_ITEM(data); update_time(self); return TRUE; } /* Yet another time function. This one takes the time as formated as we get it from libindicate and turns it into the seconds that we're looking for. It should only be called once at the init with a new indicator and again when the value changes. */ static void time_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const GTimeVal * propertydata, gpointer data) { g_debug("Got Time info"); ImMenuItem * self = IM_MENU_ITEM(data); if (self == NULL) { g_error("Menu Item callback called without a menu item"); return; } if (property == NULL || g_strcmp0(property, "time")) { g_warning("Time callback called without being sent the time."); return; } ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); priv->seconds = propertydata->tv_sec; update_time(self); if (priv->time_update_min == 0) { priv->time_update_min = g_timeout_add_seconds(60, time_update_cb, self); } g_signal_emit(G_OBJECT(self), signals[TIME_CHANGED], 0, priv->seconds, TRUE); return; } /* Returns a newly allocated string which is 'str' with all occurences of * consecutive whitespace collapsed into single space character. */ static gchar * collapse_whitespace (const gchar *str) { GString *result; gboolean in_space = FALSE; if (!str) return NULL; result = g_string_sized_new (strlen (str)); while (*str) { gunichar c = g_utf8_get_char_validated (str, -1); if (c < 0) break; if (!g_unichar_isspace (c)) { g_string_append_unichar (result, c); in_space = FALSE; } else if (!in_space) { g_string_append_c (result, ' '); in_space = TRUE; } str = g_utf8_next_char (str); } return g_string_free (result, FALSE); } /* Callback from libindicate that is for getting the sender information on a particular indicator. */ static void sender_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const gchar * propertydata, gpointer data) { gchar *label; g_debug("Got Sender Information: %s", propertydata); ImMenuItem * self = IM_MENU_ITEM(data); /* Our data should be right */ g_return_if_fail(self != NULL); /* We should have a property name */ g_return_if_fail(property != NULL); /* The Property should be sender or name */ g_return_if_fail(!g_strcmp0(property, "sender") || !g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_NAME)); /* We might get the sender variable returning a null string as it doesn't exist on newer clients but we don't want to listen to that. */ if (!g_strcmp0(property, "sender") && propertydata[0] == '\0') { return; } label = collapse_whitespace (propertydata); dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(self), INDICATOR_MENUITEM_PROP_LABEL, label); g_free (label); return; } /* Callback saying that the count is updated, we need to either put that on the menu item or just remove it if the count is gone. If that's the case we can update time. */ static void count_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, const gchar * propertydata, gpointer data) { g_debug("Got Count Information"); ImMenuItem * self = IM_MENU_ITEM(data); /* Our data should be right */ g_return_if_fail(self != NULL); /* We should have a property name */ g_return_if_fail(property != NULL); /* The Property should be count */ g_return_if_fail(!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_COUNT)); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); if (propertydata == NULL || propertydata[0] == '\0') { /* The count is either being unset or it was never set in the first place. */ if (priv->count != NULL) { g_free(priv->count); priv->count = NULL; update_time(self); dbusmenu_menuitem_property_set_bool (DBUSMENU_MENUITEM (self), INDICATOR_MENUITEM_PROP_RIGHT_IS_LOZENGE, FALSE); } return; } if (priv->count != NULL) { g_free(priv->count); } priv->count = g_strdup_printf("%s", propertydata); dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(self), INDICATOR_MENUITEM_PROP_RIGHT, priv->count); dbusmenu_menuitem_property_set_bool (DBUSMENU_MENUITEM (self), INDICATOR_MENUITEM_PROP_RIGHT_IS_LOZENGE, TRUE); return; } /* This is getting the attention variable that's looking at whether this indicator should be calling for attention or not. If we are, we need to signal that. */ static void attention_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, GVariant * propertydata, gpointer data) { g_debug("Got Attention Information"); ImMenuItem * self = IM_MENU_ITEM(data); /* Our data should be right */ g_return_if_fail(self != NULL); /* We should have a property name */ g_return_if_fail(property != NULL); /* The Property should be count */ g_return_if_fail(!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION)); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); gboolean wantit; if (g_variant_is_of_type(propertydata, G_VARIANT_TYPE_BOOLEAN)) { wantit = g_variant_get_boolean(propertydata); } else if (g_variant_is_of_type(propertydata, G_VARIANT_TYPE_STRING)) { const gchar * propstring = g_variant_get_string(propertydata, NULL); if (propstring == NULL || propstring[0] == '\0' || !g_strcmp0(propstring, "false")) { wantit = FALSE; } else { wantit = TRUE; } } else { g_warning("Got property '%s' of an unknown type.", property); return; } if (priv->attention != wantit) { priv->attention = wantit; g_signal_emit(G_OBJECT(self), signals[ATTENTION_CHANGED], 0, wantit, TRUE); } return; } /* Callback when the item gets clicked on from the Messaging Menu */ static void activate_cb (ImMenuItem * self, guint timestamp, gpointer data) { ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); indicate_listener_display(priv->listener, priv->server, priv->indicator, timestamp); } /* Callback when a property gets modified. It figures out which one got modified and notifies the appropriate person. */ void indicator_modified_cb (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator, gchar * property, ImMenuItem * self) { ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); /* Not meant for us */ if (INDICATE_LISTENER_INDICATOR_ID(indicator) != INDICATE_LISTENER_INDICATOR_ID(priv->indicator)) return; if (server != priv->server) return; /* Determine which property has been changed and request the value go to the appropriate callback. */ if (!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_NAME)) { indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_NAME, sender_cb, self); } else if (!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_TIME)) { indicate_listener_get_property_time(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_TIME, time_cb, self); } else if (!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_ICON)) { indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_ICON, icon_cb, self); } else if (!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_COUNT)) { indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_COUNT, count_cb, self); } else if (!g_strcmp0(property, INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION)) { indicate_listener_get_property_variant(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION, attention_cb, self); } else if (!g_strcmp0(property, "sender")) { /* This is a compatibility string with v1 and should be removed */ g_debug("Indicator is using 'sender' property which is a v1 string."); indicate_listener_get_property(listener, server, indicator, "sender", sender_cb, self); } return; } ImMenuItem * im_menu_item_new (IndicateListener * listener, IndicateListenerServer * server, IndicateListenerIndicator * indicator) { ImMenuItem * self = g_object_new(IM_MENU_ITEM_TYPE, NULL); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(self); priv->listener = listener; priv->server = server; priv->indicator = indicator; priv->count = NULL; priv->time_update_min = 0; priv->attention = FALSE; priv->show = TRUE; dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(self), DBUSMENU_MENUITEM_PROP_TYPE, INDICATOR_MENUITEM_TYPE); indicate_listener_displayed(listener, server, indicator, TRUE); indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_NAME, sender_cb, self); indicate_listener_get_property_time(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_TIME, time_cb, self); indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_ICON, icon_cb, self); indicate_listener_get_property(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_COUNT, count_cb, self); indicate_listener_get_property_variant(listener, server, indicator, INDICATE_INDICATOR_MESSAGES_PROP_ATTENTION, attention_cb, self); indicate_listener_get_property(listener, server, indicator, "sender", sender_cb, self); g_signal_connect(G_OBJECT(self), DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED, G_CALLBACK(activate_cb), NULL); priv->indicator_changed = g_signal_connect(G_OBJECT(listener), INDICATE_LISTENER_SIGNAL_INDICATOR_MODIFIED, G_CALLBACK(indicator_modified_cb), self); return self; } /* Gets the number of seconds for the creator of this item. */ glong im_menu_item_get_seconds (ImMenuItem * menuitem) { g_return_val_if_fail(IS_IM_MENU_ITEM(menuitem), 0); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(menuitem); if (priv->seconds == 0) { return priv->creation_seconds; } else { return priv->seconds; } } /* Gets whether or not this indicator item is asking for attention or not. */ gboolean im_menu_item_get_attention (ImMenuItem * menuitem) { g_return_val_if_fail(IS_IM_MENU_ITEM(menuitem), FALSE); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(menuitem); return priv->attention; } /* This takes care of items that need to be hidden, this is usually because they go over the count of allowed indicators. Which is more than a little bit silly. We shouldn't do that. But we need to enforce it to save users against bad apps. */ void im_menu_item_show (ImMenuItem * menuitem, gboolean show) { g_return_if_fail(IS_IM_MENU_ITEM(menuitem)); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(menuitem); if (priv->show == show) { return; } priv->show = show; /* Tell the app what we're doing to it. If it's being punished it needs to know about it. */ indicate_listener_displayed(priv->listener, priv->server, priv->indicator, priv->show); if (priv->attention) { /* If we were asking for attention we can ask for it again if we're being shown, otherwise no. */ g_signal_emit(G_OBJECT(menuitem), signals[ATTENTION_CHANGED], 0, priv->show, TRUE); } dbusmenu_menuitem_property_set(DBUSMENU_MENUITEM(menuitem), DBUSMENU_MENUITEM_PROP_VISIBLE, priv->show ? "true" : "false"); return; } /* Check to see if this item is shown. Accessor for the internal variable. */ gboolean im_menu_item_shown (ImMenuItem * menuitem) { g_return_val_if_fail(IS_IM_MENU_ITEM(menuitem), FALSE); ImMenuItemPrivate * priv = IM_MENU_ITEM_GET_PRIVATE(menuitem); return priv->show; }