source: trunk/gtk/details.c @ 9601

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

(trunk gtk) in the torrent info dialog, rename the "Tracker" tab to "Trackers". Reported by Rolcol.

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