[text] Add pre-edit string to ClutterText
authorEmmanuele Bassi <ebassi@linux.intel.com>
Wed, 9 Sep 2009 11:15:23 +0000 (12:15 +0100)
committerEmmanuele Bassi <ebassi@gnome.org>
Fri, 16 Oct 2009 11:45:29 +0000 (12:45 +0100)
Input Methods require to be able to set a "pre-edit string", that is
a string that it's just displayed into the Text actor without being
committed to the actor's buffer. The string might require custom Pango
attributes, and an update of the cursor position.

clutter/clutter-text.c
clutter/clutter-text.h
doc/reference/clutter/clutter-sections.txt
tests/interactive/test-text-field.c

index 8b06e4c..608124d 100644 (file)
@@ -104,6 +104,7 @@ struct _ClutterTextPrivate
 
   gchar *text;
   gchar *font_name;
+  gchar *preedit_str;
 
   ClutterColor text_color;
 
@@ -112,6 +113,7 @@ struct _ClutterTextPrivate
 
   PangoAttrList *attrs;
   PangoAttrList *effective_attrs;
+  PangoAttrList *preedit_attrs;
 
   guint alignment           : 2;
   guint wrap                : 1;
@@ -128,6 +130,7 @@ struct _ClutterTextPrivate
   guint selection_color_set : 1;
   guint in_select_drag      : 1;
   guint cursor_color_set    : 1;
+  guint preedit_set         : 1;
 
   /* current cursor position */
   gint position;
@@ -157,6 +160,9 @@ struct _ClutterTextPrivate
   ClutterColor cursor_color;
   guint cursor_size;
 
+  guint preedit_cursor_pos;
+  gint preedit_n_chars;
+
   ClutterColor selection_color;
 
   gint max_length;
@@ -242,6 +248,39 @@ clutter_text_clear_selection (ClutterText *self)
     }
 }
 
+static gchar *
+clutter_text_get_display_text (ClutterText *self)
+{
+  ClutterTextPrivate *priv = self->priv;
+
+  if (priv->text == NULL)
+    return g_strdup ("");
+
+  if (G_LIKELY (priv->password_char == 0))
+    return g_strndup (priv->text, priv->n_bytes);
+  else
+    {
+      GString *str = g_string_sized_new (priv->n_bytes);
+      gunichar invisible_char;
+      gchar buf[7];
+      gint char_len, i;
+
+      invisible_char = priv->password_char;
+
+      /* we need to convert the string built of invisible
+       * characters into UTF-8 for it to be fed to the Pango
+       * layout
+       */
+      memset (buf, 0, sizeof (buf));
+      char_len = g_unichar_to_utf8 (invisible_char, buf);
+
+      for (i = 0; i < priv->n_chars; i++)
+        g_string_append_len (str, buf, char_len);
+
+      return g_string_free (str, FALSE);
+    }
+}
+
 static PangoLayout *
 clutter_text_create_layout_no_cache (ClutterText *text,
                                      gfloat       allocation_width,
@@ -249,38 +288,41 @@ clutter_text_create_layout_no_cache (ClutterText *text,
 {
   ClutterTextPrivate *priv = text->priv;
   PangoLayout *layout;
+  gchar *contents;
+  gsize contents_len;
 
   layout = clutter_actor_create_pango_layout (CLUTTER_ACTOR (text), NULL);
   pango_layout_set_font_description (layout, priv->font_desc);
 
-  if (priv->text)
+  contents = clutter_text_get_display_text (text);
+  contents_len = strlen (contents);
+
+  if (priv->editable && priv->preedit_set)
     {
-      if (G_LIKELY (priv->password_char == 0))
-        pango_layout_set_text (layout, priv->text, priv->n_bytes);
+      GString *tmp = g_string_new (contents);
+      PangoAttrList *tmp_attrs = pango_attr_list_new ();
+      gint cursor_index;
+
+      if (priv->position == 0)
+        cursor_index = 0;
       else
-        {
-          GString *str = g_string_sized_new (priv->n_bytes);
-          gunichar invisible_char;
-          gchar buf[7];
-          gint char_len, i;
+        cursor_index = offset_to_bytes (contents, priv->position);
 
-          invisible_char = priv->password_char;
+      g_string_insert (tmp, cursor_index, priv->preedit_str);
 
-          /* we need to convert the string built of invisible
-           * characters into UTF-8 for it to be fed to the Pango
-           * layout
-           */
-          memset (buf, 0, sizeof (buf));
-          char_len = g_unichar_to_utf8 (invisible_char, buf);
+      pango_layout_set_text (layout, tmp->str, tmp->len);
 
-          for (i = 0; i < priv->n_chars; i++)
-            g_string_append_len (str, buf, char_len);
+      pango_attr_list_splice (tmp_attrs, priv->preedit_attrs,
+                              cursor_index,
+                              strlen (priv->preedit_str));
 
-          pango_layout_set_text (layout, str->str, str->len);
+      pango_layout_set_attributes (layout, tmp_attrs);
 
-          g_string_free (str, TRUE);
-        }
+      g_string_free (tmp, TRUE);
+      pango_attr_list_unref (tmp_attrs);
     }
+  else
+    pango_layout_set_text (layout, contents, contents_len);
 
   if (!priv->editable && priv->effective_attrs)
     pango_layout_set_attributes (layout, priv->effective_attrs);
@@ -355,6 +397,8 @@ clutter_text_create_layout_no_cache (ClutterText *text,
       pango_layout_set_height (layout, height);
     }
 
+  g_free (contents);
+
   return layout;
 }
 
@@ -505,6 +549,7 @@ clutter_text_position_to_coords (ClutterText *self,
 {
   ClutterTextPrivate *priv;
   PangoRectangle rect;
+  gint n_chars;
   gint password_char_bytes = 1;
   gint index_;
 
@@ -512,7 +557,12 @@ clutter_text_position_to_coords (ClutterText *self,
 
   priv = self->priv;
 
-  if (position < -1 || position > priv->n_chars)
+  if (priv->preedit_set)
+    n_chars = priv->n_chars + priv->preedit_n_chars;
+  else
+    n_chars = priv->n_chars;
+
+  if (position < -1 || position > n_chars)
     return FALSE;
 
   if (priv->password_char != 0)
@@ -523,7 +573,7 @@ clutter_text_position_to_coords (ClutterText *self,
       if (priv->password_char == 0)
         index_ = priv->n_bytes;
       else
-        index_ = priv->n_chars * password_char_bytes;
+        index_ = n_chars * password_char_bytes;
     }
   else if (position == 0)
     {
@@ -534,10 +584,11 @@ clutter_text_position_to_coords (ClutterText *self,
       if (priv->password_char == 0)
         index_ = offset_to_bytes (priv->text, position);
       else
-        index_ = priv->position * password_char_bytes;
+        index_ = position * password_char_bytes;
     }
 
-  pango_layout_get_cursor_pos (clutter_text_get_layout (self), index_,
+  pango_layout_get_cursor_pos (clutter_text_get_layout (self),
+                               index_,
                                &rect, NULL);
 
   if (x)
@@ -566,9 +617,17 @@ clutter_text_ensure_cursor_position (ClutterText *self)
   ClutterGeometry cursor_pos = { 0, };
   gboolean x_changed, y_changed;
   gboolean width_changed, height_changed;
+  gint position;
+
+  position = priv->position;
+
+  CLUTTER_NOTE (MISC, "Cursor at %d (preedit %s at pos: %d)",
+                position,
+                priv->preedit_set ? "set" : "unset",
+                priv->preedit_set ? priv->preedit_cursor_pos : 0);
 
   x = y = cursor_height = 0;
-  clutter_text_position_to_coords (self, priv->position,
+  clutter_text_position_to_coords (self, position,
                                    &x, &y,
                                    &cursor_height);
 
@@ -1033,11 +1092,14 @@ cursor_paint (ClutterText *self)
     {
       guint8 paint_opacity = clutter_actor_get_paint_opacity (actor);
       const ClutterColor *color;
+      gint position;
 
-      if (priv->position == 0)
+      position = priv->position;
+
+      if (position == 0)
         priv->cursor_pos.x -= priv->cursor_size;
 
-      if (priv->position == priv->selection_bound)
+      if (position == priv->selection_bound)
         {
           if (priv->cursor_color_set)
             color = &priv->cursor_color;
@@ -1055,12 +1117,11 @@ cursor_paint (ClutterText *self)
                           priv->cursor_pos.y,
                           priv->cursor_pos.x + priv->cursor_pos.width,
                           priv->cursor_pos.y + priv->cursor_pos.height);
-
         }
       else
         {
           PangoLayout *layout = clutter_text_get_layout (self);
-          const gchar *utf8 = priv->text;
+          gchar *utf8 = clutter_text_get_display_text (self);
           gint lines;
           gint start_index;
           gint end_index;
@@ -1080,10 +1141,10 @@ cursor_paint (ClutterText *self)
                                     * color->alpha
                                     / 255);
 
-          if (priv->position == 0)
+          if (position == 0)
             start_index = 0;
           else
-            start_index = offset_to_bytes (utf8, priv->position);
+            start_index = offset_to_bytes (utf8, position);
 
           if (priv->selection_bound == 0)
             end_index = 0;
@@ -1146,6 +1207,8 @@ cursor_paint (ClutterText *self)
 
               g_free (ranges);
             }
+
+          g_free (utf8);
         }
     }
 }
@@ -1554,7 +1617,6 @@ clutter_text_paint (ClutterActor *self)
            * scrolling */
           priv->text_x = text_x;
           clutter_text_ensure_cursor_position (text);
-
         }
       else
         {
@@ -2606,6 +2668,7 @@ clutter_text_init (ClutterText *self)
 
   priv->selection_color_set = FALSE;
   priv->cursor_color_set = FALSE;
+  priv->preedit_set = FALSE;
 
   priv->password_char = 0;
 
@@ -4383,3 +4446,69 @@ clutter_text_get_single_line_mode (ClutterText *self)
 
   return self->priv->single_line_mode;
 }
+
+/**
+ * clutter_text_set_preedit_string:
+ * @self: a #ClutterText
+ * @preedit_str: (allow-none): the pre-edit string, or %NULL to unset it
+ * @preedit_attrs: (allow-none): the pre-edit string attributes
+ * @cursor_pos: the cursor position for the pre-edit string
+ *
+ * Sets, or unsets, the pre-edit string. This function is useful
+ * for input methods to display a string (with eventual specific
+ * Pango attributes) before it is entered inside the #ClutterText
+ * buffer.
+ *
+ * The preedit string and attributes are ignored if the #ClutterText
+ * actor is not editable.
+ *
+ * This function should not be used by applications
+ *
+ * Since: 1.2
+ */
+void
+clutter_text_set_preedit_string (ClutterText   *self,
+                                 const gchar   *preedit_str,
+                                 PangoAttrList *preedit_attrs,
+                                 guint          cursor_pos)
+{
+  ClutterTextPrivate *priv;
+
+  g_return_if_fail (CLUTTER_IS_TEXT (self));
+
+  priv = self->priv;
+
+  g_free (priv->preedit_str);
+
+  if (priv->preedit_attrs != NULL)
+    {
+      pango_attr_list_unref (priv->preedit_attrs);
+      priv->preedit_attrs = NULL;
+    }
+
+  priv->preedit_n_chars = 0;
+  priv->preedit_cursor_pos = 0;
+
+  if (preedit_str == NULL || *preedit_str == '\0')
+    priv->preedit_set = FALSE;
+  else
+    {
+      priv->preedit_str = g_strdup (preedit_str);
+
+      if (priv->preedit_str != NULL)
+        priv->preedit_n_chars = g_utf8_strlen (priv->preedit_str, -1);
+      else
+        priv->preedit_n_chars = 0;
+
+      if (preedit_attrs != NULL)
+        priv->preedit_attrs = pango_attr_list_ref (preedit_attrs);
+
+      priv->preedit_cursor_pos =
+        CLAMP (cursor_pos, 0, priv->preedit_n_chars);
+
+      priv->preedit_set = TRUE;
+    }
+
+  clutter_text_dirty_cache (self);
+  clutter_actor_queue_relayout (CLUTTER_ACTOR (self));
+}
index f34ff96..2d8b965 100644 (file)
@@ -204,6 +204,11 @@ gboolean              clutter_text_position_to_coords   (ClutterText        *sel
                                                          gfloat             *y,
                                                          gfloat             *line_height);
 
+void                  clutter_text_set_preedit_string   (ClutterText        *self,
+                                                         const gchar        *preedit_str,
+                                                         PangoAttrList      *preedit_attr,
+                                                         guint               cursor_position);
+
 G_END_DECLS
 
 #endif /* __CLUTTER_TEXT_H__ */
index 1ba2c68..ca9afbe 100644 (file)
@@ -1604,6 +1604,7 @@ clutter_text_get_cursor_size
 <SUBSECTION>
 clutter_text_activate
 clutter_text_position_to_coords
+clutter_text_set_preedit_string
 
 <SUBSECTION Standard>
 CLUTTER_IS_TEXT
index d77ce41..0b41d06 100644 (file)
@@ -30,6 +30,182 @@ on_entry_activate (ClutterText *text,
            clutter_text_get_selection_bound (text));
 }
 
+#define is_hex_digit(c)         (((c) >= '0' && (c) <= '9') || \
+                                 ((c) >= 'a' && (c) <= 'f') || \
+                                 ((c) >= 'A' && (c) <= 'F'))
+#define to_hex_digit(c)         (((c) <= '9') ? (c) - '0' : ((c) & 7) + 9)
+
+static gboolean
+on_captured_event (ClutterText *text,
+                   ClutterEvent *event,
+                   gpointer      dummy G_GNUC_UNUSED)
+{
+  gboolean is_unicode_mode = FALSE;
+  gunichar c;
+  guint keyval;
+
+  if (event->type != CLUTTER_KEY_PRESS)
+    return FALSE;
+
+  is_unicode_mode = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (text),
+                                                        "unicode-mode"));
+
+  c = clutter_event_get_key_unicode (event);
+  keyval = clutter_event_get_key_symbol (event);
+  if (keyval == CLUTTER_u)
+    {
+      ClutterModifierType mods = clutter_event_get_state (event);
+
+      if (is_unicode_mode)
+        {
+          GString *str = g_object_get_data (G_OBJECT (text), "unicode-str");
+
+          clutter_text_set_preedit_string (text, NULL, NULL, 0);
+
+          g_object_set_data (G_OBJECT (text), "unicode-mode",
+                             GINT_TO_POINTER (FALSE));
+          g_object_set_data (G_OBJECT (text), "unicode-str",
+                             NULL);
+
+          g_string_free (str, TRUE);
+
+          return FALSE;
+        }
+
+      if ((mods & CLUTTER_CONTROL_MASK) &&
+          (mods & CLUTTER_SHIFT_MASK))
+        {
+          PangoAttrList *attrs;
+          PangoAttribute *a;
+          GString *str = g_string_sized_new (5);
+
+          g_string_append (str, "u");
+
+          g_object_set_data (G_OBJECT (text),
+                             "unicode-mode",
+                             GINT_TO_POINTER (TRUE));
+          g_object_set_data (G_OBJECT (text),
+                             "unicode-str",
+                             str);
+
+          attrs = pango_attr_list_new ();
+
+          a = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+          a->start_index = 0;
+          a->end_index = str->len;
+          pango_attr_list_insert (attrs, a);
+
+          clutter_text_set_preedit_string (text, str->str, attrs, str->len);
+
+          pango_attr_list_unref (attrs);
+
+          return TRUE;
+        }
+
+      return FALSE;
+    }
+  else if (is_unicode_mode && is_hex_digit (c))
+    {
+      GString *str = g_object_get_data (G_OBJECT (text), "unicode-str");
+      PangoAttrList *attrs;
+      PangoAttribute *a;
+      gchar buf[8];
+      gsize len;
+
+      len = g_unichar_to_utf8 (c, buf);
+      buf[len] = '\0';
+
+      g_string_append (str, buf);
+
+      g_debug ("added '%s' to '%s' (len:%d)", buf, str->str, str->len);
+
+      attrs = pango_attr_list_new ();
+
+      a = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+      a->start_index = 0;
+      a->end_index = str->len;
+      pango_attr_list_insert (attrs, a);
+
+      clutter_text_set_preedit_string (text, str->str, attrs, str->len);
+
+      pango_attr_list_unref (attrs);
+
+      return TRUE;
+    }
+  else if (is_unicode_mode && (keyval == CLUTTER_BackSpace))
+    {
+      GString *str = g_object_get_data (G_OBJECT (text), "unicode-str");
+      PangoAttrList *attrs;
+      PangoAttribute *a;
+
+      g_string_truncate (str, str->len - 1);
+
+      attrs = pango_attr_list_new ();
+
+      a = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+      a->start_index = 0;
+      a->end_index = str->len;
+      pango_attr_list_insert (attrs, a);
+
+      clutter_text_set_preedit_string (text, str->str, attrs, str->len);
+
+      pango_attr_list_unref (attrs);
+
+      return TRUE;
+    }
+  else if (is_unicode_mode &&
+           (keyval == CLUTTER_Return ||
+            keyval == CLUTTER_KP_Enter ||
+            keyval == CLUTTER_ISO_Enter ||
+            keyval == CLUTTER_KP_Space))
+    {
+      GString *str = g_object_get_data (G_OBJECT (text), "unicode-str");
+      const gchar *contents = clutter_text_get_text (text);
+      gunichar uchar = 0;
+      gchar ch;
+      gint i;
+
+      clutter_text_set_preedit_string (text, NULL, NULL, 0);
+
+      g_object_set_data (G_OBJECT (text), "unicode-mode",
+                         GINT_TO_POINTER (FALSE));
+      g_object_set_data (G_OBJECT (text), "unicode-str",
+                         NULL);
+
+      for (i = 0; i < str->len; i++)
+        {
+          ch = str->str[i];
+
+          if (is_hex_digit (ch))
+            uchar += ((gunichar) to_hex_digit (ch) << ((4 - i) * 4));
+        }
+
+      g_assert (g_unichar_validate (uchar));
+
+      g_string_overwrite (str, 0, contents);
+      g_string_insert_unichar (str,
+                               clutter_text_get_cursor_position (text),
+                               uchar);
+
+      i = clutter_text_get_cursor_position (text);
+      clutter_text_set_text (text, str->str);
+
+      if (i >= 0)
+        i += 1;
+      else
+        i = -1;
+
+      clutter_text_set_cursor_position (text, i);
+      clutter_text_set_selection_bound (text, i);
+
+      g_string_free (str, TRUE);
+
+      return TRUE;
+    }
+  else
+    return FALSE;
+}
+
 static ClutterActor *
 create_label (const ClutterColor *color,
               const gchar        *text)
@@ -77,6 +253,9 @@ create_entry (const ClutterColor *color,
   g_signal_connect (retval, "paint",
                     G_CALLBACK (on_entry_paint),
                     NULL);
+  g_signal_connect (retval, "captured-event",
+                    G_CALLBACK (on_captured_event),
+                    NULL);
 
   return retval;
 }