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, ¤tValue, -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, ¤tValue, -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