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