source: trunk/gtk/main.c @ 468

Last change on this file since 468 was 468, checked in by joshe, 16 years ago

Fix drag-and-drop.
It's better to use memory before you free it, not after.

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