mirror of
https://github.com/transmission/transmission
synced 2024-12-27 01:57:52 +00:00
aea77bc9f6
Some of the refresh events to the main window's torrent list are caused by main window relayout caused by the toolbar's GtkLabels recalculating their size after being updated once per second. To prevent relayout in some trivial cases, I'm replacing the gtk_label_set_text() calls with gtr_label_set_text() because the latter doesn't update the label widget if the old and new text strings are the same. There are other changes that can handle more important cases -- I'll test those out next.
1276 lines
45 KiB
C
1276 lines
45 KiB
C
/*
|
|
* This file Copyright (C) Mnemosyne LLC
|
|
*
|
|
* This file is licensed by the GPL version 2. Works owned by the
|
|
* Transmission project are granted a special exemption to clause 2(b)
|
|
* so that the bulk of its code can remain under the MIT license.
|
|
* This exemption does not extend to derived works not owned by
|
|
* the Transmission project.
|
|
*
|
|
* $Id$
|
|
*/
|
|
|
|
#include <ctype.h> /* isspace */
|
|
#include <limits.h> /* USHRT_MAX */
|
|
#include <stdlib.h> /* free() */
|
|
#include <unistd.h>
|
|
#include <glib/gi18n.h>
|
|
#include <gtk/gtk.h>
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/utils.h>
|
|
#include <libtransmission/version.h>
|
|
#include <libtransmission/web.h>
|
|
#include "conf.h"
|
|
#include "hig.h"
|
|
#include "tr-core.h"
|
|
#include "tr-prefs.h"
|
|
#include "util.h"
|
|
|
|
/**
|
|
***
|
|
**/
|
|
|
|
#define PREF_KEY "pref-key"
|
|
|
|
static void
|
|
response_cb( GtkDialog * dialog,
|
|
int response,
|
|
gpointer unused UNUSED )
|
|
{
|
|
if( response == GTK_RESPONSE_HELP )
|
|
{
|
|
char * uri = g_strconcat( gtr_get_help_uri(), "/html/preferences.html", NULL );
|
|
gtr_open_uri( uri );
|
|
g_free( uri );
|
|
}
|
|
|
|
if( response == GTK_RESPONSE_CLOSE )
|
|
gtk_widget_destroy( GTK_WIDGET( dialog ) );
|
|
}
|
|
|
|
static void
|
|
toggled_cb( GtkToggleButton * w,
|
|
gpointer core )
|
|
{
|
|
const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
|
|
const gboolean flag = gtk_toggle_button_get_active( w );
|
|
|
|
tr_core_set_pref_bool( TR_CORE( core ), key, flag );
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_check_button( const char * mnemonic,
|
|
const char * key,
|
|
gpointer core )
|
|
{
|
|
GtkWidget * w = gtk_check_button_new_with_mnemonic( mnemonic );
|
|
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
|
|
key ), g_free );
|
|
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( w ),
|
|
gtr_pref_flag_get( key ) );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( toggled_cb ), core );
|
|
return w;
|
|
}
|
|
|
|
#define IDLE_DATA "idle-data"
|
|
|
|
struct spin_idle_data
|
|
{
|
|
gpointer core;
|
|
GTimer * last_change;
|
|
gboolean isDouble;
|
|
};
|
|
|
|
static void
|
|
spin_idle_data_free( gpointer gdata )
|
|
{
|
|
struct spin_idle_data * data = gdata;
|
|
|
|
g_timer_destroy( data->last_change );
|
|
g_free( data );
|
|
}
|
|
|
|
static gboolean
|
|
spun_cb_idle( gpointer spin )
|
|
{
|
|
gboolean keep_waiting = TRUE;
|
|
GObject * o = G_OBJECT( spin );
|
|
struct spin_idle_data * data = g_object_get_data( o, IDLE_DATA );
|
|
|
|
/* has the user stopped making changes? */
|
|
if( g_timer_elapsed( data->last_change, NULL ) > 0.33f )
|
|
{
|
|
/* update the core */
|
|
const char * key = g_object_get_data( o, PREF_KEY );
|
|
|
|
if (data->isDouble)
|
|
{
|
|
const double value = gtk_spin_button_get_value( GTK_SPIN_BUTTON( spin ) );
|
|
tr_core_set_pref_double( TR_CORE( data->core ), key, value );
|
|
}
|
|
else
|
|
{
|
|
const int value = gtk_spin_button_get_value_as_int( GTK_SPIN_BUTTON( spin ) );
|
|
tr_core_set_pref_int( TR_CORE( data->core ), key, value );
|
|
}
|
|
|
|
/* cleanup */
|
|
g_object_set_data( o, IDLE_DATA, NULL );
|
|
keep_waiting = FALSE;
|
|
g_object_unref( G_OBJECT( o ) );
|
|
}
|
|
|
|
return keep_waiting;
|
|
}
|
|
|
|
static void
|
|
spun_cb( GtkSpinButton * w, gpointer core, gboolean isDouble )
|
|
{
|
|
/* user may be spinning through many values, so let's hold off
|
|
for a moment to keep from flooding the core with changes */
|
|
GObject * o = G_OBJECT( w );
|
|
struct spin_idle_data * data = g_object_get_data( o, IDLE_DATA );
|
|
|
|
if( data == NULL )
|
|
{
|
|
data = g_new( struct spin_idle_data, 1 );
|
|
data->core = core;
|
|
data->last_change = g_timer_new( );
|
|
data->isDouble = isDouble;
|
|
g_object_set_data_full( o, IDLE_DATA, data, spin_idle_data_free );
|
|
g_object_ref( G_OBJECT( o ) );
|
|
gtr_timeout_add_seconds( 1, spun_cb_idle, w );
|
|
}
|
|
g_timer_start( data->last_change );
|
|
}
|
|
|
|
static void
|
|
spun_cb_int( GtkSpinButton * w, gpointer core )
|
|
{
|
|
spun_cb( w, core, FALSE );
|
|
}
|
|
|
|
static void
|
|
spun_cb_double( GtkSpinButton * w, gpointer core )
|
|
{
|
|
spun_cb( w, core, TRUE );
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_spin_button( const char * key,
|
|
gpointer core,
|
|
int low,
|
|
int high,
|
|
int step )
|
|
{
|
|
GtkWidget * w = gtk_spin_button_new_with_range( low, high, step );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
|
|
gtk_spin_button_set_digits( GTK_SPIN_BUTTON( w ), 0 );
|
|
gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), gtr_pref_int_get( key ) );
|
|
g_signal_connect( w, "value-changed", G_CALLBACK( spun_cb_int ), core );
|
|
return w;
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_spin_button_double( const char * key,
|
|
gpointer core,
|
|
double low,
|
|
double high,
|
|
double step )
|
|
{
|
|
GtkWidget * w = gtk_spin_button_new_with_range( low, high, step );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
|
|
gtk_spin_button_set_digits( GTK_SPIN_BUTTON( w ), 2 );
|
|
gtk_spin_button_set_value( GTK_SPIN_BUTTON( w ), gtr_pref_double_get( key ) );
|
|
g_signal_connect( w, "value-changed", G_CALLBACK( spun_cb_double ), core );
|
|
return w;
|
|
}
|
|
|
|
static void
|
|
entry_changed_cb( GtkEntry * w, gpointer core )
|
|
{
|
|
const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
|
|
const char * value = gtk_entry_get_text( w );
|
|
|
|
tr_core_set_pref( TR_CORE( core ), key, value );
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_entry( const char * key,
|
|
gpointer core )
|
|
{
|
|
GtkWidget * w = gtk_entry_new( );
|
|
const char * value = gtr_pref_string_get( key );
|
|
|
|
if( value )
|
|
gtk_entry_set_text( GTK_ENTRY( w ), value );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup(
|
|
key ), g_free );
|
|
g_signal_connect( w, "changed", G_CALLBACK( entry_changed_cb ), core );
|
|
return w;
|
|
}
|
|
|
|
static void
|
|
chosen_cb( GtkFileChooser * w, gpointer core )
|
|
{
|
|
const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
|
|
char * value = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER( w ) );
|
|
tr_core_set_pref( TR_CORE( core ), key, value );
|
|
g_free( value );
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_path_chooser_button( const char * key, gpointer core )
|
|
{
|
|
GtkWidget * w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER );
|
|
const char * path = gtr_pref_string_get( key );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
|
|
g_signal_connect( w, "selection-changed", G_CALLBACK( chosen_cb ), core );
|
|
if( path != NULL )
|
|
gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER( w ), path );
|
|
return w;
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_file_chooser_button( const char * key, gpointer core )
|
|
{
|
|
GtkWidget * w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_OPEN );
|
|
const char * path = gtr_pref_string_get( key );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, g_strdup( key ), g_free );
|
|
if( path != NULL )
|
|
gtk_file_chooser_set_filename( GTK_FILE_CHOOSER( w ), path );
|
|
g_signal_connect( w, "selection-changed", G_CALLBACK( chosen_cb ), core );
|
|
return w;
|
|
}
|
|
|
|
static void
|
|
target_cb( GtkWidget * tb, gpointer target )
|
|
{
|
|
const gboolean b = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( tb ) );
|
|
|
|
gtk_widget_set_sensitive( GTK_WIDGET( target ), b );
|
|
}
|
|
|
|
/****
|
|
***** Torrent Tab
|
|
****/
|
|
|
|
static GtkWidget*
|
|
torrentPage( GObject * core )
|
|
{
|
|
int row = 0;
|
|
const char * s;
|
|
GtkWidget * t;
|
|
GtkWidget * w;
|
|
GtkWidget * w2;
|
|
GtkWidget * l;
|
|
|
|
t = hig_workarea_create( );
|
|
hig_workarea_add_section_title( t, &row, _( "Adding" ) );
|
|
|
|
s = _( "_Start when added" );
|
|
w = new_check_button( s, TR_PREFS_KEY_START, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Show _options dialog" );
|
|
w = new_check_button( s, PREF_KEY_OPTIONS_PROMPT, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Mo_ve .torrent file to the trash" );
|
|
w = new_check_button( s, TR_PREFS_KEY_TRASH_ORIGINAL, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
#ifdef HAVE_GIO
|
|
s = _( "Automatically _add torrents from:" );
|
|
l = new_check_button( s, PREF_KEY_DIR_WATCH_ENABLED, core );
|
|
w = new_path_chooser_button( PREF_KEY_DIR_WATCH, core );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w ),
|
|
gtr_pref_flag_get( PREF_KEY_DIR_WATCH_ENABLED ) );
|
|
g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
|
|
hig_workarea_add_row_w( t, &row, l, w, NULL );
|
|
#endif
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
hig_workarea_add_section_title( t, &row, _( "Downloading" ) );
|
|
|
|
s = _( "Append \"._part\" to incomplete files' names" );
|
|
w = new_check_button( s, TR_PREFS_KEY_RENAME_PARTIAL_FILES, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
w = new_path_chooser_button( TR_PREFS_KEY_DOWNLOAD_DIR, core );
|
|
hig_workarea_add_row( t, &row, _( "Save to _Location:" ), w, NULL );
|
|
|
|
s = _( "Keep _incomplete torrents in:" );
|
|
l = new_check_button( s, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED, core );
|
|
w = new_path_chooser_button( TR_PREFS_KEY_INCOMPLETE_DIR, core );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w ), gtr_pref_flag_get( TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED ) );
|
|
g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
|
|
hig_workarea_add_row_w( t, &row, l, w, NULL );
|
|
|
|
s = _( "Call scrip_t when torrent is completed:" );
|
|
l = new_check_button( s, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED, core );
|
|
w = new_file_chooser_button( TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME, core );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w ), gtr_pref_flag_get( TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED ) );
|
|
g_signal_connect( l, "toggled", G_CALLBACK( target_cb ), w );
|
|
hig_workarea_add_row_w( t, &row, l, w, NULL );
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
hig_workarea_add_section_title( t, &row, _( "Seeding" ) );
|
|
|
|
s = _( "Stop seeding at _ratio:" );
|
|
w = new_check_button( s, TR_PREFS_KEY_RATIO_ENABLED, core );
|
|
w2 = new_spin_button_double( TR_PREFS_KEY_RATIO, core, 0, 1000, .05 );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w2 ), gtr_pref_flag_get( TR_PREFS_KEY_RATIO_ENABLED ) );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
|
|
hig_workarea_add_row_w( t, &row, w, w2, NULL );
|
|
|
|
s = _( "Stop seeding if idle for _N minutes:" );
|
|
w = new_check_button( s, TR_PREFS_KEY_IDLE_LIMIT_ENABLED, core );
|
|
w2 = new_spin_button( TR_PREFS_KEY_IDLE_LIMIT, core, 1, 9999, 5 );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w2 ), gtr_pref_flag_get( TR_PREFS_KEY_IDLE_LIMIT_ENABLED ) );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
|
|
hig_workarea_add_row_w( t, &row, w, w2, NULL );
|
|
|
|
hig_workarea_finish( t, &row );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Desktop Tab
|
|
****/
|
|
|
|
static GtkWidget*
|
|
desktopPage( GObject * core )
|
|
{
|
|
int row = 0;
|
|
const char * s;
|
|
GtkWidget * t;
|
|
GtkWidget * w;
|
|
|
|
t = hig_workarea_create( );
|
|
hig_workarea_add_section_title( t, &row, _( "Desktop" ) );
|
|
|
|
s = _( "Inhibit _hibernation when torrents are active" );
|
|
w = new_check_button( s, PREF_KEY_INHIBIT_HIBERNATION, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Show Transmission icon in the _notification area" );
|
|
w = new_check_button( s, PREF_KEY_SHOW_TRAY_ICON, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Show _popup notifications" );
|
|
w = new_check_button( s, PREF_KEY_SHOW_DESKTOP_NOTIFICATION, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
#ifdef HAVE_LIBCANBERRA
|
|
s = _( "Play _sound when downloads are complete" );
|
|
w = new_check_button( s, PREF_KEY_PLAY_DOWNLOAD_COMPLETE_SOUND, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
#endif
|
|
|
|
hig_workarea_finish( t, &row );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Peer Tab
|
|
****/
|
|
|
|
struct blocklist_data
|
|
{
|
|
gulong updateBlocklistTag;
|
|
GtkWidget * updateBlocklistButton;
|
|
GtkWidget * updateBlocklistDialog;
|
|
GtkWidget * label;
|
|
GtkWidget * check;
|
|
TrCore * core;
|
|
};
|
|
|
|
static void
|
|
updateBlocklistText( GtkWidget * w, TrCore * core )
|
|
{
|
|
char buf1[512];
|
|
char buf2[512];
|
|
const int n = tr_blocklistGetRuleCount( tr_core_session( core ) );
|
|
g_snprintf( buf1, sizeof( buf1 ),
|
|
gtr_ngettext( "Blocklist contains %'d rule",
|
|
"Blocklist contains %'d rules", n ), n );
|
|
g_snprintf( buf2, sizeof( buf2 ), "<i>%s</i>", buf1 );
|
|
gtk_label_set_markup( GTK_LABEL( w ), buf2 );
|
|
}
|
|
|
|
/* prefs dialog is being destroyed, so stop listening to blocklist updates */
|
|
static void
|
|
privacyPageDestroyed( gpointer gdata, GObject * dead UNUSED )
|
|
{
|
|
struct blocklist_data * data = gdata;
|
|
if( data->updateBlocklistTag > 0 )
|
|
g_signal_handler_disconnect( data->core, data->updateBlocklistTag );
|
|
g_free( data );
|
|
}
|
|
|
|
/* user hit "close" in the blocklist-update dialog */
|
|
static void
|
|
onBlocklistUpdateResponse( GtkDialog * dialog, gint response UNUSED, gpointer gdata )
|
|
{
|
|
struct blocklist_data * data = gdata;
|
|
gtk_widget_destroy( GTK_WIDGET( dialog ) );
|
|
gtk_widget_set_sensitive( data->updateBlocklistButton, TRUE );
|
|
data->updateBlocklistDialog = NULL;
|
|
g_signal_handler_disconnect( data->core, data->updateBlocklistTag );
|
|
}
|
|
|
|
/* core says the blocklist was updated */
|
|
static void
|
|
onBlocklistUpdated( TrCore * core, int n, gpointer gdata )
|
|
{
|
|
const tr_bool success = n >= 0;
|
|
const int count = n >=0 ? n : tr_blocklistGetRuleCount( tr_core_session( core ) );
|
|
const char * s = gtr_ngettext( "Blocklist has %'d rule.", "Blocklist has %'d rules.", count );
|
|
struct blocklist_data * data = gdata;
|
|
GtkMessageDialog * d = GTK_MESSAGE_DIALOG( data->updateBlocklistDialog );
|
|
gtk_widget_set_sensitive( data->updateBlocklistButton, TRUE );
|
|
gtk_message_dialog_set_markup( d, success ? _( "<b>Update succeeded!</b>" ) : _( "<b>Unable to update.</b>" ) );
|
|
gtk_message_dialog_format_secondary_text( d, s, count );
|
|
updateBlocklistText( data->label, core );
|
|
}
|
|
|
|
/* user pushed a button to update the blocklist */
|
|
static void
|
|
onBlocklistUpdate( GtkButton * w, gpointer gdata )
|
|
{
|
|
GtkWidget * d;
|
|
struct blocklist_data * data = gdata;
|
|
d = gtk_message_dialog_new( GTK_WINDOW( gtk_widget_get_toplevel( GTK_WIDGET( w ) ) ),
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
GTK_MESSAGE_INFO,
|
|
GTK_BUTTONS_CLOSE,
|
|
"%s", _( "Update Blocklist" ) );
|
|
gtk_widget_set_sensitive( data->updateBlocklistButton, FALSE );
|
|
gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( d ), "%s", _( "Getting new blocklist..." ) );
|
|
data->updateBlocklistDialog = d;
|
|
g_signal_connect( d, "response", G_CALLBACK(onBlocklistUpdateResponse), data );
|
|
gtk_widget_show( d );
|
|
tr_core_blocklist_update( data->core );
|
|
data->updateBlocklistTag = g_signal_connect( data->core, "blocklist-updated", G_CALLBACK( onBlocklistUpdated ), data );
|
|
}
|
|
|
|
static void
|
|
onIntComboChanged( GtkComboBox * combo_box, gpointer core )
|
|
{
|
|
const int val = gtr_combo_box_get_active_enum( combo_box );
|
|
const char * key = g_object_get_data( G_OBJECT( combo_box ), PREF_KEY );
|
|
tr_core_set_pref_int( TR_CORE( core ), key, val );
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_encryption_combo( GObject * core, const char * key )
|
|
{
|
|
GtkWidget * w = gtr_combo_box_new_enum( _( "Allow encryption" ), TR_CLEAR_PREFERRED,
|
|
_( "Prefer encryption" ), TR_ENCRYPTION_PREFERRED,
|
|
_( "Require encryption" ), TR_ENCRYPTION_REQUIRED,
|
|
NULL );
|
|
gtr_combo_box_set_active_enum( GTK_COMBO_BOX( w ), gtr_pref_int_get( key ) );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup( key ), g_free );
|
|
g_signal_connect( w, "changed", G_CALLBACK( onIntComboChanged ), core );
|
|
return w;
|
|
}
|
|
|
|
static GtkWidget*
|
|
privacyPage( GObject * core )
|
|
{
|
|
int row = 0;
|
|
const char * s;
|
|
GtkWidget * t;
|
|
GtkWidget * w;
|
|
GtkWidget * b;
|
|
GtkWidget * h;
|
|
GtkWidget * e;
|
|
struct blocklist_data * data;
|
|
|
|
data = g_new0( struct blocklist_data, 1 );
|
|
data->core = TR_CORE( core );
|
|
|
|
t = hig_workarea_create( );
|
|
hig_workarea_add_section_title( t, &row, _( "Blocklist" ) );
|
|
|
|
b = new_check_button( _( "Enable _blocklist:" ), TR_PREFS_KEY_BLOCKLIST_ENABLED, core );
|
|
e = new_entry( TR_PREFS_KEY_BLOCKLIST_URL, core );
|
|
gtk_widget_set_size_request( e, 300, -1 );
|
|
hig_workarea_add_row_w( t, &row, b, e, NULL );
|
|
data->check = b;
|
|
g_signal_connect( b, "toggled", G_CALLBACK( target_cb ), e );
|
|
target_cb( b, e );
|
|
|
|
w = gtk_label_new( "" );
|
|
gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.5f );
|
|
updateBlocklistText( w, TR_CORE( core ) );
|
|
data->label = w;
|
|
h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
|
|
b = data->updateBlocklistButton = gtk_button_new_with_mnemonic( _( "_Update" ) );
|
|
g_object_set_data( G_OBJECT( b ), "session", tr_core_session( TR_CORE( core ) ) );
|
|
g_signal_connect( b, "clicked", G_CALLBACK( onBlocklistUpdate ), data );
|
|
g_signal_connect( data->check, "toggled", G_CALLBACK( target_cb ), b ); target_cb( data->check, b );
|
|
gtk_box_pack_start( GTK_BOX( h ), b, FALSE, FALSE, 0 );
|
|
g_signal_connect( data->check, "toggled", G_CALLBACK( target_cb ), w ); target_cb( data->check, w );
|
|
hig_workarea_add_wide_control( t, &row, h );
|
|
|
|
s = _( "Enable _automatic updates" );
|
|
w = new_check_button( s, PREF_KEY_BLOCKLIST_UPDATES_ENABLED, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
g_signal_connect( data->check, "toggled", G_CALLBACK( target_cb ), w ); target_cb( data->check, w );
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
hig_workarea_add_section_title ( t, &row, _( "Privacy" ) );
|
|
|
|
s = _( "_Encryption mode:" );
|
|
w = new_encryption_combo( core, "encryption" );
|
|
hig_workarea_add_row( t, &row, s, w, NULL );
|
|
|
|
s = _( "Use PE_X to find more peers" );
|
|
w = new_check_button( s, TR_PREFS_KEY_PEX_ENABLED, core );
|
|
s = _( "PEX is a tool for exchanging peer lists with the peers you're connected to." );
|
|
gtr_widget_set_tooltip_text( w, s );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Use _DHT to find more peers" );
|
|
w = new_check_button( s, TR_PREFS_KEY_DHT_ENABLED, core );
|
|
s = _( "DHT is a tool for finding peers without a tracker." );
|
|
gtr_widget_set_tooltip_text( w, s );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Use _Local Peer Discovery to find more peers" );
|
|
w = new_check_button( s, TR_PREFS_KEY_LPD_ENABLED, core );
|
|
s = _( "LPD is a tool for finding peers on your local network." );
|
|
gtr_widget_set_tooltip_text( w, s );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
hig_workarea_finish( t, &row );
|
|
g_object_weak_ref( G_OBJECT( t ), privacyPageDestroyed, data );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Web Tab
|
|
****/
|
|
|
|
enum
|
|
{
|
|
COL_ADDRESS,
|
|
N_COLS
|
|
};
|
|
|
|
static GtkTreeModel*
|
|
whitelist_tree_model_new( const char * whitelist )
|
|
{
|
|
int i;
|
|
char ** rules;
|
|
GtkListStore * store = gtk_list_store_new( N_COLS,
|
|
G_TYPE_STRING,
|
|
G_TYPE_STRING );
|
|
|
|
rules = g_strsplit( whitelist, ",", 0 );
|
|
|
|
for( i = 0; rules && rules[i]; ++i )
|
|
{
|
|
GtkTreeIter iter;
|
|
const char * s = rules[i];
|
|
while( isspace( *s ) ) ++s;
|
|
gtk_list_store_append( store, &iter );
|
|
gtk_list_store_set( store, &iter, COL_ADDRESS, s, -1 );
|
|
}
|
|
|
|
g_strfreev( rules );
|
|
return GTK_TREE_MODEL( store );
|
|
}
|
|
|
|
struct remote_page
|
|
{
|
|
TrCore * core;
|
|
GtkTreeView * view;
|
|
GtkListStore * store;
|
|
GtkWidget * remove_button;
|
|
GSList * widgets;
|
|
GSList * auth_widgets;
|
|
GSList * whitelist_widgets;
|
|
GtkToggleButton * rpc_tb;
|
|
GtkToggleButton * auth_tb;
|
|
GtkToggleButton * whitelist_tb;
|
|
};
|
|
|
|
static void
|
|
refreshWhitelist( struct remote_page * page )
|
|
{
|
|
GtkTreeIter iter;
|
|
GtkTreeModel * model = GTK_TREE_MODEL( page->store );
|
|
GString * gstr = g_string_new( NULL );
|
|
|
|
if( gtk_tree_model_get_iter_first( model, &iter ) ) do
|
|
{
|
|
char * address;
|
|
gtk_tree_model_get( model, &iter,
|
|
COL_ADDRESS, &address,
|
|
-1 );
|
|
g_string_append( gstr, address );
|
|
g_string_append( gstr, "," );
|
|
g_free( address );
|
|
}
|
|
while( gtk_tree_model_iter_next( model, &iter ) );
|
|
|
|
g_string_truncate( gstr, gstr->len - 1 ); /* remove the trailing comma */
|
|
|
|
tr_core_set_pref( page->core, TR_PREFS_KEY_RPC_WHITELIST, gstr->str );
|
|
|
|
g_string_free( gstr, TRUE );
|
|
}
|
|
|
|
static void
|
|
onAddressEdited( GtkCellRendererText * r UNUSED,
|
|
gchar * path_string,
|
|
gchar * address,
|
|
gpointer gpage )
|
|
{
|
|
GtkTreeIter iter;
|
|
struct remote_page * page = gpage;
|
|
GtkTreeModel * model = GTK_TREE_MODEL( page->store );
|
|
GtkTreePath * path = gtk_tree_path_new_from_string( path_string );
|
|
|
|
if( gtk_tree_model_get_iter( model, &iter, path ) )
|
|
gtk_list_store_set( page->store, &iter, COL_ADDRESS, address, -1 );
|
|
|
|
gtk_tree_path_free( path );
|
|
refreshWhitelist( page );
|
|
}
|
|
|
|
static void
|
|
onAddWhitelistClicked( GtkButton * b UNUSED,
|
|
gpointer gpage )
|
|
{
|
|
GtkTreeIter iter;
|
|
GtkTreePath * path;
|
|
struct remote_page * page = gpage;
|
|
|
|
gtk_list_store_append( page->store, &iter );
|
|
gtk_list_store_set( page->store, &iter,
|
|
COL_ADDRESS, "0.0.0.0",
|
|
-1 );
|
|
|
|
path = gtk_tree_model_get_path( GTK_TREE_MODEL( page->store ), &iter );
|
|
gtk_tree_view_set_cursor(
|
|
page->view, path,
|
|
gtk_tree_view_get_column( page->view, COL_ADDRESS ),
|
|
TRUE );
|
|
gtk_tree_path_free( path );
|
|
}
|
|
|
|
static void
|
|
onRemoveWhitelistClicked( GtkButton * b UNUSED,
|
|
gpointer gpage )
|
|
{
|
|
struct remote_page * page = gpage;
|
|
GtkTreeSelection * sel = gtk_tree_view_get_selection( page->view );
|
|
GtkTreeIter iter;
|
|
|
|
if( gtk_tree_selection_get_selected( sel, NULL, &iter ) )
|
|
{
|
|
gtk_list_store_remove( page->store, &iter );
|
|
refreshWhitelist( page );
|
|
}
|
|
}
|
|
|
|
static void
|
|
refreshRPCSensitivity( struct remote_page * page )
|
|
{
|
|
GSList * l;
|
|
const int rpc_active = gtk_toggle_button_get_active(
|
|
page->rpc_tb );
|
|
const int auth_active = gtk_toggle_button_get_active(
|
|
page->auth_tb );
|
|
const int whitelist_active = gtk_toggle_button_get_active(
|
|
page->whitelist_tb );
|
|
GtkTreeSelection * sel = gtk_tree_view_get_selection( page->view );
|
|
const int have_addr =
|
|
gtk_tree_selection_get_selected( sel, NULL,
|
|
NULL );
|
|
const int n_rules = gtk_tree_model_iter_n_children(
|
|
GTK_TREE_MODEL( page->store ), NULL );
|
|
|
|
for( l = page->widgets; l != NULL; l = l->next )
|
|
gtk_widget_set_sensitive( GTK_WIDGET( l->data ), rpc_active );
|
|
|
|
for( l = page->auth_widgets; l != NULL; l = l->next )
|
|
gtk_widget_set_sensitive( GTK_WIDGET(
|
|
l->data ), rpc_active && auth_active );
|
|
|
|
for( l = page->whitelist_widgets; l != NULL; l = l->next )
|
|
gtk_widget_set_sensitive( GTK_WIDGET( l->data ),
|
|
rpc_active && whitelist_active );
|
|
|
|
gtk_widget_set_sensitive( page->remove_button,
|
|
rpc_active && have_addr && n_rules > 1 );
|
|
}
|
|
|
|
static void
|
|
onRPCToggled( GtkToggleButton * tb UNUSED,
|
|
gpointer page )
|
|
{
|
|
refreshRPCSensitivity( page );
|
|
}
|
|
|
|
static void
|
|
onWhitelistSelectionChanged( GtkTreeSelection * sel UNUSED,
|
|
gpointer page )
|
|
{
|
|
refreshRPCSensitivity( page );
|
|
}
|
|
|
|
static void
|
|
onLaunchClutchCB( GtkButton * w UNUSED, gpointer data UNUSED )
|
|
{
|
|
const int port = gtr_pref_int_get( TR_PREFS_KEY_RPC_PORT );
|
|
char * uri = g_strdup_printf( "http://localhost:%d/transmission/web", port );
|
|
|
|
gtr_open_uri( uri );
|
|
g_free( uri );
|
|
}
|
|
|
|
static void
|
|
remotePageFree( gpointer gpage )
|
|
{
|
|
struct remote_page * page = gpage;
|
|
|
|
g_slist_free( page->widgets );
|
|
g_slist_free( page->auth_widgets );
|
|
g_slist_free( page->whitelist_widgets );
|
|
g_free( page );
|
|
}
|
|
|
|
static GtkWidget*
|
|
webPage( GObject * core )
|
|
{
|
|
const char * s;
|
|
int row = 0;
|
|
GtkWidget * t;
|
|
GtkWidget * w;
|
|
GtkWidget * h;
|
|
struct remote_page * page = g_new0( struct remote_page, 1 );
|
|
|
|
page->core = TR_CORE( core );
|
|
|
|
t = hig_workarea_create( );
|
|
g_object_set_data_full( G_OBJECT( t ), "page", page, remotePageFree );
|
|
|
|
hig_workarea_add_section_title( t, &row, _( "Web Client" ) );
|
|
|
|
/* "enabled" checkbutton */
|
|
s = _( "_Enable web client" );
|
|
w = new_check_button( s, TR_PREFS_KEY_RPC_ENABLED, core );
|
|
page->rpc_tb = GTK_TOGGLE_BUTTON( w );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
|
|
h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
|
|
w = gtk_button_new_with_mnemonic( _( "_Open web client" ) );
|
|
page->widgets = g_slist_append( page->widgets, w );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onLaunchClutchCB ), NULL );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
|
|
hig_workarea_add_wide_control( t, &row, h );
|
|
|
|
/* port */
|
|
w = new_spin_button( TR_PREFS_KEY_RPC_PORT, core, 0, USHRT_MAX, 1 );
|
|
page->widgets = g_slist_append( page->widgets, w );
|
|
w = hig_workarea_add_row( t, &row, _( "HTTP _port:" ), w, NULL );
|
|
page->widgets = g_slist_append( page->widgets, w );
|
|
|
|
/* require authentication */
|
|
s = _( "Use _authentication" );
|
|
w = new_check_button( s, TR_PREFS_KEY_RPC_AUTH_REQUIRED, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
page->auth_tb = GTK_TOGGLE_BUTTON( w );
|
|
page->widgets = g_slist_append( page->widgets, w );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
|
|
|
|
/* username */
|
|
s = _( "_Username:" );
|
|
w = new_entry( TR_PREFS_KEY_RPC_USERNAME, core );
|
|
page->auth_widgets = g_slist_append( page->auth_widgets, w );
|
|
w = hig_workarea_add_row( t, &row, s, w, NULL );
|
|
page->auth_widgets = g_slist_append( page->auth_widgets, w );
|
|
|
|
/* password */
|
|
s = _( "Pass_word:" );
|
|
w = new_entry( TR_PREFS_KEY_RPC_PASSWORD, core );
|
|
gtk_entry_set_visibility( GTK_ENTRY( w ), FALSE );
|
|
page->auth_widgets = g_slist_append( page->auth_widgets, w );
|
|
w = hig_workarea_add_row( t, &row, s, w, NULL );
|
|
page->auth_widgets = g_slist_append( page->auth_widgets, w );
|
|
|
|
/* require authentication */
|
|
s = _( "Only allow these IP a_ddresses to connect:" );
|
|
w = new_check_button( s, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
page->whitelist_tb = GTK_TOGGLE_BUTTON( w );
|
|
page->widgets = g_slist_append( page->widgets, w );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onRPCToggled ), page );
|
|
|
|
/* access control list */
|
|
{
|
|
const char * val = gtr_pref_string_get( TR_PREFS_KEY_RPC_WHITELIST );
|
|
GtkTreeModel * m = whitelist_tree_model_new( val );
|
|
GtkTreeViewColumn * c;
|
|
GtkCellRenderer * r;
|
|
GtkTreeSelection * sel;
|
|
GtkTreeView * v;
|
|
GtkWidget * w;
|
|
GtkWidget * h;
|
|
|
|
page->store = GTK_LIST_STORE( m );
|
|
w = gtk_tree_view_new_with_model( m );
|
|
g_signal_connect( w, "button-release-event",
|
|
G_CALLBACK( on_tree_view_button_released ), NULL );
|
|
|
|
page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
|
|
v = page->view = GTK_TREE_VIEW( w );
|
|
gtr_widget_set_tooltip_text( w, _( "IP addresses may use wildcards, such as 192.168.*.*" ) );
|
|
sel = gtk_tree_view_get_selection( v );
|
|
g_signal_connect( sel, "changed",
|
|
G_CALLBACK( onWhitelistSelectionChanged ), page );
|
|
g_object_unref( G_OBJECT( m ) );
|
|
gtk_tree_view_set_headers_visible( v, TRUE );
|
|
w = gtk_frame_new( NULL );
|
|
gtk_frame_set_shadow_type( GTK_FRAME( w ), GTK_SHADOW_IN );
|
|
gtk_container_add( GTK_CONTAINER( w ), GTK_WIDGET( v ) );
|
|
|
|
/* ip address column */
|
|
r = gtk_cell_renderer_text_new( );
|
|
g_signal_connect( r, "edited",
|
|
G_CALLBACK( onAddressEdited ), page );
|
|
g_object_set( G_OBJECT( r ), "editable", TRUE, NULL );
|
|
c = gtk_tree_view_column_new_with_attributes( NULL, r,
|
|
"text", COL_ADDRESS,
|
|
NULL );
|
|
gtk_tree_view_column_set_expand( c, TRUE );
|
|
gtk_tree_view_append_column( v, c );
|
|
gtk_tree_view_set_headers_visible( v, FALSE );
|
|
|
|
s = _( "Addresses:" );
|
|
w = hig_workarea_add_row( t, &row, s, w, NULL );
|
|
gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.0f );
|
|
gtk_misc_set_padding( GTK_MISC( w ), 0, GUI_PAD );
|
|
page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
|
|
|
|
h = gtk_hbox_new( TRUE, GUI_PAD );
|
|
w = gtk_button_new_from_stock( GTK_STOCK_REMOVE );
|
|
g_signal_connect( w, "clicked", G_CALLBACK(
|
|
onRemoveWhitelistClicked ), page );
|
|
page->remove_button = w;
|
|
onWhitelistSelectionChanged( sel, page );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
|
|
w = gtk_button_new_from_stock( GTK_STOCK_ADD );
|
|
page->whitelist_widgets = g_slist_append( page->whitelist_widgets, w );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onAddWhitelistClicked ), page );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
|
|
w = gtk_hbox_new( FALSE, 0 );
|
|
gtk_box_pack_start( GTK_BOX( w ), gtk_alignment_new( 0, 0, 0, 0 ),
|
|
TRUE, TRUE, 0 );
|
|
gtk_box_pack_start( GTK_BOX( w ), h, FALSE, FALSE, 0 );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
}
|
|
|
|
refreshRPCSensitivity( page );
|
|
hig_workarea_finish( t, &row );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Bandwidth Tab
|
|
****/
|
|
|
|
struct BandwidthPage
|
|
{
|
|
TrCore * core;
|
|
GSList * sched_widgets;
|
|
};
|
|
|
|
static void
|
|
refreshSchedSensitivity( struct BandwidthPage * p )
|
|
{
|
|
GSList * l;
|
|
const gboolean sched_enabled = gtr_pref_flag_get( TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED );
|
|
|
|
for( l = p->sched_widgets; l != NULL; l = l->next )
|
|
gtk_widget_set_sensitive( GTK_WIDGET( l->data ), sched_enabled );
|
|
}
|
|
|
|
static void
|
|
onSchedToggled( GtkToggleButton * tb UNUSED,
|
|
gpointer user_data )
|
|
{
|
|
refreshSchedSensitivity( user_data );
|
|
}
|
|
|
|
static void
|
|
onTimeComboChanged( GtkComboBox * w,
|
|
gpointer core )
|
|
{
|
|
GtkTreeIter iter;
|
|
|
|
if( gtk_combo_box_get_active_iter( w, &iter ) )
|
|
{
|
|
const char * key = g_object_get_data( G_OBJECT( w ), PREF_KEY );
|
|
int val = 0;
|
|
gtk_tree_model_get( gtk_combo_box_get_model(
|
|
w ), &iter, 0, &val, -1 );
|
|
tr_core_set_pref_int( TR_CORE( core ), key, val );
|
|
}
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_time_combo( GObject * core,
|
|
const char * key )
|
|
{
|
|
int val;
|
|
int i;
|
|
GtkWidget * w;
|
|
GtkCellRenderer * r;
|
|
GtkListStore * store;
|
|
|
|
/* build a store at 15 minute intervals */
|
|
store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING );
|
|
for( i = 0; i < 60 * 24; i += 15 )
|
|
{
|
|
char buf[128];
|
|
GtkTreeIter iter;
|
|
struct tm tm;
|
|
tm.tm_hour = i / 60;
|
|
tm.tm_min = i % 60;
|
|
tm.tm_sec = 0;
|
|
strftime( buf, sizeof( buf ), "%H:%M", &tm );
|
|
gtk_list_store_append( store, &iter );
|
|
gtk_list_store_set( store, &iter, 0, i, 1, buf, -1 );
|
|
}
|
|
|
|
/* build the widget */
|
|
w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) );
|
|
gtk_combo_box_set_wrap_width( GTK_COMBO_BOX( w ), 4 );
|
|
r = gtk_cell_renderer_text_new( );
|
|
gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE );
|
|
gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT(
|
|
w ), r, "text", 1, NULL );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup(
|
|
key ), g_free );
|
|
val = gtr_pref_int_get( key );
|
|
gtk_combo_box_set_active( GTK_COMBO_BOX( w ), val / ( 15 ) );
|
|
g_signal_connect( w, "changed", G_CALLBACK( onTimeComboChanged ), core );
|
|
|
|
/* cleanup */
|
|
g_object_unref( G_OBJECT( store ) );
|
|
return w;
|
|
}
|
|
|
|
static GtkWidget*
|
|
new_week_combo( GObject * core, const char * key )
|
|
{
|
|
GtkWidget * w = gtr_combo_box_new_enum( _( "Every Day" ), TR_SCHED_ALL,
|
|
_( "Weekdays" ), TR_SCHED_WEEKDAY,
|
|
_( "Weekends" ), TR_SCHED_WEEKEND,
|
|
_( "Sunday" ), TR_SCHED_SUN,
|
|
_( "Monday" ), TR_SCHED_MON,
|
|
_( "Tuesday" ), TR_SCHED_TUES,
|
|
_( "Wednesday" ), TR_SCHED_WED,
|
|
_( "Thursday" ), TR_SCHED_THURS,
|
|
_( "Friday" ), TR_SCHED_FRI,
|
|
_( "Saturday" ), TR_SCHED_SAT,
|
|
NULL );
|
|
gtr_combo_box_set_active_enum( GTK_COMBO_BOX( w ), gtr_pref_int_get( key ) );
|
|
g_object_set_data_full( G_OBJECT( w ), PREF_KEY, tr_strdup( key ), g_free );
|
|
g_signal_connect( w, "changed", G_CALLBACK( onIntComboChanged ), core );
|
|
return w;
|
|
}
|
|
|
|
static void
|
|
bandwidthPageFree( gpointer gpage )
|
|
{
|
|
struct BandwidthPage * page = gpage;
|
|
|
|
g_slist_free( page->sched_widgets );
|
|
g_free( page );
|
|
}
|
|
|
|
static GtkWidget*
|
|
bandwidthPage( GObject * core )
|
|
{
|
|
int row = 0;
|
|
const char * s;
|
|
GtkWidget * t;
|
|
GtkWidget * l;
|
|
GtkWidget * w, * w2, * h;
|
|
char buf[512];
|
|
struct BandwidthPage * page = tr_new0( struct BandwidthPage, 1 );
|
|
|
|
page->core = TR_CORE( core );
|
|
|
|
t = hig_workarea_create( );
|
|
hig_workarea_add_section_title( t, &row, _( "Speed Limits" ) );
|
|
|
|
g_snprintf( buf, sizeof( buf ), _( "_Upload (%s):" ), _(speed_K_str) );
|
|
w = new_check_button( buf, TR_PREFS_KEY_USPEED_ENABLED, core );
|
|
w2 = new_spin_button( TR_PREFS_KEY_USPEED_KBps, core, 0, INT_MAX, 5 );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w2 ), gtr_pref_flag_get( TR_PREFS_KEY_USPEED_ENABLED ) );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
|
|
hig_workarea_add_row_w( t, &row, w, w2, NULL );
|
|
|
|
g_snprintf( buf, sizeof( buf ), _( "_Download (%s):" ), _(speed_K_str) );
|
|
w = new_check_button( buf, TR_PREFS_KEY_DSPEED_ENABLED, core );
|
|
w2 = new_spin_button( TR_PREFS_KEY_DSPEED_KBps, core, 0, INT_MAX, 5 );
|
|
gtk_widget_set_sensitive( GTK_WIDGET( w2 ), gtr_pref_flag_get( TR_PREFS_KEY_DSPEED_ENABLED ) );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( target_cb ), w2 );
|
|
hig_workarea_add_row_w( t, &row, w, w2, NULL );
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
h = gtk_hbox_new( FALSE, GUI_PAD );
|
|
w = gtk_image_new_from_stock( "alt-speed-on", -1 );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
|
|
g_snprintf( buf, sizeof( buf ), "<b>%s</b>", _( "Alternative Speed Limits" ) );
|
|
w = gtk_label_new( buf );
|
|
gtk_misc_set_alignment( GTK_MISC( w ), 0.0f, 0.5f );
|
|
gtk_label_set_use_markup( GTK_LABEL( w ), TRUE );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
|
|
hig_workarea_add_section_title_widget( t, &row, h );
|
|
|
|
s = _( "Override normal speed limits manually or at scheduled times" );
|
|
g_snprintf( buf, sizeof( buf ), "<small>%s</small>", s );
|
|
w = gtk_label_new( buf );
|
|
gtk_label_set_use_markup( GTK_LABEL( w ), TRUE );
|
|
gtk_misc_set_alignment( GTK_MISC( w ), 0.5f, 0.5f );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
g_snprintf( buf, sizeof( buf ), _( "U_pload (%s):" ), _(speed_K_str) );
|
|
w = new_spin_button( TR_PREFS_KEY_ALT_SPEED_UP_KBps, core, 0, INT_MAX, 5 );
|
|
hig_workarea_add_row( t, &row, buf, w, NULL );
|
|
|
|
g_snprintf( buf, sizeof( buf ), _( "Do_wnload (%s):" ), _(speed_K_str) );
|
|
w = new_spin_button( TR_PREFS_KEY_ALT_SPEED_DOWN_KBps, core, 0, INT_MAX, 5 );
|
|
hig_workarea_add_row( t, &row, buf, w, NULL );
|
|
|
|
s = _( "_Scheduled times:" );
|
|
h = gtk_hbox_new( FALSE, 0 );
|
|
w2 = new_time_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN );
|
|
page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
|
|
gtk_box_pack_start( GTK_BOX( h ), w2, TRUE, TRUE, 0 );
|
|
w2 = l = gtk_label_new_with_mnemonic ( _( " _to " ) );
|
|
page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
|
|
gtk_box_pack_start( GTK_BOX( h ), w2, FALSE, FALSE, 0 );
|
|
w2 = new_time_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_END );
|
|
gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w2 );
|
|
page->sched_widgets = g_slist_append( page->sched_widgets, w2 );
|
|
gtk_box_pack_start( GTK_BOX( h ), w2, TRUE, TRUE, 0 );
|
|
w = new_check_button( s, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED, core );
|
|
g_signal_connect( w, "toggled", G_CALLBACK( onSchedToggled ), page );
|
|
hig_workarea_add_row_w( t, &row, w, h, NULL );
|
|
|
|
s = _( "_On days:" );
|
|
w = new_week_combo( core, TR_PREFS_KEY_ALT_SPEED_TIME_DAY );
|
|
page->sched_widgets = g_slist_append( page->sched_widgets, w );
|
|
w = hig_workarea_add_row( t, &row, s, w, NULL );
|
|
page->sched_widgets = g_slist_append( page->sched_widgets, w );
|
|
|
|
hig_workarea_finish( t, &row );
|
|
g_object_set_data_full( G_OBJECT( t ), "page", page, bandwidthPageFree );
|
|
|
|
refreshSchedSensitivity( page );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Network Tab
|
|
****/
|
|
|
|
struct network_page_data
|
|
{
|
|
TrCore * core;
|
|
GtkWidget * portLabel;
|
|
GtkWidget * portButton;
|
|
GtkWidget * portSpin;
|
|
gulong portTag;
|
|
gulong prefsTag;
|
|
};
|
|
|
|
static void
|
|
onCorePrefsChanged( TrCore * core UNUSED, const char * key, gpointer gdata )
|
|
{
|
|
if( !strcmp( key, TR_PREFS_KEY_PEER_PORT ) )
|
|
{
|
|
struct network_page_data * data = gdata;
|
|
gdk_threads_enter();
|
|
gtr_label_set_text( GTK_LABEL( data->portLabel ), _( "Status unknown" ) );
|
|
gtk_widget_set_sensitive( data->portButton, TRUE );
|
|
gtk_widget_set_sensitive( data->portSpin, TRUE );
|
|
gdk_threads_leave();
|
|
}
|
|
}
|
|
|
|
static void
|
|
networkPageDestroyed( gpointer gdata, GObject * dead UNUSED )
|
|
{
|
|
struct network_page_data * data = gdata;
|
|
if( data->prefsTag > 0 )
|
|
g_signal_handler_disconnect( data->core, data->prefsTag );
|
|
if( data->portTag > 0 )
|
|
g_signal_handler_disconnect( data->core, data->portTag );
|
|
g_free( data );
|
|
}
|
|
|
|
static void
|
|
onPortTested( TrCore * core UNUSED, gboolean isOpen, gpointer vdata )
|
|
{
|
|
struct network_page_data * data = vdata;
|
|
const char * markup = isOpen ? _( "Port is <b>open</b>" ) : _( "Port is <b>closed</b>" );
|
|
gdk_threads_enter();
|
|
gtk_label_set_markup( GTK_LABEL( data->portLabel ), markup );
|
|
gtk_widget_set_sensitive( data->portButton, TRUE );
|
|
gtk_widget_set_sensitive( data->portSpin, TRUE );
|
|
gdk_threads_leave();
|
|
}
|
|
|
|
static void
|
|
onPortTest( GtkButton * button UNUSED, gpointer vdata )
|
|
{
|
|
struct network_page_data * data = vdata;
|
|
gtk_widget_set_sensitive( data->portButton, FALSE );
|
|
gtk_widget_set_sensitive( data->portSpin, FALSE );
|
|
gtk_label_set_markup( GTK_LABEL( data->portLabel ), _( "<i>Testing...</i>" ) );
|
|
if( !data->portTag )
|
|
data->portTag = g_signal_connect( data->core, "port-tested", G_CALLBACK(onPortTested), data );
|
|
tr_core_port_test( data->core );
|
|
}
|
|
|
|
static void
|
|
onGNOMEClicked( GtkButton * button, gpointer vdata UNUSED )
|
|
{
|
|
GError * err = NULL;
|
|
|
|
if( !g_spawn_command_line_async( "gnome-network-properties", &err ) )
|
|
{
|
|
GtkWidget * d = gtk_message_dialog_new( GTK_WINDOW( gtk_widget_get_toplevel( GTK_WIDGET( button ) ) ),
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
GTK_MESSAGE_ERROR,
|
|
GTK_BUTTONS_CLOSE,
|
|
"%s", err->message );
|
|
g_signal_connect_swapped( d, "response", G_CALLBACK( gtk_widget_destroy ), d );
|
|
gtk_widget_show( d );
|
|
g_clear_error( &err );
|
|
}
|
|
}
|
|
|
|
static GtkWidget*
|
|
networkPage( GObject * core )
|
|
{
|
|
int row = 0;
|
|
const char * s;
|
|
GtkWidget * t;
|
|
GtkWidget * w;
|
|
GtkWidget * h;
|
|
GtkWidget * l;
|
|
struct network_page_data * data;
|
|
|
|
/* register to stop listening to core prefs changes when the page is destroyed */
|
|
data = g_new0( struct network_page_data, 1 );
|
|
data->core = TR_CORE( core );
|
|
|
|
/* build the page */
|
|
t = hig_workarea_create( );
|
|
hig_workarea_add_section_title( t, &row, _( "Listening Port" ) );
|
|
|
|
s = _( "_Port used for incoming connections:" );
|
|
w = data->portSpin = new_spin_button( TR_PREFS_KEY_PEER_PORT, core, 1, USHRT_MAX, 1 );
|
|
hig_workarea_add_row( t, &row, s, w, NULL );
|
|
|
|
h = gtk_hbox_new( FALSE, GUI_PAD_BIG );
|
|
l = data->portLabel = gtk_label_new( _( "Status unknown" ) );
|
|
gtk_misc_set_alignment( GTK_MISC( l ), 0.0f, 0.5f );
|
|
gtk_box_pack_start( GTK_BOX( h ), l, TRUE, TRUE, 0 );
|
|
w = data->portButton = gtk_button_new_with_mnemonic( _( "Te_st Port" ) );
|
|
gtk_box_pack_end( GTK_BOX( h ), w, FALSE, FALSE, 0 );
|
|
g_signal_connect( w, "clicked", G_CALLBACK(onPortTest), data );
|
|
hig_workarea_add_row( t, &row, NULL, h, NULL );
|
|
data->prefsTag = g_signal_connect( TR_CORE( core ), "prefs-changed", G_CALLBACK( onCorePrefsChanged ), data );
|
|
g_object_weak_ref( G_OBJECT( t ), networkPageDestroyed, data );
|
|
|
|
s = _( "Pick a _random port every time Transmission is started" );
|
|
w = new_check_button( s, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
s = _( "Use UPnP or NAT-PMP port _forwarding from my router" );
|
|
w = new_check_button( s, TR_PREFS_KEY_PORT_FORWARDING, core );
|
|
hig_workarea_add_wide_control( t, &row, w );
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
hig_workarea_add_section_title( t, &row, _( "Peer Limits" ) );
|
|
|
|
w = new_spin_button( TR_PREFS_KEY_PEER_LIMIT_TORRENT, core, 1, 300, 5 );
|
|
hig_workarea_add_row( t, &row, _( "Maximum peers per _torrent:" ), w, NULL );
|
|
w = new_spin_button( TR_PREFS_KEY_PEER_LIMIT_GLOBAL, core, 1, 3000, 5 );
|
|
hig_workarea_add_row( t, &row, _( "Maximum peers _overall:" ), w, NULL );
|
|
|
|
hig_workarea_add_section_divider( t, &row );
|
|
hig_workarea_add_section_title( t, &row, _( "Options" ) );
|
|
|
|
w = gtk_button_new_with_mnemonic( _( "Edit GNOME Proxy Settings" ) );
|
|
g_signal_connect( w, "clicked", G_CALLBACK( onGNOMEClicked ), data );
|
|
h = gtk_hbox_new( FALSE, 0 );
|
|
gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
|
|
hig_workarea_add_wide_control( t, &row, h );
|
|
|
|
hig_workarea_finish( t, &row );
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
*****
|
|
****/
|
|
|
|
GtkWidget *
|
|
gtr_prefs_dialog_new( GtkWindow * parent, GObject * core )
|
|
{
|
|
GtkWidget * d;
|
|
GtkWidget * n;
|
|
|
|
d = gtk_dialog_new_with_buttons( _(
|
|
"Transmission Preferences" ),
|
|
parent,
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
GTK_STOCK_HELP, GTK_RESPONSE_HELP,
|
|
GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
|
|
NULL );
|
|
gtk_window_set_role( GTK_WINDOW( d ), "transmission-preferences-dialog" );
|
|
gtk_container_set_border_width( GTK_CONTAINER( d ), GUI_PAD );
|
|
|
|
n = gtk_notebook_new( );
|
|
gtk_container_set_border_width ( GTK_CONTAINER ( n ), GUI_PAD );
|
|
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
torrentPage( core ),
|
|
gtk_label_new ( _( "Torrents" ) ) );
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
bandwidthPage( core ),
|
|
gtk_label_new ( _( "Speed" ) ) );
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
privacyPage( core ),
|
|
gtk_label_new ( _( "Privacy" ) ) );
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
networkPage( core ),
|
|
gtk_label_new ( _( "Network" ) ) );
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
desktopPage( core ),
|
|
gtk_label_new ( _( "Desktop" ) ) );
|
|
gtk_notebook_append_page( GTK_NOTEBOOK( n ),
|
|
webPage( core ),
|
|
gtk_label_new ( _( "Web" ) ) );
|
|
|
|
g_signal_connect( d, "response", G_CALLBACK( response_cb ), core );
|
|
gtr_dialog_set_content( GTK_DIALOG( d ), n );
|
|
return d;
|
|
}
|
|
|