[gimp] app: add infrastructure for editing pango markup based text styles



commit 360f5739da8345d037dfd6d2697ecd50450a0059
Author: Michael Natterer <mitch gimp org>
Date:   Fri Feb 26 01:22:27 2010 +0100

    app: add infrastructure for editing pango markup based text styles
    
    - keep around tags for styles in GimpTextBuffer. For now only bold,
      italic, underline and strikethrough.
    - add GimpTextStyleEditor, a widget which allows setting tags on
      a GimpTextBuffer's selection.
    - add serialize/deserialize code to/from pango markup using
      GtkTextBuffer's rich text (de)serialization infrastructure.
      Doesn't produce or handle <span> yet.

 app/widgets/Makefile.am                |    4 +
 app/widgets/gimptextbuffer-serialize.c |  609 ++++++++++++++++++++++++++++++++
 app/widgets/gimptextbuffer-serialize.h |   46 +++
 app/widgets/gimptextbuffer.c           |  130 +++++++
 app/widgets/gimptextbuffer.h           |   16 +
 app/widgets/gimptextstyleeditor.c      |  423 ++++++++++++++++++++++
 app/widgets/gimptextstyleeditor.h      |   61 ++++
 app/widgets/widgets-types.h            |    1 +
 8 files changed, 1290 insertions(+), 0 deletions(-)
---
diff --git a/app/widgets/Makefile.am b/app/widgets/Makefile.am
index 86f8bf9..331c80d 100644
--- a/app/widgets/Makefile.am
+++ b/app/widgets/Makefile.am
@@ -298,10 +298,14 @@ libappwidgets_a_sources = \
 	gimptemplateview.h		\
 	gimptextbuffer.c		\
 	gimptextbuffer.h		\
+	gimptextbuffer-serialize.c	\
+	gimptextbuffer-serialize.h	\
 	gimptexteditor.c		\
 	gimptexteditor.h		\
 	gimptextproxy.c			\
 	gimptextproxy.h			\
+	gimptextstyleeditor.c		\
+	gimptextstyleeditor.h		\
 	gimpthumbbox.c			\
 	gimpthumbbox.h			\
 	gimptoggleaction.c		\
diff --git a/app/widgets/gimptextbuffer-serialize.c b/app/widgets/gimptextbuffer-serialize.c
new file mode 100644
index 0000000..7f9eb95
--- /dev/null
+++ b/app/widgets/gimptextbuffer-serialize.c
@@ -0,0 +1,609 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * GimpTextBuffer-serialize
+ * Copyright (C) 2010  Michael Natterer <mitch gimp org>
+ *
+ * inspired by
+ * gtktextbufferserialize.c
+ * Copyright (C) 2004  Nokia Corporation.
+ *
+ * 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; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gtk/gtk.h>
+
+#include "widgets-types.h"
+
+#include "gimptextbuffer.h"
+#include "gimptextbuffer-serialize.h"
+
+#include "gimp-intl.h"
+
+
+/*  serialize  */
+
+static void
+find_list_delta (GSList  *old_list,
+                 GSList  *new_list,
+		 GList  **added,
+                 GList  **removed)
+{
+  GSList *tmp;
+  GList  *tmp_added   = NULL;
+  GList  *tmp_removed = NULL;
+
+  /* Find added tags */
+  for (tmp = new_list; tmp; tmp = g_slist_next (tmp))
+    {
+      if (! g_slist_find (old_list, tmp->data))
+	tmp_added = g_list_prepend (tmp_added, tmp->data);
+    }
+
+  *added = tmp_added;
+
+  /* Find removed tags */
+  for (tmp = old_list; tmp; tmp = g_slist_next (tmp))
+    {
+      if (! g_slist_find (new_list, tmp->data))
+	tmp_removed = g_list_prepend (tmp_removed, tmp->data);
+    }
+
+  /* We reverse the list here to match the xml semantics */
+  *removed = g_list_reverse (tmp_removed);
+}
+
+static gboolean
+open_tag (GimpTextBuffer *buffer,
+          GString        *string,
+          GtkTextTag     *tag)
+{
+  const gchar *name = gimp_text_buffer_tag_to_name (buffer, tag);
+
+  if (tag)
+    {
+      g_string_append_printf (string, "<%s>", name);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+close_tag (GimpTextBuffer *buffer,
+           GString        *string,
+           GtkTextTag     *tag)
+{
+  const gchar *name = gimp_text_buffer_tag_to_name (buffer, tag);
+
+  if (tag)
+    {
+      g_string_append_printf (string, "</%s>", name);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+guint8 *
+gimp_text_buffer_serialize (GtkTextBuffer     *register_buffer,
+                            GtkTextBuffer     *content_buffer,
+                            const GtkTextIter *start,
+                            const GtkTextIter *end,
+                            gsize             *length,
+                            gpointer           user_data)
+{
+  GString     *string;
+  GtkTextIter  iter, old_iter;
+  GSList      *tag_list, *new_tag_list;
+  GSList      *active_tags;
+
+  string = g_string_new ("<markup>");
+
+  iter        = *start;
+  tag_list    = NULL;
+  active_tags = NULL;
+
+  do
+    {
+      GList *added, *removed;
+      GList *tmp;
+      gchar *tmp_text, *escaped_text;
+
+      new_tag_list = gtk_text_iter_get_tags (&iter);
+
+      find_list_delta (tag_list, new_tag_list, &added, &removed);
+
+      /* Handle removed tags */
+      for (tmp = removed; tmp; tmp = tmp->next)
+	{
+	  GtkTextTag *tag = tmp->data;
+
+          /* Only close the tag if we didn't close it before (by using
+           * the stack logic in the while() loop below)
+           */
+          if (g_slist_find (active_tags, tag))
+            {
+              /* Drop all tags that were opened after this one (which are
+               * above this on in the stack)
+               */
+              while (active_tags->data != tag)
+                {
+                  close_tag (GIMP_TEXT_BUFFER (content_buffer),
+                             string, active_tags->data);
+
+                  added = g_list_prepend (added, active_tags->data);
+                  active_tags = g_slist_remove (active_tags, active_tags->data);
+                }
+
+              /*  then, close the tag itself  */
+              close_tag (GIMP_TEXT_BUFFER (content_buffer), string, tag);
+
+              active_tags = g_slist_remove (active_tags, active_tags->data);
+            }
+	}
+
+      /* Handle added tags */
+      for (tmp = added; tmp; tmp = tmp->next)
+	{
+	  GtkTextTag *tag = tmp->data;
+
+          open_tag (GIMP_TEXT_BUFFER (content_buffer), string, tag);
+
+	  active_tags = g_slist_prepend (active_tags, tag);
+	}
+
+      g_slist_free (tag_list);
+      tag_list = new_tag_list;
+
+      g_list_free (added);
+      g_list_free (removed);
+
+      old_iter = iter;
+
+      /* Now try to go to either the next tag toggle, or if a pixbuf appears */
+      while (TRUE)
+	{
+	  gunichar ch = gtk_text_iter_get_char (&iter);
+
+	  if (ch == 0xFFFC)
+	    {
+	      /* pixbuf? can't happen! */
+	    }
+          else if (ch == 0)
+            {
+              break;
+            }
+	  else
+            {
+              gtk_text_iter_forward_char (&iter);
+            }
+
+	  if (gtk_text_iter_toggles_tag (&iter, NULL))
+	    break;
+	}
+
+      /* We might have moved too far */
+      if (gtk_text_iter_compare (&iter, end) > 0)
+	iter = *end;
+
+      /* Append the text */
+      tmp_text = gtk_text_iter_get_slice (&old_iter, &iter);
+      escaped_text = g_markup_escape_text (tmp_text, -1);
+      g_free (tmp_text);
+
+      g_string_append (string, escaped_text);
+      g_free (escaped_text);
+    }
+  while (! gtk_text_iter_equal (&iter, end));
+
+  /* Close any open tags */
+  for (tag_list = active_tags; tag_list; tag_list = tag_list->next)
+    close_tag (GIMP_TEXT_BUFFER (content_buffer), string, tag_list->data);
+
+  g_slist_free (active_tags);
+
+  g_string_append (string, "</markup>");
+
+  *length = string->len;
+
+  return (guint8 *) g_string_free (string, FALSE);
+}
+
+
+/*  deserialize  */
+
+typedef enum
+{
+  STATE_START,
+  STATE_MARKUP,
+  STATE_TAG,
+  STATE_UNKNOWN
+} ParseState;
+
+typedef struct
+{
+  GSList        *states;
+  GtkTextBuffer *buffer;
+  GSList        *tag_stack;
+  GList         *spans;
+} ParseInfo;
+
+typedef struct
+{
+  gchar  *text;
+  GSList *tags;
+} TextSpan;
+
+static void
+set_error (GError              **err,
+           GMarkupParseContext  *context,
+           int                   error_domain,
+           int                   error_code,
+           const char           *format,
+           ...)
+{
+  gint     line, ch;
+  va_list  args;
+  gchar   *str;
+
+  g_markup_parse_context_get_position (context, &line, &ch);
+
+  va_start (args, format);
+  str = g_strdup_vprintf (format, args);
+  va_end (args);
+
+  g_set_error (err, error_domain, error_code,
+               ("Line %d character %d: %s"),
+               line, ch, str);
+
+  g_free (str);
+}
+
+static void
+push_state (ParseInfo  *info,
+            ParseState  state)
+{
+  info->states = g_slist_prepend (info->states, GINT_TO_POINTER (state));
+}
+
+static void
+pop_state (ParseInfo *info)
+{
+  g_return_if_fail (info->states != NULL);
+
+  info->states = g_slist_remove (info->states, info->states->data);
+}
+
+static ParseState
+peek_state (ParseInfo *info)
+{
+  g_return_val_if_fail (info->states != NULL, STATE_START);
+
+  return GPOINTER_TO_INT (info->states->data);
+}
+
+static gboolean
+check_no_attributes (GMarkupParseContext  *context,
+                     const char           *element_name,
+                     const char          **attribute_names,
+                     const char          **attribute_values,
+                     GError              **error)
+{
+  if (attribute_names[0] != NULL)
+    {
+      set_error (error, context,
+                 G_MARKUP_ERROR,
+                 G_MARKUP_ERROR_PARSE,
+                 _("Attribute \"%s\" is invalid on <%s> element in this context"),
+                 attribute_names[0], element_name);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+parse_tag_element (GMarkupParseContext  *context,
+                   const gchar          *element_name,
+                   const gchar         **attribute_names,
+                   const gchar         **attribute_values,
+                   ParseInfo            *info,
+                   GError              **error)
+{
+  GtkTextTag *tag;
+
+  g_assert (peek_state (info) == STATE_MARKUP ||
+	    peek_state (info) == STATE_TAG    ||
+            peek_state (info) == STATE_UNKNOWN);
+
+  tag = gimp_text_buffer_name_to_tag (GIMP_TEXT_BUFFER (info->buffer),
+                                      element_name);
+
+  if (tag)
+    {
+      info->tag_stack = g_slist_prepend (info->tag_stack, tag);
+
+      push_state (info, STATE_TAG);
+    }
+  else
+    {
+      push_state (info, STATE_UNKNOWN);
+    }
+}
+
+static void
+start_element_handler (GMarkupParseContext  *context,
+		       const gchar          *element_name,
+		       const gchar         **attribute_names,
+		       const gchar         **attribute_values,
+		       gpointer              user_data,
+		       GError              **error)
+{
+  ParseInfo *info = user_data;
+
+  switch (peek_state (info))
+    {
+    case STATE_START:
+      if (! strcmp (element_name, "markup"))
+	{
+	  if (! check_no_attributes (context, element_name,
+                                     attribute_names, attribute_values,
+                                     error))
+	    return;
+
+	  push_state (info, STATE_MARKUP);
+	  break;
+	}
+      else
+        {
+          set_error (error, context, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
+                     _("Outermost element in text must be <markup> not <%s>"),
+                     element_name);
+        }
+      break;
+
+    case STATE_MARKUP:
+    case STATE_TAG:
+    case STATE_UNKNOWN:
+      parse_tag_element (context, element_name,
+                         attribute_names, attribute_values,
+                         info, error);
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+}
+
+static void
+end_element_handler (GMarkupParseContext  *context,
+		     const gchar          *element_name,
+		     gpointer              user_data,
+		     GError              **error)
+{
+  ParseInfo *info = user_data;
+
+  switch (peek_state (info))
+    {
+    case STATE_UNKNOWN:
+      pop_state (info);
+      g_assert (peek_state (info) == STATE_UNKNOWN ||
+                peek_state (info) == STATE_TAG     ||
+		peek_state (info) == STATE_MARKUP);
+      break;
+
+    case STATE_TAG:
+      pop_state (info);
+      g_assert (peek_state (info) == STATE_UNKNOWN ||
+                peek_state (info) == STATE_TAG     ||
+		peek_state (info) == STATE_MARKUP);
+
+      /* Pop tag */
+      info->tag_stack = g_slist_delete_link (info->tag_stack,
+					     info->tag_stack);
+      break;
+
+    case STATE_MARKUP:
+      pop_state (info);
+      g_assert (peek_state (info) == STATE_START);
+
+      info->spans = g_list_reverse (info->spans);
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+}
+
+static gboolean
+all_whitespace (const char *text,
+                gint        text_len)
+{
+  const char *p   = text;
+  const char *end = text + text_len;
+
+  while (p != end)
+    {
+      if (! g_ascii_isspace (*p))
+        return FALSE;
+
+      p = g_utf8_next_char (p);
+    }
+
+  return TRUE;
+}
+
+static void
+text_handler (GMarkupParseContext  *context,
+	      const gchar          *text,
+	      gsize                 text_len,
+	      gpointer              user_data,
+	      GError              **error)
+{
+  ParseInfo *info = user_data;
+  TextSpan  *span;
+
+  if (all_whitespace (text, text_len)   &&
+      peek_state (info) != STATE_MARKUP &&
+      peek_state (info) != STATE_TAG    &&
+      peek_state (info) != STATE_UNKNOWN)
+    return;
+
+  switch (peek_state (info))
+    {
+    case STATE_START:
+      g_assert_not_reached (); /* gmarkup shouldn't do this */
+      break;
+
+    case STATE_MARKUP:
+    case STATE_TAG:
+    case STATE_UNKNOWN:
+      if (text_len == 0)
+	return;
+
+      span = g_new0 (TextSpan, 1);
+      span->text = g_strndup (text, text_len);
+      span->tags = g_slist_copy (info->tag_stack);
+
+      info->spans = g_list_prepend (info->spans, span);
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+}
+
+static void
+parse_info_init (ParseInfo     *info,
+		 GtkTextBuffer *buffer)
+{
+  info->states    = g_slist_prepend (NULL, GINT_TO_POINTER (STATE_START));
+  info->tag_stack = NULL;
+  info->spans     = NULL;
+  info->buffer    = buffer;
+}
+
+static void
+text_span_free (TextSpan *span)
+{
+  g_free (span->text);
+  g_slist_free (span->tags);
+  g_free (span);
+}
+
+static void
+parse_info_free (ParseInfo *info)
+{
+  g_slist_free (info->tag_stack);
+  g_slist_free (info->states);
+
+  g_list_foreach (info->spans, (GFunc) text_span_free, NULL);
+  g_list_free (info->spans);
+}
+
+static void
+insert_text (ParseInfo   *info,
+	     GtkTextIter *iter)
+{
+  GtkTextIter  start_iter;
+  GtkTextMark *mark;
+  GList       *tmp;
+  GSList      *tags;
+
+  start_iter = *iter;
+
+  mark = gtk_text_buffer_create_mark (info->buffer, "deserialize-insert-point",
+  				      &start_iter, TRUE);
+
+  for (tmp = info->spans; tmp; tmp = tmp->next)
+    {
+      TextSpan *span = tmp->data;
+
+      if (span->text)
+	gtk_text_buffer_insert (info->buffer, iter, span->text, -1);
+
+      gtk_text_buffer_get_iter_at_mark (info->buffer, &start_iter, mark);
+
+      /* Apply tags */
+      for (tags = span->tags; tags; tags = tags->next)
+	{
+	  GtkTextTag *tag = tags->data;
+
+	  gtk_text_buffer_apply_tag (info->buffer, tag,
+				     &start_iter, iter);
+	}
+
+      gtk_text_buffer_move_mark (info->buffer, mark, iter);
+    }
+
+  gtk_text_buffer_delete_mark (info->buffer, mark);
+}
+
+gboolean
+gimp_text_buffer_deserialize (GtkTextBuffer *register_buffer,
+                              GtkTextBuffer *content_buffer,
+                              GtkTextIter   *iter,
+                              const guint8  *text,
+                              gsize          length,
+                              gboolean       create_tags,
+                              gpointer       user_data,
+                              GError       **error)
+{
+  GMarkupParseContext *context;
+  ParseInfo            info;
+  gboolean             retval = FALSE;
+
+  static const GMarkupParser markup_parser =
+  {
+    start_element_handler,
+    end_element_handler,
+    text_handler,
+    NULL,
+    NULL
+  };
+
+  parse_info_init (&info, content_buffer);
+
+  context = g_markup_parse_context_new (&markup_parser, 0, &info, NULL);
+
+  if (! g_markup_parse_context_parse (context,
+                                      (const gchar *) text,
+                                      length,
+                                      error))
+    goto out;
+
+  if (! g_markup_parse_context_end_parse (context, error))
+    goto out;
+
+  retval = TRUE;
+
+  insert_text (&info, iter);
+
+ out:
+  parse_info_free (&info);
+
+  g_markup_parse_context_free (context);
+
+  return retval;
+}
diff --git a/app/widgets/gimptextbuffer-serialize.h b/app/widgets/gimptextbuffer-serialize.h
new file mode 100644
index 0000000..b2857c6
--- /dev/null
+++ b/app/widgets/gimptextbuffer-serialize.h
@@ -0,0 +1,46 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * GimpTextBuffer-serialize
+ * Copyright (C) 2010  Michael Natterer <mitch gimp org>
+ *
+ * inspired by
+ * gtktextbufferserialize.h
+ * Copyright (C) 2004  Nokia Corporation.
+ *
+ * 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; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_TEXT_BUFFER_SERIALIZE_H__
+#define __GIMP_TEXT_BUFFER_SERIALIZE_H__
+
+
+guint8   * gimp_text_buffer_serialize   (GtkTextBuffer     *register_buffer,
+                                         GtkTextBuffer     *content_buffer,
+                                         const GtkTextIter *start,
+                                         const GtkTextIter *end,
+                                         gsize             *length,
+                                         gpointer           user_data);
+
+gboolean   gimp_text_buffer_deserialize (GtkTextBuffer     *register_buffer,
+                                         GtkTextBuffer     *content_buffer,
+                                         GtkTextIter       *iter,
+                                         const guint8      *data,
+                                         gsize              length,
+                                         gboolean           create_tags,
+                                         gpointer           user_data,
+                                         GError           **error);
+
+
+#endif /* __GIMP_TEXT_BUFFER_SERIALIZE_H__ */
diff --git a/app/widgets/gimptextbuffer.c b/app/widgets/gimptextbuffer.c
index 36d5a22..c133a93 100644
--- a/app/widgets/gimptextbuffer.c
+++ b/app/widgets/gimptextbuffer.c
@@ -45,6 +45,7 @@
 #include "widgets-types.h"
 
 #include "gimptextbuffer.h"
+#include "gimptextbuffer-serialize.h"
 
 #include "gimp-intl.h"
 
@@ -76,6 +77,16 @@ gimp_text_buffer_class_init (GimpTextBufferClass *klass)
 static void
 gimp_text_buffer_init (GimpTextBuffer *buffer)
 {
+  buffer->markup_atom =
+    gtk_text_buffer_register_serialize_format (GTK_TEXT_BUFFER (buffer),
+                                               "application/x-gimp-pango-markup",
+                                               gimp_text_buffer_serialize,
+                                               NULL, NULL);
+
+  gtk_text_buffer_register_deserialize_format (GTK_TEXT_BUFFER (buffer),
+                                               "application/x-gimp-pango-markup",
+                                               gimp_text_buffer_deserialize,
+                                               NULL, NULL);
 }
 
 static GObject *
@@ -92,6 +103,26 @@ gimp_text_buffer_constructor (GType                  type,
 
   gtk_text_buffer_set_text (GTK_TEXT_BUFFER (buffer), "", -1);
 
+  buffer->bold_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
+                                                 "bold",
+                                                 "weight", PANGO_WEIGHT_BOLD,
+                                                 NULL);
+
+  buffer->italic_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
+                                                   "italic",
+                                                   "style", PANGO_STYLE_ITALIC,
+                                                   NULL);
+
+  buffer->underline_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
+                                                      "underline",
+                                                      "underline", PANGO_UNDERLINE_SINGLE,
+                                                      NULL);
+
+  buffer->strikethrough_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
+                                                          "strikethrough",
+                                                          "strikethrough", TRUE,
+                                                          NULL);
+
   return object;
 }
 
@@ -145,6 +176,105 @@ gimp_text_buffer_get_text (GimpTextBuffer *buffer)
                                    &start, &end, TRUE);
 }
 
+void
+gimp_text_buffer_set_markup (GimpTextBuffer *buffer,
+                             const gchar    *markup)
+{
+  g_return_if_fail (GIMP_IS_TEXT_BUFFER (buffer));
+
+  gimp_text_buffer_set_text (buffer, NULL);
+
+  if (markup)
+    {
+      GtkTextIter  start;
+      GError      *error = NULL;
+
+      gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &start);
+
+      if (! gtk_text_buffer_deserialize (GTK_TEXT_BUFFER (buffer),
+                                         GTK_TEXT_BUFFER (buffer),
+                                         buffer->markup_atom,
+                                         &start,
+                                         (const guint8 *) markup, -1,
+                                         &error))
+        {
+          g_printerr ("EEK: %s\n", error->message);
+          g_clear_error (&error);
+        }
+    }
+}
+
+gchar *
+gimp_text_buffer_get_markup (GimpTextBuffer *buffer)
+{
+  GtkTextIter start, end;
+  gsize       length;
+
+  g_return_val_if_fail (GIMP_IS_TEXT_BUFFER (buffer), NULL);
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &start, &end);
+
+  return (gchar *) gtk_text_buffer_serialize (GTK_TEXT_BUFFER (buffer),
+                                              GTK_TEXT_BUFFER (buffer),
+                                              buffer->markup_atom,
+                                              &start, &end,
+                                              &length);
+}
+
+const gchar *
+gimp_text_buffer_tag_to_name (GimpTextBuffer *buffer,
+                              GtkTextTag     *tag)
+{
+  g_return_val_if_fail (GIMP_IS_TEXT_BUFFER (buffer), NULL);
+  g_return_val_if_fail (GTK_IS_TEXT_TAG (tag), NULL);
+
+  if (tag == buffer->bold_tag)
+    {
+      return "b";
+    }
+  else if (tag == buffer->italic_tag)
+    {
+      return "i";
+    }
+  else if (tag == buffer->underline_tag)
+    {
+      return "u";
+    }
+  else if (tag == buffer->strikethrough_tag)
+    {
+      return "s";
+    }
+
+  return NULL;
+}
+
+GtkTextTag *
+gimp_text_buffer_name_to_tag (GimpTextBuffer *buffer,
+                              const gchar    *name)
+{
+  g_return_val_if_fail (GIMP_IS_TEXT_BUFFER (buffer), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  if (! strcmp (name, "b"))
+    {
+      return buffer->bold_tag;
+    }
+  else if (! strcmp (name, "i"))
+    {
+      return buffer->italic_tag;
+    }
+  else if (! strcmp (name, "u"))
+    {
+      return buffer->underline_tag;
+    }
+  else if (! strcmp (name, "s"))
+    {
+      return buffer->strikethrough_tag;
+    }
+
+  return NULL;
+}
+
 gint
 gimp_text_buffer_get_iter_index (GimpTextBuffer *buffer,
                                  GtkTextIter    *iter)
diff --git a/app/widgets/gimptextbuffer.h b/app/widgets/gimptextbuffer.h
index a7d463f..d34c876 100644
--- a/app/widgets/gimptextbuffer.h
+++ b/app/widgets/gimptextbuffer.h
@@ -34,6 +34,13 @@ typedef struct _GimpTextBufferClass  GimpTextBufferClass;
 struct _GimpTextBuffer
 {
   GtkTextBuffer  parent_instance;
+
+  GtkTextTag    *bold_tag;
+  GtkTextTag    *italic_tag;
+  GtkTextTag    *underline_tag;
+  GtkTextTag    *strikethrough_tag;
+
+  GdkAtom        markup_atom;
 };
 
 struct _GimpTextBufferClass
@@ -50,6 +57,15 @@ void             gimp_text_buffer_set_text       (GimpTextBuffer  *buffer,
                                                   const gchar     *text);
 gchar          * gimp_text_buffer_get_text       (GimpTextBuffer  *buffer);
 
+void             gimp_text_buffer_set_markup     (GimpTextBuffer  *buffer,
+                                                  const gchar     *markup);
+gchar          * gimp_text_buffer_get_markup     (GimpTextBuffer  *buffer);
+
+const gchar    * gimp_text_buffer_tag_to_name    (GimpTextBuffer  *buffer,
+                                                  GtkTextTag      *tag);
+GtkTextTag     * gimp_text_buffer_name_to_tag    (GimpTextBuffer  *buffer,
+                                                  const gchar     *name);
+
 gint             gimp_text_buffer_get_iter_index (GimpTextBuffer  *buffer,
                                                   GtkTextIter     *iter);
 
diff --git a/app/widgets/gimptextstyleeditor.c b/app/widgets/gimptextstyleeditor.c
new file mode 100644
index 0000000..36b3419
--- /dev/null
+++ b/app/widgets/gimptextstyleeditor.c
@@ -0,0 +1,423 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * GimpTextStyleEditor
+ * Copyright (C) 2010  Michael Natterer <mitch gimp org>
+ *
+ * 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; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gegl.h>
+#include <gtk/gtk.h>
+
+#include "widgets-types.h"
+
+#include "gimptextbuffer.h"
+#include "gimptextstyleeditor.h"
+
+
+enum
+{
+  PROP_0,
+  PROP_BUFFER
+};
+
+
+static GObject * gimp_text_style_editor_constructor  (GType                  type,
+                                                      guint                  n_params,
+                                                      GObjectConstructParam *params);
+static void      gimp_text_style_editor_dispose      (GObject               *object);
+static void      gimp_text_style_editor_finalize     (GObject               *object);
+static void      gimp_text_style_editor_set_property (GObject               *object,
+                                                      guint                  property_id,
+                                                      const GValue          *value,
+                                                      GParamSpec            *pspec);
+static void      gimp_text_style_editor_get_property (GObject               *object,
+                                                      guint                  property_id,
+                                                      GValue                *value,
+                                                      GParamSpec            *pspec);
+
+static GtkWidget *
+                gimp_text_style_editor_create_toggle (GimpTextStyleEditor *editor,
+                                                      GtkTextTag          *tag,
+                                                      const gchar         *stock_id);
+
+static void      gimp_text_style_editor_clear_tags   (GtkButton           *button,
+                                                      GimpTextStyleEditor *editor);
+static void      gimp_text_style_editor_tag_toggled  (GtkToggleButton     *toggle,
+                                                      GimpTextStyleEditor *editor);
+
+static void      gimp_text_style_editor_mark_set     (GtkTextBuffer       *buffer,
+                                                      GtkTextIter         *location,
+                                                      GtkTextMark         *mark,
+                                                      GimpTextStyleEditor *editor);
+
+
+G_DEFINE_TYPE (GimpTextStyleEditor, gimp_text_style_editor,
+               GTK_TYPE_HBOX)
+
+#define parent_class gimp_text_style_editor_parent_class
+
+
+static void
+gimp_text_style_editor_class_init (GimpTextStyleEditorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructor  = gimp_text_style_editor_constructor;
+  object_class->dispose      = gimp_text_style_editor_dispose;
+  object_class->finalize     = gimp_text_style_editor_finalize;
+  object_class->set_property = gimp_text_style_editor_set_property;
+  object_class->get_property = gimp_text_style_editor_get_property;
+
+  g_object_class_install_property (object_class, PROP_BUFFER,
+                                   g_param_spec_object ("buffer",
+                                                        NULL, NULL,
+                                                        GIMP_TYPE_TEXT_BUFFER,
+                                                        GIMP_PARAM_READWRITE |
+                                                        G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+gimp_text_style_editor_init (GimpTextStyleEditor *editor)
+{
+  GtkWidget *image;
+
+  editor->tag_to_toggle_hash = g_hash_table_new (g_direct_hash,
+                                                 g_direct_equal);
+
+  editor->clear_button = gtk_button_new ();
+  gtk_widget_set_can_focus (editor->clear_button, FALSE);
+  gtk_box_pack_start (GTK_BOX (editor), editor->clear_button, FALSE, FALSE, 0);
+  gtk_widget_show (editor->clear_button);
+
+  g_signal_connect (editor->clear_button, "clicked",
+                    G_CALLBACK (gimp_text_style_editor_clear_tags),
+                    editor);
+
+  image = gtk_image_new_from_stock (GTK_STOCK_CLEAR, GTK_ICON_SIZE_MENU);
+  gtk_container_add (GTK_CONTAINER (editor->clear_button), image);
+  gtk_widget_show (image);
+}
+
+static GObject *
+gimp_text_style_editor_constructor (GType                  type,
+                                    guint                  n_params,
+                                    GObjectConstructParam *params)
+{
+  GObject             *object;
+  GimpTextStyleEditor *editor;
+
+  object = G_OBJECT_CLASS (parent_class)->constructor (type, n_params, params);
+
+  editor = GIMP_TEXT_STYLE_EDITOR (object);
+
+  g_assert (GIMP_IS_TEXT_BUFFER (editor->buffer));
+
+  gimp_text_style_editor_create_toggle (editor, editor->buffer->bold_tag,
+                                        GTK_STOCK_BOLD);
+  gimp_text_style_editor_create_toggle (editor, editor->buffer->italic_tag,
+                                        GTK_STOCK_ITALIC);
+  gimp_text_style_editor_create_toggle (editor, editor->buffer->underline_tag,
+                                        GTK_STOCK_UNDERLINE);
+  gimp_text_style_editor_create_toggle (editor, editor->buffer->strikethrough_tag,
+                                        GTK_STOCK_STRIKETHROUGH);
+
+  g_signal_connect (editor->buffer, "mark-set",
+                    G_CALLBACK (gimp_text_style_editor_mark_set),
+                    editor);
+
+  return object;
+}
+
+static void
+gimp_text_style_editor_dispose (GObject *object)
+{
+  GimpTextStyleEditor *editor = GIMP_TEXT_STYLE_EDITOR (object);
+
+  if (editor->buffer)
+    {
+      g_signal_handlers_disconnect_by_func (editor->buffer,
+                                            gimp_text_style_editor_mark_set,
+                                            editor);
+    }
+
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_text_style_editor_finalize (GObject *object)
+{
+  GimpTextStyleEditor *editor = GIMP_TEXT_STYLE_EDITOR (object);
+
+  if (editor->tag_to_toggle_hash)
+    {
+      g_hash_table_unref (editor->tag_to_toggle_hash);
+      editor->tag_to_toggle_hash = NULL;
+    }
+
+  if (editor->buffer)
+    {
+      g_object_unref (editor->buffer);
+      editor->buffer = NULL;
+    }
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gimp_text_style_editor_set_property (GObject      *object,
+                                     guint         property_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  GimpTextStyleEditor *editor = GIMP_TEXT_STYLE_EDITOR (object);
+
+  switch (property_id)
+    {
+    case PROP_BUFFER:
+      editor->buffer = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_text_style_editor_get_property (GObject    *object,
+                                     guint       property_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  GimpTextStyleEditor *editor = GIMP_TEXT_STYLE_EDITOR (object);
+
+  switch (property_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, editor->buffer);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+
+/*  public functions  */
+
+GtkWidget *
+gimp_text_style_editor_new (GimpTextBuffer *buffer)
+{
+  g_return_val_if_fail (GIMP_IS_TEXT_BUFFER (buffer), NULL);
+
+  return g_object_new (GIMP_TYPE_TEXT_STYLE_EDITOR,
+                       "buffer", buffer,
+                       NULL);
+}
+
+
+/*  private functions  */
+
+static GtkWidget *
+gimp_text_style_editor_create_toggle (GimpTextStyleEditor *editor,
+                                      GtkTextTag          *tag,
+                                      const gchar         *stock_id)
+{
+  GtkWidget *toggle;
+  GtkWidget *image;
+
+  toggle = gtk_toggle_button_new ();
+  gtk_widget_set_can_focus (toggle, FALSE);
+  gtk_box_pack_start (GTK_BOX (editor), toggle, FALSE, FALSE, 0);
+  gtk_widget_show (toggle);
+
+  g_object_set_data (G_OBJECT (toggle), "tag", tag);
+  g_hash_table_insert (editor->tag_to_toggle_hash, tag, toggle);
+
+  g_signal_connect (toggle, "toggled",
+                    G_CALLBACK (gimp_text_style_editor_tag_toggled),
+                    editor);
+
+  image = gtk_image_new_from_stock (stock_id, GTK_ICON_SIZE_MENU);
+  gtk_container_add (GTK_CONTAINER (toggle), image);
+  gtk_widget_show (image);
+
+  return toggle;
+}
+
+static void
+gimp_text_style_editor_clear_tags (GtkButton           *button,
+                                   GimpTextStyleEditor *editor)
+{
+  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (editor->buffer);
+
+  if (gtk_text_buffer_get_has_selection (buffer))
+    {
+      GtkTextIter start, end;
+
+      gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
+
+      gtk_text_buffer_remove_all_tags (buffer, &start, &end);
+    }
+}
+
+static void
+gimp_text_style_editor_tag_toggled (GtkToggleButton     *toggle,
+                                    GimpTextStyleEditor *editor)
+{
+  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (editor->buffer);
+  GtkTextTag    *tag    = g_object_get_data (G_OBJECT (toggle), "tag");
+
+  if (gtk_text_buffer_get_has_selection (buffer))
+    {
+      GtkTextIter start, end;
+
+      gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
+
+      if (gtk_toggle_button_get_active (toggle))
+        {
+          gtk_text_buffer_apply_tag (buffer, tag, &start, &end);
+        }
+      else
+        {
+          gtk_text_buffer_remove_tag (buffer, tag, &start, &end);
+        }
+    }
+}
+
+static void
+gimp_text_style_editor_enable_toggle (GtkTextTag          *tag,
+                                      GtkToggleButton     *toggle,
+                                      GimpTextStyleEditor *editor)
+{
+  g_signal_handlers_block_by_func (toggle,
+                                   gimp_text_style_editor_tag_toggled,
+                                   editor);
+
+  gtk_toggle_button_set_active (toggle, TRUE);
+
+  g_signal_handlers_unblock_by_func (toggle,
+                                     gimp_text_style_editor_tag_toggled,
+                                     editor);
+}
+
+typedef struct
+{
+  GimpTextStyleEditor *editor;
+  GSList              *tags;
+  GtkTextIter          iter;
+  gboolean             any_active;
+} UpdateTogglesData;
+
+static void
+gimp_text_style_editor_update_selection (GtkTextTag        *tag,
+                                         GtkToggleButton   *toggle,
+                                         UpdateTogglesData *data)
+{
+  if (! gtk_text_iter_has_tag (&data->iter, tag))
+    {
+      g_signal_handlers_block_by_func (toggle,
+                                       gimp_text_style_editor_tag_toggled,
+                                       data->editor);
+
+      gtk_toggle_button_set_active (toggle, FALSE);
+
+      g_signal_handlers_unblock_by_func (toggle,
+                                         gimp_text_style_editor_tag_toggled,
+                                         data->editor);
+    }
+  else
+    {
+      data->any_active = TRUE;
+    }
+}
+
+static void
+gimp_text_style_editor_update_cursor (GtkTextTag        *tag,
+                                      GtkToggleButton   *toggle,
+                                      UpdateTogglesData *data)
+{
+  g_signal_handlers_block_by_func (toggle,
+                                   gimp_text_style_editor_tag_toggled,
+                                   data->editor);
+
+  gtk_toggle_button_set_active (toggle,
+                                g_slist_find (data->tags, tag) != NULL);
+
+  g_signal_handlers_unblock_by_func (toggle,
+                                     gimp_text_style_editor_tag_toggled,
+                                     data->editor);
+}
+
+static void
+gimp_text_style_editor_mark_set (GtkTextBuffer       *buffer,
+                                 GtkTextIter         *location,
+                                 GtkTextMark         *mark,
+                                 GimpTextStyleEditor *editor)
+{
+  if (gtk_text_buffer_get_has_selection (buffer))
+    {
+      GtkTextIter start, end;
+      GtkTextIter iter;
+
+      gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
+      gtk_text_iter_order (&start, &end);
+
+      /*  first, switch all toggles on  */
+      g_hash_table_foreach (editor->tag_to_toggle_hash,
+                            (GHFunc) gimp_text_style_editor_enable_toggle,
+                            editor);
+
+      for (iter = start;
+           gtk_text_iter_in_range (&iter, &start, &end);
+           gtk_text_iter_forward_cursor_position (&iter))
+        {
+          UpdateTogglesData data;
+
+          data.editor     = editor;
+          data.iter       = iter;
+          data.any_active = FALSE;
+
+          g_hash_table_foreach (editor->tag_to_toggle_hash,
+                                (GHFunc) gimp_text_style_editor_update_selection,
+                                &data);
+
+          if (! data.any_active)
+            break;
+       }
+
+    }
+  else
+    {
+      UpdateTogglesData data;
+      GtkTextIter       cursor;
+
+      gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
+                                        gtk_text_buffer_get_insert (buffer));
+
+      data.editor = editor;
+      data.tags   = gtk_text_iter_get_tags (&cursor);
+
+      g_hash_table_foreach (editor->tag_to_toggle_hash,
+                            (GHFunc) gimp_text_style_editor_update_cursor,
+                            &data);
+
+      g_slist_free (data.tags);
+    }
+}
diff --git a/app/widgets/gimptextstyleeditor.h b/app/widgets/gimptextstyleeditor.h
new file mode 100644
index 0000000..947bf50
--- /dev/null
+++ b/app/widgets/gimptextstyleeditor.h
@@ -0,0 +1,61 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * GimpTextStyleEditor
+ * Copyright (C) 2010  Michael Natterer <mitch gimp org>
+ *
+ * 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; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_TEXT_STYLE_EDITOR_H__
+#define __GIMP_TEXT_STYLE_EDITOR_H__
+
+
+#define GIMP_TYPE_TEXT_STYLE_EDITOR            (gimp_text_style_editor_get_type ())
+#define GIMP_TEXT_STYLE_EDITOR(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_TEXT_STYLE_EDITOR, GimpTextStyleEditor))
+#define GIMP_TEXT_STYLE_EDITOR_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_TEXT_STYLE_EDITOR, GimpTextStyleEditorClass))
+#define GIMP_IS_TEXT_STYLE_EDITOR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_TEXT_STYLE_EDITOR))
+#define GIMP_IS_TEXT_STYLE_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_TEXT_STYLE_EDITOR))
+#define GIMP_TEXT_STYLE_EDITOR_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_TEXT_STYLE_EDITOR, GimpTextStyleEditorClass))
+
+
+typedef struct _GimpTextStyleEditorClass GimpTextStyleEditorClass;
+
+struct _GimpTextStyleEditor
+{
+  GtkHBox         parent_instance;
+
+  GimpTextBuffer *buffer;
+
+  GtkWidget      *clear_button;
+  GtkWidget      *bold_toggle;
+  GtkWidget      *italic_toggle;
+  GtkWidget      *underline_toggle;
+  GtkWidget      *strikethrough_toggle;
+
+  GHashTable     *tag_to_toggle_hash;
+};
+
+struct _GimpTextStyleEditorClass
+{
+  GtkHBoxClass  parent_class;
+};
+
+
+GType       gimp_text_style_editor_get_type (void) G_GNUC_CONST;
+
+GtkWidget * gimp_text_style_editor_new      (GimpTextBuffer *buffer);
+
+
+#endif /*  __GIMP_TEXT_STYLE_EDITOR_H__  */
diff --git a/app/widgets/widgets-types.h b/app/widgets/widgets-types.h
index a31bf93..1acedf0 100644
--- a/app/widgets/widgets-types.h
+++ b/app/widgets/widgets-types.h
@@ -190,6 +190,7 @@ typedef struct _GimpStrokeEditor             GimpStrokeEditor;
 typedef struct _GimpTagEntry                 GimpTagEntry;
 typedef struct _GimpTagPopup                 GimpTagPopup;
 typedef struct _GimpTemplateEditor           GimpTemplateEditor;
+typedef struct _GimpTextStyleEditor          GimpTextStyleEditor;
 typedef struct _GimpThumbBox                 GimpThumbBox;
 typedef struct _GimpTranslationStore         GimpTranslationStore;
 typedef struct _GimpUnitStore                GimpUnitStore;



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]