[gitg] Add blame mode. Fixes bug #576693



commit 9e560ab9e1e14c1e805ee55613111f78090b312d
Author: Ignacio Casal Quinteiro <icq gnome org>
Date:   Fri Jul 22 12:58:17 2011 +0200

    Add blame mode. Fixes bug #576693
    
    https://bugzilla.gnome.org/show_bug.cgi?id=576693

 gitg/Makefile.am                  |    2 +
 gitg/gitg-blame-renderer.c        |  454 +++++++++++++++++++++++++++++++
 gitg/gitg-blame-renderer.h        |   61 +++++
 gitg/gitg-revision-files-panel.c  |  535 +++++++++++++++++++++++++++++++++++--
 gitg/gitg-revision-files-panel.ui |   52 +++-
 5 files changed, 1065 insertions(+), 39 deletions(-)
---
diff --git a/gitg/Makefile.am b/gitg/Makefile.am
index 5358908..e74617f 100644
--- a/gitg/Makefile.am
+++ b/gitg/Makefile.am
@@ -14,6 +14,7 @@ AM_CPPFLAGS =							\
 NOINST_H_FILES =			\
 	gitg-activatable.h		\
 	gitg-avatar-cache.h		\
+	gitg-blame-renderer.h		\
 	gitg-branch-actions.h		\
 	gitg-cell-renderer-path.h	\
 	gitg-commit-view.h		\
@@ -38,6 +39,7 @@ gitg_SOURCES =				\
 	gitg.c				\
 	gitg-activatable.c		\
 	gitg-avatar-cache.c		\
+	gitg-blame-renderer.c		\
 	gitg-branch-actions.c		\
 	gitg-cell-renderer-path.c	\
 	gitg-commit-view.c		\
diff --git a/gitg/gitg-blame-renderer.c b/gitg/gitg-blame-renderer.c
new file mode 100644
index 0000000..630c284
--- /dev/null
+++ b/gitg/gitg-blame-renderer.c
@@ -0,0 +1,454 @@
+/*
+ * gitg-blame-renderer.c
+ * This file is part of gitg - git repository viewer
+ *
+ * Copyright (C) 2011 - Ignacio Casal Quinteiro
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "gitg-blame-renderer.h"
+
+#include <libgitg/gitg-revision.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#define GITG_BLAME_RENDERER_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE((object), GITG_TYPE_BLAME_RENDERER, GitgBlameRendererPrivate))
+
+/* Properties */
+enum
+{
+	PROP_0,
+	PROP_REVISION,
+	PROP_SHOW,
+	PROP_GROUP_START
+};
+
+struct _GitgBlameRendererPrivate
+{
+	GitgRevision *revision;
+	gint max_line;
+	gboolean group_start;
+	gboolean show;
+
+	gchar *line_number;
+
+	PangoLayout *cached_layout;
+	PangoLayout *line_layout;
+};
+
+G_DEFINE_TYPE (GitgBlameRenderer, gitg_blame_renderer, GTK_SOURCE_TYPE_GUTTER_RENDERER)
+
+static void
+gitg_blame_renderer_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+	GitgBlameRenderer *self = GITG_BLAME_RENDERER (object);
+	
+	switch (prop_id)
+	{
+		case PROP_REVISION:
+			self->priv->revision = g_value_get_boxed (value);
+		break;
+		case PROP_SHOW:
+			self->priv->show = g_value_get_boolean (value);
+		break;
+		case PROP_GROUP_START:
+			self->priv->group_start = g_value_get_boolean (value);
+		break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+gitg_blame_renderer_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+	GitgBlameRenderer *self = GITG_BLAME_RENDERER (object);
+	
+	switch (prop_id)
+	{
+		case PROP_REVISION:
+			g_value_set_boxed (value, self->priv->revision);
+		break;
+		case PROP_SHOW:
+			g_value_set_boolean (value, self->priv->show);
+		break;
+		case PROP_GROUP_START:
+			g_value_set_boolean (value, self->priv->group_start);
+		break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+gitg_blame_renderer_finalize (GObject *object)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (object);
+
+	g_free (br->priv->line_number);
+
+	G_OBJECT_CLASS (gitg_blame_renderer_parent_class)->finalize (object);
+}
+
+static void
+gitg_blame_renderer_begin (GtkSourceGutterRenderer      *renderer,
+                           cairo_t                      *cr,
+                           GdkRectangle                 *background_area,
+                           GdkRectangle                 *cell_area,
+                           GtkTextIter                  *start,
+                           GtkTextIter                  *end)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+
+	br->priv->cached_layout = gtk_widget_create_pango_layout (GTK_WIDGET (gtk_source_gutter_renderer_get_view (renderer)),
+	                                                          NULL);
+	br->priv->line_layout = gtk_widget_create_pango_layout (GTK_WIDGET (gtk_source_gutter_renderer_get_view (renderer)),
+	                                                        NULL);
+}
+
+static void
+render_blame (GtkSourceGutterRenderer      *renderer,
+              cairo_t                      *ctx,
+              GdkRectangle                 *background_area,
+              GdkRectangle                 *cell_area,
+              GtkTextIter                  *start,
+              GtkTextIter                  *end,
+              GtkSourceGutterRendererState  renderer_state)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+	gchar *text;
+	PangoLayout *layout;
+	GtkWidget *widget;
+	GtkStyleContext *style_context;
+	gchar *sha1;
+	gchar short_sha1[9];
+
+	widget = GTK_WIDGET (gtk_source_gutter_renderer_get_view (renderer));
+	layout = br->priv->cached_layout;
+
+	pango_layout_set_width (layout, -1);
+
+	sha1 = gitg_revision_get_sha1 (br->priv->revision);
+	strncpy (short_sha1, sha1, 8);
+	short_sha1[8] = '\0';
+	g_free (sha1);
+
+	text = g_strdup_printf ("<b>%s</b> %s", short_sha1,
+	                        gitg_revision_get_author (br->priv->revision));
+
+	pango_layout_set_markup (layout, text, -1);
+	g_free (text);
+	style_context = gtk_widget_get_style_context (widget);
+
+	if (br->priv->group_start)
+	{
+		GdkRGBA bg_color;
+
+		gtk_style_context_get_background_color (style_context, GTK_STATE_INSENSITIVE, &bg_color);
+		gdk_cairo_set_source_rgba (ctx, &bg_color);
+
+		cairo_save (ctx);
+		cairo_move_to (ctx, background_area->x, background_area->y);
+		cairo_line_to (ctx, background_area->x + background_area->width,
+		               cell_area->y);
+		cairo_stroke (ctx);
+		cairo_restore (ctx);
+	}
+
+	gtk_render_layout (style_context,
+	                   ctx,
+	                   cell_area->x,
+	                   cell_area->y,
+	                   layout);
+}
+
+static void
+render_line (GtkSourceGutterRenderer      *renderer,
+             cairo_t                      *ctx,
+             GdkRectangle                 *background_area,
+             GdkRectangle                 *cell_area,
+             GtkTextIter                  *start,
+             GtkTextIter                  *end,
+             GtkSourceGutterRendererState  renderer_state)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+	PangoLayout *layout;
+	GtkWidget *widget;
+	GtkStyleContext *style_context;
+	gint width, height;
+
+	widget = GTK_WIDGET (gtk_source_gutter_renderer_get_view (renderer));
+	layout = br->priv->line_layout;
+
+	pango_layout_set_markup (layout, br->priv->line_number, -1);
+	pango_layout_get_size (layout, &width, &height);
+
+	style_context = gtk_widget_get_style_context (widget);
+
+	width /= PANGO_SCALE;
+
+	gtk_render_layout (style_context,
+	                   ctx,
+	                   cell_area->x + cell_area->width - width - 5,
+	                   cell_area->y,
+	                   layout);
+}
+
+static void
+gitg_blame_renderer_draw (GtkSourceGutterRenderer      *renderer,
+                          cairo_t                      *ctx,
+                          GdkRectangle                 *background_area,
+                          GdkRectangle                 *cell_area,
+                          GtkTextIter                  *start,
+                          GtkTextIter                  *end,
+                          GtkSourceGutterRendererState  renderer_state)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+
+	/* Chain up to draw background */
+	GTK_SOURCE_GUTTER_RENDERER_CLASS (
+		gitg_blame_renderer_parent_class)->draw (renderer,
+		                                         ctx,
+		                                         background_area,
+		                                         cell_area,
+		                                         start,
+		                                         end,
+		                                         renderer_state);
+
+	if (br->priv->show && br->priv->revision != NULL)
+	{
+		render_blame (renderer,
+		              ctx,
+		              background_area,
+		              cell_area,
+		              start,
+		              end,
+		              renderer_state);
+	}
+
+	render_line (renderer,
+	             ctx,
+	             background_area,
+	             cell_area,
+	             start,
+	             end,
+	             renderer_state);
+}
+
+static void
+gitg_blame_renderer_end (GtkSourceGutterRenderer *renderer)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+
+	g_object_unref (br->priv->cached_layout);
+	br->priv->cached_layout = NULL;
+
+	g_object_unref (br->priv->line_layout);
+	br->priv->line_layout = NULL;
+}
+
+static void
+gutter_renderer_query_data (GtkSourceGutterRenderer      *renderer,
+                            GtkTextIter                  *start,
+                            GtkTextIter                  *end,
+                            GtkSourceGutterRendererState  state)
+{
+	GitgBlameRenderer *br = GITG_BLAME_RENDERER (renderer);
+	gchar *text;
+	gint line;
+	gboolean current_line;
+
+	line = gtk_text_iter_get_line (start) + 1;
+
+	current_line = (state & GTK_SOURCE_GUTTER_RENDERER_STATE_CURSOR) &&
+	                gtk_text_view_get_cursor_visible (gtk_source_gutter_renderer_get_view (renderer));
+
+	if (current_line)
+	{
+		text = g_strdup_printf ("<b>%d</b>", line);
+	}
+	else
+	{
+		text = g_strdup_printf ("%d", line);
+	}
+
+	g_free (br->priv->line_number);
+	br->priv->line_number = text;
+}
+
+static void
+measure_text (GitgBlameRenderer *br,
+              const gchar       *markup,
+              gint              *width)
+{
+	PangoLayout *layout;
+	gint w;
+	gint h;
+	GtkSourceGutterRenderer *r;
+	GtkTextView *view;
+
+	r = GTK_SOURCE_GUTTER_RENDERER (br);
+	view = gtk_source_gutter_renderer_get_view (r);
+
+	layout = gtk_widget_create_pango_layout (GTK_WIDGET (view), NULL);
+
+	pango_layout_set_markup (layout,
+	                         markup,
+	                         -1);
+
+	pango_layout_get_size (layout, &w, &h);
+
+	if (width)
+	{
+		*width = w / PANGO_SCALE;
+	}
+
+	g_object_unref (layout);
+}
+
+static GtkTextBuffer *
+get_buffer (GitgBlameRenderer *renderer)
+{
+	GtkTextView *view;
+
+	view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (renderer));
+
+	return gtk_text_view_get_buffer (view);
+}
+
+static void
+recalculate_size (GitgBlameRenderer *br)
+{
+	GtkTextBuffer *buffer;
+	gchar *markup;
+	gint size;
+	gchar *text;
+	gint num, num_digits, i;
+
+	buffer = get_buffer (br);
+
+	num = gtk_text_buffer_get_line_count (buffer);
+	num_digits = 0;
+
+	while (num > 0)
+	{
+		num /= 10;
+		++num_digits;
+	}
+
+	num_digits = MAX (num_digits, 2);
+
+	text = g_new (gchar, br->priv->max_line + num_digits + 1);
+	for (i = 0; i < br->priv->max_line + num_digits; i++)
+	{
+		text[i] = '0';
+	}
+	text[br->priv->max_line + num_digits] = '\0';
+
+	markup = g_strdup_printf ("<b>%s</b>", text);
+	g_free (text);
+
+	measure_text (br, markup, &size);
+	g_free (markup);
+
+	gtk_source_gutter_renderer_set_size (GTK_SOURCE_GUTTER_RENDERER (br),
+	                                     size);
+}
+
+static void
+update_num_digits (GitgBlameRenderer *renderer,
+                   gint               max_line)
+{
+	max_line = MAX (max_line, 2);
+
+	if (max_line != renderer->priv->max_line)
+	{
+		renderer->priv->max_line = max_line;
+		recalculate_size (renderer);
+	}
+}
+
+static void
+gitg_blame_renderer_class_init (GitgBlameRendererClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	GtkSourceGutterRendererClass *renderer_class = GTK_SOURCE_GUTTER_RENDERER_CLASS (klass);
+
+	renderer_class->begin = gitg_blame_renderer_begin;
+	renderer_class->draw = gitg_blame_renderer_draw;
+	renderer_class->end= gitg_blame_renderer_end;
+	renderer_class->query_data = gutter_renderer_query_data;
+
+	object_class->set_property = gitg_blame_renderer_set_property;
+	object_class->get_property = gitg_blame_renderer_get_property;
+	object_class->finalize = gitg_blame_renderer_finalize;
+
+	g_object_class_install_property (object_class,
+	                                 PROP_REVISION,
+	                                 g_param_spec_boxed ("revision",
+	                                                     "Revision",
+	                                                     "Revision",
+	                                                     GITG_TYPE_REVISION,
+	                                                     G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+	g_object_class_install_property (object_class,
+	                                 PROP_SHOW,
+	                                 g_param_spec_boolean ("show",
+	                                                       "Show",
+	                                                       "Show",
+	                                                       FALSE,
+	                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+	g_object_class_install_property (object_class,
+	                                 PROP_GROUP_START,
+	                                 g_param_spec_boolean ("group-start",
+	                                                       "Group Start",
+	                                                       "Group start",
+	                                                       FALSE,
+	                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+	g_type_class_add_private (object_class, sizeof (GitgBlameRendererPrivate));
+}
+
+static void
+gitg_blame_renderer_init (GitgBlameRenderer *self)
+{
+	self->priv = GITG_BLAME_RENDERER_GET_PRIVATE (self);
+}
+
+GitgBlameRenderer *
+gitg_blame_renderer_new (void)
+{
+	return g_object_new (GITG_TYPE_BLAME_RENDERER, NULL);
+}
+
+void
+gitg_blame_renderer_set_max_line_count (GitgBlameRenderer *renderer,
+                                        gint               max_line)
+{
+	g_return_if_fail (GITG_IS_BLAME_RENDERER (renderer));
+
+	update_num_digits (renderer, max_line);
+}
diff --git a/gitg/gitg-blame-renderer.h b/gitg/gitg-blame-renderer.h
new file mode 100644
index 0000000..412ac90
--- /dev/null
+++ b/gitg/gitg-blame-renderer.h
@@ -0,0 +1,61 @@
+/*
+ * gitg-blame-renderer.h
+ * This file is part of gitg - git repository viewer
+ *
+ * Copyright (C) 2011 - Ignacio Casal Quinteiro
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GITG_BLAME_RENDERER_H__
+#define __GITG_BLAME_RENDERER_H__
+
+#include <gtksourceview/gtksourceview.h>
+
+G_BEGIN_DECLS
+
+#define GITG_TYPE_BLAME_RENDERER		(gitg_blame_renderer_get_type ())
+#define GITG_BLAME_RENDERER(obj)		(G_TYPE_CHECK_INSTANCE_CAST ((obj), GITG_TYPE_BLAME_RENDERER, GitgBlameRenderer))
+#define GITG_BLAME_RENDERER_CONST(obj)		(G_TYPE_CHECK_INSTANCE_CAST ((obj), GITG_TYPE_BLAME_RENDERER, GitgBlameRenderer const))
+#define GITG_BLAME_RENDERER_CLASS(klass)	(G_TYPE_CHECK_CLASS_CAST ((klass), GITG_TYPE_BLAME_RENDERER, GitgBlameRendererClass))
+#define GITG_IS_BLAME_RENDERER(obj)		(G_TYPE_CHECK_INSTANCE_TYPE ((obj), GITG_TYPE_BLAME_RENDERER))
+#define GITG_IS_BLAME_RENDERER_CLASS(klass)	(G_TYPE_CHECK_CLASS_TYPE ((klass), GITG_TYPE_BLAME_RENDERER))
+#define GITG_BLAME_RENDERER_GET_CLASS(obj)	(G_TYPE_INSTANCE_GET_CLASS ((obj), GITG_TYPE_BLAME_RENDERER, GitgBlameRendererClass))
+
+typedef struct _GitgBlameRenderer		GitgBlameRenderer;
+typedef struct _GitgBlameRendererClass		GitgBlameRendererClass;
+typedef struct _GitgBlameRendererPrivate	GitgBlameRendererPrivate;
+
+struct _GitgBlameRenderer {
+	GtkSourceGutterRenderer parent;
+	
+	GitgBlameRendererPrivate *priv;
+};
+
+struct _GitgBlameRendererClass {
+	GtkSourceGutterRendererClass parent_class;
+};
+
+GType                    gitg_blame_renderer_get_type                (void) G_GNUC_CONST;
+
+GitgBlameRenderer       *gitg_blame_renderer_new                     (void);
+
+void                     gitg_blame_renderer_set_max_line_count      (GitgBlameRenderer *renderer,
+                                                                      gint               max_line_count);
+
+G_END_DECLS
+
+#endif /* __GITG_BLAME_RENDERER_H__ */
diff --git a/gitg/gitg-revision-files-panel.c b/gitg/gitg-revision-files-panel.c
index 8cee3fe..ec698a3 100644
--- a/gitg/gitg-revision-files-panel.c
+++ b/gitg/gitg-revision-files-panel.c
@@ -33,11 +33,14 @@
 #include "gitg-utils.h"
 #include "gitg-revision-panel.h"
 #include "gitg-dirs.h"
+#include "gitg-blame-renderer.h"
 
 #define GITG_REVISION_FILES_VIEW_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE((object), GITG_TYPE_REVISION_FILES_VIEW, GitgRevisionFilesViewPrivate))
 
 #define GITG_REVISION_FILES_PANEL_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE((object), GITG_TYPE_REVISION_FILES_PANEL, GitgRevisionFilesPanelPrivate))
 
+#define BLAME_DATA "GitgRevisionFilesPanelBlame"
+
 enum
 {
 	ICON_COLUMN,
@@ -52,11 +55,27 @@ typedef struct _GitgRevisionFilesViewPrivate GitgRevisionFilesViewPrivate;
 
 struct _GitgRevisionFilesViewPrivate
 {
+	GtkNotebook *notebook_files;
+
 	GtkTreeView *tree_view;
+
 	GtkSourceView *contents;
 	GitgShell *content_shell;
 	GtkTreeStore *store;
 
+	GtkSourceView *blame_view;
+	GitgShell *blame_shell;
+	GHashTable *blames; /* hash : tag */
+	GitgBlameRenderer *blame_renderer;
+	gint blame_offset;
+	GtkTextTag *active_tag;
+	gboolean blame_active;
+
+	glong query_data_id;
+	glong query_tooltip_id;
+
+	GtkWidget *active_view;
+
 	gchar *drag_dir;
 	gchar **drag_files;
 
@@ -130,6 +149,14 @@ gitg_revision_files_view_finalize (GObject *object)
 	gitg_io_cancel (GITG_IO (self->priv->loader));
 	g_object_unref (self->priv->loader);
 
+	gitg_io_cancel (GITG_IO (self->priv->content_shell));
+	g_object_unref (self->priv->content_shell);
+
+	g_hash_table_unref (self->priv->blames);
+
+	gitg_io_cancel (GITG_IO (self->priv->blame_shell));
+	g_object_unref (self->priv->blame_shell);
+
 	G_OBJECT_CLASS (gitg_revision_files_view_parent_class)->finalize (object);
 }
 
@@ -187,10 +214,195 @@ set_revision (GitgRevisionFilesView *files_view,
 	}
 }
 
+static GtkTextTag *
+get_blame_tag_from_iter (GitgRevisionFilesView *files_view,
+                         GtkTextIter           *iter)
+{
+	GtkTextTag *tag = NULL;
+	GSList *tags, *l;
+
+	tags = gtk_text_iter_get_tags (iter);
+
+	for (l = tags; l != NULL; l = g_slist_next (l))
+	{
+		if (g_object_get_data (l->data, BLAME_DATA) != NULL)
+		{
+			tag = l->data;
+			break;
+		}
+	}
+
+	g_slist_free (tags);
+
+	return tag;
+}
+
+static void
+blame_renderer_query_data_cb (GtkSourceGutterRenderer      *renderer,
+                              GtkTextIter                  *start,
+                              GtkTextIter                  *end,
+                              GtkSourceGutterRendererState  state,
+                              GitgRevisionFilesView        *files_view)
+{
+	GtkTextTag *tag;
+	GitgRevision *rev;
+	GtkTextIter iter;
+
+	iter = *start;
+	gtk_text_iter_set_line_offset (&iter, 0);
+
+	tag = get_blame_tag_from_iter (files_view, &iter);
+
+	if (tag == NULL)
+		return;
+
+	rev = g_object_get_data (G_OBJECT (tag), BLAME_DATA);
+
+	if (gtk_text_iter_begins_tag (&iter, tag))
+	{
+		gboolean group_start = FALSE;
+
+		if (!gtk_text_iter_is_start (&iter))
+		{
+			group_start = TRUE;
+		}
+
+		g_object_set (renderer,
+		              "revision", rev,
+		              "show", TRUE,
+		              "group-start", group_start,
+		              NULL);
+	}
+	else
+	{
+		g_object_set (renderer,
+		              "revision", rev,
+		              "show", FALSE,
+		              "group-start", FALSE,
+		              NULL);
+	}
+}
+
+static gboolean
+blame_renderer_query_tooltip_cb (GtkSourceGutterRenderer *renderer,
+                                 GtkTextIter             *iter,
+                                 GdkRectangle            *area,
+                                 gint                     x,
+                                 gint                     y,
+                                 GtkTooltip              *tooltip,
+                                 GitgRevisionFilesView   *tree)
+{
+	GtkTextTag *tag;
+	GitgRevision *rev;
+	const gchar *author;
+	const gchar *committer;
+	const gchar *subject;
+	gchar *text;
+	gchar *sha1;
+	gchar *author_date;
+	gchar *committer_date;
+
+	tag = get_blame_tag_from_iter (tree, iter);
+
+	if (tag == NULL)
+		return FALSE;
+
+	rev = g_object_get_data (G_OBJECT (tag), BLAME_DATA);
+
+	if (rev == NULL)
+		return FALSE;
+
+	sha1 = gitg_revision_get_sha1 (rev);
+	author_date = gitg_revision_get_author_date_for_display (rev);
+	committer_date = gitg_revision_get_committer_date_for_display (rev);
+
+	author = _("Author");
+	committer = _("Committer");
+	subject = _("Subject");
+
+	text = g_markup_printf_escaped ("<b>SHA:</b> %s\n"
+	                                "<b>%s:</b> %s &lt;%s&gt; (%s)\n"
+	                                "<b>%s:</b> %s &lt;%s&gt; (%s)\n"
+	                                "<b>%s:</b> <i>%s</i>",
+	                                sha1,
+	                                author, gitg_revision_get_author (rev),
+	                                gitg_revision_get_author_email (rev),
+	                                author_date,
+	                                committer, gitg_revision_get_committer (rev),
+	                                gitg_revision_get_committer_email (rev),
+	                                committer_date,
+	                                subject, gitg_revision_get_subject (rev));
+
+	g_free (sha1);
+	g_free (author_date);
+	g_free (committer_date);
+	gtk_tooltip_set_markup (tooltip, text);
+	g_free (text);
+
+	return TRUE;
+}
+
+static void
+remove_blames (GitgRevisionFilesView *tree)
+{
+	GtkTextBuffer *buffer;
+	GList *tags, *tag;
+	GtkTextTagTable *table;
+
+	tree->priv->blame_active = FALSE;
+
+	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tree->priv->blame_view));
+	table = gtk_text_buffer_get_tag_table (buffer);
+
+	if (tree->priv->query_data_id != 0)
+	{
+		g_signal_handler_disconnect (tree->priv->blame_renderer,
+		                             tree->priv->query_data_id);
+		tree->priv->query_data_id = 0;
+	}
+
+	if (tree->priv->query_tooltip_id != 0)
+	{
+		g_signal_handler_disconnect (tree->priv->blame_renderer,
+		                             tree->priv->query_tooltip_id);
+		tree->priv->query_tooltip_id = 0;
+	}
+
+	tags = g_hash_table_get_values (tree->priv->blames);
+
+	for (tag = tags; tag != NULL; tag = g_list_next (tag))
+	{
+		g_object_set_data (tag->data, BLAME_DATA, NULL);
+		gtk_text_tag_table_remove (table, tag->data);
+	}
+
+	g_hash_table_remove_all (tree->priv->blames);
+
+	g_list_free (tags);
+
+	tree->priv->blame_offset = 0;
+	tree->priv->active_tag = NULL;
+
+	g_object_set (tree->priv->blame_renderer,
+	              "revision", NULL,
+	              "show", FALSE,
+	              "group-start", FALSE,
+	              NULL);
+
+	gitg_blame_renderer_set_max_line_count (tree->priv->blame_renderer, 0);
+}
+
 static void
 gitg_revision_files_view_dispose (GObject *object)
 {
-	set_revision (GITG_REVISION_FILES_VIEW (object), NULL, NULL);
+	GitgRevisionFilesView* files_view = GITG_REVISION_FILES_VIEW (object);
+
+	set_revision (files_view, NULL, NULL);
+
+	if (files_view->priv->blame_active)
+	{
+		remove_blames (files_view);
+	}
 
 	G_OBJECT_CLASS (gitg_revision_files_view_parent_class)->dispose (object);
 }
@@ -246,31 +458,43 @@ on_row_expanded (GtkTreeView          *files_view,
 }
 
 static void
-show_binary_information (GitgRevisionFilesView *tree)
+show_binary_information (GitgRevisionFilesView *tree,
+                         GtkTextBuffer         *buffer)
 {
-	GtkTextBuffer *buffer;
-
-	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(tree->priv->contents));
-
 	gtk_text_buffer_set_text (buffer,
-	                          _ ("Cannot display file content as text"),
+	                          _("Cannot display file content as text"),
 	                          -1);
 
-	gtk_source_buffer_set_language (GTK_SOURCE_BUFFER(buffer), NULL);
+	gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), NULL);
 }
 
 static void
-on_selection_changed (GtkTreeSelection     *selection,
+on_selection_changed (GtkTreeSelection      *selection,
                       GitgRevisionFilesView *tree)
 {
 	GtkTextBuffer *buffer;
-
-	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tree->priv->contents));
-
+	gboolean blame_mode;
 	GtkTreeModel *model;
 	GtkTreeIter iter;
+	GtkTreePath *path = NULL;
+	GList *rows;
+	gchar *name;
+	gchar *content_type;
 
-	gitg_io_cancel (GITG_IO (tree->priv->content_shell));
+	if (tree->priv->active_view == GTK_WIDGET (tree->priv->contents))
+	{
+		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tree->priv->contents));
+		gitg_io_cancel (GITG_IO (tree->priv->content_shell));
+		blame_mode = FALSE;
+	}
+	else
+	{
+		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tree->priv->blame_view));
+		blame_mode = TRUE;
+		gitg_io_cancel (GITG_IO (tree->priv->blame_shell));
+
+		remove_blames (tree);
+	}
 
 	gtk_text_buffer_set_text (buffer, "", -1);
 
@@ -279,8 +503,7 @@ on_selection_changed (GtkTreeSelection     *selection,
 		return;
 	}
 
-	GList *rows = gtk_tree_selection_get_selected_rows (selection, &model);
-	GtkTreePath *path = NULL;
+	rows = gtk_tree_selection_get_selected_rows (selection, &model);
 
 	if (g_list_length (rows) == 1)
 	{
@@ -294,8 +517,6 @@ on_selection_changed (GtkTreeSelection     *selection,
 		return;
 	}
 
-	gchar *name;
-	gchar *content_type;
 	gtk_tree_model_get_iter (model, &iter, path);
 	gtk_tree_path_free (path);
 	gtk_tree_model_get (model,
@@ -308,30 +529,57 @@ on_selection_changed (GtkTreeSelection     *selection,
 
 	if (!content_type)
 	{
+		g_free (name);
 		return;
 	}
 
 	if (!gitg_utils_can_display_content_type (content_type))
 	{
-		show_binary_information (tree);
+		show_binary_information (tree, buffer);
 	}
 	else
 	{
 		GtkSourceLanguage *language;
+		gchar *id;
 
 		language = gitg_utils_get_language (name, content_type);
 		gtk_source_buffer_set_language (GTK_SOURCE_BUFFER(buffer),
 		                                language);
 
-		gchar *id = node_identity (tree, &iter);
+		id = node_identity (tree, &iter);
 
-		gitg_shell_run (tree->priv->content_shell,
-		                gitg_command_new (tree->priv->repository,
-		                                   "show",
-		                                   "--encoding=UTF-8",
-		                                   id,
-		                                   NULL),
-		                NULL);
+		if (!blame_mode)
+		{
+			gitg_shell_run (tree->priv->content_shell,
+			                gitg_command_new (tree->priv->repository,
+			                                  "show",
+			                                  "--encoding=UTF-8",
+			                                  id,
+			                                  NULL),
+			                NULL);
+		}
+		else
+		{
+			gchar **hash_name;
+
+			gitg_blame_renderer_set_max_line_count (tree->priv->blame_renderer, 0);
+			tree->priv->blame_active = TRUE;
+
+			hash_name = g_strsplit (id, ":", 2);
+			gitg_shell_run (tree->priv->blame_shell,
+			                gitg_command_new (tree->priv->repository,
+			                                  "blame",
+			                                  "--encoding=UTF-8",
+			                                  "--root",
+			                                  "-l",
+			                                  hash_name[0],
+			                                  "--",
+			                                  hash_name[1],
+			                                  NULL),
+			                NULL);
+
+			g_strfreev (hash_name);
+		}
 
 		g_free (id);
 	}
@@ -510,9 +758,25 @@ on_drag_end (GtkWidget            *widget,
 }
 
 static void
+on_notebook_files_switch_page (GtkNotebook           *notebook,
+                               GtkWidget             *page,
+                               guint                  page_num,
+                               GitgRevisionFilesView *files_view)
+{
+	GtkTreeSelection *selection;
+
+	files_view->priv->active_view = gtk_bin_get_child (GTK_BIN (page));
+
+	selection = gtk_tree_view_get_selection (files_view->priv->tree_view);
+	on_selection_changed (selection, files_view);
+}
+
+static void
 gitg_revision_files_view_parser_finished (GtkBuildable *buildable,
-                                         GtkBuilder   *builder)
+                                          GtkBuilder   *builder)
 {
+	GtkSourceGutter *gutter;
+
 	if (parent_iface.parser_finished)
 	{
 		parent_iface.parser_finished (buildable, builder);
@@ -522,10 +786,26 @@ gitg_revision_files_view_parser_finished (GtkBuildable *buildable,
 	GitgRevisionFilesView *files_view = GITG_REVISION_FILES_VIEW(buildable);
 	files_view->priv->tree_view = GTK_TREE_VIEW (gtk_builder_get_object (builder,
 	                                            "revision_files"));
+	files_view->priv->notebook_files = GTK_NOTEBOOK (gtk_builder_get_object (builder,
+	                                                 "notebook_files"));
 	files_view->priv->contents = GTK_SOURCE_VIEW (gtk_builder_get_object (builder,
 	                                             "revision_files_contents"));
+	files_view->priv->blame_view = GTK_SOURCE_VIEW (gtk_builder_get_object (builder,
+	                                                "revision_blame_view"));
+	files_view->priv->blame_renderer = gitg_blame_renderer_new ();
+
+	gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (files_view->priv->blame_view),
+	                                     GTK_TEXT_WINDOW_LEFT);
+
+	gtk_source_gutter_insert (gutter,
+	                          GTK_SOURCE_GUTTER_RENDERER (files_view->priv->blame_renderer),
+	                          0);
+
+	/* set the active view */
+	files_view->priv->active_view = GTK_WIDGET (files_view->priv->contents);
 
 	gitg_utils_set_monospace_font (GTK_WIDGET(files_view->priv->contents));
+	gitg_utils_set_monospace_font (GTK_WIDGET(files_view->priv->blame_view));
 	gtk_tree_view_set_model (files_view->priv->tree_view,
 	                         GTK_TREE_MODEL(files_view->priv->store));
 
@@ -570,6 +850,11 @@ gitg_revision_files_view_parser_finished (GtkBuildable *buildable,
 	                  "changed",
 	                  G_CALLBACK(on_selection_changed),
 	                  files_view);
+
+	g_signal_connect (files_view->priv->notebook_files,
+	                  "switch-page",
+	                  G_CALLBACK (on_notebook_files_switch_page),
+	                  files_view);
 }
 
 static void
@@ -940,7 +1225,7 @@ on_contents_update (GitgShell              *shell,
 		if (content_type && !gitg_utils_can_display_content_type (content_type))
 		{
 			gitg_io_cancel (GITG_IO (shell));
-			show_binary_information (tree);
+			show_binary_information (tree, buf);
 		}
 		else
 		{
@@ -955,6 +1240,185 @@ on_contents_update (GitgShell              *shell,
 	}
 }
 
+/**
+ * parse_blame:
+ * @line: the line to be parsed
+ * @real_line: the real connect to be inserted in the text buffer
+ * @sha: the sha get from @line
+ * @size: the size for the gutter
+ */
+static void
+parse_blame (const gchar  *line,
+             const gchar **real_line,
+             gchar       **sha,
+             gint         *size)
+{
+	gunichar c;
+	const gchar *name_end;
+
+	*sha = g_strndup (line, GITG_HASH_SHA_SIZE);
+	line += GITG_HASH_SHA_SIZE;
+	*real_line = strstr (line, ") ");
+	*real_line += 2;
+
+	line = strstr (line, " (");
+	line += 2;
+	name_end = line;
+	c = g_utf8_get_char (name_end);
+
+	while (!g_unichar_isdigit (c))
+	{
+		name_end = g_utf8_next_char (name_end);
+		c = g_utf8_get_char (name_end);
+	}
+	name_end--;
+
+	*size = name_end - line + 15; /* hash name + extra space */
+}
+
+static GtkTextTag *
+get_tag_for_sha (GitgRevisionFilesView  *tree,
+                 GtkTextBuffer          *buffer,
+                 const gchar            *sha)
+{
+	GtkTextTag *tag;
+
+	tag = g_hash_table_lookup (tree->priv->blames, sha);
+
+	if (tag == NULL)
+	{
+		GitgHash hash;
+		GitgRevision *revision;
+
+		tag = gtk_text_buffer_create_tag (buffer, NULL, NULL);
+
+		gitg_hash_sha1_to_hash (sha, hash);
+		revision = gitg_repository_lookup (tree->priv->repository,
+		                                   hash);
+		g_object_set_data (G_OBJECT (tag), BLAME_DATA, revision);
+
+		g_hash_table_insert (tree->priv->blames, g_strdup (sha),
+		                     g_object_ref (tag));
+	}
+
+	return tag;
+}
+
+static void
+apply_active_blame_tag (GitgRevisionFilesView *tree,
+                        GtkTextBuffer         *buffer,
+                        GtkTextIter           *end)
+{
+	GtkTextIter start;
+
+	gtk_text_buffer_get_iter_at_offset (buffer, &start,
+	                                    tree->priv->blame_offset);
+	gtk_text_buffer_apply_tag (buffer, tree->priv->active_tag,
+	                           &start, end);
+
+	tree->priv->blame_offset = gtk_text_iter_get_offset (end);
+}
+
+static void
+insert_blame_line (GitgRevisionFilesView  *tree,
+                   GtkTextBuffer          *buffer,
+                   const gchar            *line,
+                   GtkTextIter            *iter)
+{
+	GtkTextTag *tag;
+	const gchar *real_line;
+	gint size;
+	gchar *sha;
+
+	parse_blame (line, &real_line, &sha, &size);
+	gitg_blame_renderer_set_max_line_count (tree->priv->blame_renderer, size);
+
+	tag = get_tag_for_sha (tree, buffer, sha);
+
+	if (tag != tree->priv->active_tag)
+	{
+		if (tree->priv->active_tag != NULL)
+		{
+			apply_active_blame_tag (tree, buffer, iter);
+		}
+
+		tree->priv->active_tag = tag;
+	}
+
+	g_free (sha);
+
+	gtk_text_buffer_insert (buffer, iter, real_line, -1);
+	gtk_text_buffer_insert (buffer, iter, "\n", -1);
+}
+
+static void
+on_blame_update (GitgShell              *shell,
+                 gchar                 **buffer,
+                 GitgRevisionFilesView  *tree)
+{
+	GtkTextBuffer *buf;
+	GtkTextIter end;
+	gchar *line;
+
+	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tree->priv->blame_view));
+	gtk_text_buffer_get_end_iter (buf, &end);
+
+	while ((line = *buffer++))
+	{
+		insert_blame_line (tree, buf, line, &end);
+	}
+
+	if (tree->priv->active_tag != NULL)
+	{
+		apply_active_blame_tag (tree, buf, &end);
+	}
+
+	if (gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buf)) == NULL)
+	{
+		gchar *content_type = gitg_utils_guess_content_type (buf);
+
+		if (content_type && !gitg_utils_can_display_content_type (content_type))
+		{
+			gitg_io_cancel (GITG_IO (shell));
+			show_binary_information (tree, buf);
+		}
+		else
+		{
+			GtkSourceLanguage *language;
+
+			language = gitg_utils_get_language (NULL, content_type);
+			gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buf),
+			                                language);
+		}
+
+		g_free (content_type);
+	}
+}
+
+static void
+on_blame_end (GitgShell              *shell,
+              GError                 *error,
+              GitgRevisionFilesView  *tree)
+{
+	GtkSourceGutter *gutter;
+
+	tree->priv->query_data_id =
+		g_signal_connect (tree->priv->blame_renderer,
+		                  "query-data",
+		                  G_CALLBACK (blame_renderer_query_data_cb),
+		                  tree);
+
+	tree->priv->query_tooltip_id =
+		g_signal_connect (tree->priv->blame_renderer,
+		                  "query-tooltip",
+		                  G_CALLBACK (blame_renderer_query_tooltip_cb),
+		                  tree);
+
+	gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (tree->priv->blame_view),
+	                                     GTK_TEXT_WINDOW_LEFT);
+	gtk_source_gutter_queue_draw (gutter);
+}
+
 static void
 gitg_revision_files_view_init (GitgRevisionFilesView *self)
 {
@@ -985,6 +1449,21 @@ gitg_revision_files_view_init (GitgRevisionFilesView *self)
 	                  "update",
 	                  G_CALLBACK (on_contents_update),
 	                  self);
+
+	self->priv->blames = g_hash_table_new_full (g_str_hash,
+	                                            g_str_equal,
+	                                            g_free,
+	                                            NULL);
+
+	self->priv->blame_shell = gitg_shell_new (5000);
+	g_signal_connect (self->priv->blame_shell,
+	                  "update",
+	                  G_CALLBACK (on_blame_update),
+	                  self);
+	g_signal_connect (self->priv->blame_shell,
+	                  "end",
+	                  G_CALLBACK (on_blame_end),
+	                  self);
 }
 
 static void
diff --git a/gitg/gitg-revision-files-panel.ui b/gitg/gitg-revision-files-panel.ui
index acdbc8a..57b6361 100644
--- a/gitg/gitg-revision-files-panel.ui
+++ b/gitg/gitg-revision-files-panel.ui
@@ -1,6 +1,7 @@
 <?xml version="1.0"?>
 <interface>
-  <object class="GtkSourceBuffer" id="source_buffer"></object>
+  <object class="GtkSourceBuffer" id="source_buffer_source"></object>
+  <object class="GtkSourceBuffer" id="source_buffer_blame"></object>
   <object class="GitgRevisionFilesView" id="revision_files_view">
     <property name="visible">True</property>
     <property name="can_focus">True</property>
@@ -43,17 +44,46 @@
       </packing>
     </child>
     <child>
-      <object class="GtkScrolledWindow" id="scrolled_window_files_contents">
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="hscrollbar_policy">automatic</property>
-        <property name="vscrollbar_policy">automatic</property>
-        <property name="shadow_type">etched-in</property>
+      <object class="GtkNotebook" id="notebook_files">
+        <child>
+          <object class="GtkScrolledWindow" id="scrolled_window_files_contents">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hscrollbar_policy">automatic</property>
+            <property name="vscrollbar_policy">automatic</property>
+            <property name="shadow_type">etched-in</property>
+            <child>
+              <object class="GtkSourceView" id="revision_files_contents">
+                <property name="buffer">source_buffer_source</property>
+                <property name="editable">False</property>
+                <property name="show_line_numbers">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="tab">
+          <object class="GtkLabel" id="label_source">
+            <property name="label">Source</property>
+          </object>
+        </child>
         <child>
-          <object class="GtkSourceView" id="revision_files_contents">
-            <property name="buffer">source_buffer</property>
-            <property name="editable">False</property>
-            <property name="show_line_numbers">True</property>
+          <object class="GtkScrolledWindow" id="scrolled_window_blame">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hscrollbar_policy">automatic</property>
+            <property name="vscrollbar_policy">automatic</property>
+            <property name="shadow_type">etched-in</property>
+            <child>
+              <object class="GtkSourceView" id="revision_blame_view">
+                <property name="buffer">source_buffer_blame</property>
+                <property name="editable">False</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="tab">
+          <object class="GtkLabel" id="label_blame">
+            <property name="label">Blame</property>
           </object>
         </child>
       </object>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]