source: branches/1.5x/gtk/details.c @ 7950

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

(1.5x gtk) #1871: webseed peer list in transmission gtk does not scroll, but instead resizes the window to fit it.

  • Property svn:keywords set to Date Rev Author Id
File size: 50.5 KB
Line 
1/*
2 * This file Copyright (C) 2007-2009 Charles Kerr <charles@transmissionbt.com>
3 *
4 * This file is licensed by the GPL version 2.  Works owned by the
5 * Transmission project are granted a special exemption to clause 2(b)
6 * so that the bulk of its code can remain under the MIT license.
7 * This exemption does not extend to derived works not owned by
8 * the Transmission project.
9 *
10 * $Id: details.c 7950 2009-02-26 17:53:54Z charles $
11 */
12
13#include <errno.h>
14#include <math.h> /* ceil() */
15#include <stddef.h>
16#include <stdio.h>
17#include <stdlib.h>
18#include <glib/gi18n.h>
19#include <gtk/gtk.h>
20
21#include <libtransmission/transmission.h>
22#include <libtransmission/utils.h> /* tr_httpIsValidURL */
23
24#include "actions.h"
25#include "details.h"
26#include "file-list.h"
27#include "tr-torrent.h"
28#include "tracker-list.h"
29#include "hig.h"
30#include "util.h"
31
32#define UPDATE_INTERVAL_SECONDS 2
33
34/****
35*****  PIECES VIEW
36****/
37
38/* define SHOW_PIECES */
39
40#ifdef SHOW_PIECES
41static int
42getGridSize( int   pieceCount,
43             int * n_rows,
44             int * n_cols )
45{
46    const int MAX_ACROSS = 16;
47
48    if( pieceCount >= ( MAX_ACROSS * MAX_ACROSS ) )
49    {
50        *n_rows = *n_cols = MAX_ACROSS;
51        return MAX_ACROSS * MAX_ACROSS;
52    }
53    else
54    {
55        int i;
56        for( i = 0; ( i * i ) < pieceCount; ++i ) ;
57        *n_rows = *n_cols = i;
58        return pieceCount;
59    }
60}
61
62 #define TO16( a ) ( (guint16)( ( a << 8 ) | ( a ) ) )
63 #define RGB_2_GDK( R, G, B ) { 0, TO16( R ), TO16( G ), TO16( B ) }
64
65enum { DRAW_AVAIL, DRAW_PROG };
66
67static void
68release_gobject_array( gpointer data )
69{
70    int       i;
71    GObject **objects = (GObject**) data;
72
73    for( i = 0; objects[i] != NULL; ++i )
74        g_object_unref ( G_OBJECT( objects[i] ) );
75    g_free ( objects );
76}
77
78static gboolean
79refresh_pieces( GtkWidget *            da,
80                GdkEventExpose * event UNUSED,
81                gpointer               gtor )
82{
83    tr_torrent *       tor = tr_torrent_handle( TR_TORRENT( gtor ) );
84    const tr_info *    info = tr_torrent_info( TR_TORRENT( gtor ) );
85    int                mode =
86        GPOINTER_TO_INT ( g_object_get_data ( G_OBJECT( da ), "draw-mode" ) );
87
88    GdkColormap *      colormap = gtk_widget_get_colormap ( da );
89    const int          widget_w = da->allocation.width;
90    const int          widget_h = da->allocation.height;
91    int                n_rows, n_cols;
92    const int          n_cells = getGridSize ( info->pieceCount,  &n_rows,
93                                               &n_cols );
94    const GdkRectangle grid_bounds = { 0, 0, widget_w, widget_h };
95    const double       piece_w = grid_bounds.width / (double)n_cols;
96    const double       piece_h = grid_bounds.height / (double)n_rows;
97    const int          piece_w_int = (int) ( piece_w + 1 ); /* pad for roundoff
98                                                              */
99    const int          piece_h_int = (int) ( piece_h + 1 ); /* pad for roundoff
100                                                              */
101    const gboolean     rtl = gtk_widget_get_direction( da ) ==
102                             GTK_TEXT_DIR_RTL;
103
104    guint8 *           prev_color = NULL;
105    gboolean           first_time = FALSE;
106
107    int                i, x, y;
108    int8_t *           pieces = NULL;
109    float *            completeness = NULL;
110
111    /**
112    ***  Get the Graphics Contexts...
113    **/
114
115    enum { ALL, LOTS, SOME, FEW, NONE,
116           BLACK, GRAY, BLINK,
117           N_COLORS };
118    GdkGC **           gcs = (GdkGC**) g_object_get_data ( G_OBJECT(
119                                                               da ),
120                                                           "graphics-contexts" );
121    if( gcs == NULL )
122    {
123        const GdkColor colors[N_COLORS] = {
124            RGB_2_GDK ( 114, 159, 207 ), /* all */
125            RGB_2_GDK (  52, 101, 164 ), /* lots */
126            RGB_2_GDK (  32,  74, 135 ), /* some */
127            RGB_2_GDK (  85,  87, 83 ), /* few */
128            RGB_2_GDK ( 238, 238, 236 ), /* none - tango aluminum highlight */
129            RGB_2_GDK (  46,  52, 54 ), /* black - tango slate shadow */
130            RGB_2_GDK ( 186, 189, 182 ), /* gray - tango aluminum shadow */
131            RGB_2_GDK ( 252, 233, 79 ), /* blink - tango butter highlight */
132        };
133
134        gcs = g_new ( GdkGC *, N_COLORS + 1 );
135
136        for( i = 0; i < N_COLORS; ++i )
137        {
138            gcs[i] = gdk_gc_new ( da->window );
139            gdk_gc_set_colormap ( gcs[i], colormap );
140            gdk_gc_set_rgb_fg_color ( gcs[i], &colors[i] );
141            gdk_gc_set_rgb_bg_color ( gcs[i], &colors[i] );
142        }
143
144        gcs[N_COLORS] = NULL; /* a sentinel in the release function */
145        g_object_set_data_full ( G_OBJECT( da ), "graphics-contexts",
146                                 gcs, release_gobject_array );
147    }
148
149    /**
150    ***  Get the cells' previous colors...
151    ***  (this is used for blinking when the color changes)
152    **/
153
154    prev_color = (guint8*) g_object_get_data ( G_OBJECT( da ), "prev-color" );
155    if( prev_color == NULL )
156    {
157        first_time = TRUE;
158        prev_color = g_new0 ( guint8, n_cells );
159        g_object_set_data_full ( G_OBJECT(
160                                     da ), "prev-color", prev_color, g_free );
161    }
162
163    /**
164    ***  Get the piece data values...
165    **/
166
167    switch( mode )
168    {
169        case DRAW_AVAIL:
170            pieces = g_new ( int8_t, n_cells );
171            tr_torrentAvailability ( tor, pieces, n_cells );
172            break;
173
174        case DRAW_PROG:
175            completeness = g_new ( float, n_cells );
176            tr_torrentAmountFinished ( tor, completeness, n_cells );
177            break;
178
179        default:
180            g_error( "no mode defined!" );
181    }
182
183    /**
184    ***  Draw...
185    **/
186
187    i = 0;
188    for( y = 0; y < n_rows; ++y )
189    {
190        for( x = 0; x < n_cols; ++x )
191        {
192            int draw_x = grid_bounds.x + (int)( x * piece_w );
193            int draw_y = grid_bounds.y + (int)( y * piece_h );
194            int color = BLACK;
195            int border = BLACK;
196
197            if( rtl )
198                draw_x = grid_bounds.x + grid_bounds.width -
199                         (int)( ( x + 1 ) * piece_w );
200            else
201                draw_x = grid_bounds.x + (int)( x * piece_w );
202            draw_y = grid_bounds.y + (int)( y * piece_h );
203
204            if( i < n_cells )
205            {
206                border = GRAY;
207
208                if( mode == DRAW_AVAIL )
209                {
210                    const int8_t val = pieces[i];
211                    if( val <  0 ) color = ALL;
212                    else if( val == 0 ) color = NONE;
213                    else if( val <= 4 ) color = FEW;
214                    else if( val <= 8 ) color = SOME;
215                    else color = LOTS;
216                }
217                else /* completeness */
218                {
219                    const float val = completeness[i];
220                    if( val >= 1.00 ) color = ALL;
221                    else if( val >= 0.66 ) color = LOTS;
222                    else if( val >= 0.33 ) color = SOME;
223                    else if( val >= 0.01 ) color = FEW;
224                    else color = NONE;
225                }
226
227                /* draw a "blink" for one interval when a piece changes */
228                if( first_time )
229                    prev_color[i] = color;
230                else if( color != prev_color[i] )
231                {
232                    prev_color[i] = color;
233                    color = border = BLINK;
234                }
235            }
236
237            gdk_draw_rectangle ( da->window, gcs[color], TRUE,
238                                 draw_x, draw_y,
239                                 piece_w_int, piece_h_int );
240
241            if( i < n_cells )
242                gdk_draw_rectangle ( da->window, gcs[border], FALSE,
243                                     draw_x, draw_y,
244                                     piece_w_int, piece_h_int );
245
246            ++i;
247        }
248    }
249
250    gdk_draw_rectangle ( da->window, gcs[GRAY], FALSE,
251                         grid_bounds.x, grid_bounds.y,
252                         grid_bounds.width - 1, grid_bounds.height - 1 );
253
254    g_free ( pieces );
255    g_free ( completeness );
256    return FALSE;
257}
258
259#endif
260
261/****
262*****  PEERS TAB
263****/
264
265enum
266{
267    WEBSEED_COL_URL,
268    WEBSEED_COL_DOWNLOAD_RATE,
269    N_WEBSEED_COLS
270};
271
272static const char * webseed_column_names[N_WEBSEED_COLS] =
273{
274    N_( "Web Seeds" ),
275    /* 'download speed' column header. terse to keep the column narrow. */
276    N_( "Down" )
277};
278
279static GtkTreeModel*
280webseed_model_new( const tr_torrent * tor )
281{
282    int             i;
283    const tr_info * inf = tr_torrentInfo( tor );
284    float *         speeds = tr_torrentWebSpeeds( tor );
285    GtkListStore *  store = gtk_list_store_new( N_WEBSEED_COLS,
286                                                G_TYPE_STRING,
287                                                G_TYPE_FLOAT );
288
289    for( i = 0; i < inf->webseedCount; ++i )
290    {
291        GtkTreeIter iter;
292        gtk_list_store_append( store, &iter );
293        gtk_list_store_set( store, &iter,
294                            WEBSEED_COL_URL, inf->webseeds[i],
295                            WEBSEED_COL_DOWNLOAD_RATE, speeds[i],
296                            -1 );
297    }
298
299    tr_free( speeds );
300    return GTK_TREE_MODEL( store );
301}
302
303enum
304{
305    PEER_COL_ADDRESS,
306    PEER_COL_DOWNLOAD_RATE,
307    PEER_COL_UPLOAD_RATE,
308    PEER_COL_CLIENT,
309    PEER_COL_PROGRESS,
310    PEER_COL_IS_ENCRYPTED,
311    PEER_COL_STATUS,
312    N_PEER_COLS
313};
314
315static const char* peer_column_names[N_PEER_COLS] =
316{
317    N_( "Address" ),
318    /* 'download speed' column header. terse to keep the column narrow. */
319    N_( "Down" ),
320    /* 'upload speed' column header.  terse to keep the column narrow. */
321    N_( "Up" ),
322    N_( "Client" ),
323    /* 'percent done' column header. terse to keep the column narrow. */
324    N_( "%" ),
325    " ",
326    N_( "Status" )
327};
328
329static int
330compare_peers( const void * a, const void * b )
331{
332    const tr_peer_stat * pa = a;
333    const tr_peer_stat * pb = b;
334
335    return strcmp( pa->addr, pb->addr );
336}
337
338static int
339compare_addr_to_peer( const void * addr, const void * b )
340{
341    const tr_peer_stat * peer = b;
342
343    return strcmp( addr, peer->addr );
344}
345
346static void
347peer_row_set( GtkListStore        * store,
348              GtkTreeIter         * iter,
349              const tr_peer_stat  * peer )
350{
351    const char * client = peer->client;
352
353    if( !client || !strcmp( client, "Unknown Client" ) )
354        client = " ";
355
356    gtk_list_store_set( store, iter,
357                        PEER_COL_ADDRESS, peer->addr,
358                        PEER_COL_CLIENT, client,
359                        PEER_COL_IS_ENCRYPTED, peer->isEncrypted,
360                        PEER_COL_PROGRESS, (int)( 100.0 * peer->progress ),
361                        PEER_COL_DOWNLOAD_RATE, peer->rateToClient,
362                        PEER_COL_UPLOAD_RATE, peer->rateToPeer,
363                        PEER_COL_STATUS, peer->flagStr,
364                        -1 );
365}
366
367static void
368append_peers_to_model( GtkListStore *       store,
369                       const tr_peer_stat * peers,
370                       int                  n_peers )
371{
372    int i;
373
374    for( i = 0; i < n_peers; ++i )
375    {
376        GtkTreeIter iter;
377        gtk_list_store_append( store, &iter );
378        peer_row_set ( store, &iter, &peers[i] );
379    }
380}
381
382static GtkTreeModel*
383peer_model_new( tr_torrent * tor )
384{
385    GtkListStore * m = gtk_list_store_new ( N_PEER_COLS,
386                                            G_TYPE_STRING, /* addr */
387                                            G_TYPE_FLOAT, /* downloadFromRate */
388                                            G_TYPE_FLOAT, /* uploadToRate */
389                                            G_TYPE_STRING, /* client */
390                                            G_TYPE_INT,   /* progress [0..100] */
391                                            G_TYPE_BOOLEAN, /* isEncrypted */
392                                            G_TYPE_STRING ); /* flagString */
393
394    int            n_peers = 0;
395    tr_peer_stat * peers = tr_torrentPeers ( tor, &n_peers );
396
397    qsort ( peers, n_peers, sizeof( tr_peer_stat ), compare_peers );
398    append_peers_to_model ( m, peers, n_peers );
399    tr_torrentPeersFree( peers, 0 );
400    return GTK_TREE_MODEL ( m );
401}
402
403static void
404render_encrypted( GtkTreeViewColumn  * column UNUSED,
405                  GtkCellRenderer *           renderer,
406                  GtkTreeModel *              tree_model,
407                  GtkTreeIter *               iter,
408                  gpointer             data   UNUSED )
409{
410    gboolean is_encrypted = FALSE;
411
412    gtk_tree_model_get ( tree_model, iter, PEER_COL_IS_ENCRYPTED,
413                         &is_encrypted,
414                         -1 );
415    g_object_set ( renderer, "xalign", (gfloat)0.0,
416                   "yalign", (gfloat)0.5,
417                   "stock-id", ( is_encrypted ? "transmission-lock" : NULL ),
418                   NULL );
419}
420
421static void
422render_ul_rate( GtkTreeViewColumn  * column UNUSED,
423                GtkCellRenderer *           renderer,
424                GtkTreeModel *              tree_model,
425                GtkTreeIter *               iter,
426                gpointer             data   UNUSED )
427{
428    float rate = 0.0;
429
430    gtk_tree_model_get ( tree_model, iter, PEER_COL_UPLOAD_RATE, &rate, -1 );
431    if( rate < 0.01 )
432        g_object_set ( renderer, "text", "", NULL );
433    else
434    {
435        char speedStr[64];
436        tr_strlspeed( speedStr, rate, sizeof( speedStr ) );
437        g_object_set( renderer, "text", speedStr, NULL );
438    }
439}
440
441static void
442render_dl_rate( GtkTreeViewColumn  * column UNUSED,
443                GtkCellRenderer *           renderer,
444                GtkTreeModel *              tree_model,
445                GtkTreeIter *               iter,
446                gpointer             data   UNUSED )
447{
448    float rate = 0.0;
449
450    gtk_tree_model_get ( tree_model, iter, PEER_COL_DOWNLOAD_RATE, &rate,
451                         -1 );
452    if( rate < 0.01 )
453        g_object_set ( renderer, "text", "", NULL );
454    else
455    {
456        char speedStr[64];
457        tr_strlspeed( speedStr, rate, sizeof( speedStr ) );
458        g_object_set( renderer, "text", speedStr, NULL );
459    }
460}
461
462static void
463render_client( GtkTreeViewColumn   * column UNUSED,
464               GtkCellRenderer *            renderer,
465               GtkTreeModel *               tree_model,
466               GtkTreeIter *                iter,
467               gpointer              data   UNUSED )
468{
469    char * client = NULL;
470
471    gtk_tree_model_get ( tree_model, iter, PEER_COL_CLIENT, &client,
472                         -1 );
473    g_object_set ( renderer, "text", ( client ? client : "" ), NULL );
474    g_free ( client );
475}
476
477typedef struct
478{
479    TrTorrent *     gtor;
480    GtkTreeModel *  model; /* same object as store, but recast */
481    GtkListStore *  store; /* same object as model, but recast */
482    GtkListStore *  webseeds;
483    GtkWidget *     completeness;
484    GtkWidget *     seeders_lb;
485    GtkWidget *     leechers_lb;
486    GtkWidget *     completed_lb;
487    GtkWidget *     peer_tree_view;
488}
489PeerData;
490
491static void
492fmtpeercount( GtkWidget * l,
493              int         count )
494{
495    if( 0 > count )
496    {
497        gtk_label_set_text( GTK_LABEL( l ), "?" );
498    }
499    else
500    {
501        char str[16];
502        g_snprintf( str, sizeof str, "%'d", count );
503        gtk_label_set_text( GTK_LABEL( l ), str );
504    }
505}
506
507static void
508refresh_peers( GtkWidget * top )
509{
510    int             i;
511    int             n_peers;
512    GtkTreeIter     iter;
513    PeerData *      p = (PeerData*) g_object_get_data ( G_OBJECT(
514                                                            top ),
515                                                        "peer-data" );
516    tr_torrent *    tor = tr_torrent_handle ( p->gtor );
517    GtkTreeModel *  model = p->model;
518    GtkListStore *  store = p->store;
519    tr_peer_stat *  peers;
520    const tr_stat * stat = tr_torrent_stat( p->gtor );
521    const tr_info * inf = tr_torrent_info( p->gtor );
522
523    if( inf->webseedCount )
524    {
525        float * speeds = tr_torrentWebSpeeds( tor );
526        for( i = 0; i < inf->webseedCount; ++i )
527        {
528            GtkTreeIter iter;
529            gtk_tree_model_iter_nth_child( GTK_TREE_MODEL(
530                                               p->webseeds ), &iter, NULL,
531                                           i );
532            gtk_list_store_set( p->webseeds, &iter,
533                                WEBSEED_COL_DOWNLOAD_RATE, speeds[i],
534                                -1 );
535        }
536        tr_free( speeds );
537    }
538
539    /**
540    ***  merge the peer diffs into the tree model.
541    ***
542    ***  this is more complicated than creating a new model,
543    ***  but is also (a) more efficient and (b) doesn't undo
544    ***  the view's visible area and sorting on every refresh.
545    **/
546
547    n_peers = 0;
548    peers = tr_torrentPeers ( tor, &n_peers );
549    qsort ( peers, n_peers, sizeof( tr_peer_stat ), compare_peers );
550
551    if( gtk_tree_model_get_iter_first ( model, &iter ) ) do
552        {
553            char *         addr = NULL;
554            tr_peer_stat * peer = NULL;
555            gtk_tree_model_get ( model, &iter, PEER_COL_ADDRESS, &addr, -1 );
556            peer = bsearch ( addr, peers, n_peers, sizeof( tr_peer_stat ),
557                             compare_addr_to_peer );
558            g_free ( addr );
559
560            if( peer ) /* update a pre-existing row */
561            {
562                const int pos = peer - peers;
563                const int n_rhs = n_peers - ( pos + 1 );
564                g_assert ( n_rhs >= 0 );
565
566                peer_row_set ( store, &iter, peer );
567
568                /* remove it from the tr_peer_stat list */
569                g_memmove ( peer, peer + 1, sizeof( tr_peer_stat ) * n_rhs );
570                --n_peers;
571            }
572            else if( !gtk_list_store_remove ( store, &iter ) )
573                break; /* we removed the model's last item */
574        }
575        while( gtk_tree_model_iter_next ( model, &iter ) );
576
577    append_peers_to_model ( store, peers, n_peers ); /* all these are new */
578
579#ifdef SHOW_PIECES
580    if( GDK_IS_DRAWABLE ( p->completeness->window ) )
581        refresh_pieces ( p->completeness, NULL, p->gtor );
582#endif
583
584    fmtpeercount ( p->seeders_lb, stat->seeders );
585    fmtpeercount ( p->leechers_lb, stat->leechers );
586    fmtpeercount ( p->completed_lb, stat->timesCompleted );
587
588    free( peers );
589
590    gtk_widget_queue_draw( p->peer_tree_view );
591}
592
593#if GTK_CHECK_VERSION( 2, 12, 0 )
594static gboolean
595onPeerViewQueryTooltip( GtkWidget *            widget,
596                        gint                   x,
597                        gint                   y,
598                        gboolean               keyboard_tip,
599                        GtkTooltip *           tooltip,
600                        gpointer     user_data UNUSED )
601{
602    gboolean       show_tip = FALSE;
603    GtkTreeModel * model;
604    GtkTreeIter    iter;
605
606    if( gtk_tree_view_get_tooltip_context( GTK_TREE_VIEW( widget ),
607                                           &x, &y, keyboard_tip,
608                                           &model, NULL, &iter ) )
609    {
610        const char * pch;
611        char *       str = NULL;
612        GString *    gstr = g_string_new( NULL );
613        gtk_tree_model_get( model, &iter, PEER_COL_STATUS, &str, -1 );
614        for( pch = str; pch && *pch; ++pch )
615        {
616            const char * txt = NULL;
617            switch( *pch )
618            {
619                case 'O':
620                    txt = _( "Optimistic unchoke" ); break;
621
622                case 'D':
623                    txt = _( "Downloading from this peer" ); break;
624
625                case 'd':
626                    txt = _(
627                        "We would download from this peer if they would let us" );
628                    break;
629
630                case 'U':
631                    txt = _( "Uploading to peer" ); break;
632
633                case 'u':
634                    txt = _( "We would upload to this peer if they asked" );
635                    break;
636
637                case 'K':
638                    txt = _(
639                        "Peer has unchoked us, but we're not interested" );
640                    break;
641
642                case '?':
643                    txt = _(
644                        "We unchoked this peer, but they're not interested" );
645                    break;
646
647                case 'E':
648                    txt = _( "Encrypted connection" ); break;
649
650                case 'X':
651                    txt = _(
652                        "Peer was discovered through Peer Exchange (PEX)" );
653                    break;
654
655                case 'I':
656                    txt = _( "Peer is an incoming connection" ); break;
657            }
658            if( txt )
659                g_string_append_printf( gstr, "%c: %s\n", *pch, txt );
660        }
661        if( gstr->len ) /* remove the last linefeed */
662            g_string_set_size( gstr, gstr->len - 1 );
663        gtk_tooltip_set_text( tooltip, gstr->str );
664        g_string_free( gstr, TRUE );
665        g_free( str );
666        show_tip = TRUE;
667    }
668
669    return show_tip;
670}
671
672#endif
673
674static GtkWidget*
675peer_page_new( TrTorrent * gtor )
676{
677    guint           i;
678    GtkTreeModel *  m;
679    GtkWidget *     v, *w, *ret, *sw, *l, *vbox, *hbox;
680    GtkWidget *     webtree = NULL;
681    tr_torrent *    tor = tr_torrent_handle ( gtor );
682    PeerData *      p = g_new ( PeerData, 1 );
683    const tr_info * inf = tr_torrent_info( gtor );
684
685    /* TODO: make this configurable? */
686    int view_columns[] = { PEER_COL_IS_ENCRYPTED,
687                           PEER_COL_UPLOAD_RATE,
688                           PEER_COL_DOWNLOAD_RATE,
689                           PEER_COL_PROGRESS,
690                           PEER_COL_STATUS,
691                           PEER_COL_ADDRESS,
692                           PEER_COL_CLIENT };
693
694
695    if( inf->webseedCount )
696    {
697        GtkTreeViewColumn * c;
698        GtkCellRenderer *   r;
699        const char *        t;
700        GtkWidget *         w;
701        GtkWidget *         v;
702
703        m = webseed_model_new( tr_torrent_handle( gtor ) );
704        v = gtk_tree_view_new_with_model( m );
705        g_signal_connect( v, "button-release-event", G_CALLBACK( on_tree_view_button_released ), NULL );
706        gtk_tree_view_set_rules_hint( GTK_TREE_VIEW( v ), TRUE );
707        p->webseeds = GTK_LIST_STORE( m );
708        g_object_unref( G_OBJECT( m ) );
709
710        t = _( webseed_column_names[WEBSEED_COL_URL] );
711        r = gtk_cell_renderer_text_new ( );
712        g_object_set( G_OBJECT( r ), "ellipsize", PANGO_ELLIPSIZE_END, NULL );
713        c = gtk_tree_view_column_new_with_attributes( t, r, "text", WEBSEED_COL_URL, NULL );
714        g_object_set( G_OBJECT( c ), "expand", TRUE, NULL );
715        gtk_tree_view_column_set_sort_column_id( c, WEBSEED_COL_URL );
716        gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
717
718        t = _( webseed_column_names[WEBSEED_COL_DOWNLOAD_RATE] );
719        r = gtk_cell_renderer_text_new ( );
720        c = gtk_tree_view_column_new_with_attributes ( t, r, "text", WEBSEED_COL_DOWNLOAD_RATE, NULL );
721        gtk_tree_view_column_set_cell_data_func ( c, r, render_dl_rate, NULL, NULL );
722        gtk_tree_view_column_set_sort_column_id( c, WEBSEED_COL_DOWNLOAD_RATE );
723        gtk_tree_view_append_column( GTK_TREE_VIEW( v ), c );
724
725        w = gtk_scrolled_window_new ( NULL, NULL );
726        gtk_scrolled_window_set_policy ( GTK_SCROLLED_WINDOW( w ),
727                                         GTK_POLICY_AUTOMATIC,
728                                         GTK_POLICY_AUTOMATIC );
729        gtk_scrolled_window_set_shadow_type ( GTK_SCROLLED_WINDOW( w ),
730                                              GTK_SHADOW_IN );
731        gtk_container_add ( GTK_CONTAINER( w ), v );
732
733        webtree = w;
734    }
735
736    m  = peer_model_new ( tor );
737    v = GTK_WIDGET( g_object_new( GTK_TYPE_TREE_VIEW,
738                                  "model",  gtk_tree_model_sort_new_with_model( m ),
739                                  "rules-hint", TRUE,
740#if GTK_CHECK_VERSION( 2, 12, 0 )
741                                  "has-tooltip", TRUE,
742#endif
743                                  NULL ) );
744    p->peer_tree_view = v;
745
746#if GTK_CHECK_VERSION( 2, 12, 0 )
747    g_signal_connect( v, "query-tooltip",
748                      G_CALLBACK( onPeerViewQueryTooltip ), NULL );
749#endif
750    gtk_widget_set_size_request( v, 550, 0 );
751    g_object_unref ( G_OBJECT( m ) );
752    g_signal_connect( v, "button-release-event",
753                      G_CALLBACK( on_tree_view_button_released ), NULL );
754
755    for( i = 0; i < G_N_ELEMENTS( view_columns ); ++i )
756    {
757        const int           col = view_columns[i];
758        const char *        t = _( peer_column_names[col] );
759        GtkTreeViewColumn * c;
760        GtkCellRenderer *   r;
761
762        switch( col )
763        {
764            case PEER_COL_ADDRESS:
765                r = gtk_cell_renderer_text_new ( );
766                c = gtk_tree_view_column_new_with_attributes ( t, r, "text", col, NULL );
767                break;
768
769            case PEER_COL_CLIENT:
770                r = gtk_cell_renderer_text_new ( );
771                c = gtk_tree_view_column_new_with_attributes ( t, r, "text", col, NULL );
772                gtk_tree_view_column_set_cell_data_func ( c, r, render_client, NULL, NULL );
773                break;
774
775            case PEER_COL_PROGRESS:
776                r = gtk_cell_renderer_progress_new ( );
777                c = gtk_tree_view_column_new_with_attributes ( t, r, "value", PEER_COL_PROGRESS, NULL );
778                break;
779
780            case PEER_COL_IS_ENCRYPTED:
781                r = gtk_cell_renderer_pixbuf_new ( );
782                c = gtk_tree_view_column_new_with_attributes ( t, r, NULL );
783                gtk_tree_view_column_set_sizing ( c, GTK_TREE_VIEW_COLUMN_FIXED );
784                gtk_tree_view_column_set_fixed_width ( c, 20 );
785                gtk_tree_view_column_set_cell_data_func ( c, r, render_encrypted, NULL, NULL );
786                break;
787
788            case PEER_COL_DOWNLOAD_RATE:
789                r = gtk_cell_renderer_text_new ( );
790                c = gtk_tree_view_column_new_with_attributes ( t, r, "text", col, NULL );
791                gtk_tree_view_column_set_cell_data_func ( c, r, render_dl_rate, NULL, NULL );
792                break;
793
794            case PEER_COL_UPLOAD_RATE:
795                r = gtk_cell_renderer_text_new ( );
796                c = gtk_tree_view_column_new_with_attributes ( t, r, "text", col, NULL );
797                gtk_tree_view_column_set_cell_data_func ( c, r, render_ul_rate, NULL, NULL );
798                break;
799
800            case PEER_COL_STATUS:
801                r = gtk_cell_renderer_text_new( );
802                c = gtk_tree_view_column_new_with_attributes ( t, r, "text", col, NULL );
803                break;
804
805            default:
806                abort ( );
807        }
808
809        gtk_tree_view_column_set_resizable ( c, FALSE );
810        gtk_tree_view_column_set_sort_column_id ( c, col );
811        gtk_tree_view_append_column ( GTK_TREE_VIEW( v ), c );
812    }
813
814    /* the 'expander' column has a 10-pixel margin on the left
815       that doesn't look quite correct in any of these columns...
816       so create a non-visible column and assign it as the
817       'expander column. */
818    {
819        GtkTreeViewColumn *c = gtk_tree_view_column_new ( );
820        gtk_tree_view_column_set_visible ( c, FALSE );
821        gtk_tree_view_append_column ( GTK_TREE_VIEW( v ), c );
822        gtk_tree_view_set_expander_column ( GTK_TREE_VIEW( v ), c );
823    }
824
825    w = sw = gtk_scrolled_window_new ( NULL, NULL );
826    gtk_scrolled_window_set_policy ( GTK_SCROLLED_WINDOW( w ),
827                                     GTK_POLICY_AUTOMATIC,
828                                     GTK_POLICY_AUTOMATIC );
829    gtk_scrolled_window_set_shadow_type ( GTK_SCROLLED_WINDOW( w ),
830                                          GTK_SHADOW_IN );
831    gtk_container_add ( GTK_CONTAINER( w ), v );
832
833
834    vbox = gtk_vbox_new ( FALSE, GUI_PAD );
835    gtk_container_set_border_width ( GTK_CONTAINER( vbox ), GUI_PAD_BIG );
836
837    if( webtree == NULL )
838        gtk_box_pack_start( GTK_BOX( vbox ), sw, TRUE, TRUE, 0 );
839    else {
840        GtkWidget * vpaned = gtk_vpaned_new( );
841        gtk_paned_pack1( GTK_PANED( vpaned ), webtree, FALSE, TRUE );
842        gtk_paned_pack2( GTK_PANED( vpaned ), sw, TRUE, TRUE );
843        gtk_box_pack_start( GTK_BOX( vbox ), vpaned, TRUE, TRUE, 0 );
844    }
845
846    hbox = gtk_hbox_new ( FALSE, GUI_PAD );
847    l = gtk_label_new ( NULL );
848    gtk_label_set_markup ( GTK_LABEL( l ), _( "<b>Seeders:</b>" ) );
849    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
850    l = p->seeders_lb = gtk_label_new ( NULL );
851    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
852    gtk_box_pack_start( GTK_BOX( hbox ),
853                        gtk_alignment_new ( 0.0f, 0.0f, 0.0f, 0.0f ),
854                        TRUE, TRUE, 0 );
855    l = gtk_label_new ( NULL );
856    gtk_label_set_markup ( GTK_LABEL( l ), _( "<b>Leechers:</b>" ) );
857    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
858    l = p->leechers_lb = gtk_label_new ( NULL );
859    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
860    gtk_box_pack_start( GTK_BOX( hbox ),
861                        gtk_alignment_new ( 0.0f, 0.0f, 0.0f, 0.0f ),
862                        TRUE, TRUE, 0 );
863    l = gtk_label_new ( NULL );
864    gtk_label_set_markup ( GTK_LABEL( l ), _( "<b>Times Completed:</b>" ) );
865    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
866    l = p->completed_lb = gtk_label_new ( NULL );
867    gtk_box_pack_start ( GTK_BOX( hbox ), l, FALSE, FALSE, 0 );
868    gtk_box_pack_start ( GTK_BOX( vbox ), hbox, FALSE, FALSE, 0 );
869
870    ret = vbox;
871    p->gtor = gtor;
872    p->model = m;
873    p->store = GTK_LIST_STORE( m );
874    g_object_set_data_full ( G_OBJECT( ret ), "peer-data", p, g_free );
875    return ret;
876}
877
878/****
879*****  INFO TAB
880****/
881
882static void
883refresh_time_lb( GtkWidget * l,
884                 time_t      t )
885{
886    const char * never = _( "Never" );
887
888    if( !t )
889        gtk_label_set_text( GTK_LABEL( l ), never );
890    else
891    {
892        char * str = gtr_localtime( t );
893        gtk_label_set_text( GTK_LABEL( l ), str );
894        g_free( str );
895    }
896}
897
898static GtkWidget*
899info_page_new( tr_torrent * tor )
900{
901    int             row = 0;
902    GtkWidget *     t = hig_workarea_create ( );
903    GtkWidget *     l, *w, *fr;
904    char *          pch;
905    char            sizeStr[128];
906    char            countStr[128];
907    char            buf[256];
908    GtkTextBuffer * b;
909    const tr_info * info = tr_torrentInfo( tor );
910
911    hig_workarea_add_section_title ( t, &row, _( "Details" ) );
912
913    g_snprintf( countStr, sizeof( countStr ),
914                ngettext( "%'d Piece", "%'d Pieces", info->pieceCount ),
915                info->pieceCount );
916    tr_strlsize( sizeStr, info->pieceSize, sizeof( sizeStr ) );
917    g_snprintf( buf, sizeof( buf ),
918                /* %1$s is number of pieces;
919                   %2$s is how big each piece is */
920                _( "%1$s @ %2$s" ),
921                countStr, sizeStr );
922
923    l = gtk_label_new ( buf );
924    hig_workarea_add_row ( t, &row, _( "Pieces:" ), l, NULL );
925
926    l =
927        g_object_new( GTK_TYPE_LABEL, "label", info->hashString,
928                      "selectable",
929                      TRUE,
930                      "ellipsize", PANGO_ELLIPSIZE_END,
931                      NULL );
932    hig_workarea_add_row ( t, &row, _( "Hash:" ), l, NULL );
933
934    pch = ( info->isPrivate )
935          ? _( "Private to this tracker -- PEX disabled" )
936          : _( "Public torrent" );
937    l = gtk_label_new ( pch );
938    hig_workarea_add_row ( t, &row, _( "Privacy:" ), l, NULL );
939
940    b = gtk_text_buffer_new ( NULL );
941    if( info->comment )
942        gtk_text_buffer_set_text ( b, info->comment, -1 );
943    w = gtk_text_view_new_with_buffer ( b );
944    gtk_widget_set_size_request ( w, 0u, 100u );
945    gtk_text_view_set_wrap_mode ( GTK_TEXT_VIEW( w ), GTK_WRAP_WORD );
946    gtk_text_view_set_editable ( GTK_TEXT_VIEW( w ), FALSE );
947    fr = gtk_frame_new ( NULL );
948    gtk_frame_set_shadow_type ( GTK_FRAME( fr ), GTK_SHADOW_IN );
949    gtk_container_add ( GTK_CONTAINER( fr ), w );
950    w = hig_workarea_add_row ( t, &row, _( "Comment:" ), fr, NULL );
951    gtk_misc_set_alignment ( GTK_MISC( w ), 0.0f, 0.0f );
952
953    hig_workarea_add_section_divider ( t, &row );
954    hig_workarea_add_section_title ( t, &row, _( "Origins" ) );
955
956    l = gtk_label_new ( *info->creator ? info->creator : _( "Unknown" ) );
957    gtk_label_set_ellipsize( GTK_LABEL( l ), PANGO_ELLIPSIZE_END );
958    hig_workarea_add_row ( t, &row, _( "Creator:" ), l, NULL );
959
960    l = gtk_label_new( NULL );
961    refresh_time_lb( l, info->dateCreated );
962    hig_workarea_add_row ( t, &row, _( "Date:" ), l, NULL );
963
964    hig_workarea_add_section_divider ( t, &row );
965    hig_workarea_add_section_title ( t, &row, _( "Location" ) );
966
967    l =
968        g_object_new( GTK_TYPE_LABEL, "label", tr_torrentGetDownloadDir(
969                          tor ), "selectable", TRUE,
970                      "ellipsize", PANGO_ELLIPSIZE_END, NULL );
971    hig_workarea_add_row ( t, &row, _( "Destination folder:" ), l, NULL );
972
973    l =
974        g_object_new( GTK_TYPE_LABEL, "label", info->torrent, "selectable",
975                      TRUE,
976                      "ellipsize", PANGO_ELLIPSIZE_END,
977                      NULL );
978    hig_workarea_add_row ( t, &row, _( "Torrent file:" ), l, NULL );
979
980    hig_workarea_finish ( t, &row );
981    return t;
982}
983
984/****
985*****  ACTIVITY TAB
986****/
987
988typedef struct
989{
990    GtkWidget *  state_lb;
991    GtkWidget *  progress_lb;
992    GtkWidget *  have_lb;
993    GtkWidget *  dl_lb;
994    GtkWidget *  ul_lb;
995    GtkWidget *  failed_lb;
996    GtkWidget *  ratio_lb;
997    GtkWidget *  err_lb;
998    GtkWidget *  swarm_lb;
999    GtkWidget *  date_added_lb;
1000    GtkWidget *  last_activity_lb;
1001    GtkWidget *  availability_da;
1002    TrTorrent *  gtor;
1003}
1004Activity;
1005
1006static void
1007refresh_activity( GtkWidget * top )
1008{
1009    Activity *      a = g_object_get_data ( G_OBJECT(
1010                                                top ), "activity-data" );
1011    char *          pch;
1012    char            sizeStr[64];
1013    char            sizeStr2[64];
1014    char            buf[128];
1015    const tr_stat * stat = tr_torrent_stat( a->gtor );
1016    const tr_info * info = tr_torrent_info( a->gtor );
1017    const double    complete = stat->percentComplete * 100.0;
1018    const double    done = stat->percentDone * 100.0;
1019    const double    verifiedPieceCount = (double)stat->haveValid / info->pieceSize;
1020
1021    pch = tr_torrent_status_str( a->gtor );
1022    gtk_label_set_text ( GTK_LABEL( a->state_lb ), pch );
1023    g_free ( pch );
1024
1025    if( (int)complete == (int)done )
1026        pch = g_strdup_printf( _( "%.1f%%" ), complete );
1027    else
1028        /* %1$.1f is percent of how much of what we want's been downloaded,
1029           %2$.1f is percent of how much of the whole torrent we've downloaded
1030           */
1031        pch = g_strdup_printf( _(
1032                                   "%1$.1f%% (%2$.1f%% selected)" ),
1033                               complete, done );
1034    gtk_label_set_text ( GTK_LABEL( a->progress_lb ), pch );
1035    g_free ( pch );
1036
1037    tr_strlsize( sizeStr,  stat->haveValid + stat->haveUnchecked,
1038                sizeof( sizeStr ) );
1039    tr_strlsize( sizeStr2, stat->haveValid,
1040                sizeof( sizeStr2 ) );
1041    /* %1$s is total size of what we've saved to disk
1042       %2$s is how much of it's passed the checksum test
1043       %3$s is how many pieces are verified */
1044    g_snprintf( buf, sizeof( buf ), _( "%1$s (%2$s verified in %3$d pieces)" ),
1045                sizeStr, sizeStr2, (int)ceil(verifiedPieceCount) );
1046    gtk_label_set_text( GTK_LABEL( a->have_lb ), buf );
1047
1048    tr_strlsize( sizeStr, stat->downloadedEver, sizeof( sizeStr ) );
1049    gtk_label_set_text( GTK_LABEL( a->dl_lb ), sizeStr );
1050
1051    tr_strlsize( sizeStr, stat->uploadedEver, sizeof( sizeStr ) );
1052    gtk_label_set_text( GTK_LABEL( a->ul_lb ), sizeStr );
1053
1054    tr_strlsize( sizeStr, stat->corruptEver, sizeof( sizeStr ) );
1055    gtk_label_set_text( GTK_LABEL( a->failed_lb ), sizeStr );
1056
1057    tr_strlratio( buf, stat->ratio, sizeof( buf ) );
1058    gtk_label_set_text( GTK_LABEL( a->ratio_lb ), buf );
1059
1060    tr_strlspeed( buf, stat->swarmSpeed, sizeof( buf ) );
1061    gtk_label_set_text ( GTK_LABEL( a->swarm_lb ), buf );
1062
1063    gtk_label_set_text ( GTK_LABEL( a->err_lb ),
1064                        *stat->errorString ? stat->errorString : _( "None" ) );
1065
1066    refresh_time_lb( a->date_added_lb, stat->addedDate );
1067
1068    refresh_time_lb( a->last_activity_lb, stat->activityDate );
1069
1070#ifdef SHOW_PIECES
1071    if( GDK_IS_DRAWABLE ( a->availability_da->window ) )
1072        refresh_pieces ( a->availability_da, NULL, a->gtor );
1073#endif
1074}
1075
1076static GtkWidget*
1077activity_page_new( TrTorrent * gtor )
1078{
1079    Activity * a = g_new ( Activity, 1 );
1080    int        row = 0;
1081    GtkWidget *t = hig_workarea_create ( );
1082    GtkWidget *l;
1083
1084    a->gtor = gtor;
1085
1086    hig_workarea_add_section_title ( t, &row, _( "Transfer" ) );
1087
1088    l = a->state_lb = gtk_label_new ( NULL );
1089    hig_workarea_add_row ( t, &row, _( "State:" ), l, NULL );
1090
1091    l = a->progress_lb = gtk_label_new ( NULL );
1092    hig_workarea_add_row ( t, &row, _( "Progress:" ), l, NULL );
1093
1094    l = a->have_lb = gtk_label_new ( NULL );
1095    /* "Have" refers to how much of the torrent we have */
1096    hig_workarea_add_row ( t, &row, _( "Have:" ), l, NULL );
1097
1098    l = a->dl_lb = gtk_label_new ( NULL );
1099    hig_workarea_add_row ( t, &row, _( "Downloaded:" ), l, NULL );
1100
1101    l = a->ul_lb = gtk_label_new ( NULL );
1102    hig_workarea_add_row ( t, &row, _( "Uploaded:" ), l, NULL );
1103
1104    /* how much downloaded data was corrupt */
1105    l = a->failed_lb = gtk_label_new ( NULL );
1106    hig_workarea_add_row ( t, &row, _( "Failed DL:" ), l, NULL );
1107
1108    l = a->ratio_lb = gtk_label_new ( NULL );
1109    hig_workarea_add_row ( t, &row, _( "Ratio:" ), l, NULL );
1110
1111    l = a->swarm_lb = gtk_label_new ( NULL );
1112    hig_workarea_add_row ( t, &row, _( "Swarm rate:" ), l, NULL );
1113
1114    l = a->err_lb = gtk_label_new ( NULL );
1115    hig_workarea_add_row ( t, &row, _( "Error:" ), l, NULL );
1116
1117#ifdef SHOW_PIECES
1118    hig_workarea_add_section_divider ( t, &row );
1119    hig_workarea_add_section_title ( t, &row, _( "Completion" ) );
1120
1121    w = a->availability_da = gtk_drawing_area_new ( );
1122    gtk_widget_set_size_request ( w, 0u, 100u );
1123    g_object_set_data ( G_OBJECT( w ), "draw-mode",
1124                       GINT_TO_POINTER( DRAW_PROG ) );
1125    g_signal_connect ( w, "expose-event", G_CALLBACK(
1126                           refresh_pieces ), gtor );
1127    hig_workarea_add_wide_control( t, &row, w );
1128#endif
1129
1130    hig_workarea_add_section_divider ( t, &row );
1131    hig_workarea_add_section_title ( t, &row, _( "Dates" ) );
1132
1133    l = a->date_added_lb = gtk_label_new ( NULL );
1134    hig_workarea_add_row ( t, &row, _( "Started at:" ), l, NULL );
1135
1136    l = a->last_activity_lb = gtk_label_new ( NULL );
1137    hig_workarea_add_row ( t, &row, _( "Last activity at:" ), l, NULL );
1138
1139    hig_workarea_add_section_divider ( t, &row );
1140    hig_workarea_finish ( t, &row );
1141    g_object_set_data_full ( G_OBJECT( t ), "activity-data", a, g_free );
1142    return t;
1143}
1144
1145/****
1146*****  OPTIONS
1147****/
1148
1149static void
1150speed_toggled_cb( GtkToggleButton * tb,
1151                  gpointer          gtor,
1152                  int               up_or_down )
1153{
1154    tr_torrent * tor = tr_torrent_handle ( gtor );
1155    gboolean     b = gtk_toggle_button_get_active( tb );
1156
1157    tr_torrentSetSpeedMode( tor, up_or_down, b ? TR_SPEEDLIMIT_SINGLE
1158                            : TR_SPEEDLIMIT_GLOBAL );
1159}
1160
1161static void
1162ul_speed_toggled_cb( GtkToggleButton *tb,
1163                     gpointer         gtor )
1164{
1165    speed_toggled_cb( tb, gtor, TR_UP );
1166}
1167
1168static void
1169dl_speed_toggled_cb( GtkToggleButton *tb,
1170                     gpointer         gtor )
1171{
1172    speed_toggled_cb( tb, gtor, TR_DOWN );
1173}
1174
1175static void
1176sensitize_from_check_cb( GtkToggleButton *toggle,
1177                         gpointer         w )
1178{
1179    gtk_widget_set_sensitive ( GTK_WIDGET( w ),
1180                              gtk_toggle_button_get_active( toggle ) );
1181}
1182
1183static void
1184setSpeedLimit( GtkSpinButton* spin,
1185               gpointer       gtor,
1186               int            up_or_down )
1187{
1188    tr_torrent * tor = tr_torrent_handle ( gtor );
1189    int          kb_sec = gtk_spin_button_get_value_as_int ( spin );
1190
1191    tr_torrentSetSpeedLimit( tor, up_or_down, kb_sec );
1192}
1193
1194static void
1195ul_speed_spun_cb( GtkSpinButton *spin,
1196                  gpointer       gtor )
1197{
1198    setSpeedLimit( spin, gtor, TR_UP );
1199}
1200
1201static void
1202dl_speed_spun_cb( GtkSpinButton *spin,
1203                  gpointer       gtor )
1204{
1205    setSpeedLimit( spin, gtor, TR_DOWN );
1206}
1207
1208static void
1209max_peers_spun_cb( GtkSpinButton * spin,
1210                   gpointer        gtor )
1211{
1212    const uint16_t n = gtk_spin_button_get_value( spin );
1213
1214    tr_torrentSetPeerLimit( tr_torrent_handle( gtor ), n );
1215}
1216
1217static GtkWidget*
1218options_page_new( TrTorrent * gtor )
1219{
1220    uint16_t     maxConnectedPeers;
1221    int          i, row;
1222    gboolean     b;
1223    GtkWidget *  t, *w, *tb;
1224    tr_torrent * tor = tr_torrent_handle ( gtor );
1225
1226    row = 0;
1227    t = hig_workarea_create ( );
1228    hig_workarea_add_section_title ( t, &row, _( "Limits" ) );
1229
1230    tb =
1231        gtk_check_button_new_with_mnemonic ( _(
1232                                                "Limit _download speed (KB/s):" ) );
1233    b = tr_torrentGetSpeedMode( tor, TR_DOWN ) == TR_SPEEDLIMIT_SINGLE;
1234    gtk_toggle_button_set_active ( GTK_TOGGLE_BUTTON( tb ), b );
1235    g_signal_connect ( tb, "toggled", G_CALLBACK(
1236                           dl_speed_toggled_cb ), gtor );
1237
1238    i = tr_torrentGetSpeedLimit( tor, TR_DOWN );
1239    w = gtk_spin_button_new_with_range( 1, INT_MAX, 5 );
1240    gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), i );
1241
1242    g_signal_connect ( w, "value-changed", G_CALLBACK(
1243                           dl_speed_spun_cb ), gtor );
1244    g_signal_connect ( tb, "toggled", G_CALLBACK(
1245                           sensitize_from_check_cb ), w );
1246    sensitize_from_check_cb ( GTK_TOGGLE_BUTTON( tb ), w );
1247    hig_workarea_add_row_w ( t, &row, tb, w, NULL );
1248
1249    tb =
1250        gtk_check_button_new_with_mnemonic ( _(
1251                                                "Limit _upload speed (KB/s):" ) );
1252    b = tr_torrentGetSpeedMode( tor, TR_UP ) == TR_SPEEDLIMIT_SINGLE;
1253    gtk_toggle_button_set_active ( GTK_TOGGLE_BUTTON( tb ), b );
1254    g_signal_connect ( tb, "toggled", G_CALLBACK(
1255                           ul_speed_toggled_cb ), gtor );
1256
1257    i = tr_torrentGetSpeedLimit( tor, TR_UP );
1258    w = gtk_spin_button_new_with_range( 1, INT_MAX, 5 );
1259    gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), i );
1260
1261    g_signal_connect ( w, "value-changed", G_CALLBACK(
1262                           ul_speed_spun_cb ), gtor );
1263    g_signal_connect ( tb, "toggled", G_CALLBACK(
1264                           sensitize_from_check_cb ), w );
1265    sensitize_from_check_cb ( GTK_TOGGLE_BUTTON( tb ), w );
1266    hig_workarea_add_row_w ( t, &row, tb, w, NULL );
1267
1268    hig_workarea_add_section_divider ( t, &row );
1269    hig_workarea_add_section_title ( t, &row, _( "Peer Connections" ) );
1270
1271    maxConnectedPeers = tr_torrentGetPeerLimit( tor );
1272    w = gtk_spin_button_new_with_range( 1, 3000, 5 );
1273    gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), maxConnectedPeers );
1274    hig_workarea_add_row( t, &row, _( "_Maximum peers:" ), w, w );
1275    g_signal_connect( w, "value-changed", G_CALLBACK(
1276                          max_peers_spun_cb ), gtor );
1277
1278    hig_workarea_finish ( t, &row );
1279    return t;
1280}
1281
1282static void
1283refresh_options( GtkWidget * top UNUSED )
1284{}
1285
1286/****
1287*****  TRACKER
1288****/
1289
1290#define TRACKER_PAGE "tracker-page"
1291
1292struct tracker_page
1293{
1294    TrTorrent *         gtor;
1295
1296    GtkTreeView *       view;
1297    GtkListStore *      store;
1298    GtkTreeSelection *  sel;
1299
1300    GtkWidget *         add_button;
1301    GtkWidget *         remove_button;
1302    GtkWidget *         save_button;
1303    GtkWidget *         revert_button;
1304
1305    GtkWidget *         last_scrape_time_lb;
1306    GtkWidget *         last_scrape_response_lb;
1307    GtkWidget *         next_scrape_countdown_lb;
1308
1309    GtkWidget *         last_announce_time_lb;
1310    GtkWidget *         last_announce_response_lb;
1311    GtkWidget *         next_announce_countdown_lb;
1312    GtkWidget *         manual_announce_countdown_lb;
1313};
1314
1315static GtkWidget*
1316tracker_page_new( TrTorrent * gtor )
1317{
1318    GtkWidget *           t;
1319    GtkWidget *           l;
1320    GtkWidget *           w;
1321    int                   row = 0;
1322    const char *          s;
1323    struct tracker_page * page = g_new0( struct tracker_page, 1 );
1324    const tr_info *       info = tr_torrent_info ( gtor );
1325
1326    page->gtor = gtor;
1327
1328    t = hig_workarea_create( );
1329    hig_workarea_add_section_title( t, &row, _( "Trackers" ) );
1330
1331    w = tracker_list_new( gtor );
1332    hig_workarea_add_wide_control( t, &row, w );
1333
1334    hig_workarea_add_section_divider( t, &row );
1335    hig_workarea_add_section_title( t, &row, _( "Scrape" ) );
1336
1337    s = _( "Last scrape at:" );
1338    l = gtk_label_new( NULL );
1339    page->last_scrape_time_lb = l;
1340    hig_workarea_add_row( t, &row, s, l, NULL );
1341
1342    s = _( "Tracker responded:" );
1343    l = gtk_label_new( NULL );
1344    page->last_scrape_response_lb = l;
1345    hig_workarea_add_row( t, &row, s, l, NULL );
1346
1347    s = _( "Next scrape in:" );
1348    l = gtk_label_new( NULL );
1349    page->next_scrape_countdown_lb = l;
1350    hig_workarea_add_row( t, &row, s, l, NULL );
1351
1352    hig_workarea_add_section_divider( t, &row );
1353    hig_workarea_add_section_title( t, &row, _( "Announce" ) );
1354
1355    l = gtk_label_new( info->trackers[0].announce );
1356    gtk_label_set_ellipsize( GTK_LABEL( l ), PANGO_ELLIPSIZE_END );
1357    hig_workarea_add_row ( t, &row, _( "Tracker:" ), l, NULL );
1358
1359    s = _( "Last announce at:" );
1360    l = gtk_label_new( NULL );
1361    page->last_announce_time_lb = l;
1362    hig_workarea_add_row( t, &row, s, l, NULL );
1363
1364    s = _( "Tracker responded:" );
1365    l = gtk_label_new( NULL );
1366    page->last_announce_response_lb = l;
1367    hig_workarea_add_row( t, &row, s, l, NULL );
1368
1369    s = _( "Next announce in:" );
1370    l = gtk_label_new( NULL );
1371    page->next_announce_countdown_lb = l;
1372    hig_workarea_add_row( t, &row, s, l, NULL );
1373
1374    /* how long until the tracker will honor user
1375    * pressing the "ask for more peers" button */
1376    s = _( "Manual announce allowed in:" );
1377    l = gtk_label_new( NULL );
1378    page->manual_announce_countdown_lb = l;
1379    hig_workarea_add_row( t, &row, s, l, NULL );
1380
1381    hig_workarea_finish( t, &row );
1382    g_object_set_data_full( G_OBJECT( t ), TRACKER_PAGE, page, g_free );
1383    return t;
1384}
1385
1386static void
1387refresh_countdown_lb( GtkWidget *  w,
1388                      time_t       t,
1389                      const char * countdown_done )
1390{
1391    const time_t now = time( NULL );
1392    GtkLabel *   l = GTK_LABEL( w );
1393
1394    if( t == 1 )
1395        gtk_label_set_text( l, _( "In progress" ) );
1396    else if( t < now )
1397        gtk_label_set_text( l, countdown_done );
1398    else
1399    {
1400        char      buf[1024];
1401        const int seconds = t - now;
1402        tr_strltime( buf, seconds, sizeof( buf ) );
1403        gtk_label_set_text( l, buf );
1404    }
1405}
1406
1407static void
1408refresh_tracker( GtkWidget * w )
1409{
1410    GtkWidget *           l;
1411    time_t                t;
1412    struct tracker_page * page = g_object_get_data( G_OBJECT(
1413                                                        w ), TRACKER_PAGE );
1414    const tr_stat *       torStat = tr_torrent_stat( page->gtor );
1415
1416    l = page->last_scrape_time_lb;
1417    t = torStat->lastScrapeTime;
1418    refresh_time_lb( l, t );
1419
1420    l = page->last_scrape_response_lb;
1421    gtk_label_set_text( GTK_LABEL( l ), torStat->scrapeResponse );
1422
1423    l = page->next_scrape_countdown_lb;
1424    t = torStat->nextScrapeTime;
1425    refresh_countdown_lb( l, t, _( "Never" ) );
1426
1427    l = page->last_announce_time_lb;
1428    t = torStat->lastAnnounceTime;
1429    refresh_time_lb( l, t );
1430
1431    l = page->last_announce_response_lb;
1432    gtk_label_set_text( GTK_LABEL( l ), torStat->announceResponse );
1433
1434    l = page->next_announce_countdown_lb;
1435    t = torStat->nextAnnounceTime;
1436    refresh_countdown_lb( l, t, _( "Never" ) );
1437
1438    l = page->manual_announce_countdown_lb;
1439    t = torStat->manualAnnounceTime;
1440    refresh_countdown_lb( l, t, _( "Now" ) );
1441}
1442
1443/****
1444*****  DIALOG
1445****/
1446
1447static void
1448torrent_destroyed( gpointer               dialog,
1449                   GObject * dead_torrent UNUSED )
1450{
1451    gtk_widget_destroy ( GTK_WIDGET( dialog ) );
1452}
1453
1454static void
1455remove_tag( gpointer tag )
1456{
1457    g_source_remove ( GPOINTER_TO_UINT( tag ) ); /* stop the periodic refresh */
1458}
1459
1460static void
1461response_cb( GtkDialog *  dialog,
1462             int response UNUSED,
1463             gpointer     gtor )
1464{
1465    g_object_weak_unref ( G_OBJECT( gtor ), torrent_destroyed, dialog );
1466    gtk_widget_destroy ( GTK_WIDGET( dialog ) );
1467}
1468
1469static gboolean
1470periodic_refresh( gpointer data )
1471{
1472    refresh_tracker   ( g_object_get_data ( G_OBJECT( data ), "tracker-top" ) );
1473    refresh_peers     ( g_object_get_data ( G_OBJECT( data ), "peers-top" ) );
1474    refresh_activity  ( g_object_get_data ( G_OBJECT( data ),
1475                                            "activity-top" ) );
1476    refresh_options   ( g_object_get_data ( G_OBJECT( data ), "options-top" ) );
1477    return TRUE;
1478}
1479
1480GtkWidget*
1481torrent_inspector_new( GtkWindow * parent,
1482                       TrTorrent * gtor )
1483{
1484    guint           tag;
1485    GtkWidget *     d, *n, *w;
1486    tr_torrent *    tor = tr_torrent_handle ( gtor );
1487    char            title[512];
1488    const tr_info * info = tr_torrent_info ( gtor );
1489
1490    /* create the dialog */
1491    g_snprintf( title, sizeof( title ), _( "%s Properties" ), info->name );
1492    d = gtk_dialog_new_with_buttons ( title, parent, 0,
1493                                      GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
1494                                      NULL );
1495    gtk_window_set_role ( GTK_WINDOW( d ), "tr-info" );
1496    g_signal_connect ( d, "response", G_CALLBACK ( response_cb ), gtor );
1497    gtk_dialog_set_has_separator( GTK_DIALOG( d ), FALSE );
1498    gtk_container_set_border_width( GTK_CONTAINER( d ), GUI_PAD );
1499    g_object_weak_ref ( G_OBJECT( gtor ), torrent_destroyed, d );
1500
1501
1502    /* add the notebook */
1503    n = gtk_notebook_new ( );
1504    gtk_container_set_border_width ( GTK_CONTAINER ( n ), GUI_PAD );
1505
1506    w = activity_page_new ( gtor );
1507    g_object_set_data ( G_OBJECT( d ), "activity-top", w );
1508    gtk_notebook_append_page ( GTK_NOTEBOOK( n ), w,
1509                              gtk_label_new ( _( "Activity" ) ) );
1510
1511    w = peer_page_new ( gtor );
1512    g_object_set_data ( G_OBJECT( d ), "peers-top", w );
1513    gtk_notebook_append_page ( GTK_NOTEBOOK( n ),  w,
1514                              gtk_label_new ( _( "Peers" ) ) );
1515
1516    w = tracker_page_new( gtor );
1517    g_object_set_data( G_OBJECT( d ), "tracker-top", w );
1518    gtk_notebook_append_page( GTK_NOTEBOOK( n ), w,
1519                             gtk_label_new( _( "Tracker" ) ) );
1520
1521    gtk_notebook_append_page ( GTK_NOTEBOOK( n ),
1522                              info_page_new ( tor ),
1523                              gtk_label_new ( _( "Information" ) ) );
1524
1525    w = file_list_new( gtor );
1526    gtk_container_set_border_width( GTK_CONTAINER( w ), GUI_PAD_BIG );
1527    g_object_set_data ( G_OBJECT( d ), "files-top", w );
1528    gtk_notebook_append_page ( GTK_NOTEBOOK( n ), w,
1529                              gtk_label_new ( _( "Files" ) ) );
1530
1531    w = options_page_new ( gtor );
1532    g_object_set_data ( G_OBJECT( d ), "options-top", w );
1533    gtk_notebook_append_page ( GTK_NOTEBOOK( n ), w,
1534                              gtk_label_new ( _( "Options" ) ) );
1535
1536    gtk_box_pack_start( GTK_BOX( GTK_DIALOG( d )->vbox ), n, TRUE, TRUE, 0 );
1537
1538    tag = gtr_timeout_add_seconds( UPDATE_INTERVAL_SECONDS, periodic_refresh, d );
1539    g_object_set_data_full ( G_OBJECT( d ), "tag",
1540                             GUINT_TO_POINTER( tag ), remove_tag );
1541
1542    /* return the results */
1543    periodic_refresh ( d );
1544    gtk_widget_show_all ( GTK_DIALOG( d )->vbox );
1545    return d;
1546}
1547
Note: See TracBrowser for help on using the repository browser.