source: trunk/gtk/main.c @ 248

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

Major internal restructuring for the GTK GUI,

GObject-derived wrappers are used for tr_handle_t and tr_torrent_t.

Use bencoding to store prefs and state file.
Make sure to always group error messages when adding multiple torrents at once.
Remove some unused code.
Many miscellaneous cleanups.

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