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