From 2aa1e69f98ebdece7d691bd8b5c500e39c5ab8c3 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 14 Mar 2010 05:13:02 +0000 Subject: [PATCH] (trunk gtk) first draft of a filterbar experiment --- gtk/Makefile.am | 2 + gtk/details.c | 1 + gtk/filter.c | 996 ++++++++++++++++++++++++++++++++++++++++++++++++ gtk/filter.h | 20 + gtk/tr-core.c | 26 ++ gtk/tr-core.h | 2 + gtk/tr-window.c | 319 +--------------- 7 files changed, 1054 insertions(+), 312 deletions(-) create mode 100644 gtk/filter.c create mode 100644 gtk/filter.h diff --git a/gtk/Makefile.am b/gtk/Makefile.am index a4f2cf74d..25b8a5c2b 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -42,6 +42,7 @@ noinst_HEADERS = \ conf.h \ details.h \ dialogs.h \ + filter.h \ hig.h \ icons.h \ file-list.h \ @@ -79,6 +80,7 @@ transmission_SOURCES = \ details.c \ dialogs.c \ file-list.c \ + filter.c \ hig.c \ icons.c \ main.c \ diff --git a/gtk/details.c b/gtk/details.c index e5108b70c..5872a7190 100644 --- a/gtk/details.c +++ b/gtk/details.c @@ -2097,6 +2097,7 @@ onEditTrackersResponse( GtkDialog * dialog, int response, gpointer data ) { di->trackers = NULL; di->tracker_buffer = NULL; + tr_core_torrent_changed( di->core, tr_torrentId( tor ) ); } /* cleanup */ diff --git a/gtk/filter.c b/gtk/filter.c new file mode 100644 index 000000000..5a98f2ccc --- /dev/null +++ b/gtk/filter.c @@ -0,0 +1,996 @@ +/* + * This file Copyright (C) 2010 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 "filter.h" +#include "hig.h" /* GUI_PAD */ +#include "tr-core.h" +#include "util.h" /* gtr_idle_add() */ + +#define DIRTY_KEY "tr-filter-dirty" +#define TEXT_KEY "tr-filter-text" +#define TEXT_MODE_KEY "tr-filter-text-mode" +#define TORRENT_MODEL_KEY "tr-filter-torrent-model-key" + +#if !GTK_CHECK_VERSION( 2,16,0 ) + /* FIXME: when 2.16 has been out long enough, it would be really nice to + * get rid of this libsexy usage because of its makefile strangeness */ + #define USE_SEXY + #include "sexy-icon-entry.h" +#endif + +/*** +**** +**** CATEGORIES +**** +***/ + +enum +{ + CAT_FILTER_TYPE_ALL, + CAT_FILTER_TYPE_PRIVATE, + CAT_FILTER_TYPE_PUBLIC, + CAT_FILTER_TYPE_HOST, + CAT_FILTER_TYPE_PARENT, + CAT_FILTER_TYPE_PRI_HIGH, + CAT_FILTER_TYPE_PRI_NORMAL, + CAT_FILTER_TYPE_PRI_LOW, + CAT_FILTER_TYPE_TAG, + CAT_FILTER_TYPE_SEPARATOR, +}; + +enum +{ + CAT_FILTER_COL_NAME, /* human-readable name; ie, Legaltorrents */ + CAT_FILTER_COL_COUNT, /* how many matches there are */ + CAT_FILTER_COL_TYPE, + CAT_FILTER_COL_HOST, /* pattern-matching text; ie, legaltorrents.com */ + CAT_FILTER_N_COLS +}; + +static int +pstrcmp( const void * a, const void * b ) +{ + return strcmp( *(const char**)a, *(const char**)b ); +} + +static char* +get_host_from_url( const char * url ) +{ + char * h = NULL; + char * name; + const char * first_dot; + const char * last_dot; + + tr_urlParse( url, -1, NULL, &h, NULL, NULL ); + first_dot = strchr( h, '.' ); + last_dot = strrchr( h, '.' ); + + if( ( first_dot ) && ( last_dot ) && ( first_dot != last_dot ) ) + name = g_strdup( first_dot + 1 ); + else + name = g_strdup( h ); + + tr_free( h ); + return name; +} + +static char* +get_name_from_host( const char * host ) +{ + char * name; + const char * dot = strrchr( host, '.' ); + + if( dot == NULL ) + name = g_strdup( host ); + else + name = g_strndup( host, dot - host ); + + *name = g_ascii_toupper( *name ); + + return name; +} + +static void +category_model_update_count( GtkTreeStore * store, GtkTreeIter * iter, int n ) +{ + int count; + GtkTreeModel * model = GTK_TREE_MODEL( store ); + gtk_tree_model_get( model, iter, CAT_FILTER_COL_COUNT, &count, -1 ); + if( n != count ) + gtk_tree_store_set( store, iter, CAT_FILTER_COL_COUNT, n, -1 ); +} + +static gboolean +category_filter_model_update( GtkTreeStore * store ) +{ + int i, n; + int low = 0; + int all = 0; + int high = 0; + int public = 0; + int normal = 0; + int private = 0; + int store_pos; + GtkTreeIter iter; + GtkTreeIter parent; + GtkTreeModel * model = GTK_TREE_MODEL( store ); + GPtrArray * hosts = g_ptr_array_new( ); + GHashTable * hosts_hash = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, g_free ); + GObject * o = G_OBJECT( store ); + GtkTreeModel * tmodel = GTK_TREE_MODEL( + g_object_get_data( o, TORRENT_MODEL_KEY ) ); + + g_object_steal_data( o, DIRTY_KEY ); + + /* walk through all the torrents, tallying how many matches there are + * for the various categories. also make a sorted list of all tracker + * hosts s.t. we can merge it with the existing list */ + if( gtk_tree_model_get_iter_first( tmodel, &iter )) do + { + tr_torrent * tor; + const tr_info * inf; + int keyCount; + char ** keys; + + gtk_tree_model_get( tmodel, &iter, MC_TORRENT_RAW, &tor, -1 ); + inf = tr_torrentInfo( tor ); + keyCount = 0; + keys = g_new( char*, inf->trackerCount ); + + for( i=0, n=inf->trackerCount; itrackers[i].announce ); + int * count = g_hash_table_lookup( hosts_hash, key ); + if( count == NULL ) + { + char * k = g_strdup( key ); + count = tr_new0( int, 1 ); + g_hash_table_insert( hosts_hash, k, count ); + g_ptr_array_add( hosts, k ); + } + + for( k=0; kisPrivate ) + ++private; + else + ++public; + + switch( tr_torrentGetPriority( tor ) ) + { + case TR_PRI_HIGH: ++high; break; + case TR_PRI_LOW: ++low; break; + default: ++normal; break; + } + } + while( gtk_tree_model_iter_next( tmodel, &iter ) ); + qsort( hosts->pdata, hosts->len, sizeof(char*), pstrcmp ); + + /* update the "all" count */ + gtk_tree_model_iter_nth_child( model, &iter, NULL, 0 ); + category_model_update_count( store, &iter, all ); + + /* update the "public" count */ + gtk_tree_model_iter_nth_child( model, &parent, NULL, 2 ); + gtk_tree_model_iter_nth_child( model, &iter, &parent, 0 ); + category_model_update_count( store, &iter, public ); + gtk_tree_model_iter_nth_child( model, &iter, &parent, 1 ); + category_model_update_count( store, &iter, private ); + + /* update the "priority" subtree */ + gtk_tree_model_iter_nth_child( model, &parent, NULL, 3 ); + gtk_tree_model_iter_nth_child( model, &iter, &parent, 0 ); + category_model_update_count( store, &iter, high ); + gtk_tree_model_iter_nth_child( model, &iter, &parent, 1 ); + category_model_update_count( store, &iter, normal ); + gtk_tree_model_iter_nth_child( model, &iter, &parent, 2 ); + category_model_update_count( store, &iter, low ); + + /* update the "hosts" subtree */ + gtk_tree_model_iter_nth_child( model, &parent, NULL, 4 ); + i = 0; + n = hosts->len; + store_pos = 0; + for( ;; ) + { + const gboolean new_hosts_done = i >= n; + const gboolean old_hosts_done = !gtk_tree_model_iter_nth_child( model, &iter, &parent, store_pos ); + gboolean remove_row = FALSE; + gboolean insert_row = FALSE; + + /* are we done yet? */ + if( new_hosts_done && old_hosts_done ) + break; + + /* decide what to do */ + if( new_hosts_done ) { + /* g_message( "new hosts done; remove row" ); */ + remove_row = TRUE; + } else if( old_hosts_done ) { + /* g_message( "old hosts done; insert row" ); */ + insert_row = TRUE; + } else { + int cmp; + char * host; + gtk_tree_model_get( model, &iter, CAT_FILTER_COL_HOST, &host, -1 ); + cmp = strcmp( host, hosts->pdata[i] ); + /* g_message( "cmp( %s, %s ) returns %d", host, (char*)hosts->pdata[i], cmp ); */ + if( cmp < 0 ) { + /* g_message( "cmp<0, so remove row" ); */ + remove_row = TRUE; + } else if( cmp > 0 ) { + /* g_message( "cmp>0, so insert row" ); */ + insert_row = TRUE; + } + g_free( host ); + } + + /* do something */ + if( remove_row ) { + /* g_message( "removing row and incrementing i" ); */ + gtk_tree_store_remove( store, &iter ); + } else if( insert_row ) { + const char * host = hosts->pdata[i]; + char * name = get_name_from_host( host ); + const int count = *(int*)g_hash_table_lookup( hosts_hash, host ); + gtk_tree_store_insert_with_values( store, NULL, &parent, store_pos, + CAT_FILTER_COL_HOST, host, + CAT_FILTER_COL_NAME, name, + CAT_FILTER_COL_COUNT, count, + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_HOST, + -1 ); + g_free( name ); + ++store_pos; + ++i; + } else { /* update row */ + const char * host = hosts->pdata[i]; + const int count = *(int*)g_hash_table_lookup( hosts_hash, host ); + category_model_update_count( store, &iter, count ); + ++store_pos; + ++i; + } + } + + /* cleanup */ + g_ptr_array_unref( hosts ); + g_hash_table_unref( hosts_hash ); + g_ptr_array_foreach( hosts, (GFunc)g_free, NULL ); + return FALSE; +} + +static GtkTreeModel * +category_filter_model_new( GtkTreeModel * tmodel ) +{ + GtkTreeIter iter; + GtkTreeStore * store; + const int invisible_number = -1; /* doesn't get rendered */ + + store = gtk_tree_store_new( CAT_FILTER_N_COLS, + G_TYPE_STRING, + G_TYPE_INT, + G_TYPE_INT, + G_TYPE_STRING ); + + gtk_tree_store_insert_with_values( store, NULL, NULL, -1, + CAT_FILTER_COL_NAME, _( "All" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_ALL, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, NULL, -1, + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_SEPARATOR, + -1 ); + + gtk_tree_store_insert_with_values( store, &iter, NULL, -1, + CAT_FILTER_COL_NAME, _( "Privacy" ), + CAT_FILTER_COL_COUNT, invisible_number, + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, &iter, -1, + CAT_FILTER_COL_NAME, _( "Public" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PUBLIC, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, &iter, -1, + CAT_FILTER_COL_NAME, _( "Private" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRIVATE, + -1 ); + + gtk_tree_store_insert_with_values( store, &iter, NULL, -1, + CAT_FILTER_COL_NAME, _( "Priority" ), + CAT_FILTER_COL_COUNT, invisible_number, + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, &iter, -1, + CAT_FILTER_COL_NAME, _( "High" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_HIGH, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, &iter, -1, + CAT_FILTER_COL_NAME, _( "Normal" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_NORMAL, + -1 ); + gtk_tree_store_insert_with_values( store, NULL, &iter, -1, + CAT_FILTER_COL_NAME, _( "Low" ), + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_LOW, + -1 ); + + gtk_tree_store_insert_with_values( store, &iter, NULL, -1, + CAT_FILTER_COL_NAME, _( "Trackers" ), + CAT_FILTER_COL_COUNT, invisible_number, + CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT, + -1 ); + + g_object_set_data( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel ); + category_filter_model_update( store ); + return GTK_TREE_MODEL( store ); +} + +static gboolean +is_it_a_separator( GtkTreeModel * model, GtkTreeIter * iter, gpointer data UNUSED ) +{ + int type; + gtk_tree_model_get( model, iter, CAT_FILTER_COL_TYPE, &type, -1 ); + return type == CAT_FILTER_TYPE_SEPARATOR; +} + +static void +category_model_update_idle( gpointer category_model ) +{ + GObject * o = G_OBJECT( category_model ); + gboolean pending = GPOINTER_TO_INT( g_object_get_data( o, DIRTY_KEY ) ); + if( !pending ) + { + GSourceFunc func = (GSourceFunc) category_filter_model_update; + g_object_set_data( o, DIRTY_KEY, GINT_TO_POINTER(1) ); + gtr_idle_add( func, category_model ); + } +} + +static void +torrent_model_row_changed( GtkTreeModel * tmodel UNUSED, + GtkTreePath * path UNUSED, + GtkTreeIter * iter UNUSED, + gpointer category_model ) +{ + category_model_update_idle( category_model ); +} + +static void +torrent_model_row_deleted_cb( GtkTreeModel * tmodel UNUSED, + GtkTreePath * path UNUSED, + gpointer category_model ) +{ + category_model_update_idle( category_model ); +} + +static void +render_hit_count_func( GtkCellLayout * cell_layout UNUSED, + GtkCellRenderer * cell_renderer, + GtkTreeModel * tree_model, + GtkTreeIter * iter, + gpointer data UNUSED ) +{ + int count; + char buf[512]; + + gtk_tree_model_get( tree_model, iter, CAT_FILTER_COL_COUNT, &count, -1 ); + + if( count >= 0 ) + g_snprintf( buf, sizeof( buf ), "%'d", count ); + else + *buf = '\0'; + + g_object_set( cell_renderer, "text", buf, NULL ); +} + +static GtkCellRenderer * +number_renderer_new( void ) +{ + GtkCellRenderer * r = gtk_cell_renderer_text_new( ); + + g_object_set( G_OBJECT( r ), "alignment", PANGO_ALIGN_RIGHT, + "weight", PANGO_WEIGHT_ULTRALIGHT, + "xalign", 1.0, + "xpad", GUI_PAD, + NULL ); + + return r; +} + +static GtkWidget * +category_combo_box_new( GtkTreeModel * tmodel ) +{ + GtkWidget * c; + GtkCellRenderer * r; + GtkTreeModel * category_model; + + /* create the category combobox */ + category_model = category_filter_model_new( tmodel ); + c = gtk_combo_box_new_with_model( category_model ); + gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ), + is_it_a_separator, NULL, NULL ); + gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 ); + + r = gtk_cell_renderer_text_new( ); + gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, FALSE ); + gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r, + "text", CAT_FILTER_COL_NAME, + NULL ); + + r = number_renderer_new( ); + gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE ); + gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_hit_count_func, NULL, NULL ); + + g_signal_connect( tmodel, "row-changed", + G_CALLBACK( torrent_model_row_changed ), category_model ); + g_signal_connect( tmodel, "row-inserted", + G_CALLBACK( torrent_model_row_changed ), category_model ); + g_signal_connect( tmodel, "row-deleted", + G_CALLBACK( torrent_model_row_deleted_cb ), category_model ); + + return c; +} + +/*** +**** +***/ + +static gboolean +testCategory( GtkWidget * category_combo, tr_torrent * tor ) +{ + int type; + const tr_info * inf; + GtkTreeIter iter; + GtkComboBox * combo = GTK_COMBO_BOX( category_combo ); + GtkTreeModel * model = gtk_combo_box_get_model( combo ); + + if( !gtk_combo_box_get_active_iter( combo, &iter ) ) + return TRUE; + + inf = tr_torrentInfo( tor ); + gtk_tree_model_get( model, &iter, CAT_FILTER_COL_TYPE, &type, -1 ); + switch( type ) + { + case CAT_FILTER_TYPE_ALL: + return TRUE; + + case CAT_FILTER_TYPE_PRIVATE: + return inf->isPrivate; + + case CAT_FILTER_TYPE_PUBLIC: + return !inf->isPrivate; + + case CAT_FILTER_TYPE_PRI_HIGH: + return tr_torrentGetPriority( tor ) == TR_PRI_HIGH; + + case CAT_FILTER_TYPE_PRI_NORMAL: + return tr_torrentGetPriority( tor ) == TR_PRI_NORMAL; + + case CAT_FILTER_TYPE_PRI_LOW: + return tr_torrentGetPriority( tor ) == TR_PRI_LOW; + + case CAT_FILTER_TYPE_HOST: { + int i; + char * host; + gtk_tree_model_get( model, &iter, CAT_FILTER_COL_HOST, &host, -1 ); + for( i=0; itrackerCount; ++i ) { + char * tmp = get_host_from_url( inf->trackers[i].announce ); + const gboolean hit = !strcmp( tmp, host ); + g_free( tmp ); + if( hit ) + break; + } + g_free( host ); + return i < inf->trackerCount; + } + + case CAT_FILTER_TYPE_TAG: + default: + g_message( "FIXME" ); + return TRUE; + } +} + +/*** +**** +**** STATES +**** +***/ + +enum +{ + STATE_FILTER_ALL, + STATE_FILTER_DOWNLOADING, + STATE_FILTER_SEEDING, + STATE_FILTER_ACTIVE, + STATE_FILTER_PAUSED, + STATE_FILTER_QUEUED, + STATE_FILTER_CHECKING, + STATE_FILTER_ERROR, + STATE_FILTER_SEPARATOR +}; + +enum +{ + STATE_FILTER_COL_NAME, + STATE_FILTER_COL_COUNT, + STATE_FILTER_COL_TYPE, + STATE_FILTER_N_COLS +}; + +static gboolean +state_is_it_a_separator( GtkTreeModel * m, GtkTreeIter * i, gpointer d UNUSED ) +{ + int type; + gtk_tree_model_get( m, i, STATE_FILTER_COL_TYPE, &type, -1 ); + return type == STATE_FILTER_SEPARATOR; +} + +static gboolean +test_torrent_state( tr_torrent * tor, int type ) +{ + const tr_stat * st = tr_torrentStat( tor ); + + switch( type ) + { + case STATE_FILTER_DOWNLOADING: + return st->activity == TR_STATUS_DOWNLOAD; + + case STATE_FILTER_SEEDING: + return st->activity == TR_STATUS_SEED; + + case STATE_FILTER_ACTIVE: + return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0; + + case STATE_FILTER_PAUSED: + return st->activity == TR_STATUS_STOPPED; + + case STATE_FILTER_QUEUED: + return FALSE; + + case STATE_FILTER_CHECKING: + return ( st->activity == TR_STATUS_CHECK_WAIT ) + || ( st->activity == TR_STATUS_CHECK ); + + case STATE_FILTER_ERROR: + return st->error != 0; + + default: /* STATE_FILTER_ALL */ + return TRUE; + + } +} + +static gboolean +testState( GtkWidget * state_combo, tr_torrent * tor ) +{ + int type; + GtkTreeIter iter; + GtkComboBox * combo = GTK_COMBO_BOX( state_combo ); + GtkTreeModel * model = gtk_combo_box_get_model( combo ); + + if( !gtk_combo_box_get_active_iter( combo, &iter ) ) + return TRUE; + + gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 ); + return test_torrent_state( tor, type ); +} + +static void +status_model_update_count( GtkListStore * store, GtkTreeIter * iter, int n ) +{ + int count; + gtk_tree_model_get( GTK_TREE_MODEL( store ), iter, STATE_FILTER_COL_COUNT, &count, -1 ); + if( n != count ) + gtk_list_store_set( store, iter, STATE_FILTER_COL_COUNT, n, -1 ); +} + +static void +state_filter_model_update( GtkListStore * store ) +{ + GtkTreeIter iter; + GtkTreeModel * model = GTK_TREE_MODEL( store ); + GObject * o = G_OBJECT( store ); + GtkTreeModel * tmodel = GTK_TREE_MODEL( g_object_get_data( o, TORRENT_MODEL_KEY ) ); + + g_object_steal_data( o, DIRTY_KEY ); + + if( gtk_tree_model_get_iter_first( model, &iter )) do + { + int hits; + int type; + GtkTreeIter torrent_iter; + + gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 ); + + hits = 0; + if( gtk_tree_model_get_iter_first( tmodel, &torrent_iter )) do { + tr_torrent * tor; + gtk_tree_model_get( tmodel, &torrent_iter, MC_TORRENT_RAW, &tor, -1 ); + if( test_torrent_state( tor, type ) ) + ++hits; + } while( gtk_tree_model_iter_next( tmodel, &torrent_iter ) ); + + status_model_update_count( store, &iter, hits ); + + } while( gtk_tree_model_iter_next( model, &iter ) ); +} + +static GtkTreeModel * +state_filter_model_new( GtkTreeModel * tmodel ) +{ + int i, n; + struct { + int type; + const char * name; + } types[] = { + { STATE_FILTER_ALL, N_( "All" ) }, + { STATE_FILTER_SEPARATOR, NULL }, + { STATE_FILTER_DOWNLOADING, N_( "Downloading" ) }, + { STATE_FILTER_SEEDING, N_( "Seeding" ) }, + { STATE_FILTER_ACTIVE, N_( "Active" ) }, + { STATE_FILTER_PAUSED, N_( "Paused" ) }, + { STATE_FILTER_QUEUED, N_( "Queued" ) }, + { STATE_FILTER_CHECKING, N_( "Verifying" ) }, + { STATE_FILTER_ERROR, N_( "Error" ) } + }; + GtkListStore * store; + + store = gtk_list_store_new( STATE_FILTER_N_COLS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT ); + for( i=0, n=G_N_ELEMENTS(types); ifileCount && !ret; ++i ) { + char * pch = g_utf8_casefold( inf->files[i].name, -1 ); + ret = !key || strstr( pch, key ) != NULL; + g_free( pch ); + } + break; + + case TEXT_MODE_TRACKER: + if( inf->trackerCount > 0 ) { + char * pch = g_utf8_casefold( inf->trackers[0].announce, -1 ); + ret = !key || ( strstr( pch, key ) != NULL ); + g_free( pch ); + } + break; + + default: /* NAME */ + if( !inf->name ) + ret = TRUE; + else { + char * pch = g_utf8_casefold( inf->name, -1 ); + ret = !key || ( strstr( pch, key ) != NULL ); + g_free( pch ); + } + break; + } + + return ret; +} + + +#ifdef USE_SEXY +static void +entry_icon_released( SexyIconEntry * entry UNUSED, + SexyIconEntryPosition icon_pos, + int button UNUSED, + gpointer menu ) +{ + if( icon_pos == SEXY_ICON_ENTRY_PRIMARY ) + gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0, + gtk_get_current_event_time( ) ); +} +#else +static void +entry_icon_release( GtkEntry * entry UNUSED, + GtkEntryIconPosition icon_pos, + GdkEventButton * event UNUSED, + gpointer menu ) +{ + if( icon_pos == GTK_ENTRY_ICON_SECONDARY ) + gtk_entry_set_text( entry, "" ); + + if( icon_pos == GTK_ENTRY_ICON_PRIMARY ) + gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0, + gtk_get_current_event_time( ) ); +} +#endif + +static void +filter_entry_changed( GtkEditable * e, gpointer filter_model ) +{ + char * pch; + char * folded; + + pch = gtk_editable_get_chars( e, 0, -1 ); + folded = g_utf8_casefold( pch, -1 ); + g_object_set_data_full( filter_model, TEXT_KEY, folded, g_free ); + g_free( pch ); + + gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) ); +} + +static void +filter_text_toggled_cb( GtkCheckMenuItem * menu_item, gpointer filter_model ) +{ + g_object_set_data( filter_model, TEXT_MODE_KEY, + g_object_get_data( G_OBJECT( menu_item ), TEXT_MODE_KEY ) ); + gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) ); +} + +/***** +****** +****** +****** +*****/ + +struct filter_data +{ + GtkWidget * state; + GtkWidget * category; + GtkWidget * entry; + GtkTreeModel * filter_model; +}; + +static gboolean +is_row_visible( GtkTreeModel * model, GtkTreeIter * iter, gpointer vdata ) +{ + int mode; + gboolean b; + const char * text; + tr_torrent * tor; + struct filter_data * data = vdata; + GObject * o = G_OBJECT( data->filter_model ); + + gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); + + text = (const char*) g_object_get_data( o, TEXT_KEY ); + mode = GPOINTER_TO_INT( g_object_get_data( o, TEXT_MODE_KEY ) ); + + b = ( tor != NULL ) && testCategory( data->category, tor ) + && testState( data->state, tor ) + && testText( tor, text, mode ); + + return b; +} + +static void +selection_changed_cb( GtkComboBox * combo UNUSED, gpointer vdata ) +{ + struct filter_data * data = vdata; + gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter_model ) ); +} + +GtkWidget * +gtr_filter_bar_new( GtkTreeModel * tmodel, GtkTreeModel ** filter_model ) +{ + int i; + GtkWidget * l; + GtkWidget * w; + GtkWidget * h; + GtkWidget * s; + GtkWidget * menu; + GtkWidget * state; + GtkWidget * category; + GSList * sl; + const char * str; + struct filter_data * data; + const char * filter_text_names[] = { + N_( "Name" ), N_( "Files" ), N_( "Tracker" ) + }; + + + data = g_new( struct filter_data, 1 ); + data->state = state = state_combo_box_new( tmodel ); + data->category = category = category_combo_box_new( tmodel ); + data->entry = NULL; + data->filter_model = gtk_tree_model_filter_new( tmodel, NULL ); + + g_object_set( G_OBJECT( data->category ), "width-request", 150, NULL ); + + gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( data->filter_model ), + is_row_visible, data, g_free ); + + g_signal_connect( data->category, "changed", G_CALLBACK( selection_changed_cb ), data ); + g_signal_connect( data->state, "changed", G_CALLBACK( selection_changed_cb ), data ); + + + h = gtk_hbox_new( FALSE, GUI_PAD_SMALL ); + + /* add the category combobox */ + str = _( "Show _Category:" ); + w = category; + l = gtk_label_new( NULL ); + gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str ); + gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w ); + gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 ); + gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 ); + + /* add a spacer */ + w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f ); + gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG ); + gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 ); + + /* add the state combobox */ + str = _( "_State:" ); + w = state; + l = gtk_label_new( NULL ); + gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str ); + gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w ); + gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 ); + gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 ); + + /* add a spacer */ + w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f ); + gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG ); + gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 ); + + /* add the entry field */ +#ifdef USE_SEXY + s = sexy_icon_entry_new( ); + sexy_icon_entry_add_clear_button( SEXY_ICON_ENTRY( s ) ); + w = gtk_image_new_from_stock( GTK_STOCK_FIND, GTK_ICON_SIZE_MENU ); + sexy_icon_entry_set_icon( SEXY_ICON_ENTRY( s ), + SEXY_ICON_ENTRY_PRIMARY, + GTK_IMAGE( w ) ); + g_object_unref( w ); + sexy_icon_entry_set_icon_highlight( SEXY_ICON_ENTRY( s ), + SEXY_ICON_ENTRY_PRIMARY, TRUE ); +#else + s = gtk_entry_new( ); + gtk_entry_set_icon_from_stock( GTK_ENTRY( s ), + GTK_ENTRY_ICON_PRIMARY, + GTK_STOCK_FIND); + gtk_entry_set_icon_from_stock( GTK_ENTRY( s ), + GTK_ENTRY_ICON_SECONDARY, + GTK_STOCK_CLEAR ); +#endif + + menu = gtk_menu_new( ); + sl = NULL; + for( i=0; ifilter_model ); + gtk_menu_shell_append( GTK_MENU_SHELL( menu ), w ); + gtk_widget_show( w ); + } +#ifdef USE_SEXY + g_signal_connect( s, "icon-released", G_CALLBACK( entry_icon_released ), menu ); +#else + g_signal_connect( s, "icon-release", G_CALLBACK( entry_icon_release ), menu ); +#endif + + gtk_box_pack_start( GTK_BOX( h ), s, TRUE, TRUE, 0 ); + g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), data->filter_model ); + + *filter_model = data->filter_model; + return h; +} diff --git a/gtk/filter.h b/gtk/filter.h new file mode 100644 index 000000000..4a951022d --- /dev/null +++ b/gtk/filter.h @@ -0,0 +1,20 @@ +/* + * This file Copyright (C) 2010 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:$ + */ + +#ifndef GTR_FILTER_H +#define GTR_FILTER_H + +#include + +GtkWidget * gtr_filter_bar_new( GtkTreeModel * torrent_model, GtkTreeModel ** filter_model ); + +#endif diff --git a/gtk/tr-core.c b/gtk/tr-core.c index a5d207b39..c8781e302 100644 --- a/gtk/tr-core.c +++ b/gtk/tr-core.c @@ -1666,3 +1666,29 @@ tr_core_exec( TrCore * core, const tr_benc * top ) tr_core_exec_json( core, json ); tr_free( json ); } + +/*** +**** +***/ + +void +tr_core_torrent_changed( TrCore * core, int id ) +{ + GtkTreeIter iter; + GtkTreeModel * model = tr_core_model( core ); + + if( gtk_tree_model_get_iter_first( model, &iter ) ) do + { + tr_torrent * tor; + gtk_tree_model_get( model, &iter, MC_TORRENT_RAW, &tor, -1 ); + if( tr_torrentId( tor ) == id ) + { + GtkTreePath * path = gtk_tree_model_get_path( model, &iter ); + gtk_tree_model_row_changed( model, path, &iter ); + gtk_tree_path_free( path ); + break; + } + } + while( gtk_tree_model_iter_next( model, &iter ) ); +} + diff --git a/gtk/tr-core.h b/gtk/tr-core.h index 59a18f5a3..c3ebe53ae 100644 --- a/gtk/tr-core.h +++ b/gtk/tr-core.h @@ -185,6 +185,8 @@ void tr_core_set_pref_double( TrCore * self, const char * key, double val ); *** **/ +void tr_core_torrent_changed( TrCore * core, int id ); + void tr_core_port_test( TrCore * core ); void tr_core_blocklist_update( TrCore * core ); diff --git a/gtk/tr-window.c b/gtk/tr-window.c index 3d04bdcec..1d441ad09 100644 --- a/gtk/tr-window.c +++ b/gtk/tr-window.c @@ -38,6 +38,7 @@ #include "actions.h" #include "conf.h" +#include "filter.h" #include "hig.h" #include "torrent-cell-renderer.h" #include "tr-prefs.h" @@ -109,31 +110,7 @@ typedef struct } PrivateData; -static const char* -getFilterName( int mode ) -{ - switch( mode ) - { - case FILTER_MODE_ACTIVE: return "show-active"; - case FILTER_MODE_DOWNLOADING: return "show-downloading"; - case FILTER_MODE_SEEDING: return "show-seeding"; - case FILTER_MODE_PAUSED: return "show-paused"; - default: return "show-all"; /* the fallback */ - } -} -static int -getFilterModeFromName( const char * name ) -{ - if( !strcmp( name, "show-active" ) ) return FILTER_MODE_ACTIVE; - if( !strcmp( name, "show-downloading" ) ) return FILTER_MODE_DOWNLOADING; - if( !strcmp( name, "show-seeding" ) ) return FILTER_MODE_SEEDING; - if( !strcmp( name, "show-paused" ) ) return FILTER_MODE_PAUSED; - return FILTER_MODE_ALL; /* the fallback */ -} - #define PRIVATE_DATA_KEY "private-data" -#define FILTER_MODE_KEY "tr-filter-mode" -#define FILTER_TEXT_MODE_KEY "tr-filter-text-mode" static PrivateData* get_private_data( TrWindow * w ) @@ -165,19 +142,13 @@ view_row_activated( GtkTreeView * tree_view UNUSED, action_activate( "show-torrent-properties" ); } -static gboolean is_row_visible( GtkTreeModel *, - GtkTreeIter *, - gpointer ); - static GtkWidget* -makeview( PrivateData * p, - TrCore * core ) +makeview( PrivateData * p ) { GtkWidget * view; GtkTreeViewColumn * col; GtkTreeSelection * sel; GtkCellRenderer * r; - GtkTreeModel * filter_model; view = gtk_tree_view_new( ); gtk_tree_view_set_headers_visible( GTK_TREE_VIEW( view ), FALSE ); @@ -216,21 +187,12 @@ makeview( PrivateData * p, G_CALLBACK( view_row_activated ), NULL ); - filter_model = p->filter_model = gtk_tree_model_filter_new( - tr_core_model( core ), NULL ); - - gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( - filter_model ), - is_row_visible, - p, NULL ); - - gtk_tree_view_set_model( GTK_TREE_VIEW( view ), filter_model ); + gtk_tree_view_set_model( GTK_TREE_VIEW( view ), p->filter_model ); return view; } static void syncAltSpeedButton( PrivateData * p ); -static void setFilter( PrivateData * p, int mode ); static void prefsChanged( TrCore * core UNUSED, @@ -247,10 +209,6 @@ prefsChanged( TrCore * core UNUSED, * for that, but it *does* revalidate when it thinks the style's been tweaked */ g_signal_emit_by_name( p->view, "style-set", NULL, NULL ); } - else if( !strcmp( key, PREF_KEY_FILTER_MODE ) ) - { - setFilter( p, getFilterModeFromName( pref_string_get( key ) ) ); - } else if( !strcmp( key, PREF_KEY_STATUSBAR ) ) { const gboolean isEnabled = pref_flag_get( key ); @@ -359,197 +317,6 @@ alt_speed_toggled_cb( GtkToggleButton * button, gpointer vprivate ) **** FILTER ***/ -static int -checkFilterText( filter_text_mode_t filter_text_mode, - const tr_info * inf, - const char * text ) -{ - tr_file_index_t i; - int ret = 0; - char * pch; - - switch( filter_text_mode ) - { - case FILTER_TEXT_MODE_FILES: - for( i = 0; i < inf->fileCount && !ret; ++i ) - { - pch = g_utf8_casefold( inf->files[i].name, -1 ); - ret = !text || strstr( pch, text ) != NULL; - g_free( pch ); - } - break; - - case FILTER_TEXT_MODE_TRACKER: - if( inf->trackerCount > 0 ) - { - pch = g_utf8_casefold( inf->trackers[0].announce, -1 ); - ret = !text || ( strstr( pch, text ) != NULL ); - g_free( pch ); - } - break; - - default: /* NAME */ - if( !inf->name ) - ret = TRUE; - else { - pch = g_utf8_casefold( inf->name, -1 ); - ret = !text || ( strstr( pch, text ) != NULL ); - g_free( pch ); - } - break; - } - - return ret; -} - -static int -checkFilterMode( filter_mode_t filter_mode, - tr_torrent * tor ) -{ - int ret = 0; - - switch( filter_mode ) - { - case FILTER_MODE_DOWNLOADING: - ret = tr_torrentGetActivity( tor ) == TR_STATUS_DOWNLOAD; - break; - - case FILTER_MODE_SEEDING: - ret = tr_torrentGetActivity( tor ) == TR_STATUS_SEED; - break; - - case FILTER_MODE_PAUSED: - ret = tr_torrentGetActivity( tor ) == TR_STATUS_STOPPED; - break; - - case FILTER_MODE_ACTIVE: - { - const tr_stat * s = tr_torrentStatCached( tor ); - ret = s->peersSendingToUs > 0 - || s->peersGettingFromUs > 0 - || tr_torrentGetActivity( tor ) == TR_STATUS_CHECK; - break; - } - - default: /* all */ - ret = 1; - } - - return ret; -} - -static gboolean -is_row_visible( GtkTreeModel * model, - GtkTreeIter * iter, - gpointer vprivate ) -{ - PrivateData * p = vprivate; - tr_torrent * tor; - - gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); - - return checkFilterMode( p->filter_mode, tor ) - && checkFilterText( p->filter_text_mode, tr_torrentInfo( tor ), p->filter_text ); -} - -static void updateTorrentCount( PrivateData * p ); - -static void -refilter( PrivateData * p ) -{ - gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( p->filter_model ) ); - - updateTorrentCount( p ); -} - -static void -filter_text_toggled_cb( GtkCheckMenuItem * menu_item, - gpointer vprivate ) -{ - if( gtk_check_menu_item_get_active( menu_item ) ) - { - PrivateData * p = vprivate; - p->filter_text_mode = - GPOINTER_TO_UINT( g_object_get_data( G_OBJECT( menu_item ), - FILTER_TEXT_MODE_KEY ) ); - refilter( p ); - } -} - -static void -setFilter( PrivateData * p, int mode ) -{ - if( mode != (int)p->filter_mode ) - { - int i; - - /* refilter */ - p->filter_mode = mode; - refilter( p ); - - /* update the prefs */ - tr_core_set_pref( p->core, PREF_KEY_FILTER_MODE, getFilterName( mode ) ); - - /* update the togglebuttons */ - for( i=0; ifilter_toggles[i], i==mode ); - } -} - - -static void -filter_toggled_cb( GtkToggleButton * toggle, gpointer vprivate ) -{ - if( gtk_toggle_button_get_active( toggle ) ) - { - PrivateData * p = vprivate; - const int mode = GPOINTER_TO_UINT( g_object_get_data( G_OBJECT( toggle ), FILTER_MODE_KEY ) ); - setFilter( p, mode ); - } -} - -static void -filter_entry_changed( GtkEditable * e, - gpointer vprivate ) -{ - char * pch; - PrivateData * p = vprivate; - - pch = gtk_editable_get_chars( e, 0, -1 ); - g_free( p->filter_text ); - p->filter_text = g_utf8_casefold( pch, -1 ); - refilter( p ); - g_free( pch ); -} - - -#ifdef USE_SEXY -static void -entry_icon_released( SexyIconEntry * entry UNUSED, - SexyIconEntryPosition icon_pos, - int button UNUSED, - gpointer menu ) -{ - if( icon_pos == SEXY_ICON_ENTRY_PRIMARY ) - gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0, - gtk_get_current_event_time( ) ); -} -#else -static void -entry_icon_release( GtkEntry * entry UNUSED, - GtkEntryIconPosition icon_pos, - GdkEventButton * event UNUSED, - gpointer menu ) -{ - if( icon_pos == GTK_ENTRY_ICON_SECONDARY ) - gtk_entry_set_text( entry, "" ); - - if( icon_pos == GTK_ENTRY_ICON_PRIMARY ) - gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0, - gtk_get_current_event_time( ) ); -} -#endif - #if GTK_CHECK_VERSION( 2, 12, 0 ) static void @@ -835,26 +602,10 @@ tr_window_new( GtkUIManager * ui_mgr, TrCore * core ) const char * pch; PrivateData * p; GtkWidget * mainmenu, *toolbar, *filter, *list, *status; - GtkWidget * vbox, *w, *self, *h, *s, *hbox, *menu; + GtkWidget * vbox, *w, *self, *h, *hbox, *menu; GtkWindow * win; GSList * l; - const char * filter_names[FILTER_MODE_QTY] = { - /* show all torrents */ - N_( "A_ll" ), - /* show only torrents that have connected peers */ - N_( "_Active" ), - /* show only torrents that are trying to download */ - N_( "_Downloading" ), - /* show only torrents that are trying to upload */ - N_( "_Seeding" ), - /* show only torrents that are paused */ - N_( "_Paused" ) - }; - const char * filter_text_names[FILTER_TEXT_MODE_QTY] = { - N_( "Name" ), N_( "Files" ), N_( "Tracker" ) - }; - p = g_new0( PrivateData, 1 ); p->filter_text_mode = FILTER_TEXT_MODE_NAME; p->filter_text = NULL; @@ -893,41 +644,8 @@ tr_window_new( GtkUIManager * ui_mgr, TrCore * core ) action_set_important( "show-torrent-properties", TRUE ); /* filter */ - h = filter = p->filter = gtk_hbox_new( FALSE, 0 ); + h = filter = p->filter = gtr_filter_bar_new( tr_core_model( core ), &p->filter_model ); gtk_container_set_border_width( GTK_CONTAINER( h ), GUI_PAD_SMALL ); - for( i = 0; i < FILTER_MODE_QTY; ++i ) - { - const char * mnemonic = _( filter_names[i] ); - w = gtk_toggle_button_new_with_mnemonic( mnemonic ); - g_object_set_data( G_OBJECT( w ), FILTER_MODE_KEY, GINT_TO_POINTER( i ) ); - gtk_button_set_relief( GTK_BUTTON( w ), GTK_RELIEF_NONE ); - gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( w ), i == FILTER_MODE_ALL ); - p->filter_toggles[i] = GTK_TOGGLE_BUTTON( w ); - g_signal_connect( w, "toggled", G_CALLBACK( filter_toggled_cb ), p ); - gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 ); - } - -#ifdef USE_SEXY - s = sexy_icon_entry_new( ); - sexy_icon_entry_add_clear_button( SEXY_ICON_ENTRY( s ) ); - w = gtk_image_new_from_stock( GTK_STOCK_FIND, GTK_ICON_SIZE_MENU ); - sexy_icon_entry_set_icon( SEXY_ICON_ENTRY( s ), - SEXY_ICON_ENTRY_PRIMARY, - GTK_IMAGE( w ) ); - g_object_unref( w ); - sexy_icon_entry_set_icon_highlight( SEXY_ICON_ENTRY( s ), - SEXY_ICON_ENTRY_PRIMARY, TRUE ); -#else - s = gtk_entry_new( ); - gtk_entry_set_icon_from_stock( GTK_ENTRY( s ), - GTK_ENTRY_ICON_PRIMARY, - GTK_STOCK_FIND); - gtk_entry_set_icon_from_stock( GTK_ENTRY( s ), - GTK_ENTRY_ICON_SECONDARY, - GTK_STOCK_CLEAR ); -#endif - gtk_box_pack_end( GTK_BOX( h ), s, FALSE, FALSE, 0 ); - g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), p ); /* status menu */ menu = p->status_menu = gtk_menu_new( ); @@ -1006,30 +724,8 @@ tr_window_new( GtkUIManager * ui_mgr, TrCore * core ) gtk_box_pack_end( GTK_BOX( h ), hbox, FALSE, FALSE, 0 ); - menu = gtk_menu_new( ); - l = NULL; - for( i=0; iview = makeview( p, core ); + p->view = makeview( p ); w = list = p->scroll = gtk_scrolled_window_new( NULL, NULL ); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( w ), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC ); @@ -1089,7 +785,6 @@ tr_window_new( GtkUIManager * ui_mgr, TrCore * core ) tr_sessionSetAltSpeedFunc( tr_core_session( core ), onAltSpeedToggled, p ); - filter_entry_changed( GTK_EDITABLE( s ), p ); return self; } @@ -1208,7 +903,7 @@ tr_window_update( TrWindow * self ) updateSpeeds( p ); updateTorrentCount( p ); updateStats( p ); - refilter( p ); + gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( p->filter_model ) ); } } -- 2.40.0