gtksourceview3/gtksourceview/gtksourcemap.c

1260 lines
35 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
/* gtksourcemap.c
*
* Copyright (C) 2015 Christian Hergert <christian@hergert.me>
* Copyright (C) 2015 Ignacio Casal Quinteiro <icq@gnome.org>
*
* 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 <config.h>
#endif
#include "gtksourcemap.h"
#include <string.h>
#include "gtksourcebuffer.h"
#include "gtksourcecompletion.h"
#include "gtksourcestyle-private.h"
#include "gtksourcestylescheme.h"
#include "gtksourceview-utils.h"
/**
* SECTION:map
* @Short_description: Widget that displays a map for a specific #GtkSourceView
* @Title: GtkSourceMap
* @See_also: #GtkSourceView
*
* #GtkSourceMap is a widget that maps the content of a #GtkSourceView into
* a smaller view so the user can have a quick overview of the whole document.
*
* This works by connecting a #GtkSourceView to to the #GtkSourceMap using
* the #GtkSourceMap:view property or gtk_source_map_set_view().
*
* #GtkSourceMap is a #GtkSourceView object. This means that you can add a
* #GtkSourceGutterRenderer to a gutter in the same way you would for a
* #GtkSourceView. One example might be a #GtkSourceGutterRenderer that shows
* which lines have changed in the document.
*
* Additionally, it is desirable to match the font of the #GtkSourceMap and
* the #GtkSourceView used for editing. Therefore, #GtkSourceMap:font-desc
* should be used to set the target font. You will need to adjust this to the
* desired font size for the map. A 1pt font generally seems to be an
* appropriate font size. "Monospace 1" is the default. See
* pango_font_description_set_size() for how to alter the size of an existing
* #PangoFontDescription.
*/
/*
* Implementation Notes:
*
* I tried implementing this a few different ways. They are worth noting so
* that we do not repeat the same mistakes.
*
* Originally, I thought using a GtkSourceView to do the rendering was overkill
* and would likely slow things down too much. But it turns out to have been
* the best option so far.
*
* - GtkPixelCache support results in very few GtkTextLayout relayout and
* sizing changes. Since the pixel cache renders +/- half a screen outside
* the visible range, scrolling is also quite smooth as we very rarely
* perform a new gtk_text_layout_draw().
*
* - Performance for this type of widget is dominated by text layout
* rendering. When you scale out this car, you increase the number of
* layouts to be rendered greatly.
*
* - We can pack GtkSourceGutterRenderer into the child view to provide
* additional information. This is quite handy to show information such
* as errors, line changes, and anything else that can help the user
* quickly jump to the target location.
*
* I also tried drawing the contents of the GtkSourceView onto a widget after
* performing a cairo_scale(). This does not help us much because we ignore
* pixel cache when cair_scale is not 1-to-1. This results in layout
* invalidation and worst case render paths.
*
* I also tried rendering the scrubber (overlay box) during the
* GtkTextView::draw_layer() vfunc. The problem with this approach is that
* the scrubber contents are actually pixel cached. So every time the scrubber
* moves we have to invalidate the GtkTextLayout and redraw cached contents.
* Where as drawing in the GtkTextView::draw() vfunc, after the pixel cache
* contents have been drawn results in only a composite blend, not
* invalidating any of the pixel cached text layouts.
*
* In the future, we might consider bundling a custom font for the source map.
* Other overview maps have used a "block" font. However, they typically do
* that because of the glyph rendering cost. Since we have pixel cache, that
* deficiency is largely a non-issue. But Pango recently got support for
* embedding fonts in the application, so it is at least possible to bundle
* our own font as a resource.
*
* By default we use a 1pt Monospace font. However, if the Gtksourcemap:font-desc
* property is set, we will use that instead.
*
* We do not render the background grid as it requires a bunch of
* cpu time for something that will essentially just create a solid
* color background.
*
* The width of the view is determined by the
* #GtkSourceView:right-margin-position. We cache the width of a
* single "X" character and multiple that by the right-margin-position.
* That becomes our size-request width.
*
* We do not allow horizontal scrolling so that the overflow text
* is simply not visible in the minimap.
*
* -- Christian
*/
#define DEFAULT_WIDTH 100
typedef struct
{
/*
* By default, we use "Monospace 1pt". However, most text editing
* applications will have a custom font, so we allow them to set
* that here. Generally speaking, you will want to continue using
* a 1pt font, but if they set GtkSourceMap:font-desc, then they
* should also shrink the font to the desired size.
*
* For example:
* pango_font_description_set_size(font_desc, 1 * PANGO_SCALE);
*
* Would set a 1pt font on whatever PangoFontDescription you have
* in your text editor.
*/
PangoFontDescription *font_desc;
/*
* The easiest way to style the scrubber and the sourceview is
* by using CSS. This is necessary since we can't mess with the
* fonts used in the textbuffer (as one might using GtkTextTag).
*/
GtkCssProvider *css_provider;
/* The GtkSourceView we are providing a map of */
GtkSourceView *view;
/* A weak pointer to the connected buffer */
GtkTextBuffer *buffer;
/* The location of the scrubber in widget coordinate space. */
GdkRectangle scrubber_area;
/* Weak pointer view to child view bindings */
GBinding *buffer_binding;
GBinding *indent_width_binding;
GBinding *tab_width_binding;
/* Our signal handler for buffer changes */
gulong view_notify_buffer_handler;
gulong view_vadj_value_changed_handler;
gulong view_vadj_notify_upper_handler;
/* Signals connected indirectly to the buffer */
gulong buffer_notify_style_scheme_handler;
/* Denotes if we are in a grab from button press */
guint in_press : 1;
} GtkSourceMapPrivate;
enum
{
PROP_0,
PROP_VIEW,
PROP_FONT_DESC,
N_PROPERTIES
};
G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceMap, gtk_source_map, GTK_SOURCE_TYPE_VIEW)
static GParamSpec *properties[N_PROPERTIES];
static void
update_scrubber_position (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
GtkTextIter iter;
GdkRectangle visible_area;
GdkRectangle iter_area;
GdkRectangle scrubber_area;
GtkAllocation alloc;
GtkAllocation view_alloc;
gint child_height;
gint view_height;
gint y;
priv = gtk_source_map_get_instance_private (map);
if (priv->view == NULL)
{
return;
}
gtk_widget_get_allocation (GTK_WIDGET (priv->view), &view_alloc);
gtk_widget_get_allocation (GTK_WIDGET (map), &alloc);
gtk_widget_get_preferred_height (GTK_WIDGET (priv->view), NULL, &view_height);
gtk_widget_get_preferred_height (GTK_WIDGET (map), NULL, &child_height);
gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (priv->view), &visible_area);
gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (priv->view), &iter,
visible_area.x, visible_area.y);
gtk_text_view_get_iter_location (GTK_TEXT_VIEW (map), &iter, &iter_area);
gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (map),
GTK_TEXT_WINDOW_WIDGET,
iter_area.x, iter_area.y,
NULL, &y);
scrubber_area.x = 0;
scrubber_area.width = alloc.width;
scrubber_area.y = y;
scrubber_area.height = ((gdouble)view_alloc.height /
(gdouble)view_height *
(gdouble)child_height) +
iter_area.height;
if (memcmp (&scrubber_area, &priv->scrubber_area, sizeof scrubber_area) != 0)
{
GdkWindow *window;
/*
* NOTE:
*
* Initially we had a gtk_widget_queue_draw() here thinking
* that we would hit the pixel cache and everything would be
* fine. However, it actually has a noticible improvement on
* interactivity to simply invalidate the old and new region
* in the widgets primary GdkWindow. Since the window is
* not the GTK_TEXT_WINDOW_TEXT, we don't seem to invalidate
* the pixel cache. This makes things as interactive as they
* were when drawing the scrubber from a parent widget.
*/
window = gtk_text_view_get_window (GTK_TEXT_VIEW (map), GTK_TEXT_WINDOW_WIDGET);
if (window != NULL)
{
gdk_window_invalidate_rect (window, &priv->scrubber_area, FALSE);
gdk_window_invalidate_rect (window, &scrubber_area, FALSE);
}
priv->scrubber_area = scrubber_area;
}
}
static void
gtk_source_map_rebuild_css (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
GtkSourceStyleScheme *style_scheme;
GtkSourceStyle *style = NULL;
GtkTextBuffer *buffer;
GString *gstr;
gboolean alter_alpha = TRUE;
gchar *background = NULL;
priv = gtk_source_map_get_instance_private (map);
if (priv->view == NULL)
{
return;
}
/*
* This is where we calculate the CSS that maps the font for the
* minimap as well as the styling for the scrubber.
*
* The font is calculated from #GtkSourceMap:font-desc. We convert this
* to CSS using _gtk_source_pango_font_description_to_css(). It gets
* applied to the minimap widget via the CSS style provider which we
* attach to the view in gtk_source_map_init().
*
* The rules for calculating the style for the scrubber are as follows.
*
* If the current style scheme provides a background color for the
* scrubber using the "map-overlay" style name, we use that without
* any transformations.
*
* If the style scheme contains a "selection" style scheme, used for
* selected text, we use that with a 0.75 alpha value.
*
* If none of these are met, we take the background from the
* #GtkStyleContext using the deprecated
* gtk_style_context_get_background_color(). This is non-ideal, but
* currently required since we cannot indicate that we want to
* alter the alpha for gtk_render_background().
*/
gstr = g_string_new (NULL);
/* Calculate the font if one has been set */
if (priv->font_desc != NULL)
{
gchar *css;
css = _gtk_source_pango_font_description_to_css (priv->font_desc);
g_string_append_printf (gstr, "textview { %s }\n", css != NULL ? css : "");
g_free (css);
}
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->view));
style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
if (style_scheme != NULL)
{
style = gtk_source_style_scheme_get_style (style_scheme, "map-overlay");
if (style != NULL)
{
/* styling is taking as is only if we found a "map-overlay". */
alter_alpha = FALSE;
}
else
{
style = gtk_source_style_scheme_get_style (style_scheme, "selection");
}
}
if (style != NULL)
{
g_object_get (style,
"background", &background,
NULL);
}
if (background == NULL)
{
GtkStyleContext *context;
GdkRGBA color;
/*
* We failed to locate a style for both "map-overlay" and for
* "selection". That means we need to fallback to using the
* selected color for the gtk+ theme. This uses deprecated
* API because we have no way to tell gtk_render_background()
* to render with an alpha.
*/
context = gtk_widget_get_style_context (GTK_WIDGET (priv->view));
gtk_style_context_save (context);
gtk_style_context_add_class (context, "view");
gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
gtk_style_context_get_background_color (context,
gtk_style_context_get_state (context),
&color);
G_GNUC_END_IGNORE_DEPRECATIONS;
gtk_style_context_restore (context);
background = gdk_rgba_to_string (&color);
/*
* Make sure we alter the alpha. It is possible this could be
* FALSE here if we found a style for map-overlay but it did
* not contain a background color.
*/
alter_alpha = TRUE;
}
if (alter_alpha)
{
GdkRGBA color;
gdk_rgba_parse (&color, background);
color.alpha = 0.75;
g_free (background);
background = gdk_rgba_to_string (&color);
}
if (background != NULL)
{
g_string_append_printf (gstr,
"textview.scrubber {\n"
"\tbackground-color: %s;\n"
"\tborder-top: 1px solid shade(%s,0.9);\n"
"\tborder-bottom: 1px solid shade(%s,0.9);\n"
"}\n",
background,
background,
background);
}
g_free (background);
if (gstr->len > 0)
{
gtk_css_provider_load_from_data (priv->css_provider, gstr->str, gstr->len, NULL);
}
g_string_free (gstr, TRUE);
}
static void
update_child_vadjustment (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
GtkAdjustment *vadj;
GtkAdjustment *child_vadj;
gdouble value;
gdouble upper;
gdouble page_size;
gdouble child_upper;
gdouble child_page_size;
gdouble new_value = 0.0;
priv = gtk_source_map_get_instance_private (map);
vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->view));
g_object_get (vadj,
"upper", &upper,
"value", &value,
"page-size", &page_size,
NULL);
child_vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (map));
g_object_get (child_vadj,
"upper", &child_upper,
"page-size", &child_page_size,
NULL);
/*
* FIXME:
* Technically we should take into account lower here, but in practice
* it is always 0.0.
*/
if (child_page_size < child_upper)
{
new_value = (value / (upper - page_size)) * (child_upper - child_page_size);
}
gtk_adjustment_set_value (child_vadj, new_value);
}
static void
view_vadj_value_changed (GtkSourceMap *map,
GtkAdjustment *vadj)
{
update_child_vadjustment (map);
update_scrubber_position (map);
}
static void
view_vadj_notify_upper (GtkSourceMap *map,
GParamSpec *pspec,
GtkAdjustment *vadj)
{
update_scrubber_position (map);
}
static void
buffer_notify_style_scheme (GtkSourceMap *map,
GParamSpec *pspec,
GtkTextBuffer *buffer)
{
gtk_source_map_rebuild_css (map);
}
static void
connect_buffer (GtkSourceMap *map,
GtkTextBuffer *buffer)
{
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
priv->buffer = buffer;
g_object_add_weak_pointer (G_OBJECT (buffer), (gpointer *)&priv->buffer);
priv->buffer_notify_style_scheme_handler =
g_signal_connect_object (buffer,
"notify::style-scheme",
G_CALLBACK (buffer_notify_style_scheme),
map,
G_CONNECT_SWAPPED);
buffer_notify_style_scheme (map, NULL, buffer);
}
static void
disconnect_buffer (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
if (priv->buffer == NULL)
{
return;
}
if (priv->buffer_notify_style_scheme_handler != 0)
{
g_signal_handler_disconnect (priv->buffer,
priv->buffer_notify_style_scheme_handler);
priv->buffer_notify_style_scheme_handler = 0;
}
g_object_remove_weak_pointer (G_OBJECT (priv->buffer), (gpointer *)&priv->buffer);
priv->buffer = NULL;
}
static void
view_notify_buffer (GtkSourceMap *map,
GParamSpec *pspec,
GtkSourceView *view)
{
GtkSourceMapPrivate *priv;
GtkTextBuffer *buffer;
priv = gtk_source_map_get_instance_private (map);
if (priv->buffer != NULL)
{
disconnect_buffer (map);
}
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
if (buffer != NULL)
{
connect_buffer (map, buffer);
}
}
static void
gtk_source_map_set_font_desc (GtkSourceMap *map,
const PangoFontDescription *font_desc)
{
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
if (font_desc != priv->font_desc)
{
g_clear_pointer (&priv->font_desc, pango_font_description_free);
if (font_desc)
{
priv->font_desc = pango_font_description_copy (font_desc);
}
}
gtk_source_map_rebuild_css (map);
}
static void
gtk_source_map_set_font_name (GtkSourceMap *map,
const gchar *font_name)
{
PangoFontDescription *font_desc;
if (font_name == NULL)
{
font_name = "Monospace 1";
}
font_desc = pango_font_description_from_string (font_name);
gtk_source_map_set_font_desc (map, font_desc);
pango_font_description_free (font_desc);
}
static void
gtk_source_map_get_preferred_width (GtkWidget *widget,
gint *mininum_width,
gint *natural_width)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
PangoLayout *layout;
gint height;
gint width;
priv = gtk_source_map_get_instance_private (map);
if (priv->font_desc == NULL)
{
*mininum_width = *natural_width = DEFAULT_WIDTH;
return;
}
/*
* FIXME:
*
* This seems like the type of thing we should calculate when
* rebuilding our CSS since it gets used a bunch and changes
* very little.
*/
layout = gtk_widget_create_pango_layout (GTK_WIDGET (map), "X");
pango_layout_get_pixel_size (layout, &width, &height);
g_object_unref (layout);
width *= gtk_source_view_get_right_margin_position (priv->view);
*mininum_width = *natural_width = width;
}
static void
gtk_source_map_get_preferred_height (GtkWidget *widget,
gint *minimum_height,
gint *natural_height)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
if (priv->view == NULL)
{
*minimum_height = *natural_height = 0;
return;
}
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->get_preferred_height (widget,
minimum_height,
natural_height);
*minimum_height = 0;
}
/*
* This scrolls using buffer coordinates.
* Translate your event location to a buffer coordinate before
* calling this function.
*/
static void
scroll_to_child_point (GtkSourceMap *map,
const GdkPoint *point)
{
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
if (priv->view != NULL)
{
GtkAllocation alloc;
GtkTextIter iter;
gtk_widget_get_allocation (GTK_WIDGET (map), &alloc);
gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (map),
&iter, point->x, point->y);
gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (priv->view), &iter,
0.0, TRUE, 1.0, 0.5);
}
}
static void
gtk_source_map_size_allocate (GtkWidget *widget,
GtkAllocation *alloc)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->size_allocate (widget, alloc);
update_scrubber_position (map);
}
static void
connect_view (GtkSourceMap *map,
GtkSourceView *view)
{
GtkSourceMapPrivate *priv;
GtkAdjustment *vadj;
priv = gtk_source_map_get_instance_private (map);
priv->view = view;
g_object_add_weak_pointer (G_OBJECT (view), (gpointer *)&priv->view);
vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (view));
priv->buffer_binding =
g_object_bind_property (view, "buffer",
map, "buffer",
G_BINDING_SYNC_CREATE);
g_object_add_weak_pointer (G_OBJECT (priv->buffer_binding),
(gpointer *)&priv->buffer_binding);
priv->indent_width_binding =
g_object_bind_property (view, "indent-width",
map, "indent-width",
G_BINDING_SYNC_CREATE);
g_object_add_weak_pointer (G_OBJECT (priv->indent_width_binding),
(gpointer *)&priv->indent_width_binding);
priv->tab_width_binding =
g_object_bind_property (view, "tab-width",
map, "tab-width",
G_BINDING_SYNC_CREATE);
g_object_add_weak_pointer (G_OBJECT (priv->tab_width_binding),
(gpointer *)&priv->tab_width_binding);
priv->view_notify_buffer_handler =
g_signal_connect_object (view,
"notify::buffer",
G_CALLBACK (view_notify_buffer),
map,
G_CONNECT_SWAPPED);
view_notify_buffer (map, NULL, view);
priv->view_vadj_value_changed_handler =
g_signal_connect_object (vadj,
"value-changed",
G_CALLBACK (view_vadj_value_changed),
map,
G_CONNECT_SWAPPED);
priv->view_vadj_notify_upper_handler =
g_signal_connect_object (vadj,
"notify::upper",
G_CALLBACK (view_vadj_notify_upper),
map,
G_CONNECT_SWAPPED);
if ((gtk_widget_get_events (GTK_WIDGET (priv->view)) & GDK_ENTER_NOTIFY_MASK) == 0)
{
gtk_widget_add_events (GTK_WIDGET (priv->view), GDK_ENTER_NOTIFY_MASK);
}
if ((gtk_widget_get_events (GTK_WIDGET (priv->view)) & GDK_LEAVE_NOTIFY_MASK) == 0)
{
gtk_widget_add_events (GTK_WIDGET (priv->view), GDK_LEAVE_NOTIFY_MASK);
}
/* If we are not visible, we want to block certain signal handlers */
if (!gtk_widget_get_visible (GTK_WIDGET (map)))
{
g_signal_handler_block (vadj, priv->view_vadj_value_changed_handler);
g_signal_handler_block (vadj, priv->view_vadj_notify_upper_handler);
}
gtk_source_map_rebuild_css (map);
}
static void
disconnect_view (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
GtkAdjustment *vadj;
priv = gtk_source_map_get_instance_private (map);
if (priv->view == NULL)
{
return;
}
disconnect_buffer (map);
if (priv->buffer_binding != NULL)
{
g_object_remove_weak_pointer (G_OBJECT (priv->buffer_binding),
(gpointer *)&priv->buffer_binding);
g_binding_unbind (priv->buffer_binding);
priv->buffer_binding = NULL;
}
if (priv->indent_width_binding != NULL)
{
g_object_remove_weak_pointer (G_OBJECT (priv->indent_width_binding),
(gpointer *)&priv->indent_width_binding);
g_binding_unbind (priv->indent_width_binding);
priv->indent_width_binding = NULL;
}
if (priv->tab_width_binding != NULL)
{
g_object_remove_weak_pointer (G_OBJECT (priv->tab_width_binding),
(gpointer *)&priv->tab_width_binding);
g_binding_unbind (priv->tab_width_binding);
priv->tab_width_binding = NULL;
}
if (priv->view_notify_buffer_handler != 0)
{
g_signal_handler_disconnect (priv->view, priv->view_notify_buffer_handler);
priv->view_notify_buffer_handler = 0;
}
vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->view));
if (vadj != NULL)
{
g_signal_handler_disconnect (vadj, priv->view_vadj_value_changed_handler);
priv->view_vadj_value_changed_handler = 0;
g_signal_handler_disconnect (vadj, priv->view_vadj_notify_upper_handler);
priv->view_vadj_notify_upper_handler = 0;
}
g_object_remove_weak_pointer (G_OBJECT (priv->view), (gpointer *)&priv->view);
priv->view = NULL;
}
static void
gtk_source_map_destroy (GtkWidget *widget)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
disconnect_buffer (map);
disconnect_view (map);
g_clear_object (&priv->css_provider);
g_clear_pointer (&priv->font_desc, pango_font_description_free);
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->destroy (widget);
}
static gboolean
gtk_source_map_draw (GtkWidget *widget,
cairo_t *cr)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
GtkStyleContext *style_context;
priv = gtk_source_map_get_instance_private (map);
style_context = gtk_widget_get_style_context (widget);
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->draw (widget, cr);
gtk_style_context_save (style_context);
gtk_style_context_add_class (style_context, "scrubber");
gtk_render_background (style_context, cr,
priv->scrubber_area.x, priv->scrubber_area.y,
priv->scrubber_area.width, priv->scrubber_area.height);
gtk_style_context_restore (style_context);
return FALSE;
}
static void
gtk_source_map_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkSourceMap *map = GTK_SOURCE_MAP (object);
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
switch (prop_id)
{
case PROP_FONT_DESC:
g_value_set_boxed (value, priv->font_desc);
break;
case PROP_VIEW:
g_value_set_object (value, gtk_source_map_get_view (map));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gtk_source_map_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkSourceMap *map = GTK_SOURCE_MAP (object);
switch (prop_id)
{
case PROP_VIEW:
gtk_source_map_set_view (map, g_value_get_object (value));
break;
case PROP_FONT_DESC:
gtk_source_map_set_font_desc (map, g_value_get_boxed (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static gboolean
gtk_source_map_button_press_event (GtkWidget *widget,
GdkEventButton *event)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
GdkPoint point;
priv = gtk_source_map_get_instance_private (map);
point.x = event->x;
point.y = event->y;
gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (map),
GTK_TEXT_WINDOW_WIDGET,
event->x, event->y,
&point.x, &point.y);
scroll_to_child_point (map, &point);
gtk_grab_add (widget);
priv->in_press = TRUE;
return GDK_EVENT_STOP;
}
static gboolean
gtk_source_map_button_release_event (GtkWidget *widget,
GdkEventButton *event)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
gtk_grab_remove (widget);
priv->in_press = FALSE;
return GDK_EVENT_STOP;
}
static gboolean
gtk_source_map_motion_notify_event (GtkWidget *widget,
GdkEventMotion *event)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
priv = gtk_source_map_get_instance_private (map);
if (priv->in_press && (priv->view != NULL))
{
GtkTextBuffer *buffer;
GtkAllocation alloc;
GdkRectangle area;
GtkTextIter iter;
GdkPoint point;
gdouble yratio;
gint height;
gtk_widget_get_allocation (widget, &alloc);
gtk_widget_get_preferred_height (widget, NULL, &height);
if (height > 0)
{
height = MIN (height, alloc.height);
}
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (map));
gtk_text_buffer_get_end_iter (buffer, &iter);
gtk_text_view_get_iter_location (GTK_TEXT_VIEW (map), &iter, &area);
yratio = CLAMP (event->y - alloc.y, 0, height) / (gdouble)height;
point.x = 0;
point.y = (area.y + area.height) * yratio;
scroll_to_child_point (map, &point);
}
return GDK_EVENT_STOP;
}
static gboolean
gtk_source_map_scroll_event (GtkWidget *widget,
GdkEventScroll *event)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
static const gint scroll_acceleration = 4;
priv = gtk_source_map_get_instance_private (map);
/*
* FIXME:
*
* This doesn't propagate kinetic scrolling or anything.
* We should probably make something that does that.
*/
if (priv->view != NULL)
{
gdouble x;
gdouble y;
gint count = 0;
if (event->direction == GDK_SCROLL_UP)
{
count = -scroll_acceleration;
}
else if (event->direction == GDK_SCROLL_DOWN)
{
count = scroll_acceleration;
}
else
{
gdk_event_get_scroll_deltas ((GdkEvent *)event, &x, &y);
if (y > 0)
{
count = scroll_acceleration;
}
else if (y < 0)
{
count = -scroll_acceleration;
}
}
if (count != 0)
{
g_signal_emit_by_name (priv->view, "move-viewport",
GTK_SCROLL_STEPS, count);
return GDK_EVENT_STOP;
}
}
return GDK_EVENT_PROPAGATE;
}
static void
set_view_cursor (GtkSourceMap *map)
{
GdkWindow *window;
window = gtk_text_view_get_window (GTK_TEXT_VIEW (map),
GTK_TEXT_WINDOW_TEXT);
if (window != NULL)
{
gdk_window_set_cursor (window, NULL);
}
}
static void
gtk_source_map_state_flags_changed (GtkWidget *widget,
GtkStateFlags flags)
{
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->state_flags_changed (widget, flags);
set_view_cursor (GTK_SOURCE_MAP (widget));
}
static void
gtk_source_map_realize (GtkWidget *widget)
{
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->realize (widget);
set_view_cursor (GTK_SOURCE_MAP (widget));
}
static void
gtk_source_map_show (GtkWidget *widget)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
GtkAdjustment *vadj;
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->show (widget);
priv = gtk_source_map_get_instance_private (map);
if (priv->view != NULL)
{
vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->view));
g_signal_handler_unblock (vadj, priv->view_vadj_value_changed_handler);
g_signal_handler_unblock (vadj, priv->view_vadj_notify_upper_handler);
g_object_notify (G_OBJECT (vadj), "upper");
g_signal_emit_by_name (vadj, "value-changed");
}
}
static void
gtk_source_map_hide (GtkWidget *widget)
{
GtkSourceMap *map = GTK_SOURCE_MAP (widget);
GtkSourceMapPrivate *priv;
GtkAdjustment *vadj;
GTK_WIDGET_CLASS (gtk_source_map_parent_class)->hide (widget);
priv = gtk_source_map_get_instance_private (map);
if (priv->view != NULL)
{
vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->view));
g_signal_handler_block (vadj, priv->view_vadj_value_changed_handler);
g_signal_handler_block (vadj, priv->view_vadj_notify_upper_handler);
}
}
static void
gtk_source_map_class_init (GtkSourceMapClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->get_property = gtk_source_map_get_property;
object_class->set_property = gtk_source_map_set_property;
widget_class->destroy = gtk_source_map_destroy;
widget_class->draw = gtk_source_map_draw;
widget_class->get_preferred_height = gtk_source_map_get_preferred_height;
widget_class->get_preferred_width = gtk_source_map_get_preferred_width;
widget_class->hide = gtk_source_map_hide;
widget_class->size_allocate = gtk_source_map_size_allocate;
widget_class->button_press_event = gtk_source_map_button_press_event;
widget_class->button_release_event = gtk_source_map_button_release_event;
widget_class->motion_notify_event = gtk_source_map_motion_notify_event;
widget_class->scroll_event = gtk_source_map_scroll_event;
widget_class->show = gtk_source_map_show;
widget_class->state_flags_changed = gtk_source_map_state_flags_changed;
widget_class->realize = gtk_source_map_realize;
properties[PROP_VIEW] =
g_param_spec_object ("view",
"View",
"The view this widget is mapping.",
GTK_SOURCE_TYPE_VIEW,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
properties[PROP_FONT_DESC] =
g_param_spec_boxed ("font-desc",
"Font Description",
"The Pango font description to use.",
PANGO_TYPE_FONT_DESCRIPTION,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPERTIES, properties);
}
static void
gtk_source_map_init (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
GtkSourceCompletion *completion;
GtkStyleContext *context;
priv = gtk_source_map_get_instance_private (map);
priv->css_provider = gtk_css_provider_new ();
context = gtk_widget_get_style_context (GTK_WIDGET (map));
gtk_style_context_add_provider (context,
GTK_STYLE_PROVIDER (priv->css_provider),
GTK_SOURCE_STYLE_PROVIDER_PRIORITY + 1);
g_object_set (map,
"auto-indent", FALSE,
"can-focus", FALSE,
"editable", FALSE,
"expand", FALSE,
"monospace", TRUE,
"show-line-numbers", FALSE,
"show-line-marks", FALSE,
"show-right-margin", FALSE,
"visible", TRUE,
NULL);
gtk_widget_add_events (GTK_WIDGET (map), GDK_SCROLL_MASK);
completion = gtk_source_view_get_completion (GTK_SOURCE_VIEW (map));
gtk_source_completion_block_interactive (completion);
gtk_source_map_set_font_name (map, "Monospace 1");
}
/**
* gtk_source_map_new:
*
* Creates a new #GtkSourceMap.
*
* Returns: a new #GtkSourceMap.
*
* Since: 3.18
*/
GtkWidget *
gtk_source_map_new (void)
{
return g_object_new (GTK_SOURCE_TYPE_MAP, NULL);
}
/**
* gtk_source_map_set_view:
* @map: a #GtkSourceMap
* @view: a #GtkSourceView
*
* Sets the view that @map will be doing the mapping to.
*
* Since: 3.18
*/
void
gtk_source_map_set_view (GtkSourceMap *map,
GtkSourceView *view)
{
GtkSourceMapPrivate *priv;
g_return_if_fail (GTK_SOURCE_IS_MAP (map));
g_return_if_fail (view == NULL || GTK_SOURCE_IS_VIEW (view));
priv = gtk_source_map_get_instance_private (map);
if (priv->view == view)
{
return;
}
if (priv->view != NULL)
{
disconnect_view (map);
}
if (view != NULL)
{
connect_view (map, view);
}
g_object_notify_by_pspec (G_OBJECT (map), properties[PROP_VIEW]);
}
/**
* gtk_source_map_get_view:
* @map: a #GtkSourceMap.
*
* Gets the #GtkSourceMap:view property, which is the view this widget is mapping.
*
* Returns: (transfer none) (nullable): a #GtkSourceView or %NULL.
*
* Since: 3.18
*/
GtkSourceView *
gtk_source_map_get_view (GtkSourceMap *map)
{
GtkSourceMapPrivate *priv;
g_return_val_if_fail (GTK_SOURCE_IS_MAP (map), NULL);
priv = gtk_source_map_get_instance_private (map);
return priv->view;
}