source: trunk/gtk/tr-prefs.c @ 10638

Last change on this file since 10638 was 10638, checked in by charles, 12 years ago

(trunk) #1796 "run script after torrent completion" -- implemented for libT, RPC, and the GTK+ and Qt clients

  • Property svn:keywords set to Date Rev Author Id
File size: 49.8 KB
Line 
1/*
2 * This file Copyright (C) 2007-2010 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: tr-prefs.c 10638 2010-05-08 22:42:28Z charles $
11 */
12
13#include <ctype.h> /* isspace */
14#include <limits.h> /* USHRT_MAX */
15#include <stdlib.h> /* free() */
16#include <unistd.h>
17#include <glib/gi18n.h>
18#include <gtk/gtk.h>
19#include <libtransmission/transmission.h>
20#include <libtransmission/utils.h>
21#include <libtransmission/version.h>
22#include <libtransmission/web.h>
23#include "conf.h"
24#include "hig.h"
25#include "tr-core.h"
26#include "tr-prefs.h"
27#include "util.h"
28
29/**
30***
31**/
32
33#define PREF_KEY "pref-key"
34
35static void
36response_cb( GtkDialog *     dialog,
37             int             response,
38             gpointer unused UNUSED )
39{
40    if( response == GTK_RESPONSE_HELP )
41    {
42        char * base = gtr_get_help_url( );
43        char * url = g_strdup_printf( "%s/html/preferences.html", base );
44        gtr_open_file( url );
45        g_free( url );
46        g_free( base );
47    }
48
49    if( response == GTK_RESPONSE_CLOSE )
50        gtk_widget_destroy( GTK_WIDGET( dialog ) );
51}
52
53static void
54toggled_cb( GtkToggleButton * w,
55            gpointer          core )
56{
57    const char *   key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
58    const gboolean flag = gtk_toggle_button_get_active( w );
59
60    tr_core_set_pref_bool( TR_CORE( core ), key, flag );
61}
62
63static GtkWidget*
64new_check_button( const char * mnemonic,
65                  const char * key,
66                  gpointer     core )
67{
68    GtkWidget * w = gtk_check_button_new_with_mnemonic( mnemonic );
69
70    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
71                                key ), g_free );
72    gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( w ),
73                                 pref_flag_get( key ) );
74    g_signal_connect( w, "toggled", G_CALLBACK( toggled_cb ), core );
75    return w;
76}
77
78#define IDLE_DATA "idle-data"
79
80struct spin_idle_data
81{
82    gpointer    core;
83    GTimer *    last_change;
84    gboolean    isDouble;
85};
86
87static void
88spin_idle_data_free( gpointer gdata )
89{
90    struct spin_idle_data * data = gdata;
91
92    g_timer_destroy( data->last_change );
93    g_free( data );
94}
95
96static gboolean
97spun_cb_idle( gpointer spin )
98{
99    gboolean                keep_waiting = TRUE;
100    GObject *               o = G_OBJECT( spin );
101    struct spin_idle_data * data = g_object_get_data( o, IDLE_DATA );
102
103    /* has the user stopped making changes? */
104    if( g_timer_elapsed( data->last_change, NULL ) > 0.33f )
105    {
106        /* update the core */
107        const char * key = g_object_get_data( o, PREF_KEY );
108        if (data->isDouble)
109        {
110            const double value = gtk_spin_button_get_value( GTK_SPIN_BUTTON( spin ) );
111            tr_core_set_pref_double( TR_CORE( data->core ), key, value );
112        }
113        else
114        {
115            const int    value = gtk_spin_button_get_value_as_int(
116                                 GTK_SPIN_BUTTON( spin ) );
117            tr_core_set_pref_int( TR_CORE( data->core ), key, value );
118        }
119
120        /* cleanup */
121        g_object_set_data( o, IDLE_DATA, NULL );
122        keep_waiting = FALSE;
123        g_object_unref( G_OBJECT( o ) );
124    }
125
126    return keep_waiting;
127}
128
129static void
130spun_cb( GtkSpinButton * w,
131         gpointer        core,
132         gboolean        isDouble )
133{
134    /* user may be spinning through many values, so let's hold off
135       for a moment to keep from flooding the core with changes */
136    GObject *               o = G_OBJECT( w );
137    struct spin_idle_data * data = g_object_get_data( o, IDLE_DATA );
138
139    if( data == NULL )
140    {
141        data = g_new( struct spin_idle_data, 1 );
142        data->core = core;
143        data->last_change = g_timer_new( );
144        data->isDouble = isDouble;
145        g_object_set_data_full( o, IDLE_DATA, data, spin_idle_data_free );
146        g_object_ref( G_OBJECT( o ) );
147        gtr_timeout_add_seconds( 1, spun_cb_idle, w );
148    }
149    g_timer_start( data->last_change );
150}
151
152static void
153spun_cb_int( GtkSpinButton * w,
154             gpointer        core )
155{
156    spun_cb( w, core, FALSE );
157}
158
159static void
160spun_cb_double( GtkSpinButton * w,
161                gpointer        core )
162{
163    spun_cb( w, core, TRUE );
164}
165
166static GtkWidget*
167new_spin_button( const char * key,
168                 gpointer     core,
169                 int          low,
170                 int          high,
171                 int          step )
172{
173    GtkWidget * w = gtk_spin_button_new_with_range( low, high, step );
174
175    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
176                                key ), g_free );
177    gtk_spin_button_set_digits( GTK_SPIN_BUTTON( w ), 0 );
178    gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), pref_int_get( key ) );
179    g_signal_connect( w, "value-changed", G_CALLBACK( spun_cb_int ), core );
180    return w;
181}
182
183static GtkWidget*
184new_spin_button_double( const char * key,
185                       gpointer      core,
186                       double        low,
187                       double        high,
188                       double        step )
189{
190    GtkWidget * w = gtk_spin_button_new_with_range( low, high, step );
191
192    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
193                                key ), g_free );
194    gtk_spin_button_set_digits( GTK_SPIN_BUTTON( w ), 2 );
195    gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), pref_double_get( key ) );
196    g_signal_connect( w, "value-changed", G_CALLBACK( spun_cb_double ), core );
197    return w;
198}
199
200static void
201entry_changed_cb( GtkEntry * w,
202                  gpointer   core )
203{
204    const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
205    const char * value = gtk_entry_get_text( w );
206
207    tr_core_set_pref( TR_CORE( core ), key, value );
208}
209
210static GtkWidget*
211new_entry( const char * key,
212           gpointer     core )
213{
214    GtkWidget *  w = gtk_entry_new( );
215    const char * value = pref_string_get( key );
216
217    if( value )
218        gtk_entry_set_text( GTK_ENTRY( w ), value );
219    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
220                                key ), g_free );
221    g_signal_connect( w, "changed", G_CALLBACK( entry_changed_cb ), core );
222    return w;
223}
224
225static void
226chosen_cb( GtkFileChooser * w, gpointer core )
227{
228    const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
229    char * value = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER( w ) );
230    tr_core_set_pref( TR_CORE( core ), key, value );
231    g_free( value );
232}
233
234static GtkWidget*
235new_path_chooser_button( const char * key, gpointer core )
236{
237    GtkWidget *  w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER );
238    const char * path = pref_string_get( key );
239    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
240    g_signal_connect( w, "selection-changed", G_CALLBACK( chosen_cb ), core );
241    if( path != NULL )
242        gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER( w ), path );
243    return w;
244}
245
246static GtkWidget*
247new_file_chooser_button( const char * key, gpointer core )
248{
249    GtkWidget *  w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_OPEN );
250    const char * path = pref_string_get( key );
251    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
252    if( path != NULL )
253        gtk_file_chooser_set_filename( GTK_FILE_CHOOSER( w ), path );
254    g_signal_connect( w, "selection-changed", G_CALLBACK( chosen_cb ), core );
255    return w;
256}
257
258static void
259target_cb( GtkWidget * tb,
260           gpointer    target )
261{
262    const gboolean b = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( tb ) );
263
264    gtk_widget_set_sensitive( GTK_WIDGET( target ), b );
265}
266
267/****
268*****  Torrent Tab
269****/
270
271static GtkWidget*
272torrentPage( GObject * core )
273{
274    int          row = 0;
275    const char * s;
276    GtkWidget *  t;
277    GtkWidget *  w;
278    GtkWidget *  w2;
279    GtkWidget *  l;
280
281    t = hig_workarea_create( );
282    hig_workarea_add_section_title( t, &row, _( "Adding Torrents" ) );
283
284#ifdef HAVE_GIO
285    s = _( "Automatically _add torrents from:" );
286    l = new_check_button( s, PREF_KEY_DIR_WATCH_ENABLED, core );
287    w = new_path_chooser_button( PREF_KEY_DIR_WATCH, core );
288    gtk_widget_set_sensitive( GTK_WIDGET( w ),
289                             pref_flag_get( PREF_KEY_DIR_WATCH_ENABLED ) );
290    g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
291    hig_workarea_add_row_w( t, &row, l, w, NULL );
292#endif
293
294    s = _( "Show _options dialog" );
295    w = new_check_button( s, PREF_KEY_OPTIONS_PROMPT, core );
296    hig_workarea_add_wide_control( t, &row, w );
297
298    s = _( "_Start when added" );
299    w = new_check_button( s, TR_PREFS_KEY_START, core );
300    hig_workarea_add_wide_control( t, &row, w );
301
302    s = _( "Mo_ve .torrent file to the trash" );
303    w = new_check_button( s, TR_PREFS_KEY_TRASH_ORIGINAL, core );
304    hig_workarea_add_wide_control( t, &row, w );
305
306    s = _( "Append \"._part\" to incomplete files' names" );
307    w = new_check_button( s, TR_PREFS_KEY_RENAME_PARTIAL_FILES, core );
308    hig_workarea_add_wide_control( t, &row, w );
309
310    w = new_path_chooser_button( TR_PREFS_KEY_DOWNLOAD_DIR, core );
311    hig_workarea_add_row( t, &row, _( "Save to _Location:" ), w, NULL );
312
313    s = _( "Keep _incomplete torrents in:" );
314    l = new_check_button( s, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED, core );
315    w = new_path_chooser_button( TR_PREFS_KEY_INCOMPLETE_DIR, core );
316    gtk_widget_set_sensitive( GTK_WIDGET( w ), pref_flag_get( TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED ) );
317    g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
318    hig_workarea_add_row_w( t, &row, l, w, NULL );
319
320    s = _( "Call scrip_t when torrent is completed:" );
321    l = new_check_button( s, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED, core );
322    w = new_file_chooser_button( TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME, core );
323    gtk_widget_set_sensitive( GTK_WIDGET( w ), pref_flag_get( TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED ) );
324    g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
325    hig_workarea_add_row_w( t, &row, l, w, NULL );
326
327    hig_workarea_add_section_divider( t, &row );
328    hig_workarea_add_section_title( t, &row, _( "Limits" ) );
329
330    s = _( "_Seed torrent until its ratio reaches:" );
331    w = new_check_button( s, TR_PREFS_KEY_RATIO_ENABLED, core );
332    w2 = new_spin_button_double( TR_PREFS_KEY_RATIO, core, 0, INT_MAX, .05 );
333    gtk_widget_set_sensitive( GTK_WIDGET( w2 ), pref_flag_get( TR_PREFS_KEY_RATIO_ENABLED ) );
334    g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
335    hig_workarea_add_row_w( t, &row, w, w2, NULL );
336
337    hig_workarea_finish( t, &row );
338    return t;
339}
340
341/****
342*****  Desktop Tab
343****/
344
345static GtkWidget*
346desktopPage( GObject * core )
347{
348    int          row = 0;
349    const char * s;
350    GtkWidget *  t;
351    GtkWidget *  w;
352
353    t = hig_workarea_create( );
354    hig_workarea_add_section_title( t, &row, _( "Desktop" ) );
355
356    s = _( "Inhibit _hibernation when torrents are active" );
357    w = new_check_button( s, PREF_KEY_INHIBIT_HIBERNATION, core );
358    hig_workarea_add_wide_control( t, &row, w );
359
360    s = _( "Show Transmission icon in the _notification area" );
361    w = new_check_button( s, PREF_KEY_SHOW_TRAY_ICON, core );
362    hig_workarea_add_wide_control( t, &row, w );
363
364    s = _( "Show _popup notifications" );
365    w = new_check_button( s, PREF_KEY_SHOW_DESKTOP_NOTIFICATION, core );
366    hig_workarea_add_wide_control( t, &row, w );
367
368#ifdef HAVE_LIBCANBERRA
369    s = _( "Play _sound when downloads are complete" );
370    w = new_check_button( s, PREF_KEY_PLAY_DOWNLOAD_COMPLETE_SOUND, core );
371    hig_workarea_add_wide_control( t, &row, w );
372#endif
373
374    hig_workarea_finish( t, &row );
375    return t;
376}
377
378/****
379*****  Peer Tab
380****/
381
382struct blocklist_data
383{
384    gulong      updateBlocklistTag;
385    GtkWidget * updateBlocklistButton;
386    GtkWidget * updateBlocklistDialog;
387    GtkWidget * check;
388    TrCore    * core;
389};
390
391static void
392updateBlocklistText( GtkWidget * w, TrCore * core )
393{
394    const int n = tr_blocklistGetRuleCount( tr_core_session( core ) );
395    char      buf[512];
396    g_snprintf( buf, sizeof( buf ),
397                ngettext( "Enable _blocklist (contains %'d rule)",
398                          "Enable _blocklist (contains %'d rules)", n ), n );
399    gtk_button_set_label( GTK_BUTTON( w ), buf );
400}
401
402/* prefs dialog is being destroyed, so stop listening to blocklist updates */
403static void
404privacyPageDestroyed( gpointer gdata, GObject * dead UNUSED )
405{
406    struct blocklist_data * data = gdata;
407    if( data->updateBlocklistTag > 0 )
408        g_signal_handler_disconnect( data->core, data->updateBlocklistTag );
409    g_free( data );
410}
411
412/* user hit "close" in the blocklist-update dialog */
413static void
414onBlocklistUpdateResponse( GtkDialog * dialog, gint response UNUSED, gpointer gdata )
415{
416    struct blocklist_data * data = gdata;
417    gtk_widget_destroy( GTK_WIDGET( dialog ) );
418    gtk_widget_set_sensitive( data->updateBlocklistButton, TRUE );
419    data->updateBlocklistDialog = NULL;
420    g_signal_handler_disconnect( data->core, data->updateBlocklistTag );
421}
422
423/* core says the blocklist was updated */
424static void
425onBlocklistUpdated( TrCore * core, int n, gpointer gdata )
426{
427    const char * s = ngettext( "Blocklist now has %'d rule.", "Blocklist now has %'d rules.", n );
428    struct blocklist_data * data = gdata;
429    GtkMessageDialog * d = GTK_MESSAGE_DIALOG( data->updateBlocklistDialog );
430    gtk_widget_set_sensitive( data->updateBlocklistButton, TRUE );
431    gtk_message_dialog_set_markup( d, _( "<b>Update succeeded!</b>" ) );
432    gtk_message_dialog_format_secondary_text( d, s, n );
433    updateBlocklistText( data->check, core );
434}
435
436/* user pushed a button to update the blocklist */
437static void
438onBlocklistUpdate( GtkButton * w, gpointer gdata )
439{
440    GtkWidget * d;
441    struct blocklist_data * data = gdata;
442    d = gtk_message_dialog_new( GTK_WINDOW( gtk_widget_get_toplevel( GTK_WIDGET( w ) ) ),
443                                GTK_DIALOG_DESTROY_WITH_PARENT,
444                                GTK_MESSAGE_INFO,
445                                GTK_BUTTONS_CLOSE,
446                                "%s", _( "Update Blocklist" ) );
447    gtk_widget_set_sensitive( data->updateBlocklistButton, FALSE );
448    gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( d ), "%s", _( "Getting new blocklist..." ) );
449    data->updateBlocklistDialog = d;
450    g_signal_connect( d, "response", G_CALLBACK(onBlocklistUpdateResponse), data );
451    gtk_widget_show( d );
452    tr_core_blocklist_update( data->core );
453    data->updateBlocklistTag = g_signal_connect( data->core, "blocklist-updated", G_CALLBACK( onBlocklistUpdated ), data );
454}
455
456static void
457onIntComboChanged( GtkComboBox * w, gpointer core )
458{
459    GtkTreeIter iter;
460
461    if( gtk_combo_box_get_active_iter( w, &iter ) )
462    {
463        int val = 0;
464        const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
465        gtk_tree_model_get( gtk_combo_box_get_model( w ), &iter, 0, &val, -1 );
466        tr_core_set_pref_int( TR_CORE( core ), key, val );
467    }
468}
469
470static GtkWidget*
471new_encryption_combo( GObject * core, const char * key )
472{
473    int i;
474    int selIndex;
475    GtkWidget * w;
476    GtkCellRenderer * r;
477    GtkListStore * store;
478    const int currentValue = pref_int_get( key );
479    const struct {
480        int value;
481        const char * text;
482    } items[] = {
483        { TR_CLEAR_PREFERRED,      N_( "Allow encryption" )  },
484        { TR_ENCRYPTION_PREFERRED, N_( "Prefer encryption" ) },
485        { TR_ENCRYPTION_REQUIRED,  N_( "Require encryption" )  }
486    };
487
488    /* build a store for encryption */
489    selIndex = -1;
490    store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING );
491    for( i=0; i<(int)G_N_ELEMENTS(items); ++i ) {
492        GtkTreeIter iter;
493        gtk_list_store_append( store, &iter );
494        gtk_list_store_set( store, &iter, 0, items[i].value, 1, _( items[i].text ), -1 );
495        if( items[i].value == currentValue )
496            selIndex = i;
497    }
498
499    /* build the widget */
500    w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) );
501    r = gtk_cell_renderer_text_new( );
502    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
503    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( w ), r, "text", 1, NULL );
504    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup( key ), g_free );
505    if( selIndex >= 0 )
506        gtk_combo_box_set_active( GTK_COMBO_BOX( w ), selIndex );
507    g_signal_connect( w, "changed", G_CALLBACK( onIntComboChanged ), core );
508
509    /* cleanup */
510    g_object_unref( G_OBJECT( store ) );
511    return w;
512}
513
514static GtkWidget*
515privacyPage( GObject * core )
516{
517    int                     row = 0;
518    const char *            s;
519    GtkWidget *             t;
520    GtkWidget *             w;
521    GtkWidget *             b;
522    GtkWidget *             h;
523    struct blocklist_data * data;
524
525    data = g_new0( struct blocklist_data, 1 );
526    data->core = TR_CORE( core );
527
528    t = hig_workarea_create( );
529    hig_workarea_add_section_title( t, &row, _( "Blocklist" ) );
530
531    w = new_check_button( "", TR_PREFS_KEY_BLOCKLIST_ENABLED, core );
532    updateBlocklistText( w, TR_CORE( core ) );
533    h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
534    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
535    b = data->updateBlocklistButton = gtr_button_new_from_stock( GTK_STOCK_REFRESH, _( "_Update" ) );
536    data->check = w;
537    g_object_set_data( G_OBJECT( b ), "session",
538                      tr_core_session( TR_CORE( core ) ) );
539    g_signal_connect( b, "clicked", G_CALLBACK( onBlocklistUpdate ), data );
540    gtk_box_pack_start( GTK_BOX( h ), b, FALSE, FALSE, 0 );
541    g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), b );
542    target_cb( w, b );
543    hig_workarea_add_wide_control( t, &row, h );
544
545    s = _( "Enable _automatic updates" );
546    w = new_check_button( s, PREF_KEY_BLOCKLIST_UPDATES_ENABLED, core );
547    hig_workarea_add_wide_control( t, &row, w );
548    g_signal_connect( data->check, "toggled", G_CALLBACK( target_cb ), w );
549    target_cb( data->check, w );
550
551    hig_workarea_add_section_divider( t, &row );
552    hig_workarea_add_section_title ( t, &row, _( "Privacy" ) );
553
554    s = _( "_Encryption mode:" );
555    w = new_encryption_combo( core, "encryption" );
556    hig_workarea_add_row( t, &row, s, w, NULL );
557
558    s = _( "Use PE_X to find more peers" );
559    w = new_check_button( s, TR_PREFS_KEY_PEX_ENABLED, core );
560    s = _( "PEX is a tool for exchanging peer lists with the peers you're connected to." );
561    gtr_widget_set_tooltip_text( w, s );
562    hig_workarea_add_wide_control( t, &row, w );
563
564    s = _( "Use _DHT to find more peers" );
565    w = new_check_button( s, TR_PREFS_KEY_DHT_ENABLED, core );
566    s = _( "DHT is a tool for finding peers without a tracker." );
567    gtr_widget_set_tooltip_text( w, s );
568    hig_workarea_add_wide_control( t, &row, w );
569
570    s = _( "Use Local Peer Discovery to find more peers" );
571    w = new_check_button( s, TR_PREFS_KEY_LPD_ENABLED, core );
572    s = _( "LPD is a tool for finding peers on your local network." );
573    gtr_widget_set_tooltip_text( w, s );
574    hig_workarea_add_wide_control( t, &row, w );
575
576    hig_workarea_finish( t, &row );
577    g_object_weak_ref( G_OBJECT( t ), privacyPageDestroyed, data );
578    return t;
579}
580
581/****
582*****  Web Tab
583****/
584
585enum
586{
587    COL_ADDRESS,
588    N_COLS
589};
590
591static GtkTreeModel*
592whitelist_tree_model_new( const char * whitelist )
593{
594    int            i;
595    char **        rules;
596    GtkListStore * store = gtk_list_store_new( N_COLS,
597                                               G_TYPE_STRING,
598                                               G_TYPE_STRING );
599
600    rules = g_strsplit( whitelist, ",", 0 );
601
602    for( i = 0; rules && rules[i]; ++i )
603    {
604        GtkTreeIter iter;
605        const char * s = rules[i];
606        while( isspace( *s ) ) ++s;
607        gtk_list_store_append( store, &iter );
608        gtk_list_store_set( store, &iter, COL_ADDRESS, s, -1 );
609    }
610
611    g_strfreev( rules );
612    return GTK_TREE_MODEL( store );
613}
614
615struct remote_page
616{
617    TrCore *           core;
618    GtkTreeView *      view;
619    GtkListStore *     store;
620    GtkWidget *        remove_button;
621    GSList *           widgets;
622    GSList *           auth_widgets;
623    GSList *           whitelist_widgets;
624    GtkToggleButton *  rpc_tb;
625    GtkToggleButton *  auth_tb;
626    GtkToggleButton *  whitelist_tb;
627};
628
629static void
630refreshWhitelist( struct remote_page * page )
631{
632    GtkTreeIter    iter;
633    GtkTreeModel * model = GTK_TREE_MODEL( page->store );
634    GString *      gstr = g_string_new( NULL );
635
636    if( gtk_tree_model_get_iter_first( model, &iter ) ) do
637        {
638            char * address;
639            gtk_tree_model_get( model, &iter,
640                                COL_ADDRESS, &address,
641                                -1 );
642            g_string_append( gstr, address );
643            g_string_append( gstr, "," );
644            g_free( address );
645        }
646        while( gtk_tree_model_iter_next( model, &iter ) );
647
648    g_string_truncate( gstr, gstr->len - 1 ); /* remove the trailing comma */
649
650    tr_core_set_pref( page->core, TR_PREFS_KEY_RPC_WHITELIST, gstr->str );
651
652    g_string_free( gstr, TRUE );
653}
654
655static void
656onAddressEdited( GtkCellRendererText  * r UNUSED,
657                 gchar *                  path_string,
658                 gchar *                  address,
659                 gpointer                 gpage )
660{
661    GtkTreeIter          iter;
662    struct remote_page * page = gpage;
663    GtkTreeModel *       model = GTK_TREE_MODEL( page->store );
664    GtkTreePath *        path = gtk_tree_path_new_from_string( path_string );
665
666    if( gtk_tree_model_get_iter( model, &iter, path ) )
667        gtk_list_store_set( page->store, &iter, COL_ADDRESS, address, -1 );
668
669    gtk_tree_path_free( path );
670    refreshWhitelist( page );
671}
672
673static void
674onAddWhitelistClicked( GtkButton * b UNUSED,
675                 gpointer      gpage )
676{
677    GtkTreeIter          iter;
678    GtkTreePath *        path;
679    struct remote_page * page = gpage;
680
681    gtk_list_store_append( page->store, &iter );
682    gtk_list_store_set( page->store, &iter,
683                        COL_ADDRESS,  "0.0.0.0",
684                        -1 );
685
686    path = gtk_tree_model_get_path( GTK_TREE_MODEL( page->store ), &iter );
687    gtk_tree_view_set_cursor(
688        page->view, path,
689        gtk_tree_view_get_column( page->view, COL_ADDRESS ),
690        TRUE );
691    gtk_tree_path_free( path );
692}
693
694static void
695onRemoveWhitelistClicked( GtkButton * b UNUSED,
696                    gpointer      gpage )
697{
698    struct remote_page * page = gpage;
699    GtkTreeSelection *   sel = gtk_tree_view_get_selection( page->view );
700    GtkTreeIter          iter;
701
702    if( gtk_tree_selection_get_selected( sel, NULL, &iter ) )
703    {
704        gtk_list_store_remove( page->store, &iter );
705        refreshWhitelist( page );
706    }
707}
708
709static void
710refreshRPCSensitivity( struct remote_page * page )
711{
712    GSList *           l;
713    const int          rpc_active = gtk_toggle_button_get_active(
714        page->rpc_tb );
715    const int          auth_active = gtk_toggle_button_get_active(
716        page->auth_tb );
717    const int          whitelist_active = gtk_toggle_button_get_active(
718        page->whitelist_tb );
719    GtkTreeSelection * sel = gtk_tree_view_get_selection( page->view );
720    const int          have_addr =
721        gtk_tree_selection_get_selected( sel, NULL,
722                                         NULL );
723    const int          n_rules = gtk_tree_model_iter_n_children(
724        GTK_TREE_MODEL( page->store ), NULL );
725
726    for( l = page->widgets; l != NULL; l = l->next )
727        gtk_widget_set_sensitive( GTK_WIDGET( l->data ), rpc_active );
728
729    for( l = page->auth_widgets; l != NULL; l = l->next )
730        gtk_widget_set_sensitive( GTK_WIDGET(
731                                      l->data ), rpc_active && auth_active );
732
733    for( l = page->whitelist_widgets; l != NULL; l = l->next )
734        gtk_widget_set_sensitive( GTK_WIDGET( l->data ),
735                                  rpc_active && whitelist_active );
736
737    gtk_widget_set_sensitive( page->remove_button,
738                              rpc_active && have_addr && n_rules > 1 );
739}
740
741static void
742onRPCToggled( GtkToggleButton * tb UNUSED,
743              gpointer             page )
744{
745    refreshRPCSensitivity( page );
746}
747
748static void
749onWhitelistSelectionChanged( GtkTreeSelection * sel UNUSED,
750                       gpointer               page )
751{
752    refreshRPCSensitivity( page );
753}
754
755static void
756onLaunchClutchCB( GtkButton * w UNUSED,
757                  gpointer data UNUSED )
758{
759    int    port = pref_int_get( TR_PREFS_KEY_RPC_PORT );
760    char * url = g_strdup_printf( "http://localhost:%d/transmission/web",
761                                  port );
762
763    gtr_open_file( url );
764    g_free( url );
765}
766
767static void
768remotePageFree( gpointer gpage )
769{
770    struct remote_page * page = gpage;
771
772    g_slist_free( page->widgets );
773    g_slist_free( page->auth_widgets );
774    g_slist_free( page->whitelist_widgets );
775    g_free( page );
776}
777
778static GtkWidget*
779webPage( GObject * core )
780{
781    const char *         s;
782    int                  row = 0;
783    GtkWidget *          t;
784    GtkWidget *          w;
785    GtkWidget *          h;
786    struct remote_page * page = g_new0( struct remote_page, 1 );
787
788    page->core = TR_CORE( core );
789
790    t = hig_workarea_create( );
791    g_object_set_data_full( G_OBJECT( t ), "page", page, remotePageFree );
792
793    hig_workarea_add_section_title( t, &row, _( "Web Client" ) );
794
795    /* "enabled" checkbutton */
796    s = _( "_Enable web client" );
797    w = new_check_button( s, TR_PREFS_KEY_RPC_ENABLED, core );
798    page->rpc_tb = GTK_TOGGLE_BUTTON( w );
799    g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
800    h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
801    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
802    w = gtr_button_new_from_stock( GTK_STOCK_OPEN, _( "_Open web client" ) );
803    page->widgets = g_slist_append( page->widgets, w );
804    g_signal_connect( w, "clicked", G_CALLBACK( onLaunchClutchCB ), NULL );
805    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
806    hig_workarea_add_wide_control( t, &row, h );
807
808    /* port */
809    w = new_spin_button( TR_PREFS_KEY_RPC_PORT, core, 0, USHRT_MAX, 1 );
810    page->widgets = g_slist_append( page->widgets, w );
811    w = hig_workarea_add_row( t, &row, _( "Listening _port:" ), w, NULL );
812    page->widgets = g_slist_append( page->widgets, w );
813
814    /* require authentication */
815    s = _( "Use _authentication" );
816    w = new_check_button( s, TR_PREFS_KEY_RPC_AUTH_REQUIRED, core );
817    hig_workarea_add_wide_control( t, &row, w );
818    page->auth_tb = GTK_TOGGLE_BUTTON( w );
819    page->widgets = g_slist_append( page->widgets, w );
820    g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
821
822    /* username */
823    s = _( "_Username:" );
824    w = new_entry( TR_PREFS_KEY_RPC_USERNAME, core );
825    page->auth_widgets = g_slist_append( page->auth_widgets, w );
826    w = hig_workarea_add_row( t, &row, s, w, NULL );
827    page->auth_widgets = g_slist_append( page->auth_widgets, w );
828
829    /* password */
830    s = _( "Pass_word:" );
831    w = new_entry( TR_PREFS_KEY_RPC_PASSWORD, core );
832    gtk_entry_set_visibility( GTK_ENTRY( w ), FALSE );
833    page->auth_widgets = g_slist_append( page->auth_widgets, w );
834    w = hig_workarea_add_row( t, &row, s, w, NULL );
835    page->auth_widgets = g_slist_append( page->auth_widgets, w );
836
837    /* require authentication */
838    s = _( "Only allow these IP a_ddresses to connect:" );
839    w = new_check_button( s, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, core );
840    hig_workarea_add_wide_control( t, &row, w );
841    page->whitelist_tb = GTK_TOGGLE_BUTTON( w );
842    page->widgets = g_slist_append( page->widgets, w );
843    g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
844
845    /* access control list */
846    {
847        const char *        val = pref_string_get( TR_PREFS_KEY_RPC_WHITELIST );
848        GtkTreeModel *      m = whitelist_tree_model_new( val );
849        GtkTreeViewColumn * c;
850        GtkCellRenderer *   r;
851        GtkTreeSelection *  sel;
852        GtkTreeView *       v;
853        GtkWidget *         w;
854        GtkWidget *         h;
855
856        page->store = GTK_LIST_STORE( m );
857        w = gtk_tree_view_new_with_model( m );
858        g_signal_connect( w, "button-release-event",
859                          G_CALLBACK( on_tree_view_button_released ), NULL );
860
861        page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
862        v = page->view = GTK_TREE_VIEW( w );
863        gtr_widget_set_tooltip_text( w, _( "IP addresses may use wildcards, such as 192.168.*.*" ) );
864        sel = gtk_tree_view_get_selection( v );
865        g_signal_connect( sel, "changed",
866                          G_CALLBACK( onWhitelistSelectionChanged ), page );
867        g_object_unref( G_OBJECT( m ) );
868        gtk_tree_view_set_headers_visible( v, TRUE );
869        w = gtk_frame_new( NULL );
870        gtk_frame_set_shadow_type( GTK_FRAME( w ), GTK_SHADOW_IN );
871        gtk_container_add( GTK_CONTAINER( w ), GTK_WIDGET( v ) );
872
873        /* ip address column */
874        r = gtk_cell_renderer_text_new( );
875        g_signal_connect( r, "edited",
876                          G_CALLBACK( onAddressEdited ), page );
877        g_object_set( G_OBJECT( r ), "editable", TRUE, NULL );
878        c = gtk_tree_view_column_new_with_attributes( NULL, r,
879                                                      "text", COL_ADDRESS,
880                                                      NULL );
881        gtk_tree_view_column_set_expand( c, TRUE );
882        gtk_tree_view_append_column( v, c );
883        gtk_tree_view_set_headers_visible( v, FALSE );
884
885        s = _( "Addresses:" );
886        w = hig_workarea_add_row( t, &row, s, w, NULL );
887        gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.0f );
888        gtk_misc_set_padding( GTK_MISC( w ), 0, GUI_PAD );
889        page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
890
891        h = gtk_hbox_new( TRUE, GUI_PAD );
892        w = gtk_button_new_from_stock( GTK_STOCK_REMOVE );
893        g_signal_connect( w, "clicked", G_CALLBACK(
894                              onRemoveWhitelistClicked ), page );
895        page->remove_button = w;
896        onWhitelistSelectionChanged( sel, page );
897        gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
898        w = gtk_button_new_from_stock( GTK_STOCK_ADD );
899        page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
900        g_signal_connect( w, "clicked", G_CALLBACK( onAddWhitelistClicked ), page );
901        gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
902        w = gtk_hbox_new( FALSE, 0 );
903        gtk_box_pack_start( GTK_BOX( w ), gtk_alignment_new( 0, 0, 0, 0 ),
904                            TRUE, TRUE, 0 );
905        gtk_box_pack_start( GTK_BOX( w ), h, FALSE, FALSE, 0 );
906        hig_workarea_add_wide_control( t, &row, w );
907    }
908
909    refreshRPCSensitivity( page );
910    hig_workarea_finish( t, &row );
911    return t;
912}
913
914/****
915*****  Proxy Tab
916****/
917
918struct ProxyPage
919{
920    TrCore *  core;
921    GSList *  proxy_widgets;
922    GSList *  proxy_auth_widgets;
923};
924
925static void
926refreshProxySensitivity( struct ProxyPage * p )
927{
928    GSList *       l;
929    const gboolean proxy_enabled = pref_flag_get( TR_PREFS_KEY_PROXY_ENABLED );
930    const gboolean proxy_auth_enabled = pref_flag_get( TR_PREFS_KEY_PROXY_AUTH_ENABLED );
931
932    for( l = p->proxy_widgets; l != NULL; l = l->next )
933        gtk_widget_set_sensitive( GTK_WIDGET( l->data ), proxy_enabled );
934
935    for( l = p->proxy_auth_widgets; l != NULL; l = l->next )
936        gtk_widget_set_sensitive( GTK_WIDGET( l->data ),
937                                  proxy_enabled && proxy_auth_enabled );
938}
939
940static void
941onProxyToggled( GtkToggleButton * tb UNUSED,
942                gpointer             user_data )
943{
944    refreshProxySensitivity( user_data );
945}
946
947static void
948proxyPageFree( gpointer gpage )
949{
950    struct ProxyPage * page = gpage;
951
952    g_slist_free( page->proxy_widgets );
953    g_slist_free( page->proxy_auth_widgets );
954    g_free( page );
955}
956
957static GtkTreeModel*
958proxyTypeModelNew( void )
959{
960    GtkTreeIter    iter;
961    GtkListStore * store = gtk_list_store_new( 2, G_TYPE_STRING, G_TYPE_INT );
962
963    gtk_list_store_append( store, &iter );
964    gtk_list_store_set( store, &iter, 0, "HTTP", 1, TR_PROXY_HTTP, -1 );
965    gtk_list_store_append( store, &iter );
966    gtk_list_store_set( store, &iter, 0, "SOCKS4", 1, TR_PROXY_SOCKS4, -1 );
967    gtk_list_store_append( store, &iter );
968    gtk_list_store_set( store, &iter, 0, "SOCKS5", 1, TR_PROXY_SOCKS5, -1 );
969    return GTK_TREE_MODEL( store );
970}
971
972static void
973onProxyTypeChanged( GtkComboBox * w,
974                    gpointer      gpage )
975{
976    GtkTreeIter iter;
977
978    if( gtk_combo_box_get_active_iter( w, &iter ) )
979    {
980        struct ProxyPage * page = gpage;
981        int type = TR_PROXY_HTTP;
982        gtk_tree_model_get( gtk_combo_box_get_model( w ), &iter, 1, &type, -1 );
983        tr_core_set_pref_int( TR_CORE( page->core ), TR_PREFS_KEY_PROXY_TYPE, type );
984    }
985}
986
987static GtkWidget*
988trackerPage( GObject * core )
989{
990    int                row = 0;
991    const char *       s;
992    GtkWidget *        t;
993    GtkWidget *        w;
994    GtkTreeModel *     m;
995    GtkCellRenderer *  r;
996    struct ProxyPage * page = tr_new0( struct ProxyPage, 1 );
997
998    page->core = TR_CORE( core );
999
1000    t = hig_workarea_create( );
1001    hig_workarea_add_section_title ( t, &row, _( "Tracker" ) );
1002
1003    s = _( "Connect to tracker via a pro_xy" );
1004    w = new_check_button( s, TR_PREFS_KEY_PROXY_ENABLED, core );
1005    g_signal_connect( w, "toggled", G_CALLBACK( onProxyToggled ), page );
1006    hig_workarea_add_wide_control( t, &row, w );
1007
1008    s = _( "Proxy _server:" );
1009    w = new_entry( TR_PREFS_KEY_PROXY, core );
1010    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1011    w = hig_workarea_add_row( t, &row, s, w, NULL );
1012    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1013
1014    w = new_spin_button( TR_PREFS_KEY_PROXY_PORT, core, 0, USHRT_MAX, 1 );
1015    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1016    w = hig_workarea_add_row( t, &row, _( "Proxy _port:" ), w, NULL );
1017    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1018
1019    s = _( "Proxy _type:" );
1020    m = proxyTypeModelNew( );
1021    w = gtk_combo_box_new_with_model( m );
1022    r = gtk_cell_renderer_text_new( );
1023    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
1024    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( w ), r, "text", 0, NULL );
1025    gtk_combo_box_set_active( GTK_COMBO_BOX( w ), pref_int_get( TR_PREFS_KEY_PROXY_TYPE ) );
1026    g_signal_connect( w, "changed", G_CALLBACK( onProxyTypeChanged ), page );
1027    g_object_unref( G_OBJECT( m ) );
1028    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1029    w = hig_workarea_add_row( t, &row, s, w, NULL );
1030    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1031
1032    s = _( "Use _authentication" );
1033    w = new_check_button( s, TR_PREFS_KEY_PROXY_AUTH_ENABLED, core );
1034    g_signal_connect( w, "toggled", G_CALLBACK( onProxyToggled ), page );
1035    hig_workarea_add_wide_control( t, &row, w );
1036    page->proxy_widgets = g_slist_append( page->proxy_widgets, w );
1037
1038    s = _( "_Username:" );
1039    w = new_entry( TR_PREFS_KEY_PROXY_USERNAME, core );
1040    page->proxy_auth_widgets = g_slist_append( page->proxy_auth_widgets, w );
1041    w = hig_workarea_add_row( t, &row, s, w, NULL );
1042    page->proxy_auth_widgets = g_slist_append( page->proxy_auth_widgets, w );
1043
1044    s = _( "Pass_word:" );
1045    w = new_entry( TR_PREFS_KEY_PROXY_PASSWORD, core );
1046    gtk_entry_set_visibility( GTK_ENTRY( w ), FALSE );
1047    page->proxy_auth_widgets = g_slist_append( page->proxy_auth_widgets, w );
1048    w = hig_workarea_add_row( t, &row, s, w, NULL );
1049    page->proxy_auth_widgets = g_slist_append( page->proxy_auth_widgets, w );
1050
1051    hig_workarea_finish( t, &row );
1052    g_object_set_data_full( G_OBJECT( t ), "page", page, proxyPageFree );
1053
1054    refreshProxySensitivity( page );
1055    return t;
1056}
1057
1058/****
1059*****  Bandwidth Tab
1060****/
1061
1062struct BandwidthPage
1063{
1064    TrCore *  core;
1065    GSList *  sched_widgets;
1066};
1067
1068static void
1069refreshSchedSensitivity( struct BandwidthPage * p )
1070{
1071    GSList *       l;
1072    const gboolean sched_enabled = pref_flag_get( TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED );
1073
1074    for( l = p->sched_widgets; l != NULL; l = l->next )
1075        gtk_widget_set_sensitive( GTK_WIDGET( l->data ), sched_enabled );
1076}
1077
1078static void
1079onSchedToggled( GtkToggleButton * tb UNUSED,
1080                gpointer             user_data )
1081{
1082    refreshSchedSensitivity( user_data );
1083}
1084
1085static void
1086onTimeComboChanged( GtkComboBox * w,
1087                    gpointer      core )
1088{
1089    GtkTreeIter iter;
1090
1091    if( gtk_combo_box_get_active_iter( w, &iter ) )
1092    {
1093        const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
1094        int          val = 0;
1095        gtk_tree_model_get( gtk_combo_box_get_model(
1096                                w ), &iter, 0, &val, -1 );
1097        tr_core_set_pref_int( TR_CORE( core ), key, val );
1098    }
1099}
1100
1101static GtkWidget*
1102new_time_combo( GObject *    core,
1103                const char * key )
1104{
1105    int               val;
1106    int               i;
1107    GtkWidget *       w;
1108    GtkCellRenderer * r;
1109    GtkListStore *    store;
1110
1111    /* build a store at 15 minute intervals */
1112    store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING );
1113    for( i = 0; i < 60 * 24; i += 15 )
1114    {
1115        char        buf[128];
1116        GtkTreeIter iter;
1117        struct tm   tm;
1118        tm.tm_hour = i / 60;
1119        tm.tm_min = i % 60;
1120        tm.tm_sec = 0;
1121        strftime( buf, sizeof( buf ), "%H:%M", &tm );
1122        gtk_list_store_append( store, &iter );
1123        gtk_list_store_set( store, &iter, 0, i, 1, buf, -1 );
1124    }
1125
1126    /* build the widget */
1127    w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) );
1128    gtk_combo_box_set_wrap_width( GTK_COMBO_BOX( w ), 4 );
1129    r = gtk_cell_renderer_text_new( );
1130    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
1131    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT(
1132                                        w ), r, "text", 1, NULL );
1133    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup(
1134                                key ), g_free );
1135    val = pref_int_get( key );
1136    gtk_combo_box_set_active( GTK_COMBO_BOX( w ), val / ( 15 ) );
1137    g_signal_connect( w, "changed", G_CALLBACK( onTimeComboChanged ), core );
1138
1139    /* cleanup */
1140    g_object_unref( G_OBJECT( store ) );
1141    return w;
1142}
1143
1144static GtkWidget*
1145new_week_combo( GObject * core, const char * key )
1146{
1147    int i;
1148    int selIndex;
1149    GtkWidget * w;
1150    GtkCellRenderer * r;
1151    GtkListStore * store;
1152    const int currentValue = pref_int_get( key );
1153    const struct {
1154        int value;
1155        const char * text;
1156    } items[] = {
1157        { TR_SCHED_ALL,     N_( "Every Day" ) },
1158        { TR_SCHED_WEEKDAY, N_( "Weekdays" ) },
1159        { TR_SCHED_WEEKEND, N_( "Weekends" ) },
1160        { TR_SCHED_SUN,     N_( "Sunday" ) },
1161        { TR_SCHED_MON,     N_( "Monday" ) },
1162        { TR_SCHED_TUES,    N_( "Tuesday" ) },
1163        { TR_SCHED_WED,     N_( "Wednesday" ) },
1164        { TR_SCHED_THURS,   N_( "Thursday" ) },
1165        { TR_SCHED_FRI,     N_( "Friday" ) },
1166        { TR_SCHED_SAT,     N_( "Saturday" ) }
1167    };
1168
1169    /* build a store for the days of the week */
1170    selIndex = -1;
1171    store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING );
1172    for( i=0; i<(int)G_N_ELEMENTS(items); ++i ) {
1173        GtkTreeIter iter;
1174        gtk_list_store_append( store, &iter );
1175        gtk_list_store_set( store, &iter, 0, items[i].value, 1, _( items[i].text ), -1 );
1176        if( items[i].value == currentValue )
1177            selIndex = i;
1178    }
1179
1180    /* build the widget */
1181    w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) );
1182    r = gtk_cell_renderer_text_new( );
1183    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
1184    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( w ), r, "text", 1, NULL );
1185    g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup( key ), g_free );
1186    if( selIndex >= 0 )
1187        gtk_combo_box_set_active( GTK_COMBO_BOX( w ), selIndex );
1188    g_signal_connect( w, "changed", G_CALLBACK( onIntComboChanged ), core );
1189
1190    /* cleanup */
1191    g_object_unref( G_OBJECT( store ) );
1192    return w;
1193}
1194
1195static void
1196bandwidthPageFree( gpointer gpage )
1197{
1198    struct BandwidthPage * page = gpage;
1199
1200    g_slist_free( page->sched_widgets );
1201    g_free( page );
1202}
1203
1204static GtkWidget*
1205bandwidthPage( GObject * core )
1206{
1207    int row = 0;
1208    const char * s;
1209    GtkWidget * t;
1210    GtkWidget * l;
1211    GtkWidget * w, * w2, * h;
1212    char buf[512];
1213    struct BandwidthPage * page = tr_new0( struct BandwidthPage, 1 );
1214
1215    page->core = TR_CORE( core );
1216
1217    t = hig_workarea_create( );
1218    hig_workarea_add_section_title( t, &row, _( "Speed Limits" ) );
1219
1220        s = _( "Limit _download speed (KiB/s):" );
1221        w = new_check_button( s, TR_PREFS_KEY_DSPEED_ENABLED, core );
1222        w2 = new_spin_button( TR_PREFS_KEY_DSPEED, core, 0, INT_MAX, 5 );
1223        gtk_widget_set_sensitive( GTK_WIDGET( w2 ), pref_flag_get( TR_PREFS_KEY_DSPEED_ENABLED ) );
1224        g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
1225        hig_workarea_add_row_w( t, &row, w, w2, NULL );
1226
1227        s = _( "Limit _upload speed (KiB/s):" );
1228        w = new_check_button( s, TR_PREFS_KEY_USPEED_ENABLED, core );
1229        w2 = new_spin_button( TR_PREFS_KEY_USPEED, core, 0, INT_MAX, 5 );
1230        gtk_widget_set_sensitive( GTK_WIDGET( w2 ), pref_flag_get( TR_PREFS_KEY_USPEED_ENABLED ) );
1231        g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
1232        hig_workarea_add_row_w( t, &row, w, w2, NULL );
1233
1234    hig_workarea_add_section_divider( t, &row );
1235    h = gtk_hbox_new( FALSE, GUI_PAD );
1236    w = gtk_image_new_from_stock( "alt-speed-off", -1 );
1237    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1238    g_snprintf( buf, sizeof( buf ), "<b>%s</b>", _( "Temporary Speed Limits" ) );
1239    w = gtk_label_new( buf );
1240    gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.5f );
1241    gtk_label_set_use_markup( GTK_LABEL( w ), TRUE );
1242    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1243    hig_workarea_add_section_title_widget( t, &row, h );
1244
1245        s = _( "Override normal speed limits manually or at scheduled times" );
1246        g_snprintf( buf, sizeof( buf ), "<small>%s</small>", s );
1247        w = gtk_label_new( buf );
1248        gtk_label_set_use_markup( GTK_LABEL( w ), TRUE );
1249        gtk_misc_set_alignment( GTK_MISC( w ), 0.5f, 0.5f );
1250        hig_workarea_add_wide_control( t, &row, w );
1251
1252        s = _( "Limit do_wnload speed (KiB/s):" );
1253        w = new_spin_button( TR_PREFS_KEY_ALT_SPEED_DOWN, core, 0, INT_MAX, 5 );
1254        hig_workarea_add_row( t, &row, s, w, NULL );
1255
1256        s = _( "Limit u_pload speed (KiB/s):" );
1257        w = new_spin_button( TR_PREFS_KEY_ALT_SPEED_UP, core, 0, INT_MAX, 5 );
1258        hig_workarea_add_row( t, &row, s, w, NULL );
1259
1260        s = _( "_Scheduled times:" );
1261        h = gtk_hbox_new( FALSE, 0 );
1262        w2 = new_time_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN );
1263        page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
1264        gtk_box_pack_start( GTK_BOX( h ), w2, TRUE, TRUE, 0 );
1265        w2 = l = gtk_label_new_with_mnemonic ( _( " _to " ) );
1266        page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
1267        gtk_box_pack_start( GTK_BOX( h ), w2, FALSE, FALSE, 0 );
1268        w2 = new_time_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_END );
1269        gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w2 );
1270        page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
1271        gtk_box_pack_start( GTK_BOX( h ), w2, TRUE, TRUE, 0 );
1272        w = new_check_button( s, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED, core );
1273        g_signal_connect( w, "toggled", G_CALLBACK( onSchedToggled ), page );
1274        hig_workarea_add_row_w( t, &row, w, h, NULL );
1275
1276        s = _( "_On days:" );
1277        w = new_week_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_DAY );
1278        page->sched_widgets = g_slist_append( page->sched_widgets, w );
1279        w = hig_workarea_add_row( t, &row, s, w, NULL );
1280        page->sched_widgets = g_slist_append( page->sched_widgets, w );
1281
1282    hig_workarea_finish( t, &row );
1283    g_object_set_data_full( G_OBJECT( t ), "page", page, bandwidthPageFree );
1284
1285    refreshSchedSensitivity( page );
1286    return t;
1287}
1288
1289/****
1290*****  Network Tab
1291****/
1292
1293struct network_page_data
1294{
1295    TrCore     * core;
1296    GtkWidget  * portLabel;
1297    GtkWidget  * portButton;
1298    GtkWidget  * portSpin;
1299    gulong       portTag;
1300    gulong       prefsTag;
1301};
1302
1303static void
1304onCorePrefsChanged( TrCore * core UNUSED, const char *  key, gpointer gdata )
1305{
1306    if( !strcmp( key, TR_PREFS_KEY_PEER_PORT ) )
1307    {
1308        struct network_page_data * data = gdata;
1309        gdk_threads_enter();
1310        gtk_label_set_text( GTK_LABEL( data->portLabel ), _( "Status unknown" ) );
1311        gtk_widget_set_sensitive( data->portButton, TRUE );
1312        gtk_widget_set_sensitive( data->portSpin, TRUE );
1313        gdk_threads_leave();
1314    }
1315}
1316
1317static void
1318peerPageDestroyed( gpointer gdata, GObject * dead UNUSED )
1319{
1320    struct network_page_data * data = gdata;
1321    if( data->prefsTag > 0 )
1322        g_signal_handler_disconnect( data->core, data->prefsTag );
1323    if( data->portTag > 0 )
1324        g_signal_handler_disconnect( data->core, data->portTag );
1325    g_free( data );
1326}
1327
1328static void
1329onPortTested( TrCore * core UNUSED, gboolean isOpen, gpointer vdata )
1330{
1331    struct network_page_data * data = vdata;
1332    const char * markup = isOpen ? _( "Port is <b>open</b>" ) : _( "Port is <b>closed</b>" );
1333    gdk_threads_enter();
1334    gtk_label_set_markup( GTK_LABEL( data->portLabel ), markup );
1335    gtk_widget_set_sensitive( data->portButton, TRUE );
1336    gtk_widget_set_sensitive( data->portSpin, TRUE );
1337    gdk_threads_leave();
1338}
1339
1340static void
1341onPortTest( GtkButton * button UNUSED, gpointer vdata )
1342{
1343    struct network_page_data * data = vdata;
1344    gtk_widget_set_sensitive( data->portButton, FALSE );
1345    gtk_widget_set_sensitive( data->portSpin, FALSE );
1346    gtk_label_set_markup( GTK_LABEL( data->portLabel ), _( "<i>Testing...</i>" ) );
1347    if( !data->portTag )
1348        data->portTag = g_signal_connect( data->core, "port-tested", G_CALLBACK(onPortTested), data );
1349    tr_core_port_test( data->core );
1350}
1351
1352static GtkWidget*
1353peerPage( GObject * core )
1354{
1355    int                        row = 0;
1356    const char *               s;
1357    GtkWidget *                t;
1358    GtkWidget *                w;
1359    GtkWidget *                h;
1360    GtkWidget *                l;
1361    struct network_page_data * data;
1362
1363    /* register to stop listening to core prefs changes when the page is destroyed */
1364    data = g_new0( struct network_page_data, 1 );
1365    data->core = TR_CORE( core );
1366
1367    /* build the page */
1368    t = hig_workarea_create( );
1369    hig_workarea_add_section_title( t, &row, _( "Incoming Peers" ) );
1370
1371    s = _( "_Port for incoming connections:" );
1372    w = data->portSpin = new_spin_button( TR_PREFS_KEY_PEER_PORT, core, 1, USHRT_MAX, 1 );
1373    hig_workarea_add_row( t, &row, s, w, NULL );
1374
1375    h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
1376    l = data->portLabel = gtk_label_new( _( "Status unknown" ) );
1377    gtk_misc_set_alignment( GTK_MISC( l ), 0.0f, 0.5f );
1378    gtk_box_pack_start( GTK_BOX( h ), l, TRUE, TRUE, 0 );
1379    w = data->portButton = gtk_button_new_with_mnemonic( _( "Te_st Port" ) );
1380    gtk_box_pack_end( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1381    g_signal_connect( w, "clicked", G_CALLBACK(onPortTest), data );
1382    hig_workarea_add_row( t, &row, NULL, h, NULL );
1383    data->prefsTag = g_signal_connect( TR_CORE( core ), "prefs-changed", G_CALLBACK( onCorePrefsChanged ), data );
1384    g_object_weak_ref( G_OBJECT( t ), peerPageDestroyed, data );
1385
1386    s = _( "Use UPnP or NAT-PMP port _forwarding from my router" );
1387    w = new_check_button( s, TR_PREFS_KEY_PORT_FORWARDING, core );
1388    hig_workarea_add_wide_control( t, &row, w );
1389
1390    s = _( "Pick a _random port every time Transmission is started" );
1391    w = new_check_button( s, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START, core );
1392    hig_workarea_add_wide_control( t, &row, w );
1393
1394    hig_workarea_add_section_divider( t, &row );
1395    hig_workarea_add_section_title( t, &row, _( "Limits" ) );
1396
1397    w = new_spin_button( TR_PREFS_KEY_PEER_LIMIT_TORRENT, core, 1, 300, 5 );
1398    hig_workarea_add_row( t, &row, _( "Maximum peers per _torrent:" ), w, NULL );
1399    w = new_spin_button( TR_PREFS_KEY_PEER_LIMIT_GLOBAL, core, 1, 3000, 5 );
1400    hig_workarea_add_row( t, &row, _( "Maximum peers _overall:" ), w, NULL );
1401
1402    hig_workarea_finish( t, &row );
1403    return t;
1404}
1405
1406/****
1407*****
1408****/
1409
1410GtkWidget *
1411tr_prefs_dialog_new( GObject *   core,
1412                     GtkWindow * parent )
1413{
1414    GtkWidget * d;
1415    GtkWidget * n;
1416
1417    d = gtk_dialog_new_with_buttons( _(
1418                                         "Transmission Preferences" ),
1419                                     parent,
1420                                     GTK_DIALOG_DESTROY_WITH_PARENT,
1421                                     GTK_STOCK_HELP, GTK_RESPONSE_HELP,
1422                                     GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
1423                                     NULL );
1424    gtk_window_set_role( GTK_WINDOW( d ), "transmission-preferences-dialog" );
1425    gtk_dialog_set_has_separator( GTK_DIALOG( d ), FALSE );
1426    gtk_container_set_border_width( GTK_CONTAINER( d ), GUI_PAD );
1427
1428    n = gtk_notebook_new( );
1429    gtk_container_set_border_width ( GTK_CONTAINER ( n ), GUI_PAD );
1430
1431    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1432                              torrentPage( core ),
1433                              gtk_label_new ( _( "Torrents" ) ) );
1434    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1435                              bandwidthPage( core ),
1436                              gtk_label_new ( _( "Speed" ) ) );
1437    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1438                              privacyPage( core ),
1439                              gtk_label_new ( _( "Privacy" ) ) );
1440    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1441                              peerPage( core ),
1442                              gtk_label_new ( _( "Network" ) ) );
1443    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1444                              desktopPage( core ),
1445                              gtk_label_new ( _( "Desktop" ) ) );
1446    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1447                              webPage( core ),
1448                              gtk_label_new ( _( "Web" ) ) );
1449    gtk_notebook_append_page( GTK_NOTEBOOK( n ),
1450                              trackerPage( core ),
1451                              gtk_label_new ( _( "Proxy" ) ) );
1452
1453    g_signal_connect( d, "response", G_CALLBACK( response_cb ), core );
1454    gtk_box_pack_start( GTK_BOX( GTK_DIALOG( d )->vbox ), n, TRUE, TRUE, 0 );
1455    gtk_widget_show_all( GTK_DIALOG( d )->vbox );
1456    return d;
1457}
1458
Note: See TracBrowser for help on using the repository browser.