source: trunk/gtk/details.c @ 13192

Last change on this file since 13192 was 13192, checked in by jordan, 10 years ago

(trunk gtk) To improve translations, help gettext to differentiate between the gerund and verb forms of some -ing words like "Seeding" and "Downloading" -- fixed.

I wasn't sure how to do this, so for the benefit of my future self or anyone else who's interested, here are some breadcrumbs I found: https://trac.transmissionbt.com/ticket/4717#comment:6

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