Logo Search packages:      
Sourcecode: pan version File versions  Download package

text.c

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002  Charles Kerr <charles@rebelbase.com>
 *
 * 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; version 2 of the License.
 *
 * 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
 */

#include <config.h>

#include <ctype.h>
#include <string.h>

#include <glib.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk-pixbuf/gdk-pixbuf-loader.h>
#include <gmime/gmime.h>

#include <pan/base/acache.h>
#include <pan/base/argset.h>
#include <pan/base/debug.h>
#include <pan/base/gnksa.h>
#include <pan/base/log.h>
#include <pan/base/pan-glib-extensions.h>
#include <pan/base/pan-object.h>
#include <pan/base/pan-i18n.h>
#include <pan/base/serverlist.h>
#include <pan/base/text-massager.h>
#include <pan/base/util-mime.h>

#include <pan/articlelist.h>
#include <pan/gui.h>
#include <pan/gui-headers.h>
#include <pan/globals.h>
#include <pan/grouplist.h>
#include <pan/message-window.h>
#include <pan/prefs.h>
#include <pan/queue.h>
#include <pan/task-bodies.h>
#include <pan/text.h>
#include <pan/util.h>


static GtkTextBuffer * _text_buffer = NULL;
static GtkWidget * scrolled_window = NULL;
static GtkTextMark * _begin_mark = NULL;

GdkColor text_quoted_color[3];
GdkColor signature_color;
GdkColor text_url_color;
PanCallback * current_article_changed = NULL;

/****
*****
*****    LOW-LEVEL TEXT UPDATE
*****
****/


/**
 * Returns the quote tag ("quote_0", "quote_1", etc.) appropriate for the line.
 * The right tag is calculated by adding up the number of quote characters
 * at the beginning of the line.
 *
 * @param utf8_line the line whose quote status we're checking
 * @param utf8_byte_len the byte length of utf8_line
 * @return a const string for the line's quote tag.  Never NULL.
 */
static const char *
get_quote_tag (const TextMassager     * text_massager,
               const char             * utf8_line,
               int                      utf8_byte_len)
{
      const char * str = utf8_line;
      const char * line_end = utf8_line + utf8_byte_len;
      const char * retval = "quote_0";

      if (0<utf8_byte_len && is_nonempty_string(str))
      {
            int depth = 0;

            /* walk past leading spaces */
            while (str!=line_end && g_unichar_isspace(g_utf8_get_char(str)))
                  str = g_utf8_next_char (str);

            /* count the number of spaces or quote characters */
            for (;;) {
                  if (str == line_end)
                        break;
                  else if (text_massager_is_quote_char(text_massager, (guchar)*str))
                         ++depth;
                  else if (!g_unichar_isspace(g_utf8_get_char(str)))
                        break;

                  str = g_utf8_next_char (str);
            }

            if (!depth)
                  retval = "quote_0";
            else switch (depth % 3) {
                  case 1: retval = "quote_1"; break;
                  case 2: retval = "quote_2"; break;
                  case 0: retval = "quote_3"; break;
            }
      }

      return retval;
}
/**
 * Convenience function for clearing the specified GtkTextBuffer.
 */
static void
clear_text_buffer_nolock (GtkTextBuffer * buffer)
{
      GtkTextIter start;
      GtkTextIter end;
      gtk_text_buffer_get_bounds (buffer, &start, &end);
      gtk_text_buffer_delete (buffer, &start, &end);
}

/**
 * Returns the pointer to the beginning of the next URL
 * in searchme, or NULL if no URL is found.
 * @param searchme the zero-terminated string to search for URLS.
 */
static const char*
find_next_url (const char * searchme)
{
      register const char * pch = searchme;

      for (; pch && *pch; ++pch)
      {
            if (*pch == 'h')
            {
                  if (!strncmp (pch, "http://", 7))
                        return pch;
                  if (!strncmp (pch, "https://", 8))
                        return pch;
            }
      }

      return NULL;
}

/**
 * Appends the specified body into the text buffer.
 * This function takes care of muting quotes and marking
 * quoted and URL areas in the GtkTextBuffer.
 */
static void
append_text_buffer_nolock (const TextMassager  * text_massager,
                           GtkTextBuffer       * text_buffer,
                           const char          * body,
                       gboolean              mute_quotes)
{
      PString line;
      char * freeme1 = NULL;
      char * freeme2 = NULL;
      const char * pch;
      const char * last_quote_begin = NULL;
      const char * quote_tag = NULL;
      const char * last_quote_tag = NULL;
      GtkTextIter mark_start;
      GtkTextIter mark_end;
      GtkTextIter start;
      GtkTextIter end;
      GtkTextMark * mark;
      gboolean is_sig = FALSE;
      debug_enter ("append_text_buffer_nolock");

      /* sanity checks */
      g_return_if_fail (text_buffer!=NULL);
      g_return_if_fail (GTK_IS_TEXT_BUFFER(text_buffer));

      /* mute the quoted text, if desired */
      if (mute_quotes)
            body = freeme1 = text_massager_mute_quoted (text_massager, body);

      body = pan_utf8ize (body, -1, &freeme2);

      /* insert the text */
      gtk_text_buffer_get_end_iter (text_buffer, &end);
      mark = gtk_text_buffer_create_mark (text_buffer, "blah", &end, TRUE);
      gtk_text_buffer_insert (text_buffer, &end, body, -1);
      gtk_text_buffer_get_iter_at_mark (text_buffer, &start, mark);
      gtk_text_buffer_delete_mark (text_buffer, mark);

      /* markup quotes */
      line = PSTRING_INIT;
      pch = last_quote_begin = body;
      mark_start = start;
      while (get_next_token_pstring (pch, '\n', &pch, &line))
      {
            if (!line.len || (line.len==1 && *line.str=='\n'))
                  continue;
            
            if (!is_sig)
                  is_sig = pan_is_signature_delimiter (line.str, line.len) != SIG_NONE;

            quote_tag = get_quote_tag (text_massager, line.str, line.len);
            if (strcmp (quote_tag, "quote_0"))
                  is_sig = FALSE;
            
            quote_tag = is_sig?"signature":quote_tag;
            if (last_quote_tag!=NULL && strcmp (quote_tag, last_quote_tag)) {
                  mark_end = mark_start;
                  gtk_text_iter_forward_chars (&mark_end, g_utf8_strlen(last_quote_begin,line.str-1-last_quote_begin));
                  gtk_text_buffer_apply_tag_by_name (text_buffer, last_quote_tag, &mark_start, &mark_end);
                  mark_start = mark_end;
                  gtk_text_iter_forward_chars (&mark_start, 1);
                  last_quote_begin = line.str;
            }
            last_quote_tag = quote_tag;
      }
      if (last_quote_tag != NULL) {
            gtk_text_buffer_get_end_iter (text_buffer, &mark_end);
            gtk_text_buffer_apply_tag_by_name (text_buffer, last_quote_tag, &mark_start, &mark_end);
      }

      /* markup URLs */
      pch = body;
      while ((pch = find_next_url (pch))) {
            const char * url_start = pch;
            char * url = url_extract (&pch, strlen(pch), TRUE, FALSE);
            if (url == NULL)
                  ++pch;
            else {
                  mark_start = start;
                  gtk_text_iter_forward_chars (&mark_start, g_utf8_strlen(body,url_start-body));
                  mark_end = mark_start;
                  gtk_text_iter_forward_chars (&mark_end, g_utf8_strlen(url,-1));
                  gtk_text_buffer_remove_all_tags (text_buffer, &mark_start, &mark_end);
                  gtk_text_buffer_apply_tag_by_name (text_buffer, "url", &mark_start, &mark_end);
                  g_free (url);
            }
      }

      /* cleanup */
      g_free (freeme1);
      g_free (freeme2);
      debug_exit ("append_text_buffer_nolock");
}

/**
 * Clears out the old buffer contents and populates it with body.
 * @param text_massager holds information on how to wrap/quote the text
 * @param text_buffer the text buffer to repopulate.
 * @param body the new text to be placed in the buffer
 * @param mute_quotes true if quotes are to be muted in the buffer, false otherwise.
 */
void
update_body_pane (const TextMassager  * text_massager,
                  GtkTextBuffer       * text_buffer,
                  const char          * body,
              gboolean              mute_quotes)
{
      debug_enter ("update_body_pane");

      pan_lock ();
      clear_text_buffer_nolock (text_buffer);
      append_text_buffer_nolock (text_massager, text_buffer, body, mute_quotes);
      pan_unlock ();

      debug_exit ("update_body_pane");
}


/**
***  Font
**/

void
text_set_font (void)
{
      if (body_pane_monospace_font_enabled)
      {
            pan_widget_set_font (GTK_WIDGET(Pan.text), body_pane_monospace_font);
      }
      else if (body_pane_custom_font_enabled)
      {
            pan_widget_set_font (GTK_WIDGET(Pan.text), body_pane_custom_font);
      }
      else
      {
            GtkStyle * style = gtk_widget_get_default_style ();
            char * name = pango_font_description_to_string (style->font_desc);
            pan_widget_set_font (Pan.text, name);
            g_free (name);
      }

      text_refresh ();
}



/***
****
****   SPACE READING
****
***/

static void
sylpheed_textview_smooth_scroll_do (GtkAdjustment  * vadj,
                                    gfloat           old_value,
                                    gfloat           last_value,
                                    int              step)
{
      int i;
      int change_value;
      gboolean up;

      if (old_value < last_value) {
            change_value = last_value - old_value;
            up = FALSE;
      } else {
            change_value = old_value - last_value;
            up = TRUE;
      }

      /*FIXME? gdk_key_repeat_disable (); */

      for (i=step; i<=change_value; i+=step)
            gtk_adjustment_set_value (vadj, old_value+(up?-i:i));
      gtk_adjustment_set_value (vadj, last_value);

      /*gdk_key_repeat_restore (); */
}

static void
text_read_next (gboolean more)
{
      if (more)
            header_pane_read_next ();
      else
            header_pane_read_prev ();
}

static void
text_read_more_with_article (Article * a, gpointer more_gpointer)
{
      gboolean more = more_gpointer != NULL;
      GMimeMessage * message = get_current_message ();
      const int arbitrary_font_height_pixels_hack = 18;
      Group * grouplist_group = grouplist_get_selected_group ();
      const Group * articlelist_group = articlelist_get_group ();
      const char * art_msgid = a ? article_get_message_id (a) : NULL;
      const char * cur_msgid = message ? g_mime_message_get_message_id (message) : NULL;
      GtkAdjustment * v = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW(scrolled_window));
      const float inc = v->page_size - arbitrary_font_height_pixels_hack;
      const gfloat val = CLAMP (v->value + (more ? inc : -inc),
                                v->lower,
                                MAX(v->upper,v->page_size)-MIN(v->upper,v->page_size));

      if (v->upper>=v->page_size && val!=v->value) {
            /* this article has some lines left to read */
            if (Pan.text->parent==scrolled_window && text_window_smooth_scrolling)
                  sylpheed_textview_smooth_scroll_do (v, v->value, val, text_window_smooth_scrolling_speed);
            else
                  gtk_adjustment_set_value (v, val);
      }
      else if (grouplist_group!=NULL && grouplist_group!=articlelist_group) {
            /* if user has changed group selection, change group. */
            articlelist_set_group (grouplist_group);
      }
      else if (pan_strcmp (art_msgid, cur_msgid)) {
            /* if the user has selected a different article, jump to that. */
            articlelist_activate_selected ();
      }
      else {
            /* just go to the next article */
            text_read_next (more);
      }

      if (message != NULL)
            g_object_unref (message);
}

static void
text_read_more_impl (gboolean more)
{
      const gpointer more_gpointer = GINT_TO_POINTER(more);

      if (header_pane_has_selection ())
            header_pane_first_selected (text_read_more_with_article, more_gpointer);
      else
            text_read_more_with_article (NULL, more_gpointer);
}


void
text_read_more (void)
{
      text_read_more_impl (TRUE);
}
void
text_read_less (void)
{
      text_read_more_impl (FALSE);
}

/****
*****
*****   SETTING THE TEXT FROM A RAW TEXT MESSAGE
*****
****/

static int
text_set_raw_mainthread (gpointer data)
{
      GtkTextIter start;
      GtkTextIter end;
      char * text = (char*) data;
      debug_enter ("text_set_raw_mainthread");

      if (text == NULL)
            text = g_strdup (" ");

      pan_lock ();
      gtk_text_buffer_get_bounds (_text_buffer, &start, &end);
      gtk_text_buffer_delete (_text_buffer, &start, &end);
      gtk_text_buffer_insert (_text_buffer, &start, text, -1);
      pan_unlock ();

      g_free (text);
      debug_exit ("text_set_raw_mainthread");
      return 0;
}

void
text_set_raw (const char * text)
{
      gui_queue_add (text_set_raw_mainthread, g_strdup(text));
}


/****
*****
*****   SETTING THE TEXT FROM AN ARTICLE
*****
****/


/**
 * Generates a GtkPixmap object from a given GMimePart that contains an image.
 * Used for displaying attached pictures inline.
 */
static GdkPixbuf*
get_pixbuf_from_gmime_part (const GMimePart * part)
{
      guint len;
      const char * content;
      GdkPixbuf * pixbuf = NULL;
      GdkPixbufLoader * l = NULL;

      /* create the loader */
      l = gdk_pixbuf_loader_new ();

      /* create the pixbuf */
      content = g_mime_part_get_content (part, &len);
      gdk_pixbuf_loader_write (l, (const guchar*)content, len, NULL);
      pixbuf = gdk_pixbuf_loader_get_pixbuf (l);
      gdk_pixbuf_loader_close (l, NULL);

      /* cleanup */
      if (pixbuf != NULL)
            g_object_ref (G_OBJECT(pixbuf));
      g_object_unref (G_OBJECT(l));

      return pixbuf;
}

typedef struct
{
      const TextMassager * text_massager;
      const char * default_charset;
      const GMimeMessage * message;
      GtkTextBuffer * buffer;
}
InsertPartStruct;

static void
insert_part_partfunc (GMimeObject * obj, gpointer ips_gpointer)
{
      GMimePart * part;
      InsertPartStruct * ips;
      const GMimeContentType * type;
      YencInfo * yenc;

      /* We are only looking for leaf parts... */
      if (!GMIME_IS_PART (obj))
            return;
      
      part = GMIME_PART (obj);
      ips = (InsertPartStruct*) ips_gpointer;
      type = g_mime_object_get_content_type (GMIME_OBJECT (part));
      yenc = g_object_get_data (G_OBJECT(obj), "yenc");
      
      if (yenc!= NULL
            && part->content!=NULL
            && part->content->stream!=NULL
            && g_object_get_data(G_OBJECT(part),"Y_DECODER_INSTALLED")==NULL)
      {
            GMimeStream * stream;
             
            stream = g_mime_stream_filter_new_with_stream (part->content->stream);
            g_mime_stream_filter_add (GMIME_STREAM_FILTER(stream), g_mime_filter_yenc_new (GMIME_FILTER_YENC_DIRECTION_DECODE));
            g_mime_data_wrapper_set_stream (part->content, stream);
            g_object_set_data (G_OBJECT(part), "Y_DECODER_INSTALLED", GINT_TO_POINTER(1));

            g_mime_stream_unref (stream);
      }

      if (g_mime_content_type_is_type (type, "image", "*"))
      {
            GdkPixbuf * pixbuf = get_pixbuf_from_gmime_part (part);

            if (pixbuf != NULL)
            {
                  GtkTextIter iter;
                  gtk_text_buffer_get_end_iter (ips->buffer, &iter);
                  gtk_text_buffer_insert (ips->buffer, &iter, "\n", -1);
                  gtk_text_buffer_insert_pixbuf (ips->buffer, &iter, pixbuf);
                  gtk_text_buffer_insert (ips->buffer, &iter, "\n", -1);
                  g_object_unref (pixbuf);
            }
      }
      else if (g_mime_content_type_is_type (type, "text", "*"))
      {
            char * str = pan_body_to_utf8 (part, ips->default_charset);

            if (text_get_wrap() && is_nonempty_string(str))
                  replace_gstr (&str, text_massager_fill (ips->text_massager, str));

                  append_text_buffer_nolock (ips->text_massager, ips->buffer, str, text_get_mute_quoted());

            g_free (str);
      }
}
static void
add_header (GtkTextBuffer * tbuf, int key_width, const char * header, const char * value, iconv_t iconv_utf8)
{
      if (is_nonempty_string (value))
      {
            char buf1[64];
            char buf2[64];
            char buf3[1024];
            GtkTextIter end;
            char * utf8_value;
            glong utf8_len;

            utf8_value  = g_mime_iconv_strdup (iconv_utf8, value);
            if (!utf8_value)
            {
                  const char * charset = get_charset_from_locale ();
                  iconv_t cd = g_mime_iconv_open ("UTF-8", charset);
                  utf8_value = g_mime_iconv_strdup (cd, value);
                  g_mime_iconv_close (cd);
            }
            if (!utf8_value)
                  utf8_value = g_strdup ("?");
            utf8_len = g_utf8_strlen (utf8_value, -1);


            if (!text_get_show_all_headers() && g_strstr_len (utf8_value, utf8_len, "=?"))
                  replace_gstr (&utf8_value, g_mime_utils_8bit_header_decode (utf8_value));

            gtk_text_buffer_get_end_iter (tbuf, &end);

            g_snprintf (buf1, sizeof(buf1), "%s:", header);
            g_snprintf (buf2, sizeof(buf2), "%%-%ds", key_width+3);
            g_snprintf (buf3, sizeof(buf3), buf2, buf1);
            gtk_text_buffer_insert_with_tags_by_name (tbuf, &end, buf3, -1, "header_key", NULL);

            g_snprintf (buf3, sizeof(buf3), "%s\n", utf8_value);
            gtk_text_buffer_insert_with_tags_by_name (tbuf, &end, buf3, -1, "header_val", NULL);

            g_free (utf8_value);
      }
}

typedef struct
{
      const char * key;
      const char * value;
}
AddHeaderStruct;

static void
add_headers (GtkTextBuffer * tbuf, AddHeaderStruct * headers, int header_qty, const char * default_charset)
{
      int i;
      int max_key_len = 0;

      /* sanity clause */
      g_return_if_fail (tbuf!=NULL);
      g_return_if_fail (headers!=NULL);
      g_return_if_fail (header_qty>=0);
      for (i=0; i<header_qty; ++i)
            g_return_if_fail (is_nonempty_string (headers[i].key));

      /* find the maximum header name length */
      for (i=0; i<header_qty; ++i) {
            const size_t len = strlen (headers[i].key);
            if (len > max_key_len)
                  max_key_len = len;
      }

      /* add the headers */
      if (1) {
            iconv_t cd = g_mime_iconv_open ("UTF-8", default_charset);
            for (i=0; i<header_qty; ++i)
                  if (is_nonempty_string(headers[i].key))
                        add_header (tbuf, max_key_len, headers[i].key, headers[i].value, cd);
            g_mime_iconv_close (cd);
      }
}

static void
add_newline (GtkTextBuffer * tbuf)
{
      GtkTextIter end;
      gtk_text_buffer_get_end_iter (tbuf, &end);
      gtk_text_buffer_insert (tbuf, &end, "\n", -1);
}

static void
add_header_info_nolock (GMimeMessage * message, GtkTextBuffer * tbuf, gulong header_flags, const char * default_charset)
{
      int header_qty = 0;
      AddHeaderStruct headers[128];

      if (header_flags & UI_HEADER_SUBJECT) {
            headers[header_qty].key = _("Subject");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_SUBJECT);
      }
      if (header_flags & UI_HEADER_AUTHOR) {
            headers[header_qty].key = _("From");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_FROM);
      }
      if (header_flags & UI_HEADER_REPLY_TO) {
            headers[header_qty].key = _("Reply-To");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_REPLY_TO);
      }
      if (header_flags & UI_HEADER_NEWSGROUPS) {
            headers[header_qty].key = _("Newsgroups");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_NEWSGROUPS);
      }
      if (header_flags & UI_HEADER_FOLLOWUP_TO) {
            headers[header_qty].key = _("Followup-To");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_FOLLOWUP_TO);
      }
      if (header_flags & UI_HEADER_MESSAGE_ID) {
            headers[header_qty].key = _("Message-ID");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_MESSAGE_ID);
      }
      if (header_flags & UI_HEADER_REFERENCES) {
            headers[header_qty].key = _("References");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_REFERENCES);
      }
      if (header_flags & UI_HEADER_NEWSREADER) {
            headers[header_qty].key = _("X-Newsreader");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_X_NEWSREADER);
            headers[header_qty].key = _("X-Mailer");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_X_MAILER);
            headers[header_qty].key = _("User-Agent");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_USER_AGENT);
      }
      if (header_flags & UI_HEADER_DATE) {
            headers[header_qty].key = _("Date");
            headers[header_qty++].value = g_mime_message_get_header (message, HEADER_DATE);
      }

      if (header_qty > 0) {
            add_headers (tbuf, headers, header_qty, default_charset);
            add_newline (tbuf);
      }
}

static void
add_to_headerstruct_func (const char *name, const char *value, gpointer data)
{
      AddHeaderStruct h;
      h.key = name;
      h.value = value;
      g_array_append_val ((GArray*)data, h);
}

static int
compare_headerstructs (gconstpointer va, gconstpointer vb)
{
      const AddHeaderStruct * a = (const AddHeaderStruct*) va;
      const AddHeaderStruct * b = (const AddHeaderStruct*) vb;
      return strcmp (a->key, b->key);
}
 
static void
set_text_from_message_nolock (const TextMassager   * text_massager,
                              GMimeMessage         * message)
{
      const char * default_charset = (const char*) message ? g_object_get_data (G_OBJECT(message), "default_charset") : NULL;
      debug_enter ("set_text_from_message_nolock");

      /* clear */
      clear_text_buffer_nolock (_text_buffer);

      /* add headers */
      if (message!=NULL)
      {
            if (!text_get_show_all_headers())
            {
                  add_header_info_nolock (message, _text_buffer, header_flags, default_charset);
            }
            else
            {
                  GArray * headers = g_array_new (FALSE, FALSE, sizeof(AddHeaderStruct));
                  g_mime_header_foreach (GMIME_OBJECT(message)->headers, add_to_headerstruct_func, headers);
                  g_array_sort (headers, compare_headerstructs);
                  if (message->mime_part) {
                        add_to_headerstruct_func ("MIME-Version", "1.0", headers);
                        g_mime_header_foreach (message->mime_part->headers, add_to_headerstruct_func, headers);
                  }
                  add_headers (_text_buffer, (AddHeaderStruct*)headers->data, headers->len, default_charset);
                  add_newline (_text_buffer);
                  g_array_free (headers, TRUE);
            }
      }

      /* make a mark _after_ the headers so that we know where to get the body
         from when the user hits reply/follow-up */
      if (1) {
            GtkTextIter iter;
            gtk_text_buffer_get_end_iter (_text_buffer, &iter);
            gtk_text_buffer_create_mark (_text_buffer, "body_begin", &iter, TRUE);
      }

      /* add body */
      if (message)
      {
            InsertPartStruct ips;
            ips.text_massager = text_massager;
            ips.default_charset = default_charset;
            ips.message = message;
            ips.buffer = _text_buffer;
            g_mime_message_foreach_part (message, insert_part_partfunc, &ips);
      }

      gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW(Pan.text), _begin_mark, 0.0, FALSE, 0.0, 0.0);

      /* cleanup */
      debug_exit ("set_text_from_message_nolock");
}


/****
*****
*****  CURRENT MESSAGE
*****
****/

static GMimeMessage * _current_message = NULL;
static GStaticMutex _current_message_lock = G_STATIC_MUTEX_INIT;

GMimeMessage*
get_current_message (void)
{
      GMimeMessage * retval;

      /* return refcounted message pointer */
      g_static_mutex_lock (&_current_message_lock);
      retval = _current_message;
      if (retval != NULL)
            g_object_ref (retval);
      g_static_mutex_unlock (&_current_message_lock);

      return retval;
}

gboolean
text_pane_has_message (void)
{
      return _current_message != NULL;
}

static void
set_current_message_impl (GMimeMessage * message)
{
      GMimeMessage * old_mm = NULL;
      debug_enter ("set_current_message_impl");

      /* update the current */
      g_static_mutex_lock (&_current_message_lock);
      old_mm = _current_message;
      _current_message = message;
      if (_current_message != NULL)
            g_object_ref (_current_message);
      g_static_mutex_unlock (&_current_message_lock);

      /* fire notification callback */
      pan_callback_call (current_article_changed, old_mm, _current_message);

      /* unref the old */
      if (old_mm!=NULL)
            g_object_unref (old_mm);

      debug_enter ("set_current_message_impl");
}

/**
***
**/

static int
set_current_message_mainthread (GMimeMessage * message)
{
      const TextMassager * text_massager = text_pane_get_text_massager ();

      debug_enter ("set_current_message_mainthread");

      /* update the current message */
      set_current_message_impl (message);

      /* refresh the gui */
      if (message != NULL)
      {
            pan_lock ();
            set_text_from_message_nolock (text_massager, message);
            pan_unlock ();
            pan_g_mime_message_mark_read (message);

            /* FIXME articles_set_read (&current_article, 1, TRUE); */
      }
      else
      {
            pan_lock ();
            set_text_from_message_nolock (text_massager, NULL);
            pan_unlock ();
      }

      if (message != NULL) /* pair with set_current_message */
            g_object_unref (message);
      debug_exit ("set_current_message_mainthread");
      return 0;
}

static void
set_current_message (GMimeMessage * message)
{
      if (message != NULL) {
            g_object_ref (message); /* pair with set_current_message_mainthread */
            /* must be called _before_ set_current_message_mainthread 
             * so that that function's scroll-to-mark "start" works right*/
            gui_page_set (BODY_PANE);
      }
      gui_queue_add ((GSourceFunc)set_current_message_mainthread, message);
}

void
text_clear_nolock (void)
{
      /* clear the message */
      set_current_message_impl (NULL);

      /* update gui */
      set_text_from_message_nolock (text_pane_get_text_massager(), NULL);
}

void
text_refresh (void)
{
      debug_enter ("text_refresh");

      if (Pan.text!=NULL && _current_message!=NULL)
            set_current_message (_current_message);

      debug_exit ("text_refresh");
}

static void
task_bodies_ran_cb (gpointer call_obj, gpointer call_arg, gpointer user_data)
{
      Task * task = TASK (call_obj);
      char * default_charset = (char*) PAN_OBJECT(task)->user_data;
      const int status = GPOINTER_TO_INT (call_arg);
      const char * acache_key = (const char*) user_data;

      if (status == TASK_OK)
      {
            const PString ** ids = message_identifiers_get_id_array ((const MessageIdentifier**)task->identifiers->pdata, task->identifiers->len);
            GMimeMessage * message = acache_get_message (acache_key, ids, task->identifiers->len);
            g_free (ids);

            if (message != NULL)
            {
                  const char * charset = pan_g_mime_message_get_charset (message);

                  if (!is_nonempty_string (charset))
                        charset = default_charset;

                  g_object_set_data_full (G_OBJECT(message), "default_charset", g_strdup(charset), g_free);
                  set_current_message (message);
                  g_object_unref (message);
            }
      }

      g_free (default_charset);
}

void
text_set_from_identifiers (Server             * server,
                           const char         * acache_key,
                           const char         * default_charset,
                           MessageIdentifier ** mids,
                           int                  mid_qty)
{
      if (mid_qty == 0)
      {
            set_current_message (NULL);
      }
      else
      {
            GMimeMessage * message;
            const PString ** ids;

            g_return_if_fail (is_nonempty_string(acache_key));
            g_return_if_fail (server != NULL);
            g_return_if_fail (mids != NULL);
            g_return_if_fail (mid_qty > 0);

            ids = message_identifiers_get_id_array ((const MessageIdentifier**)mids, mid_qty);
            message = acache_get_message (acache_key, ids, mid_qty);
            g_free (ids);

            if (message)
            {
                  const char * charset = pan_g_mime_message_get_charset (message);

                  if (!is_nonempty_string (charset))
                        charset = default_charset;

                  g_object_set_data_full (G_OBJECT(message), "default_charset", g_strdup(charset), g_free);
                  set_current_message (message);
                  g_object_unref (message);
                  message = NULL;
            }
            else
            {
                  Task * task = TASK (task_bodies_new (server, mids, mid_qty));
                  if (task != NULL)
                  {
                        PAN_OBJECT(task)->user_data = g_strdup (default_charset);
                        pan_callback_add (task->task_ran_callback, task_bodies_ran_cb, (gpointer)acache_key);
                        queue_add (task);
                  }
            }
      }
}

/****
*****
*****    CALLBACKS
*****
****/

static void
header_pane_group_changed_cb (gpointer call_obj, gpointer call_arg, gpointer client_data)
{
      set_current_message (NULL);
}

static int
compare_pgchar_pparticle_msgid (const void * a, const void *b)
{
      const gchar * msgid_a = (const gchar*) a;
      const gchar * msgid_b = article_get_message_id (*(const Article**)b);
      pan_warn_if_fail (is_nonempty_string(msgid_a));
      pan_warn_if_fail (is_nonempty_string(msgid_b));
      return pan_strcmp (msgid_a, msgid_b);
}     

static void
group_articles_removed_cb (gpointer call_obj, gpointer call_arg, gpointer client_data)
{
      Group * group = GROUP(call_obj);
      GPtrArray * removed = (GPtrArray*) call_arg;
      GMimeMessage * message;
      debug_enter ("group_articles_removed_cb");

      /* sanity checks */
      g_return_if_fail (group!=NULL);
      g_return_if_fail (removed!=NULL);
      g_return_if_fail (removed->len>0u);

      /* unset the current article if we need to */
      message = get_current_message ();
      if (message!=NULL)
      {
            gboolean exact_match = FALSE;
            const char * message_id = g_mime_message_get_message_id (message);

            lower_bound (message_id,
                         removed->pdata,
                         removed->len,
                       sizeof(gpointer),
                       compare_pgchar_pparticle_msgid,
                       &exact_match);

            if (exact_match)
                  set_current_message (NULL);

            g_object_unref (message);
      }

      debug_exit ("group_articles_removed_cb");
}

void
text_rot13_selected_text_nolock (void)
{
      GtkTextIter sel_start;
      GtkTextIter sel_end;

      debug_enter ("text_set_rot13");

      if (gtk_text_buffer_get_selection_bounds (_text_buffer, &sel_start, &sel_end))
      {
            char * text = gtk_text_iter_get_text (&sel_start, &sel_end);
            text_massager_rot13_inplace (text_pane_get_text_massager(), text);
            gtk_text_buffer_delete (_text_buffer, &sel_start, &sel_end);
            gtk_text_buffer_insert (_text_buffer, &sel_end, text, -1);
            g_free (text);
      }

      debug_exit ("text_set_rot13");
}

/**
*** 
***/

static gboolean
text_key_pressed (GtkWidget * w, GdkEventKey * event, gpointer unused)
{
      gboolean retval = FALSE;
      gboolean up, down;

      g_return_val_if_fail (GTK_IS_TEXT_VIEW(w), FALSE);

      up = event->keyval==GDK_Up || event->keyval==GDK_KP_Up;
      down = event->keyval==GDK_Down || event->keyval==GDK_KP_Down;

      if (up || down)
      {
            GtkAdjustment * adj;
            gdouble         val;
            
                  gtk_text_view_place_cursor_onscreen (GTK_TEXT_VIEW(w));
            adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW(scrolled_window));
            val = adj->value;
            if (up)
                  val -= adj->step_increment;
            else
                  val += adj->step_increment;

            val = MAX(val, adj->lower);
            val = MIN(val, adj->upper-adj->page_size);

            /*odebug5 ("adj: %.1f -> %.1f (%.1f/%.1f)",
                  adj->value, val, adj->lower, adj->upper-adj->page_size);*/

            gtk_adjustment_set_value (adj, val);

            retval = TRUE;
      }

      return retval;
}
/****
*****
*****    TEXT WIDGET STARTUP
*****
****/

static char*
get_url_from_location (GtkWidget * w, int x, int y)
{
      char * retval = NULL;
      gboolean clicked_on_url;
      GtkTextBuffer * text_buffer;
      static GtkTextTag * url_tag = NULL;
      GtkTextIter pos;
      int old_x = x, old_y = y;

      /* get the buffer */
      g_return_val_if_fail (GTK_IS_TEXT_VIEW(w), FALSE);
      text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(w));

      /* translate coordinates */
      gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW(w), GTK_TEXT_WINDOW_TEXT, old_x, old_y, &x, &y);

      /* did the user click on a url? */
      gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW(w), &pos, x, y);
      if (url_tag == NULL) { 
            GtkTextTagTable * tag_table = gtk_text_buffer_get_tag_table (text_buffer);
            url_tag = gtk_text_tag_table_lookup (tag_table, "url");
            if (url_tag == NULL)
                  return NULL;
      }
      
      /* ASSERTION FAILED */
      clicked_on_url = gtk_text_iter_has_tag (&pos, url_tag);
      if (clicked_on_url)
      {
            GtkTextIter begin;
            GtkTextIter end;

            /* get the URL */
            begin = end = pos;
            gtk_text_iter_backward_to_tag_toggle (&begin, NULL);
            gtk_text_iter_forward_to_tag_toggle (&end, NULL);
            retval = gtk_text_iter_get_text (&begin, &end);
      }

      return retval;
}

/**
 * Listen to button presses to see if the user has clicked on a URL.
 * If they have, we pass the URL to pan_url_show().
 */
static gboolean
text_button_pressed (GtkWidget * w, GdkEventButton * event, gpointer unused)
{
      g_return_val_if_fail (GTK_IS_TEXT_VIEW(w), FALSE);

      if (event->button==1 || event->button==2)
      {
            char * url = get_url_from_location (w, (int)event->x, (int)event->y);
            if (url != NULL)
            {
                  /* this is kind of a crude way of making sure that double-click
                   * doesn't open two or three browser windows. */
                  static time_t last_url_time = 0;
                  const time_t this_url_time = time (NULL);
                  if (this_url_time != last_url_time)
                  {
                        last_url_time = this_url_time;
                        pan_url_show (url);
                        g_free (url);
                  }
            }
      }

      return FALSE;
}

/**
 * Listen to button presses to see if the user mouses onto or off of a URL.
 * If they have, change the text widget's pointer icon appropriately.
 */
static gboolean
motion_notify_event (GtkWidget * w, GdkEventMotion * event, gpointer user_data)
{
      static GdkCursor * cursor_current = NULL;
      static GdkCursor * cursor_ibeam = NULL;
      static GdkCursor * cursor_href = NULL;

      if (event->window != NULL)
      {
            int x, y;
            char * url;
            GdkCursor * cursor_new;
            GdkModifierType state;

            /* initialize static variables */
            if (!cursor_ibeam)
                  cursor_ibeam = gdk_cursor_new (GDK_XTERM);
            if (!cursor_href)
                  cursor_href = gdk_cursor_new (GDK_HAND2);

            /* pump out x, y, and state */
            if (event->is_hint)
                  gdk_window_get_pointer (event->window, &x, &y, &state);
            else {
                  x = event->x;
                  y = event->y;
                  state = event->state;
            }

            /* decide what cursor we should be using */
            url = get_url_from_location (w, (int)event->x, (int)event->y);
            if (!url)
                  cursor_new = cursor_ibeam;
            else {
                  cursor_new = cursor_href;
                  g_free (url);
            }

            /* change the cursor if needed */
            if (cursor_new != cursor_current)
                  gdk_window_set_cursor (event->window, cursor_current=cursor_new);
      }

      return FALSE;
}


/**
 ** gtk_text_tag_table_install:
 ** @table: a #GtkTextTagTable
 ** @tag: a #GtkTextTag
 ** 
 ** Add @tag to the tag table @table or, if a tag with the same name
 ** already exists in @table, update the existing tag in the table with
 ** the values in @tag. If the tag is anonymous, it is simply added to
 ** the table.
 **/
static void
gtk_text_tag_table_install (GtkTextTagTable *table, GtkTextTag *tag)
{
      g_return_if_fail (table);
      g_return_if_fail (GTK_IS_TEXT_TAG_TABLE (table));
      g_return_if_fail (tag);
      g_return_if_fail (GTK_IS_TEXT_TAG (tag));
          
      if (!tag->name)
            gtk_text_tag_table_add(table, tag);
      else {
            GtkTextTag * existing_tag = gtk_text_tag_table_lookup (table, tag->name);

            if (existing_tag)
                  gtk_text_tag_table_remove (table, existing_tag);

            gtk_text_tag_table_add (table, tag);
      }
}

/* setup the buffer tags */
void
text_set_text_buffer_tags (GtkTextBuffer * buffer)
{
      GtkTextTag * tag;
      GtkTextTagTable * table = gtk_text_buffer_get_tag_table (buffer);
      char * header_key_font = NULL;
      char * header_val_font = NULL;
      PangoFontDescription * pfd = pango_font_description_from_string (body_pane_monospace_font);

      if (pfd) {
            header_val_font = pango_font_description_to_string (pfd);
            pango_font_description_set_weight (pfd, PANGO_WEIGHT_BOLD);
            header_key_font = pango_font_description_to_string (pfd);
            pango_font_description_free (pfd);
      }

      if (!is_nonempty_string (header_key_font))
            header_key_font = g_strdup ("courier bold");
      if (!is_nonempty_string (header_val_font))
            header_val_font = g_strdup ("courier");

      tag = gtk_text_tag_new ("header_key");
      g_object_set (tag, "font", header_key_font, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);

      tag = gtk_text_tag_new ("header_val");
      g_object_set (tag, "font", header_val_font, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);

      tag = gtk_text_tag_new ("center");
      g_object_set (tag, "justification", GTK_JUSTIFY_CENTER, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);
      
      tag = gtk_text_tag_new ("url");
      g_object_set (tag, "underline", PANGO_UNDERLINE_SINGLE, "foreground_gdk", &text_url_color, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);
      
      tag = gtk_text_tag_new ("quote_0");
      g_object_set (tag, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);
      
      tag = gtk_text_tag_new ("quote_1");
      g_object_set (tag, "foreground_gdk", &text_quoted_color[0], NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);
      
      tag = gtk_text_tag_new ("quote_2");
      g_object_set (tag, "foreground_gdk", &text_quoted_color[1], NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);

      tag = gtk_text_tag_new ("quote_3");
      g_object_set (tag, "foreground_gdk", &text_quoted_color[2], NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);

      tag = gtk_text_tag_new ("signature");
      g_object_set (tag, "foreground_gdk", &signature_color, NULL);
      gtk_text_tag_table_install (table, tag);
      g_object_unref (tag);

      g_free (header_key_font);
      g_free (header_val_font);
}

      
GtkWidget *
text_create (void)
{
      GtkWidget * text_view;
      
      /* create the text view */
      text_view = gtk_text_view_new ();
      gtk_widget_add_events (text_view, GDK_POINTER_MOTION_MASK|GDK_POINTER_MOTION_HINT_MASK);

      g_signal_connect (text_view, "motion_notify_event",
                        G_CALLBACK(motion_notify_event), NULL);
      g_signal_connect (text_view, "button_press_event",
                        G_CALLBACK(text_button_pressed), NULL);
      g_signal_connect (text_view, "key_press_event",
                        G_CALLBACK(text_key_pressed), NULL);
        gtk_container_set_border_width (GTK_CONTAINER(text_view), GUI_PAD_SMALL);
        gtk_text_view_set_editable (GTK_TEXT_VIEW(text_view), FALSE);
      gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW(text_view), FALSE);

      /* set up the buffer tags */
      _text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(text_view));
      text_set_text_buffer_tags (_text_buffer);

      /* set a beginning mark that we can always scroll to */
      if (1) {
            GtkTextIter start;
            gtk_text_buffer_get_start_iter (_text_buffer, &start);
            _begin_mark = gtk_text_buffer_create_mark  (_text_buffer, "start", &start, TRUE);
      }



      current_article_changed = pan_callback_new ();

      /* the larger scrolled window */
      scrolled_window = gtk_scrolled_window_new (NULL, NULL);
      gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(scrolled_window), GTK_SHADOW_IN);
      gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
                              GTK_POLICY_AUTOMATIC,
                              GTK_POLICY_AUTOMATIC);
      gtk_container_add (GTK_CONTAINER(scrolled_window), text_view);

      if (1) {
            GString * str = g_string_new (NULL);

            g_string_append_printf (str,
                  _("Pan %s\nCopyright (c) %d by Charles Kerr\n"
                    "\n"
                    "If you find a bug, please report it.\n"
                    "\n"), VERSION, 2003);
            g_string_append (str,
                  _("http://pan.rebelbase.com/ - Pan Homepage\n"
                    "http://pan.rebelbase.com/bugs/ - Report a Bug\n"
                    "http://pan.rebelbase.com/download/ - Upgrade\n"
                    "\n"));
            g_string_append (str,
                  _("This program is free software; you can redistribute it\n"
                    "and/or modify it under the terms of the GNU General Public\n"
                    "License as published by the Free Software Foundation;\n"
                    "version 2 of the License.\n"
                    "\n"
                    "This program is distributed in the hope that it will be\n"
                    "useful, but WITHOUT ANY WARRANTY; without even the implied\n"
                    "warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n"
                    "PURPOSE.  See the GNU General Public License for more\n"
                    "details.\n"
                    "\n"
                    "The GNU Public License can be found from the menu above\n"
                    "in Help|About|License."));
            append_text_buffer_nolock (text_pane_get_text_massager(), _text_buffer, str->str, FALSE);

            /* cleanup */
            g_string_free (str, TRUE);
      }

      /* text_box holds the header info and the scrolled text window */
        text_box = gtk_vbox_new (FALSE, 4);
      gtk_container_set_border_width (GTK_CONTAINER(text_box), 4);
        gtk_box_pack_start (GTK_BOX(text_box), scrolled_window, TRUE, TRUE, 0); 

      pan_callback_add (group_get_articles_removed_callback(),
                        group_articles_removed_cb, NULL);
      pan_callback_add (articlelist_get_group_changed_callback(),
                        header_pane_group_changed_cb, NULL);


      Pan.text = text_view;
      text_set_font ();
      return text_box;
}

/****
*****
*****    UTILITY FUNCTIONS FOR OUTSIDE CLIENTS
*****
****/

char*
text_get_message_to_reply_to (void)
{
      GtkTextIter sel_start;
      GtkTextIter sel_end;
      char * body = NULL;
      gboolean has_selection;
      debug_enter ("text_get_message_to_reply_to");

      /* get the selected text, if any */
      pan_lock ();
      has_selection = gtk_text_buffer_get_selection_bounds (_text_buffer, &sel_start, &sel_end);
      if (has_selection)
            body = gtk_text_iter_get_text (&sel_start, &sel_end);
      else
      {
            GtkTextMark * mark = gtk_text_buffer_get_mark (_text_buffer, "body_begin");
            gtk_text_buffer_get_iter_at_mark (_text_buffer, &sel_start, mark);
            gtk_text_buffer_get_end_iter (_text_buffer, &sel_end);
            body = gtk_text_buffer_get_text (_text_buffer, &sel_start, &sel_end, FALSE);
      }
      pan_unlock ();
 
      /* no selection, so user is replying to whole message sans signature */
      if (!has_selection && body!=NULL)
      {
            pan_remove_signature (body);
      }
      
      
      debug_exit ("text_get_message_to_reply_to");
      return body;
}

/****
*****
*****    MANIPULATORS:  WRAPPING
*****
****/

static gboolean _do_wrap = FALSE;

static PanCallback * _text_fill_body_changed_callback = NULL;

PanCallback*
text_get_fill_body_changed_callback (void)
{
      if (_text_fill_body_changed_callback == NULL)
            _text_fill_body_changed_callback = pan_callback_new ();

      return _text_fill_body_changed_callback;
}

void
text_set_wrap (gboolean wrap)
{
      debug_enter ("text_set_wrap");

      if (wrap != _do_wrap)
      {
            _do_wrap = wrap;

            pan_callback_call (text_get_fill_body_changed_callback(), NULL, GINT_TO_POINTER(wrap));
            text_refresh ();                            
      }

      debug_exit ("text_set_wrap");
}

gboolean
text_get_wrap (void)
{
      return _do_wrap;
}

/****
*****
*****    MANIPULATORS:  HEADERS
*****
****/

static gboolean _show_all_headers;

static PanCallback * _show_all_headers_changed_callback = NULL;

PanCallback*
text_get_show_all_headers_changed_callback (void)
{
      if (_show_all_headers_changed_callback == NULL)
            _show_all_headers_changed_callback = pan_callback_new ();

      return _show_all_headers_changed_callback;
}

void
text_set_show_all_headers (gboolean show)
{
      debug_enter ("text_set_show_all_headers");

      if (_show_all_headers != show)
      {
            _show_all_headers = show;
            pan_callback_call (text_get_show_all_headers_changed_callback(), NULL, GINT_TO_POINTER(show));
            text_refresh ();
      }

      debug_exit ("text_set_show_all_headers");
}

gboolean
text_get_show_all_headers (void)
{
      return _show_all_headers;
}

/****
*****
*****    MANIPULATORS:  MUTE QUOTED
*****
****/

static gboolean _mute_quoted = FALSE;

static PanCallback * _mute_quoted_changed_callback = NULL;

PanCallback*
text_get_mute_quoted_changed_callback (void)
{
      if (_mute_quoted_changed_callback == NULL)
            _mute_quoted_changed_callback = pan_callback_new ();

      return _mute_quoted_changed_callback;
}

void
text_set_mute_quoted (gboolean quoted)
{
      debug_enter ("text_set_mute_quoted");

      if (_mute_quoted != quoted)
      {
            _mute_quoted = quoted;
            pan_callback_call (text_get_mute_quoted_changed_callback(), NULL, GINT_TO_POINTER(quoted));
            text_refresh ();
      }

      debug_exit ("text_set_mute_quoted");
}

gboolean
text_get_mute_quoted (void)
{
      return _mute_quoted;
}

/**/

void
text_select_all (void)
{
      GtkTextIter start;
      GtkTextIter end;

      pan_lock();
      gtk_text_buffer_get_bounds (_text_buffer, &start, &end);
      gtk_text_buffer_move_mark (_text_buffer, gtk_text_buffer_get_insert(_text_buffer), &end);
      gtk_text_buffer_move_mark (_text_buffer, gtk_text_buffer_get_selection_bound (_text_buffer), &start);
      pan_unlock();
}

void
text_deselect_all (void)
{
      GtkTextIter start;
      GtkTextIter end;

      pan_lock();
      gtk_text_buffer_get_bounds (_text_buffer, &start, &end);
      gtk_text_buffer_move_mark (_text_buffer, gtk_text_buffer_get_insert(_text_buffer), &end);
      gtk_text_buffer_move_mark (_text_buffer, gtk_text_buffer_get_selection_bound (_text_buffer), &end);
      pan_unlock();
}

/**
***
**/

TextMassager* 
text_pane_get_text_massager (void)
{
      static TextMassager * text_massager = NULL;

      if (text_massager == NULL)
            text_massager = text_massager_new ();

      return text_massager;
}

Generated by  Doxygen 1.6.0   Back to index