/* * 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_PREFS_KEY_MSGLEVEL, 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 MsgData * data = gdata; const struct tr_msg_list * node; 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 TRUE; } 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_PREFS_KEY_MSGLEVEL ) ); 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_PREFS_KEY_MSGLEVEL ); 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; }