/* * This file Copyright (C) Mnemosyne LLC * * This file is licensed by the GPL version 2. Works owned by the * Transmission project are granted a special exemption to clause 2 (b) * so that the bulk of its code can remain under the MIT license. * This exemption does not extend to derived works not owned by * the Transmission project. * * $Id$ */ #include #include #include #include #include #include #include #include "file-list.h" #include "hig.h" #include "icons.h" #include "tr-prefs.h" #include "util.h" #define TR_DOWNLOAD_KEY "tr-download-key" #define TR_COLUMN_ID_KEY "tr-model-column-id-key" #define TR_PRIORITY_KEY "tr-priority-key" enum { /* these two fields could be any number at all so long as they're not * TR_PRI_LOW, TR_PRI_NORMAL, TR_PRI_HIGH, TRUE, or FALSE */ NOT_SET = 1000, MIXED = 1001 }; enum { FC_ICON, FC_LABEL, FC_LABEL_ESC, FC_PROG, FC_INDEX, FC_SIZE, FC_SIZE_STR, FC_HAVE, FC_PRIORITY, FC_ENABLED, N_FILE_COLS }; typedef struct { TrCore * core; GtkWidget * top; GtkWidget * view; GtkTreeModel * model; /* same object as store, but recast */ GtkTreeStore * store; /* same object as model, but recast */ int torrentId; guint timeout_tag; } FileData; static void clearData (FileData * data) { data->torrentId = -1; if (data->timeout_tag) { g_source_remove (data->timeout_tag); data->timeout_tag = 0; } } static void freeData (gpointer data) { clearData (data); g_free (data); } /*** **** ***/ struct RefreshData { int sort_column_id; gboolean resort_needed; tr_file_stat * refresh_file_stat; tr_torrent * tor; FileData * file_data; }; static gboolean refreshFilesForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer gdata) { struct RefreshData * refresh_data = gdata; FileData * data = refresh_data->file_data; unsigned int index; uint64_t size; uint64_t old_have; int old_prog; int old_priority; int old_enabled; const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter); gtk_tree_model_get (model, iter, FC_ENABLED, &old_enabled, FC_PRIORITY, &old_priority, FC_INDEX, &index, FC_HAVE, &old_have, FC_SIZE, &size, FC_PROG, &old_prog, -1); if (is_file) { tr_torrent * tor = refresh_data->tor; const tr_info * inf = tr_torrentInfo (tor); const int enabled = !inf->files[index].dnd; const int priority = inf->files[index].priority; const uint64_t have = refresh_data->refresh_file_stat[index].bytesCompleted; const int prog = size ? (int)((100.0*have)/size) : 1; if ((priority!=old_priority) || (enabled!=old_enabled) || (have!=old_have) || (prog!=old_prog)) { /* Changing a value in the sort column can trigger a resort * which breaks this foreach () call. (See #3529) * As a workaround: if that's about to happen, temporarily disable * sorting until we finish walking the tree. */ if (!refresh_data->resort_needed) { if ((refresh_data->resort_needed = ((refresh_data->sort_column_id==FC_PRIORITY) && (priority!=old_priority)) || ((refresh_data->sort_column_id==FC_ENABLED) && (enabled!=old_enabled)))) { refresh_data->resort_needed = TRUE; gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (data->model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); } } gtk_tree_store_set (data->store, iter, FC_PRIORITY, priority, FC_ENABLED, enabled, FC_HAVE, have, FC_PROG, prog, -1); } } else { GtkTreeIter child; uint64_t sub_size = 0; uint64_t have = 0; int prog; int enabled = NOT_SET; int priority = NOT_SET; /* since gtk_tree_model_foreach () is depth-first, we can * get the `sub' info by walking the immediate children */ if (gtk_tree_model_iter_children (model, &child, iter)) do { int child_enabled; int child_priority; int64_t child_have, child_size; gtk_tree_model_get (model, &child, FC_SIZE, &child_size, FC_HAVE, &child_have, FC_PRIORITY, &child_priority, FC_ENABLED, &child_enabled, -1); if ((child_enabled != FALSE) && (child_enabled != NOT_SET)) { sub_size += child_size; have += child_have; } if (enabled == NOT_SET) enabled = child_enabled; else if (enabled != child_enabled) enabled = MIXED; if (priority == NOT_SET) priority = child_priority; else if (priority != child_priority) priority = MIXED; } while (gtk_tree_model_iter_next (model, &child)); prog = sub_size ? (int)((100.0*have)/sub_size) : 1; if ((size!=sub_size) || (have!=old_have) || (priority!=old_priority) || (enabled!=old_enabled) || (prog!=old_prog)) { char size_str[64]; tr_strlsize (size_str, sub_size, sizeof size_str); gtk_tree_store_set (data->store, iter, FC_SIZE, sub_size, FC_SIZE_STR, size_str, FC_HAVE, have, FC_PRIORITY, priority, FC_ENABLED, enabled, FC_PROG, prog, -1); } } return FALSE; /* keep walking */ } static void gtr_tree_model_foreach_postorder_subtree (GtkTreeModel * model, GtkTreeIter * parent, GtkTreeModelForeachFunc func, gpointer data) { GtkTreeIter child; if (gtk_tree_model_iter_children (model, &child, parent)) do gtr_tree_model_foreach_postorder_subtree (model, &child, func, data); while (gtk_tree_model_iter_next (model, &child)); if (parent) func (model, NULL, parent, data); } static void gtr_tree_model_foreach_postorder (GtkTreeModel * model, GtkTreeModelForeachFunc func, gpointer data) { GtkTreeIter iter; if (gtk_tree_model_iter_nth_child (model, &iter, NULL, 0)) do gtr_tree_model_foreach_postorder_subtree (model, &iter, func, data); while (gtk_tree_model_iter_next (model, &iter)); } static void refresh (FileData * data) { tr_torrent * tor = gtr_core_find_torrent (data->core, data->torrentId); if (tor == NULL) { gtr_file_list_clear (data->top); } else { GtkSortType order; int sort_column_id; tr_file_index_t fileCount; struct RefreshData refresh_data; GtkTreeSortable * sortable = GTK_TREE_SORTABLE (data->model); gtk_tree_sortable_get_sort_column_id (sortable, &sort_column_id, &order); refresh_data.sort_column_id = sort_column_id; refresh_data.resort_needed = FALSE; refresh_data.refresh_file_stat = tr_torrentFiles (tor, &fileCount); refresh_data.tor = tor; refresh_data.file_data = data; gtr_tree_model_foreach_postorder (data->model, refreshFilesForeach, &refresh_data); if (refresh_data.resort_needed) gtk_tree_sortable_set_sort_column_id (sortable, sort_column_id, order); tr_torrentFilesFree (refresh_data.refresh_file_stat, fileCount); } } static gboolean refreshModel (gpointer file_data) { refresh (file_data); return G_SOURCE_CONTINUE; } /*** **** ***/ struct ActiveData { GtkTreeSelection * sel; GArray * array; }; static gboolean getSelectedFilesForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer gdata) { const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter); if (is_file) { struct ActiveData * data = gdata; /* active means: if it's selected or any ancestor is selected */ gboolean is_active = gtk_tree_selection_iter_is_selected (data->sel, iter); if (!is_active) { GtkTreeIter walk = *iter; GtkTreeIter parent; while (!is_active && gtk_tree_model_iter_parent (model, &parent, &walk)) { is_active = gtk_tree_selection_iter_is_selected (data->sel, &parent); walk = parent; } } if (is_active) { unsigned int i; gtk_tree_model_get (model, iter, FC_INDEX, &i, -1); g_array_append_val (data->array, i); } } return FALSE; /* keep walking */ } static GArray* getSelectedFilesAndDescendants (GtkTreeView * view) { struct ActiveData data; data.sel = gtk_tree_view_get_selection (view); data.array = g_array_new (FALSE, FALSE, sizeof (tr_file_index_t)); gtk_tree_model_foreach (gtk_tree_view_get_model (view), getSelectedFilesForeach, &data); return data.array; } struct SubtreeForeachData { GArray * array; GtkTreePath * path; }; static gboolean getSubtreeForeach (GtkTreeModel * model, GtkTreePath * path, GtkTreeIter * iter, gpointer gdata) { const gboolean is_file = !gtk_tree_model_iter_has_child (model, iter); if (is_file) { struct SubtreeForeachData * data = gdata; if (!gtk_tree_path_compare (path, data->path) || gtk_tree_path_is_descendant (path, data->path)) { unsigned int i; gtk_tree_model_get (model, iter, FC_INDEX, &i, -1); g_array_append_val (data->array, i); } } return FALSE; /* keep walking */ } static void getSubtree (GtkTreeView * view, GtkTreePath * path, GArray * indices) { struct SubtreeForeachData tmp; tmp.array = indices; tmp.path = path; gtk_tree_model_foreach (gtk_tree_view_get_model (view), getSubtreeForeach, &tmp); } /* if `path' is a selected row, all selected rows are returned. * otherwise, only the row indicated by `path' is returned. * this is for toggling all the selected rows' states in a batch. */ static GArray* getActiveFilesForPath (GtkTreeView * view, GtkTreePath * path) { GArray * indices; GtkTreeSelection * sel = gtk_tree_view_get_selection (view); if (gtk_tree_selection_path_is_selected (sel, path)) { /* clicked in a selected row... use the current selection */ indices = getSelectedFilesAndDescendants (view); } else { /* clicked OUTSIDE of the selected row... just use the clicked row */ indices = g_array_new (FALSE, FALSE, sizeof (tr_file_index_t)); getSubtree (view, path, indices); } return indices; } /*** **** ***/ void gtr_file_list_clear (GtkWidget * w) { gtr_file_list_set_torrent (w, -1); } struct build_data { GtkWidget * w; tr_torrent * tor; GtkTreeIter * iter; GtkTreeStore * store; }; struct row_struct { uint64_t length; char * name; int index; }; static void buildTree (GNode * node, gpointer gdata) { char size_str[64]; GtkTreeIter child_iter; struct build_data * build = gdata; struct row_struct *child_data = node->data; const gboolean isLeaf = node->children == NULL; const char * mime_type = isLeaf ? gtr_get_mime_type_from_filename (child_data->name) : DIRECTORY_MIME_TYPE; GdkPixbuf * icon = gtr_get_mime_type_icon (mime_type, GTK_ICON_SIZE_MENU, build->w); const tr_info * inf = tr_torrentInfo (build->tor); const int priority = isLeaf ? inf->files[ child_data->index ].priority : 0; const gboolean enabled = isLeaf ? !inf->files[ child_data->index ].dnd : TRUE; char * name_esc = g_markup_escape_text (child_data->name, -1); tr_strlsize (size_str, child_data->length, sizeof size_str); gtk_tree_store_insert_with_values (build->store, &child_iter, build->iter, INT_MAX, FC_INDEX, child_data->index, FC_LABEL, child_data->name, FC_LABEL_ESC, name_esc, FC_SIZE, child_data->length, FC_SIZE_STR, size_str, FC_ICON, icon, FC_PRIORITY, priority, FC_ENABLED, enabled, -1); if (!isLeaf) { struct build_data b = *build; b.iter = &child_iter; g_node_children_foreach (node, G_TRAVERSE_ALL, buildTree, &b); } g_free (name_esc); g_object_unref (icon); /* we're done with this node */ g_free (child_data->name); g_free (child_data); } static GNode* find_child (GNode* parent, const char * name) { GNode * child = parent->children; while (child) { const struct row_struct * child_data = child->data; if ((*child_data->name == *name) && !g_strcmp0 (child_data->name, name)) break; child = child->next; } return child; } void gtr_file_list_set_torrent (GtkWidget * w, int torrentId) { GtkTreeStore * store; FileData * data = g_object_get_data (G_OBJECT (w), "file-data"); /* unset the old fields */ clearData (data); /* instantiate the model */ store = gtk_tree_store_new (N_FILE_COLS, GDK_TYPE_PIXBUF, /* icon */ G_TYPE_STRING, /* label */ G_TYPE_STRING, /* label esc */ G_TYPE_INT, /* prog [0..100] */ G_TYPE_UINT, /* index */ G_TYPE_UINT64, /* size */ G_TYPE_STRING, /* size str */ G_TYPE_UINT64, /* have */ G_TYPE_INT, /* priority */ G_TYPE_INT); /* dl enabled */ data->store = store; data->model = GTK_TREE_MODEL (store); data->torrentId = torrentId; /* populate the model */ if (torrentId > 0) { tr_torrent * tor = gtr_core_find_torrent (data->core, torrentId); if (tor != NULL) { tr_file_index_t i; const tr_info * inf = tr_torrentInfo (tor); struct row_struct * root_data; GNode * root; struct build_data build; /* build a GNode tree of the files */ root_data = g_new0 (struct row_struct, 1); root_data->name = g_strdup (tr_torrentName (tor)); root_data->index = -1; root_data->length = 0; root = g_node_new (root_data); for (i=0; ifileCount; ++i) { int j; GNode * parent = root; const tr_file * file = &inf->files[i]; char ** tokens = g_strsplit (file->name, G_DIR_SEPARATOR_S, 0); for (j=0; tokens[j]; ++j) { const gboolean isLeaf = tokens[j+1] == NULL; const char * name = tokens[j]; GNode * node = find_child (parent, name); if (node == NULL) { struct row_struct * row = g_new (struct row_struct, 1); row->name = g_strdup (name); row->index = isLeaf ? (int)i : -1; row->length = isLeaf ? file->length : 0; node = g_node_new (row); g_node_append (parent, node); } parent = node; } g_strfreev (tokens); } /* now, add them to the model */ build.w = w; build.tor = tor; build.store = data->store; build.iter = NULL; g_node_children_foreach (root, G_TRAVERSE_ALL, buildTree, &build); /* cleanup */ g_node_destroy (root); g_free (root_data->name); g_free (root_data); } refresh (data); data->timeout_tag = gdk_threads_add_timeout_seconds (SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, refreshModel, data); } gtk_tree_view_set_model (GTK_TREE_VIEW (data->view), data->model); gtk_tree_view_expand_all (GTK_TREE_VIEW (data->view)); g_object_unref (data->model); } /*** **** ***/ static void renderDownload (GtkTreeViewColumn * column UNUSED, GtkCellRenderer * renderer, GtkTreeModel * model, GtkTreeIter * iter, gpointer data UNUSED) { gboolean enabled; gtk_tree_model_get (model, iter, FC_ENABLED, &enabled, -1); g_object_set (renderer, "inconsistent", (enabled==MIXED), "active", (enabled==TRUE), NULL); } static void renderPriority (GtkTreeViewColumn * column UNUSED, GtkCellRenderer * renderer, GtkTreeModel * model, GtkTreeIter * iter, gpointer data UNUSED) { int priority; const char * text; gtk_tree_model_get (model, iter, FC_PRIORITY, &priority, -1); switch (priority) { case TR_PRI_HIGH: text = _("High"); break; case TR_PRI_NORMAL: text = _("Normal"); break; case TR_PRI_LOW: text = _("Low"); break; default: text = _("Mixed"); break; } g_object_set (renderer, "text", text, NULL); } /* build a filename from tr_torrentGetCurrentDir () + the model's FC_LABELs */ static char* buildFilename (tr_torrent * tor, GtkTreeModel * model, GtkTreePath * path, GtkTreeIter * iter) { char * ret; GtkTreeIter child; GtkTreeIter parent = *iter; int n = gtk_tree_path_get_depth (path); char ** tokens = g_new0 (char*, n + 2); tokens[0] = g_strdup (tr_torrentGetCurrentDir (tor)); do { child = parent; gtk_tree_model_get (model, &child, FC_LABEL, &tokens[n--], -1); } while (gtk_tree_model_iter_parent (model, &parent, &child)); ret = g_build_filenamev (tokens); g_strfreev (tokens); return ret; } static gboolean onRowActivated (GtkTreeView * view, GtkTreePath * path, GtkTreeViewColumn * col UNUSED, gpointer gdata) { gboolean handled = FALSE; FileData * data = gdata; tr_torrent * tor = gtr_core_find_torrent (data->core, data->torrentId); if (tor != NULL) { GtkTreeIter iter; GtkTreeModel * model = gtk_tree_view_get_model (view); if (gtk_tree_model_get_iter (model, &iter, path)) { int prog; char * filename = buildFilename (tor, model, path, &iter); gtk_tree_model_get (model, &iter, FC_PROG, &prog, -1); /* if the file's not done, walk up the directory tree until we find * an ancestor that exists, and open that instead */ if (filename && (prog<100 || !g_file_test (filename, G_FILE_TEST_EXISTS))) do { char * tmp = g_path_get_dirname (filename); g_free (filename); filename = tmp; } while (filename && *filename && !g_file_test (filename, G_FILE_TEST_EXISTS)); if ((handled = filename && *filename)) gtr_open_file (filename); } } return handled; } static gboolean onViewPathToggled (GtkTreeView * view, GtkTreeViewColumn * col, GtkTreePath * path, FileData * data) { int cid; tr_torrent * tor; gboolean handled = FALSE; if (!col || !path) return FALSE; cid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (col), TR_COLUMN_ID_KEY)); tor = gtr_core_find_torrent (data->core, data->torrentId); if ((tor != NULL) && ((cid == FC_PRIORITY) || (cid == FC_ENABLED))) { GtkTreeIter iter; GArray * indices = getActiveFilesForPath (view, path); GtkTreeModel * model = data->model; gtk_tree_model_get_iter (model, &iter, path); if (cid == FC_PRIORITY) { int priority; gtk_tree_model_get (model, &iter, FC_PRIORITY, &priority, -1); switch (priority) { case TR_PRI_NORMAL: priority = TR_PRI_HIGH; break; case TR_PRI_HIGH: priority = TR_PRI_LOW; break; default: priority = TR_PRI_NORMAL; break; } tr_torrentSetFilePriorities (tor, (tr_file_index_t *) indices->data, (tr_file_index_t) indices->len, priority); } else { int enabled; gtk_tree_model_get (model, &iter, FC_ENABLED, &enabled, -1); enabled = !enabled; tr_torrentSetFileDLs (tor, (tr_file_index_t *) indices->data, (tr_file_index_t) indices->len, enabled); } refresh (data); g_array_free (indices, TRUE); handled = TRUE; } return handled; } /** * @note 'col' and 'path' are assumed not to be NULL. */ static gboolean getAndSelectEventPath (GtkTreeView * treeview, GdkEventButton * event, GtkTreeViewColumn ** col, GtkTreePath ** path) { GtkTreeSelection * sel; if (gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, path, col, NULL, NULL)) { sel = gtk_tree_view_get_selection (treeview); if (!gtk_tree_selection_path_is_selected (sel, *path)) { gtk_tree_selection_unselect_all (sel); gtk_tree_selection_select_path (sel, *path); } return TRUE; } return FALSE; } static gboolean onViewButtonPressed (GtkWidget * w, GdkEventButton * event, gpointer gdata) { GtkTreeViewColumn * col; GtkTreePath * path = NULL; gboolean handled = FALSE; GtkTreeView * treeview = GTK_TREE_VIEW (w); FileData * data = gdata; if ((event->type == GDK_BUTTON_PRESS) && (event->button == 1) && ! (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) && getAndSelectEventPath (treeview, event, &col, &path)) { handled = onViewPathToggled (treeview, col, path, data); if (path != NULL) gtk_tree_path_free (path); } return handled; } struct rename_data { int error; char * newname; char * path_string; FileData * file_data; }; static int on_rename_done_idle (struct rename_data * data) { if (data->error == 0) { GtkTreeIter iter; if (gtk_tree_model_get_iter_from_string (data->file_data->model, &iter, data->path_string)) gtk_tree_store_set (data->file_data->store, &iter, FC_LABEL, data->newname, -1); } else { GtkWidget * w = gtk_message_dialog_new ( GTK_WINDOW (gtk_widget_get_toplevel(data->file_data->top)), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Unable to rename file as \"%s\": %s"), data->newname, tr_strerror(data->error)); gtk_message_dialog_format_secondary_text ( GTK_MESSAGE_DIALOG (w), "%s", _("Please correct the errors and try again.")); gtk_dialog_run (GTK_DIALOG (w)); gtk_widget_destroy (w); } /* cleanup */ g_free (data->path_string); g_free (data->newname); g_free (data); return G_SOURCE_REMOVE; } static void on_rename_done (tr_torrent * tor G_GNUC_UNUSED, const char * oldpath G_GNUC_UNUSED, const char * newname G_GNUC_UNUSED, int error, struct rename_data * rename_data) { rename_data->error = error; gdk_threads_add_idle ((GSourceFunc)on_rename_done_idle, rename_data); } static void cell_edited_callback (GtkCellRendererText * cell G_GNUC_UNUSED, gchar * path_string, gchar * newname, FileData * data) { tr_torrent * tor; GString * oldpath; GtkTreeIter iter; struct rename_data * rename_data; tor = gtr_core_find_torrent (data->core, data->torrentId); if (tor == NULL) return; if (!gtk_tree_model_get_iter_from_string (data->model, &iter, path_string)) return; /* build oldpath */ oldpath = g_string_new (NULL); for (;;) { char * token = NULL; GtkTreeIter child; gtk_tree_model_get (data->model, &iter, FC_LABEL, &token, -1); g_string_prepend (oldpath, token); g_free (token); child = iter; if (!gtk_tree_model_iter_parent (data->model, &iter, &child)) break; g_string_prepend_c (oldpath, G_DIR_SEPARATOR); } /* do the renaming */ rename_data = g_new0 (struct rename_data, 1); rename_data->newname = g_strdup (newname); rename_data->file_data = data; rename_data->path_string = g_strdup (path_string); tr_torrentRenamePath (tor, oldpath->str, newname, (tr_torrent_rename_done_func*)on_rename_done, rename_data); /* cleanup */ g_string_free (oldpath, TRUE); } GtkWidget * gtr_file_list_new (TrCore * core, int torrentId) { int size; int width; GtkWidget * ret; GtkWidget * view; GtkWidget * scroll; GtkCellRenderer * rend; GtkTreeSelection * sel; GtkTreeViewColumn * col; GtkTreeView * tree_view; const char * title; PangoLayout * pango_layout; PangoContext * pango_context; PangoFontDescription * pango_font_description; FileData * data = g_new0 (FileData, 1); data->core = core; /* create the view */ view = gtk_tree_view_new (); tree_view = GTK_TREE_VIEW (view); gtk_tree_view_set_rules_hint (tree_view, TRUE); gtk_container_set_border_width (GTK_CONTAINER (view), GUI_PAD_BIG); g_signal_connect (view, "button-press-event", G_CALLBACK (onViewButtonPressed), data); g_signal_connect (view, "row_activated", G_CALLBACK (onRowActivated), data); g_signal_connect (view, "button-release-event", G_CALLBACK (on_tree_view_button_released), NULL); pango_context = gtk_widget_create_pango_context (view); pango_font_description = pango_font_description_copy (pango_context_get_font_description (pango_context)); size = pango_font_description_get_size (pango_font_description); pango_font_description_set_size (pango_font_description, size * 0.8); g_object_unref (G_OBJECT (pango_context)); /* set up view */ sel = gtk_tree_view_get_selection (tree_view); gtk_tree_selection_set_mode (sel, GTK_SELECTION_MULTIPLE); gtk_tree_view_expand_all (tree_view); gtk_tree_view_set_search_column (tree_view, FC_LABEL); /* add file column */ col = GTK_TREE_VIEW_COLUMN (g_object_new (GTK_TYPE_TREE_VIEW_COLUMN, "expand", TRUE, "title", _("Name"), NULL)); gtk_tree_view_column_set_resizable (col, TRUE); rend = gtk_cell_renderer_pixbuf_new (); gtk_tree_view_column_pack_start (col, rend, FALSE); gtk_tree_view_column_add_attribute (col, rend, "pixbuf", FC_ICON); /* add text renderer */ rend = gtk_cell_renderer_text_new (); g_object_set (rend, "editable", TRUE, NULL); g_object_set (rend, "ellipsize", PANGO_ELLIPSIZE_END, "font-desc", pango_font_description, NULL); g_signal_connect (rend, "edited", (GCallback)cell_edited_callback, data); gtk_tree_view_column_pack_start (col, rend, TRUE); gtk_tree_view_column_set_attributes (col, rend, "text", FC_LABEL, NULL); gtk_tree_view_column_set_sort_column_id (col, FC_LABEL); gtk_tree_view_append_column (tree_view, col); /* add "size" column */ title = _("Size"); rend = gtk_cell_renderer_text_new (); g_object_set (rend, "alignment", PANGO_ALIGN_RIGHT, "font-desc", pango_font_description, "xpad", GUI_PAD, "xalign", 1.0f, "yalign", 0.5f, NULL); col = gtk_tree_view_column_new_with_attributes (title, rend, NULL); gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_GROW_ONLY); gtk_tree_view_column_set_sort_column_id (col, FC_SIZE); gtk_tree_view_column_set_attributes (col, rend, "text", FC_SIZE_STR, NULL); gtk_tree_view_append_column (tree_view, col); /* add "progress" column */ title = _("Have"); pango_layout = gtk_widget_create_pango_layout (view, title); pango_layout_get_pixel_size (pango_layout, &width, NULL); width += 30; /* room for the sort indicator */ g_object_unref (G_OBJECT (pango_layout)); rend = gtk_cell_renderer_progress_new (); col = gtk_tree_view_column_new_with_attributes (title, rend, "value", FC_PROG, NULL); gtk_tree_view_column_set_fixed_width (col, width); gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_sort_column_id (col, FC_PROG); gtk_tree_view_append_column (tree_view, col); /* add "enabled" column */ title = _("Download"); pango_layout = gtk_widget_create_pango_layout (view, title); pango_layout_get_pixel_size (pango_layout, &width, NULL); width += 30; /* room for the sort indicator */ g_object_unref (G_OBJECT (pango_layout)); rend = gtk_cell_renderer_toggle_new (); col = gtk_tree_view_column_new_with_attributes (title, rend, NULL); g_object_set_data (G_OBJECT (col), TR_COLUMN_ID_KEY, GINT_TO_POINTER (FC_ENABLED)); gtk_tree_view_column_set_fixed_width (col, width); gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_cell_data_func (col, rend, renderDownload, NULL, NULL); gtk_tree_view_column_set_sort_column_id (col, FC_ENABLED); gtk_tree_view_append_column (tree_view, col); /* add priority column */ title = _("Priority"); pango_layout = gtk_widget_create_pango_layout (view, title); pango_layout_get_pixel_size (pango_layout, &width, NULL); width += 30; /* room for the sort indicator */ g_object_unref (G_OBJECT (pango_layout)); rend = gtk_cell_renderer_text_new (); g_object_set (rend, "xalign", (gfloat)0.5, "yalign", (gfloat)0.5, NULL); col = gtk_tree_view_column_new_with_attributes (title, rend, NULL); g_object_set_data (G_OBJECT (col), TR_COLUMN_ID_KEY, GINT_TO_POINTER (FC_PRIORITY)); gtk_tree_view_column_set_fixed_width (col, width); gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_sort_column_id (col, FC_PRIORITY); gtk_tree_view_column_set_cell_data_func (col, rend, renderPriority, NULL, NULL); gtk_tree_view_append_column (tree_view, col); /* add tooltip to tree */ gtk_tree_view_set_tooltip_column (tree_view, FC_LABEL_ESC); /* 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_AUTOMATIC, 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_size_request (scroll, -1, 200); ret = scroll; data->view = view; data->top = scroll; g_object_set_data_full (G_OBJECT (ret), "file-data", data, freeData); gtr_file_list_set_torrent (ret, torrentId); pango_font_description_free (pango_font_description); return ret; }