source: trunk/gtk/msgwin.c

Last change on this file was 14241, checked in by jordan, 8 years ago

Copyedit the license's revised text: (1) remove unnecessary repitition use of the word 'license' from the top of the header and source files (2) add the standard 'we hope it's useful, but no warranty' clause to COPYING (3) make explicit that linking OpenSSL is allowed (see https://people.gnome.org/~markmc/openssl-and-the-gpl.html for background) (4) sync the Qt and GTK+ clients' license popups with COPYING's revised text

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