source: trunk/gtk/main.c @ 23

Last change on this file since 23 was 23, checked in by root, 16 years ago

Update 2005-12-29

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