source: trunk/gtk/details.c @ 9253

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

(trunk) #2463: 'Mac Client shows huge "next announce in" timer'

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