source: trunk/gtk/main.c @ 204

Last change on this file since 204 was 162, checked in by titer, 16 years ago

Merge from branches/new_api:r161

File size: 35.8 KB
Line 
1/*
2  Copyright (c) 2005-2006 Joshua Elsasser. All rights reserved.
3   
4  Redistribution and use in source and binary forms, with or without
5  modification, are permitted provided that the following conditions
6  are met:
7   
8   1. Redistributions of source code must retain the above copyright
9      notice, this list of conditions and the following disclaimer.
10   2. Redistributions in binary form must reproduce the above copyright
11      notice, this list of conditions and the following disclaimer in the
12      documentation and/or other materials provided with the distribution.
13   
14  THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS "AS IS" AND
15  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24  POSSIBILITY OF SUCH DAMAGE.
25*/
26
27#include <sys/param.h>
28#include <assert.h>
29#include <errno.h>
30#include <signal.h>
31#include <string.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <time.h>
35#include <unistd.h>
36
37#include <gtk/gtk.h>
38#include <glib/gi18n.h>
39#include <glib/gstdio.h>
40
41#include "defines.h"
42
43#include "conf.h"
44#include "dialogs.h"
45#include "transmission.h"
46#include "trcellrenderertorrent.h"
47#include "util.h"
48
49#define TRACKER_EXIT_TIMEOUT    5
50
51struct cbdata {
52  tr_handle_t *tr;
53  GtkWindow *wind;
54  GtkTreeModel *model;
55  GtkTreeView *view;
56  GtkStatusbar *bar;
57  GtkWidget **buttons;
58  guint timer;
59  gboolean prefsopen;
60};
61
62struct exitdata {
63  struct cbdata *cbdata;
64  time_t started;
65  guint timer;
66};
67
68struct pieces {
69  char p[120];
70};
71
72void
73maketypes(void);
74gpointer
75tr_pieces_copy(gpointer);
76void
77tr_pieces_free(gpointer);
78
79void
80makewind(GtkWidget *wind, tr_handle_t *tr, GList *saved);
81GtkWidget *
82makewind_toolbar(struct cbdata *data);
83GtkWidget *
84makewind_list(struct cbdata *data);
85gboolean
86winclose(GtkWidget *widget, GdkEvent *event, gpointer gdata);
87gboolean
88exitcheck(gpointer gdata);
89void
90stoptransmission(tr_handle_t *tr);
91void
92setupdrag(GtkWidget *widget, struct cbdata *data);
93void
94gotdrag(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
95        GtkSelectionData *sel, guint info, guint time, gpointer gdata);
96static void
97stylekludge(GObject *obj, GParamSpec *spec, gpointer gdata);
98void
99fixbuttons(GtkTreeSelection *sel, gpointer gdata);
100void
101dfname(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model,
102       GtkTreeIter *iter, gpointer gdata);
103void
104dfprog(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model,
105       GtkTreeIter *iter, gpointer gdata);
106
107gboolean
108updatemodel(gpointer gdata);
109gboolean
110listclick(GtkWidget *widget, GdkEventButton *event, gpointer gdata);
111gboolean
112listpopup(GtkWidget *widget, gpointer gdata);
113void
114dopopupmenu(GdkEventButton *event, struct cbdata *data,
115            GList *ids, int status);
116void
117killmenu(GtkWidget *menu, gpointer *gdata);
118void
119actionclick(GtkWidget *widget, gpointer gdata);
120void
121findtorrent(GtkTreeModel *model, tr_torrent_t *tor, GtkTreeIter *iter);
122gint
123intrevcmp(gconstpointer a, gconstpointer b);
124void
125doubleclick(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col,
126            gpointer gdata);
127
128gboolean
129addtorrent(void *vdata, const char *torrent, const char *dir, gboolean paused,
130           GList **errs);
131void
132addedtorrents(void *vdata);
133gboolean
134savetorrents(tr_handle_t *tr, GtkWindow *wind);
135void
136orstatus(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
137         gpointer gdata);
138void
139makeidlist(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
140           gpointer gdata);
141void
142maketorrentlist(tr_torrent_t *tor, void *data);
143void
144setupsighandlers(void);
145void
146fatalsig(int sig);
147
148#define TR_TYPE_PIECES_NAME     "tr-type-pieces"
149#define TR_TYPE_PIECES          ((const GType)tr_type_pieces)
150#define TR_PIECES(ptr)          ((struct pieces*)ptr)
151GType tr_type_pieces;
152
153#define LIST_ACTION           "torrent-list-action"
154enum listact { ACT_OPEN, ACT_START, ACT_STOP, ACT_DELETE, ACT_INFO, ACT_PREF };
155#define LIST_ACTION_FROM      "torrent-list-action-from"
156enum listfrom { FROM_BUTTON, FROM_POPUP };
157
158#define LIST_IDS              "torrent-list-ids"
159#define LIST_MENU_WIDGET      "torrent-list-popup-menu-widget"
160
161struct { const gchar *name; const gchar *id; enum listact act; gboolean nomenu;
162  int avail; const char *ttext; const char *tpriv; }
163actionitems[] = {
164  {N_("Add"),         GTK_STOCK_ADD,          ACT_OPEN,   FALSE,  0,
165   N_("Add a new torrent"), "XXX"},
166  {N_("Start"),       GTK_STOCK_EXECUTE,      ACT_START,  FALSE,
167   TR_STATUS_INACTIVE,
168   N_("Start a torrent that is not running"), "XXX"},
169  {N_("Stop"),        GTK_STOCK_STOP,         ACT_STOP,   FALSE,
170   TR_STATUS_ACTIVE,
171   N_("Stop a torrent that is running"), "XXX"},
172  {N_("Remove"),      GTK_STOCK_REMOVE,       ACT_DELETE, FALSE, ~0,
173   N_("Remove a torrent"), "XXX"},
174  {N_("Properties"),  GTK_STOCK_PROPERTIES,   ACT_INFO,   FALSE, ~0,
175   N_("Show additional information about a torrent"), "XXX"},
176  {N_("Preferences"), GTK_STOCK_PREFERENCES,  ACT_PREF,   TRUE,   0,
177   N_("Customize application behavior"), "XXX"},
178};
179
180#define CBDATA_PTR              "callback-data-pointer"
181
182#define SIGCOUNT_MAX            3
183
184static sig_atomic_t global_sigcount = 0;
185static int global_lastsig = 0;
186
187int
188main(int argc, char **argv) {
189  GtkWidget *mainwind, *preferr, *stateerr;
190  char *err;
191  tr_handle_t *tr;
192  GList *saved;
193  const char *pref;
194  long intval;
195
196  setupsighandlers();
197
198  gtk_init(&argc, &argv);
199
200  bindtextdomain("transmission-gtk", LOCALEDIR);
201  textdomain("transmission-gtk");
202
203  g_set_application_name(_("Transmission"));
204
205  tr = tr_init();
206
207  gtk_rc_parse_string(
208    "style \"transmission-standard\" {\n"
209    " GtkDialog::action-area-border = 6\n"
210    " GtkDialog::button-spacing = 12\n"
211    " GtkDialog::content-area-border = 6\n"
212    "}\n"
213    "widget \"TransmissionDialog\" style \"transmission-standard\"\n");
214
215  if(cf_init(tr_getPrefsDirectory(), &err)) {
216    if(cf_lock(&err)) {
217      /* create main window now so any error dialogs can be it's children */
218      mainwind = gtk_window_new(GTK_WINDOW_TOPLEVEL);
219      preferr = NULL;
220      stateerr = NULL;
221
222      if(!cf_loadprefs(&err)) {
223        preferr = errmsg(GTK_WINDOW(mainwind), "%s", err);
224        g_free(err);
225      }
226      saved = cf_loadstate(&err);
227      if(NULL != err) {
228        stateerr = errmsg(GTK_WINDOW(mainwind), "%s", err);
229        g_free(err);
230      }
231
232      /* set the upload limit */
233      setlimit(tr);
234
235      /* set the listening port */
236      if(NULL != (pref = cf_getpref(PREF_PORT)) &&
237         0 < (intval = strtol(pref, NULL, 10)) && 0xffff >= intval)
238        tr_setBindPort(tr, intval);
239
240      maketypes();
241      makewind(mainwind, tr, saved);
242
243      if(NULL != preferr)
244        gtk_widget_show_all(preferr);
245      if(NULL != stateerr)
246        gtk_widget_show_all(stateerr);
247    } else {
248      gtk_widget_show(errmsg_full(NULL, (callbackfunc_t)gtk_main_quit,
249                                  NULL, "%s", err));
250      g_free(err);
251    }
252  } else {
253    gtk_widget_show(errmsg_full(NULL, (callbackfunc_t)gtk_main_quit,
254                                NULL, "%s", err));
255    g_free(err);
256  }
257
258  gtk_main();
259
260  return 0;
261}
262
263void
264maketypes(void) {
265  tr_type_pieces = g_boxed_type_register_static(
266    TR_TYPE_PIECES_NAME, tr_pieces_copy, tr_pieces_free);
267}
268
269gpointer
270tr_pieces_copy(gpointer data) {
271  return g_memdup(data, sizeof(struct pieces));
272}
273
274void
275tr_pieces_free(gpointer data) {
276  g_free(data);
277}
278
279void
280makewind(GtkWidget *wind, tr_handle_t *tr, GList *saved) {
281  GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
282  GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
283  GtkWidget *status = gtk_statusbar_new();
284  struct cbdata *data = g_new0(struct cbdata, 1);
285  GtkWidget *list;
286  GtkRequisition req;
287  GList *loaderrs, *ii;
288  struct cf_torrentstate *ts;
289  gint height;
290  char *str;
291
292  data->tr = tr;
293  data->wind = GTK_WINDOW(wind);
294  data->timer = -1;
295  /* filled in by makewind_list */
296  data->model = NULL;
297  data->view = NULL;
298  data->bar = GTK_STATUSBAR(status);
299  data->buttons = NULL;
300  data->prefsopen = FALSE;
301
302  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER,
303                                 GTK_POLICY_AUTOMATIC);
304
305  gtk_box_pack_start(GTK_BOX(vbox), makewind_toolbar(data), FALSE, FALSE, 0);
306
307  list = makewind_list(data);
308  gtk_container_add(GTK_CONTAINER(scroll), list);
309  gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
310
311  gtk_statusbar_push(GTK_STATUSBAR(status), 0, "");
312  gtk_box_pack_start(GTK_BOX(vbox), status, FALSE, FALSE, 0);
313
314  gtk_container_add(GTK_CONTAINER(wind), vbox);
315  gtk_window_set_title(data->wind, g_get_application_name());
316  g_signal_connect(G_OBJECT(wind), "delete_event", G_CALLBACK(winclose), data);
317
318  setupdrag(list, data);
319
320  loaderrs = NULL;
321  for(ii = g_list_first(saved); NULL != ii; ii = ii->next) {
322    ts = ii->data;
323    addtorrent(data, ts->ts_torrent, ts->ts_directory, ts->ts_paused,
324               &loaderrs);
325    cf_freestate(ts);
326  }
327  g_list_free(saved);
328
329  if(NULL != loaderrs) {
330    str = joinstrlist(loaderrs, "\n");
331    errmsg(GTK_WINDOW(wind), ngettext("Failed to load the torrent file %s",
332                                      "Failed to load the torrent files:\n%s",
333                                      g_list_length(loaderrs)), str);
334    g_list_foreach(loaderrs, (GFunc)g_free, NULL);
335    g_list_free(loaderrs);
336    g_free(str);
337    savetorrents(tr, GTK_WINDOW(wind));
338  }
339
340  data->timer = g_timeout_add(500, updatemodel, data);
341  updatemodel(data);
342
343  gtk_widget_show_all(vbox);
344  gtk_widget_realize(wind);
345
346  gtk_widget_size_request(list, &req);
347  height = req.height;
348  gtk_widget_size_request(scroll, &req);
349  height -= req.height;
350  gtk_widget_size_request(wind, &req);
351  height += req.height;
352  gtk_window_set_default_size(GTK_WINDOW(wind), -1, (height > req.width ?
353     MIN(height, req.width * 8 / 5) : MAX(height, req.width * 5 / 8)));
354
355  gtk_widget_show(wind);
356}
357
358GtkWidget *
359makewind_toolbar(struct cbdata *data) {
360  GtkWidget *bar = gtk_toolbar_new();
361  GtkToolItem *item;
362  unsigned int ii;
363
364  gtk_toolbar_set_tooltips(GTK_TOOLBAR(bar), TRUE);
365  gtk_toolbar_set_show_arrow(GTK_TOOLBAR(bar), FALSE);
366  gtk_toolbar_set_style(GTK_TOOLBAR(bar), GTK_TOOLBAR_BOTH);
367
368  data->buttons = g_new(GtkWidget*, ALEN(actionitems));
369
370  for(ii = 0; ii < ALEN(actionitems); ii++) {
371    item = gtk_tool_button_new_from_stock(actionitems[ii].id);
372    data->buttons[ii] = GTK_WIDGET(item);
373    gtk_tool_button_set_label(GTK_TOOL_BUTTON(item),
374                              gettext(actionitems[ii].name));
375    gtk_tool_item_set_tooltip(GTK_TOOL_ITEM(item), GTK_TOOLBAR(bar)->tooltips,
376                              gettext(actionitems[ii].ttext),
377                              actionitems[ii].tpriv);
378    g_object_set_data(G_OBJECT(item), LIST_ACTION,
379                      GINT_TO_POINTER(actionitems[ii].act));
380    g_object_set_data(G_OBJECT(item), LIST_ACTION_FROM,
381                      GINT_TO_POINTER(FROM_BUTTON));
382    g_signal_connect(G_OBJECT(item), "clicked", G_CALLBACK(actionclick), data);
383    gtk_toolbar_insert(GTK_TOOLBAR(bar), item, -1);
384  }
385
386  return bar;
387}
388
389/* XXX check for unused data in model */
390enum {MC_NAME, MC_SIZE, MC_STAT, MC_ERR, MC_TERR, MC_PROG, MC_DRATE, MC_URATE,
391      MC_ETA, MC_PEERS, MC_UPEERS, MC_DPEERS, MC_PIECES, MC_DOWN, MC_UP,
392      MC_TORRENT, MC_ROW_COUNT};
393
394GtkWidget *
395makewind_list(struct cbdata *data) {
396  GType types[] = {
397    /* info->name, info->totalSize, status,     error,      trackerError, */
398    G_TYPE_STRING, G_TYPE_UINT64,   G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING,
399    /* progress,  rateDownload, rateUpload,   eta,        peersTotal, */
400    G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_INT, G_TYPE_INT,
401    /* peersUploading, peersDownloading, pieces,         downloaded, */
402    G_TYPE_INT,        G_TYPE_INT,       TR_TYPE_PIECES, G_TYPE_UINT64,
403    /* uploaded,   the handle for the torrent */
404    G_TYPE_UINT64, G_TYPE_POINTER};
405  GtkListStore *store;
406  GtkWidget *view;
407  GtkTreeViewColumn *col;
408  GtkTreeSelection *sel;
409  GtkCellRenderer *namerend, *progrend;
410  char *str;
411
412  assert(MC_ROW_COUNT == ALEN(types));
413
414  store = gtk_list_store_newv(MC_ROW_COUNT, types);
415  view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
416  /* XXX do I need to worry about reference counts anywhere else? */
417  g_object_unref(G_OBJECT(store));
418  data->model = GTK_TREE_MODEL(store);
419  data->view = GTK_TREE_VIEW(view);
420
421  namerend = gtk_cell_renderer_text_new();
422  col = gtk_tree_view_column_new_with_attributes(_("Name"), namerend, NULL);
423  gtk_tree_view_column_set_cell_data_func(col, namerend, dfname, NULL, NULL);
424  gtk_tree_view_column_set_expand(col, TRUE);
425  gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
426
427  progrend = tr_cell_renderer_torrent_new();
428  /* this string is only used to determing the size of the progress bar */
429  str = g_markup_printf_escaped("<big>%s</big>", _("  fnord    fnord  "));
430  g_object_set(progrend, "label", str, NULL);
431  g_free(str);
432  col = gtk_tree_view_column_new_with_attributes(_("Progress"), progrend, NULL);
433  gtk_tree_view_column_set_cell_data_func(col, progrend, dfprog, NULL, NULL);
434  gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
435
436  /* XXX this shouldn't be necessary */
437  g_signal_connect(view, "notify", G_CALLBACK(stylekludge), progrend);
438
439  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE);
440  sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
441  gtk_tree_selection_set_mode(GTK_TREE_SELECTION(sel), GTK_SELECTION_MULTIPLE);
442  g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(fixbuttons), data);
443  g_signal_connect(G_OBJECT(view), "button-press-event",
444                   G_CALLBACK(listclick), data);
445  g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(listpopup), data);
446  g_signal_connect(G_OBJECT(view), "row-activated",
447                   G_CALLBACK(doubleclick), data);
448  gtk_widget_show_all(view);
449
450  return view;
451}
452
453gboolean
454winclose(GtkWidget *widget SHUTUP, GdkEvent *event SHUTUP, gpointer gdata) {
455  struct cbdata *data = gdata;
456  struct exitdata *edata;
457  tr_stat_t *st;
458  GtkTreeIter iter;
459  tr_torrent_t *tor;
460  gboolean going;
461
462  if(0 >= data->timer)
463    g_source_remove(data->timer);
464  data->timer = -1;
465
466  going = gtk_tree_model_get_iter_first(data->model, &iter);
467  while(going) {
468    gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
469    st = tr_torrentStat(tor);
470    if(TR_STATUS_ACTIVE & st->status) {
471      tr_torrentStop(tor);
472      going = gtk_tree_model_iter_next(data->model, &iter);
473    } else {
474      tr_torrentClose(data->tr, tor);
475      going = gtk_list_store_remove(GTK_LIST_STORE(data->model), &iter);
476    }
477  }
478
479  /* XXX should disable widgets or something */
480
481  /* try to wait until torrents stop before exiting */
482  edata = g_new0(struct exitdata, 1);
483  edata->cbdata = data;
484  edata->started = time(NULL);
485  edata->timer = g_timeout_add(500, exitcheck, edata);
486
487  /* returning FALSE means to destroy the window */
488  return TRUE;
489}
490
491gboolean
492exitcheck(gpointer gdata) {
493  struct exitdata *data = gdata;
494  tr_stat_t *st;
495  GtkTreeIter iter;
496  tr_torrent_t *tor;
497  gboolean go;
498
499  go = gtk_tree_model_get_iter_first(data->cbdata->model, &iter);
500  while(go) {
501    gtk_tree_model_get(data->cbdata->model, &iter, MC_TORRENT, &tor, -1);
502    st = tr_torrentStat(tor);
503    if(!(TR_STATUS_PAUSE & st->status))
504      go = gtk_tree_model_iter_next(data->cbdata->model, &iter);
505    else {
506      tr_torrentClose(data->cbdata->tr, tor);
507      go = gtk_list_store_remove(GTK_LIST_STORE(data->cbdata->model), &iter);
508    }
509  }
510
511  /* keep going if we still have torrents and haven't hit the exit timeout */
512  if(0 < tr_torrentCount(data->cbdata->tr) &&
513     time(NULL) - data->started < TRACKER_EXIT_TIMEOUT) {
514    assert(gtk_tree_model_get_iter_first(data->cbdata->model, &iter));
515    updatemodel(data->cbdata);
516    return TRUE;
517  }
518
519  /* exit otherwise */
520
521  if(0 >= data->timer)
522    g_source_remove(data->timer);
523  data->timer = -1;
524
525  stoptransmission(data->cbdata->tr);
526
527  gtk_widget_destroy(GTK_WIDGET(data->cbdata->wind));
528  g_free(data->cbdata);
529  g_free(data);
530  gtk_main_quit();
531
532  return FALSE;
533}
534
535void
536stoptransmission(tr_handle_t *tr) {
537  GList *list, *ii;
538
539  list = NULL;
540  tr_torrentIterate(tr, maketorrentlist, &list);
541  for(ii = g_list_first(list); NULL != ii; ii = ii->next)
542    tr_torrentClose(tr, ii->data);
543  g_list_free(list);
544
545  tr_close(tr);
546}
547
548void
549gotdrag(GtkWidget *widget SHUTUP, GdkDragContext *dc, gint x SHUTUP,
550        gint y SHUTUP, GtkSelectionData *sel, guint info SHUTUP, guint time,
551        gpointer gdata) {
552  struct cbdata *data = gdata;
553  char prefix[] = "file:";
554  char *files, *decoded, *deslashed, *hostless;
555  int ii, len;
556  GList *errs;
557  gboolean gotfile;
558  struct stat sb;
559  int prelen = strlen(prefix);
560
561#ifdef DND_DEBUG
562  char *sele = gdk_atom_name(sel->selection);
563  char *targ = gdk_atom_name(sel->target);
564  char *type = gdk_atom_name(sel->type);
565
566  fprintf(stderr, "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i\n",
567          sele, targ, type, sel->format, sel->length);
568  g_free(sele);
569  g_free(targ);
570  g_free(type);
571  if(8 == sel->format) {
572    for(ii = 0; ii < sel->length; ii++)
573      fprintf(stderr, "%02X ", sel->data[ii]);
574    fprintf(stderr, "\n");
575  }
576#endif
577
578  errs = NULL;
579  gotfile = FALSE;
580  if(gdk_atom_intern("XdndSelection", FALSE) == sel->selection &&
581     8 == sel->format) {
582    /* split file list on carriage returns and linefeeds */
583    files = g_new(char, sel->length + 1);
584    memcpy(files, sel->data, sel->length);
585    files[sel->length] = '\0';
586    for(ii = 0; '\0' != files[ii]; ii++)
587      if('\015' == files[ii] || '\012' == files[ii])
588        files[ii] = '\0';
589
590    /* try to get a usable filename out of the URI supplied and add it */
591    for(ii = 0; ii < sel->length; ii += len + 1) {
592      if('\0' == files[ii])
593        len = 0;
594      else {
595        len = strlen(files + ii);
596        /* de-urlencode the URI */
597        decoded = urldecode(files + ii, len);
598        if(g_utf8_validate(decoded, -1, NULL)) {
599          /* remove the file: prefix */
600          if(prelen < len && 0 == strncmp(prefix, decoded, prelen)) {
601            deslashed = decoded + prelen;
602            /* trim excess / characters from the beginning */
603            while('/' == deslashed[0] && '/' == deslashed[1])
604              deslashed++;
605            /* if the file doesn't exist, the first part might be a hostname */
606            if(0 > g_stat(deslashed, &sb) &&
607               NULL != (hostless = strchr(deslashed + 1, '/')) &&
608               0 == g_stat(hostless, &sb))
609              deslashed = hostless;
610            /* finally, try to add it as a torrent */
611            if(addtorrent(data, deslashed, NULL, FALSE, &errs))
612              gotfile = TRUE;
613          }
614        }
615        g_free(decoded);
616      }
617    }
618
619    g_free(files);
620    if(gotfile)
621      addedtorrents(data);
622
623    /* print any errors */
624    if(NULL != errs) {
625      files = joinstrlist(errs, "\n");
626      errmsg(data->wind, ngettext("Failed to load the torrent file %s",
627                                  "Failed to load the torrent files:\n%s",
628                                  g_list_length(errs)), files);
629      g_list_foreach(errs, (GFunc)g_free, NULL);
630      g_list_free(errs);
631      g_free(files);
632    }
633  }
634
635  gtk_drag_finish(dc, gotfile, FALSE, time);
636}
637
638void
639setupdrag(GtkWidget *widget, struct cbdata *data) {
640  GtkTargetEntry targets[] = {
641    { "STRING",     0, 0 },
642    { "text/plain", 0, 0 },
643    { "text/uri-list", 0, 0 },
644  };
645
646  g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data);
647
648  gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets,
649                    ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE);
650}
651
652/* kludge to have the progress bars notice theme changes */
653static void
654stylekludge(GObject *obj, GParamSpec *spec, gpointer gdata) {
655  if(0 == strcmp("style", spec->name)) {
656    tr_cell_renderer_torrent_reset_style(TR_CELL_RENDERER_TORRENT(gdata));
657    gtk_widget_queue_draw(GTK_WIDGET(obj));
658  }
659}
660
661/* disable buttons the user shouldn't be able to click on */
662void
663fixbuttons(GtkTreeSelection *sel, gpointer gdata) {
664  struct cbdata *data = gdata;
665  gboolean selected;
666  unsigned int ii;
667  int status;
668
669  if(NULL == sel)
670    sel = gtk_tree_view_get_selection(data->view);
671  status = 0;
672  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
673  selected = (0 < gtk_tree_selection_count_selected_rows(sel));
674
675  for(ii = 0; ii < ALEN(actionitems); ii++)
676    if(actionitems[ii].avail)
677      gtk_widget_set_sensitive(data->buttons[ii],
678                               (selected && (actionitems[ii].avail & status)));
679}
680
681void
682dfname(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
683       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
684  char *name, *mb, *terr, *str, *top, *bottom;
685  guint64 size;
686  gfloat prog;
687  int status, err, eta, tpeers, upeers, dpeers;
688
689  gtk_tree_model_get(model, iter, MC_NAME, &name, MC_STAT, &status,
690    MC_ERR, &err, MC_SIZE, &size, MC_PROG, &prog, MC_ETA, &eta,
691    MC_PEERS, &tpeers, MC_UPEERS, &upeers, MC_DPEERS, &dpeers, -1);
692
693  if(0 > tpeers)
694    tpeers = 0;
695  if(0 > upeers)
696    upeers = 0;
697  if(0 > dpeers)
698    dpeers = 0;
699  mb = readablesize(size);
700  prog *= 100;
701
702  if(status & TR_STATUS_CHECK)
703    top = g_strdup_printf(_("Checking existing files (%.1f%%)"), prog);
704  else if(status & TR_STATUS_DOWNLOAD) {
705    if(0 > eta)
706      top = g_strdup_printf(_("Finishing in --:--:-- (%.1f%%)"), prog);
707    else
708      top = g_strdup_printf(_("Finishing in %02i:%02i:%02i (%.1f%%)"),
709                            eta / 60 / 60, eta / 60 % 60, eta % 60, prog);
710  }
711  else if(status & TR_STATUS_SEED)
712    top = g_strdup_printf(ngettext("Seeding, uploading to %d of %d peer",
713                                   "Seeding, uploading to %d of %d peers",
714                                   tpeers), dpeers, tpeers);
715  else if(status & TR_STATUS_STOPPING)
716    top = g_strdup(_("Stopping..."));
717  else if(status & TR_STATUS_PAUSE)
718    top = g_strdup_printf(_("Stopped (%.1f%%)"), prog);
719  else {
720    top = g_strdup("");
721    assert("XXX unknown status");
722  }
723
724  if(TR_NOERROR != err) {
725    gtk_tree_model_get(model, iter, MC_TERR, &terr, -1);
726    bottom = g_strconcat(_("Error: "), terr, NULL);
727    g_free(terr);
728  }
729  else if(status & TR_STATUS_DOWNLOAD)
730    bottom = g_strdup_printf(ngettext("Downloading from %i of %i peer",
731                                      "Downloading from %i of %i peers",
732                                      tpeers), upeers, tpeers);
733  else
734    bottom = NULL;
735
736  str = g_markup_printf_escaped("<big>%s (%s)</big>\n<small>%s\n%s</small>",
737                                name, mb, top, (NULL == bottom ? "" : bottom));
738  g_object_set(rend, "markup", str, NULL);
739  g_free(name);
740  g_free(mb);
741  g_free(str);
742  g_free(top);
743  g_free(bottom);
744}
745
746void
747dfprog(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
748       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
749  char *dlstr, *ulstr, *str, *marked;
750  gfloat prog, dl, ul;
751  guint64 down, up;
752
753  gtk_tree_model_get(model, iter, MC_PROG, &prog, MC_DRATE, &dl, MC_URATE, &ul,
754                     MC_DOWN, &down, MC_UP, &up, -1);
755  if(0.0 > prog)
756    prog = 0.0;
757  else if(1.0 < prog)
758    prog = 1.0;
759
760  ulstr = readablesize(ul * 1024.0);
761  if(1.0 == prog) {
762    dlstr = ratiostr(down, up);
763    str = g_strdup_printf(_("Ratio: %s\nUL: %s/s"), dlstr, ulstr);
764  } else {
765    dlstr = readablesize(dl * 1024.0);
766    str = g_strdup_printf(_("DL: %s/s\nUL: %s/s"), dlstr, ulstr);
767  }
768  marked = g_markup_printf_escaped("<small>%s</small>", str);
769  g_object_set(rend, "text", str, "value", prog, NULL);
770  g_free(dlstr);
771  g_free(ulstr);
772  g_free(str);
773  g_free(marked);
774}
775
776gboolean
777updatemodel(gpointer gdata) {
778  struct cbdata *data = gdata;
779  tr_torrent_t *tor;
780  tr_stat_t *st;
781  tr_info_t *in;
782  GtkTreeIter iter;
783  float up, down;
784  char *upstr, *downstr, *str;
785
786  if(0 < global_sigcount) {
787    stoptransmission(data->tr);
788    global_sigcount = SIGCOUNT_MAX;
789    raise(global_lastsig);
790  }
791
792  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
793    do {
794      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
795      st = tr_torrentStat(tor);
796      in = tr_torrentInfo(tor);
797      /* XXX find out if setting the same data emits changed signal */
798      gtk_list_store_set(GTK_LIST_STORE(data->model), &iter, MC_NAME, in->name,
799        MC_SIZE, in->totalSize, MC_STAT, st->status, MC_ERR, st->error,
800        MC_TERR, st->trackerError, MC_PROG, st->progress,
801        MC_DRATE, st->rateDownload, MC_URATE, st->rateUpload, MC_ETA, st->eta,
802        MC_PEERS, st->peersTotal, MC_UPEERS, st->peersUploading,
803        MC_DPEERS, st->peersDownloading, MC_DOWN, st->downloaded,
804        MC_UP, st->uploaded, -1);
805    } while(gtk_tree_model_iter_next(data->model, &iter));
806  }
807
808  /* update the status bar */
809  tr_torrentRates(data->tr, &up, &down);
810  downstr = readablesize(down * 1024.0);
811  upstr = readablesize(up * 1024.0);
812  str = g_strdup_printf(_("     Total DL: %s/s     Total UL: %s/s"),
813                        upstr, downstr);
814  gtk_statusbar_pop(data->bar, 0);
815  gtk_statusbar_push(data->bar, 0, str);
816  g_free(str);
817  g_free(upstr);
818  g_free(downstr);
819
820  /* the status of the selected item may have changed, so update the buttons */
821  fixbuttons(NULL, data);
822
823  return TRUE;
824}
825
826/* show a popup menu for a right-click on the list */
827gboolean
828listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) {
829  struct cbdata *data = gdata;
830  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
831  GtkTreePath *path;
832  GtkTreeIter iter;
833  int status;
834  gpointer tor;
835  GList *ids;
836
837  if(GDK_BUTTON_PRESS == event->type && 3 == event->button) {
838    /* find what row, if any, the user clicked on */
839    if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path,
840                                      NULL, NULL, NULL))
841      /* no row was clicked, do the popup with no torrent IDs or status */
842      dopopupmenu(event, data, NULL, 0);
843    else {
844      if(gtk_tree_model_get_iter(data->model, &iter, path)) {
845        /* get ID and status for the right-clicked row */
846        gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
847                           MC_STAT, &status, -1);
848        /* get a list of selected IDs */
849        ids = NULL;
850        gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
851        /* is the clicked row selected? */
852        if(NULL == g_list_find(ids, tor)) {
853          /* no, do the popup for just the clicked row */
854          g_list_free(ids);
855          dopopupmenu(event, data, g_list_append(NULL, tor), status);
856        } else {
857          /* yes, do the popup for all the selected rows */
858          gtk_tree_selection_selected_foreach(sel, orstatus, &status);
859          dopopupmenu(event, data, ids, status);
860        }
861      }
862      gtk_tree_path_free(path);
863    }
864    return TRUE;
865  }
866
867  return FALSE;
868}
869
870gboolean
871listpopup(GtkWidget *widget SHUTUP, gpointer gdata) {
872  struct cbdata *data = gdata;
873  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
874  GList *ids;
875  int status;
876
877  if(0 >= gtk_tree_selection_count_selected_rows(sel))
878    dopopupmenu(NULL, data, NULL, 0);
879  else {
880    status = 0;
881    gtk_tree_selection_selected_foreach(sel, orstatus, &status);
882    ids = NULL;
883    gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
884    dopopupmenu(NULL, data, ids, status);
885  }
886
887  return TRUE;
888}
889
890void
891dopopupmenu(GdkEventButton *event, struct cbdata *data,
892            GList *ids, int status) {
893  GtkWidget *menu = gtk_menu_new();
894  GtkWidget *item;
895  unsigned int ii;
896
897  for(ii = 0; ii < ALEN(actionitems); ii++) {
898    if(actionitems[ii].nomenu ||
899       (actionitems[ii].avail &&
900        (NULL == ids || !(actionitems[ii].avail & status))))
901      continue;
902    item = gtk_menu_item_new_with_label(gettext(actionitems[ii].name));
903    /* set the action for the menu item */
904    g_object_set_data(G_OBJECT(item), LIST_ACTION,
905                      GINT_TO_POINTER(actionitems[ii].act));
906    /* show that this action came from a popup menu */
907    g_object_set_data(G_OBJECT(item), LIST_ACTION_FROM,
908                      GINT_TO_POINTER(FROM_POPUP));
909    /* set a glist of selected torrent's IDs */
910    g_object_set_data(G_OBJECT(item), LIST_IDS, ids);
911    /* set the menu widget, so the activate handler can destroy it */
912    g_object_set_data(G_OBJECT(item), LIST_MENU_WIDGET, menu);
913    g_signal_connect(G_OBJECT(item), "activate",
914                     G_CALLBACK(actionclick), data);
915    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
916  }
917
918  /* set up the glist to be freed when the menu is destroyed */
919  g_object_set_data_full(G_OBJECT(menu), LIST_IDS, ids,
920                         (GDestroyNotify)g_list_free);
921
922  /* destroy the menu if the user doesn't select anything */
923  g_signal_connect(menu, "selection-done", G_CALLBACK(killmenu), NULL);
924
925  gtk_widget_show_all(menu);
926
927  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
928                 (NULL == event ? 0 : event->button),
929                 gdk_event_get_time((GdkEvent*)event));
930}
931
932void
933killmenu(GtkWidget *menu, gpointer *gdata SHUTUP) {
934  gtk_widget_destroy(menu);
935}
936
937void
938actionclick(GtkWidget *widget, gpointer gdata) {
939  struct cbdata *data = gdata;
940  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
941  enum listact act =
942    GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION));
943  enum listfrom from =
944    GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION_FROM));
945  unsigned int actindex;
946  tr_stat_t *sb;
947  GList *ids, *ii;
948  tr_torrent_t *tor;
949  gboolean updatesave;
950  GtkTreeIter iter;
951
952  /* destroy the popup menu, if any */
953  if(FROM_POPUP == from)
954    gtk_widget_destroy(g_object_get_data(G_OBJECT(widget), LIST_MENU_WIDGET));
955
956  switch(act) {
957    case ACT_OPEN:
958      makeaddwind(data->wind, addtorrent, addedtorrents, data);
959      return;
960    case ACT_PREF:
961      if(!data->prefsopen)
962        makeprefwindow(data->wind, data->tr, &data->prefsopen);
963      return;
964    default:
965      break;
966  }
967
968  switch(from) {
969    case FROM_BUTTON:
970      ids = NULL;
971      gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
972      break;
973    case FROM_POPUP:
974      ids = g_object_get_data(G_OBJECT(widget), LIST_IDS);
975      break;
976    default:
977      assert(!"unknown action source");
978      break;
979  }
980
981  for(actindex = 0; actindex < ALEN(actionitems); actindex++)
982    if(actionitems[actindex].act == act)
983      break;
984  assert(actindex < ALEN(actionitems));
985
986  updatesave = FALSE;
987
988  for(ii = g_list_sort(ids, intrevcmp); NULL != ii; ii = ii->next) {
989    tor = ii->data;
990    sb = tr_torrentStat(tor);
991
992    /* check if this action is valid for this torrent */
993    if(actionitems[actindex].nomenu ||
994       (actionitems[actindex].avail &&
995        !(actionitems[actindex].avail & sb->status)))
996      continue;
997
998    switch(act) {
999      case ACT_START:
1000        tr_torrentStart(tor);
1001        updatesave = TRUE;
1002        break;
1003      case ACT_STOP:
1004        tr_torrentStop(tor);
1005        updatesave = TRUE;
1006        break;
1007      case ACT_DELETE:
1008        if(TR_STATUS_ACTIVE & sb->status)
1009          tr_torrentStop(tor);
1010        tr_torrentClose(data->tr, tor);
1011        findtorrent(data->model, tor, &iter);
1012        gtk_list_store_remove(GTK_LIST_STORE(data->model), &iter);
1013        updatesave = TRUE;
1014        /* XXX should only unselect deleted rows */
1015        gtk_tree_selection_unselect_all(
1016          gtk_tree_view_get_selection(data->view));
1017        break;
1018      case ACT_INFO:
1019        makeinfowind(data->wind, tor);
1020        break;
1021      default:
1022        assert(!"unknown type");
1023        break;
1024    }
1025  }
1026
1027  if(updatesave) {
1028    savetorrents(data->tr, data->wind);
1029    updatemodel(data);
1030  }
1031
1032  if(FROM_BUTTON == from)
1033    g_list_free(ids);
1034}
1035
1036void
1037findtorrent(GtkTreeModel *model, tr_torrent_t *tor, GtkTreeIter *iter) {
1038  gpointer ptr;
1039
1040  if(gtk_tree_model_get_iter_first(model, iter)) {
1041    do {
1042      gtk_tree_model_get(model, iter, MC_TORRENT, &ptr, -1);
1043      if(tor == ptr)
1044        return;
1045    } while(gtk_tree_model_iter_next(model, iter));
1046  }
1047
1048  assert(!"torrent not found");
1049}
1050
1051gint
1052intrevcmp(gconstpointer a, gconstpointer b) {
1053  int aint = GPOINTER_TO_INT(a);
1054  int bint = GPOINTER_TO_INT(b);
1055
1056  if(bint > aint)
1057    return 1;
1058  else if(bint < aint)
1059    return -1;
1060  else
1061    return 0;
1062}
1063
1064void
1065doubleclick(GtkWidget *widget SHUTUP, GtkTreePath *path,
1066            GtkTreeViewColumn *col SHUTUP, gpointer gdata) {
1067  struct cbdata *data = gdata;
1068  GtkTreeIter iter;
1069  tr_torrent_t *tor;
1070
1071  if(gtk_tree_model_get_iter(data->model, &iter, path)) {
1072    gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
1073    makeinfowind(data->wind, tor);
1074  }
1075}
1076
1077gboolean
1078addtorrent(void *vdata, const char *torrent, const char *dir, gboolean paused,
1079           GList **errs) {
1080  const struct { const int err; const char *msg; } errstrs[] = {
1081    {TR_EINVALID,       N_("not a valid torrent file")},
1082    {TR_EDUPLICATE,     N_("torrent is already open")},
1083  };
1084  struct cbdata *data = vdata;
1085  tr_torrent_t *new;
1086  char *wd;
1087  int err;
1088  unsigned int ii;
1089  GtkTreeIter iter;
1090
1091  if(NULL == dir && NULL != (dir = cf_getpref(PREF_DIR))) {
1092    if(!mkdir_p(dir, 0777)) {
1093      errmsg(data->wind, _("Failed to create the directory %s:\n%s"),
1094             dir, strerror(errno));
1095      return FALSE;
1096    }
1097  }
1098
1099  if(NULL == (new = tr_torrentInit(data->tr, torrent, &err))) {
1100    for(ii = 0; ii < ALEN(errstrs); ii++)
1101      if(err == errstrs[ii].err)
1102        break;
1103    if(NULL == errs) {
1104      if(ii == ALEN(errstrs))
1105        errmsg(data->wind, _("Failed to load the torrent file %s"), torrent);
1106      else
1107        errmsg(data->wind, _("Failed to load the torrent file %s: %s"),
1108               torrent, gettext(errstrs[ii].msg));
1109    } else {
1110      if(ii == ALEN(errstrs))
1111        *errs = g_list_append(*errs, g_strdup(torrent));
1112      else
1113        *errs = g_list_append(*errs, g_strdup_printf(_("%s (%s)"),
1114                              torrent, gettext(errstrs[ii].msg)));
1115    }
1116    return FALSE;
1117  }
1118
1119  gtk_list_store_append(GTK_LIST_STORE(data->model), &iter);
1120  gtk_list_store_set(GTK_LIST_STORE(data->model), &iter, MC_TORRENT, new, -1);
1121
1122  if(NULL != dir)
1123    tr_torrentSetFolder(new, dir);
1124  else {
1125    wd = g_new(char, MAXPATHLEN + 1);
1126    if(NULL == getcwd(wd, MAXPATHLEN + 1))
1127      tr_torrentSetFolder(new, ".");
1128    else {
1129      tr_torrentSetFolder(new, wd);
1130      free(wd);
1131    }
1132  }
1133
1134  if(!paused)
1135    tr_torrentStart(new);
1136
1137  return TRUE;
1138}
1139
1140void
1141addedtorrents(void *vdata) {
1142  struct cbdata *data = vdata;
1143
1144  updatemodel(data);
1145  savetorrents(data->tr, data->wind);
1146}
1147
1148gboolean
1149savetorrents(tr_handle_t *tr, GtkWindow *wind) {
1150  GList *torrents;
1151  char *errstr;
1152  gboolean ret;
1153
1154  torrents = NULL;
1155  tr_torrentIterate(tr, maketorrentlist, &torrents);
1156
1157  if(!(ret = cf_savestate(torrents, &errstr))) {
1158    errmsg(wind, "%s", errstr);
1159    g_free(errstr);
1160  }
1161
1162  g_list_free(torrents);
1163
1164  return ret;
1165}
1166
1167/* use with gtk_tree_selection_selected_foreach to | status of selected rows */
1168void
1169orstatus(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1170         gpointer gdata) {
1171  int *allstatus = gdata;
1172  int status;
1173
1174  gtk_tree_model_get(model, iter, MC_STAT, &status, -1);
1175  *allstatus |= status;
1176}
1177
1178void
1179makeidlist(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1180           gpointer gdata) {
1181  GList **ids = gdata;
1182  gpointer ptr;
1183
1184  gtk_tree_model_get(model, iter, MC_TORRENT, &ptr, -1);
1185  *ids = g_list_append(*ids, ptr);
1186}
1187
1188void
1189maketorrentlist(tr_torrent_t *tor, void *data) {
1190  GList **list = data;
1191
1192  *list = g_list_append(*list, tor);
1193}
1194
1195void
1196setupsighandlers(void) {
1197  int sigs[] = {SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
1198  struct sigaction sa;
1199  unsigned int ii;
1200
1201  bzero(&sa, sizeof(sa));
1202  sa.sa_handler = fatalsig;
1203  for(ii = 0; ii < ALEN(sigs); ii++)
1204    sigaction(sigs[ii], &sa, NULL);
1205}
1206
1207void
1208fatalsig(int sig) {
1209  struct sigaction sa;
1210
1211  global_lastsig = sig;
1212
1213  if(SIGCOUNT_MAX <= ++global_sigcount) {
1214    bzero(&sa, sizeof(sa));
1215    sa.sa_handler = SIG_DFL;
1216    sigaction(sig, &sa, NULL);
1217    raise(sig);
1218  }
1219}
Note: See TracBrowser for help on using the repository browser.