source: trunk/gtk/main.c @ 24

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

Update 2006-01-03

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