source: trunk/gtk/main.c @ 320

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

Add support to the GTK GUI for saving private copies of torrent files.
The prefs dialog for this sucks, but it should work.

  • Property svn:keywords set to Date Rev Author Id
File size: 33.7 KB
Line 
1/*
2  $Id: main.c 320 2006-06-10 06:53:20Z 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, 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  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, addactionflag(cf_getpref(PREF_ADDIPC)));
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,
596                  addactionflag(cf_getpref(PREF_ADDDND)));
597      freestrlist(paths);
598    }
599    g_free(files);
600  }
601
602  gtk_drag_finish(dc, (NULL != paths), FALSE, time);
603}
604
605void
606setupdrag(GtkWidget *widget, struct cbdata *data) {
607  GtkTargetEntry targets[] = {
608    { "STRING",     0, 0 },
609    { "text/plain", 0, 0 },
610    { "text/uri-list", 0, 0 },
611  };
612
613  g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data);
614
615  gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets,
616                    ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE);
617}
618
619/* kludge to have the progress bars notice theme changes */
620static void
621stylekludge(GObject *obj, GParamSpec *spec, gpointer gdata) {
622  if(0 == strcmp("style", spec->name)) {
623    tr_cell_renderer_torrent_reset_style(TR_CELL_RENDERER_TORRENT(gdata));
624    gtk_widget_queue_draw(GTK_WIDGET(obj));
625  }
626}
627
628/* disable buttons the user shouldn't be able to click on */
629void
630fixbuttons(GtkTreeSelection *sel, gpointer gdata) {
631  struct cbdata *data = gdata;
632  gboolean selected;
633  unsigned int ii;
634  int status;
635
636  if(NULL == sel)
637    sel = gtk_tree_view_get_selection(data->view);
638  status = 0;
639  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
640  selected = (0 < gtk_tree_selection_count_selected_rows(sel));
641
642  for(ii = 0; ii < ALEN(actionitems); ii++)
643    if(actionitems[ii].avail)
644      gtk_widget_set_sensitive(data->buttons[ii],
645                               (selected && (actionitems[ii].avail & status)));
646}
647
648void
649dfname(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
650       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
651  char *name, *mb, *terr, *str, *top, *bottom;
652  guint64 size;
653  gfloat prog;
654  int status, err, eta, tpeers, upeers, dpeers;
655
656  gtk_tree_model_get(model, iter, MC_NAME, &name, MC_STAT, &status,
657    MC_ERR, &err, MC_SIZE, &size, MC_PROG, &prog, MC_ETA, &eta,
658    MC_PEERS, &tpeers, MC_UPEERS, &upeers, MC_DPEERS, &dpeers, -1);
659
660  if(0 > tpeers)
661    tpeers = 0;
662  if(0 > upeers)
663    upeers = 0;
664  if(0 > dpeers)
665    dpeers = 0;
666  mb = readablesize(size);
667  prog *= 100;
668
669  if(status & TR_STATUS_CHECK)
670    top = g_strdup_printf(_("Checking existing files (%.1f%%)"), prog);
671  else if(status & TR_STATUS_DOWNLOAD) {
672    if(0 > eta)
673      top = g_strdup_printf(_("Finishing in --:--:-- (%.1f%%)"), prog);
674    else
675      top = g_strdup_printf(_("Finishing in %02i:%02i:%02i (%.1f%%)"),
676                            eta / 60 / 60, eta / 60 % 60, eta % 60, prog);
677  }
678  else if(status & TR_STATUS_SEED)
679    top = g_strdup_printf(ngettext("Seeding, uploading to %d of %d peer",
680                                   "Seeding, uploading to %d of %d peers",
681                                   tpeers), dpeers, tpeers);
682  else if(status & TR_STATUS_STOPPING)
683    top = g_strdup(_("Stopping..."));
684  else if(status & TR_STATUS_PAUSE)
685    top = g_strdup_printf(_("Stopped (%.1f%%)"), prog);
686  else {
687    top = g_strdup("");
688    g_assert_not_reached();
689  }
690
691  if(TR_NOERROR != err) {
692    gtk_tree_model_get(model, iter, MC_TERR, &terr, -1);
693    bottom = g_strconcat(_("Error: "), terr, NULL);
694    g_free(terr);
695  }
696  else if(status & TR_STATUS_DOWNLOAD)
697    bottom = g_strdup_printf(ngettext("Downloading from %i of %i peer",
698                                      "Downloading from %i of %i peers",
699                                      tpeers), upeers, tpeers);
700  else
701    bottom = NULL;
702
703  str = g_markup_printf_escaped("<big>%s (%s)</big>\n<small>%s\n%s</small>",
704                                name, mb, top, (NULL == bottom ? "" : bottom));
705  g_object_set(rend, "markup", str, NULL);
706  g_free(name);
707  g_free(mb);
708  g_free(str);
709  g_free(top);
710  if(NULL != bottom)
711    g_free(bottom);
712}
713
714void
715dfprog(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
716       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
717  char *dlstr, *ulstr, *str, *marked;
718  gfloat prog, dl, ul;
719  guint64 down, up;
720
721  gtk_tree_model_get(model, iter, MC_PROG, &prog, MC_DRATE, &dl, MC_URATE, &ul,
722                     MC_DOWN, &down, MC_UP, &up, -1);
723  if(0.0 > prog)
724    prog = 0.0;
725  else if(1.0 < prog)
726    prog = 1.0;
727
728  ulstr = readablesize(ul * 1024.0);
729  if(1.0 == prog) {
730    dlstr = ratiostr(down, up);
731    str = g_strdup_printf(_("Ratio: %s\nUL: %s/s"), dlstr, ulstr);
732  } else {
733    dlstr = readablesize(dl * 1024.0);
734    str = g_strdup_printf(_("DL: %s/s\nUL: %s/s"), dlstr, ulstr);
735  }
736  marked = g_markup_printf_escaped("<small>%s</small>", str);
737  g_object_set(rend, "text", str, "value", prog, NULL);
738  g_free(dlstr);
739  g_free(ulstr);
740  g_free(str);
741  g_free(marked);
742}
743
744gboolean
745updatemodel(gpointer gdata) {
746  struct cbdata *data = gdata;
747  TrTorrent *tor;
748  tr_stat_t *st;
749  tr_info_t *in;
750  GtkTreeIter iter;
751  float up, down;
752  char *upstr, *downstr, *str;
753
754  if(0 < global_sigcount) {
755    quittransmission(data);
756    return FALSE;
757  }
758
759  if(gtk_tree_model_get_iter_first(data->model, &iter)) {
760    do {
761      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
762      st = tr_torrent_stat(tor);
763      in = tr_torrent_info(tor);
764      g_object_unref(tor);
765      /* XXX find out if setting the same data emits changed signal */
766      gtk_list_store_set(GTK_LIST_STORE(data->model), &iter, MC_NAME, in->name,
767        MC_SIZE, in->totalSize, MC_STAT, st->status, MC_ERR, st->error,
768        MC_TERR, st->trackerError, MC_PROG, st->progress,
769        MC_DRATE, st->rateDownload, MC_URATE, st->rateUpload, MC_ETA, st->eta,
770        MC_PEERS, st->peersTotal, MC_UPEERS, st->peersUploading,
771        MC_DPEERS, st->peersDownloading, MC_DOWN, st->downloaded,
772        MC_UP, st->uploaded, -1);
773    } while(gtk_tree_model_iter_next(data->model, &iter));
774  }
775
776  /* update the status bar */
777  tr_torrentRates(tr_backend_handle(data->back), &up, &down);
778  downstr = readablesize(down * 1024.0);
779  upstr = readablesize(up * 1024.0);
780  str = g_strdup_printf(_("     Total DL: %s/s     Total UL: %s/s"),
781                        upstr, downstr);
782  gtk_statusbar_pop(data->bar, 0);
783  gtk_statusbar_push(data->bar, 0, str);
784  g_free(str);
785  g_free(upstr);
786  g_free(downstr);
787
788  /* the status of the selected item may have changed, so update the buttons */
789  fixbuttons(NULL, data);
790
791  /* check for politely stopped torrents unless we're exiting */
792  if(!data->closing)
793    tr_backend_torrents_stopped(data->back);
794
795  return TRUE;
796}
797
798/* show a popup menu for a right-click on the list */
799gboolean
800listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) {
801  struct cbdata *data = gdata;
802  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
803  GtkTreePath *path;
804  GtkTreeIter iter;
805  int status;
806  TrTorrent *tor, *issel;
807
808  if(GDK_BUTTON_PRESS == event->type && 3 == event->button) {
809    /* find what row, if any, the user clicked on */
810    if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path,
811                                      NULL, NULL, NULL))
812      gtk_tree_selection_unselect_all(sel);
813    else {
814      if(gtk_tree_model_get_iter(data->model, &iter, path)) {
815        /* get torrent and status for the right-clicked row */
816        gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
817                           MC_STAT, &status, -1);
818        issel = tor;
819        gtk_tree_selection_selected_foreach(sel, istorsel, &issel);
820        g_object_unref(tor);
821        /* if the clicked row isn't selected, select only it */
822        if(NULL != issel) {
823          gtk_tree_selection_unselect_all(sel);
824          gtk_tree_selection_select_iter(sel, &iter);
825        }
826      }
827      gtk_tree_path_free(path);
828    }
829    dopopupmenu(event, data);
830    return TRUE;
831  }
832
833  return FALSE;
834}
835
836gboolean
837listpopup(GtkWidget *widget SHUTUP, gpointer gdata) {
838  dopopupmenu(NULL, gdata);
839  return TRUE;
840}
841
842void
843dopopupmenu(GdkEventButton *event, struct cbdata *data) {
844  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
845  int count = gtk_tree_selection_count_selected_rows(sel);
846  GtkWidget *menu = gtk_menu_new();
847  GtkWidget *item;
848  unsigned int ii;
849  int status = 0;
850
851  if(NULL != data->stupidpopuphack)
852    gtk_widget_destroy(data->stupidpopuphack);
853  data->stupidpopuphack = menu;
854
855  status = 0;
856  gtk_tree_selection_selected_foreach(sel, orstatus, &status);
857
858  for(ii = 0; ii < ALEN(actionitems); ii++) {
859    if(actionitems[ii].nomenu ||
860       (actionitems[ii].avail &&
861        (0 == count || !(actionitems[ii].avail & status))))
862      continue;
863    item = gtk_menu_item_new_with_label(gettext(actionitems[ii].name));
864    /* set the action for the menu item */
865    g_object_set_data(G_OBJECT(item), LIST_ACTION,
866                      GINT_TO_POINTER(actionitems[ii].act));
867    g_signal_connect(G_OBJECT(item), "activate",
868                     G_CALLBACK(actionclick), data);
869    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
870  }
871
872  gtk_widget_show_all(menu);
873
874  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
875                 (NULL == event ? 0 : event->button),
876                 gdk_event_get_time((GdkEvent*)event));
877}
878
879void
880actionclick(GtkWidget *widget, gpointer gdata) {
881  struct cbdata *data = gdata;
882  enum listact act;
883  GtkTreeSelection *sel;
884  GList *rows, *ii;
885  GtkTreeRowReference *ref;
886  GtkTreePath *path;
887  GtkTreeIter iter;
888  TrTorrent *tor;
889  unsigned int actoff, status;
890  gboolean changed;
891
892  act = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION));
893
894  switch(act) {
895    case ACT_OPEN:
896      makeaddwind(data->wind, addtorrents, data);
897      return;
898    case ACT_PREF:
899      if(!data->prefsopen)
900        makeprefwindow(data->wind, data->back, &data->prefsopen);
901      return;
902    case ACT_START:
903    case ACT_STOP:
904    case ACT_DELETE:
905    case ACT_INFO:
906      break;
907  }
908
909  sel = gtk_tree_view_get_selection(data->view);
910  rows = gtk_tree_selection_get_selected_rows(sel, NULL);
911
912  for(ii = rows; NULL != ii; ii = ii->next) {
913    ref = gtk_tree_row_reference_new(data->model, ii->data);
914    gtk_tree_path_free(ii->data);
915    ii->data = ref;
916  }
917
918  for(actoff = 0; actoff < ALEN(actionitems); actoff++)
919    if(actionitems[actoff].act == act)
920      break;
921  g_assert(actoff < ALEN(actionitems));
922
923  changed = FALSE;
924  for(ii = rows; NULL != ii; ii = ii->next) {
925    if(NULL != (path = gtk_tree_row_reference_get_path(ii->data)) &&
926       gtk_tree_model_get_iter(data->model, &iter, path)) {
927      gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor,
928                         MC_STAT, &status, -1);
929      /* check if this action is valid for this torrent */
930      if((!actionitems[actoff].avail || actionitems[actoff].avail & status) &&
931         !actionitems[actoff].nomenu) {
932        switch(act) {
933          case ACT_START:
934            tr_torrentStart(tr_torrent_handle(tor));
935            changed = TRUE;
936            break;
937          case ACT_STOP:
938            tr_torrentStop(tr_torrent_handle(tor));
939            changed = TRUE;
940            break;
941          case ACT_DELETE:
942            /* tor will be unref'd in the politely_stopped signal handler */
943            g_object_ref(tor);
944            tr_torrent_stop_politely(tor);
945            if(TR_FSAVEPRIVATE & tr_torrent_info(tor)->flags)
946              tr_torrentRemoveSaved(tr_torrent_handle(tor));
947            gtk_list_store_remove(GTK_LIST_STORE(data->model), &iter);
948            changed = TRUE;
949            break;
950          case ACT_INFO:
951            makeinfowind(data->wind, tor);
952            break;
953          case ACT_OPEN:
954          case ACT_PREF:
955            break;
956        }
957      }
958      g_object_unref(tor);
959    }
960    if(NULL != path)
961      gtk_tree_path_free(path);
962    gtk_tree_row_reference_free(ii->data);
963  }
964  g_list_free(rows);
965
966  if(changed) {
967    savetorrents(data);
968    updatemodel(data);
969  }
970}
971
972gint
973intrevcmp(gconstpointer a, gconstpointer b) {
974  int aint = GPOINTER_TO_INT(a);
975  int bint = GPOINTER_TO_INT(b);
976
977  if(bint > aint)
978    return 1;
979  else if(bint < aint)
980    return -1;
981  else
982    return 0;
983}
984
985void
986doubleclick(GtkWidget *widget SHUTUP, GtkTreePath *path,
987            GtkTreeViewColumn *col SHUTUP, gpointer gdata) {
988  struct cbdata *data = gdata;
989  GtkTreeIter iter;
990  TrTorrent *tor;
991
992  if(gtk_tree_model_get_iter(data->model, &iter, path)) {
993    gtk_tree_model_get(data->model, &iter, MC_TORRENT, &tor, -1);
994    makeinfowind(data->wind, tor);
995    g_object_unref(tor);
996  }
997}
998
999void
1000addtorrents(void *vdata, void *state, GList *files,
1001            const char *dir, guint flags) {
1002  struct cbdata *data = vdata;
1003  GList *torlist, *errlist, *ii;
1004  char *errstr;
1005  TrTorrent *tor;
1006  GtkTreeIter iter;
1007  char *wd;
1008
1009  errlist = NULL;
1010  torlist = NULL;
1011
1012  if(NULL != state)
1013    torlist = tr_backend_load_state(data->back, state, &errlist);
1014
1015  if(NULL != files) {
1016    if(NULL == dir)
1017      dir = cf_getpref(PREF_DIR);
1018    wd = NULL;
1019    if(NULL == dir) {
1020      wd = g_new(char, MAX_PATH_LENGTH + 1);
1021      if(NULL == getcwd(wd, MAX_PATH_LENGTH + 1))
1022        dir = ".";
1023      else
1024        dir = wd;
1025    }
1026    for(ii = g_list_first(files); NULL != ii; ii = ii->next) {
1027      errstr = NULL;
1028      tor = tr_torrent_new(G_OBJECT(data->back), ii->data, dir,
1029                           flags, &errstr);
1030      if(NULL != tor)
1031        torlist = g_list_append(torlist, tor);
1032      if(NULL != errstr)
1033        errlist = g_list_append(errlist, errstr);
1034    }
1035    if(NULL != wd)
1036      g_free(wd);
1037  }
1038
1039  for(ii = g_list_first(torlist); NULL != ii; ii = ii->next) {
1040    gtk_list_store_append(GTK_LIST_STORE(data->model), &iter);
1041    gtk_list_store_set(GTK_LIST_STORE(data->model), &iter,
1042                       MC_TORRENT, ii->data, -1);
1043    /* we will always ref a torrent before politely stopping it */
1044    g_signal_connect(ii->data, "politely_stopped",
1045                     G_CALLBACK(g_object_unref), data);
1046    g_object_unref(ii->data);
1047  }
1048
1049  if(NULL != errlist) {
1050    errstr = joinstrlist(errlist, "\n");
1051    errmsg(data->wind, ngettext("Failed to load torrent file:\n%s",
1052                                "Failed to load torrent files:\n%s",
1053                                g_list_length(errlist)), errstr);
1054    g_list_foreach(errlist, (GFunc)g_free, NULL);
1055    g_list_free(errlist);
1056    g_free(errstr);
1057  }
1058
1059  if(NULL != torlist) {
1060    updatemodel(data);
1061    savetorrents(data);
1062  }
1063}
1064
1065void
1066savetorrents(struct cbdata *data) {
1067  char *errstr;
1068
1069  tr_backend_save_state(data->back, &errstr);
1070  if(NULL != errstr) {
1071    errmsg(data->wind, "%s", errstr);
1072    g_free(errstr);
1073  }
1074}
1075
1076/* use with gtk_tree_selection_selected_foreach to | status of selected rows */
1077void
1078orstatus(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1079         gpointer gdata) {
1080  int *allstatus = gdata;
1081  int status;
1082
1083  gtk_tree_model_get(model, iter, MC_STAT, &status, -1);
1084  *allstatus |= status;
1085}
1086
1087/* data should be a TrTorrent**, will set torrent to NULL if it's selected */
1088void
1089istorsel(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1090         gpointer gdata) {
1091  TrTorrent **torref = gdata;
1092  TrTorrent *tor;
1093
1094  if(NULL != *torref) {
1095    gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);
1096    if(tor == *torref)
1097      *torref = NULL;
1098    g_object_unref(tor);
1099  }
1100}
1101
1102void
1103safepipe(void) {
1104  struct sigaction sa;
1105
1106  bzero(&sa, sizeof(sa));
1107  sa.sa_handler = SIG_IGN;
1108  sigaction(SIGPIPE, &sa, NULL);
1109}
1110
1111void
1112setupsighandlers(void) {
1113  int sigs[] = {SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
1114  struct sigaction sa;
1115  unsigned int ii;
1116
1117  bzero(&sa, sizeof(sa));
1118  sa.sa_handler = fatalsig;
1119  for(ii = 0; ii < ALEN(sigs); ii++)
1120    sigaction(sigs[ii], &sa, NULL);
1121}
1122
1123void
1124fatalsig(int sig) {
1125  struct sigaction sa;
1126
1127  if(SIGCOUNT_MAX <= ++global_sigcount) {
1128    bzero(&sa, sizeof(sa));
1129    sa.sa_handler = SIG_DFL;
1130    sigaction(sig, &sa, NULL);
1131    raise(sig);
1132  }
1133}
Note: See TracBrowser for help on using the repository browser.