source: trunk/gtk/msgwin.c @ 12654

Last change on this file since 12654 was 12654, checked in by jordan, 10 years ago

remove the gtr_timeout_add_seconds() portability wrapper around gdk_threads_add_timeout_seconds(); it's unnecessary now that the minimum gtk version's been bumped.

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