source: trunk/gtk/details.c @ 11095

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

(trunk) sync some more GTK+ and Qt strings

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