Logo Search packages:      
Sourcecode: galeon version File versions

gul-ellipsizing-label.c

/* -*- 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 details.

   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>,
 */

#include "gul-ellipsizing-label.h"

#include <string.h>

typedef enum {
        GUL_ELLIPSIZE_START,
        GUL_ELLIPSIZE_MIDDLE,
        GUL_ELLIPSIZE_END
} GulEllipsizeMode;

struct GulEllipsizingLabelDetails
{
      char *full_text;
};

static void gul_ellipsizing_label_class_init (GulEllipsizingLabelClass *class);
static void gul_ellipsizing_label_init       (GulEllipsizingLabel      *label);

static GObjectClass *parent_class = NULL;

static int
gul_strcmp (const char *string_a, const char *string_b)
{
       return strcmp (string_a == NULL ? "" : string_a,
                      string_b == NULL ? "" : string_b);
}

static gboolean
gul_str_is_equal (const char *string_a, const char *string_b)
{
       return gul_strcmp (string_a, string_b) == 0;
}

#define ELLIPSIS "..."

/* Caution: this is an _expensive_ function */
static int
measure_string_width (const char  *string,
                  PangoLayout *layout)
{
      int width;
      
      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)
{
      int *widths;
      int *offsets;
      int *cuts;
      int char_len;
      int byte_len;
      const char *p;
      int i;
      PangoLayoutIter *iter;
      PangoLogAttr *attrs;
      
#define BEGINS_UTF8_CHAR(x) (((x) & 0xc0) != 0x80)
      
      char_len = g_utf8_strlen (string, -1);
      byte_len = strlen (string);
      
      widths = g_new (int, char_len);
      offsets = g_new (int, byte_len);

      /* Create a translation table from byte index to char offset */
      p = string;
      i = 0;
      while (*p) {
            int byte_index = p - 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 */
      pango_layout_set_text (layout, string, -1);
      
      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 (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 (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;
}


static char *
gul_string_ellipsize_start (const char *string, PangoLayout *layout, int width)
{
      int resulting_width;
      int *cuts;
      int *widths;
      int char_len;
      const char *p;
      int truncate_offset;

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

      if (resulting_width <= width) {
            /* String is already short enough. */
            return g_strdup (string);
      }

      /* Remove width of an ellipsis */
      width -= measure_string_width (ELLIPSIS, layout);

      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);

        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);
      
      p = g_utf8_offset_to_pointer (string, truncate_offset);
      
      return g_strconcat (ELLIPSIS, p, NULL);
}

static char *
gul_string_ellipsize_end (const char *string, PangoLayout *layout, int width)
{
      int resulting_width;
      int *cuts;
      int *widths;
      int char_len;
      const char *p;
      int truncate_offset;
      char *result;
      
      /* See explanatory comments in ellipsize_start */
      
      if (*string == '\0')
            return g_strdup ("");

      resulting_width = measure_string_width (string, layout);
      
      if (resulting_width <= width) {
            return g_strdup (string);
      }

      width -= measure_string_width (ELLIPSIS, layout);

      if (width < 0) {
            return g_strdup ("");
      }
      
      compute_character_widths (string, layout, &char_len, &widths, &cuts);
      
        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);

      p = g_utf8_offset_to_pointer (string, truncate_offset);
      
      result = g_malloc ((p - string) + strlen (ELLIPSIS) + 1);
      memcpy (result, string, (p - string));
      strcpy (result + (p - string), ELLIPSIS);

      return result;
}

static char *
gul_string_ellipsize_middle (const char *string, PangoLayout *layout, int width)
{
      int resulting_width;
      int *cuts;
      int *widths;
      int char_len;
      int starting_fragment_byte_len;
      int ending_fragment_byte_index;
      int starting_fragment_length;
      int ending_fragment_offset;
      char *result;
      
      /* See explanatory comments in ellipsize_start */
      
      if (*string == '\0')
            return g_strdup ("");

      resulting_width = measure_string_width (string, layout);
      
      if (resulting_width <= width) {
            return g_strdup (string);
      }

      width -= measure_string_width (ELLIPSIS, layout);

      if (width < 0) {
            return g_strdup ("");
      }
      
      compute_character_widths (string, layout, &char_len, &widths, &cuts);
      
      starting_fragment_length = char_len / 2;
      ending_fragment_offset = starting_fragment_length + 1;
      
      /* Shave off a character at a time from the first and the second half
       * until we can fit
       */
      resulting_width -= widths[ending_fragment_offset - 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);  
      
      /* patch the two fragments together with an ellipsis */
      result = g_malloc (strlen (string) + strlen (ELLIPSIS) + 1); /* a bit wasteful, no biggie */

      starting_fragment_byte_len = g_utf8_offset_to_pointer (string, starting_fragment_length) - string;
      ending_fragment_byte_index = g_utf8_offset_to_pointer (string, ending_fragment_offset) - string;
      
      memcpy (result, string, starting_fragment_byte_len);
      strcpy (result + starting_fragment_byte_len, ELLIPSIS);
      strcpy (result + starting_fragment_byte_len + strlen (ELLIPSIS), string + ending_fragment_byte_index);

      return result;
}


/**
 * gul_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,
                              GulEllipsizeMode mode)
{
      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 GUL_ELLIPSIZE_START:
            s = gul_string_ellipsize_start (string, layout, width);
            break;
      case GUL_ELLIPSIZE_MIDDLE:
            s = gul_string_ellipsize_middle (string, layout, width);
            break;
      case GUL_ELLIPSIZE_END:
            s = gul_string_ellipsize_end (string, layout, width);
            break;
      default:
            g_return_if_reached ();
            s = NULL;
      }
      
      pango_layout_set_text (layout, s, -1);
      
      g_free (s);
}

GType 
gul_ellipsizing_label_get_type (void)
{
        static GType gul_ellipsizing_label_type = 0;

        if (gul_ellipsizing_label_type == 0)
        {
                static const GTypeInfo our_info =
                {
                        sizeof (GulEllipsizingLabelClass),
                        NULL, /* base_init */
                        NULL, /* base_finalize */
                        (GClassInitFunc) gul_ellipsizing_label_class_init,
                        NULL,
                        NULL, /* class_data */
                        sizeof (GulEllipsizingLabel),
                        0, /* n_preallocs */
                        (GInstanceInitFunc) gul_ellipsizing_label_init
                };

                gul_ellipsizing_label_type = g_type_register_static (GTK_TYPE_LABEL,
                                                                       "GulEllipsizingLabel",
                                                                     &our_info, 0);
        }

        return gul_ellipsizing_label_type;
}

static void
gul_ellipsizing_label_init (GulEllipsizingLabel *label)
{
      label->details = g_new0 (GulEllipsizingLabelDetails, 1);
}

static void
real_finalize (GObject *object)
{
      GulEllipsizingLabel *label;

      label = GUL_ELLIPSIZING_LABEL (object);

      g_free (label->details->full_text);
      g_free (label->details);

      G_OBJECT_CLASS (parent_class)->finalize (object);
}

GtkWidget*
gul_ellipsizing_label_new (const char *string)
{
      GulEllipsizingLabel *label;
  
      label = g_object_new (GUL_TYPE_ELLIPSIZING_LABEL, NULL);
      gul_ellipsizing_label_set_text (label, string);
  
      return GTK_WIDGET (label);
}

void
gul_ellipsizing_label_set_text (GulEllipsizingLabel *label, 
                        const char          *string)
{
      g_return_if_fail (GUL_IS_ELLIPSIZING_LABEL (label));

      if (gul_str_is_equal (string, label->details->full_text)) {
            return;
      }

      g_free (label->details->full_text);
      label->details->full_text = g_strdup (string);

      /* Queues a resize as side effect */
      gtk_label_set_text (GTK_LABEL (label), label->details->full_text);
}

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)
{
      GulEllipsizingLabel *label;

      label = GUL_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->details->full_text == NULL) {
                  pango_layout_set_text (GTK_LABEL (label)->layout, "", -1);
            } else {
                  GulEllipsizeMode mode;

                  if (ABS (GTK_MISC (label)->xalign - 0.5) < 1e-12)
                        mode = GUL_ELLIPSIZE_MIDDLE;
                  else if (GTK_MISC (label)->xalign < 0.5)
                        mode = GUL_ELLIPSIZE_END;
                  else
                        mode = GUL_ELLIPSIZE_START;
                  
                  gul_pango_layout_set_text_ellipsized (GTK_LABEL (label)->layout,
                                                label->details->full_text,
                                                allocation->width,
                                                mode);
            }
      }

      GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);
}

static gboolean
real_expose_event (GtkWidget *widget, GdkEventExpose *event)
{
      GulEllipsizingLabel *label;
      GtkRequisition req;
      
      label = GUL_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
gul_ellipsizing_label_class_init (GulEllipsizingLabelClass *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;
}


Generated by  Doxygen 1.6.0   Back to index