source: trunk/gtk/filter.c @ 10380

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

(trunk gtk) add favicon support to the new filterbar

File size: 32.5 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 ? 16 : 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_CHECKING,
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 || st->peersGettingFromUs > 0;
629
630        case STATE_FILTER_PAUSED:
631            return st->activity == TR_STATUS_STOPPED;
632
633        case STATE_FILTER_QUEUED:
634            return FALSE;
635
636        case STATE_FILTER_CHECKING:
637            return ( st->activity == TR_STATUS_CHECK_WAIT )
638                || ( st->activity == TR_STATUS_CHECK );
639
640        case STATE_FILTER_ERROR:
641            return st->error != 0;
642
643        default: /* STATE_FILTER_ALL */
644            return TRUE;
645
646    }
647}
648
649static gboolean
650testState( GtkWidget * state_combo, tr_torrent * tor )
651{
652    int type;
653    GtkTreeIter iter;
654    GtkComboBox * combo = GTK_COMBO_BOX( state_combo );
655    GtkTreeModel * model = gtk_combo_box_get_model( combo );
656
657    if( !gtk_combo_box_get_active_iter( combo, &iter ) )
658        return TRUE;
659
660    gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
661    return test_torrent_state( tor, type );
662}
663
664static void
665status_model_update_count( GtkListStore * store, GtkTreeIter * iter, int n )
666{
667    int count;
668    gtk_tree_model_get( GTK_TREE_MODEL( store ), iter, STATE_FILTER_COL_COUNT, &count, -1 );
669    if( n != count )
670        gtk_list_store_set( store, iter, STATE_FILTER_COL_COUNT, n, -1 );
671}
672
673static void
674state_filter_model_update( GtkListStore * store )
675{
676    GtkTreeIter iter;
677    GtkTreeModel * model = GTK_TREE_MODEL( store );
678    GObject * o = G_OBJECT( store );
679    GtkTreeModel * tmodel = GTK_TREE_MODEL( g_object_get_data( o, TORRENT_MODEL_KEY ) );
680
681    g_object_steal_data( o, DIRTY_KEY );
682
683    if( gtk_tree_model_get_iter_first( model, &iter )) do
684    {
685        int hits;
686        int type;
687        GtkTreeIter torrent_iter;
688
689        gtk_tree_model_get( model, &iter, STATE_FILTER_COL_TYPE, &type, -1 );
690
691        hits = 0;
692        if( gtk_tree_model_get_iter_first( tmodel, &torrent_iter )) do {
693            tr_torrent * tor;
694            gtk_tree_model_get( tmodel, &torrent_iter, MC_TORRENT_RAW, &tor, -1 );
695            if( test_torrent_state( tor, type ) )
696                ++hits;
697        } while( gtk_tree_model_iter_next( tmodel, &torrent_iter ) );
698
699        status_model_update_count( store, &iter, hits );
700
701    } while( gtk_tree_model_iter_next( model, &iter ) );
702}
703
704static GtkTreeModel *
705state_filter_model_new( GtkTreeModel * tmodel )
706{
707    int i, n;
708    struct {
709        int type;
710        const char * name;
711    } types[] = {
712        { STATE_FILTER_ALL, N_( "All" ) },
713        { STATE_FILTER_SEPARATOR, NULL },
714        { STATE_FILTER_DOWNLOADING, N_( "Downloading" ) },
715        { STATE_FILTER_SEEDING, N_( "Seeding" ) },
716        { STATE_FILTER_ACTIVE, N_( "Active" ) },
717        { STATE_FILTER_PAUSED, N_( "Paused" ) },
718        { STATE_FILTER_QUEUED, N_( "Queued" ) },
719        { STATE_FILTER_CHECKING, N_( "Verifying" ) },
720        { STATE_FILTER_ERROR, N_( "Error" ) }
721    };
722    GtkListStore * store;
723
724    store = gtk_list_store_new( STATE_FILTER_N_COLS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT );
725    for( i=0, n=G_N_ELEMENTS(types); i<n; ++i )
726        gtk_list_store_insert_with_values( store, NULL, -1,
727                                           STATE_FILTER_COL_NAME, _( types[i].name ),
728                                           STATE_FILTER_COL_TYPE, types[i].type,
729                                           -1 );
730
731    g_object_set_data( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel );
732    state_filter_model_update( store );
733    return GTK_TREE_MODEL( store );
734}
735
736static void
737state_model_update_idle( gpointer state_model )
738{
739    GObject * o = G_OBJECT( state_model );
740    gboolean pending = GPOINTER_TO_INT( g_object_get_data( o, DIRTY_KEY ) );
741    if( !pending )
742    {
743        GSourceFunc func = (GSourceFunc) state_filter_model_update;
744        g_object_set_data( o, DIRTY_KEY, GINT_TO_POINTER(1) );
745        gtr_idle_add( func, state_model );
746    }
747}
748
749static void
750state_torrent_model_row_changed( GtkTreeModel  * tmodel UNUSED,
751                                 GtkTreePath   * path UNUSED,
752                                 GtkTreeIter   * iter UNUSED,
753                                 gpointer        state_model )
754{
755    state_model_update_idle( state_model );
756}
757
758static void
759state_torrent_model_row_deleted_cb( GtkTreeModel  * tmodel UNUSED,
760                                    GtkTreePath   * path UNUSED,
761                                    gpointer        state_model )
762{
763    state_model_update_idle( state_model );
764}
765
766static GtkWidget *
767state_combo_box_new( GtkTreeModel * tmodel )
768{
769    GtkWidget * c;
770    GtkCellRenderer * r;
771    GtkTreeModel * state_model;
772
773    state_model = state_filter_model_new( tmodel );
774    c = gtk_combo_box_new_with_model( state_model );
775    gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ),
776                                          state_is_it_a_separator, NULL, NULL );
777    gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 );
778
779    r = gtk_cell_renderer_text_new( );
780    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, TRUE );
781    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r, "text", STATE_FILTER_COL_NAME, NULL );
782
783    r = number_renderer_new( );
784    gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE );
785    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r, render_hit_count_func, NULL, NULL );
786
787    g_signal_connect( tmodel, "row-changed",
788                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
789    g_signal_connect( tmodel, "row-inserted",
790                      G_CALLBACK( state_torrent_model_row_changed ), state_model );
791    g_signal_connect( tmodel, "row-deleted",
792                      G_CALLBACK( state_torrent_model_row_deleted_cb ), state_model );
793
794    return c;
795}
796
797/****
798*****
799*****  ENTRY FIELD
800*****
801****/
802
803enum
804{
805    TEXT_MODE_NAME,
806    TEXT_MODE_FILES,
807    TEXT_MODE_TRACKER,
808    TEXT_MODE_N_TYPES
809};
810
811static gboolean
812testText( const tr_torrent * tor, const char * key, int mode )
813{
814    gboolean ret;
815    tr_file_index_t i;
816    const tr_info * inf = tr_torrentInfo( tor );
817
818    switch( mode )
819    {
820        case TEXT_MODE_FILES:
821            for( i=0; i<inf->fileCount && !ret; ++i ) {
822                char * pch = g_utf8_casefold( inf->files[i].name, -1 );
823                ret = !key || strstr( pch, key ) != NULL;
824                g_free( pch );
825            }
826            break;
827
828        case TEXT_MODE_TRACKER:
829            if( inf->trackerCount > 0 ) {
830                char * pch = g_utf8_casefold( inf->trackers[0].announce, -1 );
831                ret = !key || ( strstr( pch, key ) != NULL );
832                g_free( pch );
833            }
834            break;
835
836        default: /* NAME */
837            if( !inf->name )
838                ret = TRUE;
839            else {
840                char * pch = g_utf8_casefold( inf->name, -1 );
841                ret = !key || ( strstr( pch, key ) != NULL );
842                g_free( pch );
843            }
844            break;
845    }
846
847    return ret;
848}
849
850
851#ifdef USE_SEXY
852static void
853entry_icon_released( SexyIconEntry           * entry  UNUSED,
854                     SexyIconEntryPosition     icon_pos,
855                     int                       button UNUSED,
856                     gpointer                  menu )
857{
858    if( icon_pos == SEXY_ICON_ENTRY_PRIMARY )
859        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
860                        gtk_get_current_event_time( ) );
861}
862#else
863static void
864entry_icon_release( GtkEntry              * entry  UNUSED,
865                    GtkEntryIconPosition    icon_pos,
866                    GdkEventButton        * event  UNUSED,
867                    gpointer                menu )
868{
869    if( icon_pos == GTK_ENTRY_ICON_SECONDARY )
870        gtk_entry_set_text( entry, "" );
871
872    if( icon_pos == GTK_ENTRY_ICON_PRIMARY )
873        gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, 0,
874                        gtk_get_current_event_time( ) );
875}
876#endif
877
878static void
879filter_entry_changed( GtkEditable * e, gpointer filter_model )
880{
881    char * pch;
882    char * folded;
883   
884    pch = gtk_editable_get_chars( e, 0, -1 );
885    folded = g_utf8_casefold( pch, -1 );
886    g_object_set_data_full( filter_model, TEXT_KEY, folded, g_free );
887    g_free( pch );
888
889    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
890}
891
892static void
893filter_text_toggled_cb( GtkCheckMenuItem * menu_item, gpointer filter_model )
894{
895    g_object_set_data( filter_model, TEXT_MODE_KEY,
896                       g_object_get_data( G_OBJECT( menu_item ), TEXT_MODE_KEY ) );
897    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
898}
899
900/*****
901******
902******
903******
904*****/
905
906struct filter_data
907{
908    GtkWidget * state;
909    GtkWidget * category;
910    GtkWidget * entry;
911    GtkTreeModel * filter_model;
912};
913
914static gboolean
915is_row_visible( GtkTreeModel * model, GtkTreeIter * iter, gpointer vdata )
916{
917    int mode;
918    gboolean b;
919    const char * text;
920    tr_torrent * tor;
921    struct filter_data * data = vdata;
922    GObject * o = G_OBJECT( data->filter_model );
923
924    gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 );
925
926    text = (const char*) g_object_get_data( o, TEXT_KEY );
927    mode = GPOINTER_TO_INT( g_object_get_data( o, TEXT_MODE_KEY ) );
928
929    b = ( tor != NULL ) && testCategory( data->category, tor )
930                        && testState( data->state, tor )
931                        && testText( tor, text, mode );
932
933    return b;
934}
935
936static void
937selection_changed_cb( GtkComboBox * combo UNUSED, gpointer vdata )
938{
939    struct filter_data * data = vdata;
940    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter_model ) );
941}
942
943GtkWidget *
944gtr_filter_bar_new( tr_session * session, GtkTreeModel * tmodel, GtkTreeModel ** filter_model )
945{
946    int i;
947    GtkWidget * l;
948    GtkWidget * w;
949    GtkWidget * h;
950    GtkWidget * s;
951    GtkWidget * menu;
952    GtkWidget * state;
953    GtkWidget * category;
954    GSList * sl;
955    const char * str;
956    struct filter_data * data;
957    const char *  filter_text_names[] = {
958        N_( "Name" ), N_( "Files" ), N_( "Tracker" )
959    };
960
961
962    data = g_new( struct filter_data, 1 );
963    data->state = state = state_combo_box_new( tmodel );
964    data->category = category = category_combo_box_new( tmodel );
965    data->entry = NULL;
966    data->filter_model = gtk_tree_model_filter_new( tmodel, NULL );
967
968    g_object_set( G_OBJECT( data->category ), "width-request", 150, NULL );
969    g_object_set_data( G_OBJECT( gtk_combo_box_get_model( GTK_COMBO_BOX( data->category ) ) ), SESSION_KEY, session );
970
971    gtk_tree_model_filter_set_visible_func( GTK_TREE_MODEL_FILTER( data->filter_model ),
972                                            is_row_visible, data, g_free );
973
974    g_signal_connect( data->category, "changed", G_CALLBACK( selection_changed_cb ), data );
975    g_signal_connect( data->state, "changed", G_CALLBACK( selection_changed_cb ), data );
976
977
978    h = gtk_hbox_new( FALSE, GUI_PAD_SMALL );
979
980    /* add the category combobox */
981    str = _( "_Category:" );
982    w = category;
983    l = gtk_label_new( NULL );
984    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
985    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
986    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
987    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
988
989    /* add a spacer */
990    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
991    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
992    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
993
994    /* add the state combobox */
995    str = _( "_State:" );
996    w = state;
997    l = gtk_label_new( NULL );
998    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
999    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
1000    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
1001    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
1002
1003    /* add a spacer */
1004    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1005    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
1006    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1007
1008    /* add the entry field */
1009#ifdef USE_SEXY
1010    s = sexy_icon_entry_new( );
1011    sexy_icon_entry_add_clear_button( SEXY_ICON_ENTRY( s ) );
1012    w = gtk_image_new_from_stock( GTK_STOCK_FIND, GTK_ICON_SIZE_MENU );
1013    sexy_icon_entry_set_icon( SEXY_ICON_ENTRY( s ),
1014                              SEXY_ICON_ENTRY_PRIMARY,
1015                              GTK_IMAGE( w ) );
1016    g_object_unref( w );
1017    sexy_icon_entry_set_icon_highlight( SEXY_ICON_ENTRY( s ),
1018                                        SEXY_ICON_ENTRY_PRIMARY, TRUE );
1019#else
1020    s = gtk_entry_new( );
1021    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
1022                                   GTK_ENTRY_ICON_PRIMARY,
1023                                   GTK_STOCK_FIND);
1024    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ),
1025                                   GTK_ENTRY_ICON_SECONDARY,
1026                                   GTK_STOCK_CLEAR );
1027#endif
1028
1029    menu = gtk_menu_new( );
1030    sl = NULL;
1031    for( i=0; i<TEXT_MODE_N_TYPES; ++i )
1032    {
1033        const char * name = _( filter_text_names[i] );
1034        GtkWidget *  w = gtk_radio_menu_item_new_with_label ( sl, name );
1035        sl = gtk_radio_menu_item_get_group( GTK_RADIO_MENU_ITEM( w ) );
1036        g_object_set_data( G_OBJECT( w ), TEXT_MODE_KEY, GINT_TO_POINTER( i ) );
1037        g_signal_connect( w, "toggled", G_CALLBACK( filter_text_toggled_cb ), data->filter_model ); 
1038        gtk_menu_shell_append( GTK_MENU_SHELL( menu ), w );
1039        gtk_widget_show( w );
1040    }
1041#ifdef USE_SEXY
1042    g_signal_connect( s, "icon-released", G_CALLBACK( entry_icon_released ), menu );
1043#else
1044    g_signal_connect( s, "icon-release", G_CALLBACK( entry_icon_release ), menu );
1045#endif
1046
1047    gtk_box_pack_start( GTK_BOX( h ), s, TRUE, TRUE, 0 );
1048    g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), data->filter_model );
1049
1050    *filter_model = data->filter_model;
1051    return h;
1052}
Note: See TracBrowser for help on using the repository browser.