source: trunk/gtk/filter.c @ 10374

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

(trunk gtk) first draft of a filterbar experiment

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        default:
519            g_message( "FIXME" );
520            return TRUE;
521    }
522}
523
524/***
525****
526****  STATES
527****
528***/
529
530enum
531{
532    STATE_FILTER_ALL,
533    STATE_FILTER_DOWNLOADING,
534    STATE_FILTER_SEEDING,
535    STATE_FILTER_ACTIVE,
536    STATE_FILTER_PAUSED,
537    STATE_FILTER_QUEUED,
538    STATE_FILTER_CHECKING,
539    STATE_FILTER_ERROR,
540    STATE_FILTER_SEPARATOR
541};
542
543enum
544{
545    STATE_FILTER_COL_NAME,
546    STATE_FILTER_COL_COUNT,
547    STATE_FILTER_COL_TYPE,
548    STATE_FILTER_N_COLS
549};
550
551static gboolean
552state_is_it_a_separator( GtkTreeModel * m, GtkTreeIter * i, gpointer d UNUSED )
553{
554    int type;
555    gtk_tree_model_get( m, i, STATE_FILTER_COL_TYPE, &type, -1 );
556    return type == STATE_FILTER_SEPARATOR;
557}
558
559static gboolean
560test_torrent_state( tr_torrent * tor, int type )
561{
562    const tr_stat * st = tr_torrentStat( tor );
563
564    switch( type )
565    {
566        case STATE_FILTER_DOWNLOADING:
567            return st->activity == TR_STATUS_DOWNLOAD;
568
569        case STATE_FILTER_SEEDING:
570            return st->activity == TR_STATUS_SEED;
571
572        case STATE_FILTER_ACTIVE:
573            return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0;
574
575        case STATE_FILTER_PAUSED:
576            return st->activity == TR_STATUS_STOPPED;
577
578        case STATE_FILTER_QUEUED:
579            return FALSE;
580
581        case STATE_FILTER_CHECKING:
582            return ( st->activity == TR_STATUS_CHECK_WAIT )
583                || ( st->activity == TR_STATUS_CHECK );
584
585        case STATE_FILTER_ERROR:
586            return st->error != 0;
587
588        default: /* STATE_FILTER_ALL */
589            return TRUE;
590
591    }
592}
593
594static gboolean
595testState( GtkWidget * state_combo, tr_torrent * tor )
596{
597    int type;
598    GtkTreeIter iter;
599    GtkComboBox * combo = GTK_COMBO_BOX( state_combo );
600    GtkTreeModel * model = gtk_combo_box_get_model( combo );
601
602    if( !gtk_combo_box_get_active_iter( combo, &iter ) )
603        return TRUE;
604
605    gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
606    return test_torrent_state( tor, type );
607}
608
609static void
610status_model_update_count( GtkListStore * store, GtkTreeIter * iter, int n )
611{
612    int count;
613    gtk_tree_model_get( GTK_TREE_MODEL( store ), iter, STATE_FILTER_COL_COUNT, &count, -1 );
614    if( n != count )
615        gtk_list_store_set( store, iter, STATE_FILTER_COL_COUNT, n, -1 );
616}
617
618static void
619state_filter_model_update( GtkListStore * store )
620{
621    GtkTreeIter iter;
622    GtkTreeModel * model = GTK_TREE_MODEL( store );
623    GObject * o = G_OBJECT( store );
624    GtkTreeModel * tmodel = GTK_TREE_MODEL( g_object_get_data( o, TORRENT_MODEL_KEY ) );
625
626    g_object_steal_data( o, DIRTY_KEY );
627
628    if( gtk_tree_model_get_iter_first( model, &iter )) do
629    {
630        int hits;
631        int type;
632        GtkTreeIter torrent_iter;
633
634        gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
635
636        hits = 0;
637        if( gtk_tree_model_get_iter_first( tmodel, &torrent_iter )) do {
638            tr_torrent * tor;
639            gtk_tree_model_get( tmodel, &torrent_iter, MC_TORRENT_RAW, &tor, -1 );
640            if( test_torrent_state( tor, type ) )
641                ++hits;
642        } while( gtk_tree_model_iter_next( tmodel, &torrent_iter ) );
643
644        status_model_update_count( store, &iter, hits );
645
646    } while( gtk_tree_model_iter_next( model, &iter ) );
647}
648
649static GtkTreeModel *
650state_filter_model_new( GtkTreeModel * tmodel )
651{
652    int i, n;
653    struct {
654        int type;
655        const char * name;
656    } types[] = {
657        { STATE_FILTER_ALL, N_( "All" ) },
658        { STATE_FILTER_SEPARATOR, NULL },
659        { STATE_FILTER_DOWNLOADING, N_( "Downloading" ) },
660        { STATE_FILTER_SEEDING, N_( "Seeding" ) },
661        { STATE_FILTER_ACTIVE, N_( "Active" ) },
662        { STATE_FILTER_PAUSED, N_( "Paused" ) },
663        { STATE_FILTER_QUEUED, N_( "Queued" ) },
664        { STATE_FILTER_CHECKING, N_( "Verifying" ) },
665        { STATE_FILTER_ERROR, N_( "Error" ) }
666    };
667    GtkListStore * store;
668
669    store = gtk_list_store_new( STATE_FILTER_N_COLS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT );
670    for( i=0, n=G_N_ELEMENTS(types); i<n; ++i )
671        gtk_list_store_insert_with_values( store, NULL, -1,
672                                           STATE_FILTER_COL_NAME, _( types[i].name ),
673                                           STATE_FILTER_COL_TYPE, types[i].type,
674                                           -1 );
675
676    g_object_set_data( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel );
677    state_filter_model_update( store );
678    return GTK_TREE_MODEL( store );
679}
680
681static void
682state_model_update_idle( gpointer state_model )
683{
684    GObject * o = G_OBJECT( state_model );
685    gboolean pending = GPOINTER_TO_INT( g_object_get_data( o, DIRTY_KEY ) );
686    if( !pending )
687    {
688        GSourceFunc func = (GSourceFunc) state_filter_model_update;
689        g_object_set_data( o, DIRTY_KEY, GINT_TO_POINTER(1) );
690        gtr_idle_add( func, state_model );
691    }
692}
693
694static void
695state_torrent_model_row_changed( GtkTreeModel  * tmodel UNUSED,
696                                 GtkTreePath   * path UNUSED,
697                                 GtkTreeIter   * iter UNUSED,
698                                 gpointer        state_model )
699{
700    state_model_update_idle( state_model );
701}
702
703static void
704state_torrent_model_row_deleted_cb( GtkTreeModel  * tmodel UNUSED,
705                                    GtkTreePath   * path UNUSED,
706                                    gpointer        state_model )
707{
708    state_model_update_idle( state_model );
709}
710
711static GtkWidget *
712state_combo_box_new( GtkTreeModel * tmodel )
713{
714    GtkWidget * c;
715    GtkCellRenderer * r;
716    GtkTreeModel * state_model;
717
718    state_model = state_filter_model_new( tmodel );
719    c = gtk_combo_box_new_with_model( state_model );
720    gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ),
721                                          state_is_it_a_separator, NULL, NULL );
722    gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 );
723
724    r = gtk_cell_renderer_text_new( );
725    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, TRUE );
726    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r, "text", STATE_FILTER_COL_NAME, NULL );
727
728    r = number_renderer_new( );
729    gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE );
730    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_hit_count_func, NULL, NULL );
731
732    g_signal_connect( tmodel, "row-changed",
733                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
734    g_signal_connect( tmodel, "row-inserted",
735                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
736    g_signal_connect( tmodel, "row-deleted",
737                      G_CALLBACK( state_torrent_model_row_deleted_cb ), state_model );
738
739    return c;
740}
741
742/****
743*****
744*****  ENTRY FIELD
745*****
746****/
747
748enum
749{
750    TEXT_MODE_NAME,
751    TEXT_MODE_FILES,
752    TEXT_MODE_TRACKER,
753    TEXT_MODE_N_TYPES
754};
755
756static gboolean
757testText( const tr_torrent * tor, const char * key, int mode )
758{
759    gboolean ret;
760    tr_file_index_t i;
761    const tr_info * inf = tr_torrentInfo( tor );
762
763    switch( mode )
764    {
765        case TEXT_MODE_FILES:
766            for( i=0; i<inf->fileCount && !ret; ++i ) {
767                char * pch = g_utf8_casefold( inf->files[i].name, -1 );
768                ret = !key || strstr( pch, key ) != NULL;
769                g_free( pch );
770            }
771            break;
772
773        case TEXT_MODE_TRACKER:
774            if( inf->trackerCount > 0 ) {
775                char * pch = g_utf8_casefold( inf->trackers[0].announce, -1 );
776                ret = !key || ( strstr( pch, key ) != NULL );
777                g_free( pch );
778            }
779            break;
780
781        default: /* NAME */
782            if( !inf->name )
783                ret = TRUE;
784            else {
785                char * pch = g_utf8_casefold( inf->name, -1 );
786                ret = !key || ( strstr( pch, key ) != NULL );
787                g_free( pch );
788            }
789            break;
790    }
791
792    return ret;
793}
794
795
796#ifdef USE_SEXY
797static void
798entry_icon_released( SexyIconEntry           * entry  UNUSED,
799                     SexyIconEntryPosition     icon_pos,
800                     int                       button UNUSED,
801                     gpointer                  menu )
802{
803    if( icon_pos == SEXY_ICON_ENTRY_PRIMARY )
804        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
805                        gtk_get_current_event_time( ) );
806}
807#else
808static void
809entry_icon_release( GtkEntry              * entry  UNUSED,
810                    GtkEntryIconPosition    icon_pos,
811                    GdkEventButton        * event  UNUSED,
812                    gpointer                menu )
813{
814    if( icon_pos == GTK_ENTRY_ICON_SECONDARY )
815        gtk_entry_set_text( entry, "" );
816
817    if( icon_pos == GTK_ENTRY_ICON_PRIMARY )
818        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
819                        gtk_get_current_event_time( ) );
820}
821#endif
822
823static void
824filter_entry_changed( GtkEditable * e, gpointer filter_model )
825{
826    char * pch;
827    char * folded;
828   
829    pch = gtk_editable_get_chars( e, 0, -1 );
830    folded = g_utf8_casefold( pch, -1 );
831    g_object_set_data_full( filter_model, TEXT_KEY, folded, g_free );
832    g_free( pch );
833
834    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
835}
836
837static void
838filter_text_toggled_cb( GtkCheckMenuItem * menu_item, gpointer filter_model )
839{
840    g_object_set_data( filter_model, TEXT_MODE_KEY,
841                       g_object_get_data( G_OBJECT( menu_item ), TEXT_MODE_KEY ) );
842    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
843}
844
845/*****
846******
847******
848******
849*****/
850
851struct filter_data
852{
853    GtkWidget * state;
854    GtkWidget * category;
855    GtkWidget * entry;
856    GtkTreeModel * filter_model;
857};
858
859static gboolean
860is_row_visible( GtkTreeModel * model, GtkTreeIter * iter, gpointer vdata )
861{
862    int mode;
863    gboolean b;
864    const char * text;
865    tr_torrent * tor;
866    struct filter_data * data = vdata;
867    GObject * o = G_OBJECT( data->filter_model );
868
869    gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 );
870
871    text = (const char*) g_object_get_data( o, TEXT_KEY );
872    mode = GPOINTER_TO_INT( g_object_get_data( o, TEXT_MODE_KEY ) );
873
874    b = ( tor != NULL ) && testCategory( data->category, tor )
875                        && testState( data->state, tor )
876                        && testText( tor, text, mode );
877
878    return b;
879}
880
881static void
882selection_changed_cb( GtkComboBox * combo UNUSED, gpointer vdata )
883{
884    struct filter_data * data = vdata;
885    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter_model ) );
886}
887
888GtkWidget *
889gtr_filter_bar_new( GtkTreeModel * tmodel, GtkTreeModel ** filter_model )
890{
891    int i;
892    GtkWidget * l;
893    GtkWidget * w;
894    GtkWidget * h;
895    GtkWidget * s;
896    GtkWidget * menu;
897    GtkWidget * state;
898    GtkWidget * category;
899    GSList * sl;
900    const char * str;
901    struct filter_data * data;
902    const char *  filter_text_names[] = {
903        N_( "Name" ), N_( "Files" ), N_( "Tracker" )
904    };
905
906
907    data = g_new( struct filter_data, 1 );
908    data->state = state = state_combo_box_new( tmodel );
909    data->category = category = category_combo_box_new( tmodel );
910    data->entry = NULL;
911    data->filter_model = gtk_tree_model_filter_new( tmodel, NULL );
912
913    g_object_set( G_OBJECT( data->category ), "width-request", 150, NULL );
914
915    gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( data->filter_model ),
916                                            is_row_visible, data, g_free );
917
918    g_signal_connect( data->category, "changed", G_CALLBACK( selection_changed_cb ), data );
919    g_signal_connect( data->state, "changed", G_CALLBACK( selection_changed_cb ), data );
920
921
922    h = gtk_hbox_new( FALSE, GUI_PAD_SMALL );
923
924    /* add the category combobox */
925    str = _( "Show _Category:" );
926    w = category;
927    l = gtk_label_new( NULL );
928    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
929    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
930    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
931    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
932
933    /* add a spacer */
934    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
935    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
936    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
937
938    /* add the state combobox */
939    str = _( "_State:" );
940    w = state;
941    l = gtk_label_new( NULL );
942    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
943    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
944    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
945    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
946
947    /* add a spacer */
948    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
949    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
950    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
951
952    /* add the entry field */
953#ifdef USE_SEXY
954    s = sexy_icon_entry_new( );
955    sexy_icon_entry_add_clear_button( SEXY_ICON_ENTRY( s ) );
956    w = gtk_image_new_from_stock( GTK_STOCK_FIND, GTK_ICON_SIZE_MENU );
957    sexy_icon_entry_set_icon( SEXY_ICON_ENTRY( s ),
958                              SEXY_ICON_ENTRY_PRIMARY,
959                              GTK_IMAGE( w ) );
960    g_object_unref( w );
961    sexy_icon_entry_set_icon_highlight( SEXY_ICON_ENTRY( s ),
962                                        SEXY_ICON_ENTRY_PRIMARY, TRUE );
963#else
964    s = gtk_entry_new( );
965    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
966                                   GTK_ENTRY_ICON_PRIMARY,
967                                   GTK_STOCK_FIND);
968    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
969                                   GTK_ENTRY_ICON_SECONDARY,
970                                   GTK_STOCK_CLEAR );
971#endif
972
973    menu = gtk_menu_new( );
974    sl = NULL;
975    for( i=0; i<TEXT_MODE_N_TYPES; ++i )
976    {
977        const char * name = _( filter_text_names[i] );
978        GtkWidget *  w = gtk_radio_menu_item_new_with_label ( sl, name );
979        sl = gtk_radio_menu_item_get_group( GTK_RADIO_MENU_ITEM( w ) );
980        g_object_set_data( G_OBJECT( w ), TEXT_MODE_KEY, GINT_TO_POINTER( i ) );
981        g_signal_connect( w, "toggled", G_CALLBACK( filter_text_toggled_cb ), data->filter_model ); 
982        gtk_menu_shell_append( GTK_MENU_SHELL( menu ), w );
983        gtk_widget_show( w );
984    }
985#ifdef USE_SEXY
986    g_signal_connect( s, "icon-released", G_CALLBACK( entry_icon_released ), menu );
987#else
988    g_signal_connect( s, "icon-release", G_CALLBACK( entry_icon_release ), menu );
989#endif
990
991    gtk_box_pack_start( GTK_BOX( h ), s, TRUE, TRUE, 0 );
992    g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), data->filter_model );
993
994    *filter_model = data->filter_model;
995    return h;
996}
Note: See TracBrowser for help on using the repository browser.