1 /******************************************************************************
2 * Copyright (c) Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23 #include <string.h> /* strlen() */
24
25 #include <gtk/gtk.h>
26 #include <glib/gi18n.h>
27
28 #include <libtransmission/transmission.h>
29 #include <libtransmission/utils.h> /* tr_formatter_speed_KBps() */
30
31 #include "actions.h"
32 #include "conf.h"
33 #include "filter.h"
34 #include "hig.h"
35 #include "torrent-cell-renderer.h"
36 #include "tr-prefs.h"
37 #include "tr-window.h"
38 #include "util.h"
39
40 typedef struct
41 {
42 GtkWidget* speedlimit_on_item[2];
43 GtkWidget* speedlimit_off_item[2];
44 GtkWidget* ratio_on_item;
45 GtkWidget* ratio_off_item;
46 GtkWidget* scroll;
47 GtkWidget* view;
48 GtkWidget* toolbar;
49 GtkWidget* filter;
50 GtkWidget* status;
51 GtkWidget* status_menu;
52 GtkLabel* ul_lb;
53 GtkLabel* dl_lb;
54 GtkLabel* stats_lb;
55 GtkWidget* alt_speed_image;
56 GtkWidget* alt_speed_button;
57 GtkWidget* options_menu;
58 GtkTreeSelection* selection;
59 GtkCellRenderer* renderer;
60 GtkTreeViewColumn* column;
61 GtkTreeModel* filter_model;
62 TrCore* core;
63 gulong pref_handler_id;
64 }
65 PrivateData;
66
TR_DEFINE_QUARK(private_data,private_data)67 static TR_DEFINE_QUARK(private_data, private_data)
68
69 static PrivateData* get_private_data(GtkWindow * w)
70 {
71 return g_object_get_qdata(G_OBJECT(w), private_data_quark());
72 }
73
74 /***
75 ****
76 ***/
77
on_popup_menu(GtkWidget * self UNUSED,GdkEventButton * event)78 static void on_popup_menu(GtkWidget* self UNUSED, GdkEventButton* event)
79 {
80 GtkWidget* menu = gtr_action_get_widget("/main-window-popup");
81
82 #if GTK_CHECK_VERSION(3, 22, 0)
83 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
84 #else
85 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event != NULL ? event->button : 0, event != NULL ? event->time : 0);
86 #endif
87 }
88
view_row_activated(GtkTreeView * tree_view UNUSED,GtkTreePath * path UNUSED,GtkTreeViewColumn * column UNUSED,gpointer user_data UNUSED)89 static void view_row_activated(GtkTreeView* tree_view UNUSED, GtkTreePath* path UNUSED, GtkTreeViewColumn* column UNUSED,
90 gpointer user_data UNUSED)
91 {
92 gtr_action_activate("show-torrent-properties");
93 }
94
tree_view_search_equal_func(GtkTreeModel * model,gint column UNUSED,gchar const * key,GtkTreeIter * iter,gpointer search_data UNUSED)95 static gboolean tree_view_search_equal_func(GtkTreeModel* model, gint column UNUSED, gchar const* key, GtkTreeIter* iter,
96 gpointer search_data UNUSED)
97 {
98 gboolean match;
99 char* lower;
100 char const* name = NULL;
101
102 lower = g_strstrip(g_utf8_strdown(key, -1));
103 gtk_tree_model_get(model, iter, MC_NAME_COLLATED, &name, -1);
104 match = strstr(name, lower) != NULL;
105 g_free(lower);
106
107 return !match;
108 }
109
makeview(PrivateData * p)110 static GtkWidget* makeview(PrivateData* p)
111 {
112 GtkWidget* view;
113 GtkTreeViewColumn* col;
114 GtkTreeSelection* sel;
115 GtkCellRenderer* r;
116 GtkTreeView* tree_view;
117
118 view = gtk_tree_view_new();
119 tree_view = GTK_TREE_VIEW(view);
120 gtk_tree_view_set_search_column(tree_view, MC_NAME_COLLATED);
121 gtk_tree_view_set_search_equal_func(tree_view, tree_view_search_equal_func, NULL, NULL);
122 gtk_tree_view_set_headers_visible(tree_view, FALSE);
123 gtk_tree_view_set_fixed_height_mode(tree_view, TRUE);
124
125 p->selection = gtk_tree_view_get_selection(tree_view);
126
127 p->column = col = GTK_TREE_VIEW_COLUMN(g_object_new(GTK_TYPE_TREE_VIEW_COLUMN, "title", _("Torrent"), "resizable", TRUE,
128 "sizing", GTK_TREE_VIEW_COLUMN_FIXED, NULL));
129
130 p->renderer = r = torrent_cell_renderer_new();
131 gtk_tree_view_column_pack_start(col, r, FALSE);
132 gtk_tree_view_column_add_attribute(col, r, "torrent", MC_TORRENT);
133 gtk_tree_view_column_add_attribute(col, r, "piece-upload-speed", MC_SPEED_UP);
134 gtk_tree_view_column_add_attribute(col, r, "piece-download-speed", MC_SPEED_DOWN);
135
136 gtk_tree_view_append_column(tree_view, col);
137 g_object_set(r, "xpad", GUI_PAD_SMALL, "ypad", GUI_PAD_SMALL, NULL);
138
139 sel = gtk_tree_view_get_selection(tree_view);
140 gtk_tree_selection_set_mode(GTK_TREE_SELECTION(sel), GTK_SELECTION_MULTIPLE);
141
142 g_signal_connect(view, "popup-menu", G_CALLBACK(on_popup_menu), NULL);
143 g_signal_connect(view, "button-press-event", G_CALLBACK(on_tree_view_button_pressed), (void*)on_popup_menu);
144 g_signal_connect(view, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
145 g_signal_connect(view, "row-activated", G_CALLBACK(view_row_activated), NULL);
146
147 gtk_tree_view_set_model(tree_view, p->filter_model);
148 g_object_unref(p->filter_model);
149
150 return view;
151 }
152
153 static void syncAltSpeedButton(PrivateData* p);
154
prefsChanged(TrCore * core UNUSED,tr_quark const key,gpointer wind)155 static void prefsChanged(TrCore* core UNUSED, tr_quark const key, gpointer wind)
156 {
157 gboolean isEnabled;
158 PrivateData* p = get_private_data(GTK_WINDOW(wind));
159
160 switch (key)
161 {
162 case TR_KEY_compact_view:
163 g_object_set(p->renderer, "compact", gtr_pref_flag_get(key), NULL);
164 /* since the cell size has changed, we need gtktreeview to revalidate
165 * its fixed-height mode values. Unfortunately there's not an API call
166 * for that, but it *does* revalidate when it thinks the style's been tweaked */
167 g_signal_emit_by_name(p->view, "style-updated", NULL, NULL);
168 break;
169
170 case TR_KEY_show_statusbar:
171 isEnabled = gtr_pref_flag_get(key);
172 g_object_set(p->status, "visible", isEnabled, NULL);
173 break;
174
175 case TR_KEY_show_filterbar:
176 isEnabled = gtr_pref_flag_get(key);
177 g_object_set(p->filter, "visible", isEnabled, NULL);
178 break;
179
180 case TR_KEY_show_toolbar:
181 isEnabled = gtr_pref_flag_get(key);
182 g_object_set(p->toolbar, "visible", isEnabled, NULL);
183 break;
184
185 case TR_KEY_statusbar_stats:
186 gtr_window_refresh(wind);
187 break;
188
189 case TR_KEY_alt_speed_enabled:
190 case TR_KEY_alt_speed_up:
191 case TR_KEY_alt_speed_down:
192 syncAltSpeedButton(p);
193 break;
194
195 default:
196 break;
197 }
198 }
199
privateFree(gpointer vprivate)200 static void privateFree(gpointer vprivate)
201 {
202 PrivateData* p = vprivate;
203 g_signal_handler_disconnect(p->core, p->pref_handler_id);
204 g_free(p);
205 }
206
onYinYangClicked(GtkWidget * w UNUSED,gpointer vprivate)207 static void onYinYangClicked(GtkWidget* w UNUSED, gpointer vprivate)
208 {
209 PrivateData* p = vprivate;
210
211 #if GTK_CHECK_VERSION(3, 22, 0)
212 gtk_menu_popup_at_widget(GTK_MENU(p->status_menu), GTK_WIDGET(w), GDK_GRAVITY_NORTH_EAST, GDK_GRAVITY_SOUTH_EAST, NULL);
213 #else
214 gtk_menu_popup(GTK_MENU(p->status_menu), NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
215 #endif
216 }
217
218 #define STATS_MODE "stats-mode"
219
220 static struct
221 {
222 char const* val;
223 char const* i18n;
224 }
225 stats_modes[] =
226 {
227 { "total-ratio", N_("Total Ratio") },
228 { "session-ratio", N_("Session Ratio") },
229 { "total-transfer", N_("Total Transfer") },
230 { "session-transfer", N_("Session Transfer") }
231 };
232
status_menu_toggled_cb(GtkCheckMenuItem * menu_item,gpointer vprivate)233 static void status_menu_toggled_cb(GtkCheckMenuItem* menu_item, gpointer vprivate)
234 {
235 if (gtk_check_menu_item_get_active(menu_item))
236 {
237 PrivateData* p = vprivate;
238 char const* val = g_object_get_data(G_OBJECT(menu_item), STATS_MODE);
239 gtr_core_set_pref(p->core, TR_KEY_statusbar_stats, val);
240 }
241 }
242
syncAltSpeedButton(PrivateData * p)243 static void syncAltSpeedButton(PrivateData* p)
244 {
245 char u[32];
246 char d[32];
247 char* str;
248 char const* fmt;
249 gboolean const b = gtr_pref_flag_get(TR_KEY_alt_speed_enabled);
250 char const* stock = b ? "alt-speed-on" : "alt-speed-off";
251 GtkWidget* w = p->alt_speed_button;
252
253 tr_formatter_speed_KBps(u, gtr_pref_int_get(TR_KEY_alt_speed_up), sizeof(u));
254 tr_formatter_speed_KBps(d, gtr_pref_int_get(TR_KEY_alt_speed_down), sizeof(d));
255 fmt = b ? _("Click to disable Alternative Speed Limits\n (%1$s down, %2$s up)") :
256 _("Click to enable Alternative Speed Limits\n (%1$s down, %2$s up)");
257 str = g_strdup_printf(fmt, d, u);
258
259 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
260 gtk_image_set_from_stock(GTK_IMAGE(p->alt_speed_image), stock, -1);
261 g_object_set(w, "halign", GTK_ALIGN_CENTER, "valign", GTK_ALIGN_CENTER, NULL);
262 gtk_widget_set_tooltip_text(w, str);
263
264 g_free(str);
265 }
266
alt_speed_toggled_cb(GtkToggleButton * button,gpointer vprivate)267 static void alt_speed_toggled_cb(GtkToggleButton* button, gpointer vprivate)
268 {
269 PrivateData* p = vprivate;
270 gboolean const b = gtk_toggle_button_get_active(button);
271 gtr_core_set_pref_bool(p->core, TR_KEY_alt_speed_enabled, b);
272 }
273
274 /***
275 **** FILTER
276 ***/
277
findMaxAnnounceTime(GtkTreeModel * model,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer gmaxTime)278 static void findMaxAnnounceTime(GtkTreeModel* model, GtkTreePath* path UNUSED, GtkTreeIter* iter, gpointer gmaxTime)
279 {
280 tr_torrent* tor;
281 tr_stat const* torStat;
282 time_t* maxTime = gmaxTime;
283
284 gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);
285 torStat = tr_torrentStatCached(tor);
286 *maxTime = MAX(*maxTime, torStat->manualAnnounceTime);
287 }
288
onAskTrackerQueryTooltip(GtkWidget * widget UNUSED,gint x UNUSED,gint y UNUSED,gboolean keyboard_tip UNUSED,GtkTooltip * tooltip,gpointer gdata)289 static gboolean onAskTrackerQueryTooltip(GtkWidget* widget UNUSED, gint x UNUSED, gint y UNUSED, gboolean keyboard_tip UNUSED,
290 GtkTooltip* tooltip, gpointer gdata)
291 {
292 gboolean handled;
293 time_t maxTime = 0;
294 PrivateData* p = gdata;
295 time_t const now = time(NULL);
296
297 gtk_tree_selection_selected_foreach(p->selection, findMaxAnnounceTime, &maxTime);
298
299 if (maxTime <= now)
300 {
301 handled = FALSE;
302 }
303 else
304 {
305 char buf[512];
306 char timebuf[64];
307 int const seconds = maxTime - now;
308
309 tr_strltime(timebuf, seconds, sizeof(timebuf));
310 g_snprintf(buf, sizeof(buf), _("Tracker will allow requests in %s"), timebuf);
311 gtk_tooltip_set_text(tooltip, buf);
312 handled = TRUE;
313 }
314
315 return handled;
316 }
317
onAltSpeedToggledIdle(gpointer vp)318 static gboolean onAltSpeedToggledIdle(gpointer vp)
319 {
320 PrivateData* p = vp;
321 gboolean b = tr_sessionUsesAltSpeed(gtr_core_session(p->core));
322 gtr_core_set_pref_bool(p->core, TR_KEY_alt_speed_enabled, b);
323
324 return G_SOURCE_REMOVE;
325 }
326
onAltSpeedToggled(tr_session * s UNUSED,bool isEnabled UNUSED,bool byUser UNUSED,void * p)327 static void onAltSpeedToggled(tr_session* s UNUSED, bool isEnabled UNUSED, bool byUser UNUSED, void* p)
328 {
329 gdk_threads_add_idle(onAltSpeedToggledIdle, p);
330 }
331
332 /***
333 **** Speed limit menu
334 ***/
335
336 #define DIRECTION_KEY "direction-key"
337 #define ENABLED_KEY "enabled-key"
338 #define SPEED_KEY "speed-key"
339
onSpeedToggled(GtkCheckMenuItem * check,gpointer vp)340 static void onSpeedToggled(GtkCheckMenuItem* check, gpointer vp)
341 {
342 PrivateData* p = vp;
343 GObject* o = G_OBJECT(check);
344 gboolean isEnabled = g_object_get_data(o, ENABLED_KEY) != 0;
345 tr_direction dir = GPOINTER_TO_INT(g_object_get_data(o, DIRECTION_KEY));
346 tr_quark const key = dir == TR_UP ? TR_KEY_speed_limit_up_enabled : TR_KEY_speed_limit_down_enabled;
347
348 if (gtk_check_menu_item_get_active(check))
349 {
350 gtr_core_set_pref_bool(p->core, key, isEnabled);
351 }
352 }
353
onSpeedSet(GtkCheckMenuItem * check,gpointer vp)354 static void onSpeedSet(GtkCheckMenuItem* check, gpointer vp)
355 {
356 tr_quark key;
357 PrivateData* p = vp;
358 GObject* o = G_OBJECT(check);
359 int const KBps = GPOINTER_TO_INT(g_object_get_data(o, SPEED_KEY));
360 tr_direction dir = GPOINTER_TO_INT(g_object_get_data(o, DIRECTION_KEY));
361
362 key = dir == TR_UP ? TR_KEY_speed_limit_up : TR_KEY_speed_limit_down;
363 gtr_core_set_pref_int(p->core, key, KBps);
364
365 key = dir == TR_UP ? TR_KEY_speed_limit_up_enabled : TR_KEY_speed_limit_down_enabled;
366 gtr_core_set_pref_bool(p->core, key, TRUE);
367 }
368
createSpeedMenu(PrivateData * p,tr_direction dir)369 static GtkWidget* createSpeedMenu(PrivateData* p, tr_direction dir)
370 {
371 GObject* o;
372 GtkWidget* w;
373 GtkWidget* m;
374 GtkMenuShell* menu_shell;
375 int const speeds_KBps[] = { 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500, 750 };
376
377 m = gtk_menu_new();
378 menu_shell = GTK_MENU_SHELL(m);
379
380 w = gtk_radio_menu_item_new_with_label(NULL, _("Unlimited"));
381 o = G_OBJECT(w);
382 p->speedlimit_off_item[dir] = w;
383 g_object_set_data(o, DIRECTION_KEY, GINT_TO_POINTER(dir));
384 g_object_set_data(o, ENABLED_KEY, GINT_TO_POINTER(FALSE));
385 g_signal_connect(w, "toggled", G_CALLBACK(onSpeedToggled), p);
386 gtk_menu_shell_append(menu_shell, w);
387
388 w = gtk_radio_menu_item_new_with_label_from_widget(GTK_RADIO_MENU_ITEM(w), "");
389 o = G_OBJECT(w);
390 p->speedlimit_on_item[dir] = w;
391 g_object_set_data(o, DIRECTION_KEY, GINT_TO_POINTER(dir));
392 g_object_set_data(o, ENABLED_KEY, GINT_TO_POINTER(TRUE));
393 g_signal_connect(w, "toggled", G_CALLBACK(onSpeedToggled), p);
394 gtk_menu_shell_append(menu_shell, w);
395
396 w = gtk_separator_menu_item_new();
397 gtk_menu_shell_append(menu_shell, w);
398
399 for (size_t i = 0; i < G_N_ELEMENTS(speeds_KBps); ++i)
400 {
401 char buf[128];
402 tr_formatter_speed_KBps(buf, speeds_KBps[i], sizeof(buf));
403 w = gtk_menu_item_new_with_label(buf);
404 o = G_OBJECT(w);
405 g_object_set_data(o, DIRECTION_KEY, GINT_TO_POINTER(dir));
406 g_object_set_data(o, SPEED_KEY, GINT_TO_POINTER(speeds_KBps[i]));
407 g_signal_connect(w, "activate", G_CALLBACK(onSpeedSet), p);
408 gtk_menu_shell_append(menu_shell, w);
409 }
410
411 return m;
412 }
413
414 /***
415 **** Speed limit menu
416 ***/
417
418 #define RATIO_KEY "stock-ratio-index"
419
420 static double const stockRatios[] = { 0.25, 0.5, 0.75, 1, 1.5, 2, 3 };
421
onRatioToggled(GtkCheckMenuItem * check,gpointer vp)422 static void onRatioToggled(GtkCheckMenuItem* check, gpointer vp)
423 {
424 PrivateData* p = vp;
425
426 if (gtk_check_menu_item_get_active(check))
427 {
428 gboolean f = g_object_get_data(G_OBJECT(check), ENABLED_KEY) != 0;
429 gtr_core_set_pref_bool(p->core, TR_KEY_ratio_limit_enabled, f);
430 }
431 }
432
onRatioSet(GtkCheckMenuItem * check,gpointer vp)433 static void onRatioSet(GtkCheckMenuItem* check, gpointer vp)
434 {
435 PrivateData* p = vp;
436 int i = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(check), RATIO_KEY));
437 double const ratio = stockRatios[i];
438 gtr_core_set_pref_double(p->core, TR_KEY_ratio_limit, ratio);
439 gtr_core_set_pref_bool(p->core, TR_KEY_ratio_limit_enabled, TRUE);
440 }
441
createRatioMenu(PrivateData * p)442 static GtkWidget* createRatioMenu(PrivateData* p)
443 {
444 GtkWidget* m;
445 GtkWidget* w;
446 GtkMenuShell* menu_shell;
447
448 m = gtk_menu_new();
449 menu_shell = GTK_MENU_SHELL(m);
450
451 w = gtk_radio_menu_item_new_with_label(NULL, _("Seed Forever"));
452 p->ratio_off_item = w;
453 g_object_set_data(G_OBJECT(w), ENABLED_KEY, GINT_TO_POINTER(FALSE));
454 g_signal_connect(w, "toggled", G_CALLBACK(onRatioToggled), p);
455 gtk_menu_shell_append(menu_shell, w);
456
457 w = gtk_radio_menu_item_new_with_label_from_widget(GTK_RADIO_MENU_ITEM(w), "");
458 p->ratio_on_item = w;
459 g_object_set_data(G_OBJECT(w), ENABLED_KEY, GINT_TO_POINTER(TRUE));
460 g_signal_connect(w, "toggled", G_CALLBACK(onRatioToggled), p);
461 gtk_menu_shell_append(menu_shell, w);
462
463 w = gtk_separator_menu_item_new();
464 gtk_menu_shell_append(menu_shell, w);
465
466 for (size_t i = 0; i < G_N_ELEMENTS(stockRatios); ++i)
467 {
468 char buf[128];
469 tr_strlratio(buf, stockRatios[i], sizeof(buf));
470 w = gtk_menu_item_new_with_label(buf);
471 g_object_set_data(G_OBJECT(w), RATIO_KEY, GINT_TO_POINTER(i));
472 g_signal_connect(w, "activate", G_CALLBACK(onRatioSet), p);
473 gtk_menu_shell_append(menu_shell, w);
474 }
475
476 return m;
477 }
478
479 /***
480 **** Option menu
481 ***/
482
createOptionsMenu(PrivateData * p)483 static GtkWidget* createOptionsMenu(PrivateData* p)
484 {
485 GtkWidget* m;
486 GtkWidget* top = gtk_menu_new();
487 GtkMenuShell* menu_shell = GTK_MENU_SHELL(top);
488
489 m = gtk_menu_item_new_with_label(_("Limit Download Speed"));
490 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m), createSpeedMenu(p, TR_DOWN));
491 gtk_menu_shell_append(menu_shell, m);
492
493 m = gtk_menu_item_new_with_label(_("Limit Upload Speed"));
494 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m), createSpeedMenu(p, TR_UP));
495 gtk_menu_shell_append(menu_shell, m);
496
497 m = gtk_separator_menu_item_new();
498 gtk_menu_shell_append(menu_shell, m);
499
500 m = gtk_menu_item_new_with_label(_("Stop Seeding at Ratio"));
501 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m), createRatioMenu(p));
502 gtk_menu_shell_append(menu_shell, m);
503
504 gtk_widget_show_all(top);
505 return top;
506 }
507
onOptionsClicked(GtkButton * button,gpointer vp)508 static void onOptionsClicked(GtkButton* button, gpointer vp)
509 {
510 char buf1[512];
511 char buf2[512];
512 gboolean b;
513 GtkWidget* w;
514 PrivateData* p = vp;
515
516 w = p->speedlimit_on_item[TR_DOWN];
517 tr_formatter_speed_KBps(buf1, gtr_pref_int_get(TR_KEY_speed_limit_down), sizeof(buf1));
518 gtr_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(w))), buf1);
519
520 b = gtr_pref_flag_get(TR_KEY_speed_limit_down_enabled);
521 w = b ? p->speedlimit_on_item[TR_DOWN] : p->speedlimit_off_item[TR_DOWN];
522 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(w), TRUE);
523
524 w = p->speedlimit_on_item[TR_UP];
525 tr_formatter_speed_KBps(buf1, gtr_pref_int_get(TR_KEY_speed_limit_up), sizeof(buf1));
526 gtr_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(w))), buf1);
527
528 b = gtr_pref_flag_get(TR_KEY_speed_limit_up_enabled);
529 w = b ? p->speedlimit_on_item[TR_UP] : p->speedlimit_off_item[TR_UP];
530 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(w), TRUE);
531
532 tr_strlratio(buf1, gtr_pref_double_get(TR_KEY_ratio_limit), sizeof(buf1));
533 g_snprintf(buf2, sizeof(buf2), _("Stop at Ratio (%s)"), buf1);
534 gtr_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(p->ratio_on_item))), buf2);
535
536 b = gtr_pref_flag_get(TR_KEY_ratio_limit_enabled);
537 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(b ? p->ratio_on_item : p->ratio_off_item), TRUE);
538
539 #if GTK_CHECK_VERSION(3, 22, 0)
540 gtk_menu_popup_at_widget(GTK_MENU(p->options_menu), GTK_WIDGET(button), GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_SOUTH_WEST,
541 NULL);
542 #else
543 gtk_menu_popup(GTK_MENU(p->options_menu), NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
544 #endif
545 }
546
547 /***
548 **** PUBLIC
549 ***/
550
gtr_window_new(GtkApplication * app,GtkUIManager * ui_mgr,TrCore * core)551 GtkWidget* gtr_window_new(GtkApplication* app, GtkUIManager* ui_mgr, TrCore* core)
552 {
553 char const* pch;
554 char const* style;
555 PrivateData* p;
556 GtkWidget* ul_lb;
557 GtkWidget* dl_lb;
558 GtkWidget* mainmenu;
559 GtkWidget* toolbar;
560 GtkWidget* filter;
561 GtkWidget* list;
562 GtkWidget* status;
563 GtkWidget* vbox;
564 GtkWidget* w;
565 GtkWidget* self;
566 GtkWidget* menu;
567 GtkWidget* grid_w;
568 GtkWindow* win;
569 GtkCssProvider* css_provider;
570 GSList* l;
571 GtkGrid* grid;
572
573 p = g_new0(PrivateData, 1);
574
575 /* make the window */
576 self = gtk_application_window_new(app);
577 g_object_set_qdata_full(G_OBJECT(self), private_data_quark(), p, privateFree);
578 win = GTK_WINDOW(self);
579 gtk_window_set_title(win, g_get_application_name());
580 gtk_window_set_role(win, "tr-main");
581 gtk_window_set_default_size(win, gtr_pref_int_get(TR_KEY_main_window_width), gtr_pref_int_get(TR_KEY_main_window_height));
582 gtk_window_move(win, gtr_pref_int_get(TR_KEY_main_window_x), gtr_pref_int_get(TR_KEY_main_window_y));
583
584 if (gtr_pref_flag_get(TR_KEY_main_window_is_maximized))
585 {
586 gtk_window_maximize(win);
587 }
588
589 gtk_window_add_accel_group(win, gtk_ui_manager_get_accel_group(ui_mgr));
590 /* Add style provider to the window. */
591 /* Please move it to separate .css file if you’re adding more styles here. */
592 style = ".tr-workarea.frame {border-left-width: 0; border-right-width: 0; border-radius: 0;}";
593 css_provider = gtk_css_provider_new();
594 gtk_css_provider_load_from_data(css_provider, style, strlen(style), NULL);
595 gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider),
596 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
597
598 /* window's main container */
599 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
600 gtk_container_add(GTK_CONTAINER(self), vbox);
601
602 /* main menu */
603 mainmenu = gtr_action_get_widget("/main-window-menu");
604 w = gtr_action_get_widget("/main-window-menu/torrent-menu/torrent-reannounce");
605 g_signal_connect(w, "query-tooltip", G_CALLBACK(onAskTrackerQueryTooltip), p);
606
607 /* toolbar */
608 toolbar = p->toolbar = gtr_action_get_widget("/main-window-toolbar");
609 gtk_style_context_add_class(gtk_widget_get_style_context(toolbar), GTK_STYLE_CLASS_PRIMARY_TOOLBAR);
610 gtr_action_set_important("open-torrent-toolbar", TRUE);
611 gtr_action_set_important("show-torrent-properties", TRUE);
612
613 /* filter */
614 w = filter = p->filter = gtr_filter_bar_new(gtr_core_session(core), gtr_core_model(core), &p->filter_model);
615 gtk_container_set_border_width(GTK_CONTAINER(w), GUI_PAD_SMALL);
616
617 /* status menu */
618 menu = p->status_menu = gtk_menu_new();
619 l = NULL;
620 pch = gtr_pref_string_get(TR_KEY_statusbar_stats);
621
622 for (size_t i = 0; i < G_N_ELEMENTS(stats_modes); ++i)
623 {
624 char const* val = stats_modes[i].val;
625 w = gtk_radio_menu_item_new_with_label(l, _(stats_modes[i].i18n));
626 l = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(w));
627 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(w), g_strcmp0(val, pch) == 0);
628 g_object_set_data(G_OBJECT(w), STATS_MODE, (gpointer)stats_modes[i].val);
629 g_signal_connect(w, "toggled", G_CALLBACK(status_menu_toggled_cb), p);
630 gtk_menu_shell_append(GTK_MENU_SHELL(menu), w);
631 gtk_widget_show(w);
632 }
633
634 /**
635 *** Statusbar
636 **/
637
638 grid_w = status = p->status = gtk_grid_new();
639 gtk_orientable_set_orientation(GTK_ORIENTABLE(grid_w), GTK_ORIENTATION_HORIZONTAL);
640 grid = GTK_GRID(grid_w);
641 gtk_container_set_border_width(GTK_CONTAINER(grid), GUI_PAD_SMALL);
642
643 /* gear */
644 w = gtk_button_new();
645 gtk_container_add(GTK_CONTAINER(w), gtk_image_new_from_icon_name("utilities", GTK_ICON_SIZE_MENU));
646 gtk_widget_set_tooltip_text(w, _("Options"));
647 gtk_button_set_relief(GTK_BUTTON(w), GTK_RELIEF_NONE);
648 p->options_menu = createOptionsMenu(p);
649 g_signal_connect(w, "clicked", G_CALLBACK(onOptionsClicked), p);
650 gtk_container_add(GTK_CONTAINER(grid), w);
651
652 /* turtle */
653 p->alt_speed_image = gtk_image_new();
654 w = p->alt_speed_button = gtk_toggle_button_new();
655 gtk_button_set_image(GTK_BUTTON(w), p->alt_speed_image);
656 gtk_button_set_relief(GTK_BUTTON(w), GTK_RELIEF_NONE);
657 g_signal_connect(w, "toggled", G_CALLBACK(alt_speed_toggled_cb), p);
658 gtk_container_add(GTK_CONTAINER(grid), w);
659
660 /* spacer */
661 w = gtk_fixed_new();
662 gtk_widget_set_hexpand(w, TRUE);
663 gtk_container_add(GTK_CONTAINER(grid), w);
664
665 /* download */
666 w = dl_lb = gtk_label_new(NULL);
667 p->dl_lb = GTK_LABEL(w);
668 gtk_label_set_single_line_mode(p->dl_lb, TRUE);
669 gtk_container_add(GTK_CONTAINER(grid), w);
670
671 /* upload */
672 w = ul_lb = gtk_label_new(NULL);
673 g_object_set(G_OBJECT(w), "margin-left", GUI_PAD, NULL);
674 p->ul_lb = GTK_LABEL(w);
675 gtk_label_set_single_line_mode(p->ul_lb, TRUE);
676 gtk_container_add(GTK_CONTAINER(grid), w);
677
678 /* ratio */
679 w = gtk_label_new(NULL);
680 g_object_set(G_OBJECT(w), "margin-left", GUI_PAD_BIG, NULL);
681 p->stats_lb = GTK_LABEL(w);
682 gtk_label_set_single_line_mode(p->stats_lb, TRUE);
683 gtk_container_add(GTK_CONTAINER(grid), w);
684
685 /* ratio selector */
686 w = gtk_button_new();
687 gtk_widget_set_tooltip_text(w, _("Statistics"));
688 gtk_container_add(GTK_CONTAINER(w), gtk_image_new_from_icon_name("ratio", GTK_ICON_SIZE_MENU));
689 gtk_button_set_relief(GTK_BUTTON(w), GTK_RELIEF_NONE);
690 g_signal_connect(w, "clicked", G_CALLBACK(onYinYangClicked), p);
691 gtk_container_add(GTK_CONTAINER(grid), w);
692
693 /**
694 *** Workarea
695 **/
696
697 p->view = makeview(p);
698 w = list = p->scroll = gtk_scrolled_window_new(NULL, NULL);
699 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
700 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_OUT);
701 gtk_style_context_add_class(gtk_widget_get_style_context(w), "tr-workarea");
702 gtk_container_add(GTK_CONTAINER(w), p->view);
703
704 /* lay out the widgets */
705 gtk_box_pack_start(GTK_BOX(vbox), mainmenu, FALSE, FALSE, 0);
706 gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
707 gtk_box_pack_start(GTK_BOX(vbox), filter, FALSE, FALSE, 0);
708 gtk_box_pack_start(GTK_BOX(vbox), list, TRUE, TRUE, 0);
709 gtk_box_pack_start(GTK_BOX(vbox), status, FALSE, FALSE, 0);
710
711 {
712 /* this is to determine the maximum width/height for the label */
713 int w = 0;
714 int h = 0;
715 PangoLayout* pango_layout;
716 pango_layout = gtk_widget_create_pango_layout(ul_lb, "999.99 kB/s");
717 pango_layout_get_pixel_size(pango_layout, &w, &h);
718 gtk_widget_set_size_request(ul_lb, w, h);
719 gtk_widget_set_size_request(dl_lb, w, h);
720 g_object_set(ul_lb, "halign", GTK_ALIGN_END, "valign", GTK_ALIGN_CENTER, NULL);
721 g_object_set(dl_lb, "halign", GTK_ALIGN_END, "valign", GTK_ALIGN_CENTER, NULL);
722 g_object_unref(G_OBJECT(pango_layout));
723 }
724
725 /* show all but the window */
726 gtk_widget_show_all(vbox);
727
728 /* listen for prefs changes that affect the window */
729 p->core = core;
730 prefsChanged(core, TR_KEY_compact_view, self);
731 prefsChanged(core, TR_KEY_show_filterbar, self);
732 prefsChanged(core, TR_KEY_show_statusbar, self);
733 prefsChanged(core, TR_KEY_statusbar_stats, self);
734 prefsChanged(core, TR_KEY_show_toolbar, self);
735 prefsChanged(core, TR_KEY_alt_speed_enabled, self);
736 p->pref_handler_id = g_signal_connect(core, "prefs-changed", G_CALLBACK(prefsChanged), self);
737
738 tr_sessionSetAltSpeedFunc(gtr_core_session(core), onAltSpeedToggled, p);
739
740 gtr_window_refresh(GTK_WINDOW(self));
741 return self;
742 }
743
updateStats(PrivateData * p)744 static void updateStats(PrivateData* p)
745 {
746 char const* pch;
747 char up[32];
748 char down[32];
749 char ratio[32];
750 char buf[512];
751 struct tr_session_stats stats;
752 tr_session* session = gtr_core_session(p->core);
753
754 /* update the stats */
755 pch = gtr_pref_string_get(TR_KEY_statusbar_stats);
756
757 if (g_strcmp0(pch, "session-ratio") == 0)
758 {
759 tr_sessionGetStats(session, &stats);
760 tr_strlratio(ratio, stats.ratio, sizeof(ratio));
761 g_snprintf(buf, sizeof(buf), _("Ratio: %s"), ratio);
762 }
763 else if (g_strcmp0(pch, "session-transfer") == 0)
764 {
765 tr_sessionGetStats(session, &stats);
766 tr_strlsize(up, stats.uploadedBytes, sizeof(up));
767 tr_strlsize(down, stats.downloadedBytes, sizeof(down));
768 /* Translators: "size|" is here for disambiguation. Please remove it from your translation.
769 %1$s is the size of the data we've downloaded
770 %2$s is the size of the data we've uploaded */
771 g_snprintf(buf, sizeof(buf), Q_("Down: %1$s, Up: %2$s"), down, up);
772 }
773 else if (g_strcmp0(pch, "total-transfer") == 0)
774 {
775 tr_sessionGetCumulativeStats(session, &stats);
776 tr_strlsize(up, stats.uploadedBytes, sizeof(up));
777 tr_strlsize(down, stats.downloadedBytes, sizeof(down));
778 /* Translators: "size|" is here for disambiguation. Please remove it from your translation.
779 %1$s is the size of the data we've downloaded
780 %2$s is the size of the data we've uploaded */
781 g_snprintf(buf, sizeof(buf), Q_("size|Down: %1$s, Up: %2$s"), down, up);
782 }
783 else /* default is total-ratio */
784 {
785 tr_sessionGetCumulativeStats(session, &stats);
786 tr_strlratio(ratio, stats.ratio, sizeof(ratio));
787 g_snprintf(buf, sizeof(buf), _("Ratio: %s"), ratio);
788 }
789
790 gtr_label_set_text(p->stats_lb, buf);
791 }
792
updateSpeeds(PrivateData * p)793 static void updateSpeeds(PrivateData* p)
794 {
795 tr_session* session = gtr_core_session(p->core);
796
797 if (session != NULL)
798 {
799 char text_str[256];
800 char speed_str[128];
801 double upSpeed = 0;
802 double downSpeed = 0;
803 int upCount = 0;
804 int downCount = 0;
805 GtkTreeIter iter;
806 GtkTreeModel* model = gtr_core_model(p->core);
807
808 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
809 {
810 do
811 {
812 int uc;
813 int dc;
814 double us;
815 double ds;
816 gtk_tree_model_get(model, &iter,
817 MC_SPEED_UP, &us,
818 MC_SPEED_DOWN, &ds,
819 MC_ACTIVE_PEERS_UP, &uc,
820 MC_ACTIVE_PEERS_DOWN, &dc,
821 -1);
822 upSpeed += us;
823 upCount += uc;
824 downSpeed += ds;
825 downCount += dc;
826 }
827 while (gtk_tree_model_iter_next(model, &iter));
828 }
829
830 tr_formatter_speed_KBps(speed_str, downSpeed, sizeof(speed_str));
831 g_snprintf(text_str, sizeof(text_str), "%s %s", speed_str, gtr_get_unicode_string(GTR_UNICODE_DOWN));
832 gtr_label_set_text(p->dl_lb, text_str);
833 gtk_widget_set_visible(GTK_WIDGET(p->dl_lb), (downCount > 0));
834
835 tr_formatter_speed_KBps(speed_str, upSpeed, sizeof(speed_str));
836 g_snprintf(text_str, sizeof(text_str), "%s %s", speed_str, gtr_get_unicode_string(GTR_UNICODE_UP));
837 gtr_label_set_text(p->ul_lb, text_str);
838 gtk_widget_set_visible(GTK_WIDGET(p->ul_lb), ((downCount > 0) || (upCount > 0)));
839 }
840 }
841
gtr_window_refresh(GtkWindow * self)842 void gtr_window_refresh(GtkWindow* self)
843 {
844 PrivateData* p = get_private_data(self);
845
846 if (p != NULL && p->core != NULL && gtr_core_session(p->core) != NULL)
847 {
848 updateSpeeds(p);
849 updateStats(p);
850 }
851 }
852
gtr_window_get_selection(GtkWindow * w)853 GtkTreeSelection* gtr_window_get_selection(GtkWindow* w)
854 {
855 return get_private_data(w)->selection;
856 }
857
gtr_window_set_busy(GtkWindow * win,gboolean isBusy)858 void gtr_window_set_busy(GtkWindow* win, gboolean isBusy)
859 {
860 GtkWidget* w = GTK_WIDGET(win);
861
862 if (w != NULL && gtk_widget_get_realized(w))
863 {
864 GdkDisplay* display = gtk_widget_get_display(w);
865 GdkCursor* cursor = isBusy ? gdk_cursor_new_for_display(display, GDK_WATCH) : NULL;
866
867 gdk_window_set_cursor(gtk_widget_get_window(w), cursor);
868 gdk_display_flush(display);
869
870 g_clear_object(&cursor);
871 }
872 }
873