source: trunk/gtk/filter.c @ 10381

Last change on this file since 10381 was 10381, checked in by charles, 11 years ago

(trunk gtk) preserve the old filter's behavior of including torrents being verified in the "active" filter. Thanks to Waldorf for the reminder.

File size: 32.6 KB
Line 
1/*
2 * This file Copyright (C) 2010 Mnemosyne LLC
3 *
4 * This file is licensed by the GPL version 2.  Works owned by the
5 * Transmission project are granted a special exemption to clause 2(b)
6 * so that the bulk of its code can remain under the MIT license.
7 * This exemption does not extend to derived works not owned by
8 * the Transmission project.
9 *
10 * $Id:$
11 */
12
13#include <gtk/gtk.h>
14#include <glib/gi18n.h>
15
16#include <libtransmission/transmission.h>
17#include <libtransmission/utils.h>
18
19#include "favicon.h"
20#include "filter.h"
21#include "hig.h" /* GUI_PAD */
22#include "tr-core.h"
23#include "util.h" /* gtr_idle_add() */
24
25#define DIRTY_KEY          "tr-filter-dirty"
26#define SESSION_KEY        "tr-session-key"
27#define TEXT_KEY           "tr-filter-text"
28#define TEXT_MODE_KEY      "tr-filter-text-mode"
29#define TORRENT_MODEL_KEY  "tr-filter-torrent-model-key"
30
31#if !GTK_CHECK_VERSION( 2,16,0 )
32 /* FIXME: when 2.16 has been out long enough, it would be really nice to
33  * get rid of this libsexy usage because of its makefile strangeness */
34 #define USE_SEXY
35 #include "sexy-icon-entry.h"
36#endif
37
38/***
39****
40****  CATEGORIES
41****
42***/
43
44enum
45{
46    CAT_FILTER_TYPE_ALL,
47    CAT_FILTER_TYPE_PRIVATE,
48    CAT_FILTER_TYPE_PUBLIC,
49    CAT_FILTER_TYPE_HOST,
50    CAT_FILTER_TYPE_PARENT,
51    CAT_FILTER_TYPE_PRI_HIGH,
52    CAT_FILTER_TYPE_PRI_NORMAL,
53    CAT_FILTER_TYPE_PRI_LOW,
54    CAT_FILTER_TYPE_TAG,
55    CAT_FILTER_TYPE_SEPARATOR,
56};
57
58enum
59{
60    CAT_FILTER_COL_NAME, /* human-readable name; ie, Legaltorrents */
61    CAT_FILTER_COL_COUNT, /* how many matches there are */
62    CAT_FILTER_COL_TYPE,
63    CAT_FILTER_COL_HOST, /* pattern-matching text; ie, legaltorrents.com */
64    CAT_FILTER_COL_PIXBUF,
65    CAT_FILTER_N_COLS
66};
67
68static int
69pstrcmp( const void * a, const void * b )
70{
71    return strcmp( *(const char**)a, *(const char**)b );
72}
73
74static char*
75get_host_from_url( const char * url )
76{
77    char * h = NULL;
78    char * name;
79    const char * first_dot;
80    const char * last_dot;
81
82    tr_urlParse( url, -1, NULL, &h, NULL, NULL );
83    first_dot = strchr( h, '.' );
84    last_dot = strrchr( h, '.' );
85
86    if( ( first_dot ) && ( last_dot ) && ( first_dot != last_dot ) )
87        name = g_strdup( first_dot + 1 );
88    else
89        name = g_strdup( h );
90
91    tr_free( h );
92    return name;
93}
94
95static char*
96get_name_from_host( const char * host )
97{
98    char * name;
99    const char * dot = strrchr( host, '.' );
100
101    if( dot == NULL )
102        name = g_strdup( host );
103    else
104        name = g_strndup( host, dot - host );
105
106    *name = g_ascii_toupper( *name );
107
108    return name;
109}
110
111static void
112category_model_update_count( GtkTreeStore * store, GtkTreeIter * iter, int n )
113{
114    int count;
115    GtkTreeModel * model = GTK_TREE_MODEL( store );
116    gtk_tree_model_get( model, iter, CAT_FILTER_COL_COUNT, &count, -1 );
117    if( n != count )
118        gtk_tree_store_set( store, iter, CAT_FILTER_COL_COUNT, n, -1 );
119}
120
121static void
122favicon_ready_cb( gpointer pixbuf, gpointer vreference )
123{
124    GtkTreeIter iter;
125    GtkTreeRowReference * reference = vreference;
126
127    if( pixbuf != NULL )
128    {
129        GtkTreePath * path = gtk_tree_row_reference_get_path( reference );
130        GtkTreeModel * model = gtk_tree_row_reference_get_model( reference );
131
132        if( gtk_tree_model_get_iter( model, &iter, path ) )
133            gtk_tree_store_set( GTK_TREE_STORE( model ), &iter, CAT_FILTER_COL_PIXBUF, pixbuf, -1 );
134
135        gtk_tree_path_free( path );
136
137        g_object_unref( pixbuf );
138    }
139
140    gtk_tree_row_reference_free( reference );
141}
142
143static gboolean
144category_filter_model_update( GtkTreeStore * store )
145{
146    int i, n;
147    int low = 0;
148    int all = 0;
149    int high = 0;
150    int public = 0;
151    int normal = 0;
152    int private = 0;
153    int store_pos;
154    GtkTreeIter iter;
155    GtkTreeIter parent;
156    GtkTreeModel * model = GTK_TREE_MODEL( store );
157    GPtrArray * hosts = g_ptr_array_new( );
158    GHashTable * hosts_hash = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, g_free );
159    GObject * o = G_OBJECT( store );
160    GtkTreeModel * tmodel = GTK_TREE_MODEL(
161                                    g_object_get_data( o, TORRENT_MODEL_KEY ) );
162
163    g_object_steal_data( o, DIRTY_KEY );
164
165    /* walk through all the torrents, tallying how many matches there are
166     * for the various categories.  also make a sorted list of all tracker
167     * hosts s.t. we can merge it with the existing list */
168    if( gtk_tree_model_get_iter_first( tmodel, &iter )) do
169    {
170        tr_torrent * tor;
171        const tr_info * inf;
172        int keyCount;
173        char ** keys;
174
175        gtk_tree_model_get( tmodel, &iter, MC_TORRENT_RAW, &tor, -1 );
176        inf = tr_torrentInfo( tor );
177        keyCount = 0;
178        keys = g_new( char*, inf->trackerCount );
179
180        for( i=0, n=inf->trackerCount; i<n; ++i )
181        {
182            int k;
183            char * key = get_host_from_url( inf->trackers[i].announce );
184            int * count = g_hash_table_lookup( hosts_hash, key );
185            if( count == NULL )
186            {
187                char * k = g_strdup( key );
188                count = tr_new0( int, 1 );
189                g_hash_table_insert( hosts_hash, k, count );
190                g_ptr_array_add( hosts, k );
191            }
192
193            for( k=0; k<keyCount; ++k )
194                if( !strcmp( keys[k], key ) )
195                    break;
196            if( k==keyCount )
197                keys[keyCount++] = key;
198            else
199                g_free( key );
200        }
201
202        for( i=0; i<keyCount; ++i )
203        {
204            int * incrementme = g_hash_table_lookup( hosts_hash, keys[i] );
205            ++*incrementme;
206            g_free( keys[i] );
207        }
208        g_free( keys );
209
210        ++all;
211
212        if( inf->isPrivate )
213            ++private;
214        else
215            ++public;
216
217        switch( tr_torrentGetPriority( tor ) )
218        {
219            case TR_PRI_HIGH: ++high; break;
220            case TR_PRI_LOW: ++low; break;
221            default: ++normal; break;
222        }
223    }
224    while( gtk_tree_model_iter_next( tmodel, &iter ) );
225    qsort( hosts->pdata, hosts->len, sizeof(char*), pstrcmp );
226
227    /* update the "all" count */
228    gtk_tree_model_iter_nth_child( model, &iter, NULL, 0 );
229    category_model_update_count( store, &iter, all );
230
231    /* update the "public" count */
232    gtk_tree_model_iter_nth_child( model, &parent, NULL, 2 );
233    gtk_tree_model_iter_nth_child( model, &iter, &parent, 0 );
234    category_model_update_count( store, &iter, public );
235    gtk_tree_model_iter_nth_child( model, &iter, &parent, 1 );
236    category_model_update_count( store, &iter, private );
237
238    /* update the "priority" subtree */
239    gtk_tree_model_iter_nth_child( model, &parent, NULL, 3 );
240    gtk_tree_model_iter_nth_child( model, &iter, &parent, 0 );
241    category_model_update_count( store, &iter, high );
242    gtk_tree_model_iter_nth_child( model, &iter, &parent, 1 );
243    category_model_update_count( store, &iter, normal );
244    gtk_tree_model_iter_nth_child( model, &iter, &parent, 2 );
245    category_model_update_count( store, &iter, low );
246
247    /* update the "hosts" subtree */
248    gtk_tree_model_iter_nth_child( model, &parent, NULL, 4 );
249    i = 0;
250    n = hosts->len;
251    store_pos = 0;
252    for( ;; )
253    {
254        const gboolean new_hosts_done = i >= n;
255        const gboolean old_hosts_done = !gtk_tree_model_iter_nth_child( model, &iter, &parent, store_pos );
256        gboolean remove_row = FALSE;
257        gboolean insert_row = FALSE;
258
259        /* are we done yet? */
260        if( new_hosts_done && old_hosts_done )
261            break;
262
263        /* decide what to do */
264        if( new_hosts_done ) {
265            /* g_message( "new hosts done; remove row" ); */
266            remove_row = TRUE;
267        } else if( old_hosts_done ) {
268            /* g_message( "old hosts done; insert row" ); */
269            insert_row = TRUE;
270        } else {
271            int cmp;
272            char * host;
273            gtk_tree_model_get( model, &iter, CAT_FILTER_COL_HOST, &host,  -1 );
274            cmp = strcmp( host, hosts->pdata[i] );
275            /* g_message( "cmp( %s, %s ) returns %d", host, (char*)hosts->pdata[i], cmp ); */
276            if( cmp < 0 ) {
277                /* g_message( "cmp<0, so remove row" ); */
278                remove_row = TRUE;
279            } else if( cmp > 0 ) {
280                /* g_message( "cmp>0, so insert row" ); */
281                insert_row = TRUE;
282            }
283            g_free( host );
284        }
285
286        /* do something */
287        if( remove_row ) {
288            /* g_message( "removing row and incrementing i" ); */
289            gtk_tree_store_remove( store, &iter );
290        } else if( insert_row ) {
291            GtkTreeIter child;
292            GtkTreePath * path;
293            GtkTreeRowReference * reference;
294            tr_session * session = g_object_get_data( G_OBJECT( store ), SESSION_KEY );
295            const char * host = hosts->pdata[i];
296            char * name = get_name_from_host( host );
297            const int count = *(int*)g_hash_table_lookup( hosts_hash, host );
298            gtk_tree_store_insert_with_values( store, &child, &parent, store_pos,
299                CAT_FILTER_COL_HOST, host,
300                CAT_FILTER_COL_NAME, name,
301                CAT_FILTER_COL_COUNT, count,
302                CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_HOST,
303                -1 );
304            path = gtk_tree_model_get_path( model, &child );
305            reference = gtk_tree_row_reference_new( model, path );
306            gtr_get_favicon( session, host, favicon_ready_cb, reference );
307            gtk_tree_path_free( path );
308            g_free( name );
309            ++store_pos;
310            ++i;
311        } else { /* update row */
312            const char * host = hosts->pdata[i];
313            const int count = *(int*)g_hash_table_lookup( hosts_hash, host );
314            category_model_update_count( store, &iter, count );
315            ++store_pos;
316            ++i;
317        }
318    }
319   
320    /* cleanup */
321    g_ptr_array_unref( hosts );
322    g_hash_table_unref( hosts_hash );
323    g_ptr_array_foreach( hosts, (GFunc)g_free, NULL );
324    return FALSE;
325}
326
327static GtkTreeModel *
328category_filter_model_new( GtkTreeModel * tmodel )
329{
330    GtkTreeIter iter;
331    GtkTreeStore * store;
332    const int invisible_number = -1; /* doesn't get rendered */
333
334    store = gtk_tree_store_new( CAT_FILTER_N_COLS,
335                                G_TYPE_STRING,
336                                G_TYPE_INT,
337                                G_TYPE_INT,
338                                G_TYPE_STRING,
339                                GDK_TYPE_PIXBUF );
340
341    gtk_tree_store_insert_with_values( store, NULL, NULL, -1,
342        CAT_FILTER_COL_NAME, _( "All" ),
343        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_ALL,
344        -1 );
345    gtk_tree_store_insert_with_values( store, NULL, NULL, -1,
346        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_SEPARATOR,
347        -1 );
348
349    gtk_tree_store_insert_with_values( store, &iter, NULL, -1,
350        CAT_FILTER_COL_NAME, _( "Privacy" ),
351        CAT_FILTER_COL_COUNT, invisible_number,
352        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT,
353        -1 );
354    gtk_tree_store_insert_with_values( store, NULL, &iter, -1,
355        CAT_FILTER_COL_NAME, _( "Public" ),
356        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PUBLIC,
357        -1 );
358    gtk_tree_store_insert_with_values( store, NULL, &iter, -1,
359        CAT_FILTER_COL_NAME, _( "Private" ),
360        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRIVATE,
361        -1 );
362
363    gtk_tree_store_insert_with_values( store, &iter, NULL, -1,
364        CAT_FILTER_COL_NAME, _( "Priority" ),
365        CAT_FILTER_COL_COUNT, invisible_number,
366        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT,
367        -1 );
368    gtk_tree_store_insert_with_values( store, NULL, &iter, -1,
369        CAT_FILTER_COL_NAME, _( "High" ),
370        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_HIGH,
371        -1 );
372    gtk_tree_store_insert_with_values( store, NULL, &iter, -1,
373        CAT_FILTER_COL_NAME, _( "Normal" ),
374        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_NORMAL,
375        -1 );
376    gtk_tree_store_insert_with_values( store, NULL, &iter, -1,
377        CAT_FILTER_COL_NAME, _( "Low" ),
378        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PRI_LOW,
379        -1 );
380
381    gtk_tree_store_insert_with_values( store, &iter, NULL, -1,
382        CAT_FILTER_COL_NAME, _( "Trackers" ),
383        CAT_FILTER_COL_COUNT, invisible_number,
384        CAT_FILTER_COL_TYPE, CAT_FILTER_TYPE_PARENT,
385        -1 );
386
387    g_object_set_data( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel );
388    category_filter_model_update( store );
389    return GTK_TREE_MODEL( store );
390}
391
392static gboolean
393is_it_a_separator( GtkTreeModel * model, GtkTreeIter * iter, gpointer data UNUSED )
394{
395    int type;
396    gtk_tree_model_get( model, iter, CAT_FILTER_COL_TYPE, &type, -1 );
397    return type == CAT_FILTER_TYPE_SEPARATOR;
398}
399
400static void
401category_model_update_idle( gpointer category_model )
402{
403    GObject * o = G_OBJECT( category_model );
404    gboolean pending = GPOINTER_TO_INT( g_object_get_data( o, DIRTY_KEY ) );
405    if( !pending )
406    {
407        GSourceFunc func = (GSourceFunc) category_filter_model_update;
408        g_object_set_data( o, DIRTY_KEY, GINT_TO_POINTER(1) );
409        gtr_idle_add( func, category_model );
410    }
411}
412
413static void
414torrent_model_row_changed( GtkTreeModel  * tmodel UNUSED,
415                           GtkTreePath   * path UNUSED,
416                           GtkTreeIter   * iter UNUSED,
417                           gpointer        category_model )
418{
419    category_model_update_idle( category_model );
420}
421
422static void
423torrent_model_row_deleted_cb( GtkTreeModel * tmodel UNUSED,
424                              GtkTreePath  * path UNUSED,
425                              gpointer       category_model )
426{
427    category_model_update_idle( category_model );
428}
429
430static void
431render_pixbuf_func( GtkCellLayout    * cell_layout UNUSED,
432                    GtkCellRenderer  * cell_renderer,
433                    GtkTreeModel     * tree_model,
434                    GtkTreeIter      * iter,
435                    gpointer           data UNUSED )
436{
437    int type;
438    gtk_tree_model_get( tree_model, iter, CAT_FILTER_COL_TYPE, &type, -1 );
439    g_object_set( cell_renderer, "width", type==CAT_FILTER_TYPE_HOST ? 20 : 0, NULL );
440}
441static void
442render_hit_count_func( GtkCellLayout    * cell_layout UNUSED,
443                       GtkCellRenderer  * cell_renderer,
444                       GtkTreeModel     * tree_model,
445                       GtkTreeIter      * iter,
446                       gpointer           data UNUSED )
447{
448    int count;
449    char buf[512];
450
451    gtk_tree_model_get( tree_model, iter, CAT_FILTER_COL_COUNT, &count, -1 );
452
453    if( count >= 0 )
454        g_snprintf( buf, sizeof( buf ), "%'d", count );
455    else
456        *buf = '\0';
457
458    g_object_set( cell_renderer, "text", buf, NULL );
459}
460
461static GtkCellRenderer *
462number_renderer_new( void )
463{
464    GtkCellRenderer * r = gtk_cell_renderer_text_new( );
465
466    g_object_set( G_OBJECT( r ), "alignment", PANGO_ALIGN_RIGHT,
467                                 "weight", PANGO_WEIGHT_ULTRALIGHT,
468                                 "xalign", 1.0,
469                                 "xpad", GUI_PAD,
470                                 NULL );
471
472    return r;
473}
474
475static GtkWidget *
476category_combo_box_new( GtkTreeModel * tmodel )
477{
478    GtkWidget * c;
479    GtkCellRenderer * r;
480    GtkTreeModel * category_model;
481
482    /* create the category combobox */
483    category_model = category_filter_model_new( tmodel );
484    c = gtk_combo_box_new_with_model( category_model );
485    gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ),
486                                          is_it_a_separator, NULL, NULL );
487    gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 );
488
489    r = gtk_cell_renderer_pixbuf_new( );
490    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, FALSE );
491    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_pixbuf_func, NULL, NULL );
492    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r,
493                                    "pixbuf", CAT_FILTER_COL_PIXBUF,
494                                    NULL );
495   
496
497    r = gtk_cell_renderer_text_new( );
498    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, FALSE );
499    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r,
500                                    "text", CAT_FILTER_COL_NAME,
501                                    NULL );
502
503    r = number_renderer_new( );
504    gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE );
505    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_hit_count_func, NULL, NULL );
506
507    g_signal_connect( tmodel, "row-changed",
508                      G_CALLBACK( torrent_model_row_changed ), category_model );
509    g_signal_connect( tmodel, "row-inserted",
510                      G_CALLBACK( torrent_model_row_changed ), category_model );
511    g_signal_connect( tmodel, "row-deleted",
512                      G_CALLBACK( torrent_model_row_deleted_cb ), category_model );
513
514    return c;
515}
516
517/***
518****
519***/
520
521static gboolean
522testCategory( GtkWidget * category_combo, tr_torrent * tor )
523{
524    int type;
525    const tr_info * inf;
526    GtkTreeIter iter;
527    GtkComboBox * combo = GTK_COMBO_BOX( category_combo );
528    GtkTreeModel * model = gtk_combo_box_get_model( combo );
529
530    if( !gtk_combo_box_get_active_iter( combo, &iter ) )
531        return TRUE;
532
533    inf = tr_torrentInfo( tor );
534    gtk_tree_model_get( model, &iter, CAT_FILTER_COL_TYPE, &type, -1 );
535    switch( type )
536    {
537        case CAT_FILTER_TYPE_ALL:
538            return TRUE;
539
540        case CAT_FILTER_TYPE_PRIVATE:
541            return inf->isPrivate;
542
543        case CAT_FILTER_TYPE_PUBLIC:
544            return !inf->isPrivate;
545
546        case CAT_FILTER_TYPE_PRI_HIGH:
547            return tr_torrentGetPriority( tor ) == TR_PRI_HIGH;
548
549        case CAT_FILTER_TYPE_PRI_NORMAL:
550            return tr_torrentGetPriority( tor ) == TR_PRI_NORMAL;
551
552        case CAT_FILTER_TYPE_PRI_LOW:
553            return tr_torrentGetPriority( tor ) == TR_PRI_LOW;
554
555        case CAT_FILTER_TYPE_HOST: {
556            int i;
557            char * host;
558            gtk_tree_model_get( model, &iter, CAT_FILTER_COL_HOST, &host, -1 );
559            for( i=0; i<inf->trackerCount; ++i ) {
560                char * tmp = get_host_from_url( inf->trackers[i].announce );
561                const gboolean hit = !strcmp( tmp, host );
562                g_free( tmp );
563                if( hit )
564                    break;
565            }
566            g_free( host );
567            return i < inf->trackerCount;
568        }
569
570        case CAT_FILTER_TYPE_TAG:
571            /* FIXME */
572            return TRUE;
573
574        default:
575            return TRUE;
576    }
577}
578
579/***
580****
581****  STATES
582****
583***/
584
585enum
586{
587    STATE_FILTER_ALL,
588    STATE_FILTER_DOWNLOADING,
589    STATE_FILTER_SEEDING,
590    STATE_FILTER_ACTIVE,
591    STATE_FILTER_PAUSED,
592    STATE_FILTER_QUEUED,
593    STATE_FILTER_VERIFYING,
594    STATE_FILTER_ERROR,
595    STATE_FILTER_SEPARATOR
596};
597
598enum
599{
600    STATE_FILTER_COL_NAME,
601    STATE_FILTER_COL_COUNT,
602    STATE_FILTER_COL_TYPE,
603    STATE_FILTER_N_COLS
604};
605
606static gboolean
607state_is_it_a_separator( GtkTreeModel * m, GtkTreeIter * i, gpointer d UNUSED )
608{
609    int type;
610    gtk_tree_model_get( m, i, STATE_FILTER_COL_TYPE, &type, -1 );
611    return type == STATE_FILTER_SEPARATOR;
612}
613
614static gboolean
615test_torrent_state( tr_torrent * tor, int type )
616{
617    const tr_stat * st = tr_torrentStat( tor );
618
619    switch( type )
620    {
621        case STATE_FILTER_DOWNLOADING:
622            return st->activity == TR_STATUS_DOWNLOAD;
623
624        case STATE_FILTER_SEEDING:
625            return st->activity == TR_STATUS_SEED;
626
627        case STATE_FILTER_ACTIVE:
628            return ( st->peersSendingToUs > 0 )
629                || ( st->peersGettingFromUs > 0 );
630                || ( st->activity == TR_STATUS_CHECK );
631
632        case STATE_FILTER_PAUSED:
633            return st->activity == TR_STATUS_STOPPED;
634
635        case STATE_FILTER_QUEUED:
636            return FALSE;
637
638        case STATE_FILTER_VERIFYING:
639            return ( st->activity == TR_STATUS_CHECK_WAIT )
640                || ( st->activity == TR_STATUS_CHECK );
641
642        case STATE_FILTER_ERROR:
643            return st->error != 0;
644
645        default: /* STATE_FILTER_ALL */
646            return TRUE;
647
648    }
649}
650
651static gboolean
652testState( GtkWidget * state_combo, tr_torrent * tor )
653{
654    int type;
655    GtkTreeIter iter;
656    GtkComboBox * combo = GTK_COMBO_BOX( state_combo );
657    GtkTreeModel * model = gtk_combo_box_get_model( combo );
658
659    if( !gtk_combo_box_get_active_iter( combo, &iter ) )
660        return TRUE;
661
662    gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
663    return test_torrent_state( tor, type );
664}
665
666static void
667status_model_update_count( GtkListStore * store, GtkTreeIter * iter, int n )
668{
669    int count;
670    gtk_tree_model_get( GTK_TREE_MODEL( store ), iter, STATE_FILTER_COL_COUNT, &count, -1 );
671    if( n != count )
672        gtk_list_store_set( store, iter, STATE_FILTER_COL_COUNT, n, -1 );
673}
674
675static void
676state_filter_model_update( GtkListStore * store )
677{
678    GtkTreeIter iter;
679    GtkTreeModel * model = GTK_TREE_MODEL( store );
680    GObject * o = G_OBJECT( store );
681    GtkTreeModel * tmodel = GTK_TREE_MODEL( g_object_get_data( o, TORRENT_MODEL_KEY ) );
682
683    g_object_steal_data( o, DIRTY_KEY );
684
685    if( gtk_tree_model_get_iter_first( model, &iter )) do
686    {
687        int hits;
688        int type;
689        GtkTreeIter torrent_iter;
690
691        gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
692
693        hits = 0;
694        if( gtk_tree_model_get_iter_first( tmodel, &torrent_iter )) do {
695            tr_torrent * tor;
696            gtk_tree_model_get( tmodel, &torrent_iter, MC_TORRENT_RAW, &tor, -1 );
697            if( test_torrent_state( tor, type ) )
698                ++hits;
699        } while( gtk_tree_model_iter_next( tmodel, &torrent_iter ) );
700
701        status_model_update_count( store, &iter, hits );
702
703    } while( gtk_tree_model_iter_next( model, &iter ) );
704}
705
706static GtkTreeModel *
707state_filter_model_new( GtkTreeModel * tmodel )
708{
709    int i, n;
710    struct {
711        int type;
712        const char * name;
713    } types[] = {
714        { STATE_FILTER_ALL, N_( "All" ) },
715        { STATE_FILTER_SEPARATOR, NULL },
716        { STATE_FILTER_DOWNLOADING, N_( "Downloading" ) },
717        { STATE_FILTER_SEEDING, N_( "Seeding" ) },
718        { STATE_FILTER_ACTIVE, N_( "Active" ) },
719        { STATE_FILTER_PAUSED, N_( "Paused" ) },
720        { STATE_FILTER_QUEUED, N_( "Queued" ) },
721        { STATE_FILTER_CHECKING, N_( "Verifying" ) },
722        { STATE_FILTER_ERROR, N_( "Error" ) }
723    };
724    GtkListStore * store;
725
726    store = gtk_list_store_new( STATE_FILTER_N_COLS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT );
727    for( i=0, n=G_N_ELEMENTS(types); i<n; ++i )
728        gtk_list_store_insert_with_values( store, NULL, -1,
729                                           STATE_FILTER_COL_NAME, _( types[i].name ),
730                                           STATE_FILTER_COL_TYPE, types[i].type,
731                                           -1 );
732
733    g_object_set_data( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel );
734    state_filter_model_update( store );
735    return GTK_TREE_MODEL( store );
736}
737
738static void
739state_model_update_idle( gpointer state_model )
740{
741    GObject * o = G_OBJECT( state_model );
742    gboolean pending = GPOINTER_TO_INT( g_object_get_data( o, DIRTY_KEY ) );
743    if( !pending )
744    {
745        GSourceFunc func = (GSourceFunc) state_filter_model_update;
746        g_object_set_data( o, DIRTY_KEY, GINT_TO_POINTER(1) );
747        gtr_idle_add( func, state_model );
748    }
749}
750
751static void
752state_torrent_model_row_changed( GtkTreeModel  * tmodel UNUSED,
753                                 GtkTreePath   * path UNUSED,
754                                 GtkTreeIter   * iter UNUSED,
755                                 gpointer        state_model )
756{
757    state_model_update_idle( state_model );
758}
759
760static void
761state_torrent_model_row_deleted_cb( GtkTreeModel  * tmodel UNUSED,
762                                    GtkTreePath   * path UNUSED,
763                                    gpointer        state_model )
764{
765    state_model_update_idle( state_model );
766}
767
768static GtkWidget *
769state_combo_box_new( GtkTreeModel * tmodel )
770{
771    GtkWidget * c;
772    GtkCellRenderer * r;
773    GtkTreeModel * state_model;
774
775    state_model = state_filter_model_new( tmodel );
776    c = gtk_combo_box_new_with_model( state_model );
777    gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ),
778                                          state_is_it_a_separator, NULL, NULL );
779    gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 );
780
781    r = gtk_cell_renderer_text_new( );
782    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, TRUE );
783    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r, "text", STATE_FILTER_COL_NAME, NULL );
784
785    r = number_renderer_new( );
786    gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE );
787    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_hit_count_func, NULL, NULL );
788
789    g_signal_connect( tmodel, "row-changed",
790                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
791    g_signal_connect( tmodel, "row-inserted",
792                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
793    g_signal_connect( tmodel, "row-deleted",
794                      G_CALLBACK( state_torrent_model_row_deleted_cb ), state_model );
795
796    return c;
797}
798
799/****
800*****
801*****  ENTRY FIELD
802*****
803****/
804
805enum
806{
807    TEXT_MODE_NAME,
808    TEXT_MODE_FILES,
809    TEXT_MODE_TRACKER,
810    TEXT_MODE_N_TYPES
811};
812
813static gboolean
814testText( const tr_torrent * tor, const char * key, int mode )
815{
816    gboolean ret;
817    tr_file_index_t i;
818    const tr_info * inf = tr_torrentInfo( tor );
819
820    switch( mode )
821    {
822        case TEXT_MODE_FILES:
823            for( i=0; i<inf->fileCount && !ret; ++i ) {
824                char * pch = g_utf8_casefold( inf->files[i].name, -1 );
825                ret = !key || strstr( pch, key ) != NULL;
826                g_free( pch );
827            }
828            break;
829
830        case TEXT_MODE_TRACKER:
831            if( inf->trackerCount > 0 ) {
832                char * pch = g_utf8_casefold( inf->trackers[0].announce, -1 );
833                ret = !key || ( strstr( pch, key ) != NULL );
834                g_free( pch );
835            }
836            break;
837
838        default: /* NAME */
839            if( !inf->name )
840                ret = TRUE;
841            else {
842                char * pch = g_utf8_casefold( inf->name, -1 );
843                ret = !key || ( strstr( pch, key ) != NULL );
844                g_free( pch );
845            }
846            break;
847    }
848
849    return ret;
850}
851
852
853#ifdef USE_SEXY
854static void
855entry_icon_released( SexyIconEntry           * entry  UNUSED,
856                     SexyIconEntryPosition     icon_pos,
857                     int                       button UNUSED,
858                     gpointer                  menu )
859{
860    if( icon_pos == SEXY_ICON_ENTRY_PRIMARY )
861        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
862                        gtk_get_current_event_time( ) );
863}
864#else
865static void
866entry_icon_release( GtkEntry              * entry  UNUSED,
867                    GtkEntryIconPosition    icon_pos,
868                    GdkEventButton        * event  UNUSED,
869                    gpointer                menu )
870{
871    if( icon_pos == GTK_ENTRY_ICON_SECONDARY )
872        gtk_entry_set_text( entry, "" );
873
874    if( icon_pos == GTK_ENTRY_ICON_PRIMARY )
875        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
876                        gtk_get_current_event_time( ) );
877}
878#endif
879
880static void
881filter_entry_changed( GtkEditable * e, gpointer filter_model )
882{
883    char * pch;
884    char * folded;
885   
886    pch = gtk_editable_get_chars( e, 0, -1 );
887    folded = g_utf8_casefold( pch, -1 );
888    g_object_set_data_full( filter_model, TEXT_KEY, folded, g_free );
889    g_free( pch );
890
891    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
892}
893
894static void
895filter_text_toggled_cb( GtkCheckMenuItem * menu_item, gpointer filter_model )
896{
897    g_object_set_data( filter_model, TEXT_MODE_KEY,
898                       g_object_get_data( G_OBJECT( menu_item ), TEXT_MODE_KEY ) );
899    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
900}
901
902/*****
903******
904******
905******
906*****/
907
908struct filter_data
909{
910    GtkWidget * state;
911    GtkWidget * category;
912    GtkWidget * entry;
913    GtkTreeModel * filter_model;
914};
915
916static gboolean
917is_row_visible( GtkTreeModel * model, GtkTreeIter * iter, gpointer vdata )
918{
919    int mode;
920    gboolean b;
921    const char * text;
922    tr_torrent * tor;
923    struct filter_data * data = vdata;
924    GObject * o = G_OBJECT( data->filter_model );
925
926    gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 );
927
928    text = (const char*) g_object_get_data( o, TEXT_KEY );
929    mode = GPOINTER_TO_INT( g_object_get_data( o, TEXT_MODE_KEY ) );
930
931    b = ( tor != NULL ) && testCategory( data->category, tor )
932                        && testState( data->state, tor )
933                        && testText( tor, text, mode );
934
935    return b;
936}
937
938static void
939selection_changed_cb( GtkComboBox * combo UNUSED, gpointer vdata )
940{
941    struct filter_data * data = vdata;
942    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter_model ) );
943}
944
945GtkWidget *
946gtr_filter_bar_new( tr_session * session, GtkTreeModel * tmodel, GtkTreeModel ** filter_model )
947{
948    int i;
949    GtkWidget * l;
950    GtkWidget * w;
951    GtkWidget * h;
952    GtkWidget * s;
953    GtkWidget * menu;
954    GtkWidget * state;
955    GtkWidget * category;
956    GSList * sl;
957    const char * str;
958    struct filter_data * data;
959    const char *  filter_text_names[] = {
960        N_( "Name" ), N_( "Files" ), N_( "Tracker" )
961    };
962
963
964    data = g_new( struct filter_data, 1 );
965    data->state = state = state_combo_box_new( tmodel );
966    data->category = category = category_combo_box_new( tmodel );
967    data->entry = NULL;
968    data->filter_model = gtk_tree_model_filter_new( tmodel, NULL );
969
970    g_object_set( G_OBJECT( data->category ), "width-request", 170, NULL );
971    g_object_set_data( G_OBJECT( gtk_combo_box_get_model( GTK_COMBO_BOX( data->category ) ) ), SESSION_KEY, session );
972
973    gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( data->filter_model ),
974                                            is_row_visible, data, g_free );
975
976    g_signal_connect( data->category, "changed", G_CALLBACK( selection_changed_cb ), data );
977    g_signal_connect( data->state, "changed", G_CALLBACK( selection_changed_cb ), data );
978
979
980    h = gtk_hbox_new( FALSE, GUI_PAD_SMALL );
981
982    /* add the category combobox */
983    str = _( "_Category:" );
984    w = category;
985    l = gtk_label_new( NULL );
986    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
987    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
988    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
989    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
990
991    /* add a spacer */
992    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
993    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
994    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
995
996    /* add the state combobox */
997    str = _( "_State:" );
998    w = state;
999    l = gtk_label_new( NULL );
1000    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
1001    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
1002    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
1003    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
1004
1005    /* add a spacer */
1006    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1007    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
1008    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1009
1010    /* add the entry field */
1011#ifdef USE_SEXY
1012    s = sexy_icon_entry_new( );
1013    sexy_icon_entry_add_clear_button( SEXY_ICON_ENTRY( s ) );
1014    w = gtk_image_new_from_stock( GTK_STOCK_FIND, GTK_ICON_SIZE_MENU );
1015    sexy_icon_entry_set_icon( SEXY_ICON_ENTRY( s ),
1016                              SEXY_ICON_ENTRY_PRIMARY,
1017                              GTK_IMAGE( w ) );
1018    g_object_unref( w );
1019    sexy_icon_entry_set_icon_highlight( SEXY_ICON_ENTRY( s ),
1020                                        SEXY_ICON_ENTRY_PRIMARY, TRUE );
1021#else
1022    s = gtk_entry_new( );
1023    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
1024                                   GTK_ENTRY_ICON_PRIMARY,
1025                                   GTK_STOCK_FIND);
1026    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
1027                                   GTK_ENTRY_ICON_SECONDARY,
1028                                   GTK_STOCK_CLEAR );
1029#endif
1030
1031    menu = gtk_menu_new( );
1032    sl = NULL;
1033    for( i=0; i<TEXT_MODE_N_TYPES; ++i )
1034    {
1035        const char * name = _( filter_text_names[i] );
1036        GtkWidget *  w = gtk_radio_menu_item_new_with_label ( sl, name );
1037        sl = gtk_radio_menu_item_get_group( GTK_RADIO_MENU_ITEM( w ) );
1038        g_object_set_data( G_OBJECT( w ), TEXT_MODE_KEY, GINT_TO_POINTER( i ) );
1039        g_signal_connect( w, "toggled", G_CALLBACK( filter_text_toggled_cb ), data->filter_model ); 
1040        gtk_menu_shell_append( GTK_MENU_SHELL( menu ), w );
1041        gtk_widget_show( w );
1042    }
1043#ifdef USE_SEXY
1044    g_signal_connect( s, "icon-released", G_CALLBACK( entry_icon_released ), menu );
1045#else
1046    g_signal_connect( s, "icon-release", G_CALLBACK( entry_icon_release ), menu );
1047#endif
1048
1049    gtk_box_pack_start( GTK_BOX( h ), s, TRUE, TRUE, 0 );
1050    g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), data->filter_model );
1051
1052    *filter_model = data->filter_model;
1053    return h;
1054}
Note: See TracBrowser for help on using the repository browser.