mirror of
https://github.com/transmission/transmission
synced 2024-12-27 18:18:10 +00:00
928 lines
31 KiB
C
928 lines
31 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 <string.h>
|
|
#include <glib/gi18n.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/utils.h>
|
|
|
|
#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_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 = 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 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_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; 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 ) );
|
|
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;
|
|
}
|
|
|
|
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, "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;
|
|
}
|