source: trunk/gtk/filter.c @ 10395

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

(trunk gtk) fix another minor memory error in the new filter code

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