/* * This file Copyright (C) Mnemosyne LLC * * This file is licensed by the GPL version 2. Works owned by the * Transmission project are granted a special exemption to clause 2 (b) * so that the bulk of its code can remain under the MIT license. * This exemption does not extend to derived works not owned by * the Transmission project. * * $Id$ */ #include #include #include #include #include #include #include "conf.h" #include "hig.h" #include "msgwin.h" #include "tr-core.h" #include "tr-prefs.h" #include "util.h" enum { COL_SEQUENCE, COL_NAME, COL_MESSAGE, COL_TR_MSG, N_COLUMNS }; struct MsgData { TrCore * core; GtkTreeView * view; GtkListStore * store; GtkTreeModel * filter; GtkTreeModel * sort; tr_msg_level maxLevel; gboolean isPaused; guint refresh_tag; }; static struct tr_msg_list * myTail = NULL; static struct tr_msg_list * myHead = NULL; /**** ***** ****/ /* is the user looking at the latest messages? */ static gboolean is_pinned_to_new (struct MsgData * data) { gboolean pinned_to_new = FALSE; if (data->view == NULL) { pinned_to_new = TRUE; } else { GtkTreePath * last_visible; if (gtk_tree_view_get_visible_range (data->view, NULL, &last_visible)) { GtkTreeIter iter; const int row_count = gtk_tree_model_iter_n_children (data->sort, NULL); if (gtk_tree_model_iter_nth_child (data->sort, &iter, NULL, row_count-1)) { GtkTreePath * last_row = gtk_tree_model_get_path (data->sort, &iter); pinned_to_new = !gtk_tree_path_compare (last_visible, last_row); gtk_tree_path_free (last_row); } gtk_tree_path_free (last_visible); } } return pinned_to_new; } static void scroll_to_bottom (struct MsgData * data) { if (data->sort != NULL) { GtkTreeIter iter; const int row_count = gtk_tree_model_iter_n_children (data->sort, NULL); if (gtk_tree_model_iter_nth_child (data->sort, &iter, NULL, row_count-1)) { GtkTreePath * last_row = gtk_tree_model_get_path (data->sort, &iter); gtk_tree_view_scroll_to_cell (data->view, last_row, NULL, TRUE, 1, 0); gtk_tree_path_free (last_row); } } } /**** ***** ****/ static void level_combo_changed_cb (GtkComboBox * combo_box, gpointer gdata) { struct MsgData * data = gdata; const int level = gtr_combo_box_get_active_enum (combo_box); const gboolean pinned_to_new = is_pinned_to_new (data); tr_setMessageLevel (level); gtr_core_set_pref_int (data->core, TR_KEY_message_level, level); data->maxLevel = level; gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (data->filter)); if (pinned_to_new) scroll_to_bottom (data); } /* similar to asctime, but is utf8-clean */ static char* gtr_localtime (time_t time) { char buf[256], *eoln; const struct tm tm = *localtime (&time); g_strlcpy (buf, asctime (&tm), sizeof (buf)); if ((eoln = strchr (buf, '\n'))) *eoln = '\0'; return g_locale_to_utf8 (buf, -1, NULL, NULL, NULL); } static void doSave (GtkWindow * parent, struct MsgData * data, const char * filename) { FILE * fp = fopen (filename, "w+"); if (!fp) { GtkWidget * w = gtk_message_dialog_new (parent, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Couldn't save \"%s\""), filename); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (w), "%s", g_strerror (errno)); g_signal_connect_swapped (w, "response", G_CALLBACK (gtk_widget_destroy), w); gtk_widget_show (w); } else { GtkTreeIter iter; GtkTreeModel * model = GTK_TREE_MODEL (data->sort); if (gtk_tree_model_iter_children (model, &iter, NULL)) do { char * date; const char * levelStr; const struct tr_msg_list * node; gtk_tree_model_get (model, &iter, COL_TR_MSG, &node, -1); date = gtr_localtime (node->when); switch (node->level) { case TR_MSG_DBG: levelStr = "debug"; break; case TR_MSG_ERR: levelStr = "error"; break; default: levelStr = " "; break; } fprintf (fp, "%s\t%s\t%s\t%s\n", date, levelStr, (node->name ? node->name : ""), (node->message ? node->message : "")); g_free (date); } while (gtk_tree_model_iter_next (model, &iter)); fclose (fp); } } static void onSaveDialogResponse (GtkWidget * d, int response, gpointer data) { if (response == GTK_RESPONSE_ACCEPT) { char * file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (d)); doSave (GTK_WINDOW (d), data, file); g_free (file); } gtk_widget_destroy (d); } static void onSaveRequest (GtkWidget * w, gpointer data) { GtkWindow * window = GTK_WINDOW (gtk_widget_get_toplevel (w)); GtkWidget * d = gtk_file_chooser_dialog_new (_("Save Log"), window, GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_alternative_button_order (GTK_DIALOG (d), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1); g_signal_connect (d, "response", G_CALLBACK (onSaveDialogResponse), data); gtk_widget_show (d); } static void onClearRequest (GtkWidget * w UNUSED, gpointer gdata) { struct MsgData * data = gdata; gtk_list_store_clear (data->store); tr_freeMessageList (myHead); myHead = myTail = NULL; } static void onPauseToggled (GtkToggleToolButton * w, gpointer gdata) { struct MsgData * data = gdata; data->isPaused = gtk_toggle_tool_button_get_active (w); } static const char* getForegroundColor (int msgLevel) { switch (msgLevel) { case TR_MSG_DBG: return "forestgreen"; case TR_MSG_INF: return "black"; case TR_MSG_ERR: return "red"; default: g_assert_not_reached (); return "black"; } } static void renderText (GtkTreeViewColumn * column UNUSED, GtkCellRenderer * renderer, GtkTreeModel * tree_model, GtkTreeIter * iter, gpointer gcol) { const int col = GPOINTER_TO_INT (gcol); char * str = NULL; const struct tr_msg_list * node; gtk_tree_model_get (tree_model, iter, col, &str, COL_TR_MSG, &node, -1); g_object_set (renderer, "text", str, "foreground", getForegroundColor (node->level), "ellipsize", PANGO_ELLIPSIZE_END, NULL); } static void renderTime (GtkTreeViewColumn * column UNUSED, GtkCellRenderer * renderer, GtkTreeModel * tree_model, GtkTreeIter * iter, gpointer data UNUSED) { struct tm tm; char buf[16]; const struct tr_msg_list * node; gtk_tree_model_get (tree_model, iter, COL_TR_MSG, &node, -1); tm = *localtime (&node->when); g_snprintf (buf, sizeof (buf), "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); g_object_set (renderer, "text", buf, "foreground", getForegroundColor (node->level), NULL); } static void appendColumn (GtkTreeView * view, int col) { GtkCellRenderer * r; GtkTreeViewColumn * c; const char * title = NULL; switch (col) { case COL_SEQUENCE: title = _("Time"); break; /* noun. column title for a list */ case COL_NAME: title = _("Name"); break; /* noun. column title for a list */ case COL_MESSAGE: title = _("Message"); break; default: g_assert_not_reached (); } switch (col) { case COL_NAME: r = gtk_cell_renderer_text_new (); c = gtk_tree_view_column_new_with_attributes (title, r, NULL); gtk_tree_view_column_set_cell_data_func (c, r, renderText, GINT_TO_POINTER (col), NULL); gtk_tree_view_column_set_sizing (c, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_fixed_width (c, 200); gtk_tree_view_column_set_resizable (c, TRUE); break; case COL_MESSAGE: r = gtk_cell_renderer_text_new (); c = gtk_tree_view_column_new_with_attributes (title, r, NULL); gtk_tree_view_column_set_cell_data_func (c, r, renderText, GINT_TO_POINTER (col), NULL); gtk_tree_view_column_set_sizing (c, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_fixed_width (c, 500); gtk_tree_view_column_set_resizable (c, TRUE); break; case COL_SEQUENCE: r = gtk_cell_renderer_text_new (); c = gtk_tree_view_column_new_with_attributes (title, r, NULL); gtk_tree_view_column_set_cell_data_func (c, r, renderTime, NULL, NULL); gtk_tree_view_column_set_resizable (c, TRUE); break; default: g_assert_not_reached (); break; } gtk_tree_view_append_column (view, c); } static gboolean isRowVisible (GtkTreeModel * model, GtkTreeIter * iter, gpointer gdata) { const struct tr_msg_list * node; const struct MsgData * data = gdata; gtk_tree_model_get (model, iter, COL_TR_MSG, &node, -1); return node->level <= data->maxLevel; } static void onWindowDestroyed (gpointer gdata, GObject * deadWindow UNUSED) { struct MsgData * data = gdata; g_source_remove (data->refresh_tag); g_free (data); } static tr_msg_list * addMessages (GtkListStore * store, struct tr_msg_list * head) { tr_msg_list * i; static unsigned int sequence = 0; const char * default_name = g_get_application_name (); for (i=head; i && i->next; i=i->next) { const char * name = i->name ? i->name : default_name; gtk_list_store_insert_with_values (store, NULL, 0, COL_TR_MSG, i, COL_NAME, name, COL_MESSAGE, i->message, COL_SEQUENCE, ++sequence, -1); /* if it's an error message, dump it to the terminal too */ if (i->level == TR_MSG_ERR) { GString * gstr = g_string_sized_new (512); g_string_append_printf (gstr, "%s:%d %s", i->file, i->line, i->message); if (i->name != NULL) g_string_append_printf (gstr, " (%s)", i->name); g_warning ("%s", gstr->str); g_string_free (gstr, TRUE); } } return i; /* tail */ } static gboolean onRefresh (gpointer gdata) { struct MsgData * data = gdata; const gboolean pinned_to_new = is_pinned_to_new (data); if (!data->isPaused) { tr_msg_list * msgs = tr_getQueuedMessages (); if (msgs) { /* add the new messages and append them to the end of * our persistent list */ tr_msg_list * tail = addMessages (data->store, msgs); if (myTail) myTail->next = msgs; else myHead = msgs; myTail = tail; } if (pinned_to_new) scroll_to_bottom (data); } return G_SOURCE_CONTINUE; } static GtkWidget* debug_level_combo_new (void) { GtkWidget * w = gtr_combo_box_new_enum (_("Error"), TR_MSG_ERR, _("Information"), TR_MSG_INF, _("Debug"), TR_MSG_DBG, NULL); gtr_combo_box_set_active_enum (GTK_COMBO_BOX (w), gtr_pref_int_get (TR_KEY_message_level)); return w; } /** *** Public Functions **/ GtkWidget * gtr_message_log_window_new (GtkWindow * parent, TrCore * core) { GtkWidget * win; GtkWidget * vbox; GtkWidget * toolbar; GtkWidget * w; GtkWidget * view; GtkToolItem * item; struct MsgData * data; data = g_new0 (struct MsgData, 1); data->core = core; win = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_transient_for (GTK_WINDOW (win), parent); gtk_window_set_title (GTK_WINDOW (win), _("Message Log")); gtk_window_set_default_size (GTK_WINDOW (win), 560, 350); gtk_window_set_role (GTK_WINDOW (win), "message-log"); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); /** *** toolbar **/ toolbar = gtk_toolbar_new (); gtk_toolbar_set_style (GTK_TOOLBAR (toolbar), GTK_TOOLBAR_BOTH_HORIZ); gtk_style_context_add_class (gtk_widget_get_style_context (toolbar), GTK_STYLE_CLASS_PRIMARY_TOOLBAR); item = gtk_tool_button_new_from_stock (GTK_STOCK_SAVE_AS); g_object_set (G_OBJECT (item), "is-important", TRUE, NULL); g_signal_connect (item, "clicked", G_CALLBACK (onSaveRequest), data); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); item = gtk_tool_button_new_from_stock (GTK_STOCK_CLEAR); g_object_set (G_OBJECT (item), "is-important", TRUE, NULL); g_signal_connect (item, "clicked", G_CALLBACK (onClearRequest), data); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); item = gtk_separator_tool_item_new (); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); item = gtk_toggle_tool_button_new_from_stock (GTK_STOCK_MEDIA_PAUSE); g_object_set (G_OBJECT (item), "is-important", TRUE, NULL); g_signal_connect (item, "toggled", G_CALLBACK (onPauseToggled), data); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); item = gtk_separator_tool_item_new (); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); w = gtk_label_new (_("Level")); gtk_misc_set_padding (GTK_MISC (w), GUI_PAD, 0); item = gtk_tool_item_new (); gtk_container_add (GTK_CONTAINER (item), w); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); w = debug_level_combo_new (); g_signal_connect (w, "changed", G_CALLBACK (level_combo_changed_cb), data); item = gtk_tool_item_new (); gtk_container_add (GTK_CONTAINER (item), w); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); gtk_box_pack_start (GTK_BOX (vbox), toolbar, FALSE, FALSE, 0); /** *** messages **/ data->store = gtk_list_store_new (N_COLUMNS, G_TYPE_UINT, /* sequence */ G_TYPE_POINTER, /* category */ G_TYPE_POINTER, /* message */ G_TYPE_POINTER); /* struct tr_msg_list */ addMessages (data->store, myHead); onRefresh (data); /* much faster to populate *before* it has listeners */ data->filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (data->store), NULL); data->sort = gtk_tree_model_sort_new_with_model (data->filter); g_object_unref (data->filter); gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (data->sort), COL_SEQUENCE, GTK_SORT_ASCENDING); data->maxLevel = gtr_pref_int_get (TR_KEY_message_level); gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (data->filter), isRowVisible, data, NULL); view = gtk_tree_view_new_with_model (data->sort); g_object_unref (data->sort); g_signal_connect (view, "button-release-event", G_CALLBACK (on_tree_view_button_released), NULL); data->view = GTK_TREE_VIEW (view); gtk_tree_view_set_rules_hint (data->view, TRUE); appendColumn (data->view, COL_SEQUENCE); appendColumn (data->view, COL_NAME); appendColumn (data->view, COL_MESSAGE); w = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (w), GTK_SHADOW_IN); gtk_container_add (GTK_CONTAINER (w), view); gtk_box_pack_start (GTK_BOX (vbox), w, TRUE, TRUE, 0); gtk_container_add (GTK_CONTAINER (win), vbox); data->refresh_tag = gdk_threads_add_timeout_seconds (SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, onRefresh, data); g_object_weak_ref (G_OBJECT (win), onWindowDestroyed, data); scroll_to_bottom (data); gtk_widget_show_all (win); return win; }