1 /*
2  * This file Copyright (C) 2007-2014 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <ctype.h> /* isspace */
10 #include <limits.h> /* USHRT_MAX, INT_MAX */
11 #include <unistd.h>
12 #include <glib/gi18n.h>
13 #include <gtk/gtk.h>
14 #include <libtransmission/transmission.h>
15 #include <libtransmission/utils.h>
16 #include <libtransmission/version.h>
17 #include "conf.h"
18 #include "hig.h"
19 #include "tr-core.h"
20 #include "tr-prefs.h"
21 #include "util.h"
22 
23 /**
24 ***
25 **/
26 
27 struct prefs_dialog_data
28 {
29     TrCore* core;
30     gulong core_prefs_tag;
31 
32     GtkWidget* freespace_label;
33 
34     GtkWidget* port_label;
35     GtkWidget* port_button;
36     GtkWidget* port_spin;
37 };
38 
39 /**
40 ***
41 **/
42 
43 #define PREF_KEY "pref-key"
44 
response_cb(GtkDialog * dialog,int response,gpointer unused UNUSED)45 static void response_cb(GtkDialog* dialog, int response, gpointer unused UNUSED)
46 {
47     if (response == GTK_RESPONSE_HELP)
48     {
49         char* uri = g_strconcat(gtr_get_help_uri(), "/html/preferences.html", NULL);
50         gtr_open_uri(uri);
51         g_free(uri);
52     }
53 
54     if (response == GTK_RESPONSE_CLOSE)
55     {
56         gtk_widget_destroy(GTK_WIDGET(dialog));
57     }
58 }
59 
toggled_cb(GtkToggleButton * w,gpointer core)60 static void toggled_cb(GtkToggleButton* w, gpointer core)
61 {
62     tr_quark const key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w), PREF_KEY));
63     gboolean const flag = gtk_toggle_button_get_active(w);
64 
65     gtr_core_set_pref_bool(TR_CORE(core), key, flag);
66 }
67 
new_check_button(char const * mnemonic,tr_quark const key,gpointer core)68 static GtkWidget* new_check_button(char const* mnemonic, tr_quark const key, gpointer core)
69 {
70     GtkWidget* w = gtk_check_button_new_with_mnemonic(mnemonic);
71     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
72     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), gtr_pref_flag_get(key));
73     g_signal_connect(w, "toggled", G_CALLBACK(toggled_cb), core);
74     return w;
75 }
76 
77 #define IDLE_DATA "idle-data"
78 
79 struct spin_idle_data
80 {
81     gpointer core;
82     GTimer* last_change;
83     gboolean isDouble;
84 };
85 
spin_idle_data_free(gpointer gdata)86 static void spin_idle_data_free(gpointer gdata)
87 {
88     struct spin_idle_data* data = gdata;
89 
90     g_timer_destroy(data->last_change);
91     g_free(data);
92 }
93 
spun_cb_idle(gpointer spin)94 static gboolean spun_cb_idle(gpointer spin)
95 {
96     gboolean keep_waiting = TRUE;
97     GObject* o = G_OBJECT(spin);
98     struct spin_idle_data* data = g_object_get_data(o, IDLE_DATA);
99 
100     /* has the user stopped making changes? */
101     if (g_timer_elapsed(data->last_change, NULL) > 0.33F)
102     {
103         /* update the core */
104         tr_quark const key = GPOINTER_TO_INT(g_object_get_data(o, PREF_KEY));
105 
106         if (data->isDouble)
107         {
108             double const value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));
109             gtr_core_set_pref_double(TR_CORE(data->core), key, value);
110         }
111         else
112         {
113             int const value = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin));
114             gtr_core_set_pref_int(TR_CORE(data->core), key, value);
115         }
116 
117         /* cleanup */
118         g_object_set_data(o, IDLE_DATA, NULL);
119         keep_waiting = FALSE;
120         g_object_unref(G_OBJECT(o));
121     }
122 
123     return keep_waiting;
124 }
125 
spun_cb(GtkSpinButton * w,gpointer core,gboolean isDouble)126 static void spun_cb(GtkSpinButton* w, gpointer core, gboolean isDouble)
127 {
128     /* user may be spinning through many values, so let's hold off
129        for a moment to keep from flooding the core with changes */
130     GObject* o = G_OBJECT(w);
131     struct spin_idle_data* data = g_object_get_data(o, IDLE_DATA);
132 
133     if (data == NULL)
134     {
135         data = g_new(struct spin_idle_data, 1);
136         data->core = core;
137         data->last_change = g_timer_new();
138         data->isDouble = isDouble;
139         g_object_set_data_full(o, IDLE_DATA, data, spin_idle_data_free);
140         g_object_ref(G_OBJECT(o));
141         gdk_threads_add_timeout_seconds(1, spun_cb_idle, w);
142     }
143 
144     g_timer_start(data->last_change);
145 }
146 
spun_cb_int(GtkSpinButton * w,gpointer core)147 static void spun_cb_int(GtkSpinButton* w, gpointer core)
148 {
149     spun_cb(w, core, FALSE);
150 }
151 
spun_cb_double(GtkSpinButton * w,gpointer core)152 static void spun_cb_double(GtkSpinButton* w, gpointer core)
153 {
154     spun_cb(w, core, TRUE);
155 }
156 
new_spin_button(tr_quark const key,gpointer core,int low,int high,int step)157 static GtkWidget* new_spin_button(tr_quark const key, gpointer core, int low, int high, int step)
158 {
159     GtkWidget* w = gtk_spin_button_new_with_range(low, high, step);
160     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
161     gtk_spin_button_set_digits(GTK_SPIN_BUTTON(w), 0);
162     gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), gtr_pref_int_get(key));
163     g_signal_connect(w, "value-changed", G_CALLBACK(spun_cb_int), core);
164     return w;
165 }
166 
new_spin_button_double(tr_quark const key,gpointer core,double low,double high,double step)167 static GtkWidget* new_spin_button_double(tr_quark const key, gpointer core, double low, double high, double step)
168 {
169     GtkWidget* w = gtk_spin_button_new_with_range(low, high, step);
170     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
171     gtk_spin_button_set_digits(GTK_SPIN_BUTTON(w), 2);
172     gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), gtr_pref_double_get(key));
173     g_signal_connect(w, "value-changed", G_CALLBACK(spun_cb_double), core);
174     return w;
175 }
176 
entry_changed_cb(GtkEntry * w,gpointer core)177 static void entry_changed_cb(GtkEntry* w, gpointer core)
178 {
179     tr_quark const key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w), PREF_KEY));
180     char const* value = gtk_entry_get_text(w);
181 
182     gtr_core_set_pref(TR_CORE(core), key, value);
183 }
184 
new_entry(tr_quark const key,gpointer core)185 static GtkWidget* new_entry(tr_quark const key, gpointer core)
186 {
187     GtkWidget* w = gtk_entry_new();
188     char const* value = gtr_pref_string_get(key);
189 
190     if (value != NULL)
191     {
192         gtk_entry_set_text(GTK_ENTRY(w), value);
193     }
194 
195     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
196     g_signal_connect(w, "changed", G_CALLBACK(entry_changed_cb), core);
197     return w;
198 }
199 
chosen_cb(GtkFileChooser * w,gpointer core)200 static void chosen_cb(GtkFileChooser* w, gpointer core)
201 {
202     tr_quark const key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w), PREF_KEY));
203     char* value = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(w));
204     gtr_core_set_pref(TR_CORE(core), key, value);
205     g_free(value);
206 }
207 
new_path_chooser_button(tr_quark const key,gpointer core)208 static GtkWidget* new_path_chooser_button(tr_quark const key, gpointer core)
209 {
210     GtkWidget* w = gtk_file_chooser_button_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
211     char const* path = gtr_pref_string_get(key);
212     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
213 
214     if (path != NULL)
215     {
216         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(w), path);
217     }
218 
219     g_signal_connect(w, "selection-changed", G_CALLBACK(chosen_cb), core);
220     return w;
221 }
222 
new_file_chooser_button(tr_quark const key,gpointer core)223 static GtkWidget* new_file_chooser_button(tr_quark const key, gpointer core)
224 {
225     GtkWidget* w = gtk_file_chooser_button_new(NULL, GTK_FILE_CHOOSER_ACTION_OPEN);
226     char const* path = gtr_pref_string_get(key);
227     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
228 
229     if (path != NULL)
230     {
231         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(w), path);
232     }
233 
234     g_signal_connect(w, "selection-changed", G_CALLBACK(chosen_cb), core);
235     return w;
236 }
237 
target_cb(GtkWidget * tb,gpointer target)238 static void target_cb(GtkWidget* tb, gpointer target)
239 {
240     gboolean const b = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
241 
242     gtk_widget_set_sensitive(GTK_WIDGET(target), b);
243 }
244 
245 /****
246 *****  Download Tab
247 ****/
248 
downloadingPage(GObject * core,struct prefs_dialog_data * data)249 static GtkWidget* downloadingPage(GObject* core, struct prefs_dialog_data* data)
250 {
251     GtkWidget* t;
252     GtkWidget* w;
253     GtkWidget* l;
254     char const* s;
255     guint row = 0;
256 
257     t = hig_workarea_create();
258     hig_workarea_add_section_title(t, &row, C_("Gerund", "Adding"));
259 
260     s = _("Automatically add .torrent files _from:");
261     l = new_check_button(s, TR_KEY_watch_dir_enabled, core);
262     w = new_path_chooser_button(TR_KEY_watch_dir, core);
263     gtk_widget_set_sensitive(GTK_WIDGET(w), gtr_pref_flag_get(TR_KEY_watch_dir_enabled));
264     g_signal_connect(l, "toggled", G_CALLBACK(target_cb), w);
265     hig_workarea_add_row_w(t, &row, l, w, NULL);
266 
267     s = _("Show the Torrent Options _dialog");
268     w = new_check_button(s, TR_KEY_show_options_window, core);
269     hig_workarea_add_wide_control(t, &row, w);
270 
271     s = _("_Start added torrents");
272     w = new_check_button(s, TR_KEY_start_added_torrents, core);
273     hig_workarea_add_wide_control(t, &row, w);
274 
275     s = _("Mo_ve .torrent file to the trash");
276     w = new_check_button(s, TR_KEY_trash_original_torrent_files, core);
277     hig_workarea_add_wide_control(t, &row, w);
278 
279     w = new_path_chooser_button(TR_KEY_download_dir, core);
280     hig_workarea_add_row(t, &row, _("Save to _Location:"), w, NULL);
281 
282     l = data->freespace_label = gtr_freespace_label_new(TR_CORE(core), NULL);
283     g_object_set(l, "halign", GTK_ALIGN_END, "valign", GTK_ALIGN_CENTER, NULL);
284     hig_workarea_add_wide_control(t, &row, l);
285 
286     hig_workarea_add_section_divider(t, &row);
287     hig_workarea_add_section_title(t, &row, _("Download Queue"));
288 
289     s = _("Ma_ximum active downloads:");
290     w = new_spin_button(TR_KEY_download_queue_size, core, 0, INT_MAX, 1);
291     hig_workarea_add_row(t, &row, s, w, NULL);
292 
293     s = _("Downloads sharing data in the last _N minutes are active:");
294     w = new_spin_button(TR_KEY_queue_stalled_minutes, core, 1, INT_MAX, 15);
295     hig_workarea_add_row(t, &row, s, w, NULL);
296 
297     hig_workarea_add_section_divider(t, &row);
298     hig_workarea_add_section_title(t, &row, _("Incomplete"));
299 
300     s = _("Append \"._part\" to incomplete files' names");
301     w = new_check_button(s, TR_KEY_rename_partial_files, core);
302     hig_workarea_add_wide_control(t, &row, w);
303 
304     s = _("Keep _incomplete torrents in:");
305     l = new_check_button(s, TR_KEY_incomplete_dir_enabled, core);
306     w = new_path_chooser_button(TR_KEY_incomplete_dir, core);
307     gtk_widget_set_sensitive(GTK_WIDGET(w), gtr_pref_flag_get(TR_KEY_incomplete_dir_enabled));
308     g_signal_connect(l, "toggled", G_CALLBACK(target_cb), w);
309     hig_workarea_add_row_w(t, &row, l, w, NULL);
310 
311     s = _("Call scrip_t when torrent is completed:");
312     l = new_check_button(s, TR_KEY_script_torrent_done_enabled, core);
313     w = new_file_chooser_button(TR_KEY_script_torrent_done_filename, core);
314     gtk_widget_set_sensitive(GTK_WIDGET(w), gtr_pref_flag_get(TR_KEY_script_torrent_done_enabled));
315     g_signal_connect(l, "toggled", G_CALLBACK(target_cb), w);
316     hig_workarea_add_row_w(t, &row, l, w, NULL);
317 
318     return t;
319 }
320 
321 /****
322 *****  Torrent Tab
323 ****/
324 
seedingPage(GObject * core)325 static GtkWidget* seedingPage(GObject* core)
326 {
327     GtkWidget* t;
328     GtkWidget* w;
329     GtkWidget* w2;
330     char const* s;
331     guint row = 0;
332 
333     t = hig_workarea_create();
334     hig_workarea_add_section_title(t, &row, _("Limits"));
335 
336     s = _("Stop seeding at _ratio:");
337     w = new_check_button(s, TR_KEY_ratio_limit_enabled, core);
338     w2 = new_spin_button_double(TR_KEY_ratio_limit, core, 0, 1000, .05);
339     gtk_widget_set_sensitive(GTK_WIDGET(w2), gtr_pref_flag_get(TR_KEY_ratio_limit_enabled));
340     g_signal_connect(w, "toggled", G_CALLBACK(target_cb), w2);
341     hig_workarea_add_row_w(t, &row, w, w2, NULL);
342 
343     s = _("Stop seeding if idle for _N minutes:");
344     w = new_check_button(s, TR_KEY_idle_seeding_limit_enabled, core);
345     w2 = new_spin_button(TR_KEY_idle_seeding_limit, core, 1, 40320, 5);
346     gtk_widget_set_sensitive(GTK_WIDGET(w2), gtr_pref_flag_get(TR_KEY_idle_seeding_limit_enabled));
347     g_signal_connect(w, "toggled", G_CALLBACK(target_cb), w2);
348     hig_workarea_add_row_w(t, &row, w, w2, NULL);
349 
350     return t;
351 }
352 
353 /****
354 *****  Desktop Tab
355 ****/
356 
desktopPage(GObject * core)357 static GtkWidget* desktopPage(GObject* core)
358 {
359     GtkWidget* t;
360     GtkWidget* w;
361     char const* s;
362     guint row = 0;
363 
364     t = hig_workarea_create();
365     hig_workarea_add_section_title(t, &row, _("Desktop"));
366 
367     s = _("_Inhibit hibernation when torrents are active");
368     w = new_check_button(s, TR_KEY_inhibit_desktop_hibernation, core);
369     hig_workarea_add_wide_control(t, &row, w);
370 
371     s = _("Show Transmission icon in the _notification area");
372     w = new_check_button(s, TR_KEY_show_notification_area_icon, core);
373     hig_workarea_add_wide_control(t, &row, w);
374 
375     hig_workarea_add_section_divider(t, &row);
376     hig_workarea_add_section_title(t, &row, _("Notification"));
377 
378     s = _("Show a notification when torrents are a_dded");
379     w = new_check_button(s, TR_KEY_torrent_added_notification_enabled, core);
380     hig_workarea_add_wide_control(t, &row, w);
381 
382     s = _("Show a notification when torrents _finish");
383     w = new_check_button(s, TR_KEY_torrent_complete_notification_enabled, core);
384     hig_workarea_add_wide_control(t, &row, w);
385 
386     s = _("Play a _sound when torrents finish");
387     w = new_check_button(s, TR_KEY_torrent_complete_sound_enabled, core);
388     hig_workarea_add_wide_control(t, &row, w);
389 
390     return t;
391 }
392 
393 /****
394 *****  Peer Tab
395 ****/
396 
397 struct blocklist_data
398 {
399     gulong updateBlocklistTag;
400     GtkWidget* updateBlocklistButton;
401     GtkWidget* updateBlocklistDialog;
402     GtkWidget* label;
403     GtkWidget* check;
404     TrCore* core;
405 };
406 
updateBlocklistText(GtkWidget * w,TrCore * core)407 static void updateBlocklistText(GtkWidget* w, TrCore* core)
408 {
409     char buf1[512];
410     char buf2[512];
411     int const n = tr_blocklistGetRuleCount(gtr_core_session(core));
412     g_snprintf(buf1, sizeof(buf1), ngettext("Blocklist contains %'d rule", "Blocklist contains %'d rules", n), n);
413     g_snprintf(buf2, sizeof(buf2), "<i>%s</i>", buf1);
414     gtk_label_set_markup(GTK_LABEL(w), buf2);
415 }
416 
417 /* prefs dialog is being destroyed, so stop listening to blocklist updates */
privacyPageDestroyed(gpointer gdata,GObject * dead UNUSED)418 static void privacyPageDestroyed(gpointer gdata, GObject* dead UNUSED)
419 {
420     struct blocklist_data* data = gdata;
421 
422     if (data->updateBlocklistTag > 0)
423     {
424         g_signal_handler_disconnect(data->core, data->updateBlocklistTag);
425     }
426 
427     g_free(data);
428 }
429 
430 /* user hit "close" in the blocklist-update dialog */
onBlocklistUpdateResponse(GtkDialog * dialog,gint response UNUSED,gpointer gdata)431 static void onBlocklistUpdateResponse(GtkDialog* dialog, gint response UNUSED, gpointer gdata)
432 {
433     struct blocklist_data* data = gdata;
434     gtk_widget_destroy(GTK_WIDGET(dialog));
435     gtk_widget_set_sensitive(data->updateBlocklistButton, TRUE);
436     data->updateBlocklistDialog = NULL;
437     g_signal_handler_disconnect(data->core, data->updateBlocklistTag);
438     data->updateBlocklistTag = 0;
439 }
440 
441 /* core says the blocklist was updated */
onBlocklistUpdated(TrCore * core,int n,gpointer gdata)442 static void onBlocklistUpdated(TrCore* core, int n, gpointer gdata)
443 {
444     bool const success = n >= 0;
445     int const count = n >= 0 ? n : tr_blocklistGetRuleCount(gtr_core_session(core));
446     char const* s = ngettext("Blocklist has %'d rule.", "Blocklist has %'d rules.", count);
447     struct blocklist_data* data = gdata;
448     GtkMessageDialog* d = GTK_MESSAGE_DIALOG(data->updateBlocklistDialog);
449     gtk_widget_set_sensitive(data->updateBlocklistButton, TRUE);
450     gtk_message_dialog_set_markup(d, success ? _("<b>Update succeeded!</b>") : _("<b>Unable to update.</b>"));
451     gtk_message_dialog_format_secondary_text(d, s, count);
452     updateBlocklistText(data->label, core);
453 }
454 
455 /* user pushed a button to update the blocklist */
onBlocklistUpdate(GtkButton * w,gpointer gdata)456 static void onBlocklistUpdate(GtkButton* w, gpointer gdata)
457 {
458     GtkWidget* d;
459     struct blocklist_data* data = gdata;
460     d = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(w))), GTK_DIALOG_DESTROY_WITH_PARENT,
461         GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", _("Update Blocklist"));
462     gtk_widget_set_sensitive(data->updateBlocklistButton, FALSE);
463     gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(d), "%s", _("Getting new blocklist…"));
464     data->updateBlocklistDialog = d;
465     g_signal_connect(d, "response", G_CALLBACK(onBlocklistUpdateResponse), data);
466     gtk_widget_show(d);
467     gtr_core_blocklist_update(data->core);
468     data->updateBlocklistTag = g_signal_connect(data->core, "blocklist-updated", G_CALLBACK(onBlocklistUpdated), data);
469 }
470 
on_blocklist_url_changed(GtkEditable * e,gpointer gbutton)471 static void on_blocklist_url_changed(GtkEditable* e, gpointer gbutton)
472 {
473     gchar* url = gtk_editable_get_chars(e, 0, -1);
474     gboolean const is_url_valid = tr_urlParse(url, TR_BAD_SIZE, NULL, NULL, NULL, NULL);
475     gtk_widget_set_sensitive(GTK_WIDGET(gbutton), is_url_valid);
476     g_free(url);
477 }
478 
onIntComboChanged(GtkComboBox * combo_box,gpointer core)479 static void onIntComboChanged(GtkComboBox* combo_box, gpointer core)
480 {
481     int const val = gtr_combo_box_get_active_enum(combo_box);
482     tr_quark const key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(combo_box), PREF_KEY));
483     gtr_core_set_pref_int(TR_CORE(core), key, val);
484 }
485 
new_encryption_combo(GObject * core,tr_quark const key)486 static GtkWidget* new_encryption_combo(GObject* core, tr_quark const key)
487 {
488     GtkWidget* w = gtr_combo_box_new_enum(_("Allow encryption"), TR_CLEAR_PREFERRED, _("Prefer encryption"),
489         TR_ENCRYPTION_PREFERRED, _("Require encryption"), TR_ENCRYPTION_REQUIRED, NULL);
490     gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), gtr_pref_int_get(key));
491     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
492     g_signal_connect(w, "changed", G_CALLBACK(onIntComboChanged), core);
493     return w;
494 }
495 
privacyPage(GObject * core)496 static GtkWidget* privacyPage(GObject* core)
497 {
498     char const* s;
499     GtkWidget* t;
500     GtkWidget* w;
501     GtkWidget* b;
502     GtkWidget* h;
503     GtkWidget* e;
504     struct blocklist_data* data;
505     guint row = 0;
506 
507     data = g_new0(struct blocklist_data, 1);
508     data->core = TR_CORE(core);
509 
510     t = hig_workarea_create();
511     hig_workarea_add_section_title(t, &row, _("Privacy"));
512 
513     s = _("_Encryption mode:");
514     w = new_encryption_combo(core, TR_KEY_encryption);
515     hig_workarea_add_row(t, &row, s, w, NULL);
516 
517     hig_workarea_add_section_divider(t, &row);
518     hig_workarea_add_section_title(t, &row, _("Blocklist"));
519 
520     b = new_check_button(_("Enable _blocklist:"), TR_KEY_blocklist_enabled, core);
521     e = new_entry(TR_KEY_blocklist_url, core);
522     gtk_widget_set_size_request(e, 300, -1);
523     hig_workarea_add_row_w(t, &row, b, e, NULL);
524     data->check = b;
525     g_signal_connect(b, "toggled", G_CALLBACK(target_cb), e);
526     target_cb(b, e);
527 
528     w = gtk_label_new("");
529     g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
530     updateBlocklistText(w, TR_CORE(core));
531     data->label = w;
532     h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
533     gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
534     b = data->updateBlocklistButton = gtk_button_new_with_mnemonic(_("_Update"));
535     g_object_set_data(G_OBJECT(b), "session", gtr_core_session(TR_CORE(core)));
536     g_signal_connect(b, "clicked", G_CALLBACK(onBlocklistUpdate), data);
537     g_signal_connect(data->check, "toggled", G_CALLBACK(target_cb), b);
538     target_cb(data->check, b);
539     gtk_box_pack_start(GTK_BOX(h), b, FALSE, FALSE, 0);
540     g_signal_connect(data->check, "toggled", G_CALLBACK(target_cb), w);
541     target_cb(data->check, w);
542     hig_workarea_add_wide_control(t, &row, h);
543     g_signal_connect(e, "changed", G_CALLBACK(on_blocklist_url_changed), data->updateBlocklistButton);
544     on_blocklist_url_changed(GTK_EDITABLE(e), data->updateBlocklistButton);
545 
546     s = _("Enable _automatic updates");
547     w = new_check_button(s, TR_KEY_blocklist_updates_enabled, core);
548     hig_workarea_add_wide_control(t, &row, w);
549     g_signal_connect(data->check, "toggled", G_CALLBACK(target_cb), w);
550     target_cb(data->check, w);
551 
552     g_object_weak_ref(G_OBJECT(t), privacyPageDestroyed, data);
553     return t;
554 }
555 
556 /****
557 *****  Remote Tab
558 ****/
559 
560 enum
561 {
562     COL_ADDRESS,
563     N_COLS
564 };
565 
whitelist_tree_model_new(char const * whitelist)566 static GtkTreeModel* whitelist_tree_model_new(char const* whitelist)
567 {
568     char** rules;
569     GtkListStore* store = gtk_list_store_new(N_COLS,
570         G_TYPE_STRING,
571         G_TYPE_STRING);
572 
573     rules = g_strsplit(whitelist, ",", 0);
574 
575     for (int i = 0; rules != NULL && rules[i] != NULL; ++i)
576     {
577         GtkTreeIter iter;
578         char const* s = rules[i];
579 
580         while (isspace(*s))
581         {
582             ++s;
583         }
584 
585         gtk_list_store_append(store, &iter);
586         gtk_list_store_set(store, &iter, COL_ADDRESS, s, -1);
587     }
588 
589     g_strfreev(rules);
590     return GTK_TREE_MODEL(store);
591 }
592 
593 struct remote_page
594 {
595     TrCore* core;
596     GtkTreeView* view;
597     GtkListStore* store;
598     GtkWidget* remove_button;
599     GSList* widgets;
600     GSList* auth_widgets;
601     GSList* whitelist_widgets;
602     GtkToggleButton* rpc_tb;
603     GtkToggleButton* auth_tb;
604     GtkToggleButton* whitelist_tb;
605 };
606 
refreshWhitelist(struct remote_page * page)607 static void refreshWhitelist(struct remote_page* page)
608 {
609     GtkTreeIter iter;
610     GString* gstr = g_string_new(NULL);
611     GtkTreeModel* model = GTK_TREE_MODEL(page->store);
612 
613     if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
614     {
615         do
616         {
617             char* address;
618             gtk_tree_model_get(model, &iter, COL_ADDRESS, &address, -1);
619             g_string_append(gstr, address);
620             g_string_append(gstr, ",");
621             g_free(address);
622         }
623         while (gtk_tree_model_iter_next(model, &iter));
624     }
625 
626     g_string_truncate(gstr, gstr->len - 1); /* remove the trailing comma */
627 
628     gtr_core_set_pref(page->core, TR_KEY_rpc_whitelist, gstr->str);
629 
630     g_string_free(gstr, TRUE);
631 }
632 
onAddressEdited(GtkCellRendererText * r UNUSED,gchar * path_string,gchar * address,gpointer gpage)633 static void onAddressEdited(GtkCellRendererText* r UNUSED, gchar* path_string, gchar* address, gpointer gpage)
634 {
635     GtkTreeIter iter;
636     struct remote_page* page = gpage;
637     GtkTreeModel* model = GTK_TREE_MODEL(page->store);
638     GtkTreePath* path = gtk_tree_path_new_from_string(path_string);
639 
640     if (gtk_tree_model_get_iter(model, &iter, path))
641     {
642         gtk_list_store_set(page->store, &iter, COL_ADDRESS, address, -1);
643     }
644 
645     gtk_tree_path_free(path);
646     refreshWhitelist(page);
647 }
648 
onAddWhitelistClicked(GtkButton * b UNUSED,gpointer gpage)649 static void onAddWhitelistClicked(GtkButton* b UNUSED, gpointer gpage)
650 {
651     GtkTreeIter iter;
652     GtkTreePath* path;
653     struct remote_page* page = gpage;
654 
655     gtk_list_store_append(page->store, &iter);
656     gtk_list_store_set(page->store, &iter, COL_ADDRESS, "0.0.0.0", -1);
657 
658     path = gtk_tree_model_get_path(GTK_TREE_MODEL(page->store), &iter);
659     gtk_tree_view_set_cursor(page->view, path, gtk_tree_view_get_column(page->view, COL_ADDRESS), TRUE);
660     gtk_tree_path_free(path);
661 }
662 
onRemoveWhitelistClicked(GtkButton * b UNUSED,gpointer gpage)663 static void onRemoveWhitelistClicked(GtkButton* b UNUSED, gpointer gpage)
664 {
665     struct remote_page* page = gpage;
666     GtkTreeSelection* sel = gtk_tree_view_get_selection(page->view);
667     GtkTreeIter iter;
668 
669     if (gtk_tree_selection_get_selected(sel, NULL, &iter))
670     {
671         gtk_list_store_remove(page->store, &iter);
672         refreshWhitelist(page);
673     }
674 }
675 
refreshRPCSensitivity(struct remote_page * page)676 static void refreshRPCSensitivity(struct remote_page* page)
677 {
678     int const rpc_active = gtk_toggle_button_get_active(page->rpc_tb);
679     int const auth_active = gtk_toggle_button_get_active(page->auth_tb);
680     int const whitelist_active = gtk_toggle_button_get_active(page->whitelist_tb);
681     GtkTreeSelection* sel = gtk_tree_view_get_selection(page->view);
682     int const have_addr = gtk_tree_selection_get_selected(sel, NULL, NULL);
683     int const n_rules = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(page->store), NULL);
684 
685     for (GSList* l = page->widgets; l != NULL; l = l->next)
686     {
687         gtk_widget_set_sensitive(GTK_WIDGET(l->data), rpc_active);
688     }
689 
690     for (GSList* l = page->auth_widgets; l != NULL; l = l->next)
691     {
692         gtk_widget_set_sensitive(GTK_WIDGET(l->data), rpc_active && auth_active);
693     }
694 
695     for (GSList* l = page->whitelist_widgets; l != NULL; l = l->next)
696     {
697         gtk_widget_set_sensitive(GTK_WIDGET(l->data), rpc_active && whitelist_active);
698     }
699 
700     gtk_widget_set_sensitive(page->remove_button, rpc_active && have_addr && n_rules > 1);
701 }
702 
onRPCToggled(GtkToggleButton * tb UNUSED,gpointer page)703 static void onRPCToggled(GtkToggleButton* tb UNUSED, gpointer page)
704 {
705     refreshRPCSensitivity(page);
706 }
707 
onWhitelistSelectionChanged(GtkTreeSelection * sel UNUSED,gpointer page)708 static void onWhitelistSelectionChanged(GtkTreeSelection* sel UNUSED, gpointer page)
709 {
710     refreshRPCSensitivity(page);
711 }
712 
onLaunchClutchCB(GtkButton * w UNUSED,gpointer data UNUSED)713 static void onLaunchClutchCB(GtkButton* w UNUSED, gpointer data UNUSED)
714 {
715     char* uri;
716     int const port = gtr_pref_int_get(TR_KEY_rpc_port);
717 
718     uri = g_strdup_printf("http://localhost:%d/", port);
719     gtr_open_uri(uri);
720     g_free(uri);
721 }
722 
remotePageFree(gpointer gpage)723 static void remotePageFree(gpointer gpage)
724 {
725     struct remote_page* page = gpage;
726 
727     g_slist_free(page->widgets);
728     g_slist_free(page->auth_widgets);
729     g_slist_free(page->whitelist_widgets);
730     g_free(page);
731 }
732 
remotePage(GObject * core)733 static GtkWidget* remotePage(GObject* core)
734 {
735     GtkWidget* t;
736     GtkWidget* w;
737     GtkWidget* h;
738     char const* s;
739     guint row = 0;
740     struct remote_page* page = g_new0(struct remote_page, 1);
741 
742     page->core = TR_CORE(core);
743 
744     t = hig_workarea_create();
745     g_object_set_data_full(G_OBJECT(t), "page", page, remotePageFree);
746 
747     hig_workarea_add_section_title(t, &row, _("Remote Control"));
748 
749     /* "enabled" checkbutton */
750     s = _("Allow _remote access");
751     w = new_check_button(s, TR_KEY_rpc_enabled, core);
752     page->rpc_tb = GTK_TOGGLE_BUTTON(w);
753     g_signal_connect(w, "clicked", G_CALLBACK(onRPCToggled), page);
754     h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
755     gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
756     w = gtk_button_new_with_mnemonic(_("_Open web client"));
757     page->widgets = g_slist_prepend(page->widgets, w);
758     g_signal_connect(w, "clicked", G_CALLBACK(onLaunchClutchCB), NULL);
759     gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
760     hig_workarea_add_wide_control(t, &row, h);
761 
762     /* port */
763     w = new_spin_button(TR_KEY_rpc_port, core, 0, USHRT_MAX, 1);
764     page->widgets = g_slist_prepend(page->widgets, w);
765     w = hig_workarea_add_row(t, &row, _("HTTP _port:"), w, NULL);
766     page->widgets = g_slist_prepend(page->widgets, w);
767 
768     /* require authentication */
769     s = _("Use _authentication");
770     w = new_check_button(s, TR_KEY_rpc_authentication_required, core);
771     hig_workarea_add_wide_control(t, &row, w);
772     page->auth_tb = GTK_TOGGLE_BUTTON(w);
773     page->widgets = g_slist_prepend(page->widgets, w);
774     g_signal_connect(w, "clicked", G_CALLBACK(onRPCToggled), page);
775 
776     /* username */
777     s = _("_Username:");
778     w = new_entry(TR_KEY_rpc_username, core);
779     page->auth_widgets = g_slist_prepend(page->auth_widgets, w);
780     w = hig_workarea_add_row(t, &row, s, w, NULL);
781     page->auth_widgets = g_slist_prepend(page->auth_widgets, w);
782 
783     /* password */
784     s = _("Pass_word:");
785     w = new_entry(TR_KEY_rpc_password, core);
786     gtk_entry_set_visibility(GTK_ENTRY(w), FALSE);
787     page->auth_widgets = g_slist_prepend(page->auth_widgets, w);
788     w = hig_workarea_add_row(t, &row, s, w, NULL);
789     page->auth_widgets = g_slist_prepend(page->auth_widgets, w);
790 
791     /* require authentication */
792     s = _("Only allow these IP a_ddresses:");
793     w = new_check_button(s, TR_KEY_rpc_whitelist_enabled, core);
794     hig_workarea_add_wide_control(t, &row, w);
795     page->whitelist_tb = GTK_TOGGLE_BUTTON(w);
796     page->widgets = g_slist_prepend(page->widgets, w);
797     g_signal_connect(w, "clicked", G_CALLBACK(onRPCToggled), page);
798 
799     /* access control list */
800     {
801         char const* val = gtr_pref_string_get(TR_KEY_rpc_whitelist);
802         GtkTreeModel* m = whitelist_tree_model_new(val);
803         GtkTreeViewColumn* c;
804         GtkCellRenderer* r;
805         GtkTreeSelection* sel;
806         GtkTreeView* v;
807         GtkWidget* w;
808         GtkWidget* h;
809 
810         page->store = GTK_LIST_STORE(m);
811         w = gtk_tree_view_new_with_model(m);
812         g_signal_connect(w, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
813 
814         page->whitelist_widgets = g_slist_prepend(page->whitelist_widgets, w);
815         v = page->view = GTK_TREE_VIEW(w);
816         gtk_widget_set_tooltip_text(w, _("IP addresses may use wildcards, such as 192.168.*.*"));
817         sel = gtk_tree_view_get_selection(v);
818         g_signal_connect(sel, "changed",
819             G_CALLBACK(onWhitelistSelectionChanged), page);
820         g_object_unref(G_OBJECT(m));
821         gtk_tree_view_set_headers_visible(v, TRUE);
822         w = gtk_frame_new(NULL);
823         gtk_frame_set_shadow_type(GTK_FRAME(w), GTK_SHADOW_IN);
824         gtk_container_add(GTK_CONTAINER(w), GTK_WIDGET(v));
825 
826         /* ip address column */
827         r = gtk_cell_renderer_text_new();
828         g_signal_connect(r, "edited", G_CALLBACK(onAddressEdited), page);
829         g_object_set(G_OBJECT(r), "editable", TRUE, NULL);
830         c = gtk_tree_view_column_new_with_attributes(NULL, r, "text", COL_ADDRESS, NULL);
831         gtk_tree_view_column_set_expand(c, TRUE);
832         gtk_tree_view_append_column(v, c);
833         gtk_tree_view_set_headers_visible(v, FALSE);
834 
835         s = _("Addresses:");
836         w = hig_workarea_add_row(t, &row, s, w, NULL);
837         g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_START, "margin-top", GUI_PAD, "margin-bottom", GUI_PAD,
838             NULL);
839         page->whitelist_widgets = g_slist_prepend(page->whitelist_widgets, w);
840 
841         h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
842         w = gtk_button_new_from_stock(GTK_STOCK_REMOVE);
843         g_signal_connect(w, "clicked", G_CALLBACK(onRemoveWhitelistClicked), page);
844         page->remove_button = w;
845         onWhitelistSelectionChanged(sel, page);
846         gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
847         w = gtk_button_new_from_stock(GTK_STOCK_ADD);
848         page->whitelist_widgets = g_slist_prepend(page->whitelist_widgets, w);
849         g_signal_connect(w, "clicked", G_CALLBACK(onAddWhitelistClicked), page);
850         g_object_set(h, "halign", GTK_ALIGN_END, "valign", GTK_ALIGN_CENTER, NULL);
851         gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
852         hig_workarea_add_wide_control(t, &row, h);
853     }
854 
855     refreshRPCSensitivity(page);
856     return t;
857 }
858 
859 /****
860 *****  Bandwidth Tab
861 ****/
862 
863 struct BandwidthPage
864 {
865     TrCore* core;
866     GSList* sched_widgets;
867 };
868 
refreshSchedSensitivity(struct BandwidthPage * p)869 static void refreshSchedSensitivity(struct BandwidthPage* p)
870 {
871     gboolean const sched_enabled = gtr_pref_flag_get(TR_KEY_alt_speed_time_enabled);
872 
873     for (GSList* l = p->sched_widgets; l != NULL; l = l->next)
874     {
875         gtk_widget_set_sensitive(GTK_WIDGET(l->data), sched_enabled);
876     }
877 }
878 
onSchedToggled(GtkToggleButton * tb UNUSED,gpointer user_data)879 static void onSchedToggled(GtkToggleButton* tb UNUSED, gpointer user_data)
880 {
881     refreshSchedSensitivity(user_data);
882 }
883 
onTimeComboChanged(GtkComboBox * w,gpointer core)884 static void onTimeComboChanged(GtkComboBox* w, gpointer core)
885 {
886     GtkTreeIter iter;
887 
888     if (gtk_combo_box_get_active_iter(w, &iter))
889     {
890         int val = 0;
891         tr_quark const key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w), PREF_KEY));
892         gtk_tree_model_get(gtk_combo_box_get_model(w), &iter, 0, &val, -1);
893         gtr_core_set_pref_int(TR_CORE(core), key, val);
894     }
895 }
896 
new_time_combo(GObject * core,tr_quark const key)897 static GtkWidget* new_time_combo(GObject* core, tr_quark const key)
898 {
899     int val;
900     GtkWidget* w;
901     GtkCellRenderer* r;
902     GtkListStore* store;
903 
904     /* build a store at 15 minute intervals */
905     store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING);
906 
907     for (int i = 0; i < 60 * 24; i += 15)
908     {
909         char buf[128];
910         GtkTreeIter iter;
911         g_snprintf(buf, sizeof(buf), "%02d:%02d", i / 60, i % 60);
912         gtk_list_store_append(store, &iter);
913         gtk_list_store_set(store, &iter, 0, i, 1, buf, -1);
914     }
915 
916     /* build the widget */
917     w = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
918     gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(w), 4);
919     r = gtk_cell_renderer_text_new();
920     gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(w), r, TRUE);
921     gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(w), r, "text", 1, NULL);
922     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
923     val = gtr_pref_int_get(key);
924     gtk_combo_box_set_active(GTK_COMBO_BOX(w), val / (15));
925     g_signal_connect(w, "changed", G_CALLBACK(onTimeComboChanged), core);
926 
927     /* cleanup */
928     g_object_unref(G_OBJECT(store));
929     return w;
930 }
931 
new_week_combo(GObject * core,tr_quark const key)932 static GtkWidget* new_week_combo(GObject* core, tr_quark const key)
933 {
934     GtkWidget* w = gtr_combo_box_new_enum(
935         _("Every Day"), TR_SCHED_ALL,
936         _("Weekdays"), TR_SCHED_WEEKDAY,
937         _("Weekends"), TR_SCHED_WEEKEND,
938         _("Sunday"), TR_SCHED_SUN,
939         _("Monday"), TR_SCHED_MON,
940         _("Tuesday"), TR_SCHED_TUES,
941         _("Wednesday"), TR_SCHED_WED,
942         _("Thursday"), TR_SCHED_THURS,
943         _("Friday"), TR_SCHED_FRI,
944         _("Saturday"), TR_SCHED_SAT,
945         NULL);
946     gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), gtr_pref_int_get(key));
947     g_object_set_data(G_OBJECT(w), PREF_KEY, GINT_TO_POINTER(key));
948     g_signal_connect(w, "changed", G_CALLBACK(onIntComboChanged), core);
949     return w;
950 }
951 
speedPageFree(gpointer gpage)952 static void speedPageFree(gpointer gpage)
953 {
954     struct BandwidthPage* page = gpage;
955 
956     g_slist_free(page->sched_widgets);
957     g_free(page);
958 }
959 
speedPage(GObject * core)960 static GtkWidget* speedPage(GObject* core)
961 {
962     char const* s;
963     GtkWidget* t;
964     GtkWidget* l;
965     GtkWidget* w;
966     GtkWidget* w2;
967     GtkWidget* h;
968     char buf[512];
969     guint row = 0;
970     struct BandwidthPage* page = tr_new0(struct BandwidthPage, 1);
971 
972     page->core = TR_CORE(core);
973 
974     t = hig_workarea_create();
975     hig_workarea_add_section_title(t, &row, _("Speed Limits"));
976 
977     g_snprintf(buf, sizeof(buf), _("_Upload (%s):"), _(speed_K_str));
978     w = new_check_button(buf, TR_KEY_speed_limit_up_enabled, core);
979     w2 = new_spin_button(TR_KEY_speed_limit_up, core, 0, INT_MAX, 5);
980     gtk_widget_set_sensitive(GTK_WIDGET(w2), gtr_pref_flag_get(TR_KEY_speed_limit_up_enabled));
981     g_signal_connect(w, "toggled", G_CALLBACK(target_cb), w2);
982     hig_workarea_add_row_w(t, &row, w, w2, NULL);
983 
984     g_snprintf(buf, sizeof(buf), _("_Download (%s):"), _(speed_K_str));
985     w = new_check_button(buf, TR_KEY_speed_limit_down_enabled, core);
986     w2 = new_spin_button(TR_KEY_speed_limit_down, core, 0, INT_MAX, 5);
987     gtk_widget_set_sensitive(GTK_WIDGET(w2), gtr_pref_flag_get(TR_KEY_speed_limit_down_enabled));
988     g_signal_connect(w, "toggled", G_CALLBACK(target_cb), w2);
989     hig_workarea_add_row_w(t, &row, w, w2, NULL);
990 
991     hig_workarea_add_section_divider(t, &row);
992     h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
993     g_snprintf(buf, sizeof(buf), "<b>%s</b>", _("Alternative Speed Limits"));
994     w = gtk_label_new(buf);
995     g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
996     gtk_label_set_use_markup(GTK_LABEL(w), TRUE);
997     gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
998     w = gtk_image_new_from_icon_name("alt-speed-on", GTK_ICON_SIZE_MENU);
999     gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
1000     hig_workarea_add_section_title_widget(t, &row, h);
1001 
1002     s = _("Override normal speed limits manually or at scheduled times");
1003     g_snprintf(buf, sizeof(buf), "<small>%s</small>", s);
1004     w = gtk_label_new(buf);
1005     gtk_label_set_use_markup(GTK_LABEL(w), TRUE);
1006     g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
1007     hig_workarea_add_wide_control(t, &row, w);
1008 
1009     g_snprintf(buf, sizeof(buf), _("U_pload (%s):"), _(speed_K_str));
1010     w = new_spin_button(TR_KEY_alt_speed_up, core, 0, INT_MAX, 5);
1011     hig_workarea_add_row(t, &row, buf, w, NULL);
1012 
1013     g_snprintf(buf, sizeof(buf), _("Do_wnload (%s):"), _(speed_K_str));
1014     w = new_spin_button(TR_KEY_alt_speed_down, core, 0, INT_MAX, 5);
1015     hig_workarea_add_row(t, &row, buf, w, NULL);
1016 
1017     s = _("_Scheduled times:");
1018     h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1019     w2 = new_time_combo(core, TR_KEY_alt_speed_time_begin);
1020     page->sched_widgets = g_slist_prepend(page->sched_widgets, w2);
1021     gtk_box_pack_start(GTK_BOX(h), w2, TRUE, TRUE, 0);
1022     w2 = l = gtk_label_new_with_mnemonic(_(" _to "));
1023     page->sched_widgets = g_slist_prepend(page->sched_widgets, w2);
1024     gtk_box_pack_start(GTK_BOX(h), w2, FALSE, FALSE, 0);
1025     w2 = new_time_combo(core, TR_KEY_alt_speed_time_end);
1026     gtk_label_set_mnemonic_widget(GTK_LABEL(l), w2);
1027     page->sched_widgets = g_slist_prepend(page->sched_widgets, w2);
1028     gtk_box_pack_start(GTK_BOX(h), w2, TRUE, TRUE, 0);
1029     w = new_check_button(s, TR_KEY_alt_speed_time_enabled, core);
1030     g_signal_connect(w, "toggled", G_CALLBACK(onSchedToggled), page);
1031     hig_workarea_add_row_w(t, &row, w, h, NULL);
1032 
1033     s = _("_On days:");
1034     w = new_week_combo(core, TR_KEY_alt_speed_time_day);
1035     page->sched_widgets = g_slist_prepend(page->sched_widgets, w);
1036     w = hig_workarea_add_row(t, &row, s, w, NULL);
1037     page->sched_widgets = g_slist_prepend(page->sched_widgets, w);
1038 
1039     g_object_set_data_full(G_OBJECT(t), "page", page, speedPageFree);
1040 
1041     refreshSchedSensitivity(page);
1042     return t;
1043 }
1044 
1045 /****
1046 *****  Network Tab
1047 ****/
1048 
1049 struct network_page_data
1050 {
1051     TrCore* core;
1052     GtkWidget* portLabel;
1053     GtkWidget* portButton;
1054     GtkWidget* portSpin;
1055     gulong portTag;
1056     gulong prefsTag;
1057 };
1058 
onCorePrefsChanged(TrCore * core UNUSED,tr_quark const key,gpointer gdata)1059 static void onCorePrefsChanged(TrCore* core UNUSED, tr_quark const key, gpointer gdata)
1060 {
1061     if (key == TR_KEY_peer_port)
1062     {
1063         struct network_page_data* data = gdata;
1064         gtr_label_set_text(GTK_LABEL(data->portLabel), _("Status unknown"));
1065         gtk_widget_set_sensitive(data->portButton, TRUE);
1066         gtk_widget_set_sensitive(data->portSpin, TRUE);
1067     }
1068 }
1069 
networkPageDestroyed(gpointer gdata,GObject * dead UNUSED)1070 static void networkPageDestroyed(gpointer gdata, GObject* dead UNUSED)
1071 {
1072     struct network_page_data* data = gdata;
1073 
1074     if (data->prefsTag > 0)
1075     {
1076         g_signal_handler_disconnect(data->core, data->prefsTag);
1077     }
1078 
1079     if (data->portTag > 0)
1080     {
1081         g_signal_handler_disconnect(data->core, data->portTag);
1082     }
1083 
1084     g_free(data);
1085 }
1086 
onPortTested(TrCore * core UNUSED,gboolean isOpen,gpointer vdata)1087 static void onPortTested(TrCore* core UNUSED, gboolean isOpen, gpointer vdata)
1088 {
1089     struct network_page_data* data = vdata;
1090     char const* markup = isOpen ? _("Port is <b>open</b>") : _("Port is <b>closed</b>");
1091 
1092     // gdk_threads_enter();
1093     gtk_label_set_markup(GTK_LABEL(data->portLabel), markup);
1094     gtk_widget_set_sensitive(data->portButton, TRUE);
1095     gtk_widget_set_sensitive(data->portSpin, TRUE);
1096     // gdk_threads_leave();
1097 }
1098 
onPortTest(GtkButton * button UNUSED,gpointer vdata)1099 static void onPortTest(GtkButton* button UNUSED, gpointer vdata)
1100 {
1101     struct network_page_data* data = vdata;
1102     gtk_widget_set_sensitive(data->portButton, FALSE);
1103     gtk_widget_set_sensitive(data->portSpin, FALSE);
1104     gtk_label_set_markup(GTK_LABEL(data->portLabel), _("<i>Testing TCP port…</i>"));
1105 
1106     if (data->portTag == 0)
1107     {
1108         data->portTag = g_signal_connect(data->core, "port-tested", G_CALLBACK(onPortTested), data);
1109     }
1110 
1111     gtr_core_port_test(data->core);
1112 }
1113 
networkPage(GObject * core)1114 static GtkWidget* networkPage(GObject* core)
1115 {
1116     GtkWidget* t;
1117     GtkWidget* w;
1118     GtkWidget* h;
1119     GtkWidget* l;
1120     char const* s;
1121     struct network_page_data* data;
1122     guint row = 0;
1123 
1124     /* register to stop listening to core prefs changes when the page is destroyed */
1125     data = g_new0(struct network_page_data, 1);
1126     data->core = TR_CORE(core);
1127 
1128     /* build the page */
1129     t = hig_workarea_create();
1130     hig_workarea_add_section_title(t, &row, _("Listening Port"));
1131 
1132     s = _("_Port used for incoming connections:");
1133     w = data->portSpin = new_spin_button(TR_KEY_peer_port, core, 1, USHRT_MAX, 1);
1134     hig_workarea_add_row(t, &row, s, w, NULL);
1135 
1136     h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
1137     l = data->portLabel = gtk_label_new(_("Status unknown"));
1138     g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
1139     gtk_box_pack_start(GTK_BOX(h), l, TRUE, TRUE, 0);
1140     w = data->portButton = gtk_button_new_with_mnemonic(_("Te_st Port"));
1141     gtk_box_pack_end(GTK_BOX(h), w, FALSE, FALSE, 0);
1142     g_signal_connect(w, "clicked", G_CALLBACK(onPortTest), data);
1143     hig_workarea_add_row(t, &row, NULL, h, NULL);
1144     data->prefsTag = g_signal_connect(TR_CORE(core), "prefs-changed", G_CALLBACK(onCorePrefsChanged), data);
1145     g_object_weak_ref(G_OBJECT(t), networkPageDestroyed, data);
1146 
1147     s = _("Pick a _random port every time Transmission is started");
1148     w = new_check_button(s, TR_KEY_peer_port_random_on_start, core);
1149     hig_workarea_add_wide_control(t, &row, w);
1150 
1151     s = _("Use UPnP or NAT-PMP port _forwarding from my router");
1152     w = new_check_button(s, TR_KEY_port_forwarding_enabled, core);
1153     hig_workarea_add_wide_control(t, &row, w);
1154 
1155     hig_workarea_add_section_divider(t, &row);
1156     hig_workarea_add_section_title(t, &row, _("Peer Limits"));
1157 
1158     w = new_spin_button(TR_KEY_peer_limit_per_torrent, core, 1, FD_SETSIZE, 5);
1159     hig_workarea_add_row(t, &row, _("Maximum peers per _torrent:"), w, NULL);
1160     w = new_spin_button(TR_KEY_peer_limit_global, core, 1, FD_SETSIZE, 5);
1161     hig_workarea_add_row(t, &row, _("Maximum peers _overall:"), w, NULL);
1162 
1163     hig_workarea_add_section_divider(t, &row);
1164     hig_workarea_add_section_title(t, &row, _("Options"));
1165 
1166 #ifdef WITH_UTP
1167     s = _("Enable _uTP for peer communication");
1168     w = new_check_button(s, TR_KEY_utp_enabled, core);
1169     s = _("uTP is a tool for reducing network congestion.");
1170     gtk_widget_set_tooltip_text(w, s);
1171     hig_workarea_add_wide_control(t, &row, w);
1172 #endif
1173 
1174     s = _("Use PE_X to find more peers");
1175     w = new_check_button(s, TR_KEY_pex_enabled, core);
1176     s = _("PEX is a tool for exchanging peer lists with the peers you're connected to.");
1177     gtk_widget_set_tooltip_text(w, s);
1178     hig_workarea_add_wide_control(t, &row, w);
1179 
1180     s = _("Use _DHT to find more peers");
1181     w = new_check_button(s, TR_KEY_dht_enabled, core);
1182     s = _("DHT is a tool for finding peers without a tracker.");
1183     gtk_widget_set_tooltip_text(w, s);
1184     hig_workarea_add_wide_control(t, &row, w);
1185 
1186     s = _("Use _Local Peer Discovery to find more peers");
1187     w = new_check_button(s, TR_KEY_lpd_enabled, core);
1188     s = _("LPD is a tool for finding peers on your local network.");
1189     gtk_widget_set_tooltip_text(w, s);
1190     hig_workarea_add_wide_control(t, &row, w);
1191 
1192     return t;
1193 }
1194 
1195 /****
1196 *****
1197 ****/
1198 
on_prefs_dialog_destroyed(gpointer gdata,GObject * dead_dialog G_GNUC_UNUSED)1199 static void on_prefs_dialog_destroyed(gpointer gdata, GObject* dead_dialog G_GNUC_UNUSED)
1200 {
1201     struct prefs_dialog_data* data = gdata;
1202 
1203     if (data->core_prefs_tag > 0)
1204     {
1205         g_signal_handler_disconnect(data->core, data->core_prefs_tag);
1206     }
1207 
1208     g_free(data);
1209 }
1210 
on_core_prefs_changed(TrCore * core,tr_quark const key,gpointer gdata)1211 static void on_core_prefs_changed(TrCore* core, tr_quark const key, gpointer gdata)
1212 {
1213     struct prefs_dialog_data* data = gdata;
1214 
1215 #if 0
1216 
1217     if (key == TR_KEY_peer_port)
1218     {
1219         gtr_label_set_text(GTK_LABEL(data->port_label), _("Status unknown"));
1220         gtk_widget_set_sensitive(data->port_button, TRUE);
1221         gtk_widget_set_sensitive(data->port_spin, TRUE);
1222     }
1223 
1224 #endif
1225 
1226     if (key == TR_KEY_download_dir)
1227     {
1228         char const* downloadDir = tr_sessionGetDownloadDir(gtr_core_session(core));
1229         gtr_freespace_label_set_dir(data->freespace_label, downloadDir);
1230     }
1231 }
1232 
gtr_prefs_dialog_new(GtkWindow * parent,GObject * core)1233 GtkWidget* gtr_prefs_dialog_new(GtkWindow* parent, GObject* core)
1234 {
1235     GtkWidget* d;
1236     GtkWidget* n;
1237     struct prefs_dialog_data* data;
1238     tr_quark const prefs_quarks[] = { TR_KEY_peer_port, TR_KEY_download_dir };
1239 
1240     data = g_new0(struct prefs_dialog_data, 1);
1241     data->core = TR_CORE(core);
1242     data->core_prefs_tag = g_signal_connect(TR_CORE(core), "prefs-changed", G_CALLBACK(on_core_prefs_changed), data);
1243 
1244     d = gtk_dialog_new_with_buttons(_("Transmission Preferences"), parent, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_HELP,
1245         GTK_RESPONSE_HELP, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
1246     g_object_weak_ref(G_OBJECT(d), on_prefs_dialog_destroyed, data);
1247     gtk_window_set_role(GTK_WINDOW(d), "transmission-preferences-dialog");
1248     gtk_container_set_border_width(GTK_CONTAINER(d), GUI_PAD);
1249 
1250     n = gtk_notebook_new();
1251     gtk_container_set_border_width(GTK_CONTAINER(n), GUI_PAD);
1252 
1253     gtk_notebook_append_page(GTK_NOTEBOOK(n), speedPage(core), gtk_label_new(_("Speed")));
1254     gtk_notebook_append_page(GTK_NOTEBOOK(n), downloadingPage(core, data), gtk_label_new(C_("Gerund", "Downloading")));
1255     gtk_notebook_append_page(GTK_NOTEBOOK(n), seedingPage(core), gtk_label_new(C_("Gerund", "Seeding")));
1256     gtk_notebook_append_page(GTK_NOTEBOOK(n), privacyPage(core), gtk_label_new(_("Privacy")));
1257     gtk_notebook_append_page(GTK_NOTEBOOK(n), networkPage(core), gtk_label_new(_("Network")));
1258     gtk_notebook_append_page(GTK_NOTEBOOK(n), desktopPage(core), gtk_label_new(_("Desktop")));
1259     gtk_notebook_append_page(GTK_NOTEBOOK(n), remotePage(core), gtk_label_new(_("Remote")));
1260 
1261     /* init from prefs keys */
1262     for (size_t i = 0; i < G_N_ELEMENTS(prefs_quarks); ++i)
1263     {
1264         on_core_prefs_changed(TR_CORE(core), prefs_quarks[i], data);
1265     }
1266 
1267     g_signal_connect(d, "response", G_CALLBACK(response_cb), core);
1268     gtr_dialog_set_content(GTK_DIALOG(d), n);
1269     return d;
1270 }
1271