1 /*
2  * This file Copyright (C) 2008-2014 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <ctype.h> /* isxdigit() */
10 #include <errno.h>
11 #include <limits.h> /* INT_MAX */
12 #include <stdarg.h>
13 #include <string.h> /* strchr(), strrchr(), strlen(), strstr() */
14 
15 #include <gtk/gtk.h>
16 #include <glib/gi18n.h>
17 #include <gio/gio.h> /* g_file_trash() */
18 
19 #include <libtransmission/transmission.h> /* TR_RATIO_NA, TR_RATIO_INF */
20 #include <libtransmission/error.h>
21 #include <libtransmission/utils.h> /* tr_strratio() */
22 #include <libtransmission/web.h> /* tr_webResponseStr() */
23 #include <libtransmission/version.h> /* SHORT_VERSION_STRING */
24 
25 #include "conf.h"
26 #include "hig.h"
27 #include "tr-core.h"
28 #include "tr-prefs.h"
29 #include "util.h"
30 
31 /***
32 ****  UNITS
33 ***/
34 
35 int const mem_K = 1024;
36 char const* mem_K_str = N_("KiB");
37 char const* mem_M_str = N_("MiB");
38 char const* mem_G_str = N_("GiB");
39 char const* mem_T_str = N_("TiB");
40 
41 int const disk_K = 1000;
42 char const* disk_K_str = N_("kB");
43 char const* disk_M_str = N_("MB");
44 char const* disk_G_str = N_("GB");
45 char const* disk_T_str = N_("TB");
46 
47 int const speed_K = 1000;
48 char const* speed_K_str = N_("kB/s");
49 char const* speed_M_str = N_("MB/s");
50 char const* speed_G_str = N_("GB/s");
51 char const* speed_T_str = N_("TB/s");
52 
53 /***
54 ****
55 ***/
56 
gtr_get_unicode_string(int i)57 char const* gtr_get_unicode_string(int i)
58 {
59     switch (i)
60     {
61     case GTR_UNICODE_UP:
62         return "\xE2\x96\xB4";
63 
64     case GTR_UNICODE_DOWN:
65         return "\xE2\x96\xBE";
66 
67     case GTR_UNICODE_INF:
68         return "\xE2\x88\x9E";
69 
70     case GTR_UNICODE_BULLET:
71         return "\xE2\x88\x99";
72 
73     default:
74         return "err";
75     }
76 }
77 
tr_strlratio(char * buf,double ratio,size_t buflen)78 char* tr_strlratio(char* buf, double ratio, size_t buflen)
79 {
80     return tr_strratio(buf, buflen, ratio, gtr_get_unicode_string(GTR_UNICODE_INF));
81 }
82 
tr_strlpercent(char * buf,double x,size_t buflen)83 char* tr_strlpercent(char* buf, double x, size_t buflen)
84 {
85     return tr_strpercent(buf, x, buflen);
86 }
87 
tr_strlsize(char * buf,guint64 bytes,size_t buflen)88 char* tr_strlsize(char* buf, guint64 bytes, size_t buflen)
89 {
90     if (bytes == 0)
91     {
92         g_strlcpy(buf, Q_("None"), buflen);
93     }
94     else
95     {
96         tr_formatter_size_B(buf, bytes, buflen);
97     }
98 
99     return buf;
100 }
101 
tr_strltime(char * buf,int seconds,size_t buflen)102 char* tr_strltime(char* buf, int seconds, size_t buflen)
103 {
104     int days;
105     int hours;
106     int minutes;
107     char d[128];
108     char h[128];
109     char m[128];
110     char s[128];
111 
112     if (seconds < 0)
113     {
114         seconds = 0;
115     }
116 
117     days = seconds / 86400;
118     hours = (seconds % 86400) / 3600;
119     minutes = (seconds % 3600) / 60;
120     seconds = (seconds % 3600) % 60;
121 
122     g_snprintf(d, sizeof(d), ngettext("%'d day", "%'d days", days), days);
123     g_snprintf(h, sizeof(h), ngettext("%'d hour", "%'d hours", hours), hours);
124     g_snprintf(m, sizeof(m), ngettext("%'d minute", "%'d minutes", minutes), minutes);
125     g_snprintf(s, sizeof(s), ngettext("%'d second", "%'d seconds", seconds), seconds);
126 
127     if (days != 0)
128     {
129         if (days >= 4 || hours == 0)
130         {
131             g_strlcpy(buf, d, buflen);
132         }
133         else
134         {
135             g_snprintf(buf, buflen, "%s, %s", d, h);
136         }
137     }
138     else if (hours != 0)
139     {
140         if (hours >= 4 || minutes == 0)
141         {
142             g_strlcpy(buf, h, buflen);
143         }
144         else
145         {
146             g_snprintf(buf, buflen, "%s, %s", h, m);
147         }
148     }
149     else if (minutes != 0)
150     {
151         if (minutes >= 4 || seconds == 0)
152         {
153             g_strlcpy(buf, m, buflen);
154         }
155         else
156         {
157             g_snprintf(buf, buflen, "%s, %s", m, s);
158         }
159     }
160     else
161     {
162         g_strlcpy(buf, s, buflen);
163     }
164 
165     return buf;
166 }
167 
168 /* pattern-matching text; ie, legaltorrents.com */
gtr_get_host_from_url(char * buf,size_t buflen,char const * url)169 void gtr_get_host_from_url(char* buf, size_t buflen, char const* url)
170 {
171     char host[1024];
172     char const* pch;
173 
174     if ((pch = strstr(url, "://")) != NULL)
175     {
176         size_t const hostlen = strcspn(pch + 3, ":/");
177         size_t const copylen = MIN(hostlen, sizeof(host) - 1);
178         memcpy(host, pch + 3, copylen);
179         host[copylen] = '\0';
180     }
181     else
182     {
183         *host = '\0';
184     }
185 
186     if (tr_addressIsIP(host))
187     {
188         g_strlcpy(buf, url, buflen);
189     }
190     else
191     {
192         char const* first_dot = strchr(host, '.');
193         char const* last_dot = strrchr(host, '.');
194 
195         if (first_dot != NULL && last_dot != NULL && first_dot != last_dot)
196         {
197             g_strlcpy(buf, first_dot + 1, buflen);
198         }
199         else
200         {
201             g_strlcpy(buf, host, buflen);
202         }
203     }
204 }
205 
gtr_is_supported_url(char const * str)206 static gboolean gtr_is_supported_url(char const* str)
207 {
208     return str != NULL && (g_str_has_prefix(str, "ftp://") || g_str_has_prefix(str, "http://") ||
209         g_str_has_prefix(str, "https://"));
210 }
211 
gtr_is_magnet_link(char const * str)212 gboolean gtr_is_magnet_link(char const* str)
213 {
214     return str != NULL && g_str_has_prefix(str, "magnet:?");
215 }
216 
gtr_is_hex_hashcode(char const * str)217 gboolean gtr_is_hex_hashcode(char const* str)
218 {
219     if (str == NULL || strlen(str) != 40)
220     {
221         return FALSE;
222     }
223 
224     for (int i = 0; i < 40; ++i)
225     {
226         if (!isxdigit(str[i]))
227         {
228             return FALSE;
229         }
230     }
231 
232     return TRUE;
233 }
234 
getWindow(GtkWidget * w)235 static GtkWindow* getWindow(GtkWidget* w)
236 {
237     if (w == NULL)
238     {
239         return NULL;
240     }
241 
242     if (GTK_IS_WINDOW(w))
243     {
244         return GTK_WINDOW(w);
245     }
246 
247     return GTK_WINDOW(gtk_widget_get_ancestor(w, GTK_TYPE_WINDOW));
248 }
249 
gtr_add_torrent_error_dialog(GtkWidget * child,int err,tr_torrent * duplicate_torrent,char const * filename)250 void gtr_add_torrent_error_dialog(GtkWidget* child, int err, tr_torrent* duplicate_torrent, char const* filename)
251 {
252     char* secondary;
253     GtkWidget* w;
254     GtkWindow* win = getWindow(child);
255 
256     if (err == TR_PARSE_ERR)
257     {
258         secondary = g_strdup_printf(_("The torrent file \"%s\" contains invalid data."), filename);
259     }
260     else if (err == TR_PARSE_DUPLICATE)
261     {
262         secondary = g_strdup_printf(_("The torrent file \"%s\" is already in use by \"%s.\""), filename,
263             tr_torrentName(duplicate_torrent));
264     }
265     else
266     {
267         secondary = g_strdup_printf(_("The torrent file \"%s\" encountered an unknown error."), filename);
268     }
269 
270     w = gtk_message_dialog_new(win, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
271         _("Error opening torrent"));
272     gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", secondary);
273     g_signal_connect_swapped(w, "response", G_CALLBACK(gtk_widget_destroy), w);
274     gtk_widget_show_all(w);
275     g_free(secondary);
276 }
277 
278 typedef void (* PopupFunc)(GtkWidget*, GdkEventButton*);
279 
280 /* pop up the context menu if a user right-clicks.
281    if the row they right-click on isn't selected, select it. */
282 
on_tree_view_button_pressed(GtkWidget * view,GdkEventButton * event,gpointer func)283 gboolean on_tree_view_button_pressed(GtkWidget* view, GdkEventButton* event, gpointer func)
284 {
285     GtkTreeView* tv = GTK_TREE_VIEW(view);
286 
287     if (event->type == GDK_BUTTON_PRESS && event->button == 3)
288     {
289         GtkTreePath* path;
290         GtkTreeSelection* selection = gtk_tree_view_get_selection(tv);
291 
292         if (gtk_tree_view_get_path_at_pos(tv, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
293         {
294             if (!gtk_tree_selection_path_is_selected(selection, path))
295             {
296                 gtk_tree_selection_unselect_all(selection);
297                 gtk_tree_selection_select_path(selection, path);
298             }
299 
300             gtk_tree_path_free(path);
301         }
302 
303         if (func != NULL)
304         {
305             (*(PopupFunc)func)(view, event);
306         }
307 
308         return TRUE;
309     }
310 
311     return FALSE;
312 }
313 
314 /* if the user clicked in an empty area of the list,
315  * clear all the selections. */
on_tree_view_button_released(GtkWidget * view,GdkEventButton * event,gpointer unused UNUSED)316 gboolean on_tree_view_button_released(GtkWidget* view, GdkEventButton* event, gpointer unused UNUSED)
317 {
318     GtkTreeView* tv = GTK_TREE_VIEW(view);
319 
320     if (!gtk_tree_view_get_path_at_pos(tv, (gint)event->x, (gint)event->y, NULL, NULL, NULL, NULL))
321     {
322         GtkTreeSelection* selection = gtk_tree_view_get_selection(tv);
323         gtk_tree_selection_unselect_all(selection);
324     }
325 
326     return FALSE;
327 }
328 
gtr_file_trash_or_remove(char const * filename,tr_error ** error)329 bool gtr_file_trash_or_remove(char const* filename, tr_error** error)
330 {
331     GFile* file;
332     gboolean trashed = FALSE;
333     bool result = true;
334 
335     g_return_val_if_fail(filename && *filename, false);
336 
337     file = g_file_new_for_path(filename);
338 
339     if (gtr_pref_flag_get(TR_KEY_trash_can_enabled))
340     {
341         GError* err = NULL;
342         trashed = g_file_trash(file, NULL, &err);
343 
344         if (err != NULL)
345         {
346             g_message("Unable to trash file \"%s\": %s", filename, err->message);
347             tr_error_set_literal(error, err->code, err->message);
348             g_clear_error(&err);
349         }
350     }
351 
352     if (!trashed)
353     {
354         GError* err = NULL;
355         g_file_delete(file, NULL, &err);
356 
357         if (err != NULL)
358         {
359             g_message("Unable to delete file \"%s\": %s", filename, err->message);
360             tr_error_clear(error);
361             tr_error_set_literal(error, err->code, err->message);
362             g_clear_error(&err);
363             result = false;
364         }
365     }
366 
367     g_object_unref(G_OBJECT(file));
368     return result;
369 }
370 
gtr_get_help_uri(void)371 char const* gtr_get_help_uri(void)
372 {
373     static char* uri = NULL;
374 
375     if (uri == NULL)
376     {
377         char const* fmt = "https://transmissionbt.com/help/gtk/%d.%dx";
378         uri = g_strdup_printf(fmt, MAJOR_VERSION, MINOR_VERSION / 10);
379     }
380 
381     return uri;
382 }
383 
gtr_open_file(char const * path)384 void gtr_open_file(char const* path)
385 {
386     GFile* file = g_file_new_for_path(path);
387     gchar* uri = g_file_get_uri(file);
388     gtr_open_uri(uri);
389     g_free(uri);
390     g_object_unref(file);
391 }
392 
gtr_open_uri(char const * uri)393 void gtr_open_uri(char const* uri)
394 {
395     if (uri != NULL)
396     {
397         gboolean opened = FALSE;
398 
399         if (!opened)
400         {
401 #if GTK_CHECK_VERSION(3, 22, 0)
402             opened = gtk_show_uri_on_window(NULL, uri, GDK_CURRENT_TIME, NULL);
403 #else
404             opened = gtk_show_uri(NULL, uri, GDK_CURRENT_TIME, NULL);
405 #endif
406         }
407 
408         if (!opened)
409         {
410             opened = g_app_info_launch_default_for_uri(uri, NULL, NULL);
411         }
412 
413         if (!opened)
414         {
415             char* argv[] = { (char*)"xdg-open", (char*)uri, NULL };
416             opened = g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
417         }
418 
419         if (!opened)
420         {
421             g_message("Unable to open \"%s\"", uri);
422         }
423     }
424 }
425 
426 /***
427 ****
428 ***/
429 
gtr_combo_box_set_active_enum(GtkComboBox * combo_box,int value)430 void gtr_combo_box_set_active_enum(GtkComboBox* combo_box, int value)
431 {
432     int i;
433     int currentValue;
434     int const column = 0;
435     GtkTreeIter iter;
436     GtkTreeModel* model = gtk_combo_box_get_model(combo_box);
437 
438     /* do the value and current value match? */
439     if (gtk_combo_box_get_active_iter(combo_box, &iter))
440     {
441         gtk_tree_model_get(model, &iter, column, &currentValue, -1);
442 
443         if (currentValue == value)
444         {
445             return;
446         }
447     }
448 
449     /* find the one to select */
450     i = 0;
451 
452     while (gtk_tree_model_iter_nth_child(model, &iter, NULL, i))
453     {
454         gtk_tree_model_get(model, &iter, column, &currentValue, -1);
455 
456         if (currentValue == value)
457         {
458             gtk_combo_box_set_active_iter(combo_box, &iter);
459             return;
460         }
461 
462         ++i;
463     }
464 }
465 
gtr_combo_box_new_enum(char const * text_1,...)466 GtkWidget* gtr_combo_box_new_enum(char const* text_1, ...)
467 {
468     GtkWidget* w;
469     GtkCellRenderer* r;
470     GtkListStore* store;
471     char const* text;
472 
473     store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING);
474 
475     text = text_1;
476 
477     if (text != NULL)
478     {
479         va_list vl;
480 
481         va_start(vl, text_1);
482 
483         do
484         {
485             int const val = va_arg(vl, int);
486             gtk_list_store_insert_with_values(store, NULL, INT_MAX, 0, val, 1, text, -1);
487             text = va_arg(vl, char const*);
488         }
489         while (text != NULL);
490 
491         va_end(vl);
492     }
493 
494     w = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
495     r = gtk_cell_renderer_text_new();
496     gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(w), r, TRUE);
497     gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(w), r, "text", 1, NULL);
498 
499     /* cleanup */
500     g_object_unref(store);
501     return w;
502 }
503 
gtr_combo_box_get_active_enum(GtkComboBox * combo_box)504 int gtr_combo_box_get_active_enum(GtkComboBox* combo_box)
505 {
506     int value = 0;
507     GtkTreeIter iter;
508 
509     if (gtk_combo_box_get_active_iter(combo_box, &iter))
510     {
511         gtk_tree_model_get(gtk_combo_box_get_model(combo_box), &iter, 0, &value, -1);
512     }
513 
514     return value;
515 }
516 
gtr_priority_combo_new(void)517 GtkWidget* gtr_priority_combo_new(void)
518 {
519     return gtr_combo_box_new_enum(
520         _("High"), TR_PRI_HIGH,
521         _("Normal"), TR_PRI_NORMAL,
522         _("Low"), TR_PRI_LOW,
523         NULL);
524 }
525 
526 /***
527 ****
528 ***/
529 
530 #define GTR_CHILD_HIDDEN "gtr-child-hidden"
531 
gtr_widget_set_visible(GtkWidget * w,gboolean b)532 void gtr_widget_set_visible(GtkWidget* w, gboolean b)
533 {
534     /* toggle the transient children, too */
535     if (GTK_IS_WINDOW(w))
536     {
537         GList* windows = gtk_window_list_toplevels();
538         GtkWindow* window = GTK_WINDOW(w);
539 
540         for (GList* l = windows; l != NULL; l = l->next)
541         {
542             if (!GTK_IS_WINDOW(l->data))
543             {
544                 continue;
545             }
546 
547             if (gtk_window_get_transient_for(GTK_WINDOW(l->data)) != window)
548             {
549                 continue;
550             }
551 
552             if (gtk_widget_get_visible(GTK_WIDGET(l->data)) == b)
553             {
554                 continue;
555             }
556 
557             if (b && g_object_get_data(G_OBJECT(l->data), GTR_CHILD_HIDDEN) != NULL)
558             {
559                 g_object_steal_data(G_OBJECT(l->data), GTR_CHILD_HIDDEN);
560                 gtr_widget_set_visible(GTK_WIDGET(l->data), TRUE);
561             }
562             else if (!b)
563             {
564                 g_object_set_data(G_OBJECT(l->data), GTR_CHILD_HIDDEN, GINT_TO_POINTER(1));
565                 gtr_widget_set_visible(GTK_WIDGET(l->data), FALSE);
566             }
567         }
568 
569         g_list_free(windows);
570     }
571 
572     gtk_widget_set_visible(w, b);
573 }
574 
gtr_dialog_set_content(GtkDialog * dialog,GtkWidget * content)575 void gtr_dialog_set_content(GtkDialog* dialog, GtkWidget* content)
576 {
577     GtkWidget* vbox = gtk_dialog_get_content_area(dialog);
578     gtk_box_pack_start(GTK_BOX(vbox), content, TRUE, TRUE, 0);
579     gtk_widget_show_all(content);
580 }
581 
582 /***
583 ****
584 ***/
585 
gtr_unrecognized_url_dialog(GtkWidget * parent,char const * url)586 void gtr_unrecognized_url_dialog(GtkWidget* parent, char const* url)
587 {
588     char const* xt = "xt=urn:btih";
589 
590     GtkWindow* window = getWindow(parent);
591 
592     GString* gstr = g_string_new(NULL);
593 
594     GtkWidget* w = gtk_message_dialog_new(window, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", _("Unrecognized URL"));
595 
596     g_string_append_printf(gstr, _("Transmission doesn't know how to use \"%s\""), url);
597 
598     if (gtr_is_magnet_link(url) && strstr(url, xt) == NULL)
599     {
600         g_string_append_printf(gstr, "\n \n");
601         g_string_append_printf(gstr, _("This magnet link appears to be intended for something other than BitTorrent. "
602             "BitTorrent magnet links have a section containing \"%s\"."), xt);
603     }
604 
605     gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", gstr->str);
606     g_signal_connect_swapped(w, "response", G_CALLBACK(gtk_widget_destroy), w);
607     gtk_widget_show(w);
608     g_string_free(gstr, TRUE);
609 }
610 
611 /***
612 ****
613 ***/
614 
gtr_paste_clipboard_url_into_entry(GtkWidget * e)615 void gtr_paste_clipboard_url_into_entry(GtkWidget* e)
616 {
617     char* text[] =
618     {
619         g_strstrip(gtk_clipboard_wait_for_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY))),
620         g_strstrip(gtk_clipboard_wait_for_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)))
621     };
622 
623     for (size_t i = 0; i < G_N_ELEMENTS(text); ++i)
624     {
625         char* s = text[i];
626 
627         if (s != NULL && (gtr_is_supported_url(s) || gtr_is_magnet_link(s) || gtr_is_hex_hashcode(s)))
628         {
629             gtk_entry_set_text(GTK_ENTRY(e), s);
630             break;
631         }
632     }
633 
634     for (size_t i = 0; i < G_N_ELEMENTS(text); ++i)
635     {
636         g_free(text[i]);
637     }
638 }
639 
640 /***
641 ****
642 ***/
643 
gtr_label_set_text(GtkLabel * lb,char const * newstr)644 void gtr_label_set_text(GtkLabel* lb, char const* newstr)
645 {
646     char const* oldstr = gtk_label_get_text(lb);
647 
648     if (g_strcmp0(oldstr, newstr) != 0)
649     {
650         gtk_label_set_text(lb, newstr);
651     }
652 }
653 
654 /***
655 ****
656 ***/
657 
658 struct freespace_label_data
659 {
660     guint timer_id;
661     TrCore* core;
662     GtkLabel* label;
663     char* dir;
664 };
665 
666 static void on_freespace_label_core_destroyed(gpointer gdata, GObject* dead_core);
667 static void on_freespace_label_destroyed(gpointer gdata, GObject* dead_label);
668 
freespace_label_data_free(gpointer gdata)669 static void freespace_label_data_free(gpointer gdata)
670 {
671     struct freespace_label_data* data = gdata;
672 
673     if (data->core != NULL)
674     {
675         g_object_weak_unref(G_OBJECT(data->core), on_freespace_label_core_destroyed, data);
676     }
677 
678     if (data->label != NULL)
679     {
680         g_object_weak_ref(G_OBJECT(data->label), on_freespace_label_destroyed, data);
681     }
682 
683     g_source_remove(data->timer_id);
684     g_free(data->dir);
685     g_free(data);
686 }
687 
TR_DEFINE_QUARK(freespace_label_data,freespace_label_data)688 static TR_DEFINE_QUARK(freespace_label_data, freespace_label_data)
689 
690 static void on_freespace_label_core_destroyed(gpointer gdata, GObject* dead_core G_GNUC_UNUSED)
691 {
692     struct freespace_label_data* data = gdata;
693     data->core = NULL;
694     freespace_label_data_free(data);
695 }
696 
on_freespace_label_destroyed(gpointer gdata,GObject * dead_label G_GNUC_UNUSED)697 static void on_freespace_label_destroyed(gpointer gdata, GObject* dead_label G_GNUC_UNUSED)
698 {
699     struct freespace_label_data* data = gdata;
700     data->label = NULL;
701     freespace_label_data_free(data);
702 }
703 
on_freespace_timer(gpointer gdata)704 static gboolean on_freespace_timer(gpointer gdata)
705 {
706     char text[128];
707     char markup[128];
708     int64_t bytes;
709     tr_session* session;
710     struct freespace_label_data* data = gdata;
711 
712     session = gtr_core_session(data->core);
713     bytes = tr_sessionGetDirFreeSpace(session, data->dir);
714 
715     if (bytes < 0)
716     {
717         g_snprintf(text, sizeof(text), _("Error"));
718     }
719     else
720     {
721         char size[128];
722         tr_strlsize(size, bytes, sizeof(size));
723         g_snprintf(text, sizeof(text), _("%s free"), size);
724     }
725 
726     g_snprintf(markup, sizeof(markup), "<i>%s</i>", text);
727     gtk_label_set_markup(data->label, markup);
728 
729     return G_SOURCE_CONTINUE;
730 }
731 
gtr_freespace_label_new(struct _TrCore * core,char const * dir)732 GtkWidget* gtr_freespace_label_new(struct _TrCore* core, char const* dir)
733 {
734     struct freespace_label_data* data;
735 
736     data = g_new0(struct freespace_label_data, 1);
737     data->timer_id = g_timeout_add_seconds(3, on_freespace_timer, data);
738     data->core = core;
739     data->label = GTK_LABEL(gtk_label_new(NULL));
740     data->dir = g_strdup(dir);
741 
742     /* when either the core or the label is destroyed, stop updating */
743     g_object_weak_ref(G_OBJECT(core), on_freespace_label_core_destroyed, data);
744     g_object_weak_ref(G_OBJECT(data->label), on_freespace_label_destroyed, data);
745 
746     g_object_set_qdata(G_OBJECT(data->label), freespace_label_data_quark(), data);
747     on_freespace_timer(data);
748     return GTK_WIDGET(data->label);
749 }
750 
gtr_freespace_label_set_dir(GtkWidget * label,char const * dir)751 void gtr_freespace_label_set_dir(GtkWidget* label, char const* dir)
752 {
753     struct freespace_label_data* data;
754 
755     data = g_object_get_qdata(G_OBJECT(label), freespace_label_data_quark());
756 
757     tr_free(data->dir);
758     data->dir = g_strdup(dir);
759     on_freespace_timer(data);
760 }
761