/* -*- 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;
}