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