1 /*
2  * This file Copyright (C) 2008-2014 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <errno.h>
10 #include <stdio.h>
11 #include <string.h>
12 
13 #include <glib/gi18n.h>
14 #include <gtk/gtk.h>
15 
16 #include <libtransmission/transmission.h>
17 #include <libtransmission/log.h>
18 
19 #include "conf.h"
20 #include "hig.h"
21 #include "msgwin.h"
22 #include "tr-core.h"
23 #include "tr-prefs.h"
24 #include "util.h"
25 
26 enum
27 {
28     COL_SEQUENCE,
29     COL_NAME,
30     COL_MESSAGE,
31     COL_TR_MSG,
32     N_COLUMNS
33 };
34 
35 struct MsgData
36 {
37     TrCore* core;
38     GtkTreeView* view;
39     GtkListStore* store;
40     GtkTreeModel* filter;
41     GtkTreeModel* sort;
42     tr_log_level maxLevel;
43     gboolean isPaused;
44     guint refresh_tag;
45 };
46 
47 static struct tr_log_message* myTail = NULL;
48 static struct tr_log_message* myHead = NULL;
49 
50 /****
51 *****
52 ****/
53 
54 /* is the user looking at the latest messages? */
is_pinned_to_new(struct MsgData * data)55 static gboolean is_pinned_to_new(struct MsgData* data)
56 {
57     gboolean pinned_to_new = FALSE;
58 
59     if (data->view == NULL)
60     {
61         pinned_to_new = TRUE;
62     }
63     else
64     {
65         GtkTreePath* last_visible;
66 
67         if (gtk_tree_view_get_visible_range(data->view, NULL, &last_visible))
68         {
69             GtkTreeIter iter;
70             int const row_count = gtk_tree_model_iter_n_children(data->sort, NULL);
71 
72             if (gtk_tree_model_iter_nth_child(data->sort, &iter, NULL, row_count - 1))
73             {
74                 GtkTreePath* last_row = gtk_tree_model_get_path(data->sort, &iter);
75                 pinned_to_new = !gtk_tree_path_compare(last_visible, last_row);
76                 gtk_tree_path_free(last_row);
77             }
78 
79             gtk_tree_path_free(last_visible);
80         }
81     }
82 
83     return pinned_to_new;
84 }
85 
scroll_to_bottom(struct MsgData * data)86 static void scroll_to_bottom(struct MsgData* data)
87 {
88     if (data->sort != NULL)
89     {
90         GtkTreeIter iter;
91         int const row_count = gtk_tree_model_iter_n_children(data->sort, NULL);
92 
93         if (gtk_tree_model_iter_nth_child(data->sort, &iter, NULL, row_count - 1))
94         {
95             GtkTreePath* last_row = gtk_tree_model_get_path(data->sort, &iter);
96             gtk_tree_view_scroll_to_cell(data->view, last_row, NULL, TRUE, 1, 0);
97             gtk_tree_path_free(last_row);
98         }
99     }
100 }
101 
102 /****
103 *****
104 ****/
105 
level_combo_changed_cb(GtkComboBox * combo_box,gpointer gdata)106 static void level_combo_changed_cb(GtkComboBox* combo_box, gpointer gdata)
107 {
108     struct MsgData* data = gdata;
109     int const level = gtr_combo_box_get_active_enum(combo_box);
110     gboolean const pinned_to_new = is_pinned_to_new(data);
111 
112     tr_logSetLevel(level);
113     gtr_core_set_pref_int(data->core, TR_KEY_message_level, level);
114     data->maxLevel = level;
115     gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(data->filter));
116 
117     if (pinned_to_new)
118     {
119         scroll_to_bottom(data);
120     }
121 }
122 
123 /* similar to asctime, but is utf8-clean */
gtr_localtime(time_t time)124 static char* gtr_localtime(time_t time)
125 {
126     char buf[256];
127     char* eoln;
128     struct tm const tm = *localtime(&time);
129 
130     g_strlcpy(buf, asctime(&tm), sizeof(buf));
131 
132     if ((eoln = strchr(buf, '\n')) != NULL)
133     {
134         *eoln = '\0';
135     }
136 
137     return g_locale_to_utf8(buf, -1, NULL, NULL, NULL);
138 }
139 
doSave(GtkWindow * parent,struct MsgData * data,char const * filename)140 static void doSave(GtkWindow* parent, struct MsgData* data, char const* filename)
141 {
142     FILE* fp = fopen(filename, "w+");
143 
144     if (fp == NULL)
145     {
146         GtkWidget* w = gtk_message_dialog_new(parent, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Couldn't save \"%s\""),
147             filename);
148         gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", g_strerror(errno));
149         g_signal_connect_swapped(w, "response", G_CALLBACK(gtk_widget_destroy), w);
150         gtk_widget_show(w);
151     }
152     else
153     {
154         GtkTreeIter iter;
155         GtkTreeModel* model = GTK_TREE_MODEL(data->sort);
156 
157         if (gtk_tree_model_iter_children(model, &iter, NULL))
158         {
159             do
160             {
161                 char* date;
162                 char const* levelStr;
163                 struct tr_log_message const* node;
164 
165                 gtk_tree_model_get(model, &iter, COL_TR_MSG, &node, -1);
166                 date = gtr_localtime(node->when);
167 
168                 switch (node->level)
169                 {
170                 case TR_LOG_DEBUG:
171                     levelStr = "debug";
172                     break;
173 
174                 case TR_LOG_ERROR:
175                     levelStr = "error";
176                     break;
177 
178                 default:
179                     levelStr = "     ";
180                     break;
181                 }
182 
183                 fprintf(fp, "%s\t%s\t%s\t%s\n", date, levelStr, node->name != NULL ? node->name : "",
184                     node->message != NULL ? node->message : "");
185                 g_free(date);
186             }
187             while (gtk_tree_model_iter_next(model, &iter));
188         }
189 
190         fclose(fp);
191     }
192 }
193 
onSaveDialogResponse(GtkWidget * d,int response,gpointer data)194 static void onSaveDialogResponse(GtkWidget* d, int response, gpointer data)
195 {
196     if (response == GTK_RESPONSE_ACCEPT)
197     {
198         char* file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(d));
199         doSave(GTK_WINDOW(d), data, file);
200         g_free(file);
201     }
202 
203     gtk_widget_destroy(d);
204 }
205 
onSaveRequest(GtkWidget * w,gpointer data)206 static void onSaveRequest(GtkWidget* w, gpointer data)
207 {
208     GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(w));
209     GtkWidget* d = gtk_file_chooser_dialog_new(_("Save Log"), window, GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL,
210         GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);
211 
212     g_signal_connect(d, "response", G_CALLBACK(onSaveDialogResponse), data);
213     gtk_widget_show(d);
214 }
215 
onClearRequest(GtkWidget * w UNUSED,gpointer gdata)216 static void onClearRequest(GtkWidget* w UNUSED, gpointer gdata)
217 {
218     struct MsgData* data = gdata;
219 
220     gtk_list_store_clear(data->store);
221     tr_logFreeQueue(myHead);
222     myHead = myTail = NULL;
223 }
224 
onPauseToggled(GtkToggleToolButton * w,gpointer gdata)225 static void onPauseToggled(GtkToggleToolButton* w, gpointer gdata)
226 {
227     struct MsgData* data = gdata;
228 
229     data->isPaused = gtk_toggle_tool_button_get_active(w);
230 }
231 
getForegroundColor(int msgLevel)232 static char const* getForegroundColor(int msgLevel)
233 {
234     switch (msgLevel)
235     {
236     case TR_LOG_DEBUG:
237         return "forestgreen";
238 
239     case TR_LOG_INFO:
240         return "black";
241 
242     case TR_LOG_ERROR:
243         return "red";
244 
245     default:
246         g_assert_not_reached();
247         return "black";
248     }
249 }
250 
renderText(GtkTreeViewColumn * column UNUSED,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,gpointer gcol)251 static void renderText(GtkTreeViewColumn* column UNUSED, GtkCellRenderer* renderer, GtkTreeModel* tree_model, GtkTreeIter* iter,
252     gpointer gcol)
253 {
254     int const col = GPOINTER_TO_INT(gcol);
255     char* str = NULL;
256     struct tr_log_message const* node;
257 
258     gtk_tree_model_get(tree_model, iter, col, &str, COL_TR_MSG, &node, -1);
259     g_object_set(renderer, "text", str, "foreground", getForegroundColor(node->level), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
260 }
261 
renderTime(GtkTreeViewColumn * column UNUSED,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,gpointer data UNUSED)262 static void renderTime(GtkTreeViewColumn* column UNUSED, GtkCellRenderer* renderer, GtkTreeModel* tree_model, GtkTreeIter* iter,
263     gpointer data UNUSED)
264 {
265     struct tm tm;
266     char buf[16];
267     struct tr_log_message const* node;
268 
269     gtk_tree_model_get(tree_model, iter, COL_TR_MSG, &node, -1);
270     tm = *localtime(&node->when);
271     g_snprintf(buf, sizeof(buf), "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
272     g_object_set(renderer, "text", buf, "foreground", getForegroundColor(node->level), NULL);
273 }
274 
appendColumn(GtkTreeView * view,int col)275 static void appendColumn(GtkTreeView* view, int col)
276 {
277     GtkCellRenderer* r;
278     GtkTreeViewColumn* c;
279     char const* title = NULL;
280 
281     switch (col)
282     {
283     case COL_SEQUENCE:
284         title = _("Time");
285         break;
286 
287     /* noun. column title for a list */
288     case COL_NAME:
289         title = _("Name");
290         break;
291 
292     /* noun. column title for a list */
293     case COL_MESSAGE:
294         title = _("Message");
295         break;
296 
297     default:
298         g_assert_not_reached();
299     }
300 
301     switch (col)
302     {
303     case COL_NAME:
304         r = gtk_cell_renderer_text_new();
305         c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
306         gtk_tree_view_column_set_cell_data_func(c, r, renderText, GINT_TO_POINTER(col), NULL);
307         gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
308         gtk_tree_view_column_set_fixed_width(c, 200);
309         gtk_tree_view_column_set_resizable(c, TRUE);
310         break;
311 
312     case COL_MESSAGE:
313         r = gtk_cell_renderer_text_new();
314         c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
315         gtk_tree_view_column_set_cell_data_func(c, r, renderText, GINT_TO_POINTER(col), NULL);
316         gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
317         gtk_tree_view_column_set_fixed_width(c, 500);
318         gtk_tree_view_column_set_resizable(c, TRUE);
319         break;
320 
321     case COL_SEQUENCE:
322         r = gtk_cell_renderer_text_new();
323         c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
324         gtk_tree_view_column_set_cell_data_func(c, r, renderTime, NULL, NULL);
325         gtk_tree_view_column_set_resizable(c, TRUE);
326         break;
327 
328     default:
329         g_assert_not_reached();
330         break;
331     }
332 
333     gtk_tree_view_append_column(view, c);
334 }
335 
isRowVisible(GtkTreeModel * model,GtkTreeIter * iter,gpointer gdata)336 static gboolean isRowVisible(GtkTreeModel* model, GtkTreeIter* iter, gpointer gdata)
337 {
338     struct tr_log_message const* node;
339     struct MsgData const* data = gdata;
340 
341     gtk_tree_model_get(model, iter, COL_TR_MSG, &node, -1);
342 
343     return node->level <= data->maxLevel;
344 }
345 
onWindowDestroyed(gpointer gdata,GObject * deadWindow UNUSED)346 static void onWindowDestroyed(gpointer gdata, GObject* deadWindow UNUSED)
347 {
348     struct MsgData* data = gdata;
349 
350     g_source_remove(data->refresh_tag);
351 
352     g_free(data);
353 }
354 
addMessages(GtkListStore * store,struct tr_log_message * head)355 static tr_log_message* addMessages(GtkListStore* store, struct tr_log_message* head)
356 {
357     tr_log_message* i;
358     static unsigned int sequence = 0;
359     char const* default_name = g_get_application_name();
360 
361     for (i = head; i != NULL && i->next != NULL; i = i->next)
362     {
363         char const* name = i->name != NULL ? i->name : default_name;
364 
365         gtk_list_store_insert_with_values(store, NULL, 0,
366             COL_TR_MSG, i,
367             COL_NAME, name,
368             COL_MESSAGE, i->message,
369             COL_SEQUENCE, ++sequence,
370             -1);
371 
372         /* if it's an error message, dump it to the terminal too */
373         if (i->level == TR_LOG_ERROR)
374         {
375             GString* gstr = g_string_sized_new(512);
376             g_string_append_printf(gstr, "%s:%d %s", i->file, i->line, i->message);
377 
378             if (i->name != NULL)
379             {
380                 g_string_append_printf(gstr, " (%s)", i->name);
381             }
382 
383             g_warning("%s", gstr->str);
384             g_string_free(gstr, TRUE);
385         }
386     }
387 
388     return i; /* tail */
389 }
390 
onRefresh(gpointer gdata)391 static gboolean onRefresh(gpointer gdata)
392 {
393     struct MsgData* data = gdata;
394     gboolean const pinned_to_new = is_pinned_to_new(data);
395 
396     if (!data->isPaused)
397     {
398         tr_log_message* msgs = tr_logGetQueue();
399 
400         if (msgs != NULL)
401         {
402             /* add the new messages and append them to the end of
403              * our persistent list */
404             tr_log_message* tail = addMessages(data->store, msgs);
405 
406             if (myTail != NULL)
407             {
408                 myTail->next = msgs;
409             }
410             else
411             {
412                 myHead = msgs;
413             }
414 
415             myTail = tail;
416         }
417 
418         if (pinned_to_new)
419         {
420             scroll_to_bottom(data);
421         }
422     }
423 
424     return G_SOURCE_CONTINUE;
425 }
426 
debug_level_combo_new(void)427 static GtkWidget* debug_level_combo_new(void)
428 {
429     GtkWidget* w = gtr_combo_box_new_enum(
430         _("Error"), TR_LOG_ERROR,
431         _("Information"), TR_LOG_INFO,
432         _("Debug"), TR_LOG_DEBUG,
433         NULL);
434     gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), gtr_pref_int_get(TR_KEY_message_level));
435     return w;
436 }
437 
438 /**
439 ***  Public Functions
440 **/
441 
gtr_message_log_window_new(GtkWindow * parent,TrCore * core)442 GtkWidget* gtr_message_log_window_new(GtkWindow* parent, TrCore* core)
443 {
444     GtkWidget* win;
445     GtkWidget* vbox;
446     GtkWidget* toolbar;
447     GtkWidget* w;
448     GtkWidget* view;
449     GtkToolItem* item;
450     struct MsgData* data;
451 
452     data = g_new0(struct MsgData, 1);
453     data->core = core;
454 
455     win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
456     gtk_window_set_transient_for(GTK_WINDOW(win), parent);
457     gtk_window_set_title(GTK_WINDOW(win), _("Message Log"));
458     gtk_window_set_default_size(GTK_WINDOW(win), 560, 350);
459     gtk_window_set_role(GTK_WINDOW(win), "message-log");
460     vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
461 
462     /**
463     ***  toolbar
464     **/
465 
466     toolbar = gtk_toolbar_new();
467     gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);
468     gtk_style_context_add_class(gtk_widget_get_style_context(toolbar), GTK_STYLE_CLASS_PRIMARY_TOOLBAR);
469 
470     item = gtk_tool_button_new_from_stock(GTK_STOCK_SAVE_AS);
471     g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
472     g_signal_connect(item, "clicked", G_CALLBACK(onSaveRequest), data);
473     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
474 
475     item = gtk_tool_button_new_from_stock(GTK_STOCK_CLEAR);
476     g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
477     g_signal_connect(item, "clicked", G_CALLBACK(onClearRequest), data);
478     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
479 
480     item = gtk_separator_tool_item_new();
481     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
482 
483     item = gtk_toggle_tool_button_new_from_stock(GTK_STOCK_MEDIA_PAUSE);
484     g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
485     g_signal_connect(item, "toggled", G_CALLBACK(onPauseToggled), data);
486     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
487 
488     item = gtk_separator_tool_item_new();
489     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
490 
491     w = gtk_label_new(_("Level"));
492     g_object_set(w, "margin", GUI_PAD, NULL);
493     item = gtk_tool_item_new();
494     gtk_container_add(GTK_CONTAINER(item), w);
495     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
496 
497     w = debug_level_combo_new();
498     g_signal_connect(w, "changed", G_CALLBACK(level_combo_changed_cb), data);
499     item = gtk_tool_item_new();
500     gtk_container_add(GTK_CONTAINER(item), w);
501     gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
502 
503     gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
504 
505     /**
506     ***  messages
507     **/
508 
509     data->store = gtk_list_store_new(N_COLUMNS,
510         G_TYPE_UINT, /* sequence */
511         G_TYPE_POINTER, /* category */
512         G_TYPE_POINTER, /* message */
513         G_TYPE_POINTER); /* struct tr_log_message */
514 
515     addMessages(data->store, myHead);
516     onRefresh(data); /* much faster to populate *before* it has listeners */
517 
518     data->filter = gtk_tree_model_filter_new(GTK_TREE_MODEL(data->store), NULL);
519     data->sort = gtk_tree_model_sort_new_with_model(data->filter);
520     g_object_unref(data->filter);
521     gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(data->sort), COL_SEQUENCE, GTK_SORT_ASCENDING);
522     data->maxLevel = gtr_pref_int_get(TR_KEY_message_level);
523     gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(data->filter), isRowVisible, data, NULL);
524 
525     view = gtk_tree_view_new_with_model(data->sort);
526     g_object_unref(data->sort);
527     g_signal_connect(view, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
528     data->view = GTK_TREE_VIEW(view);
529     appendColumn(data->view, COL_SEQUENCE);
530     appendColumn(data->view, COL_NAME);
531     appendColumn(data->view, COL_MESSAGE);
532     w = gtk_scrolled_window_new(NULL, NULL);
533     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
534     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
535     gtk_container_add(GTK_CONTAINER(w), view);
536     gtk_box_pack_start(GTK_BOX(vbox), w, TRUE, TRUE, 0);
537     gtk_container_add(GTK_CONTAINER(win), vbox);
538 
539     data->refresh_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, onRefresh, data);
540     g_object_weak_ref(G_OBJECT(win), onWindowDestroyed, data);
541 
542     scroll_to_bottom(data);
543     gtk_widget_show_all(win);
544     return win;
545 }
546