Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
/* gtksourcecompletioninfo.c
 * This file is part of GtkSourceView
 *
 * Copyright (C) 2007 -2009 Jesús Barbero Rodríguez <chuchiperriman@gmail.com>
 * Copyright (C) 2009 - Jesse van den Kieboom <jessevdk@gnome.org>
 * Copyright (C) 2012 - Sébastien Wilmet <swilmet@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
 */

/**
 * SECTION:completioninfo
 * @title: GtkSourceCompletionInfo
 * @short_description: Calltips object
 *
 * This object can be used to show a calltip or help for the
 * current completion proposal.
 *
 * The info window has always the same size as the natural size of its child
 * widget, added with gtk_container_add().  If you want a fixed size instead, a
 * possibility is to use a scrolled window, as the following example
 * demonstrates.
 *
 * <example>
 *   <title>Fixed size with a scrolled window.</title>
 *   <programlisting>
 * GtkSourceCompletionInfo *info;
 * GtkWidget *your_widget;
 * GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
 *
 * gtk_widget_set_size_request (scrolled_window, 300, 200);
 * gtk_container_add (GTK_CONTAINER (scrolled_window), your_widget);
 * gtk_container_add (GTK_CONTAINER (info), scrolled_window);
 *   </programlisting>
 * </example>
 *
 * If the calltip is displayed on top of a certain widget, say a #GtkTextView,
 * you should attach the calltip window to the #GtkTextView with
 * gtk_window_set_attached_to().  By doing this, the calltip will be hidden when
 * the #GtkWidget::focus-out-event signal is emitted by the #GtkTextView. You
 * may also be interested by the #GtkTextBuffer:cursor-position property (when
 * its value is modified). If you use the #GtkSourceCompletionInfo through the
 * #GtkSourceCompletion machinery, you don't need to worry about this.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <gtksourceview/gtksourcecompletioninfo.h>
#include "gtksourceview-i18n.h"

struct _GtkSourceCompletionInfoPrivate
{
	guint idle_resize;

	GtkWidget *attached_to;
	gulong focus_out_event_handler;

	gint xoffset;

	guint transient_set : 1;
};

enum
{
	BEFORE_SHOW,
	N_SIGNALS
};

static guint signals[N_SIGNALS];

G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceCompletionInfo, gtk_source_completion_info, GTK_TYPE_WINDOW);

/* Resize the window */

static gboolean
idle_resize (GtkSourceCompletionInfo *info)
{
	GtkWidget *child = gtk_bin_get_child (GTK_BIN (info));
	GtkRequisition nat_size;
	guint border_width;
	gint window_width;
	gint window_height;
	gint cur_window_width;
	gint cur_window_height;

	info->priv->idle_resize = 0;

	if (child == NULL)
	{
		return G_SOURCE_REMOVE;
	}

	gtk_widget_get_preferred_size (child, NULL, &nat_size);

	border_width = gtk_container_get_border_width (GTK_CONTAINER (info));

	window_width = nat_size.width + 2 * border_width;
	window_height = nat_size.height + 2 * border_width;

	gtk_window_get_size (GTK_WINDOW (info), &cur_window_width, &cur_window_height);

	/* Avoid an infinite loop */
	if (cur_window_width != window_width || cur_window_height != window_height)
	{
		gtk_window_resize (GTK_WINDOW (info),
				   MAX (1, window_width),
				   MAX (1, window_height));
	}

	return G_SOURCE_REMOVE;
}

static void
queue_resize (GtkSourceCompletionInfo *info)
{
	if (info->priv->idle_resize == 0)
	{
		info->priv->idle_resize = g_idle_add ((GSourceFunc)idle_resize, info);
	}
}

static void
gtk_source_completion_info_check_resize (GtkContainer *container)
{
	GtkSourceCompletionInfo *info = GTK_SOURCE_COMPLETION_INFO (container);
	queue_resize (info);

	GTK_CONTAINER_CLASS (gtk_source_completion_info_parent_class)->check_resize (container);
}

/* Geometry management */

static GtkSizeRequestMode
gtk_source_completion_info_get_request_mode (GtkWidget *widget)
{
	return GTK_SIZE_REQUEST_CONSTANT_SIZE;
}

static void
gtk_source_completion_info_get_preferred_width (GtkWidget *widget,
						gint	  *min_width,
						gint	  *nat_width)
{
	GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget));
	gint width = 0;

	if (child != NULL)
	{
		GtkRequisition nat_size;
		gtk_widget_get_preferred_size (child, NULL, &nat_size);
		width = nat_size.width;
	}

	if (min_width != NULL)
	{
		*min_width = width;
	}

	if (nat_width != NULL)
	{
		*nat_width = width;
	}
}

static void
gtk_source_completion_info_get_preferred_height (GtkWidget *widget,
						 gint	   *min_height,
						 gint	   *nat_height)
{
	GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget));
	gint height = 0;

	if (child != NULL)
	{
		GtkRequisition nat_size;
		gtk_widget_get_preferred_size (child, NULL, &nat_size);
		height = nat_size.height;
	}

	if (min_height != NULL)
	{
		*min_height = height;
	}

	if (nat_height != NULL)
	{
		*nat_height = height;
	}
}

/* Init, dispose, finalize, ... */

static gboolean
focus_out_event_cb (GtkSourceCompletionInfo *info)
{
	gtk_widget_hide (GTK_WIDGET (info));
	return FALSE;
}

static void
set_attached_to (GtkSourceCompletionInfo *info,
		 GtkWidget               *attached_to)
{
	if (info->priv->attached_to != NULL)
	{
		g_object_remove_weak_pointer (G_OBJECT (info->priv->attached_to),
					      (gpointer *) &info->priv->attached_to);

		if (info->priv->focus_out_event_handler != 0)
		{
			g_signal_handler_disconnect (info->priv->attached_to,
						     info->priv->focus_out_event_handler);

			info->priv->focus_out_event_handler = 0;
		}
	}

	info->priv->attached_to = attached_to;

	if (attached_to == NULL)
	{
		return;
	}

	g_object_add_weak_pointer (G_OBJECT (attached_to),
				   (gpointer *) &info->priv->attached_to);

	info->priv->focus_out_event_handler =
		g_signal_connect_swapped (attached_to,
					  "focus-out-event",
					  G_CALLBACK (focus_out_event_cb),
					  info);

	info->priv->transient_set = FALSE;
}

static void
update_attached_to (GtkSourceCompletionInfo *info)
{
	set_attached_to (info, gtk_window_get_attached_to (GTK_WINDOW (info)));
}

static void
gtk_source_completion_info_init (GtkSourceCompletionInfo *info)
{
	info->priv = gtk_source_completion_info_get_instance_private (info);

	g_signal_connect (info,
			  "notify::attached-to",
			  G_CALLBACK (update_attached_to),
			  NULL);

	update_attached_to (info);

	/* Tooltip style */
	gtk_window_set_title (GTK_WINDOW (info), _("Completion Info"));
	gtk_widget_set_name (GTK_WIDGET (info), "gtk-tooltip");

	gtk_window_set_type_hint (GTK_WINDOW (info),
	                          GDK_WINDOW_TYPE_HINT_COMBO);

	gtk_container_set_border_width (GTK_CONTAINER (info), 1);
}

static void
gtk_source_completion_info_dispose (GObject *object)
{
	GtkSourceCompletionInfo *info = GTK_SOURCE_COMPLETION_INFO (object);

	if (info->priv->idle_resize != 0)
	{
		g_source_remove (info->priv->idle_resize);
		info->priv->idle_resize = 0;
	}

	set_attached_to (info, NULL);

	G_OBJECT_CLASS (gtk_source_completion_info_parent_class)->dispose (object);
}

static void
gtk_source_completion_info_show (GtkWidget *widget)
{
	GtkSourceCompletionInfo *info = GTK_SOURCE_COMPLETION_INFO (widget);

	/* First emit BEFORE_SHOW and then chain up */
	g_signal_emit (info, signals[BEFORE_SHOW], 0);

	if (info->priv->attached_to != NULL && !info->priv->transient_set)
	{
		GtkWidget *toplevel;

		toplevel = gtk_widget_get_toplevel (GTK_WIDGET (info->priv->attached_to));
		if (gtk_widget_is_toplevel (toplevel))
		{
			gtk_window_set_transient_for (GTK_WINDOW (info),
						      GTK_WINDOW (toplevel));
			info->priv->transient_set = TRUE;
		}
	}

	GTK_WIDGET_CLASS (gtk_source_completion_info_parent_class)->show (widget);
}

static gboolean
gtk_source_completion_info_draw (GtkWidget *widget,
                                 cairo_t   *cr)
{
	GTK_WIDGET_CLASS (gtk_source_completion_info_parent_class)->draw (widget, cr);

	gtk_render_frame (gtk_widget_get_style_context (widget),
	                  cr,
	                  0, 0,
	                  gtk_widget_get_allocated_width (widget),
	                  gtk_widget_get_allocated_height (widget));

	return FALSE;
}

static void
gtk_source_completion_info_class_init (GtkSourceCompletionInfoClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
	GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);

	object_class->dispose = gtk_source_completion_info_dispose;

	widget_class->show = gtk_source_completion_info_show;
	widget_class->draw = gtk_source_completion_info_draw;
	widget_class->get_request_mode = gtk_source_completion_info_get_request_mode;
	widget_class->get_preferred_width = gtk_source_completion_info_get_preferred_width;
	widget_class->get_preferred_height = gtk_source_completion_info_get_preferred_height;

	container_class->check_resize = gtk_source_completion_info_check_resize;

	/**
	 * GtkSourceCompletionInfo::before-show:
	 * @info: The #GtkSourceCompletionInfo who emits the signal
	 *
	 * This signal is emitted before any "show" management. You can connect
	 * to this signal if you want to change some properties or position
	 * before the real "show".
	 *
	 * Deprecated: 3.10: This signal should not be used.
	 */
	signals[BEFORE_SHOW] =
		g_signal_new ("before-show",
		              G_TYPE_FROM_CLASS (klass),
		              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION | G_SIGNAL_DEPRECATED,
		              0,
		              NULL, NULL, NULL,
		              G_TYPE_NONE, 0);
}

void
_gtk_source_completion_info_set_xoffset (GtkSourceCompletionInfo *window,
					 gint                     xoffset)
{
	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_INFO (window));

	window->priv->xoffset = xoffset;
}

/* Move to iter */

static void
get_iter_pos (GtkTextView *text_view,
              GtkTextIter *iter,
              gint        *x,
              gint        *y,
              gint        *height)
{
	GdkWindow *win;
	GdkRectangle location;
	gint win_x;
	gint win_y;
	gint xx;
	gint yy;

	gtk_text_view_get_iter_location (text_view, iter, &location);

	gtk_text_view_buffer_to_window_coords (text_view,
					       GTK_TEXT_WINDOW_WIDGET,
					       location.x,
					       location.y,
					       &win_x,
					       &win_y);

	win = gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET);
	gdk_window_get_origin (win, &xx, &yy);

	*x = win_x + xx;
	*y = win_y + yy + location.height;
	*height = location.height;
}

static void
compensate_for_gravity (GtkSourceCompletionInfo *window,
                        gint                    *x,
                        gint                    *y,
                        gint                     w,
                        gint                     h)
{
	GdkGravity gravity = gtk_window_get_gravity (GTK_WINDOW (window));

	/* Horizontal */
	switch (gravity)
	{
		case GDK_GRAVITY_NORTH:
		case GDK_GRAVITY_SOUTH:
		case GDK_GRAVITY_CENTER:
			*x = w / 2;
			break;
		case GDK_GRAVITY_NORTH_EAST:
		case GDK_GRAVITY_SOUTH_EAST:
		case GDK_GRAVITY_EAST:
			*x = w;
			break;
		case GDK_GRAVITY_NORTH_WEST:
		case GDK_GRAVITY_WEST:
		case GDK_GRAVITY_SOUTH_WEST:
		case GDK_GRAVITY_STATIC:
		default:
			*x = 0;
			break;
	}

	/* Vertical */
	switch (gravity)
	{
		case GDK_GRAVITY_WEST:
		case GDK_GRAVITY_CENTER:
		case GDK_GRAVITY_EAST:
			*y = w / 2;
			break;
		case GDK_GRAVITY_SOUTH_EAST:
		case GDK_GRAVITY_SOUTH:
		case GDK_GRAVITY_SOUTH_WEST:
			*y = w;
			break;
		case GDK_GRAVITY_NORTH:
		case GDK_GRAVITY_NORTH_EAST:
		case GDK_GRAVITY_NORTH_WEST:
		case GDK_GRAVITY_STATIC:
		default:
			*y = 0;
			break;
	}
}

static void
move_overlap (gint     *y,
              gint      h,
              gint      oy,
              gint      cy,
              gint      line_height,
              gboolean  move_up)
{
	/* Test if there is overlap */
	if (*y - cy < oy && *y - cy + h > oy - line_height)
	{
		if (move_up)
		{
			*y = oy - line_height - h + cy;
		}
		else
		{
			*y = oy + cy;
		}
	}
}

static void
move_to_iter (GtkSourceCompletionInfo *window,
	      GtkTextView             *view,
	      GtkTextIter             *iter)
{
	GdkScreen *screen;
	gint x, y;
	gint w, h;
	gint sw, sh;
	gint cx, cy;
	gint oy;
	gint height;
	gboolean overlapup;

	screen = gtk_window_get_screen (GTK_WINDOW (window));

	sw = gdk_screen_get_width (screen);
	sh = gdk_screen_get_height (screen);

	get_iter_pos (view, iter, &x, &y, &height);
	gtk_window_get_size (GTK_WINDOW (window), &w, &h);

	x += window->priv->xoffset;

	oy = y;
	compensate_for_gravity (window, &cx, &cy, w, h);

	/* Push window inside screen */
	if (x - cx + w > sw)
	{
		x = (sw - w) + cx;
	}
	else if (x - cx < 0)
	{
		x = cx;
	}

	if (y - cy + h > sh)
	{
		y = (sh - h) + cy;
		overlapup = TRUE;
	}
	else if (y - cy < 0)
	{
		y = cy;
		overlapup = FALSE;
	}
	else
	{
		overlapup = TRUE;
	}

	/* Make sure that text is still readable */
	move_overlap (&y, h, oy, cy, height, overlapup);

	gtk_window_move (GTK_WINDOW (window), x, y);
}

static void
move_to_cursor (GtkSourceCompletionInfo *window,
		GtkTextView             *view)
{
	GtkTextBuffer *buffer;
	GtkTextIter insert;

	buffer = gtk_text_view_get_buffer (view);
	gtk_text_buffer_get_iter_at_mark (buffer, &insert, gtk_text_buffer_get_insert (buffer));

	move_to_iter (window, view, &insert);
}

/* Public functions */

/**
 * gtk_source_completion_info_new:
 *
 * Returns: a new GtkSourceCompletionInfo.
 */
GtkSourceCompletionInfo *
gtk_source_completion_info_new (void)
{
	return g_object_new (GTK_SOURCE_TYPE_COMPLETION_INFO,
	                     "type", GTK_WINDOW_POPUP,
			     "border-width", 3,
	                     NULL);
}

/**
 * gtk_source_completion_info_move_to_iter:
 * @info: a #GtkSourceCompletionInfo.
 * @view: a #GtkTextView on which the info window should be positioned.
 * @iter: (nullable): a #GtkTextIter.
 *
 * Moves the #GtkSourceCompletionInfo to @iter. If @iter is %NULL @info is
 * moved to the cursor position. Moving will respect the #GdkGravity setting
 * of the info window and will ensure the line at @iter is not occluded by
 * the window.
 */
void
gtk_source_completion_info_move_to_iter (GtkSourceCompletionInfo *info,
                                         GtkTextView             *view,
                                         GtkTextIter             *iter)
{
	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_INFO (info));
	g_return_if_fail (GTK_IS_TEXT_VIEW (view));

	if (iter == NULL)
	{
		move_to_cursor (info, view);
	}
	else
	{
		move_to_iter (info, view, iter);
	}
}

/**
 * gtk_source_completion_info_set_widget:
 * @info: a #GtkSourceCompletionInfo.
 * @widget: (nullable): a #GtkWidget.
 *
 * Sets the content widget of the info window. See that the previous widget will
 * lose a reference and it can be destroyed, so if you do not want this to
 * happen you must use g_object_ref() before calling this method.
 *
 * Deprecated: 3.8: Use gtk_container_add() instead. If there is already a child
 * widget, remove it with gtk_container_remove().
 */
void
gtk_source_completion_info_set_widget (GtkSourceCompletionInfo *info,
                                       GtkWidget               *widget)
{
	GtkWidget *cur_child = NULL;

	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_INFO (info));
	g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));

	cur_child = gtk_bin_get_child (GTK_BIN (info));

	if (cur_child == widget)
	{
		return;
	}

	if (cur_child != NULL)
	{
		gtk_container_remove (GTK_CONTAINER (info), cur_child);
	}

	if (widget != NULL)
	{
		gtk_container_add (GTK_CONTAINER (info), widget);
	}
}

/**
 * gtk_source_completion_info_get_widget:
 * @info: a #GtkSourceCompletionInfo.
 *
 * Get the current content widget.
 *
 * Returns: (transfer none): The current content widget.
 *
 * Deprecated: 3.8: Use gtk_bin_get_child() instead.
 */
GtkWidget *
gtk_source_completion_info_get_widget (GtkSourceCompletionInfo* info)
{
	g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_INFO (info), NULL);

	return gtk_bin_get_child (GTK_BIN (info));
}