source: trunk/gtk/main.c @ 130

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

Display --:--:-- instead of 00:00:00 as the ETA.

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 > tpeers)
680    tpeers = 0;
681  if(0 > upeers)
682    upeers = 0;
683  if(0 > dpeers)
684    dpeers = 0;
685  mb = readablesize(size, 1);
686  prog *= 100;
687
688  if(status & TR_STATUS_CHECK)
689    top = g_strdup_printf(_("Checking existing files (%.1f%%)"), prog);
690  else if(status & TR_STATUS_DOWNLOAD) {
691    if(0 > eta)
692      top = g_strdup_printf(_("Finishing in --:--:-- (%.1f%%)"), prog);
693    else
694      top = g_strdup_printf(_("Finishing in %02i:%02i:%02i (%.1f%%)"),
695                            eta / 60 / 60, eta / 60 % 60, eta % 60, prog);
696  }
697  else if(status & TR_STATUS_SEED)
698    top = g_strdup_printf(ngettext("Seeding, uploading to %d of %d peer",
699                                   "Seeding, uploading to %d of %d peers",
700                                   tpeers), dpeers, tpeers);
701  else if(status & TR_STATUS_STOPPING)
702    top = g_strdup(_("Stopping..."));
703  else if(status & TR_STATUS_PAUSE)
704    top = g_strdup_printf(_("Stopped (%.1f%%)"), prog);
705  else {
706    top = g_strdup("");
707    assert("XXX unknown status");
708  }
709
710  if(status & TR_TRACKER_ERROR) {
711    gtk_tree_model_get(model, iter, MC_ERR, &err, -1);
712    bottom = g_strconcat(_("Error: "), err, NULL);
713    g_free(err);
714  }
715  else if(status & TR_STATUS_DOWNLOAD)
716    bottom = g_strdup_printf(ngettext("Downloading from %i of %i peer",
717                                      "Downloading from %i of %i peers",
718                                      tpeers), upeers, tpeers);
719  else
720    bottom = NULL;
721
722  str = g_markup_printf_escaped("<big>%s (%s)</big>\n<small>%s\n%s</small>",
723                                name, mb, top, (NULL == bottom ? "" : bottom));
724  g_object_set(rend, "markup", str, NULL);
725  g_free(name);
726  g_free(mb);
727  g_free(str);
728  g_free(top);
729  g_free(bottom);
730}
731
732void
733dfprog(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend,
734       GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) {
735  char *dlstr, *ulstr, *str, *marked;
736  gfloat prog, dl, ul;
737  guint64 down, up;
738
739  /* XXX should I worry about gtk_tree_model_get failing? */
740  gtk_tree_model_get(model, iter, MC_PROG, &prog, MC_DRATE, &dl, MC_URATE, &ul,
741                     MC_DOWN, &down, MC_UP, &up, -1);
742  if(0.0 > prog)
743    prog = 0.0;
744  else if(1.0 < prog)
745    prog = 1.0;
746
747  ulstr = readablesize(ul * 1024.0, 2);
748  if(1.0 == prog) {
749    dlstr = ratiostr(down, up);
750    str = g_strdup_printf(_("Ratio: %s\nUL: %s/s"), dlstr, ulstr);
751  } else {
752    dlstr = readablesize(dl * 1024.0, 2);
753    str = g_strdup_printf(_("DL: %s/s\nUL: %s/s"), dlstr, ulstr);
754  }
755  marked = g_markup_printf_escaped("<small>%s</small>", str);
756  g_object_set(rend, "text", str, "value", prog, NULL);
757  g_free(dlstr);
758  g_free(ulstr);
759  g_free(str);
760  g_free(marked);
761}
762
763gboolean
764updatemodel(gpointer gdata) {
765  struct cbdata *data = gdata;
766  tr_stat_t *st;
767  int ii, max;
768  GtkTreeIter iter;
769  float up, down;
770  char *upstr, *downstr, *str;
771
772  blocksigs();
773
774  max = tr_torrentStat(data->tr, &st);
775  for(ii = 0; ii < max; ii++) {
776    if(!(ii ? gtk_tree_model_iter_next(GTK_TREE_MODEL(data->model), &iter) :
777         gtk_tree_model_get_iter_first(GTK_TREE_MODEL(data->model), &iter)))
778      gtk_list_store_append(data->model, &iter);
779    /* XXX find out if setting the same data emits changed signal */
780    gtk_list_store_set(
781      data->model, &iter, MC_ROW_INDEX, ii,
782      MC_NAME, st[ii].info.name, MC_SIZE, st[ii].info.totalSize,
783      MC_STAT, st[ii].status, MC_ERR, st[ii].error, MC_PROG, st[ii].progress,
784      MC_DRATE, st[ii].rateDownload, MC_URATE, st[ii].rateUpload,
785      MC_ETA, st[ii].eta, MC_PEERS, st[ii].peersTotal,
786      MC_UPEERS, st[ii].peersUploading, MC_DPEERS, st[ii].peersDownloading,
787      MC_DOWN, st[ii].downloaded, MC_UP, st[ii].uploaded, -1);
788  }
789  free(st);
790
791  /* remove any excess rows */
792  if(ii ? gtk_tree_model_iter_next(GTK_TREE_MODEL(data->model), &iter) :
793     gtk_tree_model_get_iter_first(GTK_TREE_MODEL(data->model), &iter))
794    while(gtk_list_store_remove(data->model, &iter))
795      ;
796
797  /* update the status bar */
798  tr_torrentRates(data->tr, &up, &down);
799  downstr = readablesize(down * 1024.0, 2);
800  upstr = readablesize(up * 1024.0, 2);
801  str = g_strdup_printf(_("     Total DL: %s/s     Total UL: %s/s"),
802                        upstr, downstr);
803  gtk_statusbar_pop(data->bar, 0);
804  gtk_statusbar_push(data->bar, 0, str);
805  g_free(str);
806  g_free(upstr);
807  g_free(downstr);
808
809  /* the status of the selected item may have changed, so update the buttons */
810  fixbuttons(NULL, data);
811
812  unblocksigs();
813
814  return TRUE;
815}
816
817/* show a popup menu for a right-click on the list */
818gboolean
819listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) {
820  struct cbdata *data = gdata;
821  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
822  GtkTreePath *path;
823  GtkTreeIter iter;
824  int index, status;
825  GList *ids;
826
827  if(GDK_BUTTON_PRESS == event->type && 3 == event->button) {
828    /* find what row, if any, the user clicked on */
829    if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path,
830                                      NULL, NULL, NULL))
831      /* no row was clicked, do the popup with no torrent IDs or status */
832      dopopupmenu(event, data, NULL, 0);
833    else {
834      if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model), &iter, path)) {
835        /* get ID and status for the right-clicked row */
836        gtk_tree_model_get(GTK_TREE_MODEL(data->model), &iter,
837                           MC_ROW_INDEX, &index, MC_STAT, &status, -1);
838        /* get a list of selected IDs */
839        ids = NULL;
840        gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
841        /* is the clicked row selected? */
842        if(NULL == g_list_find(ids, GINT_TO_POINTER(index))) {
843          /* no, do the popup for just the clicked row */
844          g_list_free(ids);
845          dopopupmenu(event, data, g_list_append(NULL, GINT_TO_POINTER(index)),
846                      status);
847        } else {
848          /* yes, do the popup for all the selected rows */
849          gtk_tree_selection_selected_foreach(sel, orstatus, &status);
850          dopopupmenu(event, data, ids, status);
851        }
852      }
853      gtk_tree_path_free(path);
854    }
855    return TRUE;
856  }
857
858  return FALSE;
859}
860
861gboolean
862listpopup(GtkWidget *widget SHUTUP, gpointer gdata) {
863  struct cbdata *data = gdata;
864  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
865  GtkTreeModel *model;
866  GList *ids;
867  int status;
868
869  if(0 >= gtk_tree_selection_count_selected_rows(sel))
870    dopopupmenu(NULL, data, NULL, 0);
871  else {
872    assert(model == GTK_TREE_MODEL(data->model));
873    status = 0;
874    gtk_tree_selection_selected_foreach(sel, orstatus, &status);
875    ids = NULL;
876    gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
877    dopopupmenu(NULL, data, ids, status);
878  }
879
880  return TRUE;
881}
882
883void
884dopopupmenu(GdkEventButton *event, struct cbdata *data,
885            GList *ids, int status) {
886  GtkWidget *menu = gtk_menu_new();
887  GtkWidget *item;
888  unsigned int ii;
889
890  for(ii = 0; ii < ALEN(actionitems); ii++) {
891    if(actionitems[ii].nomenu ||
892       (actionitems[ii].avail &&
893        (NULL == ids || !(actionitems[ii].avail & status))))
894      continue;
895    item = gtk_menu_item_new_with_label(gettext(actionitems[ii].name));
896    /* set the action for the menu item */
897    g_object_set_data(G_OBJECT(item), LIST_ACTION,
898                      GINT_TO_POINTER(actionitems[ii].act));
899    /* show that this action came from a popup menu */
900    g_object_set_data(G_OBJECT(item), LIST_ACTION_FROM,
901                      GINT_TO_POINTER(FROM_POPUP));
902    /* set a glist of selected torrent's IDs */
903    g_object_set_data(G_OBJECT(item), LIST_INDEX, ids);
904    /* set the menu widget, so the activate handler can destroy it */
905    g_object_set_data(G_OBJECT(item), LIST_MENU_WIDGET, menu);
906    g_signal_connect(G_OBJECT(item), "activate",
907                     G_CALLBACK(actionclick), data);
908    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
909  }
910
911  /* set up the glist to be freed when the menu is destroyed */
912  g_object_set_data_full(G_OBJECT(menu), LIST_INDEX, ids,
913                         (GDestroyNotify)g_list_free);
914
915  /* destroy the menu if the user doesn't select anything */
916  g_signal_connect(menu, "selection-done", G_CALLBACK(killmenu), NULL);
917
918  gtk_widget_show_all(menu);
919
920  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
921                 (NULL == event ? 0 : event->button),
922                 gdk_event_get_time((GdkEvent*)event));
923}
924
925void
926killmenu(GtkWidget *menu, gpointer *gdata SHUTUP) {
927  gtk_widget_destroy(menu);
928}
929
930void
931actionclick(GtkWidget *widget, gpointer gdata) {
932  struct cbdata *data = gdata;
933  GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view);
934  enum listact act =
935    GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION));
936  enum listfrom from =
937    GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), LIST_ACTION_FROM));
938  int index, count;
939  unsigned int actindex;
940  tr_stat_t *sb;
941  GList *ids, *ii;
942  gboolean updatesave;
943
944  /* destroy the popup menu, if any */
945  if(FROM_POPUP == from)
946    gtk_widget_destroy(g_object_get_data(G_OBJECT(widget), LIST_MENU_WIDGET));
947
948  switch(act) {
949    case ACT_OPEN:
950      makeaddwind(addtorrent, data->wind, data->tr, addedtorrents, data);
951      return;
952    case ACT_PREF:
953      if(!data->prefsopen)
954        makeprefwindow(data->wind, data->tr, &data->prefsopen);
955      return;
956    default:
957      break;
958  }
959
960  switch(from) {
961    case FROM_BUTTON:
962      ids = NULL;
963      gtk_tree_selection_selected_foreach(sel, makeidlist, &ids);
964      /* XXX should I assert(0 <= index) to insure a row was selected? */
965      break;
966    case FROM_POPUP:
967      ids = g_object_get_data(G_OBJECT(widget), LIST_INDEX);
968      break;
969    default:
970      assert(!"unknown action source");
971      break;
972  }
973
974  for(actindex = 0; actindex < ALEN(actionitems); actindex++)
975    if(actionitems[actindex].act == act)
976      break;
977  assert(actindex < ALEN(actionitems));
978
979  blocksigs();
980  updatesave = FALSE;
981  count = tr_torrentStat(data->tr, &sb);
982
983  for(ii = g_list_sort(ids, intrevcmp); NULL != ii; ii = ii->next) {
984    index = GPOINTER_TO_INT(ii->data);
985    if(index >= count) {
986      assert(!"illegal torrent id");
987      continue;
988    }
989    /* check if this action is valid for this torrent */
990    if(actionitems[actindex].nomenu ||
991       (actionitems[actindex].avail &&
992        !(actionitems[actindex].avail & sb[index].status)))
993      continue;
994
995    switch(act) {
996      case ACT_START:
997        tr_torrentStart(data->tr, index);
998        updatesave = TRUE;
999        break;
1000      case ACT_STOP:
1001        tr_torrentStop(data->tr, index);
1002        updatesave = TRUE;
1003        break;
1004      case ACT_DELETE:
1005        if(TR_TORRENT_NEEDS_STOP(sb[index].status))
1006          tr_torrentStop(data->tr, index);
1007        tr_torrentClose(data->tr, index);
1008        updatesave = TRUE;
1009        /* XXX should only unselect deleted rows */
1010        gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(data->view));
1011        break;
1012      case ACT_INFO:
1013        makeinfowind(data->wind, data->tr, index);
1014        break;
1015      default:
1016        assert(!"unknown type");
1017        break;
1018    }
1019  }
1020  free(sb);
1021
1022  if(updatesave) {
1023    savetorrents(data->tr, data->wind, -1, NULL);
1024    updatemodel(data);
1025  }
1026
1027  unblocksigs();
1028
1029  if(FROM_BUTTON == from)
1030    g_list_free(ids);
1031}
1032
1033gint
1034intrevcmp(gconstpointer a, gconstpointer b) {
1035  int aint = GPOINTER_TO_INT(a);
1036  int bint = GPOINTER_TO_INT(b);
1037
1038  if(bint > aint)
1039    return 1;
1040  else if(bint < aint)
1041    return -1;
1042  else
1043    return 0;
1044}
1045
1046void
1047doubleclick(GtkWidget *widget SHUTUP, GtkTreePath *path,
1048            GtkTreeViewColumn *col SHUTUP, gpointer gdata) {
1049  struct cbdata *data = gdata;
1050  GtkTreeIter iter;
1051  int index;
1052
1053  if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model), &iter, path)) {
1054    gtk_tree_model_get(GTK_TREE_MODEL(data->model), &iter,
1055                       MC_ROW_INDEX, &index, -1);
1056    makeinfowind(data->wind, data->tr, index);
1057  }
1058}
1059
1060gboolean
1061addtorrent(tr_handle_t *tr, GtkWindow *parentwind, const char *torrent,
1062           const char *dir, gboolean paused, GList **errs) {
1063  char *wd;
1064
1065  if(NULL == dir && NULL != (dir = cf_getpref(PREF_DIR))) {
1066    if(!mkdir_p(dir, 0777)) {
1067      errmsg(parentwind, _("Failed to create the directory %s:\n%s"),
1068             dir, strerror(errno));
1069      return FALSE;
1070    }
1071  }
1072
1073  blocksigs();
1074
1075  if(0 != tr_torrentInit(tr, torrent)) {
1076    unblocksigs();
1077    /* XXX would be nice to have errno strings, are they printed to stdout? */
1078    if(NULL == errs)
1079      errmsg(parentwind, _("Failed to load the torrent file %s"), torrent);
1080    else
1081      *errs = g_list_append(*errs, g_strdup(torrent));
1082    return FALSE;
1083  }
1084
1085  if(NULL != dir)
1086    tr_torrentSetFolder(tr, tr_torrentCount(tr) - 1, dir);
1087  else {
1088    wd = g_new(char, MAXPATHLEN + 1);
1089    if(NULL == getcwd(wd, MAXPATHLEN + 1))
1090      tr_torrentSetFolder(tr, tr_torrentCount(tr) - 1, ".");
1091    else {
1092      tr_torrentSetFolder(tr, tr_torrentCount(tr) - 1, wd);
1093      free(wd);
1094    }
1095  }
1096
1097  if(!paused)
1098    tr_torrentStart(tr, tr_torrentCount(tr) - 1);
1099
1100  unblocksigs();
1101
1102  return TRUE;
1103}
1104
1105void
1106addedtorrents(void *vdata) {
1107  struct cbdata *data = vdata;
1108
1109  updatemodel(data);
1110  savetorrents(data->tr, data->wind, -1, NULL);
1111}
1112
1113gboolean
1114savetorrents(tr_handle_t *tr, GtkWindow *wind, int count, tr_stat_t *stat) {
1115  char *errstr;
1116  tr_stat_t *st;
1117  gboolean ret;
1118
1119  assert(NULL != tr || 0 <= count);
1120
1121  if(0 <= count)
1122    ret = cf_savestate(count, stat, &errstr);
1123  else {
1124    blocksigs();
1125    count = tr_torrentStat(tr, &st);
1126    unblocksigs();
1127    ret = cf_savestate(count, st, &errstr);
1128    free(st);
1129  }
1130
1131  if(!ret) {
1132    errmsg(wind, "%s", errstr);
1133    g_free(errstr);
1134  }
1135
1136  return ret;
1137}
1138
1139/* use with gtk_tree_selection_selected_foreach to | status of selected rows */
1140void
1141orstatus(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1142         gpointer gdata) {
1143  int *allstatus = gdata;
1144  int status;
1145
1146  gtk_tree_model_get(model, iter, MC_STAT, &status, -1);
1147  *allstatus |= status;
1148}
1149
1150void
1151makeidlist(GtkTreeModel *model, GtkTreePath *path SHUTUP, GtkTreeIter *iter,
1152           gpointer gdata) {
1153  GList **ids = gdata;
1154  int index;
1155
1156  gtk_tree_model_get(model, iter, MC_ROW_INDEX, &index, -1);
1157  *ids = g_list_append(*ids, GINT_TO_POINTER(index));
1158}
Note: See TracBrowser for help on using the repository browser.