source: trunk/gtk/main.c @ 269

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

Wait and try to send a stopped event when removing a torrent.

  • Property svn:keywords set to Date Rev Author Id
File size: 33.5 KB
Line 
1/*
2  $Id: main.c 269 2006-05-31 23:20:59Z 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    5
53
54/* interval in milliseconds to update the torrent list display */
55#define UPDATE_INTERVAL         500
56
57/* interval in milliseconds to check for stopped torrents and update display */
58#define EXIT_CHECK_INTERVAL     500
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);
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, gboolean *paused);
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  while(0 < --argc) {
271    argv++;
272    if(0 == strcmp("--", *argv))
273      return checkfilenames(argc - 1, argv + 1);
274    else if('-' != argv[0][0])
275      return checkfilenames(argc, argv);
276  }
277
278  return NULL;
279}
280
281void
282makewind(GtkWidget *wind, TrBackend *back, benc_val_t *state, GList *args) {
283  GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
284  GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
285  GtkWidget *status = gtk_statusbar_new();
286  struct cbdata *data = g_new0(struct cbdata, 1);
287  GtkWidget *list;
288  GtkRequisition req;
289  gint height;
290
291  g_object_ref(G_OBJECT(back));
292  data->back = back;
293  data->wind = GTK_WINDOW(wind);
294  data->timer = 0;
295  /* filled in by makewind_list */
296  data->model = NULL;
297  data->view = NULL;
298  data->bar = GTK_STATUSBAR(status);
299  data->buttons = NULL;
300  data->prefsopen = FALSE;
301  data->stupidpopuphack = NULL;
302  data->closing = FALSE;
303
304  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER,
305                                 GTK_POLICY_AUTOMATIC);
306
307  gtk_box_pack_start(GTK_BOX(vbox), makewind_toolbar(data), FALSE, FALSE, 0);
308
309  list = makewind_list(data);
310  gtk_container_add(GTK_CONTAINER(scroll), list);
311  gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
312
313  gtk_statusbar_push(GTK_STATUSBAR(status), 0, "");
314  gtk_box_pack_start(GTK_BOX(vbox), status, FALSE, FALSE, 0);
315
316  gtk_container_set_focus_child(GTK_CONTAINER(vbox), scroll);
317  gtk_container_add(GTK_CONTAINER(wind), vbox);
318  gtk_window_set_title(data->wind, g_get_application_name());
319  g_signal_connect(G_OBJECT(wind), "delete_event", G_CALLBACK(winclose), data);
320
321  setupdrag(list, data);
322
323  addtorrents(data, state, args, NULL, NULL);
324
325  data->timer = g_timeout_add(UPDATE_INTERVAL, updatemodel, data);
326  updatemodel(data);
327
328  gtk_widget_show_all(vbox);
329  gtk_widget_realize(wind);
330
331  gtk_widget_size_request(list, &req);
332  height = req.height;
333  gtk_widget_size_request(scroll, &req);
334  height -= req.height;
335  gtk_widget_size_request(wind, &req);
336  height += req.height;
337  gtk_window_set_default_size(GTK_WINDOW(wind), -1, (height > req.width ?
338     MIN(height, req.width * 8 / 5) : MAX(height, req.width * 5 / 8)));
339
340  gtk_widget_show(wind);
341
342  ipc_socket_setup(GTK_WINDOW(wind), addtorrents, data);
343}
344
345void
346quittransmission(struct cbdata *data) {
347  g_object_unref(G_OBJECT(data->back));
348  gtk_widget_destroy(GTK_WIDGET(data->wind));
349  if(0 < data->timer)
350    g_source_remove(data->timer);
351  if(NULL != data->stupidpopuphack)
352    gtk_widget_destroy(data->stupidpopuphack);
353  g_free(data->buttons);
354  g_free(data);
355  gtk_main_quit();
356}
357
358GtkWidget *
359makewind_toolbar(struct cbdata *data) {
360  GtkWidget *bar = gtk_toolbar_new();
361  GtkToolItem *item;
362  unsigned int ii;
363
364  gtk_toolbar_set_tooltips(GTK_TOOLBAR(bar), TRUE);
365  gtk_toolbar_set_show_arrow(GTK_TOOLBAR(bar), FALSE);
366
367  data->buttons = g_new(GtkWidget*, ALEN(actionitems));
368
369  for(ii = 0; ii < ALEN(actionitems); ii++) {
370    item = gtk_tool_button_new_from_stock(actionitems[ii].id);
371    data->buttons[ii] = GTK_WIDGET(item);
372    gtk_tool_button_set_label(GTK_TOOL_BUTTON(item),
373                              gettext(actionitems[ii].name));
374    gtk_tool_item_set_tooltip(GTK_TOOL_ITEM(item), GTK_TOOLBAR(bar)->tooltips,
375                              gettext(actionitems[ii].ttext),
376                              actionitems[ii].tpriv);
377    g_object_set_data(G_OBJECT(item), LIST_ACTION,
378                      GINT_TO_POINTER(actionitems[ii].act));
379    g_signal_connect(G_OBJECT(item), "clicked", G_CALLBACK(actionclick), data);
380    gtk_toolbar_insert(GTK_TOOLBAR(bar), item, -1);
381  }
382
383  return bar;
384}
385
386/* XXX check for unused data in model */
387enum {
388  MC_NAME, MC_SIZE, MC_STAT, MC_ERR, MC_TERR,
389  MC_PROG, MC_DRATE, MC_URATE, MC_ETA, MC_PEERS,
390  MC_UPEERS, MC_DPEERS, MC_DOWN, MC_UP,
391  MC_TORRENT, MC_ROW_COUNT,
392};
393
394GtkWidget *
395makewind_list(struct cbdata *data) {
396  GType types[] = {
397    /* info->name, info->totalSize, status,     error,      trackerError, */
398    G_TYPE_STRING, G_TYPE_UINT64,   G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING,
399    /* progress,  rateDownload, rateUpload,   eta,        peersTotal, */
400    G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_INT, G_TYPE_INT,
401    /* peersUploading, peersDownloading, downloaded,    uploaded */
402    G_TYPE_INT,        G_TYPE_INT,       G_TYPE_UINT64, G_TYPE_UINT64,
403    /* the torrent object */
404    TR_TORRENT_TYPE};
405  GtkListStore *store;
406  GtkWidget *view;
407  GtkTreeViewColumn *col;
408  GtkTreeSelection *sel;
409  GtkCellRenderer *namerend, *progrend;
410  char *str;
411
412  g_assert(MC_ROW_COUNT == ALEN(types));
413
414  store = gtk_list_store_newv(MC_ROW_COUNT, types);
415  view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
416  /* XXX do I need to worry about reference counts anywhere else? */
417  g_object_unref(G_OBJECT(store));
418  data->model = GTK_TREE_MODEL(store);
419  data->view = GTK_TREE_VIEW(view);
420
421  namerend = gtk_cell_renderer_text_new();
422  col = gtk_tree_view_column_new_with_attributes(_("Name"), namerend, NULL);
423  g_object_set(namerend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
424  gtk_tree_view_column_set_cell_data_func(col, namerend, dfname, NULL, NULL);
425  gtk_tree_view_column_set_expand(col, TRUE);
426  gtk_tree_view_column_set_resizable(col, TRUE);
427  gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
428
429  progrend = tr_cell_renderer_torrent_new();
430  /* this string is only used to determing the size of the progress bar */
431  str = g_markup_printf_escaped("<big>%s</big>", _("  fnord    fnord  "));
432  g_object_set(progrend, "label", str, NULL);
433  g_free(str);
434  col = gtk_tree_view_column_new_with_attributes(_("Progress"), progrend, NULL);
435  gtk_tree_view_column_set_cell_data_func(col, progrend, dfprog, NULL, NULL);
436  gtk_tree_view_column_set_resizable(col, TRUE);
437  gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
438
439  /* XXX this shouldn't be necessary */
440  g_signal_connect(view, "notify", G_CALLBACK(stylekludge), progrend);
441
442  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE);
443  sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
444  gtk_tree_selection_set_mode(GTK_TREE_SELECTION(sel), GTK_SELECTION_MULTIPLE);
445  g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(fixbuttons), data);
446  g_signal_connect(G_OBJECT(view), "button-press-event",
447                   G_CALLBACK(listclick), data);
448  g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(listpopup), data);
449  g_signal_connect(G_OBJECT(view), "row-activated",
450                   G_CALLBACK(doubleclick), data);
451  gtk_widget_show_all(view);
452
453  return view;
454}
455
456gboolean
457winclose(GtkWidget *widget SHUTUP, GdkEvent *event SHUTUP, gpointer gdata) {
458  struct cbdata *data = gdata;
459  struct exitdata *edata;
460  unsigned int ii;
461  GtkTreeIter iter;
462  TrTorrent *tor;
463
464  data->closing = TRUE;
465
466  /* stop the update timer */
467  if(0 < data->timer)
468    g_source_remove(data->timer);
469  data->timer = 0;
470
471  /*
472    Add a reference to all torrents in the list, which will be removed
473    when the politely-stopped signal is emitted.  This is necessary
474    because actionclick() adds a reference when it removes a torrent
475    from the model and calls tr_torrent_stop_polite() on it.
476  */
477  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
478    do
479      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
480    while(gtk_tree_model_iter_next(data->model, &iter));
481  }
482
483  /* try to politely stop all the torrents */
484  tr_backend_stop_torrents(data->back);
485
486  /* set things up to wait for torrents to stop */
487  edata = g_new0(struct exitdata, 1);
488  edata->cbdata = data;
489  edata->started = time(NULL);
490  /* check if torrents are still running */
491  if(exitcheck(edata)) {
492    /* yes, start the exit timer and disable widgets */
493    edata->timer = g_timeout_add(EXIT_CHECK_INTERVAL, exitcheck, edata);
494    for(ii = 0; ii < ALEN(actionitems); ii++)
495      gtk_widget_set_sensitive(data->buttons[ii], FALSE);
496    gtk_widget_set_sensitive(GTK_WIDGET(data->view), FALSE);
497  }
498
499  /* returning FALSE means to destroy the window */
500  return TRUE;
501}
502
503gboolean
504exitcheck(gpointer gdata) {
505  struct exitdata *data = gdata;
506
507  /* keep going if we still have torrents and haven't hit the exit timeout */
508  if(time(NULL) - data->started < TRACKER_EXIT_TIMEOUT &&
509     !tr_backend_torrents_stopped(data->cbdata->back)) {
510    updatemodel(data->cbdata);
511    return TRUE;
512  }
513
514  /* exit otherwise */
515  if(0 < data->timer)
516    g_source_remove(data->timer);
517  quittransmission(data->cbdata);
518  g_free(data);
519
520  return FALSE;
521}
522
523void
524gotdrag(GtkWidget *widget SHUTUP, GdkDragContext *dc, gint x SHUTUP,
525        gint y SHUTUP, GtkSelectionData *sel, guint info SHUTUP, guint time,
526        gpointer gdata) {
527  struct cbdata *data = gdata;
528  char prefix[] = "file:";
529  char *files, *decoded, *deslashed, *hostless;
530  int ii, len;
531  GList *errs;
532  struct stat sb;
533  int prelen = strlen(prefix);
534  GList *paths;
535
536#ifdef DND_DEBUG
537  char *sele = gdk_atom_name(sel->selection);
538  char *targ = gdk_atom_name(sel->target);
539  char *type = gdk_atom_name(sel->type);
540
541  fprintf(stderr, "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i\n",
542          sele, targ, type, sel->format, sel->length);
543  g_free(sele);
544  g_free(targ);
545  g_free(type);
546  if(8 == sel->format) {
547    for(ii = 0; ii < sel->length; ii++)
548      fprintf(stderr, "%02X ", sel->data[ii]);
549    fprintf(stderr, "\n");
550  }
551#endif
552
553  errs = NULL;
554  paths = NULL;
555  if(gdk_atom_intern("XdndSelection", FALSE) == sel->selection &&
556     8 == sel->format) {
557    /* split file list on carriage returns and linefeeds */
558    files = g_new(char, sel->length + 1);
559    memcpy(files, sel->data, sel->length);
560    files[sel->length] = '\0';
561    for(ii = 0; '\0' != files[ii]; ii++)
562      if('\015' == files[ii] || '\012' == files[ii])
563        files[ii] = '\0';
564
565    /* try to get a usable filename out of the URI supplied and add it */
566    for(ii = 0; ii < sel->length; ii += len + 1) {
567      if('\0' == files[ii])
568        len = 0;
569      else {
570        len = strlen(files + ii);
571        /* de-urlencode the URI */
572        decoded = urldecode(files + ii, len);
573        if(g_utf8_validate(decoded, -1, NULL)) {
574          /* remove the file: prefix */
575          if(prelen < len && 0 == strncmp(prefix, decoded, prelen)) {
576            deslashed = decoded + prelen;
577            /* trim excess / characters from the beginning */
578            while('/' == deslashed[0] && '/' == deslashed[1])
579              deslashed++;
580            /* if the file doesn't exist, the first part might be a hostname */
581            if(0 > g_stat(deslashed, &sb) &&
582               NULL != (hostless = strchr(deslashed + 1, '/')) &&
583               0 == g_stat(hostless, &sb))
584              deslashed = hostless;
585            /* finally, add it to the list of torrents to try adding */
586            paths = g_list_append(paths, deslashed);
587          }
588        }
589        g_free(decoded);
590      }
591    }
592
593    /* try to add any torrents we found */
594    if(NULL != paths) {
595      addtorrents(data, NULL, paths, NULL, NULL);
596      freestrlist(paths);
597    }
598    g_free(files);
599  }
600
601  gtk_drag_finish(dc, (NULL != paths), FALSE, time);
602}
603
604void
605setupdrag(GtkWidget *widget, struct cbdata *data) {
606  GtkTargetEntry targets[] = {
607    { "STRING",     0, 0 },
608    { "text/plain", 0, 0 },
609    { "text/uri-list", 0, 0 },
610  };
611
612  g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data);
613
614  gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets,
615                    ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE);
616}
617
618/* kludge to have the progress bars notice theme changes */
619static void
620stylekludge(GObject *obj, GParamSpec *spec, gpointer gdata) {
621  if(0 == strcmp("style", spec->name)) {
622    tr_cell_renderer_torrent_reset_style(TR_CELL_RENDERER_TORRENT(gdata));
623    gtk_widget_queue_draw(GTK_WIDGET(obj));
624  }
625}
626
627/* disable buttons the user shouldn't be able to click on */
628void
629fixbuttons(GtkTreeSelection *sel, gpointer gdata) {
630  struct cbdata *data = gdata;
631  gboolean selected;
632  unsigned int ii;
633  int status;
634
635  if(NULL == sel)
636    sel = gtk_tree_view_get_selection(data->view);
637  status = 0;
638  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
639  selected = (0 < gtk_tree_selection_count_selected_rows(sel));
640
641  for(ii = 0; ii < ALEN(actionitems); ii++)
642    if(actionitems[ii].avail)
643      gtk_widget_set_sensitive(data->buttons[ii],
644                               (selected && (actionitems[ii].avail & status)));
645}
646
647void
648dfname(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
649       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
650  char *name, *mb, *terr, *str, *top, *bottom;
651  guint64 size;
652  gfloat prog;
653  int status, err, eta, tpeers, upeers, dpeers;
654
655  gtk_tree_model_get(model, iter, MC_NAME, &name, MC_STAT, &status,
656    MC_ERR, &err, MC_SIZE, &size, MC_PROG, &prog, MC_ETA, &eta,
657    MC_PEERS, &tpeers, MC_UPEERS, &upeers, MC_DPEERS, &dpeers, -1);
658
659  if(0 > tpeers)
660    tpeers = 0;
661  if(0 > upeers)
662    upeers = 0;
663  if(0 > dpeers)
664    dpeers = 0;
665  mb = readablesize(size);
666  prog *= 100;
667
668  if(status & TR_STATUS_CHECK)
669    top = g_strdup_printf(_("Checking existing files (%.1f%%)"), prog);
670  else if(status & TR_STATUS_DOWNLOAD) {
671    if(0 > eta)
672      top = g_strdup_printf(_("Finishing in --:--:-- (%.1f%%)"), prog);
673    else
674      top = g_strdup_printf(_("Finishing in %02i:%02i:%02i (%.1f%%)"),
675                            eta / 60 / 60, eta / 60 % 60, eta % 60, prog);
676  }
677  else if(status & TR_STATUS_SEED)
678    top = g_strdup_printf(ngettext("Seeding, uploading to %d of %d peer",
679                                   "Seeding, uploading to %d of %d peers",
680                                   tpeers), dpeers, tpeers);
681  else if(status & TR_STATUS_STOPPING)
682    top = g_strdup(_("Stopping..."));
683  else if(status & TR_STATUS_PAUSE)
684    top = g_strdup_printf(_("Stopped (%.1f%%)"), prog);
685  else {
686    top = g_strdup("");
687    g_assert_not_reached();
688  }
689
690  if(TR_NOERROR != err) {
691    gtk_tree_model_get(model, iter, MC_TERR, &terr, -1);
692    bottom = g_strconcat(_("Error: "), terr, NULL);
693    g_free(terr);
694  }
695  else if(status & TR_STATUS_DOWNLOAD)
696    bottom = g_strdup_printf(ngettext("Downloading from %i of %i peer",
697                                      "Downloading from %i of %i peers",
698                                      tpeers), upeers, tpeers);
699  else
700    bottom = NULL;
701
702  str = g_markup_printf_escaped("<big>%s (%s)</big>\n<small>%s\n%s</small>",
703                                name, mb, top, (NULL == bottom ? "" : bottom));
704  g_object_set(rend, "markup", str, NULL);
705  g_free(name);
706  g_free(mb);
707  g_free(str);
708  g_free(top);
709  if(NULL != bottom)
710    g_free(bottom);
711}
712
713void
714dfprog(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
715       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
716  char *dlstr, *ulstr, *str, *marked;
717  gfloat prog, dl, ul;
718  guint64 down, up;
719
720  gtk_tree_model_get(model, iter, MC_PROG, &prog, MC_DRATE, &dl, MC_URATE, &ul,
721                     MC_DOWN, &down, MC_UP, &up, -1);
722  if(0.0 > prog)
723    prog = 0.0;
724  else if(1.0 < prog)
725    prog = 1.0;
726
727  ulstr = readablesize(ul * 1024.0);
728  if(1.0 == prog) {
729    dlstr = ratiostr(down, up);
730    str = g_strdup_printf(_("Ratio: %s\nUL: %s/s"), dlstr, ulstr);
731  } else {
732    dlstr = readablesize(dl * 1024.0);
733    str = g_strdup_printf(_("DL: %s/s\nUL: %s/s"), dlstr, ulstr);
734  }
735  marked = g_markup_printf_escaped("<small>%s</small>", str);
736  g_object_set(rend, "text", str, "value", prog, NULL);
737  g_free(dlstr);
738  g_free(ulstr);
739  g_free(str);
740  g_free(marked);
741}
742
743gboolean
744updatemodel(gpointer gdata) {
745  struct cbdata *data = gdata;
746  TrTorrent *tor;
747  tr_stat_t *st;
748  tr_info_t *in;
749  GtkTreeIter iter;
750  float up, down;
751  char *upstr, *downstr, *str;
752
753  if(0 < global_sigcount) {
754    quittransmission(data);
755    return FALSE;
756  }
757
758  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
759    do {
760      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
761      st = tr_torrent_stat(tor);
762      in = tr_torrent_info(tor);
763      g_object_unref(tor);
764      /* XXX find out if setting the same data emits changed signal */
765      gtk_list_store_set(GTK_LIST_STORE(data->model), &iter, MC_NAME, in->name,
766        MC_SIZE, in->totalSize, MC_STAT, st->status, MC_ERR, st->error,
767        MC_TERR, st->trackerError, MC_PROG, st->progress,
768        MC_DRATE, st->rateDownload, MC_URATE, st->rateUpload, MC_ETA, st->eta,
769        MC_PEERS, st->peersTotal, MC_UPEERS, st->peersUploading,
770        MC_DPEERS, st->peersDownloading, MC_DOWN, st->downloaded,
771        MC_UP, st->uploaded, -1);
772    } while(gtk_tree_model_iter_next(data->model, &iter));
773  }
774
775  /* update the status bar */
776  tr_torrentRates(tr_backend_handle(data->back), &up, &down);
777  downstr = readablesize(down * 1024.0);
778  upstr = readablesize(up * 1024.0);
779  str = g_strdup_printf(_("     Total DL: %s/s     Total UL: %s/s"),
780                        upstr, downstr);
781  gtk_statusbar_pop(data->bar, 0);
782  gtk_statusbar_push(data->bar, 0, str);
783  g_free(str);
784  g_free(upstr);
785  g_free(downstr);
786
787  /* the status of the selected item may have changed, so update the buttons */
788  fixbuttons(NULL, data);
789
790  /* check for politely stopped torrents unless we're exiting */
791  if(!data->closing)
792    tr_backend_torrents_stopped(data->back);
793
794  return TRUE;
795}
796
797/* show a popup menu for a right-click on the list */
798gboolean
799listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) {
800  struct cbdata *data = gdata;
801  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
802  GtkTreePath *path;
803  GtkTreeIter iter;
804  int status;
805  TrTorrent *tor, *issel;
806
807  if(GDK_BUTTON_PRESS == event->type && 3 == event->button) {
808    /* find what row, if any, the user clicked on */
809    if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path,
810                                      NULL, NULL, NULL))
811      gtk_tree_selection_unselect_all(sel);
812    else {
813      if(gtk_tree_model_get_iter(data->model, &iter, path)) {
814        /* get torrent and status for the right-clicked row */
815        gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
816                           MC_STAT, &status, -1);
817        issel = tor;
818        gtk_tree_selection_selected_foreach(sel, istorsel, &issel);
819        g_object_unref(tor);
820        /* if the clicked row isn't selected, select only it */
821        if(NULL != issel) {
822          gtk_tree_selection_unselect_all(sel);
823          gtk_tree_selection_select_iter(sel, &iter);
824        }
825      }
826      gtk_tree_path_free(path);
827    }
828    dopopupmenu(event, data);
829    return TRUE;
830  }
831
832  return FALSE;
833}
834
835gboolean
836listpopup(GtkWidget *widget SHUTUP, gpointer gdata) {
837  dopopupmenu(NULL, gdata);
838  return TRUE;
839}
840
841void
842dopopupmenu(GdkEventButton *event, struct cbdata *data) {
843  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
844  int count = gtk_tree_selection_count_selected_rows(sel);
845  GtkWidget *menu = gtk_menu_new();
846  GtkWidget *item;
847  unsigned int ii;
848  int status = 0;
849
850  if(NULL != data->stupidpopuphack)
851    gtk_widget_destroy(data->stupidpopuphack);
852  data->stupidpopuphack = menu;
853
854  status = 0;
855  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
856
857  for(ii = 0; ii < ALEN(actionitems); ii++) {
858    if(actionitems[ii].nomenu ||
859       (actionitems[ii].avail &&
860        (0 == count || !(actionitems[ii].avail & status))))
861      continue;
862    item = gtk_menu_item_new_with_label(gettext(actionitems[ii].name));
863    /* set the action for the menu item */
864    g_object_set_data(G_OBJECT(item), LIST_ACTION,
865                      GINT_TO_POINTER(actionitems[ii].act));
866    g_signal_connect(G_OBJECT(item), "activate",
867                     G_CALLBACK(actionclick), data);
868    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
869  }
870
871  gtk_widget_show_all(menu);
872
873  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
874                 (NULL == event ? 0 : event->button),
875                 gdk_event_get_time((GdkEvent*)event));
876}
877
878void
879actionclick(GtkWidget *widget, gpointer gdata) {
880  struct cbdata *data = gdata;
881  enum listact act;
882  GtkTreeSelection *sel;
883  GList *rows, *ii;
884  GtkTreeRowReference *ref;
885  GtkTreePath *path;
886  GtkTreeIter iter;
887  TrTorrent *tor;
888  unsigned int actoff, status;
889  gboolean changed;
890
891  act = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION));
892
893  switch(act) {
894    case ACT_OPEN:
895      makeaddwind(data->wind, addtorrents, data);
896      return;
897    case ACT_PREF:
898      if(!data->prefsopen)
899        makeprefwindow(data->wind, data->back, &data->prefsopen);
900      return;
901    case ACT_START:
902    case ACT_STOP:
903    case ACT_DELETE:
904    case ACT_INFO:
905      break;
906  }
907
908  sel = gtk_tree_view_get_selection(data->view);
909  rows = gtk_tree_selection_get_selected_rows(sel, NULL);
910
911  for(ii = rows; NULL != ii; ii = ii->next) {
912    ref = gtk_tree_row_reference_new(data->model, ii->data);
913    gtk_tree_path_free(ii->data);
914    ii->data = ref;
915  }
916
917  for(actoff = 0; actoff < ALEN(actionitems); actoff++)
918    if(actionitems[actoff].act == act)
919      break;
920  g_assert(actoff < ALEN(actionitems));
921
922  changed = FALSE;
923  for(ii = rows; NULL != ii; ii = ii->next) {
924    if(NULL != (path = gtk_tree_row_reference_get_path(ii->data)) &&
925       gtk_tree_model_get_iter(data->model, &iter, path)) {
926      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
927                         MC_STAT, &status, -1);
928      /* check if this action is valid for this torrent */
929      if((!actionitems[actoff].avail || actionitems[actoff].avail & status) &&
930         !actionitems[actoff].nomenu) {
931        switch(act) {
932          case ACT_START:
933            tr_torrentStart(tr_torrent_handle(tor));
934            changed = TRUE;
935            break;
936          case ACT_STOP:
937            tr_torrentStop(tr_torrent_handle(tor));
938            changed = TRUE;
939            break;
940          case ACT_DELETE:
941            /* tor will be unref'd in the politely_stopped signal handler */
942            g_object_ref(tor);
943            tr_torrent_stop_politely(tor);
944            gtk_list_store_remove(GTK_LIST_STORE(data->model), &iter);
945            changed = TRUE;
946            break;
947          case ACT_INFO:
948            makeinfowind(data->wind, tor);
949            break;
950          case ACT_OPEN:
951          case ACT_PREF:
952            break;
953        }
954      }
955      g_object_unref(tor);
956    }
957    if(NULL != path)
958      gtk_tree_path_free(path);
959    gtk_tree_row_reference_free(ii->data);
960  }
961  g_list_free(rows);
962
963  if(changed) {
964    savetorrents(data);
965    updatemodel(data);
966  }
967}
968
969gint
970intrevcmp(gconstpointer a, gconstpointer b) {
971  int aint = GPOINTER_TO_INT(a);
972  int bint = GPOINTER_TO_INT(b);
973
974  if(bint > aint)
975    return 1;
976  else if(bint < aint)
977    return -1;
978  else
979    return 0;
980}
981
982void
983doubleclick(GtkWidget *widget SHUTUP, GtkTreePath *path,
984            GtkTreeViewColumn *col SHUTUP, gpointer gdata) {
985  struct cbdata *data = gdata;
986  GtkTreeIter iter;
987  TrTorrent *tor;
988
989  if(gtk_tree_model_get_iter(data->model, &iter, path)) {
990    gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
991    makeinfowind(data->wind, tor);
992    g_object_unref(tor);
993  }
994}
995
996void
997addtorrents(void *vdata, void *state, GList *files,
998            const char *dir, gboolean *paused) {
999  struct cbdata *data = vdata;
1000  GList *torlist, *errlist, *ii;
1001  char *errstr;
1002  TrTorrent *tor;
1003  GtkTreeIter iter;
1004  char *wd;
1005
1006  errlist = NULL;
1007  torlist = NULL;
1008
1009  if(NULL != state)
1010    torlist = tr_backend_load_state(data->back, state, &errlist);
1011
1012  if(NULL != files) {
1013    if(NULL == dir)
1014      dir = cf_getpref(PREF_DIR);
1015    wd = NULL;
1016    if(NULL == dir) {
1017      wd = g_new(char, MAX_PATH_LENGTH + 1);
1018      if(NULL == getcwd(wd, MAX_PATH_LENGTH + 1))
1019        dir = ".";
1020      else
1021        dir = wd;
1022    }
1023    for(ii = g_list_first(files); NULL != ii; ii = ii->next) {
1024      errstr = NULL;
1025      tor = tr_torrent_new(G_OBJECT(data->back), ii->data,
1026                           dir, paused, &errstr);
1027      if(NULL != tor)
1028        torlist = g_list_append(torlist, tor);
1029      if(NULL != errstr)
1030        errlist = g_list_append(errlist, errstr);
1031    }
1032    if(NULL != wd)
1033      g_free(wd);
1034  }
1035
1036  for(ii = g_list_first(torlist); NULL != ii; ii = ii->next) {
1037    gtk_list_store_append(GTK_LIST_STORE(data->model), &iter);
1038    gtk_list_store_set(GTK_LIST_STORE(data->model), &iter,
1039                       MC_TORRENT, ii->data, -1);
1040    /* we will always ref a torrent before politely stopping it */
1041    g_signal_connect(ii->data, "politely_stopped",
1042                     G_CALLBACK(g_object_unref), data);
1043    g_object_unref(ii->data);
1044  }
1045
1046  if(NULL != errlist) {
1047    errstr = joinstrlist(errlist, "\n");
1048    errmsg(data->wind, ngettext("Failed to load torrent file:\n%s",
1049                                "Failed to load torrent files:\n%s",
1050                                g_list_length(errlist)), errstr);
1051    g_list_foreach(errlist, (GFunc)g_free, NULL);
1052    g_list_free(errlist);
1053    g_free(errstr);
1054  }
1055
1056  if(NULL != torlist) {
1057    updatemodel(data);
1058    savetorrents(data);
1059  }
1060}
1061
1062void
1063savetorrents(struct cbdata *data) {
1064  char *errstr;
1065
1066  tr_backend_save_state(data->back, &errstr);
1067  if(NULL != errstr) {
1068    errmsg(data->wind, "%s", errstr);
1069    g_free(errstr);
1070  }
1071}
1072
1073/* use with gtk_tree_selection_selected_foreach to | status of selected rows */
1074void
1075orstatus(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1076         gpointer gdata) {
1077  int *allstatus = gdata;
1078  int status;
1079
1080  gtk_tree_model_get(model, iter, MC_STAT, &status, -1);
1081  *allstatus |= status;
1082}
1083
1084/* data should be a TrTorrent**, will set torrent to NULL if it's selected */
1085void
1086istorsel(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1087         gpointer gdata) {
1088  TrTorrent **torref = gdata;
1089  TrTorrent *tor;
1090
1091  if(NULL != *torref) {
1092    gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);
1093    if(tor == *torref)
1094      *torref = NULL;
1095    g_object_unref(tor);
1096  }
1097}
1098
1099void
1100safepipe(void) {
1101  struct sigaction sa;
1102
1103  bzero(&sa, sizeof(sa));
1104  sa.sa_handler = SIG_IGN;
1105  sigaction(SIGPIPE, &sa, NULL);
1106}
1107
1108void
1109setupsighandlers(void) {
1110  int sigs[] = {SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
1111  struct sigaction sa;
1112  unsigned int ii;
1113
1114  bzero(&sa, sizeof(sa));
1115  sa.sa_handler = fatalsig;
1116  for(ii = 0; ii < ALEN(sigs); ii++)
1117    sigaction(sigs[ii], &sa, NULL);
1118}
1119
1120void
1121fatalsig(int sig) {
1122  struct sigaction sa;
1123
1124  if(SIGCOUNT_MAX <= ++global_sigcount) {
1125    bzero(&sa, sizeof(sa));
1126    sa.sa_handler = SIG_DFL;
1127    sigaction(sig, &sa, NULL);
1128    raise(sig);
1129  }
1130}
Note: See TracBrowser for help on using the repository browser.