1 /*-
2  * Copyright (C) 2007-2011  Peter de Ridder <peter@xfce.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17  */
18 
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22 
23 #include <exo/exo.h>
24 #include <libxfce4util/libxfce4util.h>
25 
26 #include "tgh-common.h"
27 #include "tgh-cell-renderer-graph.h"
28 #include "tgh-log-dialog.h"
29 
30 static void selection_changed (GtkTreeView*, gpointer);
31 static void cancel_clicked (GtkButton*, gpointer);
32 static void refresh_clicked (GtkButton*, gpointer);
33 
34 struct _TghLogDialog
35 {
36   GtkDialog dialog;
37 
38   GList *graph;
39 
40   GtkWidget *tree_view;
41   GtkWidget *revision_label;
42   GtkWidget *text_view;
43   GtkWidget *file_view;
44   GtkWidget *close;
45   GtkWidget *cancel;
46   GtkWidget *refresh;
47 };
48 
49 struct _TghLogDialogClass
50 {
51   GtkDialogClass dialog_class;
52 };
53 
54 G_DEFINE_TYPE (TghLogDialog, tgh_log_dialog, GTK_TYPE_DIALOG)
55 
56 enum {
57   SIGNAL_CANCEL = 0,
58   SIGNAL_REFRESH,
59   SIGNAL_COUNT
60 };
61 
62 static guint signals[SIGNAL_COUNT];
63 
64 static void
tgh_log_dialog_class_init(TghLogDialogClass * klass)65 tgh_log_dialog_class_init (TghLogDialogClass *klass)
66 {
67   signals[SIGNAL_CANCEL] = g_signal_new("cancel-clicked",
68       G_OBJECT_CLASS_TYPE (klass),
69       G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
70       0, NULL, NULL,
71       g_cclosure_marshal_VOID__VOID,
72       G_TYPE_NONE, 0);
73   signals[SIGNAL_REFRESH] = g_signal_new("refresh-clicked",
74       G_OBJECT_CLASS_TYPE (klass),
75       G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
76       0, NULL, NULL,
77       g_cclosure_marshal_VOID__VOID,
78       G_TYPE_NONE, 0);
79 }
80 
81 enum {
82   COLUMN_REVISION = 0,
83   COLUMN_AUTHOR,
84   COLUMN_AUTHOR_DATE,
85   COLUMN_COMMIT,
86   COLUMN_COMMIT_DATE,
87   COLUMN_MESSAGE,
88   COLUMN_FULL_MESSAGE,
89   COLUMN_FILE_LIST,
90   COLUMN_GRAPH,
91   COLUMN_COUNT
92 };
93 
94 enum {
95   FILE_COLUMN_FILE = 0,
96   FILE_COLUMN_PERCENTAGE,
97   FILE_COLUMN_CHANGES,
98   FILE_COLUMN_COUNT
99 };
100 
101 static void
tgh_log_dialog_init(TghLogDialog * dialog)102 tgh_log_dialog_init (TghLogDialog *dialog)
103 {
104   GtkWidget *button;
105   GtkWidget *label;
106   GtkWidget *tree_view;
107   GtkWidget *text_view;
108   GtkWidget *file_view;
109   GtkWidget *scroll_window;
110   GtkWidget *pane;
111   GtkWidget *vpane;
112   GtkWidget *box;
113   GtkCellRenderer *renderer;
114   GtkTreeModel *model;
115 
116   pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
117 
118   scroll_window = gtk_scrolled_window_new (NULL, NULL);
119   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
120 
121   dialog->tree_view = tree_view = gtk_tree_view_new ();
122 
123   renderer = tgh_cell_renderer_graph_new ();
124   g_object_set (G_OBJECT (renderer), "xalign", 0.5f, NULL);
125   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
126       -1, "", renderer,
127       "graph-iter", COLUMN_GRAPH,
128       NULL);
129 
130   renderer = gtk_cell_renderer_text_new ();
131   g_object_set (G_OBJECT (renderer), "width-chars", 9, NULL);
132   g_object_set (G_OBJECT (renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
133   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
134       -1, _("Revision"),
135       renderer, "text",
136       COLUMN_REVISION, NULL);
137 
138   renderer = gtk_cell_renderer_text_new ();
139   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
140       -1, _("Author"),
141       renderer, "text",
142       COLUMN_AUTHOR, NULL);
143 
144   renderer = gtk_cell_renderer_text_new ();
145   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
146       -1, _("AuthorDate"),
147       renderer, "text",
148       COLUMN_AUTHOR_DATE, NULL);
149 
150 #if 0
151   renderer = gtk_cell_renderer_text_new ();
152   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
153       -1, _("Commit"),
154       renderer, "text",
155       COLUMN_COMMIT, NULL);
156 
157   renderer = gtk_cell_renderer_text_new ();
158   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
159       -1, _("CommitDate"),
160       renderer, "text",
161       COLUMN_COMMIT_DATE, NULL);
162 #endif
163 
164   renderer = gtk_cell_renderer_text_new ();
165   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree_view),
166       -1, _("Message"),
167       renderer, "text",
168       COLUMN_MESSAGE, NULL);
169 
170   model = GTK_TREE_MODEL (gtk_list_store_new (COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER));
171 
172   gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), model);
173 
174   g_object_unref (model);
175 
176   g_signal_connect (G_OBJECT (tree_view), "cursor-changed", G_CALLBACK (selection_changed), dialog);
177 
178   gtk_container_add (GTK_CONTAINER (scroll_window), tree_view);
179   gtk_paned_pack1 (GTK_PANED(pane), scroll_window, TRUE, FALSE);
180   gtk_widget_show (tree_view);
181   gtk_widget_show (scroll_window);
182 
183   vpane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
184 
185   box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
186 
187   dialog->revision_label = label = gtk_label_new (_("Revision"));
188   gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
189   gtk_label_set_yalign (GTK_LABEL (label), 0.5f);
190   gtk_label_set_selectable (GTK_LABEL (label), TRUE);
191   gtk_box_pack_start (GTK_BOX (box), label, FALSE, TRUE, 0);
192   gtk_widget_show (label);
193 
194   scroll_window = gtk_scrolled_window_new (NULL, NULL);
195   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll_window), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
196 
197   dialog->text_view = text_view = gtk_text_view_new ();
198   gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (text_view), GTK_WRAP_WORD_CHAR);
199   gtk_text_view_set_editable (GTK_TEXT_VIEW (text_view), FALSE);
200 
201   gtk_container_add (GTK_CONTAINER (scroll_window), text_view);
202   gtk_box_pack_end (GTK_BOX (box), scroll_window, TRUE, TRUE, 0);
203   gtk_widget_show (text_view);
204   gtk_widget_show (scroll_window);
205 
206   gtk_paned_pack1 (GTK_PANED(vpane), box, TRUE, FALSE);
207   gtk_widget_show (box);
208 
209   scroll_window = gtk_scrolled_window_new (NULL, NULL);
210   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
211 
212   dialog->file_view = file_view = gtk_tree_view_new ();
213 
214   renderer = gtk_cell_renderer_progress_new ();
215   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (file_view),
216       -1, _("Changes"), renderer,
217       "value", FILE_COLUMN_PERCENTAGE,
218       "text", FILE_COLUMN_CHANGES,
219       NULL);
220 
221   renderer = gtk_cell_renderer_text_new ();
222   gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (file_view),
223       -1, _("File"),
224       renderer, "text",
225       FILE_COLUMN_FILE, NULL);
226 
227   model = GTK_TREE_MODEL (gtk_list_store_new (FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_INT, G_TYPE_STRING));
228 
229   gtk_tree_view_set_model (GTK_TREE_VIEW (file_view), model);
230 
231   g_object_unref (model);
232 
233   gtk_container_add (GTK_CONTAINER (scroll_window), file_view);
234   gtk_paned_pack2 (GTK_PANED(vpane), scroll_window, TRUE, FALSE);
235   gtk_widget_show (file_view);
236   gtk_widget_show (scroll_window);
237 
238   gtk_paned_pack2 (GTK_PANED(pane), vpane, TRUE, FALSE);
239   gtk_widget_show (vpane);
240 
241   gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), pane, TRUE, TRUE, 0);
242   gtk_widget_show (pane);
243 
244   gtk_window_set_title (GTK_WINDOW (dialog), _("Log"));
245 
246   gtk_button_box_set_layout(GTK_BUTTON_BOX (exo_gtk_dialog_get_action_area (GTK_DIALOG (dialog))), GTK_BUTTONBOX_EDGE);
247 
248   dialog->cancel = button = gtk_button_new_with_mnemonic (_("_Cancel"));
249   gtk_box_pack_start (GTK_BOX (exo_gtk_dialog_get_action_area (GTK_DIALOG (dialog))), button, FALSE, TRUE, 0);
250   g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (cancel_clicked), dialog);
251   gtk_widget_show (button);
252 
253   dialog->refresh = button = gtk_button_new_with_mnemonic (_("_Refresh"));
254   gtk_box_pack_start (GTK_BOX (exo_gtk_dialog_get_action_area (GTK_DIALOG (dialog))), button, FALSE, TRUE, 0);
255   g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (refresh_clicked), dialog);
256   gtk_widget_hide (button);
257 
258   dialog->close = button = gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Close"), GTK_RESPONSE_CLOSE);
259   gtk_widget_show (button);
260 
261   gtk_window_set_default_size (GTK_WINDOW (dialog), 500, 400);
262 }
263 
264 GtkWidget*
tgh_log_dialog_new(const gchar * title,GtkWindow * parent,GtkDialogFlags flags)265 tgh_log_dialog_new (const gchar *title, GtkWindow *parent, GtkDialogFlags flags)
266 {
267   TghLogDialog *dialog = g_object_new (TGH_TYPE_LOG_DIALOG, NULL);
268 
269   if(title)
270     gtk_window_set_title (GTK_WINDOW(dialog), title);
271 
272   if(parent)
273     gtk_window_set_transient_for (GTK_WINDOW(dialog), parent);
274 
275   if(flags & GTK_DIALOG_MODAL)
276     gtk_window_set_modal (GTK_WINDOW(dialog), TRUE);
277 
278   if(flags & GTK_DIALOG_DESTROY_WITH_PARENT)
279     gtk_window_set_destroy_with_parent (GTK_WINDOW(dialog), TRUE);
280 
281   return GTK_WIDGET(dialog);
282 }
283 
284 static void
tgh_graph_node_free(TghGraphNode * node)285 tgh_graph_node_free (TghGraphNode *node)
286 {
287   TghGraphNode *next;
288 
289   while (node)
290   {
291     g_free (node->name);
292     g_strfreev (node->junction);
293 
294     next = node->next;
295     g_free (node);
296     node = next;
297   }
298 }
299 
300 static TghGraphNode*
tgh_graph_node_add(TghGraphNode * prev)301 tgh_graph_node_add (TghGraphNode *prev)
302 {
303   TghGraphNode *node = g_new0 (TghGraphNode, 1);
304   if (prev)
305     prev->next = node;
306   return node;
307 }
308 
309 static TghGraphNode*
add_n_check_node(TghGraphNode * node_iter,TghGraphNode ** node_list,const gchar * name,const gchar * revision,gchar ** parents,gboolean * found)310 add_n_check_node (TghGraphNode *node_iter, TghGraphNode **node_list, const gchar *name, const gchar *revision, gchar **parents, gboolean *found)
311 {
312   TghGraphNode *iter;
313 
314   for (iter = *node_list; iter; iter = iter->next)
315   {
316     if (G_UNLIKELY(0 == strcmp (iter->name, name)))
317       return node_iter;
318   }
319 
320   node_iter = tgh_graph_node_add (node_iter);
321 
322   if (G_UNLIKELY (!*node_list))
323     *node_list = node_iter;
324 
325   node_iter->name = g_strdup (name);
326 
327   if (G_UNLIKELY (0 == strcmp (revision, name)))
328   {
329     *found = TRUE;
330     node_iter->type = TGH_GRAPH_JUNCTION;
331     node_iter->junction = g_strdupv (parents);
332   }
333 
334   return node_iter;
335 }
336 
337 void
tgh_log_dialog_add(TghLogDialog * dialog,GSList * files,const gchar * revision,gchar ** parents,const gchar * author,const gchar * author_date,const gchar * commit,const gchar * commit_date,const gchar * message)338 tgh_log_dialog_add (TghLogDialog *dialog, GSList *files, const gchar *revision, gchar **parents, const gchar *author, const gchar *author_date, const gchar *commit, const gchar *commit_date, const gchar *message)
339 {
340   GtkTreeModel *model;
341   GtkTreeIter iter;
342   gchar **lines = NULL;
343   gchar **line_iter;
344   gchar *first_line = NULL;
345   GList *graph;
346   TghGraphNode *next_node_list;
347   TghGraphNode *next_node_iter;
348   TghGraphNode *node_list = NULL;
349   TghGraphNode *node_iter = NULL;
350   gchar **junction_iter;
351   gboolean found = FALSE;
352 
353   g_return_if_fail (TGH_IS_LOG_DIALOG (dialog));
354 
355   model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->tree_view));
356 
357   graph = dialog->graph;
358 
359   if (graph)
360   {
361     next_node_list = graph->data;
362 
363     for (next_node_iter = next_node_list; next_node_iter; next_node_iter = next_node_iter->next)
364     {
365       switch (next_node_iter->type)
366       {
367         case TGH_GRAPH_LINE:
368           node_iter = add_n_check_node (node_iter, &node_list, next_node_iter->name, revision, parents, &found);
369           break;
370         case TGH_GRAPH_JUNCTION:
371           if (G_LIKELY (next_node_iter->junction))
372           {
373             for (junction_iter = next_node_iter->junction; *junction_iter; junction_iter++)
374             {
375               node_iter = add_n_check_node (node_iter, &node_list, *junction_iter, revision, parents, &found);
376             }
377           }
378           break;
379       }
380     }
381   }
382 
383   if (!found)
384   {
385     node_iter = g_new0 (TghGraphNode, 1);
386     node_iter->next = node_list;
387     node_iter->name = g_strdup (revision);
388     node_iter->type = TGH_GRAPH_JUNCTION;
389     node_iter->junction = g_strdupv (parents);
390     node_list = node_iter;
391   }
392 
393   graph = g_list_prepend (graph, node_list);
394 
395   dialog->graph = graph;
396 
397   if(message)
398   {
399     lines = g_strsplit_set (message, "\r\n", -1);
400     line_iter = lines;
401     while (*line_iter)
402     {
403       if (g_strstrip (*line_iter)[0])
404         break;
405       line_iter++;
406     }
407     if (!line_iter)
408       line_iter = lines;
409     first_line = *line_iter;
410   }
411 
412   gtk_list_store_append (GTK_LIST_STORE (model), &iter);
413   gtk_list_store_set (GTK_LIST_STORE (model), &iter,
414       COLUMN_REVISION, revision,
415       COLUMN_AUTHOR, author,
416       COLUMN_AUTHOR_DATE, author_date,
417       COLUMN_COMMIT, commit,
418       COLUMN_COMMIT_DATE, commit_date,
419       COLUMN_MESSAGE, first_line,
420       COLUMN_FULL_MESSAGE, message,
421       COLUMN_FILE_LIST, files,
422       COLUMN_GRAPH, graph,
423       -1);
424 
425   g_strfreev (lines);
426 }
427 
428 void
tgh_log_dialog_done(TghLogDialog * dialog)429 tgh_log_dialog_done (TghLogDialog *dialog)
430 {
431   g_return_if_fail (TGH_IS_LOG_DIALOG (dialog));
432 
433   gtk_widget_hide (dialog->cancel);
434   gtk_widget_show (dialog->refresh);
435 }
436 
437 static void
selection_changed(GtkTreeView * tree_view,gpointer user_data)438 selection_changed (GtkTreeView *tree_view, gpointer user_data)
439 {
440   GtkTreeIter iter;
441   GtkTreeSelection *selection;
442   GtkTreeModel *model;
443   gchar *revision;
444   gchar *message;
445   GSList *files;
446 
447   TghLogDialog *dialog = TGH_LOG_DIALOG (user_data);
448 
449   selection = gtk_tree_view_get_selection (tree_view);
450 
451   if (gtk_tree_selection_get_selected (selection, &model, &iter))
452   {
453     gtk_tree_model_get (model, &iter, COLUMN_REVISION, &revision, COLUMN_FULL_MESSAGE, &message, COLUMN_FILE_LIST, &files, -1);
454 
455     gtk_label_set_text (GTK_LABEL (dialog->revision_label), revision);
456     g_free (revision);
457 
458     gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view)), message?message:"", -1);
459     g_free (message);
460 
461     model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->file_view));
462     gtk_list_store_clear (GTK_LIST_STORE (model));
463 
464     while(files)
465     {
466       gchar *changes = g_strdup_printf ("+%u -%u", TGH_LOG_FILE (files->data)->insertions, TGH_LOG_FILE (files->data)->deletions);
467       guint sum = TGH_LOG_FILE (files->data)->insertions + TGH_LOG_FILE (files->data)->deletions;
468       gtk_list_store_append (GTK_LIST_STORE (model), &iter);
469       gtk_list_store_set (GTK_LIST_STORE (model), &iter,
470           FILE_COLUMN_FILE, TGH_LOG_FILE (files->data)->file,
471           FILE_COLUMN_PERCENTAGE, sum?TGH_LOG_FILE (files->data)->insertions * 100 / sum:0,
472           FILE_COLUMN_CHANGES, changes,
473           -1);
474       g_free (changes);
475       files = files->next;
476     }
477 
478     gtk_tree_view_expand_all (GTK_TREE_VIEW (dialog->file_view));
479   }
480 }
481 
482 static void
cancel_clicked(GtkButton * button,gpointer user_data)483 cancel_clicked (GtkButton *button, gpointer user_data)
484 {
485   TghLogDialog *dialog = TGH_LOG_DIALOG (user_data);
486 
487   gtk_widget_hide (dialog->cancel);
488   gtk_widget_show (dialog->refresh);
489 
490   g_signal_emit (dialog, signals[SIGNAL_CANCEL], 0);
491 }
492 
493 static void
refresh_clicked(GtkButton * button,gpointer user_data)494 refresh_clicked (GtkButton *button, gpointer user_data)
495 {
496   GtkTreeModel *model;
497   TghLogDialog *dialog = TGH_LOG_DIALOG (user_data);
498 
499   gtk_widget_hide (dialog->refresh);
500   gtk_widget_show (dialog->cancel);
501 
502   g_signal_emit (dialog, signals[SIGNAL_REFRESH], 0);
503 
504   model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->tree_view));
505   gtk_list_store_clear (GTK_LIST_STORE (model));
506 
507   g_list_foreach(dialog->graph, (GFunc)tgh_graph_node_free, NULL);
508   g_list_free (dialog->graph);
509   dialog->graph = NULL;
510 
511   gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->text_view)), "", -1);
512 
513   model = gtk_tree_view_get_model (GTK_TREE_VIEW (dialog->file_view));
514   gtk_list_store_clear (GTK_LIST_STORE (model));
515 }
516