diff options
Diffstat (limited to 'lib/widgets/ephy-ellipsizing-label.c')
-rw-r--r-- | lib/widgets/ephy-ellipsizing-label.c | 774 |
1 files changed, 774 insertions, 0 deletions
diff --git a/lib/widgets/ephy-ellipsizing-label.c b/lib/widgets/ephy-ellipsizing-label.c new file mode 100644 index 000000000..13f911078 --- /dev/null +++ b/lib/widgets/ephy-ellipsizing-label.c @@ -0,0 +1,774 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* eel-ellipsizing-label.c: Subclass of GtkLabel that ellipsizes the text. + + Copyright (C) 2001 Eazel, Inc. + + The Gnome Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + The Gnome 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 + Library General Public License for more priv. + + You should have received a copy of the GNU Library General Public + License along with the Gnome Library; see the file COPYING.LIB. If not, + write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. + + Author: John Sullivan <sullivan@eazel.com>, + Marco Pesenti Gritti <marco@it.gnome.org> Markup support + */ + +#include "ephy-ellipsizing-label.h" + +#include <string.h> + +struct EphyEllipsizingLabelPrivate +{ + char *full_text; + + EphyEllipsizeMode mode; +}; + +static void ephy_ellipsizing_label_class_init (EphyEllipsizingLabelClass *class); +static void ephy_ellipsizing_label_init (EphyEllipsizingLabel *label); + +static GObjectClass *parent_class = NULL; + +static int +ephy_strcmp (const char *string_a, const char *string_b) +{ + return strcmp (string_a == NULL ? "" : string_a, + string_b == NULL ? "" : string_b); +} + +static gboolean +ephy_str_is_equal (const char *string_a, const char *string_b) +{ + return ephy_strcmp (string_a, string_b) == 0; +} + +#define ELLIPSIS "..." + +/* Caution: this is an _expensive_ function */ +static int +measure_string_width (const char *string, + PangoLayout *layout, + gboolean markup) +{ + int width; + + if (markup) + { + pango_layout_set_markup (layout, string, -1); + } + else + { + pango_layout_set_text (layout, string, -1); + } + + pango_layout_get_pixel_size (layout, &width, NULL); + + return width; +} + +/* this is also plenty slow */ +static void +compute_character_widths (const char *string, + PangoLayout *layout, + int *char_len_return, + int **widths_return, + int **cuts_return, + gboolean markup) +{ + int *widths; + int *offsets; + int *cuts; + int char_len; + int byte_len; + const char *p; + const char *nm_string; + int i; + PangoLayoutIter *iter; + PangoLogAttr *attrs; + +#define BEGINS_UTF8_CHAR(x) (((x) & 0xc0) != 0x80) + + if (markup) + { + pango_layout_set_markup (layout, string, -1); + } + else + { + pango_layout_set_text (layout, string, -1); + } + + nm_string = pango_layout_get_text (layout); + + char_len = g_utf8_strlen (nm_string, -1); + byte_len = strlen (nm_string); + + widths = g_new (int, char_len); + offsets = g_new (int, byte_len); + + /* Create a translation table from byte index to char offset */ + p = nm_string; + i = 0; + while (*p) { + int byte_index = p - nm_string; + + if (BEGINS_UTF8_CHAR (*p)) { + offsets[byte_index] = i; + ++i; + } else { + offsets[byte_index] = G_MAXINT; /* segv if we try to use this */ + } + + ++p; + } + + /* Now fill in the widths array */ + iter = pango_layout_get_iter (layout); + + do { + PangoRectangle extents; + int byte_index; + + byte_index = pango_layout_iter_get_index (iter); + + if (byte_index < byte_len) { + pango_layout_iter_get_char_extents (iter, &extents); + + g_assert (BEGINS_UTF8_CHAR (nm_string[byte_index])); + g_assert (offsets[byte_index] < char_len); + + widths[offsets[byte_index]] = PANGO_PIXELS (extents.width); + } + + } while (pango_layout_iter_next_char (iter)); + + pango_layout_iter_free (iter); + + g_free (offsets); + + *widths_return = widths; + + /* Now compute character offsets that are legitimate places to + * chop the string + */ + attrs = g_new (PangoLogAttr, char_len + 1); + + pango_get_log_attrs (nm_string, byte_len, -1, + pango_context_get_language ( + pango_layout_get_context (layout)), + attrs, + char_len + 1); + + cuts = g_new (int, char_len); + i = 0; + while (i < char_len) { + cuts[i] = attrs[i].is_cursor_position; + + ++i; + } + + g_free (attrs); + + *cuts_return = cuts; + + *char_len_return = char_len; +} + +typedef struct +{ + GString *string; + int start_offset; + int end_offset; + int position; +} EllipsizeStringData; + +static void +start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + EllipsizeStringData *data = (EllipsizeStringData *)user_data; + int i; + + g_string_append_c (data->string, '<'); + g_string_append (data->string, element_name); + + for (i = 0; attribute_names[i] != NULL; i++) + { + g_string_append_c (data->string, ' '); + g_string_append (data->string, attribute_names[i]); + g_string_append (data->string, "=\""); + g_string_append (data->string, attribute_values[i]); + g_string_append_c (data->string, '"'); + } + + g_string_append_c (data->string, '>'); +} + +static void +end_element_handler (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + EllipsizeStringData *data = (EllipsizeStringData *)user_data; + + g_string_append (data->string, "</"); + g_string_append (data->string, element_name); + g_string_append_c (data->string, '>'); +} + +static void +append_ellipsized_text (const char *text, + EllipsizeStringData *data, + int text_len) +{ + int position; + int new_position; + + position = data->position; + new_position = data->position + text_len; + + if (position > data->start_offset && + new_position < data->end_offset) + { + return; + } + else if ((position < data->start_offset && + new_position < data->start_offset) || + (position > data->end_offset && + new_position > data->end_offset)) + { + g_string_append (data->string, + text); + } + else if (position <= data->start_offset && + new_position >= data->end_offset) + { + if (position < data->start_offset) + { + g_string_append_len (data->string, + text, + data->start_offset - + position); + } + + g_string_append (data->string, + ELLIPSIS); + + if (new_position > data->end_offset) + { + g_string_append_len (data->string, + text + data->end_offset - + position, + position + text_len - + data->end_offset); + } + } + + data->position = new_position; +} + +static void +text_handler (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + EllipsizeStringData *data = (EllipsizeStringData *)user_data; + + append_ellipsized_text (text, data, text_len); +} + +static GMarkupParser pango_markup_parser = { + start_element_handler, + end_element_handler, + text_handler, + NULL, + NULL +}; + +static char * +ellipsize_string (const char *string, + int start_offset, + int end_offset, + gboolean markup) +{ + GString *str; + EllipsizeStringData data; + char *result; + GMarkupParseContext *c; + + str = g_string_new (NULL); + data.string = str; + data.start_offset = start_offset; + data.end_offset = end_offset; + data.position = 0; + + if (markup) + { + c = g_markup_parse_context_new (&pango_markup_parser, + 0, &data, NULL); + g_markup_parse_context_parse (c, string, -1, NULL); + g_markup_parse_context_free (c); + } + else + { + append_ellipsized_text (string, &data, + g_utf8_strlen (string, -1)); + } + + result = str->str; + g_string_free (str, FALSE); + return result; +} + +static char * +ephy_string_ellipsize_start (const char *string, PangoLayout *layout, int width, gboolean markup) +{ + int resulting_width; + int *cuts; + int *widths; + int char_len; + int truncate_offset; + int bytes_end; + + /* Zero-length string can't get shorter - catch this here to + * avoid expensive calculations + */ + if (*string == '\0') + return g_strdup (""); + + /* I'm not sure if this short-circuit is a net win; it might be better + * to just dump this, and always do the compute_character_widths() etc. + * down below. + */ + resulting_width = measure_string_width (string, layout, markup); + + if (resulting_width <= width) { + /* String is already short enough. */ + return g_strdup (string); + } + + /* Remove width of an ellipsis */ + width -= measure_string_width (ELLIPSIS, layout, markup); + + if (width < 0) { + /* No room even for an ellipsis. */ + return g_strdup (""); + } + + /* Our algorithm involves removing enough chars from the string to bring + * the width to the required small size. However, due to ligatures, + * combining characters, etc., it's not guaranteed that the algorithm + * always works 100%. It's sort of a heuristic thing. It should work + * nearly all the time... but I wouldn't put in + * g_assert (width of resulting string < width). + * + * Hmm, another thing that this breaks with is explicit line breaks + * in "string" + */ + + compute_character_widths (string, layout, &char_len, &widths, &cuts, markup); + + for (truncate_offset = 1; truncate_offset < char_len; truncate_offset++) { + + resulting_width -= widths[truncate_offset]; + + if (resulting_width <= width && + cuts[truncate_offset]) { + break; + } + } + + g_free (cuts); + g_free (widths); + + bytes_end = g_utf8_offset_to_pointer (string, truncate_offset) - string; + + return ellipsize_string (string, 0, bytes_end, markup); +} + +static char * +ephy_string_ellipsize_end (const char *string, PangoLayout *layout, int width, gboolean markup) +{ + int resulting_width; + int *cuts; + int *widths; + int char_len; + int truncate_offset; + int bytes_end; + + /* See explanatory comments in ellipsize_start */ + + if (*string == '\0') + return g_strdup (""); + + resulting_width = measure_string_width (string, layout, markup); + + if (resulting_width <= width) { + return g_strdup (string); + } + + width -= measure_string_width (ELLIPSIS, layout, markup); + + if (width < 0) { + return g_strdup (""); + } + + compute_character_widths (string, layout, &char_len, &widths, &cuts, markup); + + for (truncate_offset = char_len - 1; truncate_offset > 0; truncate_offset--) { + resulting_width -= widths[truncate_offset]; + if (resulting_width <= width && + cuts[truncate_offset]) { + break; + } + } + + g_free (cuts); + g_free (widths); + + bytes_end = g_utf8_offset_to_pointer (string, truncate_offset) - string; + + return ellipsize_string (string, bytes_end, + char_len, markup); +} + +static char * +ephy_string_ellipsize_middle (const char *string, PangoLayout *layout, int width, gboolean markup) +{ + int resulting_width; + int *cuts; + int *widths; + int char_len; + int starting_fragment_length; + int ending_fragment_offset; + int bytes_start; + int bytes_end; + + /* See explanatory comments in ellipsize_start */ + + if (*string == '\0') + return g_strdup (""); + + resulting_width = measure_string_width (string, layout, markup); + + if (resulting_width <= width) { + return g_strdup (string); + } + + width -= measure_string_width (ELLIPSIS, layout, markup); + + if (width < 0) { + return g_strdup (""); + } + + compute_character_widths (string, layout, &char_len, &widths, &cuts, markup); + + starting_fragment_length = char_len / 2; + ending_fragment_offset = starting_fragment_length + 1; + + /* depending on whether the original string length is odd or even, start by + * shaving off the characters from the starting or ending fragment + */ + if (char_len % 2) { + goto shave_end; + } + + while (starting_fragment_length > 0 || ending_fragment_offset < char_len) { + if (resulting_width <= width && + cuts[ending_fragment_offset] && + cuts[starting_fragment_length]) { + break; + } + + if (starting_fragment_length > 0) { + resulting_width -= widths[starting_fragment_length]; + starting_fragment_length--; + } + + shave_end: + if (resulting_width <= width && + cuts[ending_fragment_offset] && + cuts[starting_fragment_length]) { + break; + } + + if (ending_fragment_offset < char_len) { + resulting_width -= widths[ending_fragment_offset]; + ending_fragment_offset++; + } + } + + g_free (cuts); + g_free (widths); + + bytes_start = g_utf8_offset_to_pointer (string, starting_fragment_length) - string; + bytes_end = g_utf8_offset_to_pointer (string, ending_fragment_offset) - string; + + return ellipsize_string (string, bytes_start, bytes_end, markup); +} + + +/** + * ephy_pango_layout_set_text_ellipsized + * + * @layout: a pango layout + * @string: A a string to be ellipsized. + * @width: Desired maximum width in points. + * @mode: The desired ellipsizing mode. + * + * Truncates a string if required to fit in @width and sets it on the + * layout. Truncation involves removing characters from the start, middle or end + * respectively and replacing them with "...". Algorithm is a bit + * fuzzy, won't work 100%. + * + */ +static void +gul_pango_layout_set_text_ellipsized (PangoLayout *layout, + const char *string, + int width, + EphyEllipsizeMode mode, + gboolean markup) +{ + char *s; + + g_return_if_fail (PANGO_IS_LAYOUT (layout)); + g_return_if_fail (string != NULL); + g_return_if_fail (width >= 0); + + switch (mode) { + case EPHY_ELLIPSIZE_START: + s = ephy_string_ellipsize_start (string, layout, width, markup); + break; + case EPHY_ELLIPSIZE_MIDDLE: + s = ephy_string_ellipsize_middle (string, layout, width, markup); + break; + case EPHY_ELLIPSIZE_END: + s = ephy_string_ellipsize_end (string, layout, width, markup); + break; + default: + g_return_if_reached (); + s = NULL; + } + + if (markup) + { + pango_layout_set_markup (layout, s, -1); + } + else + { + pango_layout_set_text (layout, s, -1); + } + + g_free (s); +} + +GType +ephy_ellipsizing_label_get_type (void) +{ + static GType ephy_ellipsizing_label_type = 0; + + if (ephy_ellipsizing_label_type == 0) + { + static const GTypeInfo our_info = + { + sizeof (EphyEllipsizingLabelClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) ephy_ellipsizing_label_class_init, + NULL, + NULL, /* class_data */ + sizeof (EphyEllipsizingLabel), + 0, /* n_preallocs */ + (GInstanceInitFunc) ephy_ellipsizing_label_init + }; + + ephy_ellipsizing_label_type = g_type_register_static (GTK_TYPE_LABEL, + "EphyEllipsizingLabel", + &our_info, 0); + } + + return ephy_ellipsizing_label_type; +} + +static void +ephy_ellipsizing_label_init (EphyEllipsizingLabel *label) +{ + label->priv = g_new0 (EphyEllipsizingLabelPrivate, 1); + + label->priv->mode = EPHY_ELLIPSIZE_NONE; +} + +static void +real_finalize (GObject *object) +{ + EphyEllipsizingLabel *label; + + label = EPHY_ELLIPSIZING_LABEL (object); + + g_free (label->priv->full_text); + g_free (label->priv); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +GtkWidget* +ephy_ellipsizing_label_new (const char *string) +{ + EphyEllipsizingLabel *label; + + label = g_object_new (EPHY_TYPE_ELLIPSIZING_LABEL, NULL); + ephy_ellipsizing_label_set_text (label, string); + + return GTK_WIDGET (label); +} + +void +ephy_ellipsizing_label_set_text (EphyEllipsizingLabel *label, + const char *string) +{ + g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label)); + + if (ephy_str_is_equal (string, label->priv->full_text)) { + return; + } + + g_free (label->priv->full_text); + label->priv->full_text = g_strdup (string); + + /* Queues a resize as side effect */ + gtk_label_set_text (GTK_LABEL (label), label->priv->full_text); +} + +void +ephy_ellipsizing_label_set_markup (EphyEllipsizingLabel *label, + const char *string) +{ + g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label)); + + if (ephy_str_is_equal (string, label->priv->full_text)) { + return; + } + + g_free (label->priv->full_text); + label->priv->full_text = g_strdup (string); + + /* Queues a resize as side effect */ + gtk_label_set_markup (GTK_LABEL (label), label->priv->full_text); +} + +void +ephy_ellipsizing_label_set_mode (EphyEllipsizingLabel *label, + EphyEllipsizeMode mode) +{ + g_return_if_fail (EPHY_IS_ELLIPSIZING_LABEL (label)); + + label->priv->mode = mode; +} + +static void +real_size_request (GtkWidget *widget, GtkRequisition *requisition) +{ + GTK_WIDGET_CLASS (parent_class)->size_request (widget, requisition); + + /* Don't demand any particular width; will draw ellipsized into whatever size we're given */ + requisition->width = 0; +} + +static void +real_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + EphyEllipsizingLabel *label; + gboolean markup; + + markup = gtk_label_get_use_markup (GTK_LABEL (widget)); + + label = EPHY_ELLIPSIZING_LABEL (widget); + + /* This is the bad hack of the century, using private + * GtkLabel layout object. If the layout is NULL + * then it got blown away since size request, + * we just punt in that case, I don't know what to do really. + */ + + if (GTK_LABEL (label)->layout != NULL) { + if (label->priv->full_text == NULL) { + pango_layout_set_text (GTK_LABEL (label)->layout, "", -1); + } else { + EphyEllipsizeMode mode; + + if (label->priv->mode != EPHY_ELLIPSIZE_NONE) + mode = label->priv->mode; + + if (ABS (GTK_MISC (label)->xalign - 0.5) < 1e-12) + mode = EPHY_ELLIPSIZE_MIDDLE; + else if (GTK_MISC (label)->xalign < 0.5) + mode = EPHY_ELLIPSIZE_END; + else + mode = EPHY_ELLIPSIZE_START; + + gul_pango_layout_set_text_ellipsized (GTK_LABEL (label)->layout, + label->priv->full_text, + allocation->width, + mode, + markup); + + gtk_widget_queue_draw (GTK_WIDGET (label)); + } + } + + GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation); +} + +static gboolean +real_expose_event (GtkWidget *widget, GdkEventExpose *event) +{ + EphyEllipsizingLabel *label; + GtkRequisition req; + + label = EPHY_ELLIPSIZING_LABEL (widget); + + /* push/pop the actual size so expose draws in the right + * place, yes this is bad hack central. Here we assume the + * ellipsized text has been set on the layout in size_allocate + */ + GTK_WIDGET_CLASS (parent_class)->size_request (widget, &req); + widget->requisition.width = req.width; + GTK_WIDGET_CLASS (parent_class)->expose_event (widget, event); + widget->requisition.width = 0; + + return FALSE; +} + + +static void +ephy_ellipsizing_label_class_init (EphyEllipsizingLabelClass *klass) +{ + GtkWidgetClass *widget_class; + + parent_class = g_type_class_peek_parent (klass); + + widget_class = GTK_WIDGET_CLASS (klass); + + G_OBJECT_CLASS (klass)->finalize = real_finalize; + + widget_class->size_request = real_size_request; + widget_class->size_allocate = real_size_allocate; + widget_class->expose_event = real_expose_event; +} + |