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