/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */ /* gtksourcespacedrawer.c * This file is part of GtkSourceView * * Copyright (C) 2008, 2011, 2016 - Paolo Borelli * Copyright (C) 2008, 2010 - Ignacio Casal Quinteiro * Copyright (C) 2010 - Garret Regier * Copyright (C) 2013 - Arpad Borsos * Copyright (C) 2015, 2016 - Sébastien Wilmet * Copyright (C) 2016 - Christian Hergert * * GtkSourceView 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.1 of the License, or (at your option) any later version. * * GtkSourceView 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, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include #endif #include "gtksourcespacedrawer.h" #include "gtksourcespacedrawer-private.h" #include "gtksourcebuffer.h" #include "gtksourceiter.h" #include "gtksourcestylescheme.h" #include "gtksourcetag.h" /** * SECTION:spacedrawer * @Short_description: Represent white space characters with symbols * @Title: GtkSourceSpaceDrawer * @See_also: #GtkSourceView * * #GtkSourceSpaceDrawer provides a way to visualize white spaces, by drawing * symbols. * * Call gtk_source_view_get_space_drawer() to get the #GtkSourceSpaceDrawer * instance of a certain #GtkSourceView. * * By default, no white spaces are drawn because the * #GtkSourceSpaceDrawer:enable-matrix is %FALSE. * * To draw white spaces, gtk_source_space_drawer_set_types_for_locations() can * be called to set the #GtkSourceSpaceDrawer:matrix property (by default all * space types are enabled at all locations). Then call * gtk_source_space_drawer_set_enable_matrix(). * * For a finer-grained method, there is also the GtkSourceTag's * #GtkSourceTag:draw-spaces property. * * # Example * * To draw non-breaking spaces everywhere and draw all types of trailing spaces * except newlines: * |[ * gtk_source_space_drawer_set_types_for_locations (space_drawer, * GTK_SOURCE_SPACE_LOCATION_ALL, * GTK_SOURCE_SPACE_TYPE_NBSP); * * gtk_source_space_drawer_set_types_for_locations (space_drawer, * GTK_SOURCE_SPACE_LOCATION_TRAILING, * GTK_SOURCE_SPACE_TYPE_ALL & * ~GTK_SOURCE_SPACE_TYPE_NEWLINE); * * gtk_source_space_drawer_set_enable_matrix (space_drawer, TRUE); * ]| * * # Use-case: draw unwanted white spaces * * A possible use-case is to draw only unwanted white spaces. Examples: * - Draw all trailing spaces. * - If the indentation and alignment must be done with spaces, draw tabs. * * And non-breaking spaces can always be drawn, everywhere, to distinguish them * from normal spaces. */ /* A drawer specially designed for the International Space Station. It comes by * default with a DVD of Matrix, in case the astronauts are bored. */ /* #define ENABLE_PROFILE */ #undef ENABLE_PROFILE struct _GtkSourceSpaceDrawerPrivate { GtkSourceSpaceTypeFlags *matrix; GdkRGBA *color; guint enable_matrix : 1; }; enum { PROP_0, PROP_ENABLE_MATRIX, PROP_MATRIX, N_PROPERTIES }; static GParamSpec *properties[N_PROPERTIES]; G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceSpaceDrawer, gtk_source_space_drawer, G_TYPE_OBJECT) static gint get_number_of_locations (void) { gint num; gint flags; num = 0; flags = GTK_SOURCE_SPACE_LOCATION_ALL; while (flags != 0) { flags >>= 1; num++; } return num; } static GVariant * get_default_matrix (void) { GVariantBuilder builder; gint num_locations; gint i; g_variant_builder_init (&builder, G_VARIANT_TYPE ("au")); num_locations = get_number_of_locations (); for (i = 0; i < num_locations; i++) { GVariant *space_types; space_types = g_variant_new_uint32 (GTK_SOURCE_SPACE_TYPE_ALL); g_variant_builder_add_value (&builder, space_types); } return g_variant_builder_end (&builder); } static gboolean is_zero_matrix (GtkSourceSpaceDrawer *drawer) { gint num_locations; gint i; num_locations = get_number_of_locations (); for (i = 0; i < num_locations; i++) { if (drawer->priv->matrix[i] != 0) { return FALSE; } } return TRUE; } static void set_zero_matrix (GtkSourceSpaceDrawer *drawer) { gint num_locations; gint i; gboolean changed = FALSE; num_locations = get_number_of_locations (); for (i = 0; i < num_locations; i++) { if (drawer->priv->matrix[i] != 0) { drawer->priv->matrix[i] = 0; changed = TRUE; } } if (changed) { g_object_notify_by_pspec (G_OBJECT (drawer), properties[PROP_MATRIX]); } } /* AND */ static GtkSourceSpaceTypeFlags get_types_at_all_locations (GtkSourceSpaceDrawer *drawer, GtkSourceSpaceLocationFlags locations) { GtkSourceSpaceTypeFlags ret = GTK_SOURCE_SPACE_TYPE_ALL; gint index; gint num_locations; gboolean found; index = 0; num_locations = get_number_of_locations (); found = FALSE; while (locations != 0 && index < num_locations) { if ((locations & 1) == 1) { ret &= drawer->priv->matrix[index]; found = TRUE; } locations >>= 1; index++; } return found ? ret : GTK_SOURCE_SPACE_TYPE_NONE; } /* OR */ static GtkSourceSpaceTypeFlags get_types_at_any_locations (GtkSourceSpaceDrawer *drawer, GtkSourceSpaceLocationFlags locations) { GtkSourceSpaceTypeFlags ret = GTK_SOURCE_SPACE_TYPE_NONE; gint index; gint num_locations; index = 0; num_locations = get_number_of_locations (); while (locations != 0 && index < num_locations) { if ((locations & 1) == 1) { ret |= drawer->priv->matrix[index]; } locations >>= 1; index++; } return ret; } static void gtk_source_space_drawer_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkSourceSpaceDrawer *drawer = GTK_SOURCE_SPACE_DRAWER (object); switch (prop_id) { case PROP_ENABLE_MATRIX: g_value_set_boolean (value, gtk_source_space_drawer_get_enable_matrix (drawer)); break; case PROP_MATRIX: g_value_set_variant (value, gtk_source_space_drawer_get_matrix (drawer)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_source_space_drawer_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkSourceSpaceDrawer *drawer = GTK_SOURCE_SPACE_DRAWER (object); switch (prop_id) { case PROP_ENABLE_MATRIX: gtk_source_space_drawer_set_enable_matrix (drawer, g_value_get_boolean (value)); break; case PROP_MATRIX: gtk_source_space_drawer_set_matrix (drawer, g_value_get_variant (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_source_space_drawer_finalize (GObject *object) { GtkSourceSpaceDrawer *drawer = GTK_SOURCE_SPACE_DRAWER (object); g_free (drawer->priv->matrix); if (drawer->priv->color != NULL) { gdk_rgba_free (drawer->priv->color); } G_OBJECT_CLASS (gtk_source_space_drawer_parent_class)->finalize (object); } static void gtk_source_space_drawer_class_init (GtkSourceSpaceDrawerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = gtk_source_space_drawer_get_property; object_class->set_property = gtk_source_space_drawer_set_property; object_class->finalize = gtk_source_space_drawer_finalize; /** * GtkSourceSpaceDrawer:enable-matrix: * * Whether the #GtkSourceSpaceDrawer:matrix property is enabled. * * Since: 3.24 */ properties[PROP_ENABLE_MATRIX] = g_param_spec_boolean ("enable-matrix", "Enable Matrix", "", FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); /** * GtkSourceSpaceDrawer:matrix: * * The :matrix property is a #GVariant property to specify where and * what kind of white spaces to draw. * * The #GVariant is of type `"au"`, an array of unsigned integers. Each * integer is a combination of #GtkSourceSpaceTypeFlags. There is one * integer for each #GtkSourceSpaceLocationFlags, in the same order as * they are defined in the enum (%GTK_SOURCE_SPACE_LOCATION_NONE and * %GTK_SOURCE_SPACE_LOCATION_ALL are not taken into account). * * If the array is shorter than the number of locations, then the value * for the missing locations will be %GTK_SOURCE_SPACE_TYPE_NONE. * * By default, %GTK_SOURCE_SPACE_TYPE_ALL is set for all locations. * * Since: 3.24 */ properties[PROP_MATRIX] = g_param_spec_variant ("matrix", "Matrix", "", G_VARIANT_TYPE ("au"), get_default_matrix (), G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, N_PROPERTIES, properties); } static void gtk_source_space_drawer_init (GtkSourceSpaceDrawer *drawer) { drawer->priv = gtk_source_space_drawer_get_instance_private (drawer); drawer->priv->matrix = g_new0 (GtkSourceSpaceTypeFlags, get_number_of_locations ()); } /** * gtk_source_space_drawer_new: * * Creates a new #GtkSourceSpaceDrawer object. Useful for storing space drawing * settings independently of a #GtkSourceView. * * Returns: a new #GtkSourceSpaceDrawer. * Since: 3.24 */ GtkSourceSpaceDrawer * gtk_source_space_drawer_new (void) { return g_object_new (GTK_SOURCE_TYPE_SPACE_DRAWER, NULL); } static GtkSourceSpaceLocationFlags get_nonzero_locations_for_draw_spaces_flags (GtkSourceSpaceDrawer *drawer) { GtkSourceSpaceLocationFlags locations = GTK_SOURCE_SPACE_LOCATION_NONE; GtkSourceSpaceTypeFlags types; types = gtk_source_space_drawer_get_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_LEADING); if (types != GTK_SOURCE_SPACE_TYPE_NONE) { locations |= GTK_SOURCE_SPACE_LOCATION_LEADING; } types = gtk_source_space_drawer_get_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT); if (types != GTK_SOURCE_SPACE_TYPE_NONE) { locations |= GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT; } types = gtk_source_space_drawer_get_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_TRAILING); if (types != GTK_SOURCE_SPACE_TYPE_NONE) { locations |= GTK_SOURCE_SPACE_LOCATION_TRAILING; } return locations; } GtkSourceDrawSpacesFlags _gtk_source_space_drawer_get_flags (GtkSourceSpaceDrawer *drawer) { GtkSourceSpaceLocationFlags locations; GtkSourceSpaceTypeFlags common_types; GtkSourceDrawSpacesFlags flags = 0; g_return_val_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer), 0); if (!drawer->priv->enable_matrix) { return 0; } locations = get_nonzero_locations_for_draw_spaces_flags (drawer); common_types = gtk_source_space_drawer_get_types_for_locations (drawer, locations); if (locations & GTK_SOURCE_SPACE_LOCATION_LEADING) { flags |= GTK_SOURCE_DRAW_SPACES_LEADING; } if (locations & GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT) { flags |= GTK_SOURCE_DRAW_SPACES_TEXT; } if (locations & GTK_SOURCE_SPACE_LOCATION_TRAILING) { flags |= GTK_SOURCE_DRAW_SPACES_TRAILING; } if (common_types & GTK_SOURCE_SPACE_TYPE_SPACE) { flags |= GTK_SOURCE_DRAW_SPACES_SPACE; } if (common_types & GTK_SOURCE_SPACE_TYPE_TAB) { flags |= GTK_SOURCE_DRAW_SPACES_TAB; } if (common_types & GTK_SOURCE_SPACE_TYPE_NEWLINE) { flags |= GTK_SOURCE_DRAW_SPACES_NEWLINE; } if (common_types & GTK_SOURCE_SPACE_TYPE_NBSP) { flags |= GTK_SOURCE_DRAW_SPACES_NBSP; } return flags; } static GtkSourceSpaceLocationFlags get_locations_from_draw_spaces_flags (GtkSourceDrawSpacesFlags flags) { GtkSourceSpaceLocationFlags locations = GTK_SOURCE_SPACE_LOCATION_NONE; if (flags & GTK_SOURCE_DRAW_SPACES_LEADING) { locations |= GTK_SOURCE_SPACE_LOCATION_LEADING; } if (flags & GTK_SOURCE_DRAW_SPACES_TEXT) { locations |= GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT; } if (flags & GTK_SOURCE_DRAW_SPACES_TRAILING) { locations |= GTK_SOURCE_SPACE_LOCATION_TRAILING; } if (locations == GTK_SOURCE_SPACE_LOCATION_NONE) { locations = (GTK_SOURCE_SPACE_LOCATION_LEADING | GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT | GTK_SOURCE_SPACE_LOCATION_TRAILING); } return locations; } static GtkSourceSpaceTypeFlags get_space_types_from_draw_spaces_flags (GtkSourceDrawSpacesFlags flags) { GtkSourceSpaceTypeFlags types = GTK_SOURCE_SPACE_TYPE_NONE; if (flags & GTK_SOURCE_DRAW_SPACES_SPACE) { types |= GTK_SOURCE_SPACE_TYPE_SPACE; } if (flags & GTK_SOURCE_DRAW_SPACES_TAB) { types |= GTK_SOURCE_SPACE_TYPE_TAB; } if (flags & GTK_SOURCE_DRAW_SPACES_NEWLINE) { types |= GTK_SOURCE_SPACE_TYPE_NEWLINE; } if (flags & GTK_SOURCE_DRAW_SPACES_NBSP) { types |= GTK_SOURCE_SPACE_TYPE_NBSP; } return types; } void _gtk_source_space_drawer_set_flags (GtkSourceSpaceDrawer *drawer, GtkSourceDrawSpacesFlags flags) { GtkSourceSpaceLocationFlags locations; GtkSourceSpaceTypeFlags types; g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); gtk_source_space_drawer_set_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_ALL, GTK_SOURCE_SPACE_TYPE_NONE); locations = get_locations_from_draw_spaces_flags (flags); types = get_space_types_from_draw_spaces_flags (flags); gtk_source_space_drawer_set_types_for_locations (drawer, locations, types); gtk_source_space_drawer_set_enable_matrix (drawer, TRUE); } /** * gtk_source_space_drawer_get_types_for_locations: * @drawer: a #GtkSourceSpaceDrawer. * @locations: one or several #GtkSourceSpaceLocationFlags. * * If only one location is specified, this function returns what kind of * white spaces are drawn at that location. The value is retrieved from the * #GtkSourceSpaceDrawer:matrix property. * * If several locations are specified, this function returns the logical AND for * those locations. Which means that if a certain kind of white space is present * in the return value, then that kind of white space is drawn at all the * specified @locations. * * Returns: a combination of #GtkSourceSpaceTypeFlags. * Since: 3.24 */ GtkSourceSpaceTypeFlags gtk_source_space_drawer_get_types_for_locations (GtkSourceSpaceDrawer *drawer, GtkSourceSpaceLocationFlags locations) { g_return_val_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer), GTK_SOURCE_SPACE_TYPE_NONE); return get_types_at_all_locations (drawer, locations); } /** * gtk_source_space_drawer_set_types_for_locations: * @drawer: a #GtkSourceSpaceDrawer. * @locations: one or several #GtkSourceSpaceLocationFlags. * @types: a combination of #GtkSourceSpaceTypeFlags. * * Modifies the #GtkSourceSpaceDrawer:matrix property at the specified * @locations. * * Since: 3.24 */ void gtk_source_space_drawer_set_types_for_locations (GtkSourceSpaceDrawer *drawer, GtkSourceSpaceLocationFlags locations, GtkSourceSpaceTypeFlags types) { gint index; gint num_locations; gboolean changed = FALSE; g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); index = 0; num_locations = get_number_of_locations (); while (locations != 0 && index < num_locations) { if ((locations & 1) == 1 && drawer->priv->matrix[index] != types) { drawer->priv->matrix[index] = types; changed = TRUE; } locations >>= 1; index++; } if (changed) { g_object_notify_by_pspec (G_OBJECT (drawer), properties[PROP_MATRIX]); } } /** * gtk_source_space_drawer_get_matrix: * @drawer: a #GtkSourceSpaceDrawer. * * Gets the value of the #GtkSourceSpaceDrawer:matrix property, as a #GVariant. * An empty array can be returned in case the matrix is a zero matrix. * * The gtk_source_space_drawer_get_types_for_locations() function may be more * convenient to use. * * Returns: the #GtkSourceSpaceDrawer:matrix value as a new floating #GVariant * instance. * Since: 3.24 */ GVariant * gtk_source_space_drawer_get_matrix (GtkSourceSpaceDrawer *drawer) { GVariantBuilder builder; gint num_locations; gint i; g_return_val_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer), NULL); if (is_zero_matrix (drawer)) { return g_variant_new ("au", NULL); } g_variant_builder_init (&builder, G_VARIANT_TYPE ("au")); num_locations = get_number_of_locations (); for (i = 0; i < num_locations; i++) { GVariant *space_types; space_types = g_variant_new_uint32 (drawer->priv->matrix[i]); g_variant_builder_add_value (&builder, space_types); } return g_variant_builder_end (&builder); } /** * gtk_source_space_drawer_set_matrix: * @drawer: a #GtkSourceSpaceDrawer. * @matrix: (transfer floating) (nullable): the new matrix value, or %NULL. * * Sets a new value to the #GtkSourceSpaceDrawer:matrix property, as a * #GVariant. If @matrix is %NULL, then an empty array is set. * * If @matrix is floating, it is consumed. * * The gtk_source_space_drawer_set_types_for_locations() function may be more * convenient to use. * * Since: 3.24 */ void gtk_source_space_drawer_set_matrix (GtkSourceSpaceDrawer *drawer, GVariant *matrix) { gint num_locations; gint index; GVariantIter iter; gboolean changed = FALSE; g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); if (matrix == NULL) { set_zero_matrix (drawer); return; } g_return_if_fail (g_variant_is_of_type (matrix, G_VARIANT_TYPE ("au"))); g_variant_iter_init (&iter, matrix); num_locations = get_number_of_locations (); index = 0; while (index < num_locations) { GVariant *child; guint32 space_types; child = g_variant_iter_next_value (&iter); if (child == NULL) { break; } space_types = g_variant_get_uint32 (child); if (drawer->priv->matrix[index] != space_types) { drawer->priv->matrix[index] = space_types; changed = TRUE; } g_variant_unref (child); index++; } while (index < num_locations) { if (drawer->priv->matrix[index] != 0) { drawer->priv->matrix[index] = 0; changed = TRUE; } index++; } if (changed) { g_object_notify_by_pspec (G_OBJECT (drawer), properties[PROP_MATRIX]); } if (g_variant_is_floating (matrix)) { g_variant_ref_sink (matrix); g_variant_unref (matrix); } } /** * gtk_source_space_drawer_get_enable_matrix: * @drawer: a #GtkSourceSpaceDrawer. * * Returns: whether the #GtkSourceSpaceDrawer:matrix property is enabled. * Since: 3.24 */ gboolean gtk_source_space_drawer_get_enable_matrix (GtkSourceSpaceDrawer *drawer) { g_return_val_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer), FALSE); return drawer->priv->enable_matrix; } /** * gtk_source_space_drawer_set_enable_matrix: * @drawer: a #GtkSourceSpaceDrawer. * @enable_matrix: the new value. * * Sets whether the #GtkSourceSpaceDrawer:matrix property is enabled. * * Since: 3.24 */ void gtk_source_space_drawer_set_enable_matrix (GtkSourceSpaceDrawer *drawer, gboolean enable_matrix) { g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); enable_matrix = enable_matrix != FALSE; if (drawer->priv->enable_matrix != enable_matrix) { drawer->priv->enable_matrix = enable_matrix; g_object_notify_by_pspec (G_OBJECT (drawer), properties[PROP_ENABLE_MATRIX]); } } static gboolean matrix_get_mapping (GValue *value, GVariant *variant, gpointer user_data) { g_value_set_variant (value, variant); return TRUE; } static GVariant * matrix_set_mapping (const GValue *value, const GVariantType *expected_type, gpointer user_data) { return g_value_dup_variant (value); } /** * gtk_source_space_drawer_bind_matrix_setting: * @drawer: a #GtkSourceSpaceDrawer object. * @settings: a #GSettings object. * @key: the @settings key to bind. * @flags: flags for the binding. * * Binds the #GtkSourceSpaceDrawer:matrix property to a #GSettings key. * * The #GSettings key must be of the same type as the * #GtkSourceSpaceDrawer:matrix property, that is, `"au"`. * * The g_settings_bind() function cannot be used, because the default GIO * mapping functions don't support #GVariant properties (maybe it will be * supported by a future GIO version, in which case this function can be * deprecated). * * Since: 3.24 */ void gtk_source_space_drawer_bind_matrix_setting (GtkSourceSpaceDrawer *drawer, GSettings *settings, const gchar *key, GSettingsBindFlags flags) { GVariant *value; g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); g_return_if_fail (G_IS_SETTINGS (settings)); g_return_if_fail (key != NULL); g_return_if_fail ((flags & G_SETTINGS_BIND_INVERT_BOOLEAN) == 0); value = g_settings_get_value (settings, key); if (!g_variant_is_of_type (value, G_VARIANT_TYPE ("au"))) { g_warning ("%s(): the GSettings key must be of type \"au\".", G_STRFUNC); g_variant_unref (value); return; } g_variant_unref (value); g_settings_bind_with_mapping (settings, key, drawer, "matrix", flags, matrix_get_mapping, matrix_set_mapping, NULL, NULL); } void _gtk_source_space_drawer_update_color (GtkSourceSpaceDrawer *drawer, GtkSourceView *view) { GtkSourceBuffer *buffer; GtkSourceStyleScheme *style_scheme; g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); g_return_if_fail (GTK_SOURCE_IS_VIEW (view)); if (drawer->priv->color != NULL) { gdk_rgba_free (drawer->priv->color); drawer->priv->color = NULL; } buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view))); style_scheme = gtk_source_buffer_get_style_scheme (buffer); if (style_scheme != NULL) { GtkSourceStyle *style; style = _gtk_source_style_scheme_get_draw_spaces_style (style_scheme); if (style != NULL) { gchar *color_str = NULL; gboolean color_set; GdkRGBA color; g_object_get (style, "foreground", &color_str, "foreground-set", &color_set, NULL); if (color_set && color_str != NULL && gdk_rgba_parse (&color, color_str)) { drawer->priv->color = gdk_rgba_copy (&color); } g_free (color_str); } } if (drawer->priv->color == NULL) { GtkStyleContext *context; GdkRGBA color; context = gtk_widget_get_style_context (GTK_WIDGET (view)); gtk_style_context_save (context); gtk_style_context_set_state (context, GTK_STATE_FLAG_INSENSITIVE); gtk_style_context_get_color (context, gtk_style_context_get_state (context), &color); gtk_style_context_restore (context); drawer->priv->color = gdk_rgba_copy (&color); } } static inline gboolean is_tab (gunichar ch) { return ch == '\t'; } static inline gboolean is_nbsp (gunichar ch) { return g_unichar_break_type (ch) == G_UNICODE_BREAK_NON_BREAKING_GLUE; } static inline gboolean is_narrowed_nbsp (gunichar ch) { return ch == 0x202F; } static inline gboolean is_space (gunichar ch) { return g_unichar_type (ch) == G_UNICODE_SPACE_SEPARATOR; } static gboolean is_newline (const GtkTextIter *iter) { if (gtk_text_iter_is_end (iter)) { GtkSourceBuffer *buffer; buffer = GTK_SOURCE_BUFFER (gtk_text_iter_get_buffer (iter)); return gtk_source_buffer_get_implicit_trailing_newline (buffer); } return gtk_text_iter_ends_line (iter); } static inline gboolean is_whitespace (gunichar ch) { return (g_unichar_isspace (ch) || is_nbsp (ch) || is_space (ch)); } static void draw_space_at_pos (cairo_t *cr, GdkRectangle rect) { gint x, y; gdouble w; x = rect.x; y = rect.y + rect.height * 2 / 3; w = rect.width; cairo_save (cr); cairo_move_to (cr, x + w * 0.5, y); cairo_arc (cr, x + w * 0.5, y, 0.8, 0, 2 * G_PI); cairo_stroke (cr); cairo_restore (cr); } static void draw_tab_at_pos (cairo_t *cr, GdkRectangle rect) { gint x, y; gdouble w, h; x = rect.x; y = rect.y + rect.height * 2 / 3; w = rect.width; h = rect.height; cairo_save (cr); cairo_move_to (cr, x + w * 1 / 8, y); cairo_rel_line_to (cr, w * 6 / 8, 0); cairo_rel_line_to (cr, -h * 1 / 4, -h * 1 / 4); cairo_rel_move_to (cr, +h * 1 / 4, +h * 1 / 4); cairo_rel_line_to (cr, -h * 1 / 4, +h * 1 / 4); cairo_stroke (cr); cairo_restore (cr); } static void draw_newline_at_pos (cairo_t *cr, GdkRectangle rect) { gint x, y; gdouble w, h; x = rect.x; y = rect.y + rect.height / 3; w = 2 * rect.width; h = rect.height; cairo_save (cr); if (gtk_widget_get_default_direction () == GTK_TEXT_DIR_LTR) { cairo_move_to (cr, x + w * 7 / 8, y); cairo_rel_line_to (cr, 0, h * 1 / 3); cairo_rel_line_to (cr, -w * 6 / 8, 0); cairo_rel_line_to (cr, +h * 1 / 4, -h * 1 / 4); cairo_rel_move_to (cr, -h * 1 / 4, +h * 1 / 4); cairo_rel_line_to (cr, +h * 1 / 4, +h * 1 / 4); } else { cairo_move_to (cr, x + w * 1 / 8, y); cairo_rel_line_to (cr, 0, h * 1 / 3); cairo_rel_line_to (cr, w * 6 / 8, 0); cairo_rel_line_to (cr, -h * 1 / 4, -h * 1 / 4); cairo_rel_move_to (cr, +h * 1 / 4, +h * 1 / 4); cairo_rel_line_to (cr, -h * 1 / 4, -h * 1 / 4); } cairo_stroke (cr); cairo_restore (cr); } static void draw_nbsp_at_pos (cairo_t *cr, GdkRectangle rect, gboolean narrowed) { gint x, y; gdouble w, h; x = rect.x; y = rect.y + rect.height / 2; w = rect.width; h = rect.height; cairo_save (cr); cairo_move_to (cr, x + w * 1 / 6, y); cairo_rel_line_to (cr, w * 4 / 6, 0); cairo_rel_line_to (cr, -w * 2 / 6, +h * 1 / 4); cairo_rel_line_to (cr, -w * 2 / 6, -h * 1 / 4); if (narrowed) { cairo_fill (cr); } else { cairo_stroke (cr); } cairo_restore (cr); } static void draw_whitespace_at_iter (GtkTextView *text_view, GtkTextIter *iter, cairo_t *cr) { gunichar ch; GdkRectangle rect; gtk_text_view_get_iter_location (text_view, iter, &rect); /* If the space is at a line-wrap position, or if the character is a * newline, we get 0 width so we fallback to the height. */ if (rect.width == 0) { rect.width = rect.height; } ch = gtk_text_iter_get_char (iter); if (is_tab (ch)) { draw_tab_at_pos (cr, rect); } else if (is_nbsp (ch)) { draw_nbsp_at_pos (cr, rect, is_narrowed_nbsp (ch)); } else if (is_space (ch)) { draw_space_at_pos (cr, rect); } else if (is_newline (iter)) { draw_newline_at_pos (cr, rect); } } static void draw_spaces_tag_foreach (GtkTextTag *tag, gboolean *found) { if (*found) { return; } if (GTK_SOURCE_IS_TAG (tag)) { gboolean draw_spaces_set; g_object_get (tag, "draw-spaces-set", &draw_spaces_set, NULL); if (draw_spaces_set) { *found = TRUE; } } } static gboolean buffer_has_draw_spaces_tag (GtkTextBuffer *buffer) { GtkTextTagTable *table; gboolean found = FALSE; table = gtk_text_buffer_get_tag_table (buffer); gtk_text_tag_table_foreach (table, (GtkTextTagTableForeach) draw_spaces_tag_foreach, &found); return found; } static void space_needs_drawing_according_to_tag (const GtkTextIter *iter, gboolean *has_tag, gboolean *needs_drawing) { GSList *tags; GSList *l; *has_tag = FALSE; *needs_drawing = FALSE; tags = gtk_text_iter_get_tags (iter); tags = g_slist_reverse (tags); for (l = tags; l != NULL; l = l->next) { GtkTextTag *tag = l->data; if (GTK_SOURCE_IS_TAG (tag)) { gboolean draw_spaces_set; gboolean draw_spaces; g_object_get (tag, "draw-spaces-set", &draw_spaces_set, "draw-spaces", &draw_spaces, NULL); if (draw_spaces_set) { *has_tag = TRUE; *needs_drawing = draw_spaces; break; } } } g_slist_free (tags); } static GtkSourceSpaceLocationFlags get_iter_locations (const GtkTextIter *iter, const GtkTextIter *leading_end, const GtkTextIter *trailing_start) { GtkSourceSpaceLocationFlags iter_locations = GTK_SOURCE_SPACE_LOCATION_NONE; if (gtk_text_iter_compare (iter, leading_end) < 0) { iter_locations |= GTK_SOURCE_SPACE_LOCATION_LEADING; } if (gtk_text_iter_compare (trailing_start, iter) <= 0) { iter_locations |= GTK_SOURCE_SPACE_LOCATION_TRAILING; } /* Neither leading nor trailing, must be in text. */ if (iter_locations == GTK_SOURCE_SPACE_LOCATION_NONE) { iter_locations = GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT; } return iter_locations; } static GtkSourceSpaceTypeFlags get_iter_space_type (const GtkTextIter *iter) { gunichar ch; ch = gtk_text_iter_get_char (iter); if (is_tab (ch)) { return GTK_SOURCE_SPACE_TYPE_TAB; } else if (is_nbsp (ch)) { return GTK_SOURCE_SPACE_TYPE_NBSP; } else if (is_space (ch)) { return GTK_SOURCE_SPACE_TYPE_SPACE; } else if (is_newline (iter)) { return GTK_SOURCE_SPACE_TYPE_NEWLINE; } return GTK_SOURCE_SPACE_TYPE_NONE; } static gboolean space_needs_drawing_according_to_matrix (GtkSourceSpaceDrawer *drawer, const GtkTextIter *iter, const GtkTextIter *leading_end, const GtkTextIter *trailing_start) { GtkSourceSpaceLocationFlags iter_locations; GtkSourceSpaceTypeFlags iter_space_type; GtkSourceSpaceTypeFlags allowed_space_types; iter_locations = get_iter_locations (iter, leading_end, trailing_start); iter_space_type = get_iter_space_type (iter); allowed_space_types = get_types_at_any_locations (drawer, iter_locations); return (iter_space_type & allowed_space_types) != 0; } static gboolean space_needs_drawing (GtkSourceSpaceDrawer *drawer, const GtkTextIter *iter, const GtkTextIter *leading_end, const GtkTextIter *trailing_start) { gboolean has_tag; gboolean needs_drawing; /* Check the GtkSourceTag:draw-spaces property (higher priority) */ space_needs_drawing_according_to_tag (iter, &has_tag, &needs_drawing); if (has_tag) { return needs_drawing; } /* Check the matrix */ return (drawer->priv->enable_matrix && space_needs_drawing_according_to_matrix (drawer, iter, leading_end, trailing_start)); } static void get_line_end (GtkTextView *text_view, const GtkTextIter *start_iter, GtkTextIter *line_end, gint max_x, gint max_y, gboolean is_wrapping) { gint min; gint max; GdkRectangle rect; *line_end = *start_iter; if (!gtk_text_iter_ends_line (line_end)) { gtk_text_iter_forward_to_line_end (line_end); } /* Check if line_end is inside the bounding box anyway. */ gtk_text_view_get_iter_location (text_view, line_end, &rect); if (( is_wrapping && rect.y < max_y) || (!is_wrapping && rect.x < max_x)) { return; } min = gtk_text_iter_get_line_offset (start_iter); max = gtk_text_iter_get_line_offset (line_end); while (max >= min) { gint i; i = (min + max) >> 1; gtk_text_iter_set_line_offset (line_end, i); gtk_text_view_get_iter_location (text_view, line_end, &rect); if (( is_wrapping && rect.y < max_y) || (!is_wrapping && rect.x < max_x)) { min = i + 1; } else if (( is_wrapping && rect.y > max_y) || (!is_wrapping && rect.x > max_x)) { max = i - 1; } else { break; } } } void _gtk_source_space_drawer_draw (GtkSourceSpaceDrawer *drawer, GtkSourceView *view, cairo_t *cr) { GtkTextView *text_view; GtkTextBuffer *buffer; GdkRectangle clip; gint min_x; gint min_y; gint max_x; gint max_y; GtkTextIter start; GtkTextIter end; GtkTextIter iter; GtkTextIter leading_end; GtkTextIter trailing_start; GtkTextIter line_end; gboolean is_wrapping; #ifdef ENABLE_PROFILE static GTimer *timer = NULL; if (timer == NULL) { timer = g_timer_new (); } g_timer_start (timer); #endif g_return_if_fail (GTK_SOURCE_IS_SPACE_DRAWER (drawer)); g_return_if_fail (GTK_SOURCE_IS_VIEW (view)); g_return_if_fail (cr != NULL); if (drawer->priv->color == NULL) { g_warning ("GtkSourceSpaceDrawer: color not set."); return; } text_view = GTK_TEXT_VIEW (view); buffer = gtk_text_view_get_buffer (text_view); if ((!drawer->priv->enable_matrix || is_zero_matrix (drawer)) && !buffer_has_draw_spaces_tag (buffer)) { return; } if (!gdk_cairo_get_clip_rectangle (cr, &clip)) { return; } is_wrapping = gtk_text_view_get_wrap_mode (text_view) != GTK_WRAP_NONE; min_x = clip.x; min_y = clip.y; max_x = min_x + clip.width; max_y = min_y + clip.height; gtk_text_view_get_iter_at_location (text_view, &start, min_x, min_y); gtk_text_view_get_iter_at_location (text_view, &end, max_x, max_y); cairo_save (cr); gdk_cairo_set_source_rgba (cr, drawer->priv->color); cairo_set_line_width (cr, 0.8); cairo_translate (cr, -0.5, -0.5); iter = start; _gtk_source_iter_get_leading_spaces_end_boundary (&iter, &leading_end); _gtk_source_iter_get_trailing_spaces_start_boundary (&iter, &trailing_start); get_line_end (text_view, &iter, &line_end, max_x, max_y, is_wrapping); while (TRUE) { gunichar ch = gtk_text_iter_get_char (&iter); gint ly; /* Allow end iter, to draw implicit trailing newline. */ if ((is_whitespace (ch) || gtk_text_iter_is_end (&iter)) && space_needs_drawing (drawer, &iter, &leading_end, &trailing_start)) { draw_whitespace_at_iter (text_view, &iter, cr); } if (gtk_text_iter_is_end (&iter) || gtk_text_iter_compare (&iter, &end) >= 0) { break; } gtk_text_iter_forward_char (&iter); if (gtk_text_iter_compare (&iter, &line_end) > 0) { GtkTextIter next_iter = iter; /* Move to the first iter in the exposed area of the * next line. */ if (!gtk_text_iter_starts_line (&next_iter)) { /* We're trying to move forward on the last * line of the buffer, so we can stop now. */ if (!gtk_text_iter_forward_line (&next_iter)) { break; } } gtk_text_view_get_line_yrange (text_view, &next_iter, &ly, NULL); gtk_text_view_get_iter_at_location (text_view, &next_iter, min_x, ly); /* Move back one char otherwise tabs may not be redrawn. */ if (!gtk_text_iter_starts_line (&next_iter)) { gtk_text_iter_backward_char (&next_iter); } /* Ensure that we have actually advanced, since the * above backward_char() is dangerous and can lead to * infinite loops. */ if (gtk_text_iter_compare (&next_iter, &iter) > 0) { iter = next_iter; } _gtk_source_iter_get_leading_spaces_end_boundary (&iter, &leading_end); _gtk_source_iter_get_trailing_spaces_start_boundary (&iter, &trailing_start); get_line_end (text_view, &iter, &line_end, max_x, max_y, is_wrapping); } }; cairo_restore (cr); #ifdef ENABLE_PROFILE g_timer_stop (timer); /* Same indentation as similar features in gtksourceview.c. */ g_print (" %s time: %g (sec * 1000)\n", G_STRFUNC, g_timer_elapsed (timer, NULL) * 1000); #endif }