/* * transmission-remote-gtk - A GTK RPC client to Transmission * Copyright (C) 2011-2013 Alan Fitton * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include "protocol-constants.h" #include "trg-files-model-common.h" #include "trg-files-tree-view-common.h" #include "trg-files-tree.h" #include "trg-files-model.h" #include "trg-client.h" #include "torrent.h" #include "util.h" #include "trg-files-model.h" G_DEFINE_TYPE(TrgFilesModel, trg_files_model, GTK_TYPE_TREE_STORE) #define TRG_FILES_MODEL_GET_PRIVATE(o) \ (G_TYPE_INSTANCE_GET_PRIVATE ((o), TRG_TYPE_FILES_MODEL, TrgFilesModelPrivate)) typedef struct _TrgFilesModelPrivate TrgFilesModelPrivate; struct _TrgFilesModelPrivate { gint64 torrentId; guint n_items; gboolean accept; }; /* Push a given increment to a treemodel node and its parents. * Used for updating bytescompleted. * This is only used for user interaction, the initial population is done * in a simple tree before for performance. * */ static void trg_files_update_parent_progress(GtkTreeModel * model, GtkTreeIter * iter, gint64 increment) { GtkTreeIter back_iter = *iter; if (increment < 1) return; while (1) { GtkTreeIter tmp_iter; gint64 lastCompleted, newCompleted, length; if (!gtk_tree_model_iter_parent(model, &tmp_iter, &back_iter)) break; gtk_tree_model_get(model, &tmp_iter, FILESCOL_BYTESCOMPLETED, &lastCompleted, FILESCOL_SIZE, &length, -1); newCompleted = lastCompleted + increment; gtk_tree_store_set(GTK_TREE_STORE(model), &tmp_iter, FILESCOL_PROGRESS, file_get_progress(length, newCompleted), FILESCOL_BYTESCOMPLETED, newCompleted, -1); back_iter = tmp_iter; } } /* Update the bytesCompleted and size for a nodes parents, and also figure out * if a priority/enabled change requires updating the parents (needs to iterate * over the other nodes at its level). * * It's faster doing it in here than when it's in the model. */ static void trg_files_tree_update_ancestors(trg_files_tree_node * node) { trg_files_tree_node *back_iter = node; gint pri_result = node->priority; gint enabled_result = node->enabled; while ((back_iter = back_iter->parent)) { GList *li; for (li = back_iter->children; li; li = g_list_next(li)) { trg_files_tree_node *back_node = (trg_files_tree_node *) li->data; gint common_result = 0; if (back_node->priority != pri_result) common_result = pri_result = TR_PRI_MIXED; if (back_node->enabled != enabled_result) common_result = enabled_result = TR_PRI_MIXED; if (common_result == TR_PRI_MIXED) break; } back_iter->bytesCompleted += node->bytesCompleted; back_iter->length += node->length; back_iter->priority = pri_result; back_iter->enabled = enabled_result; } } static void store_add_node(GtkTreeStore * store, GtkTreeIter * parent, trg_files_tree_node * node) { GtkTreeIter child; GList *li; if (node->name) { gdouble progress = file_get_progress(node->length, node->bytesCompleted); gtk_tree_store_insert_with_values(store, &child, parent, INT_MAX, FILESCOL_WANTED, node->enabled, FILESCOL_PROGRESS, progress, FILESCOL_SIZE, node->length, FILESCOL_ID, node->index, FILESCOL_PRIORITY, node->priority, FILESCOL_NAME, node->name, -1); } for (li = node->children; li; li = g_list_next(li)) store_add_node(store, node->name ? &child : NULL, (trg_files_tree_node *) li->data); } static trg_files_tree_node *trg_file_parser_node_insert(trg_files_tree_node * top, trg_files_tree_node * last, JsonObject * file, gint index, JsonArray * enabled, JsonArray * priorities) { gchar **path = g_strsplit(file_get_name(file), "/", -1); trg_files_tree_node *lastIter = last; GList *parentList = NULL; gchar *path_el; GList *li; int i; /* Build up a list of pointers to each parent trg_files_tree_node * reversing the order as it iterates over its parent. */ if (lastIter) while ((lastIter = lastIter->parent)) parentList = g_list_prepend(parentList, lastIter); li = parentList; lastIter = top; /* Iterate over the path list which contains each file/directory * component of the path in order. */ for (i = 0; (path_el = path[i]); i++) { gboolean isFile = !path[i + 1]; trg_files_tree_node *target_node = NULL; /* No point checking for files. If there is a last parents iterator * check it for a shortcut. I'm assuming that these come in order of * directory at least to give performance a boost. */ if (li && !isFile) { trg_files_tree_node *lastPathNode = (trg_files_tree_node *) li->data; if (!g_strcmp0(lastPathNode->name, path[i])) { target_node = lastPathNode; li = g_list_next(li); } else { /* No need to check any further. */ li = NULL; } } if (!target_node && lastIter && lastIter->childrenHash && !isFile) target_node = g_hash_table_lookup(lastIter->childrenHash, path_el); /* Node needs creating */ if (!target_node) { target_node = g_new0(trg_files_tree_node, 1); target_node->name = g_strdup(path[i]); target_node->parent = lastIter; trg_files_tree_node_add_child(lastIter, target_node); } lastIter = target_node; /* Files have more properties set here than for files. * Directories are updated from here too, by trg_files_tree_update_ancestors * working up the parents. */ if (isFile) { target_node->length = file_get_length(file); target_node->bytesCompleted = file_get_bytes_completed(file); target_node->index = index; target_node->enabled = (gint) json_array_get_int_element(enabled, index); target_node->priority = (gint) json_array_get_int_element(priorities, index); trg_files_tree_update_ancestors(target_node); } else { target_node->index = -1; } } g_list_free(parentList); g_strfreev(path); return lastIter; } void trg_files_model_set_accept(TrgFilesModel * model, gboolean accept) { TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(model); priv->accept = accept; } static void trg_files_model_iter_update(TrgFilesModel * model, GtkTreeIter * filesIter, JsonObject * file, JsonArray * wantedArray, JsonArray * prioritiesArray, gint id) { TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(model); gint64 fileLength = file_get_length(file); gint64 fileCompleted = file_get_bytes_completed(file); gint64 lastCompleted; gint wanted = (gint) json_array_get_int_element(wantedArray, id); gint priority = (gint) json_array_get_int_element(prioritiesArray, id); gdouble progress = file_get_progress(fileLength, fileCompleted); gtk_tree_model_get(GTK_TREE_MODEL(model), filesIter, FILESCOL_BYTESCOMPLETED, &lastCompleted, -1); gtk_tree_store_set(GTK_TREE_STORE(model), filesIter, FILESCOL_PROGRESS, progress, FILESCOL_BYTESCOMPLETED, fileCompleted, -1); trg_files_update_parent_progress(GTK_TREE_MODEL(model), filesIter, fileCompleted - lastCompleted); if (priv->accept) gtk_tree_store_set(GTK_TREE_STORE(model), filesIter, FILESCOL_WANTED, wanted, FILESCOL_PRIORITY, priority, -1); } static void trg_files_model_class_init(TrgFilesModelClass * klass) { g_type_class_add_private(klass, sizeof(TrgFilesModelPrivate)); } static void trg_files_model_init(TrgFilesModel * self) { TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(self); GType column_types[FILESCOL_COLUMNS]; priv->accept = TRUE; column_types[FILESCOL_NAME] = G_TYPE_STRING; column_types[FILESCOL_SIZE] = G_TYPE_INT64; column_types[FILESCOL_PROGRESS] = G_TYPE_DOUBLE; column_types[FILESCOL_ID] = G_TYPE_INT; column_types[FILESCOL_WANTED] = G_TYPE_INT; column_types[FILESCOL_PRIORITY] = G_TYPE_INT; column_types[FILESCOL_BYTESCOMPLETED] = G_TYPE_INT64; gtk_tree_store_set_column_types(GTK_TREE_STORE(self), FILESCOL_COLUMNS, column_types); } struct MinorUpdateData { GList *filesList; JsonArray *priorities; JsonArray *wanted; }; static gboolean trg_files_model_update_foreach(GtkListStore * model, GtkTreePath * path G_GNUC_UNUSED, GtkTreeIter * iter, gpointer data) { struct MinorUpdateData *mud = (struct MinorUpdateData *) data; JsonObject *file; gint id; gtk_tree_model_get(GTK_TREE_MODEL(model), iter, FILESCOL_ID, &id, -1); if (id >= 0) { file = json_node_get_object(g_list_nth_data(mud->filesList, id)); trg_files_model_iter_update(TRG_FILES_MODEL(model), iter, file, mud->wanted, mud->priorities, id); } return FALSE; } struct FirstUpdateThreadData { TrgFilesModel *model; GtkTreeView *tree_view; JsonArray *files; JsonArray *priorities; JsonArray *wanted; guint n_items; trg_files_tree_node *top_node; gint64 torrent_id; GList *filesList; gboolean idle_add; }; static gboolean trg_files_model_applytree_idlefunc(gpointer data) { struct FirstUpdateThreadData *args = (struct FirstUpdateThreadData *) data; TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(args->model); if (args->torrent_id == priv->torrentId) { store_add_node(GTK_TREE_STORE(args->model), NULL, args->top_node); gtk_tree_view_expand_all(args->tree_view); priv->n_items = args->n_items; priv->accept = TRUE; } trg_files_tree_node_free(args->top_node); g_free(data); return FALSE; } static gpointer trg_files_model_buildtree_threadfunc(gpointer data) { struct FirstUpdateThreadData *args = (struct FirstUpdateThreadData *) data; trg_files_tree_node *lastNode = NULL; GList *li; args->top_node = g_new0(trg_files_tree_node, 1); for (li = args->filesList; li; li = g_list_next(li)) { JsonObject *file = json_node_get_object((JsonNode *) li->data); lastNode = trg_file_parser_node_insert(args->top_node, lastNode, file, args->n_items++, args->wanted, args->priorities); } g_list_free(args->filesList); json_array_unref(args->files); if (args->idle_add) g_idle_add(trg_files_model_applytree_idlefunc, data); return NULL; } void trg_files_model_update(TrgFilesModel * model, GtkTreeView * tv, gint64 updateSerial, JsonObject * t, gint mode) { TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(model); JsonArray *files = torrent_get_files(t); GList *filesList = json_array_get_elements(files); guint filesListLength = g_list_length(filesList); JsonArray *priorities = torrent_get_priorities(t); JsonArray *wanted = torrent_get_wanted(t); priv->torrentId = torrent_get_id(t); /* It's quicker to build this up with simple data structures before * putting it into GTK models. */ if (mode == TORRENT_GET_MODE_FIRST || priv->n_items != filesListLength) { struct FirstUpdateThreadData *futd = g_new0(struct FirstUpdateThreadData, 1); gtk_tree_store_clear(GTK_TREE_STORE(model)); json_array_ref(files); futd->tree_view = tv; futd->files = files; futd->priorities = priorities; futd->wanted = wanted; futd->filesList = filesList; futd->torrent_id = priv->torrentId; futd->model = model; futd->idle_add = filesListLength > TRG_FILES_MODEL_CREATE_THREAD_IF_GT; /* If this update has more than a given number of files, build up the * simple tree in a thread, then g_idle_add a function which * adds the contents of this prebuilt tree. * * If less than or equal to, I don't think it's worth spawning threads * for. Just do it in the main loop. */ if (futd->idle_add) { g_thread_create(trg_files_model_buildtree_threadfunc, futd, FALSE, NULL); } else { trg_files_model_buildtree_threadfunc(futd); trg_files_model_applytree_idlefunc(futd); } } else { struct MinorUpdateData mud; mud.priorities = priorities; mud.wanted = wanted; mud.filesList = filesList; gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) trg_files_model_update_foreach, &mud); g_list_free(filesList); } } gint64 trg_files_model_get_torrent_id(TrgFilesModel * model) { TrgFilesModelPrivate *priv = TRG_FILES_MODEL_GET_PRIVATE(model); return priv->torrentId; } TrgFilesModel *trg_files_model_new(void) { return g_object_new(TRG_TYPE_FILES_MODEL, NULL); }