source: trunk/gtk/details.c @ 9627

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

(trunk libT, gtk) #2625 "ability to create a magnet link" -- add hook for generating a magnet link from a tr_torrent, and use it in the Torrent Properties dialog in the GTK+ client

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