mirror of
https://github.com/transmission/transmission
synced 2024-12-24 16:52:39 +00:00
24b9dc7dc2
Looking over the 2.20 tickets, this one keeps sticking out. A popup menu may be useful if the "delete files" ticket is accepted in 2.30, but for this release (2.20) it doesn't really add anything new. One lesson from the 2.1x cycle was that features are a harder to remove than to add. So I'm going to hold off on adding this feature until it has a clear raison d'être.
1053 lines
35 KiB
C
1053 lines
35 KiB
C
/*
|
|
* 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 <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <glib/gi18n.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
|
|
#include "file-list.h"
|
|
#include "hig.h"
|
|
#include "icons.h"
|
|
#include "tr-prefs.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_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 );
|
|
|
|
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_get_iter_first( model, &iter ) ) 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 = NULL;
|
|
tr_session * session = tr_core_session( data->core );
|
|
|
|
if( session != NULL )
|
|
tor = tr_torrentFindFromId( session, 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 = tr_torrentFindFromId( session, data->torrentId );
|
|
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 TRUE;
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
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;
|
|
|
|
tr_strlsize( size_str, child_data->length, sizeof size_str );
|
|
|
|
#if GTK_CHECK_VERSION(2,10,0)
|
|
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_SIZE, child_data->length,
|
|
FC_SIZE_STR, size_str,
|
|
FC_ICON, icon,
|
|
FC_PRIORITY, priority,
|
|
FC_ENABLED, enabled,
|
|
-1 );
|
|
#else
|
|
gtk_tree_store_append( build->store, &child_iter, build->iter );
|
|
gtk_tree_store_set( build->store, &child_iter,
|
|
FC_INDEX, child_data->index,
|
|
FC_LABEL, child_data->name,
|
|
FC_SIZE, child_data->length,
|
|
FC_SIZE_STR, size_str,
|
|
FC_ICON, icon,
|
|
FC_PRIORITY, priority,
|
|
FC_ENABLED, enabled,
|
|
-1 );
|
|
#endif
|
|
|
|
if( !isLeaf )
|
|
{
|
|
struct build_data b = *build;
|
|
b.iter = &child_iter;
|
|
g_node_children_foreach( node, G_TRAVERSE_ALL, buildTree, &b );
|
|
}
|
|
|
|
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 ) && !strcmp( 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_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_session * session = tr_core_session( data->core );
|
|
tr_torrent * tor = tr_torrentFindFromId( session, 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( inf->name );
|
|
root_data->index = -1;
|
|
root_data->length = 0;
|
|
root = g_node_new( root_data );
|
|
for( i=0; i<inf->fileCount; ++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 = gtr_timeout_add_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 ) );
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
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 = tr_torrentFindFromId( tr_core_session( 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;
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
fileMenuSetDownload( GtkWidget * item, gpointer gdata )
|
|
{
|
|
tr_torrent * tor;
|
|
FileData * data = gdata;
|
|
|
|
if(( tor = tr_torrentFindFromId( tr_core_session( data->core ), data->torrentId )))
|
|
{
|
|
const tr_bool download_flag = g_object_get_data( G_OBJECT( item ), TR_DOWNLOAD_KEY ) != NULL;
|
|
GArray * indices = getSelectedFilesAndDescendants( GTK_TREE_VIEW( data->view ) );
|
|
|
|
tr_torrentSetFileDLs( tor,
|
|
(tr_file_index_t *) indices->data,
|
|
(tr_file_index_t) indices->len,
|
|
download_flag );
|
|
|
|
refresh( data );
|
|
g_array_free( indices, TRUE );
|
|
}
|
|
}
|
|
|
|
static void
|
|
fileMenuSetPriority( GtkWidget * item, gpointer gdata )
|
|
{
|
|
tr_torrent * tor;
|
|
FileData * data = gdata;
|
|
|
|
if(( tor = tr_torrentFindFromId( tr_core_session( data->core ), data->torrentId )))
|
|
{
|
|
const int priority = GPOINTER_TO_INT( g_object_get_data( G_OBJECT( item ), TR_PRIORITY_KEY ) );
|
|
GArray * indices = getSelectedFilesAndDescendants( GTK_TREE_VIEW( data->view ) );
|
|
tr_torrentSetFilePriorities( tor,
|
|
(tr_file_index_t *) indices->data,
|
|
(tr_file_index_t) indices->len,
|
|
priority );
|
|
refresh( data );
|
|
g_array_free( indices, TRUE );
|
|
}
|
|
}
|
|
|
|
static void
|
|
fileMenuPopup( GtkWidget * w, GdkEventButton * event, gpointer filedata )
|
|
{
|
|
GtkWidget * item;
|
|
GtkWidget * menu;
|
|
|
|
menu = gtk_menu_new( );
|
|
item = gtk_menu_item_new_with_label( _( "Set Priority High" ) );
|
|
g_object_set_data( G_OBJECT( item ), TR_PRIORITY_KEY,
|
|
GINT_TO_POINTER( TR_PRI_HIGH ) );
|
|
g_signal_connect( item, "activate",
|
|
G_CALLBACK( fileMenuSetPriority ), filedata );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
item = gtk_menu_item_new_with_label( _( "Set Priority Normal" ) );
|
|
g_object_set_data( G_OBJECT( item ), TR_PRIORITY_KEY,
|
|
GINT_TO_POINTER( TR_PRI_NORMAL ) );
|
|
g_signal_connect( item, "activate",
|
|
G_CALLBACK( fileMenuSetPriority ), filedata );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
item = gtk_menu_item_new_with_label( _( "Set Priority Low" ) );
|
|
g_object_set_data( G_OBJECT( item ), TR_PRIORITY_KEY,
|
|
GINT_TO_POINTER( TR_PRI_LOW ) );
|
|
g_signal_connect( item, "activate",
|
|
G_CALLBACK( fileMenuSetPriority ), filedata );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
item = gtk_separator_menu_item_new( );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
item = gtk_menu_item_new_with_label( _( "Download" ) );
|
|
g_object_set_data( G_OBJECT( item ), TR_DOWNLOAD_KEY,
|
|
GINT_TO_POINTER( TRUE ) );
|
|
g_signal_connect( item, "activate",
|
|
G_CALLBACK( fileMenuSetDownload ), filedata );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
item = gtk_menu_item_new_with_label( _( "Do Not Download" ) );
|
|
g_object_set_data( G_OBJECT( item ), TR_DOWNLOAD_KEY,
|
|
GINT_TO_POINTER( FALSE ) );
|
|
g_signal_connect( item, "activate",
|
|
G_CALLBACK( fileMenuSetDownload ), filedata );
|
|
gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item );
|
|
|
|
gtk_widget_show_all( menu );
|
|
gtk_menu_attach_to_widget( GTK_MENU( menu ), w, NULL );
|
|
gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL,
|
|
event ? event->button : 0,
|
|
event ? event->time : gtk_get_current_event_time( ) );
|
|
}
|
|
#endif
|
|
|
|
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 = tr_torrentFindFromId( tr_core_session( 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 )
|
|
{
|
|
tr_torrent * tor;
|
|
GtkTreeViewColumn * col = NULL;
|
|
GtkTreePath * path = NULL;
|
|
FileData * data = gdata;
|
|
gboolean handled = FALSE;
|
|
GtkTreeView * treeview = GTK_TREE_VIEW( w );
|
|
|
|
tor = tr_torrentFindFromId( tr_core_session( data->core ),
|
|
data->torrentId );
|
|
if( tor == NULL )
|
|
return FALSE;
|
|
|
|
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 0
|
|
else if( event->type == GDK_BUTTON_PRESS && event->button == 3
|
|
&& getAndSelectEventPath( treeview, event, &col, &path ) )
|
|
{
|
|
GtkTreeSelection * sel = gtk_tree_view_get_selection( treeview );
|
|
if( gtk_tree_selection_count_selected_rows( sel ) > 0 )
|
|
{
|
|
fileMenuPopup( w, event, data );
|
|
handled = TRUE;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
gtk_tree_path_free( path );
|
|
return handled;
|
|
}
|
|
|
|
#if 0
|
|
static gboolean
|
|
onViewPopupMenu( GtkWidget * w, gpointer gdata )
|
|
{
|
|
fileMenuPopup( w, NULL, gdata );
|
|
return TRUE;
|
|
}
|
|
#endif
|
|
|
|
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 );
|
|
#if 0
|
|
g_signal_connect( view, "popup-menu",
|
|
G_CALLBACK( onViewPopupMenu ), data );
|
|
#endif
|
|
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, "ellipsize", PANGO_ELLIPSIZE_END, "font-desc", pango_font_description, NULL );
|
|
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 );
|
|
|
|
/* 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;
|
|
}
|