diff --git a/gtk/Jamfile b/gtk/Jamfile index 1e68e146e..1439ed043 100644 --- a/gtk/Jamfile +++ b/gtk/Jamfile @@ -1,6 +1,6 @@ SubDir TOP gtk ; -GTK_SRC = conf.c main.c prefs.c util.c ; +GTK_SRC = conf.c main.c prefs.c util.c gtkcellrenderertorrent.c ; Main transmission-gtk : $(GTK_SRC) ; LinkLibraries transmission-gtk : libtransmission.a ; diff --git a/gtk/gtkcellrenderertorrent.c b/gtk/gtkcellrenderertorrent.c new file mode 100644 index 000000000..899abda17 --- /dev/null +++ b/gtk/gtkcellrenderertorrent.c @@ -0,0 +1,300 @@ +/* gtkcellrenderertorrent.c + * Copyright (C) 2002 Naba Kumar + * heavily modified by Jörgen Scheibengruber + * heavily modified by Marco Pesenti Gritti + * heavily modified by Josh Elsasser for transmission + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +/* + * Modified by the GTK+ Team and others 1997-2004. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#include + +#include "gtkcellrenderertorrent.h" +#include "util.h" + +enum { PROP_0, PROP_VALUE, PROP_TEXT, PROP_LABEL }; + +struct _GtkCellRendererTorrentPrivate { + gfloat value; + gchar *text; + PangoAttrList *text_attrs; + gchar *label; + PangoAttrList *label_attrs; + GtkStyle *style; +}; + +static void +finalize(GObject *object); +static void +get_property(GObject *obj, guint id, GValue *value, GParamSpec *spec); +static void +set_property(GObject *obj, guint id, const GValue *value, GParamSpec *spec); +static void +get_size(GtkCellRenderer *cell, GtkWidget *widget, GdkRectangle *area, + gint *xoff, gint *yoff, gint *width, gint *height); +static void +render(GtkCellRenderer *cell, GdkWindow *window, GtkWidget *widget, + GdkRectangle *bg, GdkRectangle *area, GdkRectangle *exp, guint flags); + + +G_DEFINE_TYPE(GtkCellRendererTorrent, gtk_cell_renderer_torrent, GTK_TYPE_CELL_RENDERER); + +static void +gtk_cell_renderer_torrent_class_init (GtkCellRendererTorrentClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass); + + object_class->finalize = finalize; + object_class->get_property = get_property; + object_class->set_property = set_property; + + cell_class->get_size = get_size; + cell_class->render = render; + + g_object_class_install_property( + object_class, PROP_VALUE, + g_param_spec_float("value", "Value", "Value of the torrent bar", + 0.0, 1.0, 0.0, G_PARAM_READWRITE)); + + g_object_class_install_property( + object_class, PROP_TEXT, + g_param_spec_string ("text", "Text", "Text under the torrent bar", + /* XXX should I have NULL or "" here, and is initial strdup needed? */ + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property( + object_class, PROP_LABEL, + g_param_spec_string ("label", "Label", "Text on the torrent bar", + NULL, G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, + sizeof (GtkCellRendererTorrentPrivate)); +} + +static void +gtk_cell_renderer_torrent_init(GtkCellRendererTorrent *tcell) { + tcell->priv = G_TYPE_INSTANCE_GET_PRIVATE( + tcell, GTK_TYPE_CELL_RENDERER_TORRENT, GtkCellRendererTorrentPrivate); + tcell->priv->value = 0.0; + tcell->priv->text = g_strdup(""); + tcell->priv->text_attrs = NULL; + tcell->priv->label = g_strdup(""); + tcell->priv->text_attrs = NULL; + tcell->priv->style = NULL; +} + +GtkCellRenderer* +gtk_cell_renderer_torrent_new(void) { + return g_object_new (GTK_TYPE_CELL_RENDERER_TORRENT, NULL); +} + +static void +finalize(GObject *object) { + GtkCellRendererTorrent *tcell = GTK_CELL_RENDERER_TORRENT(object); + + g_free(tcell->priv->text); + g_free(tcell->priv->label); + + if(NULL != tcell->priv->text_attrs) + pango_attr_list_unref(tcell->priv->text_attrs); + if(NULL != tcell->priv->label_attrs) + pango_attr_list_unref(tcell->priv->label_attrs); + if(NULL != tcell->priv->style) { + gtk_style_detach(tcell->priv->style); + gtk_style_unref(tcell->priv->style); + } + + G_OBJECT_CLASS (gtk_cell_renderer_torrent_parent_class)->finalize(object); +} + +static void +get_property(GObject *object, guint id, GValue *value, GParamSpec *pspec) { + GtkCellRendererTorrent *tcell = GTK_CELL_RENDERER_TORRENT (object); + + switch (id) { + case PROP_VALUE: + g_value_set_float (value, tcell->priv->value); + break; + case PROP_TEXT: + g_value_set_string (value, tcell->priv->text); + break; + case PROP_LABEL: + g_value_set_string (value, tcell->priv->label); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec); + } +} + +static void +set_property(GObject *obj, guint id, const GValue *value, GParamSpec *spec) { + GtkCellRendererTorrent *tcell = GTK_CELL_RENDERER_TORRENT(obj); + gchar **prop = NULL; + PangoAttrList **attrs = NULL; + /*GError *err = NULL;*/ + const gchar *markup; + + switch(id) { + case PROP_VALUE: + tcell->priv->value = g_value_get_float(value); + break; + + case PROP_TEXT: + prop = &tcell->priv->text; + attrs = &tcell->priv->text_attrs; + /* fallthrough */ + + case PROP_LABEL: + if(PROP_LABEL == id) { + prop = &tcell->priv->label; + attrs = &tcell->priv->label_attrs; + } + + if(NULL == (markup = g_value_get_string(value))) + markup = ""; + + g_free(*prop); + if(NULL != *attrs) + pango_attr_list_unref(*attrs); + + *prop = g_strdup(markup); + + /* + if(pango_parse_markup(markup, -1, 0, attrs, prop, NULL, &err)) + break; + + g_warning ("Failed to parse markup: %s", err->message); + g_error_free(err); + */ + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, spec); + } +} + +static void +get_size(GtkCellRenderer *cell, GtkWidget *widget, GdkRectangle *area, + gint *xoff, gint *yoff, gint *width, gint *height) { + GtkCellRendererTorrent *tcell = GTK_CELL_RENDERER_TORRENT(cell); + /* XXX do I have to unref the context? */ + PangoLayout *layout = pango_layout_new(gtk_widget_get_pango_context(widget)); + PangoRectangle rect; + gint h = cell->ypad * 2; + gint w1, w2; + + pango_layout_set_markup(layout, tcell->priv->label, -1); + pango_layout_get_pixel_extents(layout, NULL, &rect); + w1 = rect.width; + h += rect.height; + + pango_layout_set_markup(layout, tcell->priv->text, -1); + pango_layout_get_pixel_extents(layout, NULL, &rect); + w2 = rect.width; + h += rect.height; + + if(NULL != xoff) + *xoff = 0; + if(NULL != yoff) + *yoff = (area->height - h) / 2; + if(NULL != width) + *width = MAX(w1, w2) + cell->xpad * 2; + if(NULL != height) + *height = h; + + g_object_unref(layout); +} + +#define RECTARGS(rect) (rect).x, (rect).y, (rect).width, (rect).height + +static void +render(GtkCellRenderer *cell, GdkWindow *window, GtkWidget *widget, + GdkRectangle *bg SHUTUP, GdkRectangle *area, GdkRectangle *exp SHUTUP, + guint flags) { + GtkCellRendererTorrent *tcell = GTK_CELL_RENDERER_TORRENT(cell); + PangoContext *ctx = gtk_widget_get_pango_context(widget); + PangoLayout *llayout, *tlayout; + PangoRectangle lrect, trect; + GdkRectangle bar, complete, text; + gboolean rtl; + GtkStyle *style; + + /* try to use the style for GtkProgressBar */ + if(NULL == tcell->priv->style) + if(NULL != (tcell->priv->style = gtk_rc_get_style_by_paths( + gtk_widget_get_settings(widget), NULL, NULL, + gtk_progress_bar_get_type()))) + tcell->priv->style = gtk_style_attach(gtk_style_ref(tcell->priv->style), + window); + style = (NULL == tcell->priv->style ? widget->style : tcell->priv->style); + + rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL; + + /* get the text layouts */ + llayout = pango_layout_new(ctx); + /* XXX cache parsed markup? */ + pango_layout_set_markup(llayout, tcell->priv->label, -1); + pango_layout_get_pixel_extents(llayout, NULL, &lrect); + tlayout = pango_layout_new(ctx); + pango_layout_set_markup(tlayout, tcell->priv->text, -1); + pango_layout_get_pixel_extents (tlayout, NULL, &trect); + + /* set up the dimensions for the bar */ + bar.x = area->x + cell->xpad; + bar.y = area->y + cell->ypad + + (area->height - lrect.height - trect.height) / 2; + bar.width = area->width - cell->xpad * 2; + bar.height = lrect.height; + + /* set up the dimensions for the complete portion of the bar */ + complete.x = bar.x + style->xthickness; + complete.y = bar.y + style->ythickness; + complete.width = (bar.width - style->xthickness * 2) * tcell->priv->value; + complete.height = bar.height - style->ythickness * 2; + if(rtl) + complete.x += bar.width - complete.width; + + /* set up the dimensions for the text under the bar */ + text.x = bar.x; + text.y = bar.y + bar.height; + text.width = bar.width; + text.height = area->height - bar.height; + + /* draw the background of the bar */ + if(complete.width < bar.width) + gtk_paint_box(style, window, GTK_STATE_NORMAL, GTK_SHADOW_IN, + NULL, widget, "trough", RECTARGS(bar)); + + /* draw the complete portion of the bar */ + if(0 < complete.width) + gtk_paint_box(style, window, GTK_STATE_PRELIGHT, GTK_SHADOW_OUT, + NULL, widget, "bar", RECTARGS(complete)); + + /* draw the text under the bar */ + gtk_paint_layout(style, window, (GTK_CELL_RENDERER_SELECTED & flags ? + GTK_STATE_SELECTED : GTK_STATE_NORMAL), FALSE, &text, + widget, "cellrenderertext", text.x, text.y, tlayout); + + /* free memory */ + g_object_unref(llayout); + g_object_unref(tlayout); +} diff --git a/gtk/gtkcellrenderertorrent.h b/gtk/gtkcellrenderertorrent.h new file mode 100644 index 000000000..6da79a855 --- /dev/null +++ b/gtk/gtkcellrenderertorrent.h @@ -0,0 +1,69 @@ +/* gtkcellrenderertorrent.h + * Copyright (C) 2002 Naba Kumar + * modified by Jörgen Scheibengruber + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +/* + * Modified by the GTK+ Team and others 1997-2004. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#ifndef __GTK_CELL_RENDERER_TORRENT_H__ +#define __GTK_CELL_RENDERER_TORRENT_H__ + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_CELL_RENDERER_TORRENT (gtk_cell_renderer_torrent_get_type ()) +#define GTK_CELL_RENDERER_TORRENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CELL_RENDERER_TORRENT, GtkCellRendererTorrent)) +#define GTK_CELL_RENDERER_TORRENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_CELL_RENDERER_TORRENT, GtkCellRendererTorrentClass)) +#define GTK_IS_CELL_RENDERER_TORRENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_CELL_RENDERER_TORRENT)) +#define GTK_IS_CELL_RENDERER_TORRENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_CELL_RENDERER_TORRENT)) +#define GTK_CELL_RENDERER_TORRENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_CELL_RENDERER_TORRENT, GtkCellRendererTorrentClass)) + +typedef struct _GtkCellRendererTorrent GtkCellRendererTorrent; +typedef struct _GtkCellRendererTorrentClass GtkCellRendererTorrentClass; +typedef struct _GtkCellRendererTorrentPrivate GtkCellRendererTorrentPrivate; + +struct _GtkCellRendererTorrent +{ + GtkCellRenderer parent_instance; + + /*< private >*/ + GtkCellRendererTorrentPrivate *priv; +}; + +struct _GtkCellRendererTorrentClass +{ + GtkCellRendererClass parent_class; + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); +}; + +GType gtk_cell_renderer_torrent_get_type (void) G_GNUC_CONST; +GtkCellRenderer* gtk_cell_renderer_torrent_new (void); + +G_END_DECLS + +#endif /* __GTK_CELL_RENDERER_TORRENT_H__ */ diff --git a/gtk/main.c b/gtk/main.c index f18dc225f..a9fc2e517 100644 --- a/gtk/main.c +++ b/gtk/main.c @@ -36,17 +36,20 @@ #include #include "conf.h" +#include "gtkcellrenderertorrent.h" #include "prefs.h" #include "transmission.h" #include "util.h" -#define TRACKER_EXIT_TIMEOUT 30 +#define TRACKER_EXIT_TIMEOUT 5 struct cbdata { tr_handle_t *tr; GtkWindow *wind; GtkListStore *model; GtkTreeView *view; + GtkStatusbar *bar; + GtkWidget **buttons; guint timer; }; @@ -77,15 +80,23 @@ GtkWidget * makewind_toolbar(struct cbdata *data); GtkWidget * makewind_list(struct cbdata *data); +void +fixbuttons(GtkTreeSelection *sel, gpointer gdata); +void +dfname(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, + GtkTreeIter *iter, gpointer gdata); +void +dfprog(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, + GtkTreeIter *iter, gpointer gdata); gboolean updatemodel(gpointer gdata); gboolean listclick(GtkWidget *widget, GdkEventButton *event, gpointer gdata); gboolean -listpopup(GtkWidget *widget, gpointer userdata); +listpopup(GtkWidget *widget, gpointer gdata); void -dopopupmenu(GtkWidget *widget, GdkEventButton *event, struct cbdata *data); +dopopupmenu(GdkEventButton *event, struct cbdata *data, GtkTreeIter *iter); void actionclick(GtkWidget *widget, gpointer gdata); @@ -115,20 +126,22 @@ enum listfrom { FROM_BUTTON, FROM_POPUP }; #define LIST_INDEX "torrent-list-index" -struct { gint pos; const gchar *name; const gchar *id; enum listact act; - const char *ttext; const char *tpriv; } +struct { const gchar *name; const gchar *id; enum listact act; gboolean nomenu; + int avail; const char *ttext; const char *tpriv; } actionitems[] = { - {0, "Add", GTK_STOCK_ADD, ACT_OPEN, + {"Add", GTK_STOCK_ADD, ACT_OPEN, FALSE, 0, "Add a new torrent file", "XXX"}, - {1, "Resume", GTK_STOCK_MEDIA_PLAY, ACT_START, + {"Resume", GTK_STOCK_MEDIA_PLAY, ACT_START, FALSE, + (TR_STATUS_STOPPING | TR_STATUS_PAUSE), "Resume a torrent that has been paused", "XXX"}, - {2, "Pause", GTK_STOCK_MEDIA_PAUSE, ACT_STOP, + {"Pause", GTK_STOCK_MEDIA_PAUSE, ACT_STOP, FALSE, + ~(TR_STATUS_STOPPING | TR_STATUS_PAUSE), "Pause a torrent", "XXX"}, - {3, "Remove", GTK_STOCK_REMOVE, ACT_DELETE, + {"Remove", GTK_STOCK_REMOVE, ACT_DELETE, FALSE, ~0, "Remove a torrent from the list", "XXX"}, - {4, "Properties", GTK_STOCK_PROPERTIES, ACT_INFO, + {"Properties", GTK_STOCK_PROPERTIES, ACT_INFO, FALSE, ~0, "Get additional information for a torrent", "XXX"}, - {5, "Preferences", GTK_STOCK_PREFERENCES, ACT_PREF, + {"Preferences", GTK_STOCK_PREFERENCES, ACT_PREF, TRUE, 0, "Open preferences dialog", "XXX"}, }; @@ -214,6 +227,7 @@ void makewind(GtkWidget *wind, tr_handle_t *tr, GList *saved) { GtkWidget *vbox = gtk_vbox_new(FALSE, 0); GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + GtkWidget *status = gtk_statusbar_new(); struct cbdata *data = g_new0(struct cbdata, 1); GtkWidget *list; GtkRequisition req; @@ -226,13 +240,18 @@ makewind(GtkWidget *wind, tr_handle_t *tr, GList *saved) { /* filled in by makewind_list */ data->model = NULL; data->view = NULL; + data->bar = GTK_STATUSBAR(status); + data->buttons = NULL; gtk_box_pack_start(GTK_BOX(vbox), makewind_toolbar(data), FALSE, FALSE, 0); list = makewind_list(data); gtk_widget_size_request(list, &req); gtk_container_add(GTK_CONTAINER(scroll), list); - gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 5); + gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0); + + gtk_statusbar_push(GTK_STATUSBAR(status), 0, ""); + gtk_box_pack_start(GTK_BOX(vbox), status, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(wind), vbox); gtk_window_set_default_size(GTK_WINDOW(wind), req.width, req.height); @@ -344,8 +363,11 @@ makewind_toolbar(struct cbdata *data) { gtk_toolbar_set_tooltips(GTK_TOOLBAR(bar), TRUE); gtk_toolbar_set_style(GTK_TOOLBAR(bar), GTK_TOOLBAR_BOTH); + data->buttons = g_new(GtkWidget*, ALEN(actionitems)); + for(ii = 0; ii < ALEN(actionitems); ii++) { item = gtk_tool_button_new_from_stock(actionitems[ii].id); + data->buttons[ii] = GTK_WIDGET(item); gtk_tool_button_set_label(GTK_TOOL_BUTTON(item), actionitems[ii].name); gtk_tool_item_set_tooltip(GTK_TOOL_ITEM(item), GTK_TOOLBAR(bar)->tooltips, actionitems[ii].ttext, actionitems[ii].tpriv); @@ -354,12 +376,13 @@ makewind_toolbar(struct cbdata *data) { g_object_set_data(G_OBJECT(item), LIST_ACTION_FROM, GINT_TO_POINTER(FROM_BUTTON)); g_signal_connect(G_OBJECT(item), "clicked", G_CALLBACK(actionclick), data); - gtk_toolbar_insert(GTK_TOOLBAR(bar), GTK_TOOL_ITEM(item), actionitems[ii].pos); + gtk_toolbar_insert(GTK_TOOLBAR(bar), item, -1); } return bar; } +/* XXX check for unused data in model */ enum {MC_NAME, MC_SIZE, MC_STAT, MC_ERR, MC_PROG, MC_DRATE, MC_URATE, MC_ETA, MC_PEERS, MC_UPEERS, MC_DPEERS, MC_PIECES, MC_DOWN, MC_UP, MC_ROW_INDEX, MC_ROW_COUNT}; @@ -368,7 +391,7 @@ GtkWidget * makewind_list(struct cbdata *data) { GType types[] = { /* info->name, info->totalSize, status, error, progress */ - G_TYPE_STRING, G_TYPE_UINT64, G_TYPE_INT, G_TYPE_STRING, G_TYPE_INT, + G_TYPE_STRING, G_TYPE_UINT64, G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, /* rateDownload, rateUpload, eta, peersTotal, peersUploading */ G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, /* peersDownloading, pieces, downloaded, uploaded */ @@ -377,10 +400,9 @@ makewind_list(struct cbdata *data) { G_TYPE_INT}; GtkListStore *model; GtkWidget *view; - /*GtkTreeViewColumn *col;*/ + GtkTreeViewColumn *col; GtkTreeSelection *sel; - GtkCellRenderer *rend; - GtkCellRenderer *rendprog; + GtkCellRenderer *namerend, *progrend; assert(MC_ROW_COUNT == ALEN(types)); @@ -391,53 +413,22 @@ makewind_list(struct cbdata *data) { data->model = model; data->view = GTK_TREE_VIEW(view); - rend = gtk_cell_renderer_text_new(); - rendprog = gtk_cell_renderer_progress_new(); - g_object_set(rendprog, "text", "", NULL); + namerend = gtk_cell_renderer_text_new(); + col = gtk_tree_view_column_new_with_attributes("Name", namerend, NULL); + gtk_tree_view_column_set_cell_data_func(col, namerend, dfname, NULL, NULL); + gtk_tree_view_column_set_expand(col, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Name", rend, - "text", MC_NAME, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Size", rend, - "text", MC_SIZE, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Status", rend, - "text", MC_STAT, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Error", rend, - "text", MC_ERR, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Progress", rendprog, - "value", MC_PROG, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Download Rate", rend, - "text", MC_DRATE, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Upload Rate", rend, - "text", MC_URATE, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("ETA", rend, - "text", MC_ETA, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Peers", rend, - "text", MC_PEERS, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Seeders", rend, - "text", MC_UPEERS, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Leechers", rend, - "text", MC_DPEERS, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Downloaded", rend, - "text", MC_DOWN, NULL)); - gtk_tree_view_append_column(GTK_TREE_VIEW(view), - gtk_tree_view_column_new_with_attributes("Uploaded", rend, - "text", MC_UP, NULL)); + progrend = gtk_cell_renderer_torrent_new(); + g_object_set(progrend, "label", " fnord fnord ", NULL); + col = gtk_tree_view_column_new_with_attributes("Progress", progrend, NULL); + gtk_tree_view_column_set_cell_data_func(col, progrend, dfprog, NULL, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); gtk_tree_selection_set_mode(GTK_TREE_SELECTION(sel), GTK_SELECTION_SINGLE); + g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(fixbuttons), data); g_signal_connect(G_OBJECT(view), "button-press-event", G_CALLBACK(listclick), data); g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(listpopup), data); @@ -446,27 +437,133 @@ makewind_list(struct cbdata *data) { return view; } +/* disable buttons the user shouldn't be able to click on */ +void +fixbuttons(GtkTreeSelection *sel, gpointer gdata) { + struct cbdata *data = gdata; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean selected; + unsigned int ii; + int status; + + if(NULL == sel) + sel = gtk_tree_view_get_selection(data->view); + if((selected = gtk_tree_selection_get_selected(sel, &model, &iter))) + gtk_tree_model_get(model, &iter, MC_STAT, &status, -1); + + for(ii = 0; ii < ALEN(actionitems); ii++) + if(actionitems[ii].avail) + gtk_widget_set_sensitive(data->buttons[ii], + (selected && (actionitems[ii].avail & status))); +} + +void +dfname(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend, + GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) { + char *name, *mb, *err, *str, *top, *bottom; + guint64 size; + gfloat prog; + int status, eta, tpeers, upeers, dpeers; + + /* XXX should I worry about gtk_tree_model_get failing? */ + gtk_tree_model_get(model, iter, MC_NAME, &name, MC_STAT, &status, + MC_SIZE, &size, MC_PROG, &prog, MC_ETA, &eta, MC_PEERS, &tpeers, + MC_UPEERS, &upeers, MC_DPEERS, &dpeers, -1); + + if(0 > eta) + eta = 0; + if(0 > tpeers) + tpeers = 0; + if(0 > upeers) + upeers = 0; + if(0 > dpeers) + dpeers = 0; + mb = readablesize(size, 1); + prog *= 100; + + if(status & TR_STATUS_CHECK) + top = g_strdup_printf("Checking existing files (%.1f%%)", prog); + else if(status & TR_STATUS_DOWNLOAD) + top = g_strdup_printf("Finishing in %02i:%02i:%02i (%.1f%%)", + eta / 60 / 60, eta / 60 % 60, eta % 60, prog); + else if(status & TR_STATUS_SEED) + top = g_strdup_printf("Seeding, uploading to %d of %d peer%s", + dpeers, tpeers, (1 == tpeers ? "" : "s")); + else if(status & TR_STATUS_STOPPING) + top = g_strdup("Stopping..."); + else if(status & TR_STATUS_PAUSE) + top = g_strdup_printf("Paused (%.1f%%)", prog); + else { + top = g_strdup(""); + assert("XXX unknown status"); + } + + if(status & TR_TRACKER_ERROR) { + gtk_tree_model_get(model, iter, MC_ERR, &err, -1); + bottom = g_strconcat("Error: ", err, NULL); + g_free(err); + } + else if(status & TR_STATUS_DOWNLOAD) + bottom = g_strdup_printf("Downloading from %i of %i peer%s", + upeers, tpeers, (1 == tpeers ? "" : "s")); + else + bottom = NULL; + + str = g_markup_printf_escaped("%s (%s)\n%s\n%s", + name, mb, top, (NULL == bottom ? "" : bottom)); + g_object_set(rend, "markup", str, NULL); + g_free(name); + g_free(mb); + g_free(str); + g_free(top); + g_free(bottom); +} + +void +dfprog(GtkTreeViewColumn *col SHUTUP, GtkCellRenderer *rend, + GtkTreeModel *model, GtkTreeIter *iter, gpointer gdata SHUTUP) { + char *dlstr, *ulstr, *str; + gfloat prog, dl, ul; + + /* XXX should I worry about gtk_tree_model_get failing? */ + gtk_tree_model_get(model, iter, MC_PROG, &prog, + MC_DRATE, &dl, MC_URATE, &ul, -1); + if(0.0 > prog) + prog = 0.0; + else if(1.0 < prog) + prog = 1.0; + + dlstr = readablesize(dl * 1024.0, 2); + ulstr = readablesize(ul * 1024.0, 2); + str = g_strdup_printf("DL: %s/s\nUL: %s/s", dlstr, ulstr); + g_object_set(rend, "text", str, "value", prog, NULL); + g_free(dlstr); + g_free(ulstr); + g_free(str); +} + gboolean updatemodel(gpointer gdata) { struct cbdata *data = gdata; tr_stat_t *st; - int ii, max, prog; + int ii, max; GtkTreeIter iter; + float up, down; + char *upstr, *downstr, *str; max = tr_torrentStat(data->tr, &st); for(ii = 0; ii < max; ii++) { if(!(ii ? gtk_tree_model_iter_next(GTK_TREE_MODEL(data->model), &iter) : gtk_tree_model_get_iter_first(GTK_TREE_MODEL(data->model), &iter))) gtk_list_store_append(data->model, &iter); - if(0.0 > (prog = st[ii].progress * 100.0)) - prog = 0; - else if(100 < prog) - prog = 100; /* XXX find out if setting the same data emits changed signal */ - gtk_list_store_set(data->model, &iter, MC_ROW_INDEX, ii, - MC_NAME, st[ii].info.name, MC_SIZE, st[ii].info.totalSize, MC_STAT, st[ii].status, - MC_ERR, st[ii].error, MC_PROG, prog, MC_DRATE, st[ii].rateDownload, - MC_URATE, st[ii].rateUpload, MC_ETA, st[ii].eta, MC_PEERS, st[ii].peersTotal, + gtk_list_store_set( + data->model, &iter, MC_ROW_INDEX, ii, + MC_NAME, st[ii].info.name, MC_SIZE, st[ii].info.totalSize, + MC_STAT, st[ii].status, MC_ERR, st[ii].error, MC_PROG, st[ii].progress, + MC_DRATE, st[ii].rateDownload, MC_URATE, st[ii].rateUpload, + MC_ETA, st[ii].eta, MC_PEERS, st[ii].peersTotal, MC_UPEERS, st[ii].peersUploading, MC_DPEERS, st[ii].peersDownloading, MC_DOWN, st[ii].downloaded, MC_UP, st[ii].uploaded, -1); } @@ -477,15 +574,38 @@ updatemodel(gpointer gdata) { while(gtk_list_store_remove(data->model, &iter)) ; + /* update the status bar */ + tr_torrentRates(data->tr, &up, &down); + downstr = readablesize(down * 1024.0, 2); + upstr = readablesize(up * 1024.0, 2); + str = g_strdup_printf(" Total DL: %s/s Total UL: %s/s", upstr, downstr); + gtk_statusbar_pop(data->bar, 0); + gtk_statusbar_push(data->bar, 0, str); + g_free(str); + g_free(upstr); + g_free(downstr); + + /* the status of the selected item may have changed, so update the buttons */ + fixbuttons(NULL, data); + return TRUE; } gboolean -listclick(GtkWidget *widget, GdkEventButton *event, gpointer gdata) { +listclick(GtkWidget *widget SHUTUP, GdkEventButton *event, gpointer gdata) { struct cbdata *data = gdata; + GtkTreePath *path; + GtkTreeIter iter; if(GDK_BUTTON_PRESS == event->type && 3 == event->button) { - dopopupmenu(widget, event, data); + if(!gtk_tree_view_get_path_at_pos(data->view, event->x, event->y, &path, + NULL, NULL, NULL)) + dopopupmenu(event, data, NULL); + else { + if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model), &iter, path)) + dopopupmenu(event, data, &iter); + gtk_tree_path_free(path); + } return TRUE; } @@ -493,33 +613,40 @@ listclick(GtkWidget *widget, GdkEventButton *event, gpointer gdata) { } gboolean -listpopup(GtkWidget *widget, gpointer userdata) { - dopopupmenu(widget, NULL, userdata); +listpopup(GtkWidget *widget SHUTUP, gpointer gdata) { + struct cbdata *data = gdata; + GtkTreeSelection *sel = gtk_tree_view_get_selection(data->view); + GtkTreeModel *model; + GtkTreeIter iter; + + if(!gtk_tree_selection_get_selected(sel, &model, &iter)) + dopopupmenu(NULL, data, NULL); + else { + assert(model == GTK_TREE_MODEL(data->model)); + dopopupmenu(NULL, data, &iter); + } return TRUE; } void -dopopupmenu(GtkWidget *widget SHUTUP, GdkEventButton *event, - struct cbdata *data) { +dopopupmenu(GdkEventButton *event, struct cbdata *data, GtkTreeIter *iter) { GtkWidget *menu = gtk_menu_new(); GtkWidget *item; - GtkTreePath *path; - GtkTreeIter iter; unsigned int ii; - int index; + int index, status; index = -1; - if(NULL != event && gtk_tree_view_get_path_at_pos( - data->view, event->x, event->y, &path, NULL, NULL, NULL)) { - if(gtk_tree_model_get_iter(GTK_TREE_MODEL(data->model), &iter, path)) - gtk_tree_model_get(GTK_TREE_MODEL(data->model), &iter, MC_ROW_INDEX, &index, -1); - gtk_tree_path_free(path); - } + if(NULL != iter) + gtk_tree_model_get(GTK_TREE_MODEL(data->model), iter, + MC_ROW_INDEX, &index, MC_STAT, &status, -1); /* XXX am I leaking references here? */ - /* XXX can I cache this in cbdata? */ for(ii = 0; ii < ALEN(actionitems); ii++) { + if(actionitems[ii].nomenu || + (actionitems[ii].avail && + (0 > index || !(actionitems[ii].avail & status)))) + continue; item = gtk_menu_item_new_with_label(actionitems[ii].name); g_object_set_data(G_OBJECT(item), LIST_ACTION, GINT_TO_POINTER(actionitems[ii].act)); @@ -583,10 +710,12 @@ actionclick(GtkWidget *widget, gpointer gdata) { case ACT_START: tr_torrentStart(data->tr, index); savetorrents(data->tr, data->wind, -1, NULL); + updatemodel(data); break; case ACT_STOP: tr_torrentStop(data->tr, index); savetorrents(data->tr, data->wind, -1, NULL); + updatemodel(data); break; case ACT_DELETE: /* XXX need to be able to stat just one torrent */ diff --git a/gtk/util.c b/gtk/util.c index 7c86e7587..224868c7c 100644 --- a/gtk/util.c +++ b/gtk/util.c @@ -55,6 +55,23 @@ strbool(const char *str) { return FALSE; } +char * +readablesize(guint64 size, int decimals) { + const char *sizes[] = {"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}; + unsigned int ii; + double small = size; + + for(ii = 0; ii + 1 < ALEN(sizes) && 1024.0 <= small / 1024.0; ii++) + small /= 1024.0; + + if(1024.0 <= small) { + small /= 1024.0; + ii++; + } + + return g_strdup_printf("%.*f %s", decimals, small, sizes[ii]); +} + gboolean mkdir_p(const char *name, mode_t mode) { struct stat sb; diff --git a/gtk/util.h b/gtk/util.h index e52806218..46353e34b 100644 --- a/gtk/util.h +++ b/gtk/util.h @@ -43,6 +43,9 @@ gboolean strbool(const char *str); +char * +readablesize(guint64 size, int decimals); + gboolean mkdir_p(const char *name, mode_t mode); diff --git a/macosx/Controller.m b/macosx/Controller.m index ab3ff34e7..c9ae3eded 100644 --- a/macosx/Controller.m +++ b/macosx/Controller.m @@ -850,11 +850,37 @@ static void sleepCallBack( void * controller, io_service_t y, URLWithString:@"http://transmission.m0k.org/forum/"]]; } +- (BOOL) hasGrowl +{ + NSFileManager * manager = [NSFileManager defaultManager]; + NSString * helper = @"/Library/PreferencePanes/Growl.prefPane/" + "Contents/Resources/GrowlHelperApp.app"; + + if( [manager fileExistsAtPath: helper] ) + { + /* Growl was installed for all users */ + return YES; + } + if( [manager fileExistsAtPath: [[NSString stringWithFormat: @"~%@", + helper] stringByExpandingTildeInPath]] ) + { + /* Growl was installed for this user only */ + return YES; + } + + return NO; +} + - (void) notifyGrowl: (NSString * ) file { NSString * growlScript; NSAppleScript * appleScript; NSDictionary * error; + + if( ![self hasGrowl] ) + { + return; + } growlScript = [NSString stringWithFormat: @"tell application \"System Events\"\n" @@ -880,6 +906,11 @@ static void sleepCallBack( void * controller, io_service_t y, NSString * growlScript; NSAppleScript * appleScript; NSDictionary * error; + + if( ![self hasGrowl] ) + { + return; + } growlScript = [NSString stringWithFormat: @"tell application \"System Events\"\n"