/* * Clutter. * * An OpenGL based 'interactive canvas' library. * * Copyright (C) 2008 Intel Corporation. * * Authored By: Øyvind Kolås * Emmanuele Bassi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ /** * SECTION:clutter-text * @short_description: An actor for displaying and editing text * * #ClutterText is an actor that displays custom text using Pango * as the text rendering engine. * * #ClutterText also allows inline editing of the text if the * actor is set editable using clutter_text_set_editable(). * * Selection using keyboard or pointers can be enabled using * clutter_text_set_selectable(). * * #ClutterText is available since Clutter 1.0 */ /* TODO: undo/redo hooks? */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include "clutter-text.h" #include "clutter-actor-private.h" #include "clutter-binding-pool.h" #include "clutter-debug.h" #include "clutter-enum-types.h" #include "clutter-keysyms.h" #include "clutter-main.h" #include "clutter-marshal.h" #include "clutter-private.h" /* includes pango/cogl-pango.h */ #include "clutter-profile.h" #include "clutter-units.h" /* cursor width in pixels */ #define DEFAULT_CURSOR_SIZE 2 /* We need at least three cached layouts to run the allocation without * regenerating a new layout. First the layout will be generated at * full width to get the preferred width, then it will be generated at * the preferred width to get the preferred height and then it might * be regenerated at a different width to get the height for the * actual allocated width * * since we might get multiple queries from layout managers doing a * double-pass allocations, like tabular ones, we should use 6 slots */ #define N_CACHED_LAYOUTS 6 #define CLUTTER_TEXT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLUTTER_TYPE_TEXT, ClutterTextPrivate)) typedef struct _LayoutCache LayoutCache; static const ClutterColor default_cursor_color = { 0, 0, 0, 255 }; static const ClutterColor default_selection_color = { 0, 0, 0, 255 }; static const ClutterColor default_text_color = { 0, 0, 0, 255 }; G_DEFINE_TYPE (ClutterText, clutter_text, CLUTTER_TYPE_ACTOR); struct _LayoutCache { /* Cached layout. Pango internally caches the computed extents * when they are requested so there is no need to cache that as * well */ PangoLayout *layout; /* A number representing the age of this cache (so that when a * new layout is needed the last used cache is replaced) */ guint age; }; struct _ClutterTextPrivate { PangoFontDescription *font_desc; /* the displayed text */ gchar *text; gchar *font_name; gchar *preedit_str; ClutterColor text_color; LayoutCache cached_layouts[N_CACHED_LAYOUTS]; guint cache_age; /* These are the attributes set by the attributes property */ PangoAttrList *attrs; /* These are the attributes derived from the text when the use-markup property is set */ PangoAttrList *markup_attrs; /* This is the combination of the above two lists. It is set to NULL whenever either of them changes and then regenerated by merging the two lists whenever a layout is needed */ PangoAttrList *effective_attrs; /* These are the attributes for the preedit string. These are merged with the effective attributes into a temporary list before creating a layout */ PangoAttrList *preedit_attrs; guint alignment : 2; guint wrap : 1; guint use_underline : 1; guint use_markup : 1; guint ellipsize : 3; guint single_line_mode : 1; guint wrap_mode : 3; guint justify : 1; guint editable : 1; guint cursor_visible : 1; guint activatable : 1; guint selectable : 1; guint selection_color_set : 1; guint in_select_drag : 1; guint cursor_color_set : 1; guint preedit_set : 1; guint is_default_font : 1; /* current cursor position */ gint position; /* current 'other end of selection' position */ gint selection_bound; /* the x position in the PangoLayout, used to * avoid drifting when repeatedly moving up|down */ gint x_pos; /* the x position of the PangoLayout when in * single line mode, to scroll the contents of the * text actor */ gint text_x; /* the length of the text, in bytes */ gint n_bytes; /* the length of the text, in characters */ gint n_chars; /* Where to draw the cursor */ ClutterGeometry cursor_pos; ClutterColor cursor_color; guint cursor_size; guint preedit_cursor_pos; gint preedit_n_chars; ClutterColor selection_color; gint max_length; gunichar password_char; /* Signal handler for when the backend changes its font settings */ guint font_changed_id; /* Signal handler for when the :text-direction changes */ guint direction_changed_id; }; enum { PROP_0, PROP_FONT_NAME, PROP_FONT_DESCRIPTION, PROP_TEXT, PROP_COLOR, PROP_USE_MARKUP, PROP_ATTRIBUTES, PROP_LINE_ALIGNMENT, PROP_LINE_WRAP, PROP_LINE_WRAP_MODE, PROP_JUSTIFY, PROP_ELLIPSIZE, PROP_POSITION, PROP_SELECTION_BOUND, PROP_SELECTION_COLOR, PROP_SELECTION_COLOR_SET, PROP_CURSOR_VISIBLE, PROP_CURSOR_COLOR, PROP_CURSOR_COLOR_SET, PROP_CURSOR_SIZE, PROP_EDITABLE, PROP_SELECTABLE, PROP_ACTIVATABLE, PROP_PASSWORD_CHAR, PROP_MAX_LENGTH, PROP_SINGLE_LINE_MODE, PROP_LAST }; static GParamSpec *obj_props[PROP_LAST]; enum { TEXT_CHANGED, CURSOR_EVENT, ACTIVATE, INSERT_TEXT, DELETE_TEXT, LAST_SIGNAL }; static guint text_signals[LAST_SIGNAL] = { 0, }; static void clutter_text_font_changed_cb (ClutterText *text); #define offset_real(t,p) ((p) == -1 ? g_utf8_strlen ((t), -1) : (p)) static gint offset_to_bytes (const gchar *text, gint pos) { const gchar *ptr; if (pos < 0) return strlen (text); /* Loop over each character in the string until we either reach the end or the requested position */ for (ptr = text; *ptr && pos-- > 0; ptr = g_utf8_next_char (ptr)); return ptr - text; } #define bytes_to_offset(t,p) (g_utf8_pointer_to_offset ((t), (t) + (p))) static inline void clutter_text_clear_selection (ClutterText *self) { ClutterTextPrivate *priv = self->priv; if (priv->selection_bound != priv->position) { priv->selection_bound = priv->position; _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTION_BOUND]); clutter_actor_queue_redraw (CLUTTER_ACTOR (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 void clutter_text_ensure_effective_attributes (ClutterText *self) { ClutterTextPrivate *priv = self->priv; /* If we already have the effective attributes then we don't need to do anything */ if (priv->effective_attrs == NULL) { if (priv->attrs != NULL) { /* If there are no markup attributes then we can just use these attributes directly */ if (priv->markup_attrs == NULL) priv->effective_attrs = pango_attr_list_ref (priv->attrs); else { /* Otherwise we need to merge the two lists */ PangoAttrIterator *iter; GSList *attributes, *l; priv->effective_attrs = pango_attr_list_copy (priv->markup_attrs); iter = pango_attr_list_get_iterator (priv->attrs); do { attributes = pango_attr_iterator_get_attrs (iter); for (l = attributes; l != NULL; l = l->next) { PangoAttribute *attr = l->data; pango_attr_list_insert (priv->effective_attrs, attr); } g_slist_free (attributes); } while (pango_attr_iterator_next (iter)); } } else if (priv->markup_attrs != NULL) { /* We can just use the markup attributes directly */ priv->effective_attrs = pango_attr_list_ref (priv->markup_attrs); } } } static PangoLayout * clutter_text_create_layout_no_cache (ClutterText *text, gint width, gint height, PangoEllipsizeMode ellipsize) { ClutterTextPrivate *priv = text->priv; PangoLayout *layout; gchar *contents; gsize contents_len; CLUTTER_STATIC_TIMER (text_layout_timer, "Mainloop", "Text Layout", "Layout creation", 0); CLUTTER_TIMER_START (_clutter_uprof_context, text_layout_timer); layout = clutter_actor_create_pango_layout (CLUTTER_ACTOR (text), NULL); pango_layout_set_font_description (layout, priv->font_desc); contents = clutter_text_get_display_text (text); contents_len = strlen (contents); if (priv->editable && priv->preedit_set) { GString *tmp = g_string_new (contents); PangoAttrList *tmp_attrs = pango_attr_list_new (); gint cursor_index; if (priv->position == 0) cursor_index = 0; else cursor_index = offset_to_bytes (contents, priv->position); g_string_insert (tmp, cursor_index, priv->preedit_str); pango_layout_set_text (layout, tmp->str, tmp->len); if (priv->preedit_attrs != NULL) { pango_attr_list_splice (tmp_attrs, priv->preedit_attrs, cursor_index, strlen (priv->preedit_str)); pango_layout_set_attributes (layout, tmp_attrs); } g_string_free (tmp, TRUE); pango_attr_list_unref (tmp_attrs); } else pango_layout_set_text (layout, contents, contents_len); if (!priv->editable) { /* This will merge the markup attributes and the attributes property if needed */ clutter_text_ensure_effective_attributes (text); if (priv->effective_attrs != NULL) pango_layout_set_attributes (layout, priv->effective_attrs); } pango_layout_set_alignment (layout, priv->alignment); pango_layout_set_single_paragraph_mode (layout, priv->single_line_mode); pango_layout_set_justify (layout, priv->justify); pango_layout_set_wrap (layout, priv->wrap_mode); pango_layout_set_ellipsize (layout, ellipsize); pango_layout_set_width (layout, width); pango_layout_set_height (layout, height); g_free (contents); CLUTTER_TIMER_STOP (_clutter_uprof_context, text_layout_timer); return layout; } static void clutter_text_dirty_cache (ClutterText *text) { ClutterTextPrivate *priv = text->priv; int i; /* Delete the cached layouts so they will be recreated the next time they are needed */ for (i = 0; i < N_CACHED_LAYOUTS; i++) if (priv->cached_layouts[i].layout) { g_object_unref (priv->cached_layouts[i].layout); priv->cached_layouts[i].layout = NULL; } } /* * clutter_text_set_font_description_internal: * @self: a #ClutterText * @desc: a #PangoFontDescription * * Sets @desc as the font description to be used by the #ClutterText * actor. The font description ownership is transferred to @self so * the #PangoFontDescription must not be freed after this function * * This function will also set the :font-name field as a side-effect * * This function will evict the layout cache, and queue a relayout if * the #ClutterText actor has contents. */ static inline void clutter_text_set_font_description_internal (ClutterText *self, PangoFontDescription *desc) { ClutterTextPrivate *priv = self->priv; if (priv->font_desc == desc) return; if (priv->font_desc != NULL) pango_font_description_free (priv->font_desc); priv->font_desc = desc; /* update the font name string we use */ g_free (priv->font_name); priv->font_name = pango_font_description_to_string (priv->font_desc); clutter_text_dirty_cache (self); if (priv->text && priv->text[0] != '\0') clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_FONT_DESCRIPTION]); } static void clutter_text_font_changed_cb (ClutterText *text) { if (text->priv->is_default_font) { PangoFontDescription *font_desc; ClutterSettings *settings; gchar *font_name = NULL; settings = clutter_settings_get_default (); g_object_get (settings, "font-name", &font_name, NULL); CLUTTER_NOTE (ACTOR, "Text[%p]: default font changed to '%s'", text, font_name); font_desc = pango_font_description_from_string (font_name); clutter_text_set_font_description_internal (text, font_desc); g_free (font_name); } clutter_text_dirty_cache (text); clutter_actor_queue_relayout (CLUTTER_ACTOR (text)); } static void clutter_text_direction_changed_cb (GObject *gobject, GParamSpec *pspec) { clutter_text_dirty_cache (CLUTTER_TEXT (gobject)); /* no need to queue a relayout: set_text_direction() will do that for us */ } /* * clutter_text_create_layout: * @text: a #ClutterText * @allocation_width: the allocation width * @allocation_height: the allocation height * * Like clutter_text_create_layout_no_cache(), but will also ensure * the glyphs cache. If a previously cached layout generated using the * same width is available then that will be used instead of * generating a new one. */ static PangoLayout * clutter_text_create_layout (ClutterText *text, gfloat allocation_width, gfloat allocation_height) { ClutterTextPrivate *priv = text->priv; LayoutCache *oldest_cache = priv->cached_layouts; gboolean found_free_cache = FALSE; gint width = -1; gint height = -1; PangoEllipsizeMode ellipsize = PANGO_ELLIPSIZE_NONE; int i; CLUTTER_STATIC_COUNTER (text_cache_hit_counter, "Text layout cache hit counter", "Increments for each layout cache hit", 0); CLUTTER_STATIC_COUNTER (text_cache_miss_counter, "Text layout cache miss counter", "Increments for each layout cache miss", 0); /* First determine the width, height, and ellipsize mode that * we need for the layout. The ellipsize mode depends on * allocation_width/allocation_size as follows: * * Cases, assuming ellipsize != NONE on actor: * * Width request: ellipsization can be set or not on layout, * doesn't matter. * * Height request: ellipsization must never be set on layout * if wrap=true, because we need to measure the wrapped * height. It must always be set if wrap=false. * * Allocate: ellipsization must always be set. * * See http://bugzilla.gnome.org/show_bug.cgi?id=560931 */ if (priv->ellipsize != PANGO_ELLIPSIZE_NONE) { if (allocation_height < 0 && priv->wrap) ; /* must not set ellipsization on wrap=true height request */ else { if (!priv->editable) ellipsize = priv->ellipsize; } } /* When painting, we always need to set the width, since * we might need to align to the right. When getting the * height, however, there are some cases where we know that * the width won't affect the width. * * - editable, single-line text actors, since those can * scroll the layout. * - non-wrapping, non-ellipsizing actors. */ if (allocation_width >= 0 && (allocation_height >= 0 || !((priv->editable && priv->single_line_mode) || (priv->ellipsize == PANGO_ELLIPSIZE_NONE && !priv->wrap)))) { width = allocation_width * 1024 + 0.5f; } /* Pango only uses height if ellipsization is enabled, so don't set * height if ellipsize isn't set. Pango implicitly enables wrapping * if height is set, so don't set height if wrapping is disabled. * In other words, only set height if we want to both wrap then * ellipsize and we're not in single line mode. * * See http://bugzilla.gnome.org/show_bug.cgi?id=560931 if this * seems odd. */ if (allocation_height >= 0 && priv->wrap && priv->ellipsize != PANGO_ELLIPSIZE_NONE && !priv->single_line_mode) { height = allocation_height * 1024 + 0.5f; } /* Search for a cached layout with the same width and keep * track of the oldest one */ for (i = 0; i < N_CACHED_LAYOUTS; i++) { if (priv->cached_layouts[i].layout == NULL) { /* Always prefer free cache spaces */ found_free_cache = TRUE; oldest_cache = priv->cached_layouts + i; } else { PangoLayout *cached = priv->cached_layouts[i].layout; gint cached_width = pango_layout_get_width (cached); gint cached_height = pango_layout_get_height (cached); gint cached_ellipsize = pango_layout_get_ellipsize (cached); if (cached_width == width && cached_height == height && cached_ellipsize == ellipsize) { /* If this cached layout is using the same size then we can * just return that directly */ CLUTTER_NOTE (ACTOR, "ClutterText: %p: cache hit for size %.2fx%.2f", text, allocation_width, allocation_height); CLUTTER_COUNTER_INC (_clutter_uprof_context, text_cache_hit_counter); return priv->cached_layouts[i].layout; } /* When getting the preferred height for a specific width, * we might be able to reuse the layout from getting the * preferred width. If the width that the layout gives * unconstrained is less than the width that we are using * than the height will be unaffected by that width. */ if (allocation_height < 0 && cached_width == -1 && cached_ellipsize == ellipsize) { PangoRectangle logical_rect; pango_layout_get_extents (priv->cached_layouts[i].layout, NULL, &logical_rect); if (logical_rect.width <= width) { /* We've been asked for our height for the width we gave as a result * of a _get_preferred_width call */ CLUTTER_NOTE (ACTOR, "ClutterText: %p: cache hit for size %.2fx%.2f " "(unwrapped width narrower than given width)", text, allocation_width, allocation_height); CLUTTER_COUNTER_INC (_clutter_uprof_context, text_cache_hit_counter); return priv->cached_layouts[i].layout; } } if (!found_free_cache && (priv->cached_layouts[i].age < oldest_cache->age)) { oldest_cache = priv->cached_layouts + i; } } } CLUTTER_NOTE (ACTOR, "ClutterText: %p: cache miss for size %.2fx%.2f", text, allocation_width, allocation_height); CLUTTER_COUNTER_INC (_clutter_uprof_context, text_cache_miss_counter); /* If we make it here then we didn't have a cached version so we need to recreate the layout */ if (oldest_cache->layout) g_object_unref (oldest_cache->layout); oldest_cache->layout = clutter_text_create_layout_no_cache (text, width, height, ellipsize); cogl_pango_ensure_glyph_cache_for_layout (oldest_cache->layout); /* Mark the 'time' this cache was created and advance the time */ oldest_cache->age = priv->cache_age++; return oldest_cache->layout; } static gint clutter_text_coords_to_position (ClutterText *text, gfloat x, gfloat y) { ClutterTextPrivate *priv = text->priv; gint index_; gint px, py; gint trailing; /* Take any offset due to scrolling into account */ if (priv->single_line_mode) x += priv->text_x * -1; px = x * PANGO_SCALE; py = y * PANGO_SCALE; pango_layout_xy_to_index (clutter_text_get_layout (text), px, py, &index_, &trailing); return index_ + trailing; } /** * clutter_text_position_to_coords: * @self: a #ClutterText * @position: position in characters * @x: (out): return location for the X coordinate, or %NULL * @y: (out): return location for the Y coordinate, or %NULL * @line_height: (out): return location for the line height, or %NULL * * Retrieves the coordinates of the given @position. * * Return value: %TRUE if the conversion was successful * * Since: 1.0 */ gboolean clutter_text_position_to_coords (ClutterText *self, gint position, gfloat *x, gfloat *y, gfloat *line_height) { ClutterTextPrivate *priv; PangoRectangle rect; gint n_chars; gint password_char_bytes = 1; gint index_; g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); priv = self->priv; 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) password_char_bytes = g_unichar_to_utf8 (priv->password_char, NULL); if (position == -1) { if (priv->password_char == 0) { if (priv->editable && priv->preedit_set) index_ = priv->n_bytes + strlen (priv->preedit_str); else index_ = priv->n_bytes; } else index_ = n_chars * password_char_bytes; } else if (position == 0) { index_ = 0; } else { gchar *text = clutter_text_get_display_text (self); GString *tmp = g_string_new (text); gint cursor_index; cursor_index = offset_to_bytes (text, priv->position); if (priv->preedit_str != NULL) g_string_insert (tmp, cursor_index, priv->preedit_str); if (priv->password_char == 0) index_ = offset_to_bytes (tmp->str, position); else index_ = position * password_char_bytes; g_free (text); g_string_free (tmp, TRUE); } pango_layout_get_cursor_pos (clutter_text_get_layout (self), index_, &rect, NULL); if (x) { *x = (gfloat) rect.x / 1024.0f; /* Take any offset due to scrolling into account */ if (priv->single_line_mode) *x += priv->text_x; } if (y) *y = (gfloat) rect.y / 1024.0f; if (line_height) *line_height = (gfloat) rect.height / 1024.0f; return TRUE; } static inline void clutter_text_ensure_cursor_position (ClutterText *self) { ClutterTextPrivate *priv = self->priv; gfloat x, y, cursor_height; ClutterGeometry cursor_pos = { 0, }; gboolean x_changed, y_changed; gboolean width_changed, height_changed; gint position; position = priv->position; if (priv->editable && priv->preedit_set) { if (position == -1) position = priv->n_chars; position += priv->preedit_cursor_pos; } 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, position, &x, &y, &cursor_height); cursor_pos.x = x; cursor_pos.y = y; cursor_pos.width = priv->cursor_size; cursor_pos.height = cursor_height - 2; x_changed = priv->cursor_pos.x != cursor_pos.x; y_changed = priv->cursor_pos.y != cursor_pos.y; width_changed = priv->cursor_pos.width != cursor_pos.width; height_changed = priv->cursor_pos.height != cursor_pos.height; if (x_changed || y_changed || width_changed || height_changed) { priv->cursor_pos = cursor_pos; g_signal_emit (self, text_signals[CURSOR_EVENT], 0, &priv->cursor_pos); } } /** * clutter_text_delete_selection: * @self: a #ClutterText * * Deletes the currently selected text * * This function is only useful in subclasses of #ClutterText * * Return value: %TRUE if text was deleted or if the text actor * is empty, and %FALSE otherwise * * Since: 1.0 */ gboolean clutter_text_delete_selection (ClutterText *self) { ClutterTextPrivate *priv; gint start_index; gint end_index; gint old_position, old_selection; g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); priv = self->priv; if (!priv->text) return TRUE; start_index = offset_real (priv->text, priv->position); end_index = offset_real (priv->text, priv->selection_bound); if (end_index == start_index) return FALSE; if (end_index < start_index) { gint temp = start_index; start_index = end_index; end_index = temp; } old_position = priv->position; old_selection = priv->selection_bound; clutter_text_delete_text (self, start_index, end_index); priv->position = start_index; priv->selection_bound = start_index; /* Not required to be guarded by g_object_freeze/thaw_notify */ if (priv->position != old_position) _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_POSITION]); if (priv->selection_bound != old_selection) _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTION_BOUND]); return TRUE; } /* * Utility function to update both cursor position and selection bound * at once */ static inline void clutter_text_set_positions (ClutterText *self, gint new_pos, gint new_bound) { g_object_freeze_notify (G_OBJECT (self)); clutter_text_set_cursor_position (self, new_pos); clutter_text_set_selection_bound (self, new_bound); g_object_thaw_notify (G_OBJECT (self)); } static inline void clutter_text_set_text_internal (ClutterText *self, const gchar *text) { ClutterTextPrivate *priv = self->priv; g_object_freeze_notify (G_OBJECT (self)); if (priv->max_length > 0) { gint len = g_utf8_strlen (text, -1); if (len < priv->max_length) { g_free (priv->text); priv->text = g_strdup (text); priv->n_bytes = strlen (text); priv->n_chars = len; } else { gchar *p = g_utf8_offset_to_pointer (text, priv->max_length); gchar *n = g_malloc0 ((p - text) + 1); g_free (priv->text); g_utf8_strncpy (n, text, priv->max_length); priv->text = n; priv->n_bytes = strlen (n); priv->n_chars = priv->max_length; } } else { g_free (priv->text); priv->text = g_strdup (text); priv->n_bytes = strlen (text); priv->n_chars = g_utf8_strlen (text, -1); } if (priv->n_bytes == 0) clutter_text_set_positions (self, -1, -1); clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); g_signal_emit (self, text_signals[TEXT_CHANGED], 0); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_TEXT]); g_object_thaw_notify (G_OBJECT (self)); } static inline void clutter_text_set_markup_internal (ClutterText *self, const gchar *str) { ClutterTextPrivate *priv = self->priv; GError *error; gchar *text = NULL; PangoAttrList *attrs = NULL; gboolean res; error = NULL; res = pango_parse_markup (str, -1, 0, &attrs, &text, NULL, &error); if (!res) { if (G_LIKELY (error)) { g_warning ("Failed to set the markup of the actor of class '%s': %s", G_OBJECT_TYPE_NAME (self), error->message); g_error_free (error); } else g_warning ("Failed to set the markup of the actor of class '%s'", G_OBJECT_TYPE_NAME (self)); return; } if (text) { gint tmp_pos = 0; g_signal_emit (self, text_signals[DELETE_TEXT], 0, 0, -1); g_signal_emit (self, text_signals[INSERT_TEXT], 0, text, strlen (text), &tmp_pos); clutter_text_set_text_internal (self, text); g_free (text); } /* Store the new markup attributes */ if (priv->markup_attrs != NULL) pango_attr_list_unref (priv->markup_attrs); priv->markup_attrs = attrs; /* Clear the effective attributes so they will be regenerated when a layout is created */ if (priv->effective_attrs != NULL) { pango_attr_list_unref (priv->effective_attrs); priv->effective_attrs = NULL; } } static void clutter_text_set_property (GObject *gobject, guint prop_id, const GValue *value, GParamSpec *pspec) { ClutterText *self = CLUTTER_TEXT (gobject); switch (prop_id) { case PROP_TEXT: clutter_text_set_text_internal (self, g_value_get_string (value)); break; case PROP_COLOR: clutter_text_set_color (self, clutter_value_get_color (value)); break; case PROP_FONT_NAME: clutter_text_set_font_name (self, g_value_get_string (value)); break; case PROP_FONT_DESCRIPTION: clutter_text_set_font_description (self, g_value_get_boxed (value)); break; case PROP_USE_MARKUP: clutter_text_set_use_markup (self, g_value_get_boolean (value)); break; case PROP_ATTRIBUTES: clutter_text_set_attributes (self, g_value_get_boxed (value)); break; case PROP_LINE_ALIGNMENT: clutter_text_set_line_alignment (self, g_value_get_enum (value)); break; case PROP_LINE_WRAP: clutter_text_set_line_wrap (self, g_value_get_boolean (value)); break; case PROP_LINE_WRAP_MODE: clutter_text_set_line_wrap_mode (self, g_value_get_enum (value)); break; case PROP_JUSTIFY: clutter_text_set_justify (self, g_value_get_boolean (value)); break; case PROP_ELLIPSIZE: clutter_text_set_ellipsize (self, g_value_get_enum (value)); break; case PROP_POSITION: clutter_text_set_cursor_position (self, g_value_get_int (value)); break; case PROP_SELECTION_BOUND: clutter_text_set_selection_bound (self, g_value_get_int (value)); break; case PROP_SELECTION_COLOR: clutter_text_set_selection_color (self, g_value_get_boxed (value)); break; case PROP_CURSOR_VISIBLE: clutter_text_set_cursor_visible (self, g_value_get_boolean (value)); break; case PROP_CURSOR_COLOR: clutter_text_set_cursor_color (self, g_value_get_boxed (value)); break; case PROP_CURSOR_SIZE: clutter_text_set_cursor_size (self, g_value_get_int (value)); break; case PROP_EDITABLE: clutter_text_set_editable (self, g_value_get_boolean (value)); break; case PROP_ACTIVATABLE: clutter_text_set_activatable (self, g_value_get_boolean (value)); break; case PROP_SELECTABLE: clutter_text_set_selectable (self, g_value_get_boolean (value)); break; case PROP_PASSWORD_CHAR: clutter_text_set_password_char (self, g_value_get_uint (value)); break; case PROP_MAX_LENGTH: clutter_text_set_max_length (self, g_value_get_int (value)); break; case PROP_SINGLE_LINE_MODE: clutter_text_set_single_line_mode (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); } } static void clutter_text_get_property (GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { ClutterTextPrivate *priv = CLUTTER_TEXT (gobject)->priv; switch (prop_id) { case PROP_TEXT: g_value_set_string (value, priv->text); break; case PROP_FONT_NAME: g_value_set_string (value, priv->font_name); break; case PROP_FONT_DESCRIPTION: g_value_set_boxed (value, priv->font_desc); break; case PROP_USE_MARKUP: g_value_set_boolean (value, priv->use_markup); break; case PROP_COLOR: clutter_value_set_color (value, &priv->text_color); break; case PROP_CURSOR_VISIBLE: g_value_set_boolean (value, priv->cursor_visible); break; case PROP_CURSOR_COLOR: clutter_value_set_color (value, &priv->cursor_color); break; case PROP_CURSOR_COLOR_SET: g_value_set_boolean (value, priv->cursor_color_set); break; case PROP_CURSOR_SIZE: g_value_set_int (value, priv->cursor_size); break; case PROP_POSITION: g_value_set_int (value, priv->position); break; case PROP_SELECTION_BOUND: g_value_set_int (value, priv->selection_bound); break; case PROP_EDITABLE: g_value_set_boolean (value, priv->editable); break; case PROP_SELECTABLE: g_value_set_boolean (value, priv->selectable); break; case PROP_SELECTION_COLOR: clutter_value_set_color (value, &priv->selection_color); break; case PROP_SELECTION_COLOR_SET: g_value_set_boolean (value, priv->selection_color_set); break; case PROP_ACTIVATABLE: g_value_set_boolean (value, priv->activatable); break; case PROP_PASSWORD_CHAR: g_value_set_uint (value, priv->password_char); break; case PROP_MAX_LENGTH: g_value_set_int (value, priv->max_length); break; case PROP_SINGLE_LINE_MODE: g_value_set_boolean (value, priv->single_line_mode); break; case PROP_ELLIPSIZE: g_value_set_enum (value, priv->ellipsize); break; case PROP_LINE_WRAP: g_value_set_boolean (value, priv->wrap); break; case PROP_LINE_WRAP_MODE: g_value_set_enum (value, priv->wrap_mode); break; case PROP_LINE_ALIGNMENT: g_value_set_enum (value, priv->alignment); break; case PROP_JUSTIFY: g_value_set_boolean (value, priv->justify); break; case PROP_ATTRIBUTES: g_value_set_boxed (value, priv->attrs); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); } } static void clutter_text_dispose (GObject *gobject) { ClutterText *self = CLUTTER_TEXT (gobject); ClutterTextPrivate *priv = self->priv; /* get rid of the entire cache */ clutter_text_dirty_cache (self); if (priv->direction_changed_id) { g_signal_handler_disconnect (self, priv->direction_changed_id); priv->direction_changed_id = 0; } if (priv->font_changed_id) { g_signal_handler_disconnect (clutter_get_default_backend (), priv->font_changed_id); priv->font_changed_id = 0; } G_OBJECT_CLASS (clutter_text_parent_class)->dispose (gobject); } static void clutter_text_finalize (GObject *gobject) { ClutterText *self = CLUTTER_TEXT (gobject); ClutterTextPrivate *priv = self->priv; if (priv->font_desc) pango_font_description_free (priv->font_desc); if (priv->attrs) pango_attr_list_unref (priv->attrs); if (priv->markup_attrs) pango_attr_list_unref (priv->markup_attrs); if (priv->effective_attrs) pango_attr_list_unref (priv->effective_attrs); if (priv->preedit_attrs) pango_attr_list_unref (priv->preedit_attrs); g_free (priv->text); g_free (priv->font_name); G_OBJECT_CLASS (clutter_text_parent_class)->finalize (gobject); } static void cursor_paint (ClutterText *self) { ClutterTextPrivate *priv = self->priv; ClutterActor *actor = CLUTTER_ACTOR (self); if (priv->editable && priv->cursor_visible) { guint8 paint_opacity = clutter_actor_get_paint_opacity (actor); const ClutterColor *color; gint position; position = priv->position; if (position == 0) priv->cursor_pos.x -= priv->cursor_size; if (position == priv->selection_bound) { if (priv->cursor_color_set) color = &priv->cursor_color; else color = &priv->text_color; cogl_set_source_color4ub (color->red, color->green, color->blue, paint_opacity * color->alpha / 255); cogl_rectangle (priv->cursor_pos.x, 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); gchar *utf8 = clutter_text_get_display_text (self); gint lines; gint start_index; gint end_index; gint line_no; if (priv->selection_color_set) color = &priv->selection_color; else if (priv->cursor_color_set) color = &priv->cursor_color; else color = &priv->text_color; cogl_set_source_color4ub (color->red, color->green, color->blue, paint_opacity * color->alpha / 255); if (position == 0) start_index = 0; else start_index = offset_to_bytes (utf8, position); if (priv->selection_bound == 0) end_index = 0; else end_index = offset_to_bytes (utf8, priv->selection_bound); if (start_index > end_index) { gint temp = start_index; start_index = end_index; end_index = temp; } lines = pango_layout_get_line_count (layout); for (line_no = 0; line_no < lines; line_no++) { PangoLayoutLine *line; gint n_ranges; gint *ranges; gint i; gint index_; gint maxindex; gfloat y, height; line = pango_layout_get_line_readonly (layout, line_no); pango_layout_line_x_to_index (line, G_MAXINT, &maxindex, NULL); if (maxindex < start_index) continue; pango_layout_line_get_x_ranges (line, start_index, end_index, &ranges, &n_ranges); pango_layout_line_x_to_index (line, 0, &index_, NULL); clutter_text_position_to_coords (self, bytes_to_offset (utf8, index_), NULL, &y, &height); for (i = 0; i < n_ranges; i++) { gint range_x; gint range_width; range_x = ranges[i * 2] / PANGO_SCALE; /* Account for any scrolling in single line mode */ if (priv->single_line_mode) range_x += priv->text_x; range_width = (ranges[i * 2 + 1] - ranges[i * 2]) / PANGO_SCALE; cogl_rectangle (range_x, y, range_x + range_width, y + height); } g_free (ranges); } g_free (utf8); } } } static gint clutter_text_move_word_backward (ClutterText *self, gint start) { ClutterTextPrivate *priv = self->priv; gint retval = start; if (priv->text && start > 0) { PangoLayout *layout = clutter_text_get_layout (self); PangoLogAttr *log_attrs = NULL; gint n_attrs = 0; pango_layout_get_log_attrs (layout, &log_attrs, &n_attrs); retval = start - 1; while (retval > 0 && !log_attrs[retval].is_word_start) retval -= 1; g_free (log_attrs); } return retval; } static gint clutter_text_move_word_forward (ClutterText *self, gint start) { ClutterTextPrivate *priv = self->priv; gint retval = start; if (priv->text && start < priv->n_chars) { PangoLayout *layout = clutter_text_get_layout (self); PangoLogAttr *log_attrs = NULL; gint n_attrs = 0; pango_layout_get_log_attrs (layout, &log_attrs, &n_attrs); retval = start + 1; while (retval < priv->n_chars && !log_attrs[retval].is_word_end) retval += 1; g_free (log_attrs); } return retval; } static gint clutter_text_move_line_start (ClutterText *self, gint start) { ClutterTextPrivate *priv = self->priv; PangoLayoutLine *layout_line; PangoLayout *layout; gint line_no; gint index_; gint position; layout = clutter_text_get_layout (self); if (start == 0) index_ = 0; else index_ = offset_to_bytes (priv->text, start); pango_layout_index_to_line_x (layout, index_, 0, &line_no, NULL); layout_line = pango_layout_get_line_readonly (layout, line_no); if (!layout_line) return FALSE; pango_layout_line_x_to_index (layout_line, 0, &index_, NULL); position = bytes_to_offset (priv->text, index_); return position; } static gint clutter_text_move_line_end (ClutterText *self, gint start) { ClutterTextPrivate *priv = self->priv; PangoLayoutLine *layout_line; PangoLayout *layout; gint line_no; gint index_; gint trailing; gint position; layout = clutter_text_get_layout (self); if (start == 0) index_ = 0; else index_ = offset_to_bytes (priv->text, priv->position); pango_layout_index_to_line_x (layout, index_, 0, &line_no, NULL); layout_line = pango_layout_get_line_readonly (layout, line_no); if (!layout_line) return FALSE; pango_layout_line_x_to_index (layout_line, G_MAXINT, &index_, &trailing); index_ += trailing; position = bytes_to_offset (priv->text, index_); return position; } static void clutter_text_select_word (ClutterText *self) { gint cursor_pos = self->priv->position; gint start_pos, end_pos; start_pos = clutter_text_move_word_backward (self, cursor_pos); end_pos = clutter_text_move_word_forward (self, cursor_pos); clutter_text_set_selection (self, start_pos, end_pos); } static void clutter_text_select_line (ClutterText *self) { ClutterTextPrivate *priv = self->priv; gint cursor_pos = priv->position; gint start_pos, end_pos; if (priv->single_line_mode) { start_pos = 0; end_pos = -1; } else { start_pos = clutter_text_move_line_start (self, cursor_pos); end_pos = clutter_text_move_line_end (self, cursor_pos); } clutter_text_set_selection (self, start_pos, end_pos); } static gboolean clutter_text_button_press (ClutterActor *actor, ClutterButtonEvent *event) { ClutterText *self = CLUTTER_TEXT (actor); ClutterTextPrivate *priv = self->priv; gboolean res = FALSE; gfloat x, y; gint index_; /* we'll steal keyfocus if we do not have it */ clutter_actor_grab_key_focus (actor); /* if the actor is empty we just reset everything and not * set up the dragging of the selection since there's nothing * to select */ if (priv->text == NULL || priv->text[0] == '\0') { clutter_text_set_positions (self, -1, -1); return TRUE; } res = clutter_actor_transform_stage_point (actor, event->x, event->y, &x, &y); if (res) { gint offset; index_ = clutter_text_coords_to_position (self, x, y); offset = bytes_to_offset (priv->text, index_); /* what we select depends on the number of button clicks we * receive: * * 1: just position the cursor and the selection * 2: select the current word * 3: select the contents of the whole actor */ if (event->click_count == 1) { clutter_text_set_positions (self, offset, offset); } else if (event->click_count == 2) { clutter_text_select_word (self); } else if (event->click_count == 3) { clutter_text_select_line (self); } } /* grab the pointer */ priv->in_select_drag = TRUE; clutter_grab_pointer (actor); return TRUE; } static gboolean clutter_text_motion (ClutterActor *actor, ClutterMotionEvent *mev) { ClutterText *self = CLUTTER_TEXT (actor); ClutterTextPrivate *priv = self->priv; gfloat x, y; gint index_, offset; gboolean res; if (!priv->in_select_drag) return FALSE; res = clutter_actor_transform_stage_point (actor, mev->x, mev->y, &x, &y); if (!res) return FALSE; index_ = clutter_text_coords_to_position (self, x, y); offset = bytes_to_offset (priv->text, index_); if (priv->selectable) clutter_text_set_cursor_position (self, offset); else clutter_text_set_positions (self, offset, offset); return TRUE; } static gboolean clutter_text_button_release (ClutterActor *actor, ClutterButtonEvent *bev) { ClutterText *self = CLUTTER_TEXT (actor); ClutterTextPrivate *priv = self->priv; if (priv->in_select_drag) { clutter_ungrab_pointer (); priv->in_select_drag = FALSE; return TRUE; } return FALSE; } static gboolean clutter_text_key_press (ClutterActor *actor, ClutterKeyEvent *event) { ClutterText *self = CLUTTER_TEXT (actor); ClutterTextPrivate *priv = self->priv; ClutterBindingPool *pool; gboolean res; if (!priv->editable) return FALSE; /* we need to use the ClutterText type name to find our own * key bindings; subclasses will override or chain up this * event handler, so they can do whatever they want there */ pool = clutter_binding_pool_find (g_type_name (CLUTTER_TYPE_TEXT)); g_assert (pool != NULL); /* we allow passing synthetic events that only contain * the Unicode value and not the key symbol */ if (event->keyval == 0 && (event->flags & CLUTTER_EVENT_FLAG_SYNTHETIC)) res = FALSE; else res = clutter_binding_pool_activate (pool, event->keyval, event->modifier_state, G_OBJECT (actor)); /* if the key binding has handled the event we bail out * as fast as we can; otherwise, we try to insert the * Unicode character inside the key event into the text * actor */ if (res) return TRUE; /* Skip keys when control is pressed */ else if ((event->modifier_state & CLUTTER_CONTROL_MASK) == 0) { gunichar key_unichar; key_unichar = clutter_event_get_key_unicode ((ClutterEvent *) event); /* return is reported as CR, but we want LF */ if (key_unichar == '\r') key_unichar = '\n'; if (key_unichar == '\n' || (g_unichar_validate (key_unichar) && !g_unichar_iscntrl (key_unichar))) { /* truncate the eventual selection so that the * Unicode character can replace it */ clutter_text_delete_selection (self); clutter_text_insert_unichar (self, key_unichar); return TRUE; } } return FALSE; } #define TEXT_PADDING 2 static void clutter_text_paint (ClutterActor *self) { ClutterText *text = CLUTTER_TEXT (self); ClutterTextPrivate *priv = text->priv; PangoLayout *layout; ClutterActorBox alloc = { 0, }; CoglColor color = { 0, }; guint8 real_opacity; gint text_x = priv->text_x; gboolean clip_set = FALSE; if (G_UNLIKELY (priv->font_desc == NULL || priv->text == NULL)) { CLUTTER_NOTE (ACTOR, "desc: %p, text %p", priv->font_desc ? priv->font_desc : 0x0, priv->text ? priv->text : 0x0); return; } clutter_actor_get_allocation_box (self, &alloc); if (priv->editable && priv->single_line_mode) layout = clutter_text_create_layout (text, -1, -1); else layout = clutter_text_create_layout (text, alloc.x2 - alloc.x1, alloc.y2 - alloc.y1); if (priv->editable && priv->cursor_visible) clutter_text_ensure_cursor_position (text); if (priv->editable && priv->single_line_mode) { PangoRectangle logical_rect = { 0, }; gint actor_width, text_width; pango_layout_get_extents (layout, NULL, &logical_rect); cogl_clip_push_rectangle (0, 0, (alloc.x2 - alloc.x1), (alloc.y2 - alloc.y1)); clip_set = TRUE; actor_width = (alloc.x2 - alloc.x1) - 2 * TEXT_PADDING; text_width = logical_rect.width / PANGO_SCALE; if (actor_width < text_width) { gint cursor_x = priv->cursor_pos.x; if (priv->position == -1) { text_x = actor_width - text_width; } else if (priv->position == 0) { text_x = TEXT_PADDING; } else { if (cursor_x < 0) { text_x = text_x - cursor_x - TEXT_PADDING; } else if (cursor_x > actor_width) { text_x = text_x + (actor_width - cursor_x) - TEXT_PADDING; } } /* Update the absolute cursor position as it may have moved due to * scrolling */ priv->text_x = text_x; clutter_text_ensure_cursor_position (text); } else { text_x = TEXT_PADDING; } } else text_x = 0; priv->text_x = text_x; cursor_paint (text); real_opacity = clutter_actor_get_paint_opacity (self) * priv->text_color.alpha / 255; CLUTTER_NOTE (PAINT, "painting text (text: '%s')", priv->text); cogl_color_init_from_4ub (&color, priv->text_color.red, priv->text_color.green, priv->text_color.blue, real_opacity); cogl_pango_render_layout (layout, text_x, 0, &color, 0); if (clip_set) cogl_clip_pop (); } static gboolean clutter_text_get_paint_volume (ClutterActor *self, ClutterPaintVolume *volume) { return _clutter_actor_set_default_paint_volume (self, CLUTTER_TYPE_TEXT, volume); } static void clutter_text_get_preferred_width (ClutterActor *self, gfloat for_height, gfloat *min_width_p, gfloat *natural_width_p) { ClutterText *text = CLUTTER_TEXT (self); ClutterTextPrivate *priv = text->priv; PangoRectangle logical_rect = { 0, }; PangoLayout *layout; gint logical_width; gfloat layout_width; layout = clutter_text_create_layout (text, -1, -1); pango_layout_get_extents (layout, NULL, &logical_rect); /* the X coordinate of the logical rectangle might be non-zero * according to the Pango documentation; hence, we need to offset * the width accordingly */ logical_width = logical_rect.x + logical_rect.width; layout_width = logical_width > 0 ? (logical_width / 1024.0f) : 1; if (min_width_p) { if (priv->wrap || priv->ellipsize || priv->editable) *min_width_p = 1; else *min_width_p = layout_width; } if (natural_width_p) { if (priv->editable && priv->single_line_mode) *natural_width_p = layout_width + TEXT_PADDING * 2; else *natural_width_p = layout_width; } } static void clutter_text_get_preferred_height (ClutterActor *self, gfloat for_width, gfloat *min_height_p, gfloat *natural_height_p) { ClutterTextPrivate *priv = CLUTTER_TEXT (self)->priv; if (for_width == 0) { if (min_height_p) *min_height_p = 0; if (natural_height_p) *natural_height_p = 0; } else { PangoLayout *layout; PangoRectangle logical_rect = { 0, }; gint logical_height; gfloat layout_height; layout = clutter_text_create_layout (CLUTTER_TEXT (self), for_width, -1); pango_layout_get_extents (layout, NULL, &logical_rect); /* the Y coordinate of the logical rectangle might be non-zero * according to the Pango documentation; hence, we need to offset * the height accordingly */ logical_height = logical_rect.y + logical_rect.height; layout_height = ceilf ((gfloat) logical_height / 1024.0f + 0.5); if (min_height_p) { /* if we wrap and ellipsize then the minimum height is * going to be at least the size of the first line */ if (priv->ellipsize && priv->wrap) { PangoLayoutLine *line; gfloat line_height; line = pango_layout_get_line_readonly (layout, 0); pango_layout_line_get_extents (line, NULL, &logical_rect); logical_height = logical_rect.y + logical_rect.height; line_height = (gfloat) logical_height / 1024.0f; *min_height_p = line_height; } else *min_height_p = layout_height; } if (natural_height_p) *natural_height_p = layout_height; } } static void clutter_text_allocate (ClutterActor *self, const ClutterActorBox *box, ClutterAllocationFlags flags) { ClutterText *text = CLUTTER_TEXT (self); ClutterActorClass *parent_class; /* Ensure that there is a cached layout with the right width so * that we don't need to create the text during the paint run * * if the Text is editable and in single line mode we don't want * to have any limit on the layout size, since the paint will clip * it to the allocation of the actor */ if (text->priv->editable && text->priv->single_line_mode) clutter_text_create_layout (text, -1, -1); else clutter_text_create_layout (text, box->x2 - box->x1, box->y2 - box->y1); parent_class = CLUTTER_ACTOR_CLASS (clutter_text_parent_class); parent_class->allocate (self, box, flags); } static gboolean clutter_text_real_move_left (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos = priv->position; gint new_pos = 0; gint len; len = priv->n_chars; g_object_freeze_notify (G_OBJECT (self)); if (pos != 0 && len != 0) { if (modifiers & CLUTTER_CONTROL_MASK) { if (pos == -1) new_pos = clutter_text_move_word_backward (self, len); else new_pos = clutter_text_move_word_backward (self, pos); } else { if (pos == -1) new_pos = len - 1; else new_pos = pos - 1; } clutter_text_set_cursor_position (self, new_pos); } if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_move_right (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos = priv->position; gint len = priv->n_chars; gint new_pos = 0; g_object_freeze_notify (G_OBJECT (self)); if (pos != -1 && len !=0) { if (modifiers & CLUTTER_CONTROL_MASK) { if (pos != len) new_pos = clutter_text_move_word_forward (self, pos); } else { if (pos != len) new_pos = pos + 1; } clutter_text_set_cursor_position (self, new_pos); } if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_move_up (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; PangoLayoutLine *layout_line; PangoLayout *layout; gint line_no; gint index_, trailing; gint pos; gint x; layout = clutter_text_get_layout (self); if (priv->position == 0) index_ = 0; else index_ = offset_to_bytes (priv->text, priv->position); pango_layout_index_to_line_x (layout, index_, 0, &line_no, &x); line_no -= 1; if (line_no < 0) return FALSE; if (priv->x_pos != -1) x = priv->x_pos; layout_line = pango_layout_get_line_readonly (layout, line_no); if (!layout_line) return FALSE; pango_layout_line_x_to_index (layout_line, x, &index_, &trailing); g_object_freeze_notify (G_OBJECT (self)); pos = bytes_to_offset (priv->text, index_); clutter_text_set_cursor_position (self, pos + trailing); /* Store the target x position to avoid drifting left and right when moving the cursor up and down */ priv->x_pos = x; if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_move_down (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; PangoLayoutLine *layout_line; PangoLayout *layout; gint line_no; gint index_, trailing; gint x; gint pos; layout = clutter_text_get_layout (self); if (priv->position == 0) index_ = 0; else index_ = offset_to_bytes (priv->text, priv->position); pango_layout_index_to_line_x (layout, index_, 0, &line_no, &x); if (priv->x_pos != -1) x = priv->x_pos; layout_line = pango_layout_get_line_readonly (layout, line_no + 1); if (!layout_line) return FALSE; pango_layout_line_x_to_index (layout_line, x, &index_, &trailing); g_object_freeze_notify (G_OBJECT (self)); pos = bytes_to_offset (priv->text, index_); clutter_text_set_cursor_position (self, pos + trailing); /* Store the target x position to avoid drifting left and right when moving the cursor up and down */ priv->x_pos = x; if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_line_start (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint position; g_object_freeze_notify (G_OBJECT (self)); position = clutter_text_move_line_start (self, priv->position); clutter_text_set_cursor_position (self, position); if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_line_end (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint position; g_object_freeze_notify (G_OBJECT (self)); position = clutter_text_move_line_end (self, priv->position); clutter_text_set_cursor_position (self, position); if (!(priv->selectable && (modifiers & CLUTTER_SHIFT_MASK))) clutter_text_clear_selection (self); g_object_thaw_notify (G_OBJECT (self)); return TRUE; } static gboolean clutter_text_real_select_all (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { clutter_text_set_positions (self, 0, self->priv->n_chars); return TRUE; } static gboolean clutter_text_real_del_next (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos; gint len; if (clutter_text_delete_selection (self)) return TRUE; pos = priv->position; len = priv->n_chars; if (len && pos != -1 && pos < len) clutter_text_delete_text (self, pos, pos + 1); return TRUE; } static gboolean clutter_text_real_del_word_next (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos; gint len; pos = priv->position; len = priv->n_chars; if (len && pos != -1 && pos < len) { gint end; end = clutter_text_move_word_forward (self, pos); clutter_text_delete_text (self, pos, end); if (priv->selection_bound >= end) { gint new_bound; new_bound = priv->selection_bound - (end - pos); clutter_text_set_selection_bound (self, new_bound); } else if (priv->selection_bound > pos) { clutter_text_set_selection_bound (self, pos); } } return TRUE; } static gboolean clutter_text_real_del_prev (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos; gint len; if (clutter_text_delete_selection (self)) return TRUE; pos = priv->position; len = priv->n_chars; if (pos != 0 && len != 0) { if (pos == -1) { clutter_text_delete_text (self, len - 1, len); clutter_text_set_positions (self, -1, -1); } else { clutter_text_delete_text (self, pos - 1, pos); clutter_text_set_positions (self, pos - 1, pos - 1); } } return TRUE; } static gboolean clutter_text_real_del_word_prev (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { ClutterTextPrivate *priv = self->priv; gint pos; gint len; pos = priv->position; len = priv->n_chars; if (pos != 0 && len != 0) { gint new_pos; if (pos == -1) { new_pos = clutter_text_move_word_backward (self, len); clutter_text_delete_text (self, new_pos, len); clutter_text_set_positions (self, -1, -1); } else { new_pos = clutter_text_move_word_backward (self, pos); clutter_text_delete_text (self, new_pos, pos); clutter_text_set_cursor_position (self, new_pos); if (priv->selection_bound >= pos) { gint new_bound; new_bound = priv->selection_bound - (pos - new_pos); clutter_text_set_selection_bound (self, new_bound); } else if (priv->selection_bound >= new_pos) { clutter_text_set_selection_bound (self, new_pos); } } } return TRUE; } static gboolean clutter_text_real_activate (ClutterText *self, const gchar *action, guint keyval, ClutterModifierType modifiers) { return clutter_text_activate (self); } static inline void clutter_text_add_move_binding (ClutterBindingPool *pool, const gchar *action, guint key_val, ClutterModifierType additional_modifiers, GCallback callback) { clutter_binding_pool_install_action (pool, action, key_val, 0, callback, NULL, NULL); clutter_binding_pool_install_action (pool, action, key_val, CLUTTER_SHIFT_MASK, callback, NULL, NULL); if (additional_modifiers != 0) { clutter_binding_pool_install_action (pool, action, key_val, additional_modifiers, callback, NULL, NULL); clutter_binding_pool_install_action (pool, action, key_val, CLUTTER_SHIFT_MASK | additional_modifiers, callback, NULL, NULL); } } static void clutter_text_class_init (ClutterTextClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); ClutterBindingPool *binding_pool; GParamSpec *pspec; g_type_class_add_private (klass, sizeof (ClutterTextPrivate)); gobject_class->set_property = clutter_text_set_property; gobject_class->get_property = clutter_text_get_property; gobject_class->dispose = clutter_text_dispose; gobject_class->finalize = clutter_text_finalize; actor_class->paint = clutter_text_paint; actor_class->get_paint_volume = clutter_text_get_paint_volume; actor_class->get_preferred_width = clutter_text_get_preferred_width; actor_class->get_preferred_height = clutter_text_get_preferred_height; actor_class->allocate = clutter_text_allocate; actor_class->key_press_event = clutter_text_key_press; actor_class->button_press_event = clutter_text_button_press; actor_class->button_release_event = clutter_text_button_release; actor_class->motion_event = clutter_text_motion; /** * ClutterText:font-name: * * The font to be used by the #ClutterText, as a string * that can be parsed by pango_font_description_from_string(). * * If set to %NULL, the default system font will be used instead. * * Since: 1.0 */ pspec = g_param_spec_string ("font-name", P_("Font Name"), P_("The font to be used by the text"), NULL, CLUTTER_PARAM_READWRITE); obj_props[PROP_FONT_NAME] = pspec; g_object_class_install_property (gobject_class, PROP_FONT_NAME, pspec); /** * ClutterText:font-description: * * The #PangoFontDescription that should be used by the #ClutterText * * If you have a string describing the font then you should look at * #ClutterText:font-name instead * * Since: 1.2 */ pspec = g_param_spec_boxed ("font-description", P_("Font Description"), P_("The font description to be used"), PANGO_TYPE_FONT_DESCRIPTION, CLUTTER_PARAM_READWRITE); obj_props[PROP_FONT_DESCRIPTION] = pspec; g_object_class_install_property (gobject_class, PROP_FONT_DESCRIPTION, pspec); /** * ClutterText:text: * * The text to render inside the actor. * * Since: 1.0 */ pspec = g_param_spec_string ("text", P_("Text"), P_("The text to render"), "", CLUTTER_PARAM_READWRITE); obj_props[PROP_TEXT] = pspec; g_object_class_install_property (gobject_class, PROP_TEXT, pspec); /** * ClutterText:color: * * The color used to render the text. * * Since: 1.0 */ pspec = clutter_param_spec_color ("color", P_("Font Color"), P_("Color of the font used by the text"), &default_text_color, CLUTTER_PARAM_READWRITE); obj_props[PROP_COLOR] = pspec; g_object_class_install_property (gobject_class, PROP_COLOR, pspec); /** * ClutterText:editable: * * Whether key events delivered to the actor causes editing. * * Since: 1.0 */ pspec = g_param_spec_boolean ("editable", P_("Editable"), P_("Whether the text is editable"), TRUE, G_PARAM_READWRITE); obj_props[PROP_EDITABLE] = pspec; g_object_class_install_property (gobject_class, PROP_EDITABLE, pspec); /** * ClutterText:selectable: * * Whether it is possible to select text, either using the pointer * or the keyboard. * * Since: 1.0 */ pspec = g_param_spec_boolean ("selectable", P_("Selectable"), P_("Whether the text is selectable"), TRUE, G_PARAM_READWRITE); obj_props[PROP_SELECTABLE] = pspec; g_object_class_install_property (gobject_class, PROP_SELECTABLE, pspec); /** * ClutterText:activatable: * * Toggles whether return invokes the activate signal or not. * * Since: 1.0 */ pspec = g_param_spec_boolean ("activatable", P_("Activatable"), P_("Whether pressing return causes the activate signal to be emitted"), TRUE, G_PARAM_READWRITE); obj_props[PROP_ACTIVATABLE] = pspec; g_object_class_install_property (gobject_class, PROP_ACTIVATABLE, pspec); /** * ClutterText:cursor-visible: * * Whether the input cursor is visible or not, it will only be visible * if both #ClutterText:cursor-visible and #ClutterText:editable are * set to %TRUE. * * Since: 1.0 */ pspec = g_param_spec_boolean ("cursor-visible", P_("Cursor Visible"), P_("Whether the input cursor is visible"), TRUE, CLUTTER_PARAM_READWRITE); obj_props[PROP_CURSOR_VISIBLE] = pspec; g_object_class_install_property (gobject_class, PROP_CURSOR_VISIBLE, pspec); /** * ClutterText:cursor-color: * * The color of the cursor. * * Since: 1.0 */ pspec = clutter_param_spec_color ("cursor-color", P_("Cursor Color"), P_("Cursor Color"), &default_cursor_color, CLUTTER_PARAM_READWRITE); obj_props[PROP_CURSOR_COLOR] = pspec; g_object_class_install_property (gobject_class, PROP_CURSOR_COLOR, pspec); /** * ClutterText:cursor-color-set: * * Will be set to %TRUE if #ClutterText:cursor-color has been set. * * Since: 1.0 */ pspec = g_param_spec_boolean ("cursor-color-set", P_("Cursor Color Set"), P_("Whether the cursor color has been set"), FALSE, CLUTTER_PARAM_READABLE); obj_props[PROP_CURSOR_COLOR_SET] = pspec; g_object_class_install_property (gobject_class, PROP_CURSOR_COLOR_SET, pspec); /** * ClutterText:cursor-size: * * The size of the cursor, in pixels. If set to -1 the size used will * be the default cursor size of 2 pixels. * * Since: 1.0 */ pspec = g_param_spec_int ("cursor-size", P_("Cursor Size"), P_("The width of the cursor, in pixels"), -1, G_MAXINT, DEFAULT_CURSOR_SIZE, CLUTTER_PARAM_READWRITE); obj_props[PROP_CURSOR_SIZE] = pspec; g_object_class_install_property (gobject_class, PROP_CURSOR_SIZE, pspec); /** * ClutterText:position: * * The current input cursor position. -1 is taken to be the end of the text * * Since: 1.0 */ pspec = g_param_spec_int ("position", P_("Cursor Position"), P_("The cursor position"), -1, G_MAXINT, -1, CLUTTER_PARAM_READWRITE); obj_props[PROP_POSITION] = pspec; g_object_class_install_property (gobject_class, PROP_POSITION, pspec); /** * ClutterText:selection-bound: * * The current input cursor position. -1 is taken to be the end of the text * * Since: 1.0 */ pspec = g_param_spec_int ("selection-bound", P_("Selection-bound"), P_("The cursor position of the other end of the selection"), -1, G_MAXINT, -1, CLUTTER_PARAM_READWRITE); obj_props[PROP_SELECTION_BOUND] = pspec; g_object_class_install_property (gobject_class, PROP_SELECTION_BOUND, pspec); /** * ClutterText:selection-color: * * The color of the selection. * * Since: 1.0 */ pspec = clutter_param_spec_color ("selection-color", P_("Selection Color"), P_("Selection Color"), &default_selection_color, CLUTTER_PARAM_READWRITE); obj_props[PROP_SELECTION_COLOR] = pspec; g_object_class_install_property (gobject_class, PROP_SELECTION_COLOR, pspec); /** * ClutterText:selection-color-set: * * Will be set to %TRUE if #ClutterText:selection-color has been set. * * Since: 1.0 */ pspec = g_param_spec_boolean ("selection-color-set", P_("Selection Color Set"), P_("Whether the selection color has been set"), FALSE, CLUTTER_PARAM_READABLE); obj_props[PROP_SELECTION_COLOR_SET] = pspec; g_object_class_install_property (gobject_class, PROP_SELECTION_COLOR_SET, pspec); /** * ClutterText:attributes: * * A list of #PangoStyleAttributes to be applied to the * contents of the #ClutterText actor. * * Since: 1.0 */ pspec = g_param_spec_boxed ("attributes", P_("Attributes"), P_("A list of style attributes to apply to the contents of the actor"), PANGO_TYPE_ATTR_LIST, CLUTTER_PARAM_READWRITE); obj_props[PROP_ATTRIBUTES] = pspec; g_object_class_install_property (gobject_class, PROP_ATTRIBUTES, pspec); /** * ClutterText:use-markup: * * Whether the text includes Pango markup. * * For more informations about the Pango markup format, see * pango_layout_set_markup() in the Pango documentation. * * It is not possible to round-trip this property between * %TRUE and %FALSE. Once a string with markup has been set on * a #ClutterText actor with :use-markup set to %TRUE, the markup * is stripped from the string. * * Since: 1.0 */ pspec = g_param_spec_boolean ("use-markup", P_("Use markup"), P_("Whether or not the text includes Pango markup"), FALSE, CLUTTER_PARAM_READWRITE); obj_props[PROP_USE_MARKUP] = pspec; g_object_class_install_property (gobject_class, PROP_USE_MARKUP, pspec); /** * ClutterText:line-wrap: * * Whether to wrap the lines of #ClutterText:text if the contents * exceed the available allocation. The wrapping strategy is * controlled by the #ClutterText:line-wrap-mode property. * * Since: 1.0 */ pspec = g_param_spec_boolean ("line-wrap", P_("Line wrap"), P_("If set, wrap the lines if the text becomes too wide"), FALSE, CLUTTER_PARAM_READWRITE); obj_props[PROP_LINE_WRAP] = pspec; g_object_class_install_property (gobject_class, PROP_LINE_WRAP, pspec); /** * ClutterText:line-wrap-mode: * * If #ClutterText:line-wrap is set to %TRUE, this property will * control how the text is wrapped. * * Since: 1.0 */ pspec = g_param_spec_enum ("line-wrap-mode", P_("Line wrap mode"), P_("Control how line-wrapping is done"), PANGO_TYPE_WRAP_MODE, PANGO_WRAP_WORD, CLUTTER_PARAM_READWRITE); obj_props[PROP_LINE_WRAP_MODE] = pspec; g_object_class_install_property (gobject_class, PROP_LINE_WRAP_MODE, pspec); /** * ClutterText:ellipsize: * * The preferred place to ellipsize the contents of the #ClutterText actor * * Since: 1.0 */ pspec = g_param_spec_enum ("ellipsize", P_("Ellipsize"), P_("The preferred place to ellipsize the string"), PANGO_TYPE_ELLIPSIZE_MODE, PANGO_ELLIPSIZE_NONE, CLUTTER_PARAM_READWRITE); obj_props[PROP_ELLIPSIZE] = pspec; g_object_class_install_property (gobject_class, PROP_ELLIPSIZE, pspec); /** * ClutterText:line-alignment: * * The preferred alignment for the text. This property controls * the alignment of multi-line paragraphs. * * Since: 1.0 */ pspec = g_param_spec_enum ("line-alignment", P_("Line Alignment"), P_("The preferred alignment for the string, for multi-line text"), PANGO_TYPE_ALIGNMENT, PANGO_ALIGN_LEFT, CLUTTER_PARAM_READWRITE); obj_props[PROP_LINE_ALIGNMENT] = pspec; g_object_class_install_property (gobject_class, PROP_LINE_ALIGNMENT, pspec); /** * ClutterText:justify: * * Whether the contents of the #ClutterText should be justified * on both margins. * * Since: 1.0 */ pspec = g_param_spec_boolean ("justify", P_("Justify"), P_("Whether the text should be justified"), FALSE, CLUTTER_PARAM_READWRITE); obj_props[PROP_JUSTIFY] = pspec; g_object_class_install_property (gobject_class, PROP_JUSTIFY, pspec); /** * ClutterText:password-char: * * If non-zero, the character that should be used in place of * the actual text in a password text actor. * * Since: 1.0 */ pspec = g_param_spec_unichar ("password-char", P_("Password Character"), P_("If non-zero, use this character to display the actor's contents"), 0, CLUTTER_PARAM_READWRITE); obj_props[PROP_PASSWORD_CHAR] = pspec; g_object_class_install_property (gobject_class, PROP_PASSWORD_CHAR, pspec); /** * ClutterText:max-length: * * The maximum length of the contents of the #ClutterText actor. * * Since: 1.0 */ pspec = g_param_spec_int ("max-length", P_("Max Length"), P_("Maximum length of the text inside the actor"), -1, G_MAXINT, 0, CLUTTER_PARAM_READWRITE); obj_props[PROP_MAX_LENGTH] = pspec; g_object_class_install_property (gobject_class, PROP_MAX_LENGTH, pspec); /** * ClutterText:single-line-mode: * * Whether the #ClutterText actor should be in single line mode * or not. A single line #ClutterText actor will only contain a * single line of text, scrolling it in case its length is bigger * than the allocated size. * * Setting this property will also set the #ClutterText:activatable * property as a side-effect. * * The #ClutterText:single-line-mode property is used only if the * #ClutterText:editable property is set to %TRUE. * * Since: 1.0 */ pspec = g_param_spec_boolean ("single-line-mode", P_("Single Line Mode"), P_("Whether the text should be a single line"), FALSE, CLUTTER_PARAM_READWRITE); obj_props[PROP_SINGLE_LINE_MODE] = pspec; g_object_class_install_property (gobject_class, PROP_SINGLE_LINE_MODE, pspec); /** * ClutterText::text-changed: * @self: the #ClutterText that emitted the signal * * The ::text-changed signal is emitted after @actor's text changes * * Since: 1.0 */ text_signals[TEXT_CHANGED] = g_signal_new (I_("text-changed"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ClutterTextClass, text_changed), NULL, NULL, _clutter_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * ClutterText::insert-text: * @self: the #ClutterText that emitted the signal * @new_text: the new text to insert * @new_text_length: the length of the new text, in bytes, or -1 if * new_text is nul-terminated * @position: the position, in characters, at which to insert the * new text. this is an in-out parameter. After the signal * emission is finished, it should point after the newly * inserted text. * * This signal is emitted when text is inserted into the actor by * the user. It is emitted before @self text changes. * * Since: 1.2 */ text_signals[INSERT_TEXT] = g_signal_new (I_("insert-text"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, 0, NULL, NULL, _clutter_marshal_VOID__STRING_INT_POINTER, G_TYPE_NONE, 3, G_TYPE_STRING, G_TYPE_INT, G_TYPE_POINTER); /** * ClutterText::delete-text: * @self: the #ClutterText that emitted the signal * @start_pos: the starting position * @end_pos: the end position * * This signal is emitted when text is deleted from the actor by * the user. It is emitted before @self text changes. * * Since: 1.2 */ text_signals[DELETE_TEXT] = g_signal_new (I_("delete-text"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, 0, NULL, NULL, _clutter_marshal_VOID__INT_INT, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT); /** * ClutterText::cursor-event: * @self: the #ClutterText that emitted the signal * @geometry: the coordinates of the cursor * * The ::cursor-event signal is emitted whenever the cursor position * changes inside a #ClutterText actor. Inside @geometry it is stored * the current position and size of the cursor, relative to the actor * itself. * * Since: 1.0 */ text_signals[CURSOR_EVENT] = g_signal_new (I_("cursor-event"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ClutterTextClass, cursor_event), NULL, NULL, _clutter_marshal_VOID__BOXED, G_TYPE_NONE, 1, CLUTTER_TYPE_GEOMETRY | G_SIGNAL_TYPE_STATIC_SCOPE); /** * ClutterText::activate * @self: the #ClutterText that emitted the signal * * The ::activate signal is emitted each time the actor is 'activated' * by the user, normally by pressing the 'Enter' key. The signal is * emitted only if #ClutterText:activatable is set to %TRUE. * * Since: 1.0 */ text_signals[ACTIVATE] = g_signal_new (I_("activate"), G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ClutterTextClass, activate), NULL, NULL, _clutter_marshal_VOID__VOID, G_TYPE_NONE, 0); binding_pool = clutter_binding_pool_get_for_class (klass); clutter_text_add_move_binding (binding_pool, "move-left", CLUTTER_KEY_Left, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_move_left)); clutter_text_add_move_binding (binding_pool, "move-left", CLUTTER_KEY_KP_Left, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_move_left)); clutter_text_add_move_binding (binding_pool, "move-right", CLUTTER_KEY_Right, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_move_right)); clutter_text_add_move_binding (binding_pool, "move-right", CLUTTER_KEY_KP_Right, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_move_right)); clutter_text_add_move_binding (binding_pool, "move-up", CLUTTER_KEY_Up, 0, G_CALLBACK (clutter_text_real_move_up)); clutter_text_add_move_binding (binding_pool, "move-up", CLUTTER_KEY_KP_Up, 0, G_CALLBACK (clutter_text_real_move_up)); clutter_text_add_move_binding (binding_pool, "move-down", CLUTTER_KEY_Down, 0, G_CALLBACK (clutter_text_real_move_down)); clutter_text_add_move_binding (binding_pool, "move-down", CLUTTER_KEY_KP_Down, 0, G_CALLBACK (clutter_text_real_move_down)); clutter_text_add_move_binding (binding_pool, "line-start", CLUTTER_KEY_Home, 0, G_CALLBACK (clutter_text_real_line_start)); clutter_text_add_move_binding (binding_pool, "line-start", CLUTTER_KEY_KP_Home, 0, G_CALLBACK (clutter_text_real_line_start)); clutter_text_add_move_binding (binding_pool, "line-start", CLUTTER_KEY_Begin, 0, G_CALLBACK (clutter_text_real_line_start)); clutter_text_add_move_binding (binding_pool, "line-end", CLUTTER_KEY_End, 0, G_CALLBACK (clutter_text_real_line_end)); clutter_text_add_move_binding (binding_pool, "line-end", CLUTTER_KEY_KP_End, 0, G_CALLBACK (clutter_text_real_line_end)); clutter_binding_pool_install_action (binding_pool, "select-all", CLUTTER_KEY_a, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_select_all), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-next", CLUTTER_KEY_Delete, 0, G_CALLBACK (clutter_text_real_del_next), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-next", CLUTTER_KEY_Delete, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_del_word_next), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-next", CLUTTER_KEY_KP_Delete, 0, G_CALLBACK (clutter_text_real_del_next), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-next", CLUTTER_KEY_KP_Delete, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_del_word_next), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-prev", CLUTTER_KEY_BackSpace, 0, G_CALLBACK (clutter_text_real_del_prev), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "delete-prev", CLUTTER_KEY_BackSpace, CLUTTER_CONTROL_MASK, G_CALLBACK (clutter_text_real_del_word_prev), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "activate", CLUTTER_KEY_Return, 0, G_CALLBACK (clutter_text_real_activate), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "activate", CLUTTER_KEY_KP_Enter, 0, G_CALLBACK (clutter_text_real_activate), NULL, NULL); clutter_binding_pool_install_action (binding_pool, "activate", CLUTTER_KEY_ISO_Enter, 0, G_CALLBACK (clutter_text_real_activate), NULL, NULL); } static void clutter_text_init (ClutterText *self) { ClutterSettings *settings; ClutterTextPrivate *priv; gchar *font_name; int i; self->priv = priv = CLUTTER_TEXT_GET_PRIVATE (self); priv->alignment = PANGO_ALIGN_LEFT; priv->wrap = FALSE; priv->wrap_mode = PANGO_WRAP_WORD; priv->ellipsize = PANGO_ELLIPSIZE_NONE; priv->use_underline = FALSE; priv->use_markup = FALSE; priv->justify = FALSE; for (i = 0; i < N_CACHED_LAYOUTS; i++) priv->cached_layouts[i].layout = NULL; /* default to "" so that clutter_text_get_text() will * return a valid string and we can safely call strlen() * or strcmp() on it */ priv->text = g_strdup (""); priv->text_color = default_text_color; priv->cursor_color = default_cursor_color; priv->selection_color = default_selection_color; /* get the default font name from the context; we don't use * set_font_description() here because we are initializing * the Text and we don't need notifications and sanity checks */ settings = clutter_settings_get_default (); g_object_get (settings, "font-name", &font_name, NULL); priv->font_name = font_name; /* font_name is allocated */ priv->font_desc = pango_font_description_from_string (font_name); priv->is_default_font = TRUE; priv->position = -1; priv->selection_bound = -1; priv->x_pos = -1; priv->cursor_visible = TRUE; priv->editable = FALSE; priv->selectable = TRUE; priv->selection_color_set = FALSE; priv->cursor_color_set = FALSE; priv->preedit_set = FALSE; priv->password_char = 0; priv->max_length = 0; priv->cursor_size = DEFAULT_CURSOR_SIZE; memset (&priv->cursor_pos, 0, sizeof (ClutterGeometry)); priv->font_changed_id = g_signal_connect_swapped (clutter_get_default_backend (), "settings-changed", G_CALLBACK (clutter_text_font_changed_cb), self); priv->direction_changed_id = g_signal_connect (self, "notify::text-direction", G_CALLBACK (clutter_text_direction_changed_cb), NULL); } /** * clutter_text_new: * * Creates a new #ClutterText actor. This actor can be used to * display and edit text. * * Return value: the newly created #ClutterText actor * * Since: 1.0 */ ClutterActor * clutter_text_new (void) { return g_object_new (CLUTTER_TYPE_TEXT, NULL); } /** * clutter_text_new_full: * @font_name: a string with a font description * @text: the contents of the actor * @color: the color to be used to render @text * * Creates a new #ClutterText actor, using @font_name as the font * description; @text will be used to set the contents of the actor; * and @color will be used as the color to render @text. * * This function is equivalent to calling clutter_text_new(), * clutter_text_set_font_name(), clutter_text_set_text() and * clutter_text_set_color(). * * Return value: the newly created #ClutterText actor * * Since: 1.0 */ ClutterActor * clutter_text_new_full (const gchar *font_name, const gchar *text, const ClutterColor *color) { return g_object_new (CLUTTER_TYPE_TEXT, "font-name", font_name, "text", text, "color", color, NULL); } /** * clutter_text_new_with_text: * @font_name: a string with a font description * @text: the contents of the actor * * Creates a new #ClutterText actor, using @font_name as the font * description; @text will be used to set the contents of the actor. * * This function is equivalent to calling clutter_text_new(), * clutter_text_set_font_name(), and clutter_text_set_text(). * * Return value: the newly created #ClutterText actor * * Since: 1.0 */ ClutterActor * clutter_text_new_with_text (const gchar *font_name, const gchar *text) { return g_object_new (CLUTTER_TYPE_TEXT, "font-name", font_name, "text", text, NULL); } /** * clutter_text_set_editable: * @self: a #ClutterText * @editable: whether the #ClutterText should be editable * * Sets whether the #ClutterText actor should be editable. * * An editable #ClutterText with key focus set using * clutter_actor_grab_key_focus() or clutter_stage_take_key_focus() * will receive key events and will update its contents accordingly. * * Since: 1.0 */ void clutter_text_set_editable (ClutterText *self, gboolean editable) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->editable != editable) { priv->editable = editable; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_EDITABLE]); } } /** * clutter_text_get_editable: * @self: a #ClutterText * * Retrieves whether a #ClutterText is editable or not. * * Return value: %TRUE if the actor is editable * * Since: 1.0 */ gboolean clutter_text_get_editable (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); return self->priv->editable; } /** * clutter_text_set_selectable: * @self: a #ClutterText * @selectable: whether the #ClutterText actor should be selectable * * Sets whether a #ClutterText actor should be selectable. * * A selectable #ClutterText will allow selecting its contents using * the pointer or the keyboard. * * Since: 1.0 */ void clutter_text_set_selectable (ClutterText *self, gboolean selectable) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->selectable != selectable) { priv->selectable = selectable; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTABLE]); } } /** * clutter_text_get_selectable: * @self: a #ClutterText * * Retrieves whether a #ClutterText is selectable or not. * * Return value: %TRUE if the actor is selectable * * Since: 1.0 */ gboolean clutter_text_get_selectable (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), TRUE); return self->priv->selectable; } /** * clutter_text_set_activatable: * @self: a #ClutterText * @activatable: whether the #ClutterText actor should be activatable * * Sets whether a #ClutterText actor should be activatable. * * An activatable #ClutterText actor will emit the #ClutterText::activate * signal whenever the 'Enter' (or 'Return') key is pressed; if it is not * activatable, a new line will be appended to the current content. * * An activatable #ClutterText must also be set as editable using * clutter_text_set_editable(). * * Since: 1.0 */ void clutter_text_set_activatable (ClutterText *self, gboolean activatable) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->activatable != activatable) { priv->activatable = activatable; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ACTIVATABLE]); } } /** * clutter_text_get_activatable: * @self: a #ClutterText * * Retrieves whether a #ClutterText is activatable or not. * * Return value: %TRUE if the actor is activatable * * Since: 1.0 */ gboolean clutter_text_get_activatable (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), TRUE); return self->priv->activatable; } /** * clutter_text_activate: * @self: a #ClutterText * * Emits the #ClutterText::activate signal, if @self has been set * as activatable using clutter_text_set_activatable(). * * This function can be used to emit the ::activate signal inside * a #ClutterActor::captured-event or #ClutterActor::key-press-event * signal handlers before the default signal handler for the * #ClutterText is invoked. * * Return value: %TRUE if the ::activate signal has been emitted, * and %FALSE otherwise * * Since: 1.0 */ gboolean clutter_text_activate (ClutterText *self) { ClutterTextPrivate *priv; g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); priv = self->priv; if (priv->activatable) { g_signal_emit (self, text_signals[ACTIVATE], 0); return TRUE; } return FALSE; } /** * clutter_text_set_cursor_visible: * @self: a #ClutterText * @cursor_visible: whether the cursor should be visible * * Sets whether the cursor of a #ClutterText actor should be * visible or not. * * The color of the cursor will be the same as the text color * unless clutter_text_set_cursor_color() has been called. * * The size of the cursor can be set using clutter_text_set_cursor_size(). * * The position of the cursor can be changed programmatically using * clutter_text_set_cursor_position(). * * Since: 1.0 */ void clutter_text_set_cursor_visible (ClutterText *self, gboolean cursor_visible) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->cursor_visible != cursor_visible) { priv->cursor_visible = cursor_visible; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CURSOR_VISIBLE]); } } /** * clutter_text_get_cursor_visible: * @self: a #ClutterText * * Retrieves whether the cursor of a #ClutterText actor is visible. * * Return value: %TRUE if the cursor is visible * * Since: 1.0 */ gboolean clutter_text_get_cursor_visible (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), TRUE); return self->priv->cursor_visible; } /** * clutter_text_set_cursor_color: * @self: a #ClutterText * @color: the color of the cursor, or %NULL to unset it * * Sets the color of the cursor of a #ClutterText actor. * * If @color is %NULL, the cursor color will be the same as the * text color. * * Since: 1.0 */ void clutter_text_set_cursor_color (ClutterText *self, const ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (color) { priv->cursor_color = *color; priv->cursor_color_set = TRUE; } else priv->cursor_color_set = FALSE; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CURSOR_COLOR]); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CURSOR_COLOR_SET]); } /** * clutter_text_get_cursor_color: * @self: a #ClutterText * @color: return location for a #ClutterColor * * Retrieves the color of the cursor of a #ClutterText actor. * * Since: 1.0 */ void clutter_text_get_cursor_color (ClutterText *self, ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (color != NULL); priv = self->priv; *color = priv->cursor_color; } /** * clutter_text_set_selection: * @self: a #ClutterText * @start_pos: start of the selection, in characters * @end_pos: end of the selection, in characters * * Selects the region of text between @start_pos and @end_pos. * * This function changes the position of the cursor to match * @start_pos and the selection bound to match @end_pos. * * Since: 1.0 */ void clutter_text_set_selection (ClutterText *self, gssize start_pos, gssize end_pos) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (end_pos < 0) end_pos = priv->n_chars; start_pos = MIN (priv->n_chars, start_pos); end_pos = MIN (priv->n_chars, end_pos); clutter_text_set_positions (self, start_pos, end_pos); } /** * clutter_text_get_selection: * @self: a #ClutterText * * Retrieves the currently selected text. * * Return value: a newly allocated string containing the currently * selected text, or %NULL. Use g_free() to free the returned * string. * * Since: 1.0 */ gchar * clutter_text_get_selection (ClutterText *self) { ClutterTextPrivate *priv; gchar *str; gint len; gint start_index, end_index; gint start_offset, end_offset; g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); priv = self->priv; start_index = priv->position; end_index = priv->selection_bound; if (end_index == start_index) return g_strdup (""); if ((end_index != -1 && end_index < start_index) || start_index == -1) { gint temp = start_index; start_index = end_index; end_index = temp; } start_offset = offset_to_bytes (priv->text, start_index); end_offset = offset_to_bytes (priv->text, end_index); len = end_offset - start_offset; str = g_malloc (len + 1); g_utf8_strncpy (str, priv->text + start_offset, end_index - start_index); return str; } /** * clutter_text_set_selection_bound: * @self: a #ClutterText * @selection_bound: the position of the end of the selection, in characters * * Sets the other end of the selection, starting from the current * cursor position. * * If @selection_bound is -1, the selection unset. * * Since: 1.0 */ void clutter_text_set_selection_bound (ClutterText *self, gint selection_bound) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->selection_bound != selection_bound) { gint len = priv->n_chars; if (selection_bound < 0 || selection_bound >= len) priv->selection_bound = -1; else priv->selection_bound = selection_bound; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTION_BOUND]); } } /** * clutter_text_get_selection_bound: * @self: a #ClutterText * * Retrieves the other end of the selection of a #ClutterText actor, * in characters from the current cursor position. * * Return value: the position of the other end of the selection * * Since: 1.0 */ gint clutter_text_get_selection_bound (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), -1); return self->priv->selection_bound; } /** * clutter_text_set_selection_color: * @self: a #ClutterText * @color: the color of the selection, or %NULL to unset it * * Sets the color of the selection of a #ClutterText actor. * * If @color is %NULL, the selection color will be the same as the * cursor color, or if no cursor color is set either then it will be * the same as the text color. * * Since: 1.0 */ void clutter_text_set_selection_color (ClutterText *self, const ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (color) { priv->selection_color = *color; priv->selection_color_set = TRUE; } else priv->selection_color_set = FALSE; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTION_COLOR]); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SELECTION_COLOR_SET]); } /** * clutter_text_get_selection_color: * @self: a #ClutterText * @color: return location for a #ClutterColor * * Retrieves the color of the selection of a #ClutterText actor. * * Since: 1.0 */ void clutter_text_get_selection_color (ClutterText *self, ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (color != NULL); priv = self->priv; *color = priv->selection_color; } /** * clutter_text_set_font_description: * @self: a #ClutterText * @font_desc: a #PangoFontDescription * * Sets @font_desc as the font description for a #ClutterText * * The #PangoFontDescription is copied by the #ClutterText actor * so you can safely call pango_font_description_free() on it after * calling this function. * * Since: 1.2 */ void clutter_text_set_font_description (ClutterText *self, PangoFontDescription *font_desc) { PangoFontDescription *copy; g_return_if_fail (CLUTTER_IS_TEXT (self)); copy = pango_font_description_copy (font_desc); clutter_text_set_font_description_internal (self, copy); } /** * clutter_text_get_font_description: * @self: a #ClutterText * * Retrieves the #PangoFontDescription used by @self * * Return value: a #PangoFontDescription. The returned value is owned * by the #ClutterText actor and it should not be modified or freed * * Since: 1.2 */ PangoFontDescription * clutter_text_get_font_description (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); return self->priv->font_desc; } /** * clutter_text_get_font_name: * @self: a #ClutterText * * Retrieves the font name as set by clutter_text_set_font_name(). * * Return value: a string containing the font name. The returned * string is owned by the #ClutterText actor and should not be * modified or freed * * Since: 1.0 */ G_CONST_RETURN gchar * clutter_text_get_font_name (ClutterText *text) { g_return_val_if_fail (CLUTTER_IS_TEXT (text), NULL); return text->priv->font_name; } /** * clutter_text_set_font_name: * @self: a #ClutterText * @font_name: (allow-none): a font name, or %NULL to set the default font name * * Sets the font used by a #ClutterText. The @font_name string * must either be %NULL, which means that the font name from the * default #ClutterBackend will be used; or be something that can * be parsed by the pango_font_description_from_string() function, * like: * * |[ * clutter_text_set_font_name (text, "Sans 10pt"); * clutter_text_set_font_name (text, "Serif 16px"); * clutter_text_set_font_name (text, "Helvetica 10"); * ]| * * Since: 1.0 */ void clutter_text_set_font_name (ClutterText *self, const gchar *font_name) { ClutterTextPrivate *priv; PangoFontDescription *desc; gboolean is_default_font; g_return_if_fail (CLUTTER_IS_TEXT (self)); /* get the default font name from the backend */ if (font_name == NULL || font_name[0] == '\0') { ClutterSettings *settings = clutter_settings_get_default (); gchar *default_font_name = NULL; g_object_get (settings, "font-name", &default_font_name, NULL); if (default_font_name != NULL) font_name = default_font_name; else { /* last fallback */ default_font_name = g_strdup ("Sans 12"); } is_default_font = TRUE; } else is_default_font = FALSE; priv = self->priv; if (g_strcmp0 (priv->font_name, font_name) == 0) goto out; desc = pango_font_description_from_string (font_name); if (!desc) { g_warning ("Attempting to create a PangoFontDescription for " "font name '%s', but failed.", font_name); goto out; } /* this will set the font_name field as well */ clutter_text_set_font_description_internal (self, desc); priv->is_default_font = is_default_font; _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_FONT_NAME]); out: if (is_default_font) g_free ((gchar *) font_name); } /** * clutter_text_get_text: * @self: a #ClutterText * * Retrieves a pointer to the current contents of a #ClutterText * actor. * * If you need a copy of the contents for manipulating, either * use g_strdup() on the returned string, or use: * * |[ * copy = clutter_text_get_chars (text, 0, -1); * ]| * * Which will return a newly allocated string. * * Return value: the contents of the actor. The returned string * is owned by the #ClutterText actor and should never be * modified or freed * * Since: 1.0 */ G_CONST_RETURN gchar * clutter_text_get_text (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); return self->priv->text; } static inline void clutter_text_set_use_markup_internal (ClutterText *self, gboolean use_markup) { ClutterTextPrivate *priv = self->priv; if (priv->use_markup != use_markup) { priv->use_markup = use_markup; /* reset the attributes lists so that they can be * re-generated */ if (priv->effective_attrs != NULL) { pango_attr_list_unref (priv->effective_attrs); priv->effective_attrs = NULL; } if (priv->markup_attrs) { pango_attr_list_unref (priv->markup_attrs); priv->markup_attrs = NULL; } _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_USE_MARKUP]); } } /** * clutter_text_set_text: * @self: a #ClutterText * @text: the text to set. Passing %NULL is the same as passing "" (the * empty string) * * Sets the contents of a #ClutterText actor. * * If the #ClutterText:use-markup property was set to %TRUE it * will be reset to %FALSE as a side effect. If you want to * maintain the #ClutterText:use-markup you should use the * clutter_text_set_markup() function instead * * Since: 1.0 */ void clutter_text_set_text (ClutterText *self, const gchar *text) { g_return_if_fail (CLUTTER_IS_TEXT (self)); g_signal_emit (self, text_signals[DELETE_TEXT], 0, 0, -1); if (text) { gint tmp_pos = 0; g_signal_emit (self, text_signals[INSERT_TEXT], 0, text, strlen (text), &tmp_pos); } clutter_text_set_use_markup_internal (self, FALSE); clutter_text_set_text_internal (self, text ? text : ""); } /** * clutter_text_set_markup: * @self: a #ClutterText * @markup: a string containing Pango markup. Passing %NULL is the same * as passing "" (the empty string) * * Sets @markup as the contents of a #ClutterText. * * This is a convenience function for setting a string containing * Pango markup, and it is logically equivalent to: * * |[ * clutter_text_set_text (CLUTTER_TEXT (actor), markup); * clutter_text_set_use_markup (CLUTTER_TEXT (actor), TRUE); * ]| * * Since: 1.0 */ void clutter_text_set_markup (ClutterText *self, const gchar *markup) { g_return_if_fail (CLUTTER_IS_TEXT (self)); clutter_text_set_use_markup_internal (self, TRUE); if (markup != NULL && *markup != '\0') clutter_text_set_markup_internal (self, markup); else clutter_text_set_text_internal (self, ""); } /** * clutter_text_get_layout: * @self: a #ClutterText * * Retrieves the current #PangoLayout used by a #ClutterText actor. * * Return value: (transfer none): a #PangoLayout. The returned object is owned by * the #ClutterText actor and should not be modified or freed * * Since: 1.0 */ PangoLayout * clutter_text_get_layout (ClutterText *self) { gfloat width, height; g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); if (self->priv->editable && self->priv->single_line_mode) return clutter_text_create_layout (self, -1, -1); clutter_actor_get_size (CLUTTER_ACTOR (self), &width, &height); return clutter_text_create_layout (self, width, height); } /** * clutter_text_set_color: * @self: a #ClutterText * @color: a #ClutterColor * * Sets the color of the contents of a #ClutterText actor. * * The overall opacity of the #ClutterText actor will be the * result of the alpha value of @color and the composited * opacity of the actor itself on the scenegraph, as returned * by clutter_actor_get_paint_opacity(). * * Since: 1.0 */ void clutter_text_set_color (ClutterText *self, const ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (color != NULL); priv = self->priv; priv->text_color = *color; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_COLOR]); } /** * clutter_text_get_color: * @self: a #ClutterText * @color: return location for a #ClutterColor * * Retrieves the text color as set by clutter_text_set_color(). * * Since: 1.0 */ void clutter_text_get_color (ClutterText *self, ClutterColor *color) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (color != NULL); priv = self->priv; *color = priv->text_color; } /** * clutter_text_set_ellipsize: * @self: a #ClutterText * @mode: a #PangoEllipsizeMode * * Sets the mode used to ellipsize (add an ellipsis: "...") to the * text if there is not enough space to render the entire contents * of a #ClutterText actor * * Since: 1.0 */ void clutter_text_set_ellipsize (ClutterText *self, PangoEllipsizeMode mode) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END); priv = self->priv; if ((PangoEllipsizeMode) priv->ellipsize != mode) { priv->ellipsize = mode; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ELLIPSIZE]); } } /** * clutter_text_get_ellipsize: * @self: a #ClutterText * * Returns the ellipsizing position of a #ClutterText actor, as * set by clutter_text_set_ellipsize(). * * Return value: #PangoEllipsizeMode * * Since: 1.0 */ PangoEllipsizeMode clutter_text_get_ellipsize (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), PANGO_ELLIPSIZE_NONE); return self->priv->ellipsize; } /** * clutter_text_get_line_wrap: * @self: a #ClutterText * * Retrieves the value set using clutter_text_set_line_wrap(). * * Return value: %TRUE if the #ClutterText actor should wrap * its contents * * Since: 1.0 */ gboolean clutter_text_get_line_wrap (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); return self->priv->wrap; } /** * clutter_text_set_line_wrap: * @self: a #ClutterText * @line_wrap: whether the contents should wrap * * Sets whether the contents of a #ClutterText actor should wrap, * if they don't fit the size assigned to the actor. * * Since: 1.0 */ void clutter_text_set_line_wrap (ClutterText *self, gboolean line_wrap) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->wrap != line_wrap) { priv->wrap = line_wrap; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_LINE_WRAP]); } } /** * clutter_text_set_line_wrap_mode: * @self: a #ClutterText * @wrap_mode: the line wrapping mode * * If line wrapping is enabled (see clutter_text_set_line_wrap()) this * function controls how the line wrapping is performed. The default is * %PANGO_WRAP_WORD which means wrap on word boundaries. * * Since: 1.0 */ void clutter_text_set_line_wrap_mode (ClutterText *self, PangoWrapMode wrap_mode) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->wrap_mode != wrap_mode) { priv->wrap_mode = wrap_mode; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_LINE_WRAP_MODE]); } } /** * clutter_text_get_line_wrap_mode: * @self: a #ClutterText * * Retrieves the line wrap mode used by the #ClutterText actor. * * See clutter_text_set_line_wrap_mode (). * * Return value: the wrap mode used by the #ClutterText * * Since: 1.0 */ PangoWrapMode clutter_text_get_line_wrap_mode (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), PANGO_WRAP_WORD); return self->priv->wrap_mode; } /** * clutter_text_set_attributes: * @self: a #ClutterText * @attrs: a #PangoAttrList or %NULL to unset the attributes * * Sets the attributes list that are going to be applied to the * #ClutterText contents. * * The #ClutterText actor will take a reference on the #PangoAttrList * passed to this function. * * Since: 1.0 */ void clutter_text_set_attributes (ClutterText *self, PangoAttrList *attrs) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (attrs) pango_attr_list_ref (attrs); if (priv->attrs) pango_attr_list_unref (priv->attrs); priv->attrs = attrs; /* Clear the effective attributes so they will be regenerated when a layout is created */ if (priv->effective_attrs) { pango_attr_list_unref (priv->effective_attrs); priv->effective_attrs = NULL; } clutter_text_dirty_cache (self); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ATTRIBUTES]); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); } /** * clutter_text_get_attributes: * @self: a #ClutterText * * Gets the attribute list that was set on the #ClutterText actor * clutter_text_set_attributes(), if any. * * Return value: (transfer none): the attribute list, or %NULL if none was set. The * returned value is owned by the #ClutterText and should not be unreferenced. * * Since: 1.0 */ PangoAttrList * clutter_text_get_attributes (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); return self->priv->attrs; } /** * clutter_text_set_line_alignment: * @self: a #ClutterText * @alignment: A #PangoAlignment * * Sets the way that the lines of a wrapped label are aligned with * respect to each other. This does not affect the overall alignment * of the label within its allocated or specified width. * * To align a #ClutterText actor you should add it to a container * that supports alignment, or use the anchor point. * * Since: 1.0 */ void clutter_text_set_line_alignment (ClutterText *self, PangoAlignment alignment) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->alignment != alignment) { priv->alignment = alignment; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_LINE_ALIGNMENT]); } } /** * clutter_text_get_line_alignment: * @self: a #ClutterText * * Retrieves the alignment of a #ClutterText, as set by * clutter_text_set_line_alignment(). * * Return value: a #PangoAlignment * * Since: 1.0 */ PangoAlignment clutter_text_get_line_alignment (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), PANGO_ALIGN_LEFT); return self->priv->alignment; } /** * clutter_text_set_use_markup: * @self: a #ClutterText * @setting: %TRUE if the text should be parsed for markup. * * Sets whether the contents of the #ClutterText actor contains markup * in Pango's text markup language. * * Setting #ClutterText:use-markup on an editable #ClutterText will * not have any effect except hiding the markup. * * See also #ClutterText:use-markup. * * Since: 1.0 */ void clutter_text_set_use_markup (ClutterText *self, gboolean setting) { ClutterTextPrivate *priv; gchar *str; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; str = g_strdup (priv->text); clutter_text_set_use_markup_internal (self, setting); if (setting) clutter_text_set_markup_internal (self, str); else clutter_text_set_text_internal (self, str); g_free (str); clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); } /** * clutter_text_get_use_markup: * @self: a #ClutterText * * Retrieves whether the contents of the #ClutterText actor should be * parsed for the Pango text markup. * * Return value: %TRUE if the contents will be parsed for markup * * Since: 1.0 */ gboolean clutter_text_get_use_markup (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); return self->priv->use_markup; } /** * clutter_text_set_justify: * @self: a #ClutterText * @justify: whether the text should be justified * * Sets whether the text of the #ClutterText actor should be justified * on both margins. This setting is ignored if Clutter is compiled * against Pango < 1.18. * * Since: 1.0 */ void clutter_text_set_justify (ClutterText *self, gboolean justify) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->justify != justify) { priv->justify = justify; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_JUSTIFY]); } } /** * clutter_text_get_justify: * @self: a #ClutterText * * Retrieves whether the #ClutterText actor should justify its contents * on both margins. * * Return value: %TRUE if the text should be justified * * Since: 0.6 */ gboolean clutter_text_get_justify (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); return self->priv->justify; } /** * clutter_text_get_cursor_position: * @self: a #ClutterText * * Retrieves the cursor position. * * Return value: the cursor position, in characters * * Since: 1.0 */ gint clutter_text_get_cursor_position (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), -1); return self->priv->position; } /** * clutter_text_set_cursor_position: * @self: a #ClutterText * @position: the new cursor position, in characters * * Sets the cursor of a #ClutterText actor at @position. * * The position is expressed in characters, not in bytes. * * Since: 1.0 */ void clutter_text_set_cursor_position (ClutterText *self, gint position) { ClutterTextPrivate *priv; gint len; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->position == position) return; len = priv->n_chars; if (position < 0 || position >= len) priv->position = -1; else priv->position = position; /* Forget the target x position so that it will be recalculated next time the cursor is moved up or down */ priv->x_pos = -1; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_POSITION]); } /** * clutter_text_set_cursor_size: * @self: a #ClutterText * @size: the size of the cursor, in pixels, or -1 to use the * default value * * Sets the size of the cursor of a #ClutterText. The cursor * will only be visible if the #ClutterText:cursor-visible property * is set to %TRUE. * * Since: 1.0 */ void clutter_text_set_cursor_size (ClutterText *self, gint size) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->cursor_size != size) { if (size < 0) size = DEFAULT_CURSOR_SIZE; priv->cursor_size = size; clutter_actor_queue_redraw (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CURSOR_SIZE]); } } /** * clutter_text_get_cursor_size: * @self: a #ClutterText * * Retrieves the size of the cursor of a #ClutterText actor. * * Return value: the size of the cursor, in pixels * * Since: 1.0 */ guint clutter_text_get_cursor_size (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), DEFAULT_CURSOR_SIZE); return self->priv->cursor_size; } /** * clutter_text_set_password_char: * @self: a #ClutterText * @wc: a Unicode character, or 0 to unset the password character * * Sets the character to use in place of the actual text in a * password text actor. * * If @wc is 0 the text will be displayed as it is entered in the * #ClutterText actor. * * Since: 1.0 */ void clutter_text_set_password_char (ClutterText *self, gunichar wc) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->password_char != wc) { priv->password_char = wc; clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PASSWORD_CHAR]); } } /** * clutter_text_get_password_char: * @self: a #ClutterText * * Retrieves the character to use in place of the actual text * as set by clutter_text_set_password_char(). * * Return value: a Unicode character or 0 if the password * character is not set * * Since: 1.0 */ gunichar clutter_text_get_password_char (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), 0); return self->priv->password_char; } /** * clutter_text_set_max_length: * @self: a #ClutterText * @max: the maximum number of characters allowed in the text actor; 0 * to disable or -1 to set the length of the current string * * Sets the maximum allowed length of the contents of the actor. If the * current contents are longer than the given length, then they will be * truncated to fit. * * Since: 1.0 */ void clutter_text_set_max_length (ClutterText *self, gint max) { ClutterTextPrivate *priv; gchar *new = NULL; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->max_length != max) { if (max < 0) max = priv->n_chars; priv->max_length = max; new = g_strdup (priv->text); clutter_text_set_text (self, new); g_free (new); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_MAX_LENGTH]); } } /** * clutter_text_get_max_length: * @self: a #ClutterText * * Gets the maximum length of text that can be set into a text actor. * * See clutter_text_set_max_length(). * * Return value: the maximum number of characters. * * Since: 1.0 */ gint clutter_text_get_max_length (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), 0); return self->priv->max_length; } /** * clutter_text_insert_unichar: * @self: a #ClutterText * @wc: a Unicode character * * Inserts @wc at the current cursor position of a * #ClutterText actor. * * Since: 1.0 */ void clutter_text_insert_unichar (ClutterText *self, gunichar wc) { ClutterTextPrivate *priv; GString *new = NULL; glong pos; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (g_unichar_validate (wc)); if (wc == 0) return; priv = self->priv; new = g_string_new (priv->text); if (priv->text) pos = offset_to_bytes (priv->text, priv->position); else pos = 0; new = g_string_insert_unichar (new, pos, wc); g_signal_emit (self, text_signals[INSERT_TEXT], 0, &wc, 1, &pos); clutter_text_set_text_internal (self, new->str); if (priv->position >= 0) clutter_text_set_positions (self, priv->position + 1, priv->position + 1); g_string_free (new, TRUE); } /** * clutter_text_insert_text: * @self: a #ClutterText * @text: the text to be inserted * @position: the position of the insertion, or -1 * * Inserts @text into a #ClutterActor at the given position. * * If @position is a negative number, the text will be appended * at the end of the current contents of the #ClutterText. * * The position is expressed in characters, not in bytes. * * Since: 1.0 */ void clutter_text_insert_text (ClutterText *self, const gchar *text, gssize position) { ClutterTextPrivate *priv; GString *new = NULL; gint pos_bytes; g_return_if_fail (CLUTTER_IS_TEXT (self)); g_return_if_fail (text != NULL); priv = self->priv; pos_bytes = offset_to_bytes (priv->text, position); new = g_string_new (priv->text); new = g_string_insert (new, pos_bytes, text); g_signal_emit (self, text_signals[INSERT_TEXT], 0, text, g_utf8_strlen (text, -1), &position); clutter_text_set_text_internal (self, new->str); if (position >= 0 && priv->position >= position) { gint new_pos = priv->position + g_utf8_strlen (text, -1); clutter_text_set_positions (self, new_pos, new_pos); } g_string_free (new, TRUE); } /** * clutter_text_delete_text: * @self: a #ClutterText * @start_pos: starting position * @end_pos: ending position * * Deletes the text inside a #ClutterText actor between @start_pos * and @end_pos. * * The starting and ending positions are expressed in characters, * not in bytes. * * Since: 1.0 */ void clutter_text_delete_text (ClutterText *self, gssize start_pos, gssize end_pos) { ClutterTextPrivate *priv; GString *new = NULL; gint start_bytes; gint end_bytes; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (!priv->text) return; if (start_pos == 0) start_bytes = 0; else start_bytes = offset_to_bytes (priv->text, start_pos); if (end_pos == -1) end_bytes = offset_to_bytes (priv->text, priv->n_chars); else end_bytes = offset_to_bytes (priv->text, end_pos); new = g_string_new (priv->text); new = g_string_erase (new, start_bytes, end_bytes - start_bytes); g_signal_emit (self, text_signals[DELETE_TEXT], 0, start_pos, end_pos); clutter_text_set_text_internal (self, new->str); g_string_free (new, TRUE); } /** * clutter_text_delete_chars: * @self: a #ClutterText * @n_chars: the number of characters to delete * * Deletes @n_chars inside a #ClutterText actor, starting from the * current cursor position. * * Since: 1.0 */ void clutter_text_delete_chars (ClutterText *self, guint n_chars) { ClutterTextPrivate *priv; GString *new = NULL; gint pos; gint num_pos; gint start_pos; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (!priv->text) return; new = g_string_new (priv->text); if (priv->position == -1) { num_pos = offset_to_bytes (priv->text, priv->n_chars - n_chars); new = g_string_erase (new, num_pos, -1); } else { pos = offset_to_bytes (priv->text, priv->position - n_chars); num_pos = offset_to_bytes (priv->text, priv->position); new = g_string_erase (new, pos, num_pos - pos); } start_pos = clutter_text_get_cursor_position (self); g_signal_emit (self, text_signals[DELETE_TEXT], 0, start_pos, start_pos + n_chars); clutter_text_set_text_internal (self, new->str); if (priv->position > 0) clutter_text_set_cursor_position (self, priv->position - n_chars); g_string_free (new, TRUE); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_TEXT]); } /** * clutter_text_get_chars: * @self: a #ClutterText * @start_pos: start of text, in characters * @end_pos: end of text, in characters * * Retrieves the contents of the #ClutterText actor between * @start_pos and @end_pos, but not including @end_pos. * * The positions are specified in characters, not in bytes. * * Return value: a newly allocated string with the contents of * the text actor between the specified positions. Use g_free() * to free the resources when done * * Since: 1.0 */ gchar * clutter_text_get_chars (ClutterText *self, gssize start_pos, gssize end_pos) { ClutterTextPrivate *priv; gint start_index, end_index; g_return_val_if_fail (CLUTTER_IS_TEXT (self), NULL); priv = self->priv; if (end_pos < 0) end_pos = priv->n_chars; start_pos = MIN (priv->n_chars, start_pos); end_pos = MIN (priv->n_chars, end_pos); start_index = g_utf8_offset_to_pointer (priv->text, start_pos) - priv->text; end_index = g_utf8_offset_to_pointer (priv->text, end_pos) - priv->text; return g_strndup (priv->text + start_index, end_index - start_index); } /** * clutter_text_set_single_line_mode: * @self: a #ClutterText * @single_line: whether to enable single line mode * * Sets whether a #ClutterText actor should be in single line mode * or not. Only editable #ClutterTexts can be in single line * mode. * * A text actor in single line mode will not wrap text and will clip * the visible area to the predefined size. The contents of the * text actor will scroll to display the end of the text if its length * is bigger than the allocated width. * * When setting the single line mode the #ClutterText:activatable * property is also set as a side effect. Instead of entering a new * line character, the text actor will emit the #ClutterText::activate * signal. * * Since: 1.0 */ void clutter_text_set_single_line_mode (ClutterText *self, gboolean single_line) { ClutterTextPrivate *priv; g_return_if_fail (CLUTTER_IS_TEXT (self)); priv = self->priv; if (priv->single_line_mode != single_line) { g_object_freeze_notify (G_OBJECT (self)); priv->single_line_mode = single_line; if (priv->single_line_mode) { priv->activatable = TRUE; _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_ACTIVATABLE]); } clutter_text_dirty_cache (self); clutter_actor_queue_relayout (CLUTTER_ACTOR (self)); _clutter_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SINGLE_LINE_MODE]); g_object_thaw_notify (G_OBJECT (self)); } } /** * clutter_text_get_single_line_mode: * @self: a #ClutterText * * Retrieves whether the #ClutterText actor is in single line mode. * * Return value: %TRUE if the #ClutterText actor is in single line mode * * Since: 1.0 */ gboolean clutter_text_get_single_line_mode (ClutterText *self) { g_return_val_if_fail (CLUTTER_IS_TEXT (self), FALSE); 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); priv->preedit_str = NULL; 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)); }