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