source: trunk/gtk/filter.c @ 10394

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

(trunk gtk) fix memory leak

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