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