source: trunk/gtk/filter.c @ 10382

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

(trunk gtk) small filter/favicon code cleanups

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