source: trunk/gtk/filter.c @ 13192

Last change on this file since 13192 was 13192, checked in by jordan, 10 years ago

(trunk gtk) To improve translations, help gettext to differentiate between the gerund and verb forms of some -ing words like "Seeding" and "Downloading" -- fixed.

I wasn't sure how to do this, so for the benefit of my future self or anyone else who's interested, here are some breadcrumbs I found: https://trac.transmissionbt.com/ticket/4717#comment:6

  • Property svn:keywords set to Date Rev Author Id
File size: 32.8 KB
Line 
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 13192 2012-02-03 17:12:17Z 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 * name;
693        const char * stock_id;
694    } types[] = {
695        { ACTIVITY_FILTER_ALL, N_( "All" ), NULL },
696        { ACTIVITY_FILTER_SEPARATOR, NULL, NULL },
697        { ACTIVITY_FILTER_ACTIVE, N_( "Active" ), GTK_STOCK_EXECUTE },
698        { ACTIVITY_FILTER_DOWNLOADING, NC_( "Verb", "Downloading" ), GTK_STOCK_GO_DOWN },
699        { ACTIVITY_FILTER_SEEDING, NC_( "Verb", "Seeding" ), GTK_STOCK_GO_UP },
700        { ACTIVITY_FILTER_PAUSED, N_( "Paused" ), GTK_STOCK_MEDIA_PAUSE },
701        { ACTIVITY_FILTER_FINISHED, N_( "Finished" ), NULL },
702        { ACTIVITY_FILTER_VERIFYING, NC_( "Verb", "Verifying" ), GTK_STOCK_REFRESH },
703        { ACTIVITY_FILTER_ERROR, N_( "Error" ), GTK_STOCK_DIALOG_ERROR }
704    };
705    GtkListStore * store = gtk_list_store_new( ACTIVITY_FILTER_N_COLS,
706                                               G_TYPE_STRING,
707                                               G_TYPE_INT,
708                                               G_TYPE_INT,
709                                               G_TYPE_STRING );
710    for( i=0, n=G_N_ELEMENTS(types); i<n; ++i )
711        gtk_list_store_insert_with_values( store, NULL, -1,
712            ACTIVITY_FILTER_COL_NAME, _( types[i].name ),
713            ACTIVITY_FILTER_COL_TYPE, types[i].type,
714            ACTIVITY_FILTER_COL_STOCK_ID, types[i].stock_id,
715            -1 );
716
717    g_object_set_qdata( G_OBJECT( store ), TORRENT_MODEL_KEY, tmodel );
718    activity_filter_model_update( store );
719    return GTK_TREE_MODEL( store );
720}
721
722static void
723render_activity_pixbuf_func( GtkCellLayout    * cell_layout UNUSED,
724                             GtkCellRenderer  * cell_renderer,
725                             GtkTreeModel     * tree_model,
726                             GtkTreeIter      * iter,
727                             gpointer           data UNUSED )
728{
729    int type;
730    int width;
731    int ypad;
732    const gboolean leaf = !gtk_tree_model_iter_has_child( tree_model, iter );
733
734    gtk_tree_model_get( tree_model, iter, ACTIVITY_FILTER_COL_TYPE, &type, -1 );
735    width = type == ACTIVITY_FILTER_ALL ? 0 : 20;
736    ypad = type == ACTIVITY_FILTER_ALL ? 0 : 2;
737
738    g_object_set( cell_renderer, "width", width,
739                                 "sensitive", leaf,
740                                 "ypad", ypad,
741                                 NULL );
742}
743
744static void
745activity_model_update_idle( gpointer activity_model )
746{
747    GObject * o = G_OBJECT( activity_model );
748    const gboolean pending = g_object_get_qdata( o, DIRTY_KEY ) != NULL;
749    if( !pending )
750    {
751        GSourceFunc func = (GSourceFunc) activity_filter_model_update;
752        g_object_set_qdata( o, DIRTY_KEY, GINT_TO_POINTER(1) );
753        gdk_threads_add_idle( func, activity_model );
754    }
755}
756
757static void
758activity_torrent_model_row_changed( GtkTreeModel  * tmodel UNUSED,
759                                    GtkTreePath   * path UNUSED,
760                                    GtkTreeIter   * iter UNUSED,
761                                    gpointer        activity_model )
762{
763    activity_model_update_idle( activity_model );
764}
765
766static void
767activity_torrent_model_row_deleted_cb( GtkTreeModel  * tmodel UNUSED,
768                                       GtkTreePath   * path UNUSED,
769                                       gpointer        activity_model )
770{
771    activity_model_update_idle( activity_model );
772}
773
774static void
775disconnect_activity_model_callbacks( gpointer tmodel, GObject * cat_model )
776{
777    g_signal_handlers_disconnect_by_func( tmodel, activity_torrent_model_row_changed, cat_model );
778    g_signal_handlers_disconnect_by_func( tmodel, activity_torrent_model_row_deleted_cb, cat_model );
779}
780
781static GtkWidget *
782activity_combo_box_new( GtkTreeModel * tmodel )
783{
784    GtkWidget * c;
785    GtkCellRenderer * r;
786    GtkTreeModel * activity_model;
787
788    activity_model = activity_filter_model_new( tmodel );
789    c = gtk_combo_box_new_with_model( activity_model );
790    g_object_unref( activity_model );
791    gtk_combo_box_set_row_separator_func( GTK_COMBO_BOX( c ),
792                                       activity_is_it_a_separator, NULL, NULL );
793    gtk_combo_box_set_active( GTK_COMBO_BOX( c ), 0 );
794
795    r = gtk_cell_renderer_pixbuf_new( );
796    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, FALSE );
797    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r,
798                                    "stock-id", ACTIVITY_FILTER_COL_STOCK_ID,
799                                    NULL );
800    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r,
801                                        render_activity_pixbuf_func, NULL, NULL );
802
803    r = gtk_cell_renderer_text_new( );
804    gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( c ), r, TRUE );
805    gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( c ), r,
806                                    "text", ACTIVITY_FILTER_COL_NAME,
807                                    NULL );
808
809    r = number_renderer_new( );
810    gtk_cell_layout_pack_end( GTK_CELL_LAYOUT( c ), r, TRUE );
811    gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( c ), r,
812                                        render_number_func, NULL, NULL );
813
814    g_object_weak_ref( G_OBJECT( activity_model ), disconnect_activity_model_callbacks, tmodel );
815    g_signal_connect( tmodel, "row-changed", G_CALLBACK( activity_torrent_model_row_changed ), activity_model );
816    g_signal_connect( tmodel, "row-inserted", G_CALLBACK( activity_torrent_model_row_changed ), activity_model );
817    g_signal_connect( tmodel, "row-deleted", G_CALLBACK( activity_torrent_model_row_deleted_cb ), activity_model );
818
819    return c;
820}
821
822/****
823*****
824*****  ENTRY FIELD
825*****
826****/
827
828static gboolean
829testText( const tr_torrent * tor, const char * key )
830{
831    gboolean ret = FALSE;
832
833    if( !key || !*key )
834    {
835        ret = TRUE;
836    }
837    else
838    {
839        tr_file_index_t i;
840        const tr_info * inf = tr_torrentInfo( tor );
841
842        /* test the torrent name... */
843        {
844            char * pch = g_utf8_casefold( tr_torrentName( tor ), -1 );
845            ret = !key || strstr( pch, key ) != NULL;
846            g_free( pch );
847        }
848
849        /* test the files... */
850        for( i=0; i<inf->fileCount && !ret; ++i )
851        {
852            char * pch = g_utf8_casefold( inf->files[i].name, -1 );
853            ret = !key || strstr( pch, key ) != NULL;
854            g_free( pch );
855        }
856    }
857
858    return ret;
859}
860
861static void
862entry_clear( GtkEntry * e )
863{
864    gtk_entry_set_text( e, "" );
865}
866
867static void
868filter_entry_changed( GtkEditable * e, gpointer filter_model )
869{
870    char * pch;
871    char * folded;
872
873    pch = gtk_editable_get_chars( e, 0, -1 );
874    folded = g_utf8_casefold( pch, -1 );
875    g_strstrip( folded );
876    g_object_set_qdata_full( filter_model, TEXT_KEY, folded, g_free );
877    g_free( pch );
878
879    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( filter_model ) );
880}
881
882/*****
883******
884******
885******
886*****/
887
888struct filter_data
889{
890    GtkWidget * activity;
891    GtkWidget * category;
892    GtkWidget * entry;
893    GtkTreeModel * filter_model;
894    int active_activity_type;
895    int active_category_type;
896    char * active_category_host;
897};
898
899static gboolean
900is_row_visible( GtkTreeModel * model, GtkTreeIter * iter, gpointer vdata )
901{
902    const char * text;
903    tr_torrent * tor;
904    struct filter_data * data = vdata;
905    GObject * o = G_OBJECT( data->filter_model );
906
907    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
908
909    text = (const char*) g_object_get_qdata( o, TEXT_KEY );
910
911    return ( tor != NULL ) && test_category( tor, data->active_category_type, data->active_category_host )
912                           && test_torrent_activity( tor, data->active_activity_type )
913                           && testText( tor, text );
914}
915
916static void
917selection_changed_cb( GtkComboBox * combo, gpointer vdata )
918{
919    int type;
920    char * host;
921    GtkTreeIter iter;
922    GtkTreeModel * model;
923    struct filter_data * data = vdata;
924
925    /* set data->active_activity_type from the activity combobox */
926    combo = GTK_COMBO_BOX( data->activity );
927    model = gtk_combo_box_get_model( combo );
928    if( gtk_combo_box_get_active_iter( combo, &iter ) )
929        gtk_tree_model_get( model, &iter, ACTIVITY_FILTER_COL_TYPE, &type, -1 );
930    else
931        type = ACTIVITY_FILTER_ALL;
932    data->active_activity_type = type;
933
934    /* set the active category type & host from the category combobox */
935    combo = GTK_COMBO_BOX( data->category );
936    model = gtk_combo_box_get_model( combo );
937    if( gtk_combo_box_get_active_iter( combo, &iter ) ) {
938        gtk_tree_model_get( model, &iter, CAT_FILTER_COL_TYPE, &type,
939                                          CAT_FILTER_COL_HOST, &host,
940                                          -1 );
941    } else {
942        type = CAT_FILTER_TYPE_ALL;
943        host = NULL;
944    }
945    g_free( data->active_category_host );
946    data->active_category_host = host;
947    data->active_category_type = type;
948
949    /* refilter */
950    gtk_tree_model_filter_refilter( GTK_TREE_MODEL_FILTER( data->filter_model ) );
951}
952
953GtkWidget *
954gtr_filter_bar_new( tr_session * session, GtkTreeModel * tmodel, GtkTreeModel ** filter_model )
955{
956    GtkWidget * l;
957    GtkWidget * w;
958    GtkWidget * h;
959    GtkWidget * s;
960    GtkWidget * activity;
961    GtkWidget * category;
962    const char * str;
963    struct filter_data * data;
964
965    g_assert( DIRTY_KEY == 0 );
966    TEXT_KEY = g_quark_from_static_string( "tr-filter-text-key" );
967    DIRTY_KEY = g_quark_from_static_string( "tr-filter-dirty-key" );
968    SESSION_KEY = g_quark_from_static_string( "tr-session-key" );
969    TORRENT_MODEL_KEY = g_quark_from_static_string( "tr-filter-torrent-model-key" );
970
971    data = g_new0( struct filter_data, 1 );
972    data->activity = activity = activity_combo_box_new( tmodel );
973    data->category = category = category_combo_box_new( tmodel );
974    data->filter_model = gtk_tree_model_filter_new( tmodel, NULL );
975
976    g_object_set( G_OBJECT( data->category ), "width-request", 170, NULL );
977    g_object_set_qdata( G_OBJECT( gtk_combo_box_get_model( GTK_COMBO_BOX( data->category ) ) ), SESSION_KEY, session );
978
979    gtk_tree_model_filter_set_visible_func(
980        GTK_TREE_MODEL_FILTER( data->filter_model ),
981        is_row_visible, data, g_free );
982
983    g_signal_connect( data->category, "changed", G_CALLBACK( selection_changed_cb ), data );
984    g_signal_connect( data->activity, "changed", G_CALLBACK( selection_changed_cb ), data );
985
986
987    h = gtr_hbox_new( FALSE, GUI_PAD_SMALL );
988
989    /* add the activity combobox */
990    str = _( "_Show:" );
991    w = activity;
992    l = gtk_label_new( NULL );
993    gtk_label_set_markup_with_mnemonic( GTK_LABEL( l ), str );
994    gtk_label_set_mnemonic_widget( GTK_LABEL( l ), w );
995    gtk_box_pack_start( GTK_BOX( h ), l, FALSE, FALSE, 0 );
996    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
997
998    /* add a spacer */
999    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1000    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
1001    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1002
1003    /* add the category combobox */
1004    w = category;
1005    gtk_box_pack_start( GTK_BOX( h ), w, TRUE, TRUE, 0 );
1006
1007    /* add a spacer */
1008    w = gtk_alignment_new( 0.0f, 0.0f, 0.0f, 0.0f );
1009    gtk_widget_set_size_request( w, 0u, GUI_PAD_BIG );
1010    gtk_box_pack_start( GTK_BOX( h ), w, FALSE, FALSE, 0 );
1011
1012    /* add the entry field */
1013    s = gtk_entry_new( );
1014    gtk_entry_set_icon_from_stock( GTK_ENTRY( s ), GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLEAR );
1015    g_signal_connect( s, "icon-release", G_CALLBACK( entry_clear ), NULL );
1016    gtk_box_pack_start( GTK_BOX( h ), s, TRUE, TRUE, 0 );
1017
1018    g_signal_connect( s, "changed", G_CALLBACK( filter_entry_changed ), data->filter_model );
1019    selection_changed_cb( NULL, data );
1020
1021    *filter_model = data->filter_model;
1022    return h;
1023}
Note: See TracBrowser for help on using the repository browser.