source: trunk/gtk/main.c @ 125

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

Rewrite the drag and drop code to handle multiple files (oops),
as well as being much more robust about misformatted file URIs.

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