mirror of
https://github.com/transmission/transmission
synced 2024-12-27 01:57:52 +00:00
879a2afcbd
The Berne Convention says that the copyright year is moot, so instead of adding another year to each file as in previous years, I've removed the year altogether from the source code comments in libtransmission, gtk, qt, utils, daemon, and cli. Juliusz's copyright notice in tr-dht and Johannes' copyright notice in tr-lpd have been left alone; it didn't seem appropriate to modify them.
540 lines
17 KiB
C
540 lines
17 KiB
C
/*
|
|
* 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 <errno.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <glib/gi18n.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
|
|
#include "conf.h"
|
|
#include "hig.h"
|
|
#include "tr-core.h"
|
|
#include "msgwin.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;
|
|
|
|
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 );
|
|
tr_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 );
|
|
}
|
|
|
|
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 )
|
|
{
|
|
const char * foreground;
|
|
|
|
switch( msgLevel )
|
|
{
|
|
case TR_MSG_DBG:
|
|
foreground = "forestgreen"; break;
|
|
|
|
case TR_MSG_INF:
|
|
foreground = "black"; break;
|
|
|
|
case TR_MSG_ERR:
|
|
foreground = "red"; break;
|
|
|
|
default:
|
|
g_assert_not_reached( );
|
|
}
|
|
return foreground;
|
|
}
|
|
|
|
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_column_set_sort_column_id( c, col );
|
|
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 );
|
|
}
|
|
|
|
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_vbox_new( FALSE, 0 );
|
|
|
|
/**
|
|
*** toolbar
|
|
**/
|
|
|
|
toolbar = gtk_toolbar_new( );
|
|
gtk_toolbar_set_style( GTK_TOOLBAR( toolbar ), GTK_TOOLBAR_BOTH_HORIZ );
|
|
|
|
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 );
|
|
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_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 = gtr_timeout_add_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;
|
|
}
|
|
|