source: branches/joshe/gtk/main.c @ 77

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

Add internationalization support.

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