source: trunk/gtk/details.c @ 10231

Last change on this file since 10231 was 10231, checked in by charles, 12 years ago

(trunk gtk) #2932 "The 'comment' field in the Torrent Properties dialog loses selection" -- fixed in trunk for 1.91

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