1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007 Imendio AB
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of the
8  * License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public
16  * License along with this program; if not, write to the
17  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20 
21 #include <config.h>
22 #include <math.h>
23 #include <gtk/gtk.h>
24 
25 #include "giggle-graph-renderer.h"
26 #include "git-revision.h"
27 
28 #define GET_PRIV(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), GIGGLE_TYPE_GRAPH_RENDERER, GiggleGraphRendererPrivate))
29 
30 /* included padding */
31 #define PATH_SPACE(font_size) (font_size + 3)
32 #define DOT_RADIUS(font_size) (font_size / 2)
33 #define LINE_WIDTH(font_size) ((font_size / 6) << 1) /* we want the closest even number <= size/3 */
34 #define NEXT_COLOR(n_color)   ((n_color % (G_N_ELEMENTS (colors) - 1)) + 1)
35 #define INVALID_COLOR         0
36 
37 typedef struct GiggleGraphRendererPrivate GiggleGraphRendererPrivate;
38 
39 struct GiggleGraphRendererPrivate {
40 	gint            n_paths;
41 	GHashTable     *paths_info;
42 	GitRevision *revision;
43 };
44 
45 typedef struct GiggleGraphRendererPathState GiggleGraphRendererPathState;
46 
47 struct GiggleGraphRendererPathState {
48 	gushort upper_n_color : 8;
49 	gushort lower_n_color : 8;
50 	gushort n_path;
51 };
52 
53 enum {
54 	PROP_0,
55 	PROP_REVISION,
56 };
57 
58 static GdkColor colors[] = {
59 	/* invalid color */
60 	{ 0x0, 0x0000, 0x0000, 0x0000 },
61 	/* light palette */
62 	{ 0x0, 0xfc00, 0xe900, 0x4f00 }, /* butter */
63 	{ 0x0, 0xfc00, 0xaf00, 0x3e00 }, /* orange */
64 	{ 0x0, 0xe900, 0xb900, 0x6e00 }, /* chocolate */
65 	{ 0x0, 0x8a00, 0xe200, 0x3400 }, /* chameleon */
66 	{ 0x0, 0x7200, 0x9f00, 0xcf00 }, /* sky blue */
67 	{ 0x0, 0xad00, 0x7f00, 0xa800 }, /* plum */
68 	{ 0x0, 0xef00, 0x2900, 0x2900 }, /* scarlet red */
69 #if 0
70 	{ 0x0, 0xee00, 0xee00, 0xec00 }, /* aluminium */
71 #endif
72 	{ 0x0, 0x8800, 0x8a00, 0x8500 }, /* no name grey */
73 	/* medium palette */
74 	{ 0x0, 0xed00, 0xd400, 0x0000 }, /* butter */
75 	{ 0x0, 0xf500, 0x7900, 0x0000 }, /* orange */
76 	{ 0x0, 0xc100, 0x7d00, 0x1100 }, /* chocolate */
77 	{ 0x0, 0x7300, 0xd200, 0x1600 }, /* chameleon */
78 	{ 0x0, 0x3400, 0x6500, 0xa400 }, /* sky blue */
79 	{ 0x0, 0x7500, 0x5000, 0x7b00 }, /* plum */
80 	{ 0x0, 0xcc00, 0x0000, 0x0000 }, /* scarlet red */
81 #if 0
82 	{ 0x0, 0xd300, 0xd700, 0xcf00 }, /* aluminium */
83 #endif
84 	{ 0x0, 0x5500, 0x5700, 0x5300 }, /* no name grey */
85 	/* dark palette */
86 	{ 0x0, 0xc400, 0xa000, 0x0000 }, /* butter */
87 	{ 0x0, 0xce00, 0x5c00, 0x0000 }, /* orange */
88 	{ 0x0, 0x8f00, 0x5900, 0x0200 }, /* chocolate */
89 	{ 0x0, 0x4e00, 0x9a00, 0x0600 }, /* chameleon */
90 	{ 0x0, 0x2000, 0x4a00, 0x8700 }, /* sky blue */
91 	{ 0x0, 0x5c00, 0x3500, 0x6600 }, /* plum */
92 	{ 0x0, 0xa400, 0x0000, 0x0000 }, /* scarlet red */
93 #if 0
94 	{ 0x0, 0xba00, 0xbd00, 0xb600 }, /* aluminium */
95 #endif
96 	{ 0x0, 0x2e00, 0x3400, 0x3600 }, /* no name grey */
97 };
98 
99 static GQuark revision_paths_state_quark;
100 
101 static void giggle_graph_renderer_finalize     (GObject         *object);
102 static void giggle_graph_renderer_get_property (GObject         *object,
103 						guint            param_id,
104 						GValue          *value,
105 						GParamSpec      *pspec);
106 static void giggle_graph_renderer_set_property (GObject         *object,
107 						guint            param_id,
108 						const GValue    *value,
109 						GParamSpec      *pspec);
110 static void giggle_graph_renderer_get_size     (GtkCellRenderer *cell,
111 						GtkWidget       *widget,
112 						const GdkRectangle    *cell_area,
113 						gint            *x_offset,
114 						gint            *y_offset,
115 						gint            *width,
116 						gint            *height);
117 static void giggle_graph_renderer_render       (GtkCellRenderer *cell,
118 						cairo_t         *cr,
119 						GtkWidget       *widget,
120 						const GdkRectangle    *background_area,
121 						const GdkRectangle    *cell_area,
122 						GtkCellRendererState   flags);
123 
G_DEFINE_TYPE(GiggleGraphRenderer,giggle_graph_renderer,GTK_TYPE_CELL_RENDERER)124 G_DEFINE_TYPE (GiggleGraphRenderer, giggle_graph_renderer, GTK_TYPE_CELL_RENDERER)
125 
126 static void
127 giggle_graph_renderer_class_init (GiggleGraphRendererClass *class)
128 {
129 	GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (class);
130 	GObjectClass         *object_class = G_OBJECT_CLASS (class);
131 
132 	cell_class->get_size = giggle_graph_renderer_get_size;
133 	cell_class->render = giggle_graph_renderer_render;
134 
135 	object_class->finalize = giggle_graph_renderer_finalize;
136 	object_class->set_property = giggle_graph_renderer_set_property;
137 	object_class->get_property = giggle_graph_renderer_get_property;
138 
139 	g_object_class_install_property (
140 		object_class,
141 		PROP_REVISION,
142 		g_param_spec_object ("revision",
143 				     "revision",
144 				     "revision",
145 				     GIT_TYPE_REVISION,
146 				     G_PARAM_READWRITE));
147 
148 	g_type_class_add_private (object_class,
149 				  sizeof (GiggleGraphRendererPrivate));
150 
151 	revision_paths_state_quark = g_quark_from_static_string ("giggle-revision-paths-state");
152 }
153 
154 static void
giggle_graph_renderer_init(GiggleGraphRenderer * instance)155 giggle_graph_renderer_init (GiggleGraphRenderer *instance)
156 {
157 	instance->_priv = GET_PRIV (instance);
158 }
159 
160 static void
giggle_graph_renderer_finalize(GObject * object)161 giggle_graph_renderer_finalize (GObject *object)
162 {
163 	GiggleGraphRendererPrivate *priv;
164 
165 	priv = GET_PRIV (object);
166 
167 	if (priv->paths_info) {
168 		g_hash_table_destroy (priv->paths_info);
169 	}
170 
171 	G_OBJECT_CLASS (giggle_graph_renderer_parent_class)->finalize (object);
172 }
173 
174 static void
giggle_graph_renderer_get_property(GObject * object,guint param_id,GValue * value,GParamSpec * pspec)175 giggle_graph_renderer_get_property (GObject    *object,
176 				    guint       param_id,
177 				    GValue     *value,
178 				    GParamSpec *pspec)
179 {
180 	GiggleGraphRendererPrivate *priv = GIGGLE_GRAPH_RENDERER (object)->_priv;
181 
182 	switch (param_id) {
183 	case PROP_REVISION:
184 		g_value_set_object (value, priv->revision);
185 		break;
186 	default:
187 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
188 	}
189 }
190 
191 static void
giggle_graph_renderer_set_property(GObject * object,guint param_id,const GValue * value,GParamSpec * pspec)192 giggle_graph_renderer_set_property (GObject      *object,
193 				    guint         param_id,
194 				    const GValue *value,
195 				    GParamSpec   *pspec)
196 {
197 	GiggleGraphRendererPrivate *priv = GIGGLE_GRAPH_RENDERER (object)->_priv;
198 
199 	switch (param_id) {
200 	case PROP_REVISION:
201 		if (priv->revision) {
202 			g_object_unref (priv->revision);
203 		}
204 		priv->revision = GIT_REVISION (g_value_dup_object (value));
205 		break;
206 	default:
207 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
208 	}
209 }
210 
211 static void
giggle_graph_renderer_get_size(GtkCellRenderer * cell,GtkWidget * widget,const GdkRectangle * cell_area,gint * x_offset,gint * y_offset,gint * width,gint * height)212 giggle_graph_renderer_get_size (GtkCellRenderer *cell,
213 				GtkWidget       *widget,
214 				const GdkRectangle    *cell_area,
215 				gint            *x_offset,
216 				gint            *y_offset,
217 				gint            *width,
218 				gint            *height)
219 {
220 	GiggleGraphRendererPrivate *priv;
221 	gint size;
222 
223 	priv = GIGGLE_GRAPH_RENDERER (cell)->_priv;
224 	size = PANGO_PIXELS (pango_font_description_get_size (gtk_widget_get_style (widget)->font_desc));
225 
226 	if (height) {
227 		*height = PATH_SPACE (size);
228 	}
229 
230 	if (width) {
231 		/* the +1 is because we leave half at each side */
232 		*width = PATH_SPACE (size) * (priv->n_paths + 1);
233 	}
234 
235 	if (x_offset) {
236 		x_offset = 0;
237 	}
238 
239 	if (y_offset) {
240 		y_offset = 0;
241 	}
242 }
243 
244 static void
giggle_graph_renderer_render(GtkCellRenderer * cell,cairo_t * cr,GtkWidget * widget,const GdkRectangle * background_area,const GdkRectangle * cell_area,GtkCellRendererState flags)245 giggle_graph_renderer_render (GtkCellRenderer *cell,
246 			      cairo_t         *cr,
247 			      GtkWidget       *widget,
248 			      const GdkRectangle    *background_area,
249 			      const GdkRectangle    *cell_area,
250 			      GtkCellRendererState flags)
251 {
252 	GiggleGraphRendererPrivate   *priv;
253 	GiggleGraphRendererPathState *path_state;
254 	GitRevision               *revision;
255 	GArray                       *paths_state;
256 	GHashTable                   *table;
257 	gint                          x, y, h;
258 	gint                          cur_pos, pos;
259 	GList                        *children;
260 	gint                          size, i;
261 
262 	priv = GIGGLE_GRAPH_RENDERER (cell)->_priv;
263 
264 	if (!priv->revision) {
265 		return;
266 	}
267 
268 	x = cell_area->x;
269 	y = background_area->y;
270 	h = background_area->height;
271 	revision = priv->revision;
272 	size = PANGO_PIXELS (pango_font_description_get_size (gtk_widget_get_style (widget)->font_desc));
273 
274 	table = g_hash_table_new (g_direct_hash, g_direct_equal);
275 	paths_state = g_object_get_qdata (G_OBJECT (revision), revision_paths_state_quark);
276 	children = git_revision_get_children (revision);
277 	cur_pos = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision));
278 	cairo_set_line_width (cr, LINE_WIDTH (size));
279 	cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
280 
281 	/* paint paths */
282 	for (i = 0; i < paths_state->len; i++) {
283 		path_state = & g_array_index (paths_state, GiggleGraphRendererPathState, i);
284 		g_hash_table_insert (table, GINT_TO_POINTER ((gint) path_state->n_path), path_state);
285 		pos = path_state->n_path;
286 
287 		if (path_state->lower_n_color != INVALID_COLOR &&
288 		    (pos != cur_pos || git_revision_has_parents (revision))) {
289 			gdk_cairo_set_source_color (cr, &colors[path_state->lower_n_color]);
290 			cairo_move_to (cr, x + (pos * PATH_SPACE (size)), y + (h / 2));
291 			cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y + h);
292 			cairo_stroke  (cr);
293 		}
294 
295 		if (path_state->upper_n_color != INVALID_COLOR) {
296 			gdk_cairo_set_source_color (cr, &colors[path_state->upper_n_color]);
297 			cairo_move_to (cr, x + (pos * PATH_SPACE (size)), y);
298 			cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y + (h / 2));
299 			cairo_stroke  (cr);
300 		}
301 	}
302 
303 	/* paint connections between paths */
304 	while (children) {
305 		pos = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, children->data));
306 		path_state = g_hash_table_lookup (table, GINT_TO_POINTER (pos));
307 
308 		if (path_state->upper_n_color != INVALID_COLOR) {
309 			gdk_cairo_set_source_color (cr, &colors[path_state->upper_n_color]);
310 			cairo_move_to (cr,
311 				       x + (cur_pos * PATH_SPACE (size)),
312 				       y + (h / 2));
313 			cairo_line_to (cr,
314 				       x + (pos * PATH_SPACE (size)),
315 				       y + (h / 2));
316 
317 			/* redraw the upper part of the path before
318 			 * stroking to get a rounded connection
319 			 */
320 			cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y);
321 
322 			cairo_stroke  (cr);
323 		}
324 
325 		children = children->next;
326 	}
327 
328 	/* paint circle */
329 	cairo_set_source_rgb (cr, 0, 0, 0);
330 	cairo_arc (cr,
331 		   x + (cur_pos * PATH_SPACE (size)),
332 		   y + (h / 2),
333 		   DOT_RADIUS (size), 0, 2 * G_PI);
334 	cairo_stroke (cr);
335 
336 	/* paint internal circle */
337 	path_state = g_hash_table_lookup (table, GINT_TO_POINTER (cur_pos));
338 	gdk_cairo_set_source_color (cr, &colors[path_state->lower_n_color]);
339 	cairo_arc (cr,
340 		   x + (cur_pos * PATH_SPACE (size)),
341 		   y + (h / 2),
342 		   DOT_RADIUS (size) - 1, 0, 2 * G_PI);
343 	cairo_fill (cr);
344 	cairo_stroke (cr);
345 
346 	g_hash_table_destroy (table);
347 }
348 
349 GtkCellRenderer *
giggle_graph_renderer_new(void)350 giggle_graph_renderer_new (void)
351 {
352 	return g_object_new (GIGGLE_TYPE_GRAPH_RENDERER, NULL);
353 }
354 
355 static void
find_free_path(GHashTable * visible_paths,gint * n_paths,gint * path)356 find_free_path (GHashTable *visible_paths,
357 		gint       *n_paths,
358 		gint       *path)
359 {
360 	gint cur_path = 1;
361 
362 	/* find first path not in list */
363 	while (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (cur_path))) {
364 		cur_path++;
365 	}
366 
367 	*path = cur_path;
368 
369 	/* increment number of paths */
370 	if (*path > *n_paths) {
371 		*n_paths = *path;
372 	}
373 }
374 
375 static void
get_initial_status_foreach(gpointer key,gpointer value,gpointer user_data)376 get_initial_status_foreach (gpointer key,
377 			    gpointer value,
378 			    gpointer user_data)
379 {
380 	GiggleGraphRendererPathState  path_state;
381 	GArray                       *array;
382 	gint                          n_color, n_path;
383 
384 	array = (GArray *) user_data;
385 	n_color = GPOINTER_TO_INT (value);
386 	n_path = GPOINTER_TO_INT (key);
387 
388 	path_state.n_path = n_path;
389 	path_state.lower_n_color = n_color;
390 	path_state.upper_n_color = n_color;
391 
392 	g_array_append_val (array, path_state);
393 }
394 
395 static GArray *
get_initial_status(GHashTable * visible_paths)396 get_initial_status (GHashTable *visible_paths)
397 {
398 	GArray *array;
399 	guint      size;
400 
401 	size = g_hash_table_size (visible_paths);
402 	array = g_array_sized_new (FALSE, TRUE, sizeof (GiggleGraphRendererPathState), size);
403 
404 	g_hash_table_foreach (visible_paths, (GHFunc) get_initial_status_foreach, array);
405 
406 	return array;
407 }
408 
409 static void
free_paths_state(GArray * array)410 free_paths_state (GArray *array)
411 {
412 	g_array_free (array, TRUE);
413 }
414 
415 static void
giggle_graph_renderer_calculate_revision_state(GiggleGraphRenderer * renderer,GitRevision * revision,GHashTable * visible_paths,gint * n_color)416 giggle_graph_renderer_calculate_revision_state (GiggleGraphRenderer *renderer,
417 						GitRevision      *revision,
418 						GHashTable          *visible_paths,
419 						gint                *n_color)
420 {
421 	GiggleGraphRendererPathState  path_state;
422 	GiggleGraphRendererPrivate   *priv;
423 	GitRevision               *rev;
424 	GArray                       *paths_state;
425 	GList                        *children;
426 	gboolean                      current_path_reused = FALSE;
427 	gboolean                      update_color;
428 	gint                          n_path, i;
429 
430 	priv = renderer->_priv;
431 	children = git_revision_get_children (revision);
432 	update_color = (g_list_length (children) > 1);
433 	paths_state = get_initial_status (visible_paths);
434 
435 	while (children) {
436 		rev = GIT_REVISION (children->data);
437 		n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, rev));
438 
439 		if (!n_path) {
440 			/* there wasn't a path for this revision, choose one */
441 			if (!current_path_reused) {
442 				n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision));
443 				current_path_reused = TRUE;
444 			} else {
445 				find_free_path (visible_paths, &priv->n_paths, &n_path);
446 			}
447 
448 			g_hash_table_insert (priv->paths_info, rev, GINT_TO_POINTER (n_path));
449 			path_state.lower_n_color =
450 				GPOINTER_TO_INT (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (n_path)));
451 
452 			if (update_color) {
453 				path_state.upper_n_color = *n_color = NEXT_COLOR (*n_color);
454 			} else {
455 				path_state.upper_n_color = path_state.lower_n_color;
456 			}
457 		} else {
458 			path_state.lower_n_color =
459 				GPOINTER_TO_INT (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (n_path)));
460 
461 			path_state.upper_n_color = path_state.lower_n_color;
462 		}
463 
464 		path_state.n_path = n_path;
465 		g_hash_table_insert (visible_paths, GINT_TO_POINTER (n_path), GINT_TO_POINTER ((gint) path_state.upper_n_color));
466 		g_array_append_val (paths_state, path_state);
467 
468 		children = children->next;
469 	}
470 
471 	if (!current_path_reused) {
472 		/* current path is a dead end, remove it from the visible paths table */
473 		n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision));
474 		g_hash_table_remove (visible_paths, GINT_TO_POINTER (n_path));
475 
476 		for (i = 0; i < paths_state->len; i++) {
477 			path_state = g_array_index (paths_state, GiggleGraphRendererPathState, i);
478 
479 			if (path_state.n_path == n_path) {
480 				path_state.upper_n_color = INVALID_COLOR;
481 				((GiggleGraphRendererPathState *) paths_state->data)[i] = path_state;
482 				break;
483 			}
484 		}
485 	}
486 
487 	g_object_set_qdata_full (G_OBJECT (revision), revision_paths_state_quark,
488 				 paths_state, (GDestroyNotify) free_paths_state);
489 }
490 
491 void
giggle_graph_renderer_validate_model(GiggleGraphRenderer * renderer,GtkTreeModel * model,gint column)492 giggle_graph_renderer_validate_model (GiggleGraphRenderer *renderer,
493 				      GtkTreeModel        *model,
494 				      gint                 column)
495 {
496 	GiggleGraphRendererPrivate *priv;
497 	GtkTreeIter                 iter;
498 	gint                        n_children;
499 	gint                        n_color = 0;
500 	GitRevision             *revision;
501 	GHashTable                 *visible_paths;
502 	GType                       contained_type;
503 	gint                        n_path;
504 
505 	g_return_if_fail (GIGGLE_IS_GRAPH_RENDERER (renderer));
506 	g_return_if_fail (GTK_IS_TREE_MODEL (model));
507 
508 	priv = renderer->_priv;
509 	contained_type = gtk_tree_model_get_column_type (model, column);
510 
511 	/*g_return_if_fail (contained_type == GIT_TYPE_REVISION);*/
512 
513 	if (priv->paths_info) {
514 		g_hash_table_destroy (priv->paths_info);
515 	}
516 
517 	priv->n_paths = 0;
518 	priv->paths_info = g_hash_table_new (g_direct_hash, g_direct_equal);
519 	visible_paths = g_hash_table_new (g_direct_hash, g_direct_equal);
520 	n_children = gtk_tree_model_iter_n_children (model, NULL);
521 
522 	while (n_children) {
523 		/* need to calculate state backwards for proper color asignment */
524 		n_children--;
525 		gtk_tree_model_iter_nth_child (model, &iter, NULL, n_children);
526 		gtk_tree_model_get (model, &iter, column, &revision, -1);
527 
528 		if (revision) {
529 			if (!git_revision_has_parents (revision)) {
530 				n_color = NEXT_COLOR (n_color);
531 				find_free_path (visible_paths, &priv->n_paths, &n_path);
532 				g_hash_table_insert (priv->paths_info, revision, GINT_TO_POINTER (n_path));
533 				g_hash_table_insert (visible_paths, GINT_TO_POINTER (n_path), GINT_TO_POINTER (n_color));
534 			}
535 
536 			giggle_graph_renderer_calculate_revision_state (renderer, revision, visible_paths, &n_color);
537 			g_object_unref (revision);
538 		}
539 	}
540 
541 	g_hash_table_destroy (visible_paths);
542 }
543