source: branches/oneport/gtk/main.c @ 49

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

Many small usability improvements.
Remove an ugly wart in the conf api.

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