aboutsummaryrefslogtreecommitdiffstats
path: root/lib/widgets/ephy-ellipsizing-label.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/widgets/ephy-ellipsizing-label.c')
-rw-r--r--lib/widgets/ephy-ellipsizing-label.c774
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;
+}
+