1260 lines
35 KiB
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;
|
|
}
|