diff --git a/clutter/clutter-text.c b/clutter/clutter-text.c index 8b06e4cc0..608124de3 100644 --- a/clutter/clutter-text.c +++ b/clutter/clutter-text.c @@ -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)); +} diff --git a/clutter/clutter-text.h b/clutter/clutter-text.h index f34ff967a..2d8b9659b 100644 --- a/clutter/clutter-text.h +++ b/clutter/clutter-text.h @@ -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__ */ diff --git a/doc/reference/clutter/clutter-sections.txt b/doc/reference/clutter/clutter-sections.txt index 1ba2c6868..ca9afbee5 100644 --- a/doc/reference/clutter/clutter-sections.txt +++ b/doc/reference/clutter/clutter-sections.txt @@ -1604,6 +1604,7 @@ clutter_text_get_cursor_size clutter_text_activate clutter_text_position_to_coords +clutter_text_set_preedit_string CLUTTER_IS_TEXT diff --git a/tests/interactive/test-text-field.c b/tests/interactive/test-text-field.c index d77ce41a4..0b41d06f2 100644 --- a/tests/interactive/test-text-field.c +++ b/tests/interactive/test-text-field.c @@ -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; }