aboutsummaryrefslogtreecommitdiffstats
path: root/trunk/libempathy-gtk/empathy-ui-utils.c
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/libempathy-gtk/empathy-ui-utils.c')
-rw-r--r--trunk/libempathy-gtk/empathy-ui-utils.c1423
1 files changed, 1423 insertions, 0 deletions
diff --git a/trunk/libempathy-gtk/empathy-ui-utils.c b/trunk/libempathy-gtk/empathy-ui-utils.c
new file mode 100644
index 000000000..bc4d7597b
--- /dev/null
+++ b/trunk/libempathy-gtk/empathy-ui-utils.c
@@ -0,0 +1,1423 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2002-2007 Imendio AB
+ * Copyright (C) 2007-2008 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Authors: Mikael Hallendal <micke@imendio.com>
+ * Richard Hult <richard@imendio.com>
+ * Martyn Russell <martyn@imendio.com>
+ * Xavier Claessens <xclaesse@gmail.com>
+ *
+ * Part of this file is copied from GtkSourceView (gtksourceiter.c):
+ * Paolo Maggi
+ * Jeroen Zwartepoorte
+ */
+
+#include <config.h>
+
+#include <string.h>
+#include <X11/Xatom.h>
+#include <gdk/gdkx.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <gio/gio.h>
+#include <glade/glade.h>
+
+#include <libmissioncontrol/mc-profile.h>
+
+#include "empathy-ui-utils.h"
+#include "empathy-images.h"
+
+#define DEBUG_FLAG EMPATHY_DEBUG_OTHER
+#include <libempathy/empathy-debug.h>
+
+struct SizeData {
+ gint width;
+ gint height;
+ gboolean preserve_aspect_ratio;
+};
+
+static GladeXML *
+get_glade_file (const gchar *filename,
+ const gchar *root,
+ const gchar *domain,
+ const gchar *first_required_widget,
+ va_list args)
+{
+ GladeXML *gui;
+ const char *name;
+ GtkWidget **widget_ptr;
+
+ DEBUG ("Loading glade file %s", filename);
+
+ gui = glade_xml_new (filename, root, domain);
+
+ if (!gui) {
+ g_warning ("Couldn't find necessary glade file '%s'", filename);
+ }
+
+ for (name = first_required_widget; name; name = va_arg (args, char *)) {
+ widget_ptr = va_arg (args, void *);
+
+ *widget_ptr = glade_xml_get_widget (gui, name);
+
+ if (!*widget_ptr) {
+ g_warning ("Glade file '%s' is missing widget '%s'.",
+ filename, name);
+ continue;
+ }
+ }
+
+ return gui;
+}
+
+void
+empathy_glade_get_file_simple (const gchar *filename,
+ const gchar *root,
+ const gchar *domain,
+ const gchar *first_required_widget, ...)
+{
+ va_list args;
+ GladeXML *gui;
+
+ va_start (args, first_required_widget);
+
+ gui = get_glade_file (filename,
+ root,
+ domain,
+ first_required_widget,
+ args);
+
+ va_end (args);
+
+ if (gui) {
+ g_object_unref (gui);
+ }
+}
+
+GladeXML *
+empathy_glade_get_file (const gchar *filename,
+ const gchar *root,
+ const gchar *domain,
+ const gchar *first_required_widget, ...)
+{
+ va_list args;
+ GladeXML *gui;
+
+ va_start (args, first_required_widget);
+
+ gui = get_glade_file (filename,
+ root,
+ domain,
+ first_required_widget,
+ args);
+
+ va_end (args);
+
+ if (!gui) {
+ return NULL;
+ }
+
+ return gui;
+}
+
+void
+empathy_glade_connect (GladeXML *gui,
+ gpointer user_data,
+ gchar *first_widget, ...)
+{
+ va_list args;
+ const gchar *name;
+ const gchar *signal;
+ GtkWidget *widget;
+ gpointer *callback;
+
+ va_start (args, first_widget);
+
+ for (name = first_widget; name; name = va_arg (args, char *)) {
+ signal = va_arg (args, void *);
+ callback = va_arg (args, void *);
+
+ widget = glade_xml_get_widget (gui, name);
+ if (!widget) {
+ g_warning ("Glade file is missing widget '%s', aborting",
+ name);
+ continue;
+ }
+
+ g_signal_connect (widget,
+ signal,
+ G_CALLBACK (callback),
+ user_data);
+ }
+
+ va_end (args);
+}
+
+void
+empathy_glade_setup_size_group (GladeXML *gui,
+ GtkSizeGroupMode mode,
+ gchar *first_widget, ...)
+{
+ va_list args;
+ GtkWidget *widget;
+ GtkSizeGroup *size_group;
+ const gchar *name;
+
+ va_start (args, first_widget);
+
+ size_group = gtk_size_group_new (mode);
+
+ for (name = first_widget; name; name = va_arg (args, char *)) {
+ widget = glade_xml_get_widget (gui, name);
+ if (!widget) {
+ g_warning ("Glade file is missing widget '%s'", name);
+ continue;
+ }
+
+ gtk_size_group_add_widget (size_group, widget);
+ }
+
+ g_object_unref (size_group);
+
+ va_end (args);
+}
+
+const gchar *
+empathy_icon_name_from_account (McAccount *account)
+{
+ McProfile *profile;
+
+ profile = mc_account_get_profile (account);
+
+ return mc_profile_get_icon_name (profile);
+}
+
+const gchar *
+empathy_icon_name_for_presence (McPresence presence)
+{
+ switch (presence) {
+ case MC_PRESENCE_AVAILABLE:
+ return EMPATHY_IMAGE_AVAILABLE;
+ case MC_PRESENCE_DO_NOT_DISTURB:
+ return EMPATHY_IMAGE_BUSY;
+ case MC_PRESENCE_AWAY:
+ return EMPATHY_IMAGE_AWAY;
+ case MC_PRESENCE_EXTENDED_AWAY:
+ return EMPATHY_IMAGE_EXT_AWAY;
+ case MC_PRESENCE_HIDDEN:
+ return EMPATHY_IMAGE_HIDDEN;
+ case MC_PRESENCE_OFFLINE:
+ case MC_PRESENCE_UNSET:
+ return EMPATHY_IMAGE_OFFLINE;
+ default:
+ g_assert_not_reached ();
+ }
+
+ return NULL;
+}
+
+const gchar *
+empathy_icon_name_for_contact (EmpathyContact *contact)
+{
+ McPresence presence;
+
+ g_return_val_if_fail (EMPATHY_IS_CONTACT (contact),
+ EMPATHY_IMAGE_OFFLINE);
+
+ presence = empathy_contact_get_presence (contact);
+ return empathy_icon_name_for_presence (presence);
+}
+
+GdkPixbuf *
+empathy_pixbuf_from_data (gchar *data,
+ gsize data_size)
+{
+ GdkPixbufLoader *loader;
+ GdkPixbuf *pixbuf = NULL;
+ GError *error = NULL;
+
+ if (!data) {
+ return NULL;
+ }
+
+ loader = gdk_pixbuf_loader_new ();
+ if (!gdk_pixbuf_loader_write (loader, data, data_size, &error)) {
+ DEBUG ("Failed to write to pixbuf loader: %s",
+ error ? error->message : "No error given");
+ g_clear_error (&error);
+ g_object_unref (loader);
+ return NULL;
+ }
+ if (!gdk_pixbuf_loader_close (loader, &error)) {
+ DEBUG ("Failed to close pixbuf loader: %s",
+ error ? error->message : "No error given");
+ g_clear_error (&error);
+ g_object_unref (loader);
+ return NULL;
+ }
+
+ pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
+ if (pixbuf) {
+ g_object_ref (pixbuf);
+ }
+
+ g_object_unref (loader);
+
+ return pixbuf;
+}
+
+static void
+pixbuf_from_avatar_size_prepared_cb (GdkPixbufLoader *loader,
+ int width,
+ int height,
+ struct SizeData *data)
+{
+ g_return_if_fail (width > 0 && height > 0);
+
+ if (data->preserve_aspect_ratio && (data->width > 0 || data->height > 0)) {
+ if (width < data->width && height < data->height) {
+ width = width;
+ height = height;
+ }
+
+ if (data->width < 0) {
+ width = width * (double) data->height / (gdouble) height;
+ height = data->height;
+ } else if (data->height < 0) {
+ height = height * (double) data->width / (double) width;
+ width = data->width;
+ } else if ((double) height * (double) data->width >
+ (double) width * (double) data->height) {
+ width = 0.5 + (double) width * (double) data->height / (double) height;
+ height = data->height;
+ } else {
+ height = 0.5 + (double) height * (double) data->width / (double) width;
+ width = data->width;
+ }
+ } else {
+ if (data->width > 0) {
+ width = data->width;
+ }
+
+ if (data->height > 0) {
+ height = data->height;
+ }
+ }
+
+ gdk_pixbuf_loader_set_size (loader, width, height);
+}
+
+static void
+empathy_avatar_pixbuf_roundify (GdkPixbuf *pixbuf)
+{
+ gint width, height, rowstride;
+ guchar *pixels;
+
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+ rowstride = gdk_pixbuf_get_rowstride (pixbuf);
+ pixels = gdk_pixbuf_get_pixels (pixbuf);
+
+ if (width < 6 || height < 6) {
+ return;
+ }
+
+ /* Top left */
+ pixels[3] = 0;
+ pixels[7] = 0x80;
+ pixels[11] = 0xC0;
+ pixels[rowstride + 3] = 0x80;
+ pixels[rowstride * 2 + 3] = 0xC0;
+
+ /* Top right */
+ pixels[width * 4 - 1] = 0;
+ pixels[width * 4 - 5] = 0x80;
+ pixels[width * 4 - 9] = 0xC0;
+ pixels[rowstride + (width * 4) - 1] = 0x80;
+ pixels[(2 * rowstride) + (width * 4) - 1] = 0xC0;
+
+ /* Bottom left */
+ pixels[(height - 1) * rowstride + 3] = 0;
+ pixels[(height - 1) * rowstride + 7] = 0x80;
+ pixels[(height - 1) * rowstride + 11] = 0xC0;
+ pixels[(height - 2) * rowstride + 3] = 0x80;
+ pixels[(height - 3) * rowstride + 3] = 0xC0;
+
+ /* Bottom right */
+ pixels[height * rowstride - 1] = 0;
+ pixels[(height - 1) * rowstride - 1] = 0x80;
+ pixels[(height - 2) * rowstride - 1] = 0xC0;
+ pixels[height * rowstride - 5] = 0x80;
+ pixels[height * rowstride - 9] = 0xC0;
+}
+
+static gboolean
+empathy_gdk_pixbuf_is_opaque (GdkPixbuf *pixbuf)
+{
+ gint width, height, rowstride, i;
+ guchar *pixels;
+ guchar *row;
+
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+ rowstride = gdk_pixbuf_get_rowstride (pixbuf);
+ pixels = gdk_pixbuf_get_pixels (pixbuf);
+
+ row = pixels;
+ for (i = 3; i < rowstride; i+=4) {
+ if (row[i] < 0xfe) {
+ return FALSE;
+ }
+ }
+
+ for (i = 1; i < height - 1; i++) {
+ row = pixels + (i*rowstride);
+ if (row[3] < 0xfe || row[rowstride-1] < 0xfe) {
+ return FALSE;
+ }
+ }
+
+ row = pixels + ((height-1) * rowstride);
+ for (i = 3; i < rowstride; i+=4) {
+ if (row[i] < 0xfe) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+GdkPixbuf *
+empathy_pixbuf_from_avatar_scaled (EmpathyAvatar *avatar,
+ gint width,
+ gint height)
+{
+ GdkPixbuf *pixbuf;
+ GdkPixbufLoader *loader;
+ struct SizeData data;
+ GError *error = NULL;
+
+ if (!avatar) {
+ return NULL;
+ }
+
+ data.width = width;
+ data.height = height;
+ data.preserve_aspect_ratio = TRUE;
+
+ loader = gdk_pixbuf_loader_new ();
+
+ g_signal_connect (loader, "size-prepared",
+ G_CALLBACK (pixbuf_from_avatar_size_prepared_cb),
+ &data);
+
+ if (!gdk_pixbuf_loader_write (loader, avatar->data, avatar->len, &error)) {
+ g_warning ("Couldn't write avatar image:%p with "
+ "length:%" G_GSIZE_FORMAT " to pixbuf loader: %s",
+ avatar->data, avatar->len, error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ gdk_pixbuf_loader_close (loader, NULL);
+
+ pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
+ if (!gdk_pixbuf_get_has_alpha (pixbuf)) {
+ GdkPixbuf *rounded_pixbuf;
+
+ rounded_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8,
+ gdk_pixbuf_get_width (pixbuf),
+ gdk_pixbuf_get_height (pixbuf));
+ gdk_pixbuf_copy_area (pixbuf, 0, 0,
+ gdk_pixbuf_get_width (pixbuf),
+ gdk_pixbuf_get_height (pixbuf),
+ rounded_pixbuf,
+ 0, 0);
+ pixbuf = rounded_pixbuf;
+ } else {
+ g_object_ref (pixbuf);
+ }
+
+ if (empathy_gdk_pixbuf_is_opaque (pixbuf)) {
+ empathy_avatar_pixbuf_roundify (pixbuf);
+ }
+
+ g_object_unref (loader);
+
+ return pixbuf;
+}
+
+GdkPixbuf *
+empathy_pixbuf_avatar_from_contact_scaled (EmpathyContact *contact,
+ gint width,
+ gint height)
+{
+ EmpathyAvatar *avatar;
+
+ g_return_val_if_fail (EMPATHY_IS_CONTACT (contact), NULL);
+
+ avatar = empathy_contact_get_avatar (contact);
+
+ return empathy_pixbuf_from_avatar_scaled (avatar, width, height);
+}
+
+GdkPixbuf *
+empathy_pixbuf_scale_down_if_necessary (GdkPixbuf *pixbuf, gint max_size)
+{
+ gint width, height;
+ gdouble factor;
+
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+
+ if (width > max_size || height > max_size) {
+ factor = (gdouble) max_size / MAX (width, height);
+
+ width = width * factor;
+ height = height * factor;
+
+ return gdk_pixbuf_scale_simple (pixbuf,
+ width, height,
+ GDK_INTERP_HYPER);
+ }
+
+ return g_object_ref (pixbuf);
+}
+
+GdkPixbuf *
+empathy_pixbuf_from_icon_name (const gchar *icon_name,
+ GtkIconSize icon_size)
+{
+ GtkIconTheme *theme;
+ GdkPixbuf *pixbuf = NULL;
+ GError *error = NULL;
+ gint w, h;
+ gint size = 48;
+
+ if (!icon_name) {
+ return NULL;
+ }
+
+ theme = gtk_icon_theme_get_default ();
+
+ if (gtk_icon_size_lookup (icon_size, &w, &h)) {
+ size = (w + h) / 2;
+ }
+
+ pixbuf = gtk_icon_theme_load_icon (theme,
+ icon_name,
+ size,
+ 0,
+ &error);
+ if (error) {
+ DEBUG ("Error loading icon: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ return pixbuf;
+}
+
+/* Stolen from GtkSourceView, hence the weird intendation. Please keep it like
+ * that to make it easier to apply changes from the original code.
+ */
+#define GTK_TEXT_UNKNOWN_CHAR 0xFFFC
+
+/* this function acts like g_utf8_offset_to_pointer() except that if it finds a
+ * decomposable character it consumes the decomposition length from the given
+ * offset. So it's useful when the offset was calculated for the normalized
+ * version of str, but we need a pointer to str itself. */
+static const gchar *
+pointer_from_offset_skipping_decomp (const gchar *str, gint offset)
+{
+ gchar *casefold, *normal;
+ const gchar *p, *q;
+
+ p = str;
+ while (offset > 0)
+ {
+ q = g_utf8_next_char (p);
+ casefold = g_utf8_casefold (p, q - p);
+ normal = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ offset -= g_utf8_strlen (normal, -1);
+ g_free (casefold);
+ g_free (normal);
+ p = q;
+ }
+ return p;
+}
+
+static const gchar *
+g_utf8_strcasestr (const gchar *haystack, const gchar *needle)
+{
+ gsize needle_len;
+ gsize haystack_len;
+ const gchar *ret = NULL;
+ gchar *p;
+ gchar *casefold;
+ gchar *caseless_haystack;
+ gint i;
+
+ g_return_val_if_fail (haystack != NULL, NULL);
+ g_return_val_if_fail (needle != NULL, NULL);
+
+ casefold = g_utf8_casefold (haystack, -1);
+ caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+
+ needle_len = g_utf8_strlen (needle, -1);
+ haystack_len = g_utf8_strlen (caseless_haystack, -1);
+
+ if (needle_len == 0)
+ {
+ ret = (gchar *)haystack;
+ goto finally_1;
+ }
+
+ if (haystack_len < needle_len)
+ {
+ ret = NULL;
+ goto finally_1;
+ }
+
+ p = (gchar*)caseless_haystack;
+ needle_len = strlen (needle);
+ i = 0;
+
+ while (*p)
+ {
+ if ((strncmp (p, needle, needle_len) == 0))
+ {
+ ret = pointer_from_offset_skipping_decomp (haystack, i);
+ goto finally_1;
+ }
+
+ p = g_utf8_next_char (p);
+ i++;
+ }
+
+finally_1:
+ g_free (caseless_haystack);
+
+ return ret;
+}
+
+static gboolean
+g_utf8_caselessnmatch (const char *s1, const char *s2,
+ gssize n1, gssize n2)
+{
+ gchar *casefold;
+ gchar *normalized_s1;
+ gchar *normalized_s2;
+ gint len_s1;
+ gint len_s2;
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (s1 != NULL, FALSE);
+ g_return_val_if_fail (s2 != NULL, FALSE);
+ g_return_val_if_fail (n1 > 0, FALSE);
+ g_return_val_if_fail (n2 > 0, FALSE);
+
+ casefold = g_utf8_casefold (s1, n1);
+ normalized_s1 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+
+ casefold = g_utf8_casefold (s2, n2);
+ normalized_s2 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+
+ len_s1 = strlen (normalized_s1);
+ len_s2 = strlen (normalized_s2);
+
+ if (len_s1 < len_s2)
+ goto finally_2;
+
+ ret = (strncmp (normalized_s1, normalized_s2, len_s2) == 0);
+
+finally_2:
+ g_free (normalized_s1);
+ g_free (normalized_s2);
+
+ return ret;
+}
+
+static void
+forward_chars_with_skipping (GtkTextIter *iter,
+ gint count,
+ gboolean skip_invisible,
+ gboolean skip_nontext,
+ gboolean skip_decomp)
+{
+ gint i;
+
+ g_return_if_fail (count >= 0);
+
+ i = count;
+
+ while (i > 0)
+ {
+ gboolean ignored = FALSE;
+
+ /* minimal workaround to avoid the infinite loop of bug #168247.
+ * It doesn't fix the problemjust the symptom...
+ */
+ if (gtk_text_iter_is_end (iter))
+ return;
+
+ if (skip_nontext && gtk_text_iter_get_char (iter) == GTK_TEXT_UNKNOWN_CHAR)
+ ignored = TRUE;
+
+ if (!ignored && skip_invisible &&
+ /* _gtk_text_btree_char_is_invisible (iter)*/ FALSE)
+ ignored = TRUE;
+
+ if (!ignored && skip_decomp)
+ {
+ /* being UTF8 correct sucks; this accounts for extra
+ offsets coming from canonical decompositions of
+ UTF8 characters (e.g. accented characters) which
+ g_utf8_normalize() performs */
+ gchar *normal;
+ gchar buffer[6];
+ gint buffer_len;
+
+ buffer_len = g_unichar_to_utf8 (gtk_text_iter_get_char (iter), buffer);
+ normal = g_utf8_normalize (buffer, buffer_len, G_NORMALIZE_NFD);
+ i -= (g_utf8_strlen (normal, -1) - 1);
+ g_free (normal);
+ }
+
+ gtk_text_iter_forward_char (iter);
+
+ if (!ignored)
+ --i;
+ }
+}
+
+static gboolean
+lines_match (const GtkTextIter *start,
+ const gchar **lines,
+ gboolean visible_only,
+ gboolean slice,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter next;
+ gchar *line_text;
+ const gchar *found;
+ gint offset;
+
+ if (*lines == NULL || **lines == '\0')
+ {
+ if (match_start)
+ *match_start = *start;
+ if (match_end)
+ *match_end = *start;
+ return TRUE;
+ }
+
+ next = *start;
+ gtk_text_iter_forward_line (&next);
+
+ /* No more text in buffer, but *lines is nonempty */
+ if (gtk_text_iter_equal (start, &next))
+ return FALSE;
+
+ if (slice)
+ {
+ if (visible_only)
+ line_text = gtk_text_iter_get_visible_slice (start, &next);
+ else
+ line_text = gtk_text_iter_get_slice (start, &next);
+ }
+ else
+ {
+ if (visible_only)
+ line_text = gtk_text_iter_get_visible_text (start, &next);
+ else
+ line_text = gtk_text_iter_get_text (start, &next);
+ }
+
+ if (match_start) /* if this is the first line we're matching */
+ {
+ found = g_utf8_strcasestr (line_text, *lines);
+ }
+ else
+ {
+ /* If it's not the first line, we have to match from the
+ * start of the line.
+ */
+ if (g_utf8_caselessnmatch (line_text, *lines, strlen (line_text),
+ strlen (*lines)))
+ found = line_text;
+ else
+ found = NULL;
+ }
+
+ if (found == NULL)
+ {
+ g_free (line_text);
+ return FALSE;
+ }
+
+ /* Get offset to start of search string */
+ offset = g_utf8_strlen (line_text, found - line_text);
+
+ next = *start;
+
+ /* If match start needs to be returned, set it to the
+ * start of the search string.
+ */
+ forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE);
+ if (match_start)
+ {
+ *match_start = next;
+ }
+
+ /* Go to end of search string */
+ forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE);
+
+ g_free (line_text);
+
+ ++lines;
+
+ if (match_end)
+ *match_end = next;
+
+ /* pass NULL for match_start, since we don't need to find the
+ * start again.
+ */
+ return lines_match (&next, lines, visible_only, slice, NULL, match_end);
+}
+
+/* strsplit () that retains the delimiter as part of the string. */
+static gchar **
+strbreakup (const char *string,
+ const char *delimiter,
+ gint max_tokens)
+{
+ GSList *string_list = NULL, *slist;
+ gchar **str_array, *s, *casefold, *new_string;
+ guint i, n = 1;
+
+ g_return_val_if_fail (string != NULL, NULL);
+ g_return_val_if_fail (delimiter != NULL, NULL);
+
+ if (max_tokens < 1)
+ max_tokens = G_MAXINT;
+
+ s = strstr (string, delimiter);
+ if (s)
+ {
+ guint delimiter_len = strlen (delimiter);
+
+ do
+ {
+ guint len;
+
+ len = s - string + delimiter_len;
+ new_string = g_new (gchar, len + 1);
+ strncpy (new_string, string, len);
+ new_string[len] = 0;
+ casefold = g_utf8_casefold (new_string, -1);
+ g_free (new_string);
+ new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+ string_list = g_slist_prepend (string_list, new_string);
+ n++;
+ string = s + delimiter_len;
+ s = strstr (string, delimiter);
+ } while (--max_tokens && s);
+ }
+
+ if (*string)
+ {
+ n++;
+ casefold = g_utf8_casefold (string, -1);
+ new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+ string_list = g_slist_prepend (string_list, new_string);
+ }
+
+ str_array = g_new (gchar*, n);
+
+ i = n - 1;
+
+ str_array[i--] = NULL;
+ for (slist = string_list; slist; slist = slist->next)
+ str_array[i--] = slist->data;
+
+ g_slist_free (string_list);
+
+ return str_array;
+}
+
+gboolean
+empathy_text_iter_forward_search (const GtkTextIter *iter,
+ const gchar *str,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ const GtkTextIter *limit)
+{
+ gchar **lines = NULL;
+ GtkTextIter match;
+ gboolean retval = FALSE;
+ GtkTextIter search;
+ gboolean visible_only;
+ gboolean slice;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+ g_return_val_if_fail (str != NULL, FALSE);
+
+ if (limit && gtk_text_iter_compare (iter, limit) >= 0)
+ return FALSE;
+
+ if (*str == '\0') {
+ /* If we can move one char, return the empty string there */
+ match = *iter;
+
+ if (gtk_text_iter_forward_char (&match)) {
+ if (limit && gtk_text_iter_equal (&match, limit)) {
+ return FALSE;
+ }
+
+ if (match_start) {
+ *match_start = match;
+ }
+ if (match_end) {
+ *match_end = match;
+ }
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ visible_only = TRUE;
+ slice = FALSE;
+
+ /* locate all lines */
+ lines = strbreakup (str, "\n", -1);
+
+ search = *iter;
+
+ do {
+ /* This loop has an inefficient worst-case, where
+ * gtk_text_iter_get_text () is called repeatedly on
+ * a single line.
+ */
+ GtkTextIter end;
+
+ if (limit && gtk_text_iter_compare (&search, limit) >= 0) {
+ break;
+ }
+
+ if (lines_match (&search, (const gchar**)lines,
+ visible_only, slice, &match, &end)) {
+ if (limit == NULL ||
+ (limit && gtk_text_iter_compare (&end, limit) <= 0)) {
+ retval = TRUE;
+
+ if (match_start) {
+ *match_start = match;
+ }
+ if (match_end) {
+ *match_end = end;
+ }
+ }
+ break;
+ }
+ } while (gtk_text_iter_forward_line (&search));
+
+ g_strfreev ((gchar**)lines);
+
+ return retval;
+}
+
+static const gchar *
+g_utf8_strrcasestr (const gchar *haystack, const gchar *needle)
+{
+ gsize needle_len;
+ gsize haystack_len;
+ const gchar *ret = NULL;
+ gchar *p;
+ gchar *casefold;
+ gchar *caseless_haystack;
+ gint i;
+
+ g_return_val_if_fail (haystack != NULL, NULL);
+ g_return_val_if_fail (needle != NULL, NULL);
+
+ casefold = g_utf8_casefold (haystack, -1);
+ caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD);
+ g_free (casefold);
+
+ needle_len = g_utf8_strlen (needle, -1);
+ haystack_len = g_utf8_strlen (caseless_haystack, -1);
+
+ if (needle_len == 0)
+ {
+ ret = (gchar *)haystack;
+ goto finally_1;
+ }
+
+ if (haystack_len < needle_len)
+ {
+ ret = NULL;
+ goto finally_1;
+ }
+
+ i = haystack_len - needle_len;
+ p = g_utf8_offset_to_pointer (caseless_haystack, i);
+ needle_len = strlen (needle);
+
+ while (p >= caseless_haystack)
+ {
+ if (strncmp (p, needle, needle_len) == 0)
+ {
+ ret = pointer_from_offset_skipping_decomp (haystack, i);
+ goto finally_1;
+ }
+
+ p = g_utf8_prev_char (p);
+ i--;
+ }
+
+finally_1:
+ g_free (caseless_haystack);
+
+ return ret;
+}
+
+static gboolean
+backward_lines_match (const GtkTextIter *start,
+ const gchar **lines,
+ gboolean visible_only,
+ gboolean slice,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter line, next;
+ gchar *line_text;
+ const gchar *found;
+ gint offset;
+
+ if (*lines == NULL || **lines == '\0')
+ {
+ if (match_start)
+ *match_start = *start;
+ if (match_end)
+ *match_end = *start;
+ return TRUE;
+ }
+
+ line = next = *start;
+ if (gtk_text_iter_get_line_offset (&next) == 0)
+ {
+ if (!gtk_text_iter_backward_line (&next))
+ return FALSE;
+ }
+ else
+ gtk_text_iter_set_line_offset (&next, 0);
+
+ if (slice)
+ {
+ if (visible_only)
+ line_text = gtk_text_iter_get_visible_slice (&next, &line);
+ else
+ line_text = gtk_text_iter_get_slice (&next, &line);
+ }
+ else
+ {
+ if (visible_only)
+ line_text = gtk_text_iter_get_visible_text (&next, &line);
+ else
+ line_text = gtk_text_iter_get_text (&next, &line);
+ }
+
+ if (match_start) /* if this is the first line we're matching */
+ {
+ found = g_utf8_strrcasestr (line_text, *lines);
+ }
+ else
+ {
+ /* If it's not the first line, we have to match from the
+ * start of the line.
+ */
+ if (g_utf8_caselessnmatch (line_text, *lines, strlen (line_text),
+ strlen (*lines)))
+ found = line_text;
+ else
+ found = NULL;
+ }
+
+ if (found == NULL)
+ {
+ g_free (line_text);
+ return FALSE;
+ }
+
+ /* Get offset to start of search string */
+ offset = g_utf8_strlen (line_text, found - line_text);
+
+ forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE);
+
+ /* If match start needs to be returned, set it to the
+ * start of the search string.
+ */
+ if (match_start)
+ {
+ *match_start = next;
+ }
+
+ /* Go to end of search string */
+ forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE);
+
+ g_free (line_text);
+
+ ++lines;
+
+ if (match_end)
+ *match_end = next;
+
+ /* try to match the rest of the lines forward, passing NULL
+ * for match_start so lines_match will try to match the entire
+ * line */
+ return lines_match (&next, lines, visible_only,
+ slice, NULL, match_end);
+}
+
+gboolean
+empathy_text_iter_backward_search (const GtkTextIter *iter,
+ const gchar *str,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ const GtkTextIter *limit)
+{
+ gchar **lines = NULL;
+ GtkTextIter match;
+ gboolean retval = FALSE;
+ GtkTextIter search;
+ gboolean visible_only;
+ gboolean slice;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+ g_return_val_if_fail (str != NULL, FALSE);
+
+ if (limit && gtk_text_iter_compare (iter, limit) <= 0)
+ return FALSE;
+
+ if (*str == '\0')
+ {
+ /* If we can move one char, return the empty string there */
+ match = *iter;
+
+ if (gtk_text_iter_backward_char (&match))
+ {
+ if (limit && gtk_text_iter_equal (&match, limit))
+ return FALSE;
+
+ if (match_start)
+ *match_start = match;
+ if (match_end)
+ *match_end = match;
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ }
+
+ visible_only = TRUE;
+ slice = TRUE;
+
+ /* locate all lines */
+ lines = strbreakup (str, "\n", -1);
+
+ search = *iter;
+
+ while (TRUE)
+ {
+ /* This loop has an inefficient worst-case, where
+ * gtk_text_iter_get_text () is called repeatedly on
+ * a single line.
+ */
+ GtkTextIter end;
+
+ if (limit && gtk_text_iter_compare (&search, limit) <= 0)
+ break;
+
+ if (backward_lines_match (&search, (const gchar**)lines,
+ visible_only, slice, &match, &end))
+ {
+ if (limit == NULL || (limit &&
+ gtk_text_iter_compare (&end, limit) > 0))
+ {
+ retval = TRUE;
+
+ if (match_start)
+ *match_start = match;
+ if (match_end)
+ *match_end = end;
+ }
+ break;
+ }
+
+ if (gtk_text_iter_get_line_offset (&search) == 0)
+ {
+ if (!gtk_text_iter_backward_line (&search))
+ break;
+ }
+ else
+ {
+ gtk_text_iter_set_line_offset (&search, 0);
+ }
+ }
+
+ g_strfreev ((gchar**)lines);
+
+ return retval;
+}
+
+gboolean
+empathy_window_get_is_visible (GtkWindow *window)
+{
+ GdkWindowState state;
+ GdkWindow *gdk_window;
+
+ g_return_val_if_fail (GTK_IS_WINDOW (window), FALSE);
+
+ gdk_window = GTK_WIDGET (window)->window;
+ if (!gdk_window) {
+ return FALSE;
+ }
+
+ state = gdk_window_get_state (gdk_window);
+ if (state & (GDK_WINDOW_STATE_WITHDRAWN | GDK_WINDOW_STATE_ICONIFIED)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+empathy_window_iconify (GtkWindow *window, GtkStatusIcon *status_icon)
+{
+ GdkRectangle icon_location;
+ gulong data[4];
+ Display *dpy;
+ GdkWindow *gdk_window;
+
+ gtk_status_icon_get_geometry (status_icon, NULL, &icon_location, NULL);
+ gdk_window = GTK_WIDGET (window)->window;
+ dpy = gdk_x11_drawable_get_xdisplay (gdk_window);
+
+ data[0] = icon_location.x;
+ data[1] = icon_location.y;
+ data[2] = icon_location.width;
+ data[3] = icon_location.height;
+
+ XChangeProperty (dpy,
+ GDK_WINDOW_XID (gdk_window),
+ gdk_x11_get_xatom_by_name_for_display (gdk_drawable_get_display (gdk_window),
+ "_NET_WM_ICON_GEOMETRY"),
+ XA_CARDINAL, 32, PropModeReplace,
+ (guchar *)&data, 4);
+
+ gtk_window_set_skip_taskbar_hint (window, TRUE);
+ gtk_window_iconify (window);
+}
+
+/* Takes care of moving the window to the current workspace. */
+void
+empathy_window_present (GtkWindow *window,
+ gboolean steal_focus)
+{
+ guint32 timestamp;
+
+ g_return_if_fail (GTK_IS_WINDOW (window));
+
+ /* There are three cases: hidden, visible, visible on another
+ * workspace.
+ */
+
+ if (!empathy_window_get_is_visible (window)) {
+ /* Hide it so present brings it to the current workspace. */
+ gtk_widget_hide (GTK_WIDGET (window));
+ }
+
+ timestamp = gtk_get_current_event_time ();
+ gtk_window_set_skip_taskbar_hint (window, FALSE);
+ gtk_window_present_with_time (window, timestamp);
+ /* FIXME: This shouldn't be required as gtk_window_present's doc says
+ * it deiconify automatically. */
+ gtk_window_deiconify (window);
+}
+
+GtkWindow *
+empathy_get_toplevel_window (GtkWidget *widget)
+{
+ GtkWidget *toplevel;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ toplevel = gtk_widget_get_toplevel (widget);
+ if (GTK_IS_WINDOW (toplevel) &&
+ GTK_WIDGET_TOPLEVEL (toplevel)) {
+ return GTK_WINDOW (toplevel);
+ }
+
+ return NULL;
+}
+
+/* The URL opening code can't handle schemeless strings, so we try to be
+ * smart and add http if there is no scheme or doesn't look like a mail
+ * address. This should work in most cases, and let us click on strings
+ * like "www.gnome.org".
+ */
+static gchar *
+fixup_url (const gchar *url)
+{
+ if (!g_str_has_prefix (url, "ghelp:") &&
+ !strstr (url, ":/") &&
+ !strstr (url, "@")) {
+ return g_strdup_printf ("http://%s", url);
+ } else {
+ return NULL;
+ }
+}
+
+void
+empathy_url_show (const char *url)
+{
+ gchar *real_url;
+ GError *error = NULL;
+
+ real_url = fixup_url (url);
+ if (real_url) {
+ url = real_url;
+ }
+
+ /* FIXME: this does not work for multihead, we should use
+ * GdkAppLaunchContext or gtk_show_url, see bug #514396.
+ */
+ g_app_info_launch_default_for_uri (url, NULL, &error);
+ if (error) {
+ GtkWidget *dialog;
+
+ dialog = gtk_message_dialog_new (NULL, 0,
+ GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
+ _("Unable to open URI"));
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ error->message);
+
+ g_signal_connect (dialog, "response",
+ G_CALLBACK (gtk_widget_destroy),
+ NULL);
+ gtk_window_present (GTK_WINDOW (dialog));
+
+ g_clear_error (&error);
+ }
+
+ g_free (real_url);
+}
+
+static void
+link_button_hook (GtkLinkButton *button,
+ const gchar *link,
+ gpointer user_data)
+{
+ empathy_url_show (link);
+}
+
+GtkWidget *
+empathy_link_button_new (const gchar *url,
+ const gchar *title)
+{
+ static gboolean hook = FALSE;
+
+ if (!hook) {
+ hook = TRUE;
+ gtk_link_button_set_uri_hook (link_button_hook, NULL, NULL);
+ }
+
+ return gtk_link_button_new_with_label (url, title);
+}
+
+void
+empathy_toggle_button_set_state_quietly (GtkWidget *widget,
+ GCallback callback,
+ gpointer user_data,
+ gboolean active)
+{
+ g_return_if_fail (GTK_IS_TOGGLE_BUTTON (widget));
+
+ g_signal_handlers_block_by_func (widget, callback, user_data);
+ g_object_set (widget, "active", active, NULL);
+ g_signal_handlers_unblock_by_func (widget, callback, user_data);
+}
+
+GtkTextTag *
+empathy_text_buffer_tag_set (GtkTextBuffer *buffer,
+ const gchar *tag_name,
+ const gchar *first_property_name,
+ ...)
+{
+ GtkTextTagTable *table;
+ GtkTextTag *tag;
+
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+ g_return_val_if_fail (tag_name != NULL, NULL);
+
+ table = gtk_text_buffer_get_tag_table (buffer);
+ tag = gtk_text_tag_table_lookup (table, tag_name);
+
+ if (!tag) {
+ tag = gtk_text_tag_new (tag_name);
+ gtk_text_tag_table_add (table, tag);
+ g_object_unref (tag);
+ } else {
+ /* Clear the old values so that we don't affect the new theme. */
+ g_object_set (tag,
+ "background-set", FALSE,
+ "foreground-set", FALSE,
+ "invisible-set", FALSE,
+ "justification-set", FALSE,
+ "paragraph-background-set", FALSE,
+ "pixels-above-lines-set", FALSE,
+ "pixels-below-lines-set", FALSE,
+ "rise-set", FALSE,
+ "scale-set", FALSE,
+ "size-set", FALSE,
+ "style-set", FALSE,
+ "weight-set", FALSE,
+ NULL);
+ }
+
+ if (first_property_name) {
+ va_list list;
+
+ va_start (list, first_property_name);
+ g_object_set_valist (G_OBJECT (tag), first_property_name, list);
+ va_end (list);
+ }
+
+ return tag;
+}
+