source: trunk/gtk/filter.c @ 10377

Last change on this file since 10377 was 10377, checked in by charles, 12 years ago

(trunk gtk) use Rolcol's suggestions on the filterbar

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