source: trunk/gtk/details.c @ 8706

Last change on this file since 8706 was 8706, checked in by charles, 13 years ago

(trunk gtk) plug very minor memory leak in the gtk properties dialog, found by valgrind

  • Property svn:keywords set to Date Rev Author Id
File size: 66.2 KB
Line 
1/*
2 * This file Copyright (C) 2007-2009 Charles Kerr <charles@transmissionbt.com>
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: details.c 8706 2009-06-15 21:01:20Z charles $
11 */
12
13#include <assert.h>
14#include <math.h> /* ceil() */
15#include <stddef.h>
16#include <stdio.h> /* sscanf */
17#include <stdlib.h>
18#include <glib/gi18n.h>
19#include <gtk/gtk.h>
20
21#include <libtransmission/transmission.h>
22#include <libtransmission/utils.h> /* tr_free */
23
24#include "actions.h"
25#include "details.h"
26#include "file-list.h"
27#include "tracker-list.h"
28#include "hig.h"
29#include "util.h"
30
31#define DETAILS_KEY "details-data"
32
33#define UPDATE_INTERVAL_SECONDS 2
34
35struct DetailsImpl
36{
37    GtkWidget * peersPage;
38    GtkWidget * trackerPage;
39    GtkWidget * activityPage;
40
41    GtkWidget * honorLimitsCheck;
42    GtkWidget * upLimitedCheck;
43    GtkWidget * upLimitSpin;
44    GtkWidget * downLimitedCheck;
45    GtkWidget * downLimitSpin;
46    GtkWidget * bandwidthCombo;
47    GtkWidget * seedGlobalRadio;
48    GtkWidget * seedForeverRadio;
49    GtkWidget * seedCustomRadio;
50    GtkWidget * seedCustomSpin;
51    GtkWidget * maxPeersSpin;
52
53    guint honorLimitsCheckTag;
54    guint upLimitedCheckTag;
55    guint downLimitedCheckTag;
56    guint downLimitSpinTag;
57    guint upLimitSpinTag;
58    guint bandwidthComboTag;
59    guint seedForeverRadioTag;
60    guint seedGlobalRadioTag;
61    guint seedCustomRadioTag;
62    guint seedCustomSpinTag;
63    guint maxPeersSpinTag;
64
65    GtkWidget * state_lb;
66    GtkWidget * progress_lb;
67    GtkWidget * have_lb;
68    GtkWidget * dl_lb;
69    GtkWidget * ul_lb;
70    GtkWidget * failed_lb;
71    GtkWidget * ratio_lb;
72    GtkWidget * error_lb;
73    GtkWidget * swarm_lb;
74    GtkWidget * date_added_lb;
75    GtkWidget * last_activity_lb;
76
77    GtkWidget * pieces_lb;
78    GtkWidget * hash_lb;
79    GtkWidget * privacy_lb;
80    GtkWidget * creator_lb;
81    GtkWidget * date_created_lb;
82    GtkWidget * destination_lb;
83    GtkWidget * torrentfile_lb;
84    GtkTextBuffer * comment_buffer;
85
86    GHashTable * peer_hash;
87    GHashTable * webseed_hash;
88    GtkListStore * peer_store;
89    GtkListStore * webseed_store;
90    GtkWidget * seeders_lb;
91    GtkWidget * leechers_lb;
92    GtkWidget * completed_lb;
93    GtkWidget * webseed_view;
94
95    GtkWidget * tracker_list;
96    GtkWidget * last_scrape_time_lb;
97    GtkWidget * last_scrape_response_lb;
98    GtkWidget * next_scrape_countdown_lb;
99    GtkWidget * last_announce_time_lb;
100    GtkWidget * last_announce_response_lb;
101    GtkWidget * next_announce_countdown_lb;
102    GtkWidget * manual_announce_countdown_lb;
103
104    GtkWidget * file_list;
105
106    GSList * ids;
107    TrCore * core;
108    guint periodic_refresh_tag;
109    guint prefs_changed_tag;
110};
111
112static tr_torrent**
113getTorrents( struct DetailsImpl * d, int * setmeCount )
114{
115    int n = g_slist_length( d->ids );
116    int torrentCount = 0;
117    tr_session * session = tr_core_session( d->core );
118    tr_torrent ** torrents = NULL;
119
120    if( session != NULL )
121    {
122        GSList * l;
123
124        torrents = g_new( tr_torrent*, n );
125
126        for( l=d->ids; l!=NULL; l=l->next ) {
127            const int id = GPOINTER_TO_INT( l->data );
128            tr_torrent * tor = tr_torrentFindFromId( session, id );
129            if( tor )
130                torrents[torrentCount++] = tor;
131        }
132    }
133
134    *setmeCount = torrentCount;
135    return torrents;
136}
137
138/****
139*****
140*****  OPTIONS TAB
141*****
142****/
143
144static void
145set_togglebutton_if_different( GtkWidget * w, guint tag, gboolean value )
146{
147    GtkToggleButton * toggle = GTK_TOGGLE_BUTTON( w );
148    const gboolean currentValue = gtk_toggle_button_get_active( toggle );
149    if( currentValue != value )
150    {
151        g_signal_handler_block( toggle, tag );
152        gtk_toggle_button_set_active( toggle, value );
153        g_signal_handler_unblock( toggle, tag );
154    }
155}
156
157static void
158set_int_spin_if_different( GtkWidget * w, guint tag, int value )
159{
160    GtkSpinButton * spin = GTK_SPIN_BUTTON( w );
161    const int currentValue = gtk_spin_button_get_value_as_int( spin );
162    if( currentValue != value )
163    {
164        g_signal_handler_block( spin, tag );
165        gtk_spin_button_set_value( spin, value );
166        g_signal_handler_unblock( spin, tag );
167    }
168}
169
170static void
171set_double_spin_if_different( GtkWidget * w, guint tag, double value )
172{
173    GtkSpinButton * spin = GTK_SPIN_BUTTON( w );
174    const double currentValue = gtk_spin_button_get_value( spin );
175    if( ( (int)(currentValue*100) != (int)(value*100) ) )
176    {
177        g_signal_handler_block( spin, tag );
178        gtk_spin_button_set_value( spin, value );
179        g_signal_handler_unblock( spin, tag );
180    }
181}
182
183static void
184set_int_combo_if_different( GtkWidget * w, guint tag, int column, int value )
185{
186    int i;
187    int currentValue;
188    GtkTreeIter iter;
189    GtkComboBox * combobox = GTK_COMBO_BOX( w );
190    GtkTreeModel * model = gtk_combo_box_get_model( combobox );
191
192    /* do the value and current value match? */
193    if( gtk_combo_box_get_active_iter( combobox, &iter ) ) {
194        gtk_tree_model_get( model, &iter, column, &currentValue, -1 );
195        if( currentValue == value )
196            return;
197    }
198
199    /* find the one to select */
200    i = 0;
201    while(( gtk_tree_model_iter_nth_child( model, &iter, NULL, i++ ))) {
202        gtk_tree_model_get( model, &iter, column, &currentValue, -1 );
203        if( currentValue == value ) {
204            g_signal_handler_block( combobox, tag );
205            gtk_combo_box_set_active_iter( combobox, &iter );
206            g_signal_handler_unblock( combobox, tag );
207            return;
208        }
209    }
210}
211
212static void
213unset_combo( GtkWidget * w, guint tag )
214{
215    GtkComboBox * combobox = GTK_COMBO_BOX( w );
216
217    g_signal_handler_block( combobox, tag );
218    gtk_combo_box_set_active( combobox, -1 );
219    g_signal_handler_unblock( combobox, tag );
220}
221
222static void
223refreshOptions( struct DetailsImpl * di, tr_torrent ** torrents, int n )
224{
225    /***
226    ****  Options Page
227    ***/
228
229    /* honorLimitsCheck */
230    if( n ) {
231        const tr_bool baseline = tr_torrentUsesSessionLimits( torrents[0] );
232        int i;
233        for( i=1; i<n; ++i )
234            if( baseline != tr_torrentUsesSessionLimits( torrents[i] ) )
235                break;
236        if( i == n )
237            set_togglebutton_if_different( di->honorLimitsCheck,
238                                           di->honorLimitsCheckTag, baseline );
239    }
240   
241    /* downLimitedCheck */
242    if( n ) {
243        const tr_bool baseline = tr_torrentUsesSpeedLimit( torrents[0], TR_DOWN );
244        int i;
245        for( i=1; i<n; ++i )
246            if( baseline != tr_torrentUsesSpeedLimit( torrents[i], TR_DOWN ) )
247                break;
248        if( i == n )
249            set_togglebutton_if_different( di->downLimitedCheck,
250                                           di->downLimitedCheckTag, baseline );
251    }
252
253    /* downLimitSpin */
254    if( n ) {
255        const int baseline = tr_torrentGetSpeedLimit( torrents[0], TR_DOWN );
256        int i;
257        for( i=1; i<n; ++i )
258            if( baseline != tr_torrentGetSpeedLimit( torrents[i], TR_DOWN ) )
259                break;
260        if( i == n )
261            set_int_spin_if_different( di->downLimitSpin,
262                                       di->downLimitSpinTag, baseline );
263    }
264   
265    /* upLimitedCheck */
266    if( n ) {
267        const tr_bool baseline = tr_torrentUsesSpeedLimit( torrents[0], TR_UP );
268        int i;
269        for( i=1; i<n; ++i )
270            if( baseline != tr_torrentUsesSpeedLimit( torrents[i], TR_UP ) )
271                break;
272        if( i == n )
273            set_togglebutton_if_different( di->upLimitedCheck,
274                                           di->upLimitedCheckTag, baseline );
275    }
276
277    /* upLimitSpin */
278    if( n ) {
279        const int baseline = tr_torrentGetSpeedLimit( torrents[0], TR_UP );
280        int i;
281        for( i=1; i<n; ++i )
282            if( baseline != tr_torrentGetSpeedLimit( torrents[i], TR_UP ) )
283                break;
284        if( i == n )
285            set_int_spin_if_different( di->upLimitSpin,
286                                       di->upLimitSpinTag, baseline );
287    }
288
289    /* bandwidthCombo */
290    if( n ) {
291        const int baseline = tr_torrentGetPriority( torrents[0] );
292        int i;
293        for( i=1; i<n; ++i )
294            if( baseline != tr_torrentGetPriority( torrents[i] ) )
295                break;
296        if( i == n )
297            set_int_combo_if_different( di->bandwidthCombo,
298                                        di->bandwidthComboTag, 0, baseline );
299        else
300            unset_combo( di->bandwidthCombo, di->bandwidthComboTag );
301    }
302
303    /* seedGlobalRadio */
304    /* seedForeverRadio */
305    /* seedCustomRadio */
306    if( n ) {
307        guint t;
308        const int baseline = tr_torrentGetRatioMode( torrents[0] );
309        int i;
310        for( i=1; i<n; ++i )
311            if( baseline != (int)tr_torrentGetRatioMode( torrents[i] ) )
312                break;
313        if( i == n ) {
314            GtkWidget * w;
315            switch( baseline ) {
316                case TR_RATIOLIMIT_SINGLE: w = di->seedCustomRadio;
317                                           t = di->seedCustomRadioTag; break;
318                case TR_RATIOLIMIT_UNLIMITED: w = di->seedForeverRadio;
319                                              t = di->seedForeverRadioTag; break;
320                default /*TR_RATIOLIMIT_GLOBAL*/: w = di->seedGlobalRadio;
321                                                  t = di->seedGlobalRadioTag; break;
322            }
323            set_togglebutton_if_different( w, t, TRUE );
324        }
325    }
326
327    /* seedCustomSpin */
328    if( n ) {
329        const double baseline = tr_torrentGetRatioLimit( torrents[0] );
330        set_double_spin_if_different( di->seedCustomSpin,
331                                      di->seedCustomSpinTag, baseline );
332    }
333
334    /* maxPeersSpin */
335    if( n ) {
336        const int baseline = tr_torrentGetPeerLimit( torrents[0] );
337        set_int_spin_if_different( di->maxPeersSpin,
338                                   di->maxPeersSpinTag, baseline );
339    }
340}
341
342static void
343torrent_set_bool( struct DetailsImpl * di, const char * key, gboolean value )
344{
345    GSList *l;
346    tr_benc top, *args, *ids;
347
348    tr_bencInitDict( &top, 2 );
349    tr_bencDictAddStr( &top, "method", "torrent-set" );
350    args = tr_bencDictAddDict( &top, "arguments", 2 );
351    tr_bencDictAddBool( args, key, value );
352    ids = tr_bencDictAddList( args, "ids", g_slist_length(di->ids) );
353    for( l=di->ids; l; l=l->next )
354        tr_bencListAddInt( ids, GPOINTER_TO_INT( l->data ) );
355
356    tr_core_exec( di->core, &top );
357    tr_bencFree( &top );
358}
359
360static void
361torrent_set_int( struct DetailsImpl * di, const char * key, int value )
362{
363    GSList *l;
364    tr_benc top, *args, *ids;
365
366    tr_bencInitDict( &top, 2 );
367    tr_bencDictAddStr( &top, "method", "torrent-set" );
368    args = tr_bencDictAddDict( &top, "arguments", 2 );
369    tr_bencDictAddInt( args, key, value );
370    ids = tr_bencDictAddList( args, "ids", g_slist_length(di->ids) );
371    for( l=di->ids; l; l=l->next )
372        tr_bencListAddInt( ids, GPOINTER_TO_INT( l->data ) );
373
374    tr_core_exec( di->core, &top );
375    tr_bencFree( &top );
376}
377
378static void
379torrent_set_real( struct DetailsImpl * di, const char * key, double value )
380{
381    GSList *l;
382    tr_benc top, *args, *ids;
383
384    tr_bencInitDict( &top, 2 );
385    tr_bencDictAddStr( &top, "method", "torrent-set" );
386    args = tr_bencDictAddDict( &top, "arguments", 2 );
387    tr_bencDictAddReal( args, key, value );
388    ids = tr_bencDictAddList( args, "ids", g_slist_length(di->ids) );
389    for( l=di->ids; l; l=l->next )
390        tr_bencListAddInt( ids, GPOINTER_TO_INT( l->data ) );
391
392    tr_core_exec( di->core, &top );
393    tr_bencFree( &top );
394}
395
396static void
397up_speed_toggled_cb( GtkToggleButton * tb, gpointer d )
398{
399    torrent_set_bool( d, "uploadLimited", gtk_toggle_button_get_active( tb ) );
400}
401
402static void
403down_speed_toggled_cb( GtkToggleButton *tb, gpointer d )
404{
405    torrent_set_bool( d, "downloadLimited", gtk_toggle_button_get_active( tb ) );
406}
407
408static void
409global_speed_toggled_cb( GtkToggleButton * tb, gpointer d )
410{
411    torrent_set_bool( d, "honorsSessionLimits", gtk_toggle_button_get_active( tb ) );
412}
413
414#define RATIO_KEY "ratio-mode"
415
416static void
417ratio_mode_changed_cb( GtkToggleButton * tb, struct DetailsImpl * d )
418{
419    if( gtk_toggle_button_get_active( tb ) )
420    {
421        GObject * o = G_OBJECT( tb );
422        const int mode = GPOINTER_TO_INT( g_object_get_data( o, RATIO_KEY ) );
423        torrent_set_int( d, "seedRatioMode", mode );
424    }
425}
426
427static void
428up_speed_spun_cb( GtkSpinButton * s, struct DetailsImpl * di )
429{
430    torrent_set_int( di, "uploadLimit", gtk_spin_button_get_value_as_int( s ) );
431}
432
433static void
434down_speed_spun_cb( GtkSpinButton * s, struct DetailsImpl * di )
435{
436    torrent_set_int( di, "downloadLimit", gtk_spin_button_get_value_as_int( s ) );
437}
438
439static void
440ratio_spun_cb( GtkSpinButton * s, struct DetailsImpl * di )
441{
442    torrent_set_real( di, "seedRatioLimit", gtk_spin_button_get_value( s ) );
443}
444
445static void
446max_peers_spun_cb( GtkSpinButton * s, struct DetailsImpl * di )
447{
448    torrent_set_int( di, "peer-limit", gtk_spin_button_get_value( s ) );
449}
450
451static char*
452get_global_ratio_radiobutton_string( void )
453{
454    char * s;
455    const gboolean b = pref_flag_get( TR_PREFS_KEY_RATIO_ENABLED );
456    const double d = pref_double_get( TR_PREFS_KEY_RATIO );
457
458    if( b )
459        s = g_strdup_printf( _( "Use _Global setting  (currently: stop seeding when a torrent's ratio reaches %.2f)" ), d );
460    else
461        s = g_strdup( _( "Use _Global setting  (currently: seed regardless of ratio)" ) );
462
463    return s;
464}
465
466static void
467prefsChanged( TrCore * core UNUSED, const char *  key, gpointer rb )
468{
469    if( !strcmp( key, TR_PREFS_KEY_RATIO_ENABLED ) ||
470        !strcmp( key, TR_PREFS_KEY_RATIO ) )
471    {
472        char * s = get_global_ratio_radiobutton_string( );
473        gtk_button_set_label( GTK_BUTTON( rb ), s );
474        g_free( s );
475    }
476}
477
478static void
479onPriorityChanged( GtkComboBox * w, struct DetailsImpl * di )
480{
481    GtkTreeIter iter;
482
483    if( gtk_combo_box_get_active_iter( w, &iter ) )
484    {
485        int val = 0;
486        gtk_tree_model_get( gtk_combo_box_get_model( w ), &iter, 0, &val, -1 );
487        torrent_set_int( di, "bandwidthPriority", val );
488    }
489}
490
491static GtkWidget*
492new_priority_combo( struct DetailsImpl * di )
493{
494    int i;
495    guint tag;
496    GtkWidget * w;
497    GtkCellRenderer * r;
498    GtkListStore * store;
499    const struct {
500        int value;
501        const char * text;
502    } items[] = {
503        { TR_PRI_LOW,    N_( "Low" )  },
504        { TR_PRI_NORMAL, N_( "Normal" ) },
505        { TR_PRI_HIGH,   N_( "High" )  }
506    };
507
508    store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING );
509    for( i=0; i<(int)G_N_ELEMENTS(items); ++i ) {
510        GtkTreeIter iter;
511        gtk_list_store_append( store, &iter );
512        gtk_list_store_set( store, &iter, 0, items[i].value,
513                                          1, _( items[i].text ),
514                                         -1 );
515    }
516
517    w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) );
518    r = gtk_cell_renderer_text_new( );
519    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
520    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( w ), r, "text", 1, NULL );
521    tag = g_signal_connect( w, "changed", G_CALLBACK( onPriorityChanged ), di );
522    di->bandwidthComboTag = tag;
523
524    /* cleanup */
525    g_object_unref( store );
526    return w;
527}
528
529
530static GtkWidget*
531options_page_new( struct DetailsImpl * d )
532{
533    guint tag;
534    int row;
535    char *s;
536    GSList *group;
537    GtkWidget *t, *w, *tb, *h;
538
539    row = 0;
540    t = hig_workarea_create( );
541    hig_workarea_add_section_title( t, &row, _( "Speed" ) );
542
543    tb = hig_workarea_add_wide_checkbutton( t, &row, _( "Honor global _limits" ), 0 );
544    d->honorLimitsCheck = tb;
545    tag = g_signal_connect( tb, "toggled", G_CALLBACK( global_speed_toggled_cb ), d );
546    d->honorLimitsCheckTag = tag;
547
548    tb = gtk_check_button_new_with_mnemonic( _( "Limit _download speed (KB/s):" ) );
549    gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( tb ), FALSE );
550    d->downLimitedCheck = tb;
551    tag = g_signal_connect( tb, "toggled", G_CALLBACK( down_speed_toggled_cb ), d );
552    d->downLimitedCheckTag = tag;
553
554    w = gtk_spin_button_new_with_range( 1, INT_MAX, 5 );
555    tag = g_signal_connect( w, "value-changed", G_CALLBACK( down_speed_spun_cb ), d );
556    d->downLimitSpinTag = tag;
557    hig_workarea_add_row_w( t, &row, tb, w, NULL );
558    d->downLimitSpin = w;
559
560    tb = gtk_check_button_new_with_mnemonic( _( "Limit _upload speed (KB/s):" ) );
561    d->upLimitedCheck = tb;
562    tag = g_signal_connect( tb, "toggled", G_CALLBACK( up_speed_toggled_cb ), d );
563    d->upLimitedCheckTag = tag;
564
565    w = gtk_spin_button_new_with_range( 1, INT_MAX, 5 );
566    tag = g_signal_connect( w, "value-changed", G_CALLBACK( up_speed_spun_cb ), d );
567    d->upLimitSpinTag = tag;
568    hig_workarea_add_row_w( t, &row, tb, w, NULL );
569    d->upLimitSpin = w;
570
571    w = new_priority_combo( d );
572    hig_workarea_add_row( t, &row, _( "_Bandwidth priority:" ), w, NULL );
573    d->bandwidthCombo = w;
574
575    hig_workarea_add_section_divider( t, &row );
576    hig_workarea_add_section_title( t, &row, _( "Seed-Until Ratio" ) );
577
578    group = NULL;
579    s = get_global_ratio_radiobutton_string( );
580    w = gtk_radio_button_new_with_mnemonic( group, s );
581    tag = g_signal_connect( d->core, "prefs-changed", G_CALLBACK( prefsChanged ), w );
582    d->prefs_changed_tag = tag;
583    group = gtk_radio_button_get_group( GTK_RADIO_BUTTON( w ) );
584    hig_workarea_add_wide_control( t, &row, w );
585    g_free( s );
586    g_object_set_data( G_OBJECT( w ), RATIO_KEY, GINT_TO_POINTER( TR_RATIOLIMIT_GLOBAL ) );
587    tag = g_signal_connect( w, "toggled", G_CALLBACK( ratio_mode_changed_cb ), d );
588    d->seedGlobalRadio = w;
589    d->seedGlobalRadioTag = tag;
590
591    w = gtk_radio_button_new_with_mnemonic( group, _( "Seed _regardless of ratio" ) );
592    group = gtk_radio_button_get_group( GTK_RADIO_BUTTON( w ) );
593    hig_workarea_add_wide_control( t, &row, w );
594    g_object_set_data( G_OBJECT( w ), RATIO_KEY, GINT_TO_POINTER( TR_RATIOLIMIT_UNLIMITED ) );
595    tag = g_signal_connect( w, "toggled", G_CALLBACK( ratio_mode_changed_cb ), d );
596    d->seedForeverRadio = w;
597    d->seedForeverRadioTag = tag;
598
599    h = gtk_hbox_new( FALSE, GUI_PAD );
600    w = gtk_radio_button_new_with_mnemonic( group, _( "_Stop seeding when a torrent's ratio reaches" ) );
601    d->seedCustomRadio = w;
602    g_object_set_data( G_OBJECT( w ), RATIO_KEY, GINT_TO_POINTER( TR_RATIOLIMIT_SINGLE ) );
603    tag = g_signal_connect( w, "toggled", G_CALLBACK( ratio_mode_changed_cb ), d );
604    d->seedCustomRadioTag = tag;
605    group = gtk_radio_button_get_group( GTK_RADIO_BUTTON( w ) );
606    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
607    w = gtk_spin_button_new_with_range( 0, INT_MAX, .05 );
608    gtk_spin_button_set_digits( GTK_SPIN_BUTTON( w ), 2 );
609    tag = g_signal_connect( w, "value-changed", G_CALLBACK( ratio_spun_cb ), d );
610    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
611    hig_workarea_add_wide_control( t, &row, h );
612    d->seedCustomSpin = w;
613    d->seedCustomSpinTag = tag;
614   
615    hig_workarea_add_section_divider( t, &row );
616    hig_workarea_add_section_title( t, &row, _( "Peer Connections" ) );
617
618    w = gtk_spin_button_new_with_range( 1, 3000, 5 );
619    hig_workarea_add_row( t, &row, _( "_Maximum peers:" ), w, w );
620    tag = g_signal_connect( w, "value-changed", G_CALLBACK( max_peers_spun_cb ), d );
621    d->maxPeersSpin = w;
622    d->maxPeersSpinTag = tag;
623
624    hig_workarea_finish( t, &row );
625    return t;
626}
627
628/****
629*****
630*****  ACTIVITY TAB
631*****
632****/
633
634static const char * activityString( int activity )
635{
636    switch( activity )
637    {
638        case TR_STATUS_CHECK_WAIT: return _( "Waiting to verify local data" ); break;
639        case TR_STATUS_CHECK:      return _( "Verifying local data" ); break;
640        case TR_STATUS_DOWNLOAD:   return _( "Downloading" ); break;
641        case TR_STATUS_SEED:       return _( "Seeding" ); break;
642        case TR_STATUS_STOPPED:    return _( "Paused" ); break;
643    }
644
645    return "";
646}
647
648static void
649refreshActivity( struct DetailsImpl * di, tr_torrent ** torrents, int n )
650{
651    int i;
652    const char * str;
653    const char * none = _( "None" );
654    const char * mixed = _( "Mixed" );
655    char buf[512];
656    const tr_stat ** stats = g_new( const tr_stat*, n );
657    for( i=0; i<n; ++i )
658        stats[i] = tr_torrentStatCached( torrents[i] );
659
660    /* state_lb */
661    if( n <= 0 )
662        str = none;
663    else {
664        const int baseline = stats[0]->activity;
665        for( i=1; i<n; ++i )
666            if( baseline != (int)stats[i]->activity )
667                break;
668        if( i==n )
669            str = activityString( baseline );
670        else
671            str = mixed;
672    }
673    gtk_label_set_text( GTK_LABEL( di->state_lb ), str );
674
675
676    /* progress_lb */
677    if( n <= 0 )
678        str = none;
679    else {
680        double sizeWhenDone = 0;
681        double leftUntilDone = 0;
682        for( i=0; i<n; ++i ) {
683            sizeWhenDone += stats[i]->sizeWhenDone;
684            leftUntilDone += stats[i]->leftUntilDone;
685        }
686        g_snprintf( buf, sizeof( buf ), _( "%.1f%%" ), 100.0*((sizeWhenDone-leftUntilDone)/sizeWhenDone) );
687        str = buf;
688    }
689    gtk_label_set_text( GTK_LABEL( di->progress_lb ), str );
690
691
692    /* have_lb */
693    if( n <= 0 )
694        str = none;
695    else {
696        char buf1[128];
697        char buf2[128];
698        double haveUnchecked = 0;
699        double haveValid = 0;
700        double verifiedPieces = 0;
701        for( i=0; i<n; ++i ) {
702            const double v = stats[i]->haveValid;
703            haveUnchecked += stats[i]->haveUnchecked;
704            haveValid += v;
705            verifiedPieces += v / tr_torrentInfo(torrents[i])->pieceSize;
706        }
707        tr_strlsize( buf1, haveValid + haveUnchecked, sizeof( buf1 ) );
708        tr_strlsize( buf2, haveValid, sizeof( buf2 ) );
709        i = (int) ceil( verifiedPieces );
710        g_snprintf( buf, sizeof( buf ), ngettext( "%1$s (%2$s verified in %3$d piece)",
711                                                  "%1$s (%2$s verified in %3$d pieces)",
712                                                  verifiedPieces ),
713                                        buf1, buf2, i );
714        str = buf;
715    }
716    gtk_label_set_text( GTK_LABEL( di->have_lb ), str );
717
718   
719    /* dl_lb */
720    if( n <= 0 )
721        str = none;
722    else {
723        uint64_t sum = 0;
724        for( i=0; i<n; ++i ) sum += stats[i]->downloadedEver;
725        str = tr_strlsize( buf, sum, sizeof( buf ) );
726    }
727    gtk_label_set_text( GTK_LABEL( di->dl_lb ), str );
728
729   
730    /* ul_lb */
731    if( n <= 0 )
732        str = none;
733    else {
734        uint64_t sum = 0;
735        for( i=0; i<n; ++i ) sum += stats[i]->uploadedEver;
736        str = tr_strlsize( buf, sum, sizeof( buf ) );
737    }
738    gtk_label_set_text( GTK_LABEL( di->ul_lb ), str );
739
740
741    /* corrupt ever */
742    if( n <= 0 )
743        str = none;
744    else {
745        uint64_t sum = 0;
746        for( i=0; i<n; ++i ) sum += stats[i]->corruptEver;
747        str = tr_strlsize( buf, sum, sizeof( buf ) );
748    }
749    gtk_label_set_text( GTK_LABEL( di->failed_lb ), str );
750
751
752    /* ratio */
753    if( n <= 0 )
754        str = none;
755    else {
756        uint64_t up = 0;
757        uint64_t down = 0;
758        for( i=0; i<n; ++i ) {
759            up += stats[i]->uploadedEver;
760            down += stats[i]->downloadedEver;
761        }
762        str = tr_strlratio( buf, tr_getRatio( up, down ), sizeof( buf ) );
763    }
764    gtk_label_set_text( GTK_LABEL( di->ratio_lb ), str );
765
766
767    /* swarmspeed */
768    if( n <= 0 )
769        str = none;
770    else {
771        double swarmSpeed = 0;
772        for( i=0; i<n; ++i )
773            swarmSpeed += stats[i]->swarmSpeed;
774        str = tr_strlspeed( buf, swarmSpeed, sizeof( buf ) );
775    }
776    gtk_label_set_text( GTK_LABEL( di->swarm_lb ), str );
777
778
779    /* error */
780    if( n <= 0 )
781        str = none;
782    else {
783        const char * baseline = stats[0]->errorString;
784        for( i=1; i<n; ++i )
785            if( strcmp( baseline, stats[i]->errorString ) )
786                break;
787        if( i==n )
788            str = baseline;
789        else
790            str = mixed;
791    }
792    if( !str || !*str )
793        str = none;
794    gtk_label_set_text( GTK_LABEL( di->error_lb ), str );
795
796
797    /* date added */
798    if( n <= 0 )
799        str = none;
800    else {
801        const time_t baseline = stats[0]->addedDate;
802        for( i=1; i<n; ++i )
803            if( baseline != stats[i]->addedDate )
804                break;
805        if( i==n )
806            str = gtr_localtime2( buf, baseline, sizeof( buf ) );
807        else
808            str = mixed;
809    }
810    gtk_label_set_text( GTK_LABEL( di->date_added_lb ), str );
811
812
813    /* activity date */
814    if( n <= 0 )
815        str = none;
816    else {
817        const time_t baseline = stats[0]->activityDate;
818        for( i=1; i<n; ++i )
819            if( baseline != stats[i]->activityDate )
820                break;
821        if( i==n )
822            str = gtr_localtime2( buf, baseline, sizeof( buf ) );
823        else
824            str = mixed;
825    }
826    gtk_label_set_text( GTK_LABEL( di->last_activity_lb ), str );
827
828    g_free( stats );
829}
830
831static GtkWidget*
832activity_page_new( struct DetailsImpl * di )
833{
834    int  row = 0;
835    GtkWidget * l;
836    GtkWidget * t = hig_workarea_create( );
837
838    hig_workarea_add_section_title( t, &row, _( "Transfer" ) );
839
840    l = di->state_lb = gtk_label_new( NULL );
841    hig_workarea_add_row( t, &row, _( "State:" ), l, NULL );
842
843    l = di->progress_lb = gtk_label_new( NULL );
844    hig_workarea_add_row( t, &row, _( "Progress:" ), l, NULL );
845
846    l = di->have_lb = gtk_label_new( NULL );
847    /* "Have" refers to how much of the torrent we have */
848    hig_workarea_add_row( t, &row, _( "Have:" ), l, NULL );
849
850    l = di->dl_lb = gtk_label_new( NULL );
851    hig_workarea_add_row( t, &row, _( "Downloaded:" ), l, NULL );
852
853    l = di->ul_lb = gtk_label_new( NULL );
854    hig_workarea_add_row( t, &row, _( "Uploaded:" ), l, NULL );
855
856    /* how much downloaded data was corrupt */
857    l = di->failed_lb = gtk_label_new( NULL );
858    hig_workarea_add_row( t, &row, _( "Failed DL:" ), l, NULL );
859
860    l = di->ratio_lb = gtk_label_new( NULL );
861    hig_workarea_add_row( t, &row, _( "Ratio:" ), l, NULL );
862
863    l = di->swarm_lb = gtk_label_new( NULL );
864    hig_workarea_add_row( t, &row, _( "Swarm speed:" ), l, NULL );
865
866    l = di->error_lb = gtk_label_new( NULL );
867    hig_workarea_add_row( t, &row, _( "Error:" ), l, NULL );
868
869    hig_workarea_add_section_divider( t, &row );
870    hig_workarea_add_section_title( t, &row, _( "Dates" ) );
871
872    l = di->date_added_lb = gtk_label_new( NULL );
873    hig_workarea_add_row( t, &row, _( "Started at:" ), l, NULL );
874
875    l = di->last_activity_lb = gtk_label_new( NULL );
876    hig_workarea_add_row( t, &row, _( "Last activity at:" ), l, NULL );
877
878    hig_workarea_add_section_divider( t, &row );
879    hig_workarea_finish( t, &row );
880    return t;
881}
882
883/****
884*****
885*****  INFO TAB
886*****
887****/
888
889static void
890refreshInfo( struct DetailsImpl * di, tr_torrent ** torrents, int n )
891{
892    int i;
893    char buf[128];
894    const char * str;
895    const char * none = _( "None" );
896    const char * mixed = _( "Mixed" );
897    const char * unknown = _( "Unknown" );
898    const tr_info ** infos = g_new( const tr_info*, n );
899
900    /* info */
901    for( i=0; i<n; ++i )
902        infos[i] = tr_torrentInfo( torrents[i] );
903
904    /* pieces_lb */
905    if( n <= 0 )
906        str = none;
907    else {
908        int sum = 0;
909        const int baseline = infos[0]->pieceSize;
910        for( i=0; i<n; ++i )
911            sum += infos[i]->pieceCount;
912        g_snprintf( buf, sizeof( buf ),
913                    ngettext( "%'d Piece", "%'d Pieces", sum ), sum );
914        for( i=1; i<n; ++i )
915            if( baseline != (int)infos[i]->pieceSize )
916                break;
917        if( i==n ) {
918            char tmp1[64];
919            char tmp2[64];
920            g_strlcpy( tmp1, buf, sizeof( tmp1 ) );
921            tr_strlsize( tmp2, baseline, sizeof( tmp2 ) );
922            g_snprintf( buf, sizeof( buf ), _( "%1$s @ %2$s" ), tmp1, tmp2 );
923        }
924        str = buf;
925    }
926    gtk_label_set_text( GTK_LABEL( di->pieces_lb ), str );
927   
928
929    /* hash_lb */
930    if( n<=0 )
931        str = none;
932    else if ( n==1 )
933        str = infos[0]->hashString; 
934    else
935        str = mixed;
936    gtk_label_set_text( GTK_LABEL( di->hash_lb ), str );
937
938
939    /* privacy_lb */
940    if( n<=0 )
941        str = none;
942    else {
943        const tr_bool baseline = infos[0]->isPrivate;
944        for( i=1; i<n; ++i )
945            if( baseline != infos[i]->isPrivate )
946                break;
947        if( i!=n )
948            str = mixed;
949        else if( baseline )
950            str = _( "Private to this tracker -- DHT and PEX disabled" );
951        else
952            str = _( "Public torrent" );
953    }
954    gtk_label_set_text( GTK_LABEL( di->privacy_lb ), str );
955
956
957    /* comment_buffer */
958    if( n<=0 )
959        str = "";
960    else {
961        const char * baseline = infos[0]->comment ? infos[0]->comment : "";
962        for( i=1; i<n; ++i )
963            if( strcmp( baseline, infos[i]->comment ? infos[i]->comment : "" ) )
964                break;
965        if( i==n )
966            str = baseline;
967        else
968            str = mixed;
969    }
970    gtk_text_buffer_set_text( di->comment_buffer, str, -1 );
971
972
973    /* creator_lb */
974    if( n<=0 )
975        str = none;
976    else {
977        const char * baseline = infos[0]->creator ? infos[0]->creator : "";
978        for( i=1; i<n; ++i )
979            if( strcmp( baseline, infos[i]->creator ? infos[i]->creator : "" ) )
980                break;
981        if( i==n )
982            str = baseline;
983        else
984            str = mixed;
985    }
986    if( !str || !*str )
987        str = unknown;
988    gtk_label_set_text( GTK_LABEL( di->creator_lb ), str );
989
990
991    /* date_created_lb */
992    if( n<=0 )
993        str = none;
994    else {
995        const time_t baseline = infos[0]->dateCreated;
996        for( i=1; i<n; ++i )
997            if( baseline != infos[i]->dateCreated )
998                break;
999        if( i==n )
1000            str = gtr_localtime2( buf, baseline, sizeof( buf ) );
1001        else
1002            str = mixed;
1003    }
1004    gtk_label_set_text( GTK_LABEL( di->date_created_lb ), str );
1005   
1006
1007    /* destination_lb */       
1008    if( n<=0 )
1009        str = none;
1010    else {
1011        const char * baseline = tr_torrentGetDownloadDir( torrents[0] );
1012        for( i=1; i<n; ++i )
1013            if( strcmp( baseline, tr_torrentGetDownloadDir( torrents[i] ) ) )
1014                break;
1015        if( i==n )
1016            str = baseline;
1017        else
1018            str = mixed;
1019    }
1020    gtk_label_set_text( GTK_LABEL( di->destination_lb ), str );
1021
1022
1023    /* torrentfile_lb */
1024    if( n<=0 )
1025        str = none;
1026    else if( n==1 )
1027        str = infos[0]->torrent;
1028    else
1029        str = mixed;
1030    gtk_label_set_text( GTK_LABEL( di->torrentfile_lb ), str );
1031
1032    g_free( infos );
1033}
1034
1035static GtkWidget*
1036info_page_new( struct DetailsImpl * di )
1037{
1038    int row = 0;
1039    GtkTextBuffer * b;
1040    GtkWidget *l, *w, *fr, *sw;
1041    GtkWidget *t = hig_workarea_create( );
1042
1043    hig_workarea_add_section_title( t, &row, _( "Details" ) );
1044
1045        /* pieces */
1046        l = di->pieces_lb = gtk_label_new( NULL );
1047        hig_workarea_add_row( t, &row, _( "Pieces:" ), l, NULL );
1048
1049        /* hash */
1050        l = g_object_new( GTK_TYPE_LABEL, "selectable", TRUE,
1051                                          "ellipsize", PANGO_ELLIPSIZE_END,
1052                                           NULL );
1053        hig_workarea_add_row( t, &row, _( "Hash:" ), l, NULL );
1054        di->hash_lb = l;
1055
1056        /* privacy */
1057        l = gtk_label_new( NULL );
1058        hig_workarea_add_row( t, &row, _( "Privacy:" ), l, NULL );
1059        di->privacy_lb = l;
1060
1061        /* comment */
1062        b = di->comment_buffer = gtk_text_buffer_new( NULL );
1063        w = gtk_text_view_new_with_buffer( b );
1064        gtk_widget_set_size_request( w, 0u, 100u );
1065        gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW( w ), GTK_WRAP_WORD );
1066        gtk_text_view_set_editable( GTK_TEXT_VIEW( w ), FALSE );
1067        sw = gtk_scrolled_window_new( NULL, NULL );
1068        gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( sw ),
1069                                        GTK_POLICY_AUTOMATIC,
1070                                        GTK_POLICY_AUTOMATIC );
1071        gtk_container_add( GTK_CONTAINER( sw ), w );
1072        fr = gtk_frame_new( NULL );
1073        gtk_frame_set_shadow_type( GTK_FRAME( fr ), GTK_SHADOW_IN );
1074        gtk_container_add( GTK_CONTAINER( fr ), sw );
1075        w = hig_workarea_add_row( t, &row, _( "Comment:" ), fr, NULL );
1076        gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.0f );
1077
1078    hig_workarea_add_section_divider( t, &row );
1079    hig_workarea_add_section_title( t, &row, _( "Origins" ) );
1080
1081        l = di->creator_lb = gtk_label_new( NULL );
1082        gtk_label_set_ellipsize( GTK_LABEL( l ), PANGO_ELLIPSIZE_END );
1083        hig_workarea_add_row( t, &row, _( "Creator:" ), l, NULL );
1084
1085        l = di->date_created_lb = gtk_label_new( NULL );
1086        hig_workarea_add_row( t, &row, _( "Date:" ), l, NULL );
1087
1088    hig_workarea_add_section_divider( t, &row );
1089    hig_workarea_add_section_title( t, &row, _( "Location" ) );
1090
1091        l = g_object_new( GTK_TYPE_LABEL, "selectable", TRUE,
1092                                          "ellipsize", PANGO_ELLIPSIZE_END,
1093                                          NULL );
1094        hig_workarea_add_row( t, &row, _( "Destination:" ), l, NULL );
1095        di->destination_lb = l;
1096
1097        l = g_object_new( GTK_TYPE_LABEL, "selectable", TRUE,
1098                                          "ellipsize", PANGO_ELLIPSIZE_END,
1099                                          NULL );
1100        hig_workarea_add_row( t, &row, _( "Torrent file:" ), l, NULL );
1101        di->torrentfile_lb = l;
1102
1103    hig_workarea_finish( t, &row );
1104    return t;
1105}
1106
1107/****
1108*****
1109*****  PEERS TAB
1110*****
1111****/
1112
1113enum
1114{
1115    WEBSEED_COL_KEY,
1116    WEBSEED_COL_WAS_UPDATED,
1117    WEBSEED_COL_URL,
1118    WEBSEED_COL_DOWNLOAD_RATE_DOUBLE,
1119    WEBSEED_COL_DOWNLOAD_RATE_STRING,
1120    N_WEBSEED_COLS
1121};
1122
1123static const char*
1124getWebseedColumnNames( int column )
1125{
1126    switch( column )
1127    {
1128        case WEBSEED_COL_URL: return _( "Webseeds" );
1129        case WEBSEED_COL_DOWNLOAD_RATE_DOUBLE:
1130        case WEBSEED_COL_DOWNLOAD_RATE_STRING: return _( "Down" );
1131        default: return "";
1132    }
1133}
1134
1135static GtkListStore*
1136webseed_model_new( void )
1137{
1138    return gtk_list_store_new( N_WEBSEED_COLS,
1139                               G_TYPE_STRING,   /* key */
1140                               G_TYPE_BOOLEAN,  /* was-updated */
1141                               G_TYPE_STRING,   /* url */
1142                               G_TYPE_DOUBLE,   /* download rate double */
1143                               G_TYPE_STRING ); /* download rate string */
1144}
1145
1146enum
1147{
1148    PEER_COL_KEY,
1149    PEER_COL_WAS_UPDATED,
1150    PEER_COL_ADDRESS,
1151    PEER_COL_ADDRESS_COLLATED,
1152    PEER_COL_DOWNLOAD_RATE_DOUBLE,
1153    PEER_COL_DOWNLOAD_RATE_STRING,
1154    PEER_COL_UPLOAD_RATE_DOUBLE,
1155    PEER_COL_UPLOAD_RATE_STRING,
1156    PEER_COL_CLIENT,
1157    PEER_COL_PROGRESS,
1158    PEER_COL_ENCRYPTION_STOCK_ID,
1159    PEER_COL_STATUS,
1160    N_PEER_COLS
1161};
1162
1163static const char*
1164getPeerColumnName( int column )
1165{
1166    switch( column )
1167    {
1168        case PEER_COL_ADDRESS: return _( "Address" );
1169        case PEER_COL_DOWNLOAD_RATE_STRING:
1170        case PEER_COL_DOWNLOAD_RATE_DOUBLE: return _( "Down" );
1171        case PEER_COL_UPLOAD_RATE_STRING:
1172        case PEER_COL_UPLOAD_RATE_DOUBLE: return _( "Up" );
1173        case PEER_COL_CLIENT: return _( "Client" );
1174        case PEER_COL_PROGRESS: return _( "%" );
1175        case PEER_COL_STATUS: return _( "Status" );
1176        default: return "";
1177    }
1178}
1179
1180static GtkListStore*
1181peer_store_new( void )
1182{
1183    return gtk_list_store_new( N_PEER_COLS,
1184                               G_TYPE_STRING,   /* key */
1185                               G_TYPE_BOOLEAN,  /* was-updated */
1186                               G_TYPE_STRING,   /* address */
1187                               G_TYPE_STRING,   /* collated address */
1188                               G_TYPE_FLOAT,    /* download speed float */
1189                               G_TYPE_STRING,   /* download speed string */
1190                               G_TYPE_FLOAT,    /* upload speed float */
1191                               G_TYPE_STRING,   /* upload speed string  */
1192                               G_TYPE_STRING,   /* client */
1193                               G_TYPE_INT,      /* progress [0..100] */
1194                               G_TYPE_STRING,   /* encryption stock id */
1195                               G_TYPE_STRING);  /* flagString */
1196}
1197
1198static void
1199initPeerRow( GtkListStore        * store,
1200             GtkTreeIter         * iter,
1201             const char          * key,
1202             const tr_peer_stat  * peer )
1203{
1204    int q[4];
1205    char up_speed[128];
1206    char down_speed[128];
1207    char collated_name[128];
1208    const char * client = peer->client;
1209
1210    if( !client || !strcmp( client, "Unknown Client" ) )
1211        client = "";
1212
1213    tr_strlspeed( up_speed, peer->rateToPeer, sizeof( up_speed ) );
1214    tr_strlspeed( down_speed, peer->rateToClient, sizeof( down_speed ) );
1215    if( sscanf( peer->addr, "%d.%d.%d.%d", q, q+1, q+2, q+3 ) != 4 )
1216        g_strlcpy( collated_name, peer->addr, sizeof( collated_name ) );
1217    else
1218        g_snprintf( collated_name, sizeof( collated_name ),
1219                    "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3] );
1220
1221    gtk_list_store_set( store, iter,
1222                        PEER_COL_ADDRESS, peer->addr,
1223                        PEER_COL_ADDRESS_COLLATED, collated_name,
1224                        PEER_COL_CLIENT, client,
1225                        PEER_COL_ENCRYPTION_STOCK_ID, peer->isEncrypted ? "transmission-lock" : NULL,
1226                        PEER_COL_KEY, key,
1227                        -1 );
1228}
1229
1230static void
1231refreshPeerRow( GtkListStore        * store,
1232                GtkTreeIter         * iter,
1233                const tr_peer_stat  * peer )
1234{
1235    char up_speed[128];
1236    char down_speed[128];
1237
1238    if( peer->rateToPeer > 0.01 )
1239        tr_strlspeed( up_speed, peer->rateToPeer, sizeof( up_speed ) );
1240    else
1241        *up_speed = '\0';
1242
1243    if( peer->rateToClient > 0.01 )
1244        tr_strlspeed( down_speed, peer->rateToClient, sizeof( down_speed ) );
1245    else
1246        *down_speed = '\0';
1247
1248    gtk_list_store_set( store, iter,
1249                        PEER_COL_PROGRESS, (int)( 100.0 * peer->progress ),
1250                        PEER_COL_DOWNLOAD_RATE_DOUBLE, peer->rateToClient,
1251                        PEER_COL_DOWNLOAD_RATE_STRING, down_speed,
1252                        PEER_COL_UPLOAD_RATE_DOUBLE, peer->rateToPeer,
1253                        PEER_COL_UPLOAD_RATE_STRING, up_speed,
1254                        PEER_COL_STATUS, peer->flagStr,
1255                        PEER_COL_WAS_UPDATED, TRUE,
1256                        -1 );
1257}
1258
1259static void
1260refreshPeerList( struct DetailsImpl * di, tr_torrent ** torrents, int n )
1261{
1262    int i;
1263    int * peerCount;
1264    GtkTreeIter iter;
1265    GHashTable * hash = di->peer_hash;
1266    GtkListStore * store = di->peer_store;
1267    GtkTreeModel * model = GTK_TREE_MODEL( store );
1268    struct tr_peer_stat ** peers;
1269
1270    /* step 1: get all the peers */
1271    peers = g_new( struct tr_peer_stat*, n );
1272    peerCount = g_new( int, n );
1273    for( i=0; i<n; ++i )
1274        peers[i] = tr_torrentPeers( torrents[i], &peerCount[i] );
1275
1276    /* step 2: mark all the peers in the list as not-updated */
1277    model = GTK_TREE_MODEL( store );
1278    if( gtk_tree_model_get_iter_first( model, &iter ) ) do
1279        gtk_list_store_set( store, &iter, PEER_COL_WAS_UPDATED, FALSE, -1 );
1280    while( gtk_tree_model_iter_next( model, &iter ) );
1281
1282    /* step 3: add any new peers */
1283    for( i=0; i<n; ++i ) {
1284        int j;
1285        const tr_torrent * tor = torrents[i];
1286        for( j=0; j<peerCount[i]; ++j ) {
1287            const tr_peer_stat * s = &peers[i][j];
1288            char key[128];
1289            g_snprintf( key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr );
1290            if( g_hash_table_lookup( hash, key ) == NULL ) {
1291                GtkTreePath * p;
1292                gtk_list_store_append( store, &iter );
1293                initPeerRow( store, &iter, key, s );
1294                p = gtk_tree_model_get_path( model, &iter );
1295                g_hash_table_insert( hash, g_strdup( key ),
1296                                     gtk_tree_row_reference_new( model, p ) );
1297                gtk_tree_path_free( p );
1298            }
1299        }
1300    }
1301
1302    /* step 4: update the peers */
1303    for( i=0; i<n; ++i ) {
1304        int j;
1305        const tr_torrent * tor = torrents[i];
1306        for( j=0; j<peerCount[i]; ++j ) {
1307            const tr_peer_stat * s = &peers[i][j];
1308            char key[128];
1309            GtkTreeRowReference * ref;
1310            GtkTreePath * p;
1311            g_snprintf( key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr );
1312            ref = g_hash_table_lookup( hash, key );
1313            p = gtk_tree_row_reference_get_path( ref );
1314            gtk_tree_model_get_iter( model, &iter, p );
1315            refreshPeerRow( store, &iter, s );
1316            gtk_tree_path_free( p );
1317        }
1318    }
1319
1320    /* step 5: remove peers that have disappeared */
1321    model = GTK_TREE_MODEL( store );
1322    if( gtk_tree_model_get_iter_first( model, &iter ) ) {
1323        gboolean more = TRUE;
1324        while( more ) {
1325            gboolean b;
1326            gtk_tree_model_get( model, &iter, PEER_COL_WAS_UPDATED, &b, -1 );
1327            if( b )
1328                more = gtk_tree_model_iter_next( model, &iter );
1329            else {
1330                char * key;
1331                gtk_tree_model_get( model, &iter, PEER_COL_KEY, &key, -1 );
1332                g_hash_table_remove( hash, key );
1333                more = gtk_list_store_remove( store, &iter );
1334                g_free( key );
1335            }
1336        }
1337    }
1338
1339    /* step 6: cleanup */
1340    for( i=0; i<n; ++i )
1341        tr_torrentPeersFree( peers[i], peerCount[i] );
1342    tr_free( peers );
1343    tr_free( peerCount );
1344}
1345
1346static void
1347refreshWebseedList( struct DetailsImpl * di, tr_torrent ** torrents, int n )
1348{
1349    int i;
1350    int total = 0;
1351    GtkTreeIter iter;
1352    GHashTable * hash = di->webseed_hash;
1353    GtkListStore * store = di->webseed_store;
1354    GtkTreeModel * model = GTK_TREE_MODEL( store );
1355   
1356    /* step 1: mark all webseeds as not-updated */
1357    if( gtk_tree_model_get_iter_first( model, &iter ) ) do
1358        gtk_list_store_set( store, &iter, WEBSEED_COL_WAS_UPDATED, FALSE, -1 );
1359    while( gtk_tree_model_iter_next( model, &iter ) );
1360
1361    /* step 2: add any new webseeds */
1362    for( i=0; i<n; ++i ) {
1363        int j;
1364        const tr_torrent * tor = torrents[i];
1365        const tr_info * inf = tr_torrentInfo( tor );
1366        total += inf->webseedCount;
1367        for( j=0; j<inf->webseedCount; ++j ) {
1368            char key[256];
1369            const char * url = inf->webseeds[j];
1370            g_snprintf( key, sizeof(key), "%d.%s", tr_torrentId( tor ), url );
1371            if( g_hash_table_lookup( hash, key ) == NULL ) {
1372                GtkTreePath * p;
1373                gtk_list_store_append( store, &iter );
1374                gtk_list_store_set( store, &iter, WEBSEED_COL_URL, url,
1375                                                  WEBSEED_COL_KEY, key,
1376                                                  -1 );
1377                p = gtk_tree_model_get_path( model, &iter );
1378                g_hash_table_insert( hash, g_strdup( key ),
1379                                     gtk_tree_row_reference_new( model, p ) );
1380                gtk_tree_path_free( p );
1381            }
1382        }
1383    }
1384
1385    /* step 3: update the webseeds */
1386    for( i=0; i<n; ++i ) {
1387        int j;
1388        const tr_torrent * tor = torrents[i];
1389        const tr_info * inf = tr_torrentInfo( tor );
1390        float * speeds = tr_torrentWebSpeeds( tor );
1391        for( j=0; j<inf->webseedCount; ++j ) {
1392            char buf[128];
1393            char key[256];
1394            const char * url = inf->webseeds[j];
1395            GtkTreePath * p;
1396            GtkTreeRowReference * ref;
1397            g_snprintf( key, sizeof(key), "%d.%s", tr_torrentId( tor ), url );
1398            ref = g_hash_table_lookup( hash, key );
1399            p = gtk_tree_row_reference_get_path( ref );
1400            gtk_tree_model_get_iter( model, &iter, p );
1401            if( speeds[j] > 0.01 )
1402                tr_strlspeed( buf, speeds[j], sizeof( buf ) );
1403            else
1404                *buf = '\0';
1405            gtk_list_store_set( store, &iter, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE, (double)speeds[j],
1406                                              WEBSEED_COL_DOWNLOAD_RATE_STRING, buf,
1407                                              WEBSEED_COL_WAS_UPDATED, TRUE,
1408                                              -1 );
1409            gtk_tree_path_free( p );
1410        }
1411        tr_free( speeds );
1412    }
1413
1414    /* step 4: remove webseeds that have disappeared */
1415    if( gtk_tree_model_get_iter_first( model, &iter ) ) {
1416        gboolean more = TRUE;
1417        while( more ) {
1418            gboolean b;
1419            gtk_tree_model_get( model, &iter, WEBSEED_COL_WAS_UPDATED, &b, -1 );
1420            if( b )
1421                more = gtk_tree_model_iter_next( model, &iter );
1422            else {
1423                char * key;
1424                gtk_tree_model_get( model, &iter, WEBSEED_COL_KEY, &key, -1 );
1425                if( key != NULL )
1426                    g_hash_table_remove( hash, key );
1427                more = gtk_list_store_remove( store, &iter );
1428                g_free( key );
1429            }
1430        }
1431    }
1432
1433    /* most of the time there are no webseeds...
1434       if that's the case, don't waste space showing an empty list */
1435    if( total > 0 )
1436        gtk_widget_show( di->webseed_view );
1437    else
1438        gtk_widget_hide( di->webseed_view );
1439}
1440
1441static void
1442refreshPeers( struct DetailsImpl * di, tr_torrent ** torrents, int n )
1443{
1444    int i;
1445    char buf[512];
1446    const char * none = _( "None" );
1447
1448    /* seeders_lb */
1449    /* leechers_lb */
1450    /* completed_lb */
1451    if( n<=0 ) {
1452        gtk_label_set_text( GTK_LABEL( di->seeders_lb ), none );
1453        gtk_label_set_text( GTK_LABEL( di->leechers_lb ), none );
1454        gtk_label_set_text( GTK_LABEL( di->completed_lb ), none );
1455    } else {
1456        int seeders = 0;
1457        int leechers = 0;
1458        int completed = 0;
1459        for( i=0; i<n; ++i ) {
1460            const tr_stat * s = tr_torrentStat( torrents[i] );
1461            seeders = s->seeders;
1462            leechers = s->leechers;
1463            completed += s->timesCompleted;
1464        }
1465        g_snprintf( buf, sizeof( buf ), "%'d", seeders );
1466        gtk_label_set_text( GTK_LABEL( di->seeders_lb ), buf );
1467        g_snprintf( buf, sizeof( buf ), "%'d", leechers );
1468        gtk_label_set_text( GTK_LABEL( di->leechers_lb ), buf );
1469        g_snprintf( buf, sizeof( buf ), "%'d", completed );
1470        gtk_label_set_text( GTK_LABEL( di->completed_lb ), buf );
1471    }
1472
1473    refreshPeerList( di, torrents, n );
1474    refreshWebseedList( di, torrents, n );
1475}
1476
1477#if GTK_CHECK_VERSION( 2,12,0 )
1478static gboolean
1479onPeerViewQueryTooltip( GtkWidget   * widget,
1480                        gint          x,
1481                        gint          y,
1482                        gboolean      keyboard_tip,
1483                        GtkTooltip  * tooltip,
1484                        gpointer      user_data UNUSED )
1485{
1486    gboolean       show_tip = FALSE;
1487    GtkTreeModel * model;
1488    GtkTreeIter    iter;
1489
1490    if( gtk_tree_view_get_tooltip_context( GTK_TREE_VIEW( widget ),
1491                                           &x, &y, keyboard_tip,
1492                                           &model, NULL, &iter ) )
1493    {
1494        const char * pch;
1495        char *       str = NULL;
1496        GString *    gstr = g_string_new( NULL );
1497        gtk_tree_model_get( model, &iter, PEER_COL_STATUS, &str, -1 );
1498        for( pch = str; pch && *pch; ++pch )
1499        {
1500            const char * s = NULL;
1501            switch( *pch )
1502            {
1503                case 'O': s = _( "Optimistic unchoke" ); break;
1504                case 'D': s = _( "Downloading from this peer" ); break;
1505                case 'd': s = _( "We would download from this peer if they would let us" ); break;
1506                case 'U': s = _( "Uploading to peer" ); break; 
1507                case 'u': s = _( "We would upload to this peer if they asked" ); break;
1508                case 'K': s = _( "Peer has unchoked us, but we're not interested" ); break;
1509                case '?': s = _( "We unchoked this peer, but they're not interested" ); break;
1510                case 'E': s = _( "Encrypted connection" ); break; 
1511                case 'X': s = _( "Peer was discovered through Peer Exchange (PEX)" ); break;
1512                case 'H': s = _( "Peer was discovered through DHT" ); break;
1513                case 'I': s = _( "Peer is an incoming connection" ); break;
1514            }
1515            if( s )
1516                g_string_append_printf( gstr, "%c: %s\n", *pch, s );
1517        }
1518        if( gstr->len ) /* remove the last linefeed */
1519            g_string_set_size( gstr, gstr->len - 1 );
1520        gtk_tooltip_set_text( tooltip, gstr->str );
1521        g_string_free( gstr, TRUE );
1522        g_free( str );
1523        show_tip = TRUE;
1524    }
1525
1526    return show_tip;
1527}
1528#endif
1529
1530static GtkWidget*
1531peer_page_new( struct DetailsImpl * di )
1532{
1533    guint i;
1534    const char * str;
1535    GtkListStore *store;
1536    GtkWidget *v, *w, *ret, *sw, *l, *vbox, *hbox;
1537    GtkWidget *webtree = NULL;
1538    GtkTreeViewColumn * c;
1539    GtkCellRenderer *   r;
1540    int view_columns[] = { PEER_COL_ENCRYPTION_STOCK_ID,
1541                           PEER_COL_UPLOAD_RATE_STRING,
1542                           PEER_COL_DOWNLOAD_RATE_STRING,
1543                           PEER_COL_PROGRESS,
1544                           PEER_COL_STATUS,
1545                           PEER_COL_ADDRESS,
1546                           PEER_COL_CLIENT };
1547
1548
1549    /* webseeds */
1550
1551    store = di->webseed_store = webseed_model_new( );
1552    v = gtk_tree_view_new_with_model( GTK_TREE_MODEL( store ) );
1553    g_signal_connect( v, "button-release-event", G_CALLBACK( on_tree_view_button_released ), NULL );
1554    gtk_tree_view_set_rules_hint( GTK_TREE_VIEW( v ), TRUE );
1555    g_object_unref( store );
1556
1557    str = getWebseedColumnNames( WEBSEED_COL_URL );
1558    r = gtk_cell_renderer_text_new( );
1559    g_object_set( G_OBJECT( r ), "ellipsize", PANGO_ELLIPSIZE_END, NULL );
1560    c = gtk_tree_view_column_new_with_attributes( str, r, "text", WEBSEED_COL_URL, NULL );
1561    g_object_set( G_OBJECT( c ), "expand", TRUE, NULL );
1562    gtk_tree_view_column_set_sort_column_id( c, WEBSEED_COL_URL );
1563    gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
1564
1565    str = getWebseedColumnNames( WEBSEED_COL_DOWNLOAD_RATE_STRING );
1566    r = gtk_cell_renderer_text_new( );
1567    c = gtk_tree_view_column_new_with_attributes( str, r, "text", WEBSEED_COL_DOWNLOAD_RATE_STRING, NULL );
1568    gtk_tree_view_column_set_sort_column_id( c, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE );
1569    gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
1570
1571    w = gtk_scrolled_window_new( NULL, NULL );
1572    gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( w ),
1573                                    GTK_POLICY_AUTOMATIC,
1574                                    GTK_POLICY_AUTOMATIC );
1575    gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( w ),
1576                                         GTK_SHADOW_IN );
1577    gtk_container_add( GTK_CONTAINER( w ), v );
1578
1579    webtree = w;
1580    di->webseed_view = w;
1581
1582    /* peers */
1583
1584    store  = di->peer_store = peer_store_new( );
1585    v = GTK_WIDGET( g_object_new( GTK_TYPE_TREE_VIEW,
1586                                  "model",  gtk_tree_model_sort_new_with_model( GTK_TREE_MODEL( store ) ),
1587                                  "rules-hint", TRUE,
1588#if GTK_CHECK_VERSION( 2,12,0 )
1589                                  "has-tooltip", TRUE,
1590#endif
1591                                  NULL ) );
1592
1593#if GTK_CHECK_VERSION( 2,12,0 )
1594    g_signal_connect( v, "query-tooltip",
1595                      G_CALLBACK( onPeerViewQueryTooltip ), NULL );
1596#endif
1597    g_object_unref( store );
1598    g_signal_connect( v, "button-release-event",
1599                      G_CALLBACK( on_tree_view_button_released ), NULL );
1600
1601    for( i=0; i<G_N_ELEMENTS( view_columns ); ++i )
1602    {
1603        const int col = view_columns[i];
1604        const char * t = getPeerColumnName( col );
1605        int sort_col = col;
1606
1607        switch( col )
1608        {
1609            case PEER_COL_ADDRESS:
1610                r = gtk_cell_renderer_text_new( );
1611                c = gtk_tree_view_column_new_with_attributes( t, r, "text", col, NULL );
1612                sort_col = PEER_COL_ADDRESS_COLLATED;
1613                break;
1614
1615            case PEER_COL_CLIENT:
1616                r = gtk_cell_renderer_text_new( );
1617                c = gtk_tree_view_column_new_with_attributes( t, r, "text", col, NULL );
1618                break;
1619
1620            case PEER_COL_PROGRESS:
1621                r = gtk_cell_renderer_progress_new( );
1622                c = gtk_tree_view_column_new_with_attributes( t, r, "value", PEER_COL_PROGRESS, NULL );
1623                break;
1624
1625            case PEER_COL_ENCRYPTION_STOCK_ID:
1626                r = gtk_cell_renderer_pixbuf_new( );
1627                g_object_set( r, "xalign", (gfloat)0.0,
1628                                 "yalign", (gfloat)0.5,
1629                                 NULL );
1630                c = gtk_tree_view_column_new_with_attributes( t, r, "stock-id", PEER_COL_ENCRYPTION_STOCK_ID, NULL );
1631                gtk_tree_view_column_set_sizing( c, GTK_TREE_VIEW_COLUMN_FIXED );
1632                gtk_tree_view_column_set_fixed_width( c, 20 );
1633                break;
1634
1635            case PEER_COL_DOWNLOAD_RATE_STRING:
1636                r = gtk_cell_renderer_text_new( );
1637                c = gtk_tree_view_column_new_with_attributes( t, r, "text", col, NULL );
1638                sort_col = PEER_COL_DOWNLOAD_RATE_DOUBLE;
1639                break;
1640
1641            case PEER_COL_UPLOAD_RATE_STRING:
1642                r = gtk_cell_renderer_text_new( );
1643                c = gtk_tree_view_column_new_with_attributes( t, r, "text", col, NULL );
1644                sort_col = PEER_COL_UPLOAD_RATE_DOUBLE;
1645                break;
1646
1647            case PEER_COL_STATUS:
1648                r = gtk_cell_renderer_text_new( );
1649                c = gtk_tree_view_column_new_with_attributes( t, r, "text", col, NULL );
1650                break;
1651
1652            default:
1653                abort( );
1654        }
1655
1656        gtk_tree_view_column_set_resizable( c, FALSE );
1657        gtk_tree_view_column_set_sort_column_id( c, sort_col );
1658        gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
1659    }
1660
1661    /* the 'expander' column has a 10-pixel margin on the left
1662       that doesn't look quite correct in any of these columns...
1663       so create a non-visible column and assign it as the
1664       'expander column. */
1665    {
1666        GtkTreeViewColumn *c = gtk_tree_view_column_new( );
1667        gtk_tree_view_column_set_visible( c, FALSE );
1668        gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
1669        gtk_tree_view_set_expander_column( GTK_TREE_VIEW( v ), c );
1670    }
1671
1672    w = sw = gtk_scrolled_window_new( NULL, NULL );
1673    gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( w ),
1674                                    GTK_POLICY_AUTOMATIC,
1675                                    GTK_POLICY_AUTOMATIC );
1676    gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( w ),
1677                                         GTK_SHADOW_IN );
1678    gtk_container_add( GTK_CONTAINER( w ), v );
1679
1680
1681    vbox = gtk_vbox_new( FALSE, GUI_PAD );
1682    gtk_container_set_border_width( GTK_CONTAINER( vbox ), GUI_PAD_BIG );
1683
1684    v = gtk_vpaned_new( );
1685    gtk_paned_pack1( GTK_PANED( v ), webtree, FALSE, TRUE );
1686    gtk_paned_pack2( GTK_PANED( v ), sw, TRUE, TRUE );
1687    gtk_box_pack_start( GTK_BOX( vbox ), v, TRUE, TRUE, 0 );
1688
1689    hbox = gtk_hbox_new( FALSE, GUI_PAD );
1690    l = gtk_label_new( NULL );
1691    gtk_label_set_markup( GTK_LABEL( l ), _( "<b>Seeders:</b>" ) );
1692    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1693    l = di->seeders_lb = gtk_label_new( NULL );
1694    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1695    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1696    gtk_box_pack_start( GTK_BOX( hbox ), w, TRUE, TRUE, 0 );
1697    l = gtk_label_new( NULL );
1698    gtk_label_set_markup( GTK_LABEL( l ), _( "<b>Leechers:</b>" ) );
1699    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1700    l = di->leechers_lb = gtk_label_new( NULL );
1701    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1702    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1703    gtk_box_pack_start( GTK_BOX( hbox ), w, TRUE, TRUE, 0 );
1704    l = gtk_label_new( NULL );
1705    gtk_label_set_markup( GTK_LABEL( l ), _( "<b>Times Completed:</b>" ) );
1706    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1707    l = di->completed_lb = gtk_label_new( NULL );
1708    gtk_box_pack_start( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
1709    gtk_box_pack_start( GTK_BOX( vbox ), hbox, FALSE, FALSE, 0 );
1710
1711    /* ip-to-GtkTreeRowReference */
1712    di->peer_hash = g_hash_table_new_full( g_str_hash,
1713                                           g_str_equal,
1714                                           (GDestroyNotify)g_free,
1715                                           (GDestroyNotify)gtk_tree_row_reference_free );
1716
1717    /* url-to-GtkTreeRowReference */
1718    di->webseed_hash = g_hash_table_new_full( g_str_hash,
1719                                              g_str_equal,
1720                                              (GDestroyNotify)g_free,
1721                                              (GDestroyNotify)gtk_tree_row_reference_free );
1722                           
1723    ret = vbox;
1724    return ret;
1725}
1726
1727
1728
1729/****
1730*****  TRACKER
1731****/
1732
1733static void
1734refreshTracker( struct DetailsImpl * di, tr_torrent ** torrents, int n )
1735{
1736    int i;
1737    char buf[256];
1738    const char * str;
1739    const char * none = _("None" );
1740    const char * nowStr = _("Now" );
1741    const char * mixed = _( "Mixed" );
1742    const char * noneSent = _( "None sent" );
1743    const char * inProgress = _( "In progress" );
1744    const time_t now = time( NULL );
1745    const tr_stat ** stats;
1746
1747    stats = g_new( const tr_stat*, n );
1748    for( i=0; i<n; ++i )
1749        stats[i] = tr_torrentStatCached( torrents[i] );
1750
1751    /* last_scrape_time_lb */
1752    if( n<1 )
1753        str = none;
1754    else {
1755        const time_t baseline = stats[0]->lastScrapeTime;
1756        for( i=1; i<n; ++i )
1757            if( baseline != stats[i]->lastScrapeTime )
1758                break;
1759        if( i!=n )
1760            str = mixed;
1761        else if( baseline==0 )
1762            str = noneSent;
1763        else
1764            str = gtr_localtime2( buf, baseline, sizeof( buf ) );
1765    }
1766    gtk_label_set_text( GTK_LABEL( di->last_scrape_time_lb ), str );
1767
1768
1769    /* last_scrape_response_lb */
1770    if( n<1 )
1771        str = none;
1772    else {
1773        const char * baseline = stats[0]->scrapeResponse;
1774        for( i=1; i<n; ++i )
1775            if( strcmp( baseline, stats[i]->scrapeResponse ) )
1776                break;
1777        if( i==n )
1778            str = baseline;
1779        else
1780            str = mixed;
1781    }
1782    gtk_label_set_text( GTK_LABEL( di->last_scrape_response_lb ), str );
1783
1784
1785    /* next_scrape_countdown_lb */
1786    if( n<1 )
1787        str = none;
1788    else {
1789        const time_t baseline = stats[0]->nextScrapeTime;
1790        for( i=1; i<n; ++i )
1791            if( baseline != stats[i]->nextScrapeTime )
1792                break;
1793        if( i!=n )
1794            str = mixed;
1795        else if( baseline <= now )
1796            str = inProgress;
1797        else
1798            str = tr_strltime( buf, baseline - now, sizeof( buf ) );
1799    }
1800    gtk_label_set_text( GTK_LABEL( di->next_scrape_countdown_lb ), str );
1801
1802
1803    /* last_announce_time_lb */
1804    if( n<1 )
1805        str = none;
1806    else {
1807        const time_t baseline = stats[0]->lastAnnounceTime;
1808        for( i=1; i<n; ++i )
1809            if( baseline != stats[i]->lastAnnounceTime )
1810                break;
1811        if( i!=n )
1812            str = mixed;
1813        else if( baseline==0 )
1814            str = noneSent;
1815        else
1816            str = gtr_localtime2( buf, baseline, sizeof( buf ) );
1817    }
1818    gtk_label_set_text( GTK_LABEL( di->last_announce_time_lb ), str );
1819
1820
1821    /* last_announce_response_lb */
1822    if( n<1 )
1823        str = none;
1824    else {
1825        const char * baseline = stats[0]->announceResponse;
1826        for( i=1; i<n; ++i )
1827            if( strcmp( baseline, stats[i]->announceResponse ) )
1828                break;
1829        if( i==n )
1830            str = baseline;
1831        else
1832            str = mixed;
1833    }
1834    gtk_label_set_text( GTK_LABEL( di->last_announce_response_lb ), str );
1835
1836
1837    /* next_announce_countdown_lb */
1838    if( n<1 )
1839        str = none;
1840    else {
1841        const time_t baseline = stats[0]->nextAnnounceTime;
1842        for( i=1; i<n; ++i )
1843            if( baseline != stats[i]->nextAnnounceTime )
1844                break;
1845        if( i!=n )
1846            str = mixed;
1847        else if( baseline==0 )
1848            str = none;
1849        else if( baseline==1 || baseline<=now )
1850            str = inProgress;
1851        else
1852            str = tr_strltime( buf, baseline - now, sizeof( buf ) );
1853    }
1854    gtk_label_set_text( GTK_LABEL( di->next_announce_countdown_lb ), str );
1855
1856
1857    /* manual_announce_countdown_lb */
1858    if( n<1 )
1859        str = none;
1860    else {
1861        const time_t baseline = stats[0]->manualAnnounceTime;
1862        for( i=1; i<n; ++i )
1863            if( baseline != stats[i]->manualAnnounceTime )
1864                break;
1865        if( i!=n )
1866            str = mixed;
1867        else if( baseline<1 )
1868            str = none;
1869        else if( baseline<=now )
1870            str = nowStr;
1871        else
1872            str = tr_strltime( buf, baseline - now, sizeof( buf ) );
1873    }
1874    gtk_label_set_text( GTK_LABEL( di->manual_announce_countdown_lb ), str );
1875
1876
1877    g_free( stats );
1878}
1879
1880static GtkWidget*
1881tracker_page_new( struct DetailsImpl * di )
1882{
1883    int row = 0;
1884    const char * s;
1885    GtkWidget *t, *l, *w;
1886
1887    t = hig_workarea_create( );
1888    hig_workarea_add_section_title( t, &row, _( "Trackers" ) );
1889
1890        w = tracker_list_new( tr_core_session( di->core ), -1, FALSE );
1891        hig_workarea_add_wide_control( t, &row, w );
1892        di->tracker_list = w;
1893
1894    hig_workarea_add_section_divider( t, &row );
1895    hig_workarea_add_section_title( t, &row, _( "Scrape" ) );
1896
1897        s = _( "Last scrape at:" );
1898        l = gtk_label_new( NULL );
1899        di->last_scrape_time_lb = l;
1900        hig_workarea_add_row( t, &row, s, l, NULL );
1901
1902        s = _( "Tracker responded:" );
1903        l = gtk_label_new( NULL );
1904        di->last_scrape_response_lb = l;
1905        hig_workarea_add_row( t, &row, s, l, NULL );
1906
1907        s = _( "Next scrape in:" );
1908        l = gtk_label_new( NULL );
1909        di->next_scrape_countdown_lb = l;
1910        hig_workarea_add_row( t, &row, s, l, NULL );
1911
1912    hig_workarea_add_section_divider( t, &row );
1913    hig_workarea_add_section_title( t, &row, _( "Announce" ) );
1914
1915        l = gtk_label_new( NULL );
1916        gtk_label_set_ellipsize( GTK_LABEL( l ), PANGO_ELLIPSIZE_END );
1917        hig_workarea_add_row( t, &row, _( "Tracker:" ), l, NULL );
1918
1919        s = _( "Last announce at:" );
1920        l = gtk_label_new( NULL );
1921        di->last_announce_time_lb = l;
1922        hig_workarea_add_row( t, &row, s, l, NULL );
1923
1924        s = _( "Tracker responded:" );
1925        l = gtk_label_new( NULL );
1926        di->last_announce_response_lb = l;
1927        hig_workarea_add_row( t, &row, s, l, NULL );
1928
1929        s = _( "Next announce in:" );
1930        l = gtk_label_new( NULL );
1931        di->next_announce_countdown_lb = l;
1932        hig_workarea_add_row( t, &row, s, l, NULL );
1933
1934        /* how long until the tracker will honor user
1935         * pressing the "ask for more peers" button */
1936        s = _( "Manual announce allowed in:" );
1937        l = gtk_label_new( NULL );
1938        di->manual_announce_countdown_lb = l;
1939        hig_workarea_add_row( t, &row, s, l, NULL );
1940
1941    hig_workarea_finish( t, &row );
1942    return t;
1943}
1944
1945
1946/****
1947*****  DIALOG
1948****/
1949
1950static void
1951refresh( struct DetailsImpl * di )
1952{
1953    int n;
1954    tr_torrent ** torrents = getTorrents( di, &n );
1955
1956    refreshInfo( di, torrents, n );
1957    refreshPeers( di, torrents, n );
1958    refreshTracker( di, torrents, n );
1959    refreshOptions( di, torrents, n );
1960    refreshActivity( di, torrents, n );
1961
1962    g_free( torrents );
1963}
1964
1965static gboolean
1966periodic_refresh( gpointer data )
1967{
1968    refresh( data );
1969    return TRUE;
1970}
1971
1972static void
1973details_free( gpointer gdata )
1974{
1975    struct DetailsImpl * data = gdata;
1976    g_signal_handler_disconnect( data->core, data->prefs_changed_tag );
1977    g_source_remove( data->periodic_refresh_tag );
1978    g_hash_table_destroy( data->webseed_hash );
1979    g_slist_free( data->ids );
1980    g_free( data );
1981}
1982
1983static void
1984response_cb( GtkDialog * dialog, int a UNUSED, gpointer b UNUSED )
1985{
1986    GtkWidget * w = GTK_WIDGET( dialog );
1987    torrent_inspector_set_torrents( w, NULL );
1988    gtk_widget_destroy( w );
1989}
1990
1991GtkWidget*
1992torrent_inspector_new( GtkWindow * parent, TrCore * core )
1993{
1994    GtkWidget * d, * n, * w, * l;
1995    struct DetailsImpl * di = g_new0( struct DetailsImpl, 1 );
1996
1997    /* create the dialog */
1998    di->core = core;
1999    d = gtk_dialog_new_with_buttons( NULL, parent, 0,
2000                                     GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
2001                                     NULL );
2002    gtk_window_set_role( GTK_WINDOW( d ), "tr-info" );
2003    g_signal_connect( d, "response", G_CALLBACK( response_cb ), NULL );
2004    gtk_dialog_set_has_separator( GTK_DIALOG( d ), FALSE );
2005    gtk_container_set_border_width( GTK_CONTAINER( d ), GUI_PAD );
2006    g_object_set_data_full( G_OBJECT( d ), DETAILS_KEY, di, details_free );
2007
2008    n = gtk_notebook_new( );
2009    gtk_container_set_border_width( GTK_CONTAINER( n ), GUI_PAD );
2010
2011    w = activity_page_new( di );
2012    l = gtk_label_new( _( "Activity" ) );
2013    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w, l );
2014
2015    w = peer_page_new( di );
2016    l = gtk_label_new( _( "Peers" ) );
2017    gtk_notebook_append_page( GTK_NOTEBOOK( n ),  w, l );
2018
2019    w = tracker_page_new( di );
2020    l = gtk_label_new( _( "Tracker" ) );
2021    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w, l );
2022
2023    w = info_page_new( di );
2024    l = gtk_label_new( _( "Information" ) );
2025    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w, l );
2026
2027    w = file_list_new( core, 0 );
2028    gtk_container_set_border_width( GTK_CONTAINER( w ), GUI_PAD_BIG );
2029    l = gtk_label_new( _( "Files" ) );
2030    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w, l );
2031    di->file_list = w;
2032
2033    w = options_page_new( di );
2034    l = gtk_label_new( _( "Options" ) );
2035    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w, l );
2036
2037    gtk_box_pack_start( GTK_BOX( GTK_DIALOG( d )->vbox ), n, TRUE, TRUE, 0 );
2038
2039    di->periodic_refresh_tag = gtr_timeout_add_seconds( UPDATE_INTERVAL_SECONDS,
2040                                                        periodic_refresh, di );
2041    periodic_refresh( di );
2042    gtk_widget_show_all( GTK_DIALOG( d )->vbox );
2043    return d;
2044}
2045
2046void
2047torrent_inspector_set_torrents( GtkWidget * w, GSList * ids )
2048{
2049    struct DetailsImpl * di = g_object_get_data( G_OBJECT( w ), DETAILS_KEY );
2050    const int len = g_slist_length( ids );
2051    char title[256];
2052
2053    g_slist_free( di->ids );
2054    di->ids = g_slist_copy( ids );
2055
2056    if( len == 1 )
2057    {
2058        const int id = GPOINTER_TO_INT( ids->data );
2059        tr_session * session = tr_core_session( di->core );
2060        tr_torrent * tor = tr_torrentFindFromId( session, id );
2061        const tr_info * inf = tr_torrentInfo( tor );
2062        g_snprintf( title, sizeof( title ), _( "%s Properties" ), inf->name );
2063
2064        file_list_set_torrent( di->file_list, id );
2065        tracker_list_set_torrent( di->tracker_list, id );
2066       
2067    }
2068   else
2069   {
2070        file_list_clear( di->file_list );
2071        tracker_list_clear( di->tracker_list );
2072        g_snprintf( title, sizeof( title ), _( "%'d Torrent Properties" ), len );
2073    }
2074
2075    gtk_window_set_title( GTK_WINDOW( w ), title );
2076
2077    refresh( di );
2078}
Note: See TracBrowser for help on using the repository browser.