/****************************************************************************** * $Id:$ * * Copyright (c) 2005-2007 Transmission authors and contributors * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *****************************************************************************/ #include #include #include #include #include "transmission.h" #include "platform.h" /* for tr_getTorrentsDirectory */ #include "actions.h" #include "tr_torrent.h" #include "dot-icons.h" #include "hig.h" #include "torrent-inspector.h" #include "util.h" #define UPDATE_INTERVAL_MSEC 1500 /**** ***** PIECES VIEW ****/ static int getGridSize (int pieceCount, int * n_rows, int * n_cols) { const int MAX_ACROSS = 16; if (pieceCount >= (MAX_ACROSS * MAX_ACROSS)) { *n_rows = *n_cols = MAX_ACROSS; return MAX_ACROSS * MAX_ACROSS; } else { int i; for (i=0; (i*i) < pieceCount; ++i); *n_rows = *n_cols = i; return pieceCount; } } #define TO16(a) ((guint16)((a<<8)|(a))) #define RGB_2_GDK(R,G,B) { 0, TO16(R), TO16(G), TO16(B) } enum { DRAW_AVAIL, DRAW_PROG }; static void release_gobject_array (gpointer data) { int i; GObject **objects = (GObject**) data; for (i=0; objects[i]!=NULL; ++i) g_object_unref (G_OBJECT(objects[i])); g_free (objects); } static gboolean refresh_pieces (GtkWidget * da, GdkEventExpose * event UNUSED, gpointer gtor) { tr_torrent_t * tor = tr_torrent_handle( TR_TORRENT(gtor) ); const tr_info_t * info = tr_torrent_info( TR_TORRENT(gtor) ); int mode = GPOINTER_TO_INT (g_object_get_data (G_OBJECT(da), "draw-mode")); GdkColormap * colormap = gtk_widget_get_colormap (da); const int widget_w = da->allocation.width; const int widget_h = da->allocation.height; int n_rows, n_cols; const int n_cells = getGridSize (info->pieceCount, &n_rows, &n_cols); const GdkRectangle grid_bounds = { 0, 0, widget_w, widget_h }; const double piece_w = grid_bounds.width / (double)n_cols; const double piece_h = grid_bounds.height / (double)n_rows; const int piece_w_int = (int) (piece_w + 1); /* pad for roundoff */ const int piece_h_int = (int) (piece_h + 1); /* pad for roundoff */ guint8 * prev_color = NULL; gboolean first_time = FALSE; int i, x, y; int8_t * pieces = NULL; float * completeness = NULL; /** *** Get the Graphics Contexts... **/ enum { ALL, LOTS, SOME, FEW, NONE, BLACK, GRAY, BLINK, N_COLORS }; GdkGC **gcs = (GdkGC**) g_object_get_data (G_OBJECT(da), "graphics-contexts"); if (gcs == NULL) { gcs = g_new (GdkGC*, N_COLORS+1); const GdkColor colors [N_COLORS] = { RGB_2_GDK ( 0, 226, 255 ), /* all */ RGB_2_GDK ( 0, 153, 204 ), /* lots */ RGB_2_GDK ( 0, 102, 153 ), /* some */ RGB_2_GDK ( 0, 51, 102 ), /* few */ RGB_2_GDK ( 255, 255, 255 ), /* none */ RGB_2_GDK ( 0, 0, 0 ), /* black */ RGB_2_GDK ( 181, 181, 181 ), /* gray */ RGB_2_GDK ( 255, 164, 0 ), /* blink - orange */ }; for (i=0; iwindow); gdk_gc_set_colormap (gcs[i], colormap); gdk_gc_set_rgb_fg_color (gcs[i], &colors[i]); gdk_gc_set_rgb_bg_color (gcs[i], &colors[i]); }; gcs[N_COLORS] = NULL; /* a sentinel in the release function */ g_object_set_data_full (G_OBJECT(da), "graphics-contexts", gcs, release_gobject_array); } /** *** Get the cells' previous colors... *** (this is used for blinking when the color changes) **/ prev_color = (guint8*) g_object_get_data (G_OBJECT(da), "prev-color"); if (prev_color == NULL) { first_time = TRUE; prev_color = g_new0 (guint8, n_cells); g_object_set_data_full (G_OBJECT(da), "prev-color", prev_color, g_free); } /** *** Get the piece data values... **/ switch (mode) { case DRAW_AVAIL: pieces = g_new (gint8, n_cells); tr_torrentAvailability ( tor, pieces, n_cells ); break; case DRAW_PROG: completeness = g_new (float, n_cells); tr_torrentAmountFinished ( tor, completeness, n_cells ); break; default: g_error("no mode defined!"); } /** *** Draw... **/ i = 0; for (y=0; y= 1.00) color = ALL; else if (val >= 0.66) color = LOTS; else if (val >= 0.33) color = SOME; else if (val >= 0.01) color = FEW; else color = NONE; } /* draw a "blink" for one interval when a piece changes */ if (first_time) prev_color[i] = color; else if (color != prev_color[i]) { prev_color[i] = color; color = border = BLINK; } } gdk_draw_rectangle (da->window, gcs[color], TRUE, draw_x, draw_y, piece_w_int, piece_h_int); if (i < n_cells) gdk_draw_rectangle (da->window, gcs[border], FALSE, draw_x, draw_y, piece_w_int, piece_h_int); ++i; } } gdk_draw_rectangle (da->window, gcs[GRAY], FALSE, grid_bounds.x, grid_bounds.y, grid_bounds.width-1, grid_bounds.height-1); g_free (pieces); g_free (completeness); return FALSE; } /**** ***** PEERS TAB ****/ enum { PEER_COL_ADDRESS, PEER_COL_PORT, PEER_COL_CLIENT, PEER_COL_PROGRESS, PEER_COL_IS_CONNECTED, PEER_COL_IS_DOWNLOADING, PEER_COL_DOWNLOAD_RATE, PEER_COL_IS_UPLOADING, PEER_COL_UPLOAD_RATE, N_PEER_COLS }; static const char* peer_column_names[N_PEER_COLS] = { N_("Address"), N_("Port"), N_("Client"), N_("Progress"), "", N_("Downloading"), N_("DL Rate"), N_("Uploading"), N_("UL Rate") }; static int compare_peers (const void * a, const void * b) { const tr_peer_stat_t * pa = (const tr_peer_stat_t *) a; const tr_peer_stat_t * pb = (const tr_peer_stat_t *) b; return strcmp (pa->addr, pb->addr); } static int compare_addr_to_peer (const void * a, const void * b) { const char * addr = (const char *) a; const tr_peer_stat_t * peer = (const tr_peer_stat_t *) b; return strcmp (addr, peer->addr); } static void peer_row_set (GtkTreeStore * store, GtkTreeIter * iter, const tr_peer_stat_t * peer) { const char * client = peer->client; if (!client || !strcmp(client,"Unknown Client")) client = ""; gtk_tree_store_set (store, iter, PEER_COL_ADDRESS, peer->addr, PEER_COL_PORT, peer->port, PEER_COL_CLIENT, client, PEER_COL_PROGRESS, (int)(100.0*peer->progress + 0.5), PEER_COL_IS_CONNECTED, peer->isConnected, PEER_COL_IS_DOWNLOADING, peer->isDownloading, PEER_COL_DOWNLOAD_RATE, peer->downloadFromRate, PEER_COL_IS_UPLOADING, peer->isUploading, PEER_COL_UPLOAD_RATE, peer->uploadToRate, -1); } static void append_peers_to_model (GtkTreeStore * store, const tr_peer_stat_t * peers, int n_peers) { int i; for (i=0; i count ) { gtk_label_set_text( GTK_LABEL(l), _("?") ); } else { char str[16]; snprintf( str, sizeof str, "%i", count ); gtk_label_set_text( GTK_LABEL(l), str ); } } static void refresh_peers (GtkWidget * top) { int i; int n_peers; GtkTreeIter iter; PeerData * p = (PeerData*) g_object_get_data (G_OBJECT(top), "peer-data"); tr_torrent_t * tor = tr_torrent_handle ( p->gtor ); GtkTreeModel * model = p->model; GtkTreeStore * store = p->store; tr_peer_stat_t * peers; const tr_stat_t * stat = tr_torrent_stat( p->gtor ); /** *** merge the peer diffs into the tree model. *** *** this is more complicated than creating a new model, *** but is also (a) more efficient and (b) doesn't undo *** the view's visible area and sorting on every refresh. **/ n_peers = 0; peers = tr_torrentPeers (tor, &n_peers); qsort (peers, n_peers, sizeof(tr_peer_stat_t), compare_peers); i = 0; if (gtk_tree_model_get_iter_first (model, &iter)) do { char * addr = NULL; tr_peer_stat_t * peer = NULL; gtk_tree_model_get (model, &iter, PEER_COL_ADDRESS, &addr, -1); peer = bsearch (addr, peers, n_peers, sizeof(tr_peer_stat_t), compare_addr_to_peer); g_free (addr); if (peer) /* update a pre-existing row */ { const int pos = peer - peers; const int n_rhs = n_peers - (pos+1); g_assert (n_rhs >= 0); peer_row_set (store, &iter, peer); /* remove it from the tr_peer_stat_t list */ g_memmove (peer, peer+1, sizeof(tr_peer_stat_t)*n_rhs); --n_peers; } else if (!gtk_tree_store_remove (store, &iter)) break; /* we removed the model's last item */ } while (gtk_tree_model_iter_next (model, &iter)); append_peers_to_model (store, peers, n_peers); /* all these are new */ if (GDK_IS_DRAWABLE (p->completeness->window)) refresh_pieces (p->completeness, NULL, p->gtor); fmtpeercount (p->seeders_lb, stat->seeders); fmtpeercount (p->leechers_lb, stat->leechers); fmtpeercount (p->completed_lb, stat->completedFromTracker ); free (peers); } static GtkWidget* peer_page_new ( TrTorrent * gtor ) { guint i; GtkTreeModel *m; GtkWidget *h, *v, *w, *ret, *da, *sw, *l, *vbox, *hbox; tr_torrent_t * tor = tr_torrent_handle (gtor); PeerData * p = g_new (PeerData, 1); char name[64]; /* TODO: make this configurable? */ int view_columns[] = { PEER_COL_IS_CONNECTED, PEER_COL_ADDRESS, PEER_COL_CLIENT, PEER_COL_PROGRESS, PEER_COL_UPLOAD_RATE, PEER_COL_DOWNLOAD_RATE }; m = peer_model_new (tor); v = gtk_tree_view_new_with_model (m); gtk_tree_view_set_rules_hint (GTK_TREE_VIEW(v), TRUE); g_object_unref (G_OBJECT(m)); for (i=0; i%s", _("Piece Availability")); l = gtk_label_new (NULL); gtk_misc_set_alignment (GTK_MISC(l), 0.0f, 0.5f); gtk_label_set_markup (GTK_LABEL(l), name); gtk_box_pack_start (GTK_BOX(vbox), l, FALSE, FALSE, 0); w = da = p->completeness = gtk_drawing_area_new (); gtk_widget_set_usize (w, 0u, 100u); g_object_set_data (G_OBJECT(w), "draw-mode", GINT_TO_POINTER(DRAW_AVAIL)); g_signal_connect (w, "expose-event", G_CALLBACK(refresh_pieces), gtor); h = gtk_hbox_new (FALSE, GUI_PAD); w = gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f); gtk_widget_set_usize (w, GUI_PAD_BIG, 0); gtk_box_pack_start (GTK_BOX(h), w, FALSE, FALSE, 0); gtk_box_pack_start_defaults (GTK_BOX(h), da); gtk_box_pack_start (GTK_BOX(vbox), h, FALSE, FALSE, 0); /* a small vertical spacer */ w = gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f); gtk_widget_set_usize (w, 0u, GUI_PAD); gtk_box_pack_start (GTK_BOX(vbox), w, FALSE, FALSE, 0); g_snprintf (name, sizeof(name), "%s", _("Peers")); l = gtk_label_new (NULL); gtk_misc_set_alignment (GTK_MISC(l), 0.0f, 0.5f); gtk_label_set_markup (GTK_LABEL(l), name); gtk_box_pack_start (GTK_BOX(vbox), l, FALSE, FALSE, 0); h = gtk_hbox_new (FALSE, GUI_PAD); w = gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f); gtk_widget_set_usize (w, GUI_PAD_BIG, 0); gtk_box_pack_start (GTK_BOX(h), w, FALSE, FALSE, 0); gtk_box_pack_start_defaults (GTK_BOX(h), sw); gtk_box_pack_start_defaults (GTK_BOX(vbox), h); hbox = gtk_hbox_new (FALSE, GUI_PAD); w = gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f); gtk_widget_set_usize (w, GUI_PAD_BIG, 0); gtk_box_pack_start (GTK_BOX(hbox), w, FALSE, FALSE, 0); g_snprintf (name, sizeof(name), "%s:", _("Seeders")); l = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL(l), name); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); l = p->seeders_lb = gtk_label_new (NULL); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); gtk_box_pack_start_defaults (GTK_BOX(hbox), gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f)); g_snprintf (name, sizeof(name), "%s:", _("Leechers")); l = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL(l), name); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); l = p->leechers_lb = gtk_label_new (NULL); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); gtk_box_pack_start_defaults (GTK_BOX(hbox), gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f)); g_snprintf (name, sizeof(name), "%s:", _("Completed")); l = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL(l), name); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); l = p->completed_lb = gtk_label_new (NULL); gtk_box_pack_start (GTK_BOX(hbox), l, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX(vbox), hbox, FALSE, FALSE, 0); ret = vbox; p->gtor = gtor; p->model = m; p->store = GTK_TREE_STORE(m); g_object_set_data_full (G_OBJECT(ret), "peer-data", p, g_free); return ret; } /**** ***** INFO TAB ****/ static GtkWidget* info_page_new (tr_torrent_t * tor) { int row = 0; GtkWidget *t = hig_workarea_create (); GtkWidget *l, *w, *fr; char *pch; char *dname, *bname; const char * default_torrents_dir; char buf[256]; char name[128]; const char * namefmt = "%s:"; GtkTextBuffer * b; tr_tracker_info_t * track; const tr_info_t* info = tr_torrentInfo(tor); hig_workarea_add_section_title (t, &row, _("Torrent Information")); hig_workarea_add_section_spacer (t, row, 5); g_snprintf (name, sizeof(name), namefmt, _("Tracker")); track = info->trackerList->list; pch = track->port==80 ? g_strdup_printf ("http://%s%s", track->address, track->announce) : g_strdup_printf ("http://%s:%d%s", track->address, track->port, track->announce); l = gtk_label_new (pch); hig_workarea_add_row (t, &row, name, l, NULL); g_free (pch); g_snprintf (name, sizeof(name), namefmt, _("Pieces")); pch = readablesize (info->pieceSize); g_snprintf (buf, sizeof(buf), "%d (%s)", info->pieceCount, pch); l = gtk_label_new (buf); hig_workarea_add_row (t, &row, name, l, NULL); g_free (pch); g_snprintf (name, sizeof(name), namefmt, _("Hash")); l = gtk_label_new (info->hashString); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Secure")); pch = (info->flags & TR_FLAG_PRIVATE) ? _("Private Torrent, PEX disabled") : _("Public Torrent"); l = gtk_label_new (pch); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Comment")); b = gtk_text_buffer_new (NULL); gtk_text_buffer_set_text (b, info->comment, -1); w = gtk_text_view_new_with_buffer (b); gtk_widget_set_size_request (w, 0u, 100u); gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW(w), GTK_WRAP_WORD); gtk_text_view_set_editable (GTK_TEXT_VIEW(w), FALSE); fr = gtk_frame_new (NULL); gtk_frame_set_shadow_type (GTK_FRAME(fr), GTK_SHADOW_IN); gtk_container_add (GTK_CONTAINER(fr), w); hig_workarea_add_row (t, &row, name, fr, NULL); hig_workarea_add_section_divider (t, &row); hig_workarea_add_section_title (t, &row, _("Created By")); hig_workarea_add_section_spacer (t, row, 2); g_snprintf (name, sizeof(name), namefmt, _("Creator")); l = gtk_label_new (*info->creator ? info->creator : _("N/A")); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Date")); pch = rfc822date ((guint64)info->dateCreated * 1000u); l = gtk_label_new (pch); hig_workarea_add_row (t, &row, name, l, NULL); g_free (pch); hig_workarea_add_section_divider (t, &row); hig_workarea_add_section_title (t, &row, _("Location")); hig_workarea_add_section_spacer (t, row, 3); g_snprintf (name, sizeof(name), namefmt, _("Downloaded Data")); l = gtk_label_new (tr_torrentGetFolder (tor)); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Torrent File Path")); default_torrents_dir = tr_getTorrentsDirectory(); dname = g_path_get_dirname (info->torrent); l = gtk_label_new (strstr(dname,default_torrents_dir)==dname ? _("Transmission Support Folder") : dname); hig_workarea_add_row (t, &row, name, l, NULL); g_free (dname); g_snprintf (name, sizeof(name), namefmt, _("Torrent File Name")); bname = g_path_get_basename (info->torrent); l = gtk_label_new (bname); hig_workarea_add_row (t, &row, name, l, NULL); g_free (bname); hig_workarea_finish (t, &row); return t; } /**** ***** ACTIVITY TAB ****/ typedef struct { GtkWidget * state_lb; GtkWidget * valid_dl_lb; GtkWidget * dl_lb; GtkWidget * ul_lb; GtkWidget * ratio_lb; GtkWidget * err_lb; GtkWidget * remaining_lb; GtkWidget * swarm_lb; GtkWidget * date_added_lb; GtkWidget * last_activity_lb; GtkWidget * availability_da; TrTorrent * gtor; } Activity; static void refresh_activity (GtkWidget * top) { Activity * a = (Activity*) g_object_get_data (G_OBJECT(top), "activity-data"); const tr_stat_t * stat = tr_torrent_stat( a->gtor ); guint64 size; char *pch; pch = tr_torrent_status_str( a->gtor ); gtk_label_set_text (GTK_LABEL(a->state_lb), pch); g_free (pch); size = stat->downloadedValid; pch = readablesize (size); gtk_label_set_text (GTK_LABEL(a->valid_dl_lb), pch); g_free (pch); pch = readablesize (stat->downloaded); gtk_label_set_text (GTK_LABEL(a->dl_lb), pch); g_free (pch); pch = readablesize (stat->uploaded); gtk_label_set_text (GTK_LABEL(a->ul_lb), pch); g_free (pch); pch = ratiostr (stat->downloaded, stat->uploaded); gtk_label_set_text (GTK_LABEL(a->ratio_lb), pch); g_free (pch); pch = readablespeed (stat->swarmspeed); gtk_label_set_text (GTK_LABEL(a->swarm_lb), pch); g_free (pch); pch = readablesize (stat->left); gtk_label_set_text (GTK_LABEL(a->remaining_lb), pch); g_free (pch); gtk_label_set_text (GTK_LABEL(a->err_lb), *stat->errorString ? stat->errorString : _("None")); pch = stat->startDate ? rfc822date (stat->startDate) : g_strdup_printf (_("?")); gtk_label_set_text (GTK_LABEL(a->date_added_lb), pch); g_free (pch); pch = stat->activityDate ? rfc822date (stat->activityDate) : g_strdup_printf (_("?")); gtk_label_set_text (GTK_LABEL(a->last_activity_lb), pch); g_free (pch); if (GDK_IS_DRAWABLE (a->availability_da->window)) refresh_pieces (a->availability_da, NULL, a->gtor); } static GtkWidget* activity_page_new (TrTorrent * gtor) { Activity * a = g_new (Activity, 1); int row = 0; GtkWidget *t = hig_workarea_create (); GtkWidget *l, *w; char name[128]; const char * namefmt = "%s:"; a->gtor = gtor; hig_workarea_add_section_title (t, &row, _("Transfer")); hig_workarea_add_section_spacer (t, row, 8); g_snprintf (name, sizeof(name), namefmt, _("State")); l = a->state_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Valid DL")); l = a->valid_dl_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Downloaded")); l = a->dl_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Uploaded")); l = a->ul_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Ratio")); l = a->ratio_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Remaining")); l = a->remaining_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Swarm Rate")); l = a->swarm_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Error")); l = a->err_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Completeness")); w = a->availability_da = gtk_drawing_area_new (); gtk_widget_set_usize (w, 0u, 100u); g_object_set_data (G_OBJECT(w), "draw-mode", GINT_TO_POINTER(DRAW_PROG)); g_signal_connect (w, "expose-event", G_CALLBACK(refresh_pieces), gtor); hig_workarea_add_row (t, &row, name, w, NULL); hig_workarea_add_section_divider (t, &row); hig_workarea_add_section_title (t, &row, _("Dates")); hig_workarea_add_section_spacer (t, row, 3); g_snprintf (name, sizeof(name), namefmt, _("Added")); l = a->date_added_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); g_snprintf (name, sizeof(name), namefmt, _("Last Activity")); l = a->last_activity_lb = gtk_label_new (NULL); hig_workarea_add_row (t, &row, name, l, NULL); hig_workarea_add_section_divider (t, &row); hig_workarea_finish (t, &row); g_object_set_data_full (G_OBJECT(t), "activity-data", a, g_free); return t; } /**** ***** FILES TAB ****/ #define STRIPROOT( path ) \ ( g_path_is_absolute( (path) ) ? g_path_skip_root( (path) ) : (path) ) enum { FC_STOCK, FC_LABEL, FC_PROG, FC_KEY, FC_INDEX, FC_SIZE, FC_PRIORITY, N_FILE_COLS }; typedef struct { TrTorrent * gtor; GtkTreeModel * model; /* same object as store, but recast */ GtkTreeStore * store; /* same object as model, but recast */ GtkTreeSelection * selection; } FileData; static const char* priorityToString( const int priority ) { switch( priority ) { case TR_PRI_HIGH: return _("High"); case TR_PRI_NORMAL: return _("Normal"); case TR_PRI_LOW: return _("Low"); case TR_PRI_DND: return _("Don't Get"); default: return "BUG!"; } } static tr_priority_t stringToPriority( const char* str ) { if( !strcmp( str, _( "High" ) ) ) return TR_PRI_HIGH; if( !strcmp( str, _( "Low" ) ) ) return TR_PRI_LOW; if( !strcmp( str, _( "Don't Get" ) ) ) return TR_PRI_DND;; return TR_PRI_NORMAL; } static void parsepath( const tr_torrent_t * tor, GtkTreeStore * store, GtkTreeIter * ret, const char * path, int index, uint64_t size ) { GtkTreeModel * model; GtkTreeIter * parent, start, iter; char * file, * lower, * mykey, *escaped=0; const char * stock; int priority = 0; model = GTK_TREE_MODEL( store ); parent = NULL; file = g_path_get_basename( path ); if( 0 != strcmp( file, path ) ) { char * dir = g_path_get_dirname( path ); parsepath( tor, store, &start, dir, index, size ); parent = &start; g_free( dir ); } lower = g_utf8_casefold( file, -1 ); mykey = g_utf8_collate_key( lower, -1 ); if( gtk_tree_model_iter_children( model, &iter, parent ) ) do { gboolean stop; char * modelkey; gtk_tree_model_get( model, &iter, FC_KEY, &modelkey, -1 ); stop = (modelkey!=NULL) && !strcmp(mykey,modelkey); g_free (modelkey); if (stop) goto done; } while( gtk_tree_model_iter_next( model, &iter ) ); gtk_tree_store_append( store, &iter, parent ); if( NULL == ret ) { stock = GTK_STOCK_FILE; } else { stock = GTK_STOCK_DIRECTORY; size = 0; index = -1; } if (index != -1) priority = tr_torrentGetFilePriority( tor, index ); escaped = g_markup_escape_text (file, -1); gtk_tree_store_set( store, &iter, FC_INDEX, index, FC_LABEL, escaped, FC_KEY, mykey, FC_STOCK, stock, FC_PRIORITY, priorityToString(priority), FC_SIZE, size, -1 ); done: g_free( escaped ); g_free( mykey ); g_free( lower ); g_free( file ); if( NULL != ret ) *ret = iter; } static uint64_t getdirtotals( GtkTreeStore * store, GtkTreeIter * parent ) { GtkTreeModel * model; GtkTreeIter iter; uint64_t mysize, subsize; char * sizestr, * name, * label; model = GTK_TREE_MODEL( store ); mysize = 0; if( gtk_tree_model_iter_children( model, &iter, parent ) ) do { if( gtk_tree_model_iter_has_child( model, &iter ) ) { subsize = getdirtotals( store, &iter ); gtk_tree_store_set( store, &iter, FC_SIZE, subsize, -1 ); } else { gtk_tree_model_get( model, &iter, FC_SIZE, &subsize, -1 ); } gtk_tree_model_get( model, &iter, FC_LABEL, &name, -1 ); sizestr = readablesize( subsize ); label = g_markup_printf_escaped( "%s (%s)", name, sizestr ); g_free( sizestr ); g_free( name ); gtk_tree_store_set( store, &iter, FC_LABEL, label, -1 ); g_free( label ); mysize += subsize; } while( gtk_tree_model_iter_next( model, &iter ) ); return mysize; } static void updateprogress( GtkTreeModel * model, GtkTreeStore * store, GtkTreeIter * parent, const float * progress, guint64 * setmeGotSize, guint64 * setmeTotalSize) { GtkTreeIter iter; guint64 gotSize=0, totalSize=0; if( gtk_tree_model_iter_children( model, &iter, parent ) ) do { int oldProg, newProg; guint64 subGot, subTotal; if (gtk_tree_model_iter_has_child( model, &iter ) ) { updateprogress( model, store, &iter, progress, &subGot, &subTotal); } else { int index, percent; gtk_tree_model_get( model, &iter, FC_SIZE, &subTotal, FC_INDEX, &index, -1 ); g_assert( 0 <= index ); percent = (int)(progress[index]*100.0 + 0.5); /* [0...100] */ subGot = (guint64)(subTotal * percent/100.0); } if (!subTotal) subTotal = 1; /* avoid div by zero */ g_assert (subGot <= subTotal); /* why not just set it every time? because that causes the "priorities" combobox to pop down */ gtk_tree_model_get (model, &iter, FC_PROG, &oldProg, -1); newProg = (int)(100.0*subGot/subTotal + 0.5); if (oldProg != newProg) gtk_tree_store_set (store, &iter, FC_PROG, (int)(100.0*subGot/subTotal + 0.5), -1); gotSize += subGot; totalSize += subTotal; } while( gtk_tree_model_iter_next( model, &iter ) ); *setmeGotSize = gotSize; *setmeTotalSize = totalSize; } static GtkTreeModel* priority_model_new (void) { GtkTreeIter iter; GtkListStore * store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT); gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, _("High"), 1, TR_PRI_HIGH, -1); gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, _("Normal"), 1, TR_PRI_NORMAL, -1); gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, _("Low"), 1, TR_PRI_LOW, -1); gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, _("Don't Get"), 1, TR_PRI_DND, -1); return GTK_TREE_MODEL (store); } static void refreshPriorityActions( GtkTreeSelection * sel ) { GtkTreeIter iter; GtkTreeModel * model; const gboolean has_selection = gtk_tree_selection_get_selected( sel, &model, &iter ); action_sensitize ( "priority-high", has_selection ); action_sensitize ( "priority-normal", has_selection ); action_sensitize ( "priority-low", has_selection ); action_sensitize ( "priority-dnd", has_selection ); if( has_selection ) { /* set the action priority base on the model's values */ char * pch = NULL; const char * key; gtk_tree_model_get( model, &iter, FC_PRIORITY, &pch, -1 ); switch( stringToPriority( pch ) ) { case TR_PRI_HIGH: key = "priority-high"; break; case TR_PRI_LOW: key = "priority-low"; break; case TR_PRI_DND: key = "priority-dnd"; break; default: key = "priority-normal"; break; } action_toggle( key, TRUE ); g_free( pch ); } } static void set_priority (GtkTreeSelection * selection, GtkTreeStore * store, GtkTreeIter * iter, tr_torrent_t * tor, int priority_val, const char * priority_str) { int index; GtkTreeIter child; gtk_tree_model_get( GTK_TREE_MODEL(store), iter, FC_INDEX, &index, -1 ); if (index >= 0) tr_torrentSetFilePriority( tor, index, priority_val ); gtk_tree_store_set( store, iter, FC_PRIORITY, priority_str, -1 ); if( gtk_tree_model_iter_children( GTK_TREE_MODEL(store), &child, iter ) ) do set_priority( selection, store, &child, tor, priority_val, priority_str ); while( gtk_tree_model_iter_next( GTK_TREE_MODEL(store), &child ) ); refreshPriorityActions( selection ); } static void priority_changed_cb (GtkCellRendererText * cell UNUSED, const gchar * path, const gchar * value, void * file_data) { GtkTreeIter iter; FileData * d = (FileData*) file_data; if (gtk_tree_model_get_iter_from_string (d->model, &iter, path)) { tr_torrent_t * tor = tr_torrent_handle( d->gtor ); const tr_priority_t priority = stringToPriority( value ); set_priority( d->selection, d->store, &iter, tor, priority, value ); } } /* FIXME: NULL this back out when popup goes down */ static GtkWidget * popupView = NULL; static void on_popup_menu ( GtkWidget * view, GdkEventButton * event ) { GtkWidget * menu = action_get_widget ( "/file-popup" ); popupView = view; gtk_menu_popup (GTK_MENU(menu), NULL, NULL, NULL, NULL, (event ? event->button : 0), (event ? event->time : 0)); } static void fileSelectionChangedCB( GtkTreeSelection * sel, gpointer unused UNUSED ) { refreshPriorityActions( sel ); } void set_selected_file_priority ( tr_priority_t priority_val ) { if( popupView && GTK_IS_TREE_VIEW(popupView) ) { GtkTreeView * view = GTK_TREE_VIEW( popupView ); tr_torrent_t * tor = (tr_torrent_t*) g_object_get_data (G_OBJECT(view), "torrent-handle"); const char * priority_str = priorityToString( priority_val ); GtkTreeModel * model; GtkTreeIter iter; GtkTreeSelection * sel = gtk_tree_view_get_selection (view); gtk_tree_selection_get_selected( sel, &model, &iter ); set_priority( sel, GTK_TREE_STORE(model), &iter, tor, priority_val, priority_str ); } } GtkWidget * file_page_new ( TrTorrent * gtor ) { GtkWidget * ret; FileData * data; tr_info_t * inf; tr_torrent_t * tor; GtkTreeStore * store; int ii; GtkWidget * view, * scroll; GtkCellRenderer * rend; GtkTreeViewColumn * col; GtkTreeSelection * sel; GtkTreeModel * model; store = gtk_tree_store_new ( N_FILE_COLS, G_TYPE_STRING, /* stock */ G_TYPE_STRING, /* label */ G_TYPE_INT, /* prog [0..100] */ G_TYPE_STRING, /* key */ G_TYPE_INT, /* index */ G_TYPE_UINT64, /* size */ G_TYPE_STRING); /* priority */ /* set up the model */ tor = tr_torrent_handle( gtor ); inf = tr_torrent_info( gtor ); for( ii = 0; ii < inf->fileCount; ii++ ) { parsepath( tor, store, NULL, STRIPROOT( inf->files[ii].name ), ii, inf->files[ii].length ); } getdirtotals( store, NULL ); gtk_tree_sortable_set_sort_column_id( GTK_TREE_SORTABLE( store ), FC_KEY, GTK_SORT_ASCENDING ); /* create the view */ view = gtk_tree_view_new_with_model( GTK_TREE_MODEL( store ) ); g_object_set_data (G_OBJECT(view), "torrent-handle", tor ); g_signal_connect( view, "popup-menu", G_CALLBACK(on_popup_menu), NULL ); g_signal_connect( view, "button-press-event", G_CALLBACK(on_tree_view_button_pressed), on_popup_menu); /* add file column */ col = GTK_TREE_VIEW_COLUMN (g_object_new (GTK_TYPE_TREE_VIEW_COLUMN, "sizing", GTK_TREE_VIEW_COLUMN_AUTOSIZE, "expand", TRUE, "title", _("File"), NULL)); rend = gtk_cell_renderer_pixbuf_new(); gtk_tree_view_column_pack_start( col, rend, FALSE ); gtk_tree_view_column_add_attribute( col, rend, "stock-id", FC_STOCK ); /* add text renderer */ rend = gtk_cell_renderer_text_new(); g_object_set( rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL ); gtk_tree_view_column_pack_start( col, rend, TRUE ); gtk_tree_view_column_add_attribute( col, rend, "markup", FC_LABEL ); gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); /* add progress column */ rend = gtk_cell_renderer_progress_new(); col = gtk_tree_view_column_new_with_attributes ( _("Progress"), rend, "value", FC_PROG, NULL); gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); /* set up view */ sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); gtk_tree_view_expand_all( GTK_TREE_VIEW( view ) ); gtk_tree_view_set_search_column( GTK_TREE_VIEW( view ), FC_LABEL ); g_signal_connect( sel, "changed", G_CALLBACK(fileSelectionChangedCB), NULL ); fileSelectionChangedCB( sel, NULL ); /* add priority column */ model = priority_model_new (); col = gtk_tree_view_column_new (); gtk_tree_view_column_set_title (col, _("Priority")); rend = gtk_cell_renderer_combo_new (); gtk_tree_view_column_pack_start (col, rend, TRUE); g_object_set (G_OBJECT(rend), "model", model, "editable", TRUE, "has-entry", FALSE, "text-column", 0, NULL); g_object_unref (G_OBJECT(model)); gtk_tree_view_column_add_attribute (col, rend, "text", FC_PRIORITY); gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); /* create the scrolled window and stick the view in it */ scroll = gtk_scrolled_window_new( NULL, NULL ); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scroll ), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC ); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(scroll), GTK_SHADOW_IN); gtk_container_add( GTK_CONTAINER( scroll ), view ); gtk_widget_set_usize (scroll, 0u, 200u); gtk_container_set_border_width (GTK_CONTAINER(scroll), GUI_PAD); ret = scroll; data = g_new (FileData, 1); data->gtor = gtor; data->model = GTK_TREE_MODEL(store); data->store = store; data->selection = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); g_object_set_data_full (G_OBJECT(ret), "file-data", data, g_free); g_signal_connect (G_OBJECT(rend), "edited", G_CALLBACK(priority_changed_cb), data); return ret; } static void refresh_files (GtkWidget * top) { guint64 foo, bar; FileData * data = (FileData*) g_object_get_data (G_OBJECT(top), "file-data"); tr_torrent_t * tor = tr_torrent_handle( data->gtor ); float * progress = tr_torrentCompletion ( tor ); updateprogress (data->model, data->store, NULL, progress, &foo, &bar); free( progress ); } /**** ***** OPTIONS ****/ static void ul_speed_toggled_cb (GtkToggleButton *tb, gpointer gtor) { tr_torrent_set_upload_cap_enabled (TR_TORRENT(gtor), gtk_toggle_button_get_active(tb)); } static void dl_speed_toggled_cb (GtkToggleButton *tb, gpointer gtor) { tr_torrent_set_download_cap_enabled (TR_TORRENT(gtor), gtk_toggle_button_get_active(tb)); } static void seeding_cap_toggled_cb (GtkToggleButton *tb, gpointer gtor) { tr_torrent_set_seeding_cap_enabled (TR_TORRENT(gtor), gtk_toggle_button_get_active(tb)); } static void sensitize_from_check_cb (GtkToggleButton *toggle, gpointer w) { gtk_widget_set_sensitive (GTK_WIDGET(w), gtk_toggle_button_get_active(toggle)); } static void ul_speed_spun_cb (GtkSpinButton *spin, gpointer gtor) { tr_torrent_set_upload_cap_speed (TR_TORRENT(gtor), gtk_spin_button_get_value_as_int (spin)); } static void dl_speed_spun_cb (GtkSpinButton *spin, gpointer gtor) { tr_torrent_set_download_cap_speed (TR_TORRENT(gtor), gtk_spin_button_get_value_as_int (spin)); } static void seeding_ratio_spun_cb (GtkSpinButton *spin, gpointer gtor) { tr_torrent_set_seeding_cap_ratio (TR_TORRENT(gtor), gtk_spin_button_get_value(spin)); } GtkWidget* options_page_new ( TrTorrent * gtor ) { int row; GtkAdjustment *a; GtkWidget *t, *w, *tb; row = 0; t = hig_workarea_create (); hig_workarea_add_section_title (t, &row, _("Transfer Bandwidth")); hig_workarea_add_section_spacer (t, row, 2); tb = gtk_check_button_new_with_mnemonic (_("Limit _Download Speed (KiB/s):")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(tb), gtor->dl_cap_enabled); g_signal_connect (tb, "toggled", G_CALLBACK(dl_speed_toggled_cb), gtor); a = (GtkAdjustment*) gtk_adjustment_new (gtor->dl_cap, 0.0, G_MAXDOUBLE, 1, 1, 1); w = gtk_spin_button_new (a, 1, 0); g_signal_connect (w, "value-changed", G_CALLBACK(dl_speed_spun_cb), gtor); g_signal_connect (tb, "toggled", G_CALLBACK(sensitize_from_check_cb), w); sensitize_from_check_cb (GTK_TOGGLE_BUTTON(tb), w); hig_workarea_add_row_w (t, &row, tb, w, NULL); tb = gtk_check_button_new_with_mnemonic (_("Limit _Upload Speed (KiB/s):")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(tb), gtor->ul_cap_enabled); g_signal_connect (tb, "toggled", G_CALLBACK(ul_speed_toggled_cb), gtor); a = (GtkAdjustment*) gtk_adjustment_new (gtor->ul_cap, 0.0, G_MAXDOUBLE, 1, 1, 1); w = gtk_spin_button_new (a, 1, 0); g_signal_connect (w, "value-changed", G_CALLBACK(ul_speed_spun_cb), gtor); g_signal_connect (tb, "toggled", G_CALLBACK(sensitize_from_check_cb), w); sensitize_from_check_cb (GTK_TOGGLE_BUTTON(tb), w); hig_workarea_add_row_w (t, &row, tb, w, NULL); hig_workarea_add_section_divider (t, &row); hig_workarea_add_section_title (t, &row, _("Seeding")); hig_workarea_add_section_spacer (t, row, 1); tb = gtk_check_button_new_with_mnemonic (_("_Stop Seeding at Ratio:")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(tb), gtor->seeding_cap_enabled); g_signal_connect (tb, "toggled", G_CALLBACK(seeding_cap_toggled_cb), gtor); a = (GtkAdjustment*) gtk_adjustment_new (gtor->seeding_cap, 0.0, G_MAXDOUBLE, 1, 1, 1); w = gtk_spin_button_new (a, 1, 1); g_signal_connect (w, "value-changed", G_CALLBACK(seeding_ratio_spun_cb), gtor); g_signal_connect (tb, "toggled", G_CALLBACK(sensitize_from_check_cb), w); sensitize_from_check_cb (GTK_TOGGLE_BUTTON(tb), w); hig_workarea_add_row_w (t, &row, tb, w, NULL); hig_workarea_finish (t, &row); return t; } static void refresh_options (GtkWidget * top UNUSED) { } /**** ***** DIALOG ****/ static void torrent_destroyed (gpointer dialog, GObject * dead_torrent UNUSED) { gtk_widget_destroy (GTK_WIDGET(dialog)); } static void remove_tag (gpointer tag) { g_source_remove (GPOINTER_TO_UINT(tag)); /* stop the periodic refresh */ } static void response_cb (GtkDialog *dialog, int response UNUSED, gpointer gtor) { g_object_weak_unref (G_OBJECT(gtor), torrent_destroyed, dialog); gtk_widget_destroy (GTK_WIDGET(dialog)); } static gboolean periodic_refresh (gpointer data) { refresh_peers (g_object_get_data (G_OBJECT(data), "peers-top")); refresh_activity (g_object_get_data (G_OBJECT(data), "activity-top")); refresh_files (g_object_get_data (G_OBJECT(data), "files-top")); refresh_options (g_object_get_data (G_OBJECT(data), "options-top")); return TRUE; } GtkWidget* torrent_inspector_new ( GtkWindow * parent, TrTorrent * gtor ) { guint tag; char *size, *pch; GtkWidget *d, *n, *w; tr_torrent_t * tor = tr_torrent_handle (gtor); tr_info_t * info = tr_torrent_info (gtor); /* create the dialog */ pch = g_strdup_printf ("%s: %s", g_get_application_name(), _("Torrent Inspector")); d = gtk_dialog_new_with_buttons (pch, parent, 0, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL); gtk_window_set_role (GTK_WINDOW(d), "tr-info" ); g_signal_connect (d, "response", G_CALLBACK (response_cb), gtor); g_object_weak_ref (G_OBJECT(gtor), torrent_destroyed, d); g_free (pch); /* add label with file name and size */ size = readablesize( info->totalSize ); pch = g_markup_printf_escaped( "%s\n%s", info->name, size ); w = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL(w), pch); gtk_label_set_justify (GTK_LABEL(w), GTK_JUSTIFY_CENTER); gtk_misc_set_alignment (GTK_MISC(w), 0.5f, 0.5f); gtk_box_pack_start (GTK_BOX(GTK_DIALOG(d)->vbox), w, 0, 0, GUI_PAD); g_free (pch); g_free (size); /* add the notebook */ n = gtk_notebook_new (); w = activity_page_new (gtor); g_object_set_data (G_OBJECT(d), "activity-top", w); gtk_notebook_append_page (GTK_NOTEBOOK(n), w, gtk_label_new_with_mnemonic (_("_Activity"))); w = peer_page_new (gtor); g_object_set_data (G_OBJECT(d), "peers-top", w); gtk_notebook_append_page (GTK_NOTEBOOK(n), w, gtk_label_new_with_mnemonic (_("_Peers"))); gtk_notebook_append_page (GTK_NOTEBOOK(n), info_page_new (tor), gtk_label_new_with_mnemonic (_("_Info"))); w = file_page_new (gtor); g_object_set_data (G_OBJECT(d), "files-top", w); gtk_notebook_append_page (GTK_NOTEBOOK(n), w, gtk_label_new_with_mnemonic (_("_Files"))); w = options_page_new (gtor); g_object_set_data (G_OBJECT(d), "options-top", w); gtk_notebook_append_page (GTK_NOTEBOOK(n), w, gtk_label_new_with_mnemonic (_("_Options"))); gtk_box_pack_start_defaults (GTK_BOX(GTK_DIALOG(d)->vbox), n); tag = g_timeout_add (UPDATE_INTERVAL_MSEC, periodic_refresh, d); g_object_set_data_full (G_OBJECT(d), "tag", GUINT_TO_POINTER(tag), remove_tag); /* return the results */ periodic_refresh (d); gtk_widget_show_all (GTK_DIALOG(d)->vbox); return d; }