source: trunk/gtk/main.c @ 525

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

Move the ETA cap of 99:59:59 from libtransmission to the MacOS X GUI.
Display the ETA better in the GTK GUI.

  • Property svn:keywords set to Date Rev Author Id
File size: 35.1 KB
Line 
1/*
2  $Id: main.c 525 2006-07-04 22:31:14Z 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, *timestr;
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(_("Stalled (%.1f%%)"), prog);
704    else {
705      timestr = readabletime(eta);
706      top = g_strdup_printf(_("Finishing in %s (%.1f%%)"), timestr, prog);
707      g_free(timestr);
708    }
709  }
710  else if(status & TR_STATUS_SEED)
711    top = g_strdup_printf(ngettext("Seeding, uploading to %d of %d peer",
712                                   "Seeding, uploading to %d of %d peers",
713                                   tpeers), dpeers, tpeers);
714  else if(status & TR_STATUS_STOPPING)
715    top = g_strdup(_("Stopping..."));
716  else if(status & TR_STATUS_PAUSE)
717    top = g_strdup_printf(_("Stopped (%.1f%%)"), prog);
718  else {
719    top = g_strdup("");
720    g_assert_not_reached();
721  }
722
723  if(TR_NOERROR != err) {
724    gtk_tree_model_get(model, iter, MC_TERR, &terr, -1);
725    bottom = g_strconcat(_("Error: "), terr, NULL);
726    g_free(terr);
727  }
728  else if(status & TR_STATUS_DOWNLOAD)
729    bottom = g_strdup_printf(ngettext("Downloading from %i of %i peer",
730                                      "Downloading from %i of %i peers",
731                                      tpeers), upeers, tpeers);
732  else
733    bottom = NULL;
734
735  str = g_markup_printf_escaped("<big>%s (%s)</big>\n<small>%s\n%s</small>",
736                                name, mb, top, (NULL == bottom ? "" : bottom));
737  g_object_set(rend, "markup", str, NULL);
738  g_free(name);
739  g_free(mb);
740  g_free(str);
741  g_free(top);
742  if(NULL != bottom)
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  TrTorrent *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    quittransmission(data);
788    return FALSE;
789  }
790
791  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
792    do {
793      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
794      st = tr_torrent_stat(tor);
795      in = tr_torrent_info(tor);
796      g_object_unref(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(tr_backend_handle(data->back), &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  /* check for politely stopped torrents unless we're exiting */
824  if(!data->closing)
825    tr_backend_torrents_stopped(data->back);
826
827  return TRUE;
828}
829
830/* show a popup menu for a right-click on the list */
831gboolean
832listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) {
833  struct cbdata *data = gdata;
834  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
835  GtkTreePath *path;
836  GtkTreeIter iter;
837  int status;
838  TrTorrent *tor, *issel;
839
840  if(GDK_BUTTON_PRESS == event->type && 3 == event->button) {
841    /* find what row, if any, the user clicked on */
842    if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path,
843                                      NULL, NULL, NULL))
844      gtk_tree_selection_unselect_all(sel);
845    else {
846      if(gtk_tree_model_get_iter(data->model, &iter, path)) {
847        /* get torrent and status for the right-clicked row */
848        gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
849                           MC_STAT, &status, -1);
850        issel = tor;
851        gtk_tree_selection_selected_foreach(sel, istorsel, &issel);
852        g_object_unref(tor);
853        /* if the clicked row isn't selected, select only it */
854        if(NULL != issel) {
855          gtk_tree_selection_unselect_all(sel);
856          gtk_tree_selection_select_iter(sel, &iter);
857        }
858      }
859      gtk_tree_path_free(path);
860    }
861    dopopupmenu(event, data);
862    return TRUE;
863  }
864
865  return FALSE;
866}
867
868gboolean
869listpopup(GtkWidget *widget SHUTUP, gpointer gdata) {
870  dopopupmenu(NULL, gdata);
871  return TRUE;
872}
873
874void
875dopopupmenu(GdkEventButton *event, struct cbdata *data) {
876  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
877  int count = gtk_tree_selection_count_selected_rows(sel);
878  GtkWidget *menu = gtk_menu_new();
879  GtkWidget *item;
880  unsigned int ii;
881  int status = 0;
882
883  if(NULL != data->stupidpopuphack)
884    gtk_widget_destroy(data->stupidpopuphack);
885  data->stupidpopuphack = menu;
886
887  status = 0;
888  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
889
890  for(ii = 0; ii < ALEN(actionitems); ii++) {
891    if(actionitems[ii].nomenu ||
892       (actionitems[ii].avail &&
893        (0 == count || !(actionitems[ii].avail & status))))
894      continue;
895    item = gtk_menu_item_new_with_label(gettext(actionitems[ii].name));
896    /* set the action for the menu item */
897    g_object_set_data(G_OBJECT(item), LIST_ACTION,
898                      GINT_TO_POINTER(actionitems[ii].act));
899    g_signal_connect(G_OBJECT(item), "activate",
900                     G_CALLBACK(actionclick), data);
901    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
902  }
903
904  gtk_widget_show_all(menu);
905
906  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
907                 (NULL == event ? 0 : event->button),
908                 gdk_event_get_time((GdkEvent*)event));
909}
910
911void
912actionclick(GtkWidget *widget, gpointer gdata) {
913  struct cbdata *data = gdata;
914  enum listact act;
915  GtkTreeSelection *sel;
916  GList *rows, *ii;
917  GtkTreeRowReference *ref;
918  GtkTreePath *path;
919  GtkTreeIter iter;
920  TrTorrent *tor;
921  unsigned int actoff, status;
922  gboolean changed;
923
924  act = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION));
925
926  switch(act) {
927    case ACT_OPEN:
928      makeaddwind(data->wind, addtorrents, data);
929      return;
930    case ACT_PREF:
931      if(!data->prefsopen)
932        makeprefwindow(data->wind, data->back, &data->prefsopen);
933      return;
934    case ACT_START:
935    case ACT_STOP:
936    case ACT_DELETE:
937    case ACT_INFO:
938      break;
939  }
940
941  sel = gtk_tree_view_get_selection(data->view);
942  rows = gtk_tree_selection_get_selected_rows(sel, NULL);
943
944  for(ii = rows; NULL != ii; ii = ii->next) {
945    ref = gtk_tree_row_reference_new(data->model, ii->data);
946    gtk_tree_path_free(ii->data);
947    ii->data = ref;
948  }
949
950  for(actoff = 0; actoff < ALEN(actionitems); actoff++)
951    if(actionitems[actoff].act == act)
952      break;
953  g_assert(actoff < ALEN(actionitems));
954
955  changed = FALSE;
956  for(ii = rows; NULL != ii; ii = ii->next) {
957    if(NULL != (path = gtk_tree_row_reference_get_path(ii->data)) &&
958       gtk_tree_model_get_iter(data->model, &iter, path)) {
959      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
960                         MC_STAT, &status, -1);
961      /* check if this action is valid for this torrent */
962      if((!actionitems[actoff].avail || actionitems[actoff].avail & status) &&
963         !actionitems[actoff].nomenu) {
964        switch(act) {
965          case ACT_START:
966            tr_torrentStart(tr_torrent_handle(tor));
967            changed = TRUE;
968            break;
969          case ACT_STOP:
970            tr_torrentStop(tr_torrent_handle(tor));
971            changed = TRUE;
972            break;
973          case ACT_DELETE:
974            /* tor will be unref'd in the politely_stopped signal handler */
975            g_object_ref(tor);
976            tr_torrent_stop_politely(tor);
977            if(TR_FSAVEPRIVATE & tr_torrent_info(tor)->flags)
978              tr_torrentRemoveSaved(tr_torrent_handle(tor));
979            gtk_list_store_remove(GTK_LIST_STORE(data->model), &iter);
980            changed = TRUE;
981            break;
982          case ACT_INFO:
983            makeinfowind(data->wind, tor);
984            break;
985          case ACT_OPEN:
986          case ACT_PREF:
987            break;
988        }
989      }
990      g_object_unref(tor);
991    }
992    if(NULL != path)
993      gtk_tree_path_free(path);
994    gtk_tree_row_reference_free(ii->data);
995  }
996  g_list_free(rows);
997
998  if(changed) {
999    savetorrents(data);
1000    updatemodel(data);
1001  }
1002}
1003
1004gint
1005intrevcmp(gconstpointer a, gconstpointer b) {
1006  int aint = GPOINTER_TO_INT(a);
1007  int bint = GPOINTER_TO_INT(b);
1008
1009  if(bint > aint)
1010    return 1;
1011  else if(bint < aint)
1012    return -1;
1013  else
1014    return 0;
1015}
1016
1017void
1018doubleclick(GtkWidget *widget SHUTUP, GtkTreePath *path,
1019            GtkTreeViewColumn *col SHUTUP, gpointer gdata) {
1020  struct cbdata *data = gdata;
1021  GtkTreeIter iter;
1022  TrTorrent *tor;
1023
1024  if(gtk_tree_model_get_iter(data->model, &iter, path)) {
1025    gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
1026    makeinfowind(data->wind, tor);
1027    g_object_unref(tor);
1028  }
1029}
1030
1031void
1032addtorrents(void *vdata, void *state, GList *files,
1033            const char *dir, guint flags) {
1034  struct cbdata *data = vdata;
1035  GList *torlist, *errlist, *ii;
1036  char *errstr;
1037  TrTorrent *tor;
1038  GtkTreeIter iter;
1039  char *wd;
1040
1041  errlist = NULL;
1042  torlist = NULL;
1043
1044  if(NULL != state)
1045    torlist = tr_backend_load_state(data->back, state, &errlist);
1046
1047  if(NULL != files) {
1048    if(NULL == dir)
1049      dir = cf_getpref(PREF_DIR);
1050    wd = NULL;
1051    if(NULL == dir) {
1052      wd = g_new(char, MAX_PATH_LENGTH + 1);
1053      if(NULL == getcwd(wd, MAX_PATH_LENGTH + 1))
1054        dir = ".";
1055      else
1056        dir = wd;
1057    }
1058    for(ii = g_list_first(files); NULL != ii; ii = ii->next) {
1059      errstr = NULL;
1060      tor = tr_torrent_new(G_OBJECT(data->back), ii->data, dir,
1061                           flags, &errstr);
1062      if(NULL != tor)
1063        torlist = g_list_append(torlist, tor);
1064      if(NULL != errstr)
1065        errlist = g_list_append(errlist, errstr);
1066    }
1067    if(NULL != wd)
1068      g_free(wd);
1069  }
1070
1071  for(ii = g_list_first(torlist); NULL != ii; ii = ii->next) {
1072    gtk_list_store_append(GTK_LIST_STORE(data->model), &iter);
1073    gtk_list_store_set(GTK_LIST_STORE(data->model), &iter,
1074                       MC_TORRENT, ii->data, -1);
1075    /* we will always ref a torrent before politely stopping it */
1076    g_signal_connect(ii->data, "politely_stopped",
1077                     G_CALLBACK(g_object_unref), data);
1078    g_object_unref(ii->data);
1079  }
1080
1081  if(NULL != errlist) {
1082    errstr = joinstrlist(errlist, "\n");
1083    errmsg(data->wind, ngettext("Failed to load torrent file:\n%s",
1084                                "Failed to load torrent files:\n%s",
1085                                g_list_length(errlist)), errstr);
1086    g_list_foreach(errlist, (GFunc)g_free, NULL);
1087    g_list_free(errlist);
1088    g_free(errstr);
1089  }
1090
1091  if(NULL != torlist) {
1092    updatemodel(data);
1093    savetorrents(data);
1094  }
1095}
1096
1097void
1098savetorrents(struct cbdata *data) {
1099  char *errstr;
1100
1101  tr_backend_save_state(data->back, &errstr);
1102  if(NULL != errstr) {
1103    errmsg(data->wind, "%s", errstr);
1104    g_free(errstr);
1105  }
1106}
1107
1108/* use with gtk_tree_selection_selected_foreach to | status of selected rows */
1109void
1110orstatus(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1111         gpointer gdata) {
1112  int *allstatus = gdata;
1113  int status;
1114
1115  gtk_tree_model_get(model, iter, MC_STAT, &status, -1);
1116  *allstatus |= status;
1117}
1118
1119/* data should be a TrTorrent**, will set torrent to NULL if it's selected */
1120void
1121istorsel(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1122         gpointer gdata) {
1123  TrTorrent **torref = gdata;
1124  TrTorrent *tor;
1125
1126  if(NULL != *torref) {
1127    gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);
1128    if(tor == *torref)
1129      *torref = NULL;
1130    g_object_unref(tor);
1131  }
1132}
1133
1134void
1135safepipe(void) {
1136  struct sigaction sa;
1137
1138  bzero(&sa, sizeof(sa));
1139  sa.sa_handler = SIG_IGN;
1140  sigaction(SIGPIPE, &sa, NULL);
1141}
1142
1143void
1144setupsighandlers(void) {
1145  int sigs[] = {SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
1146  struct sigaction sa;
1147  unsigned int ii;
1148
1149  bzero(&sa, sizeof(sa));
1150  sa.sa_handler = fatalsig;
1151  for(ii = 0; ii < ALEN(sigs); ii++)
1152    sigaction(sigs[ii], &sa, NULL);
1153}
1154
1155void
1156fatalsig(int sig) {
1157  struct sigaction sa;
1158
1159  if(SIGCOUNT_MAX <= ++global_sigcount) {
1160    bzero(&sa, sizeof(sa));
1161    sa.sa_handler = SIG_DFL;
1162    sigaction(sig, &sa, NULL);
1163    raise(sig);
1164  }
1165}
Note: See TracBrowser for help on using the repository browser.