Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
collect.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2010, 2015 Bruce Guenter.
4 Copyright (C) 2010-2013 Henrik Andersson.
5 Copyright (C) 2010-2013, 2016 johannes hanika.
6 Copyright (C) 2010 Josep Puigdemont.
7 Copyright (C) 2010 Stuart Henderson.
8 Copyright (C) 2010-2018 Tobias Ellinghaus.
9 Copyright (C) 2011 Antony Dovgal.
10 Copyright (C) 2011 Brian Teague.
11 Copyright (C) 2011 Moritz Lipp.
12 Copyright (C) 2011 Robert Bieber.
13 Copyright (C) 2012 calca.
14 Copyright (C) 2012 José Carlos García Sogo.
15 Copyright (C) 2012 Richard Wonka.
16 Copyright (C) 2012-2013 Simon Spannagel.
17 Copyright (C) 2013, 2016, 2019-2022 Aldric Renaudin.
18 Copyright (C) 2013 Benjamin Cahill.
19 Copyright (C) 2013 Gaspard Jankowiak.
20 Copyright (C) 2013-2016 Jérémy Rosen.
21 Copyright (C) 2013-2015, 2018-2022 Pascal Obry.
22 Copyright (C) 2013-2016 Roman Lebedev.
23 Copyright (C) 2013 Thomas Pryds.
24 Copyright (C) 2013-2014 Ulrich Pegelow.
25 Copyright (C) 2015 Pedro Côrte-Real.
26 Copyright (C) 2016 Erik Duisters.
27 Copyright (C) 2017 Dan Torop.
28 Copyright (C) 2017 parafin.
29 Copyright (C) 2017 pgkos.
30 Copyright (C) 2018 Maurizio Paglia.
31 Copyright (C) 2018 Peter Budai.
32 Copyright (C) 2018 rawfiner.
33 Copyright (C) 2018 Rick Yorgason.
34 Copyright (C) 2018 Rikard Öxler.
35 Copyright (C) 2018, 2020 Sam Smith.
36 Copyright (C) 2019, 2022-2023, 2025-2026 Aurélien PIERRE.
37 Copyright (C) 2019-2022 Diederik Ter Rahe.
38 Copyright (C) 2019 Heiko Bauke.
39 Copyright (C) 2019-2022 Philippe Weyland.
40 Copyright (C) 2020-2021 Chris Elston.
41 Copyright (C) 2020 codingdave@gmail.com.
42 Copyright (C) 2020 EdgarLux.
43 Copyright (C) 2020 GrahamByrnes.
44 Copyright (C) 2020-2021 Hubert Kowalski.
45 Copyright (C) 2020 JP Verrue.
46 Copyright (C) 2020 jpverrue.
47 Copyright (C) 2020 Marco.
48 Copyright (C) 2020 Matt Maguire.
49 Copyright (C) 2020, 2022 Nicolas Auffray.
50 Copyright (C) 2020 Reinout Nonhebel.
51 Copyright (C) 2020 Vincent THOMAS.
52 Copyright (C) 2021 Arnaud TANGUY.
53 Copyright (C) 2021 Bill Ferguson.
54 Copyright (C) 2021 David-Tillmann Schaefer.
55 Copyright (C) 2021 Harald.
56 Copyright (C) 2021 luzpaz.
57 Copyright (C) 2021 Marco Carrarini.
58 Copyright (C) 2021 quovadit.
59 Copyright (C) 2021 Ralf Brown.
60 Copyright (C) 2022 Martin Bařinka.
61 Copyright (C) 2022 Miloš Komarčević.
62 Copyright (C) 2023 Luca Zulberti.
63 Copyright (C) 2026 Miguel Moquillon.
64
65 darktable is free software: you can redistribute it and/or modify
66 it under the terms of the GNU General Public License as published by
67 the Free Software Foundation, either version 3 of the License, or
68 (at your option) any later version.
69
70 darktable is distributed in the hope that it will be useful,
71 but WITHOUT ANY WARRANTY; without even the implied warranty of
72 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
73 GNU General Public License for more details.
74
75 You should have received a copy of the GNU General Public License
76 along with darktable. If not, see <http://www.gnu.org/licenses/>.
77*/
78
79/*
80 Library module — browse and manage collections shown in the lighttable.
81
82 This is *only* the GUI. The SQL engine lives in src/common/collection.c: it reads the conf
83 keys plugins/lighttable/collect/{num_rules,item<N>,mode<N>,string<N>} and turns them into
84 the query (get_query_string / dt_collection_update_query). So this module's whole contract
85 is: write those conf keys through the helpers in "Section 2 — conf layer", then call
86 _commit_colllection(). Everything else here is presentation and management.
87
88 Three tabs (a notebook tab-bar drives one shared value view):
89 - Folders : film-rolls/folders, as a flat List or a hierarchical Tree. The place to
90 relocate and remove film-rolls (in batches). item0 = FILMROLL | FOLDERS.
91 - Collections : tags, as a hierarchical Tree. Browse, rename, delete (batches). item0 = TAG.
92 - Queries : an arbitrary multi-rule builder (property/value/AND-OR-NOT), plus a raw
93 SQL escape hatch (item0 = DT_COLLECTION_PROP_QUERY).
94
95 Right-click on a Folders/Collections row opens a context menu built from a small ACTIONS
96 table (Section 6) — adding a bulk operation (e.g. pre-render thumbnails) is one table row,
97 fed by _rows_to_imgids() / dt_collection_get_images_for_rule(), which map the selected rows
98 to the matching image ids so any whole-set operation can be bolted on without touching the
99 view code.
100
101 TODO:
102 - when querying on numeric/datetime fields and using a range like `[2021;2022]`, limit the
103 treeview content to items actually fitting within that range (same behaviour as when typing
104 text for folders/filenames: the list is reduced to matching elements),
105 - range selection from queries, using `[;]` syntax is weird, find a better way to handle ranges
106 through regular GUI/API using `>` on the lower bound `AND` `<` on the higher bound.
107 This may have to dynamically add a new rule and spawn comboboxes based on user selections
108 in treeview. Though the current text-based listing makes for a simple GUI and expressive
109 queries writing, it conflicts with current GUI and the syntax is too advanced.
110 I don't know what the best course of action is here.
111 - implement drag & drap from thumbtable thumbnails to:
112 1. folders/filmrolls: move dragged images to the target folder (disable/forbid it if more
113 than one treeview row is selected)
114 2. tags (collections): attach the target tag to the dragged images,
115 3. in both cases, refresh treeview and lighttable/thumbtable view to update images that moved
116 elsewhere,
117 - de-implement the preferences (hidden) popup and add every view configuration parameters to the
118 front widget, into the relevant tab if needed to not pollute the overall view,
119 - sort by ID has no effect on folders treeview, it is only sorted alphabetically. Hide the
120 "sort by" combobox entirely in that case, for consistency.
121 - add an entry in context menu (on right click in treeview/list) to pre-render all thumbnails
122 from the target collection (see gui/actions/run.c menu for example). An API already exists
123 to produce a GList of imgids from a collection extracted from treeview row.
124*/
125
126#include "libs/collect.h"
127#include "bauhaus/bauhaus.h"
128#include "common/collection.h"
129#include "common/darktable.h"
130#include "common/datetime.h"
131#include "common/debug.h"
132#include "common/film.h"
133#include "common/image.h"
134#include "common/map_locations.h"
135#include "common/metadata.h"
136#include "common/mipmap_cache.h"
137#include "common/selection.h"
138#include "common/tags.h"
139#include "common/utility.h"
140#include "control/conf.h"
141#include "control/control.h"
142#include "control/jobs.h"
144#include "dtgtk/button.h"
145#include "dtgtk/paint.h"
146#include "dtgtk/togglebutton.h"
147#include "gui/drag_and_drop.h"
148#include "gui/gtk.h"
150#include "libs/lib.h"
151#include "libs/lib_api.h"
152#include "views/view.h"
153#ifndef _WIN32
154#include <gio/gunixmounts.h>
155#endif
156#ifdef GDK_WINDOWING_QUARTZ
157#include "osx/osx.h"
158#endif
159
160DT_MODULE(3)
161
162#define MAX_RULES 10
163#define PARAM_STRING_SIZE 256
164
171
185
187{
188 int num;
191 GtkWidget *op_combo; // comparison operator selector (numeric/date/rating properties)
194 gboolean typing;
195 gboolean reveal; // one-shot: after a view/tab switch, unfold the tree to the preserved query
197 void *lib_collect; // backref to dt_lib_collect_t
199
200typedef struct dt_lib_collect_t
201{
204
207
208 GtkTreeView *view;
210
211 GtkTreeModel *treefilter;
212 GtkTreeModel *listfilter;
213
214 // Folders-tab inline controls
215 GtkWidget *folders_controls; // hbox holding the widgets below
216 GtkWidget *recursive_check; // "include sub-folders" -> '*' suffix
217 GtkWidget *sort_dir; // ascending/descending toggle
218 GtkWidget *sort_by; // film-roll sort key (id / folder name)
219 GtkWidget *folder_levels; // show_folder_levels: levels shown in film-roll names (List only)
220
221 // Collections-tab inline controls
222 GtkWidget *collections_controls; // hbox holding the widget below
223 GtkWidget *no_uncategorized; // "no 'uncategorized' group" for childless tags
224
225 // Queries-tab raw SQL escape
229
231#ifdef _WIN32
232 GVolumeMonitor *vmonitor;
233#else
234 GUnixMountMonitor *vmonitor;
235#endif
237
239{
240 uint32_t item : 16;
241 uint32_t mode : 16;
242 char string[PARAM_STRING_SIZE];
244
250
251typedef struct _range_t
252{
253 gchar *start;
254 gchar *stop;
255 GtkTreePath *path1;
256 GtkTreePath *path2;
258
259// ---- forward declarations ----
261static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr);
262static void combo_changed(GtkWidget *combo, dt_lib_collect_rule_t *dr);
263static void collection_updated(gpointer instance, dt_collection_change_t query_change,
264 dt_collection_properties_t changed_property, gpointer imgs, int next,
265 gpointer self);
266static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d);
267static void update_view(dt_lib_collect_rule_t *dr);
268static void _populate_collect_combo(GtkWidget *w);
269static int _combo_get_active_collection(GtkWidget *combo);
270static gboolean _combo_set_active_collection(GtkWidget *combo, const int property);
271static void _op_changed(GtkWidget *w, dt_lib_collect_rule_t *dr);
272
273// =====================================================================================
274// Section 0 — property predicates
275// =====================================================================================
276
277static int is_time_property(int property)
278{
279 return property == DT_COLLECTION_PROP_TIME || property == DT_COLLECTION_PROP_IMPORT_TIMESTAMP
282}
283
284static gboolean item_is_folder(int item)
285{
287}
288
289static gboolean item_is_tag(int item)
290{
291 return item == DT_COLLECTION_PROP_TAG;
292}
293
294static gboolean item_is_numeric(int item)
295{
299}
300
301// A property displayed as a hierarchical tree (vs a flat list).
302static gboolean item_is_tree(int item)
303{
304 return item == DT_COLLECTION_PROP_FOLDERS || item == DT_COLLECTION_PROP_TAG
306}
307
308// Comparison operators offered by the operator combo for numeric/date/rating properties.
309// OP_TOKENS is the prefix written into the rule string; OP_LABELS is what the user sees.
310static const char *const OP_TOKENS[] = { "", "<", "<=", ">", ">=", "<>" };
311static const char *const OP_LABELS[] = { "=", "<", "≤", ">", "≥", "≠" };
312#define COLLECT_N_OPS ((int)G_N_ELEMENTS(OP_TOKENS))
313
314// Split a rule string into a leading operator index (into OP_TOKENS) and the remaining value.
315static void _split_operator(const char *text, int *op_idx, const char **value)
316{
317 *op_idx = 0;
318 *value = text ? text : "";
319 if(IS_NULL_PTR(text) || text[0] == '[') return; // a [a;b] range carries no leading operator
320 if(g_str_has_prefix(text, "<="))
321 {
322 *op_idx = 2;
323 *value = text + 2;
324 }
325 else if(g_str_has_prefix(text, ">="))
326 {
327 *op_idx = 4;
328 *value = text + 2;
329 }
330 else if(g_str_has_prefix(text, "<>"))
331 {
332 *op_idx = 5;
333 *value = text + 2;
334 }
335 else if(g_str_has_prefix(text, "<"))
336 {
337 *op_idx = 1;
338 *value = text + 1;
339 }
340 else if(g_str_has_prefix(text, ">"))
341 {
342 *op_idx = 3;
343 *value = text + 1;
344 }
345 else if(g_str_has_prefix(text, "="))
346 {
347 *op_idx = 0;
348 *value = text + 1;
349 }
350 while(**value == ' ') (*value)++;
351}
352
353// =====================================================================================
354// Section 1 — module identity & presets (conf-only; unchanged contract)
355// =====================================================================================
356
357const char *name(struct dt_lib_module_t *self)
358{
359 return _("Library");
360}
361
362const char **views(dt_lib_module_t *self)
363{
364 static const char *v[] = { "lighttable", "map", "print", NULL };
365 return v;
366}
367
369{
371}
372
374{
375 return 400;
376}
377
378void *legacy_params(struct dt_lib_module_t *self, const void *const old_params, const size_t old_params_size,
379 const int old_version, int *new_version, size_t *new_size)
380{
381 if(old_version == 1 || old_version == 2)
382 {
383 // v1->v2 and v2->v3 only reordered/extended the property enum; presets store the property
384 // index. Rather than carry the historical remap tables, drop incompatible presets.
385 return NULL;
386 }
387 return NULL;
388}
389
391{
393
394#define CLEAR_PARAMS(r) \
395 { \
396 memset(&params, 0, sizeof(params)); \
397 params.rules = 1; \
398 params.rule[0].mode = 0; \
399 params.rule[0].item = r; \
400 }
401
402 GDateTime *now = g_date_time_new_now_local();
403 char *datetime_today = g_date_time_format(now, "%Y:%m:%d");
404 GDateTime *gdt = g_date_time_add_days(now, -1);
405 char *datetime_24hrs = g_date_time_format(gdt, "> %Y:%m:%d %H:%M");
406 g_date_time_unref(gdt);
407 gdt = g_date_time_add_days(now, -30);
408 char *datetime_30d = g_date_time_format(gdt, "> %Y:%m:%d");
409 g_date_time_unref(gdt);
410 g_date_time_unref(now);
411
413 g_strlcpy(params.rule[0].string, datetime_today, PARAM_STRING_SIZE);
414 dt_lib_presets_add(_("imported: today"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
415
417 g_strlcpy(params.rule[0].string, datetime_24hrs, PARAM_STRING_SIZE);
418 dt_lib_presets_add(_("imported: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
419
421 g_strlcpy(params.rule[0].string, datetime_30d, PARAM_STRING_SIZE);
422 dt_lib_presets_add(_("imported: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params),
423 TRUE);
424
426 g_strlcpy(params.rule[0].string, datetime_today, PARAM_STRING_SIZE);
427 dt_lib_presets_add(_("taken: today"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
428
430 g_strlcpy(params.rule[0].string, datetime_24hrs, PARAM_STRING_SIZE);
431 dt_lib_presets_add(_("taken: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
432
434 g_strlcpy(params.rule[0].string, datetime_30d, PARAM_STRING_SIZE);
435 dt_lib_presets_add(_("taken: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
436
437 dt_free(datetime_today);
438 dt_free(datetime_24hrs);
439 dt_free(datetime_30d);
440#undef CLEAR_PARAMS
441}
442
443// =====================================================================================
444// Section 2 — conf layer (the single source of truth read by the SQL engine)
445// =====================================================================================
446
447static int _rules_count()
448{
449 return CLAMP(dt_conf_get_int("plugins/lighttable/collect/num_rules"), 1, MAX_RULES);
450}
451
452static void _rules_set_count(int n)
453{
454 dt_conf_set_int("plugins/lighttable/collect/num_rules", CLAMP(n, 1, MAX_RULES));
455}
456
457static int _rule_get_item(int n)
458{
459 char k[64];
460 snprintf(k, sizeof(k), "plugins/lighttable/collect/item%1d", n);
461 return dt_conf_get_int(k);
462}
463
464static void _rule_set_item(int n, int item)
465{
466 char k[64];
467 snprintf(k, sizeof(k), "plugins/lighttable/collect/item%1d", n);
468 dt_conf_set_int(k, item);
469}
470
471static int _rule_get_mode(int n)
472{
473 char k[64];
474 snprintf(k, sizeof(k), "plugins/lighttable/collect/mode%1d", n);
475 return dt_conf_get_int(k);
476}
477
478static void _rule_set_mode(int n, int mode)
479{
480 char k[64];
481 snprintf(k, sizeof(k), "plugins/lighttable/collect/mode%1d", n);
482 dt_conf_set_int(k, mode);
483}
484
485static gchar *_rule_get_string(int n)
486{
487 char k[64];
488 snprintf(k, sizeof(k), "plugins/lighttable/collect/string%1d", n);
489 return dt_conf_get_string(k);
490}
491
492static void _rule_set_string(int n, const char *s)
493{
494 char k[64];
495 snprintf(k, sizeof(k), "plugins/lighttable/collect/string%1d", n);
496 dt_conf_set_string(k, s ? s : "");
497}
498
499// Push the GUI state of one rule (combo property + operator + entry text) into conf. For
500// numeric/date/rating properties the chosen operator is prepended to the value.
502{
503 const int property = _combo_get_active_collection(dr->combo);
504 const char *val = gtk_entry_get_text(GTK_ENTRY(dr->text));
505 gchar *s;
506 if(item_is_numeric(property) && val[0] != '[') // a [a;b] range carries its own operator
507 {
508 const int idx = CLAMP(gtk_combo_box_get_active(GTK_COMBO_BOX(dr->op_combo)), 0, COLLECT_N_OPS - 1);
509 s = g_strconcat(OP_TOKENS[idx], val, NULL);
510 }
511 else
512 s = g_strdup(val);
513 _rule_set_string(dr->num, s);
514 dt_free(s);
515 _rule_set_item(dr->num, property);
516}
517
518// Pull the conf state of one rule back into the GUI (without firing the change handlers).
520{
522 const int property = _combo_get_active_collection(dr->combo);
523 gchar *text = _rule_get_string(dr->num);
524 if(text)
525 {
526 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
527 g_signal_handlers_block_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
528
529 if(item_is_numeric(property))
530 {
531 int idx;
532 const char *val;
533 _split_operator(text, &idx, &val);
534 gtk_combo_box_set_active(GTK_COMBO_BOX(dr->op_combo), idx);
535 gtk_entry_set_text(GTK_ENTRY(dr->text), val);
536 }
537 else
538 {
539 gtk_entry_set_text(GTK_ENTRY(dr->text), text);
540 }
541
542 gtk_editable_set_position(GTK_EDITABLE(dr->text), -1);
543 dr->typing = FALSE;
544
545 g_signal_handlers_unblock_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
546 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
547 dt_free(text);
548 }
549}
550
551// Rebuild the collection query from the conf rules and refresh the lighttable.
552// Note: _commit() would conflict with msys64/ucrt64/include/io.h namespace on Windows.
557
558// Like _commit_colllection() but without bouncing back into our own collection_updated() handler.
567
569{
570 return (dt_lib_collect_t *)r->lib_collect;
571}
572
574{
575 return d->rule + d->active_rule;
576}
577
579{
580 d->nb_rules = _rules_count();
581 d->active_rule = CLAMP(d->active_rule, 0, d->nb_rules - 1);
582}
583
584// get_params/set_params/gui_reset rely on the conf layer above, so they stay tiny.
586{
587 dt_lib_collect_params_t *p = d->params;
588 memset(p, 0, sizeof(dt_lib_collect_params_t));
589 const int n = _rules_count();
590 for(int i = 0; i < n; i++)
591 {
592 p->rule[i].item = _rule_get_item(i);
593 p->rule[i].mode = _rule_get_mode(i);
594 gchar *s = _rule_get_string(i);
595 if(s) g_strlcpy(p->rule[i].string, s, PARAM_STRING_SIZE);
596 dt_free(s);
597 }
598 p->rules = n;
599}
600
602{
604 *size = sizeof(dt_lib_collect_params_t);
605 void *p = malloc(*size);
606 memcpy(p, ((dt_lib_collect_t *)self->data)->params, *size);
607 return p;
608}
609
610int set_params(dt_lib_module_t *self, const void *params, int size)
611{
613 for(uint32_t i = 0; i < p->rules; i++)
614 {
615 _rule_set_item(i, p->rule[i].item);
616 _rule_set_mode(i, p->rule[i].mode);
617 _rule_set_string(i, p->rule[i].string);
618 }
619 _rules_set_count(p->rules);
623 return 0;
624}
625
638
639// =====================================================================================
640// Section 3 — property combo
641// =====================================================================================
642
644{
645 return GPOINTER_TO_UINT(dt_bauhaus_combobox_get_data(combo)) - 1;
646}
647
648static gboolean _combo_set_active_collection(GtkWidget *combo, const int property)
649{
650 const gboolean found = dt_bauhaus_combobox_set_from_value(combo, property + 1);
652 return found;
653}
654
656{
657#define ADD_COLLECT_ENTRY(value) \
658 dt_bauhaus_combobox_add_full(w, dt_collection_name(value), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, \
659 GUINT_TO_POINTER(value + 1), NULL, TRUE)
660
664
666 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
667 {
668 const uint32_t keyid = dt_metadata_get_keyid_by_display_order(i);
669 const gchar *name_ = dt_metadata_get_name(keyid);
670 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name_);
671 const gboolean hidden = dt_conf_get_int(setting) & DT_METADATA_FLAG_HIDDEN;
672 dt_free(setting);
673 const int meta_type = dt_metadata_get_type(keyid);
675 }
679
686
693
699#undef ADD_COLLECT_ENTRY
700}
701
702// =====================================================================================
703// Section 4 — model population (flat list + hierarchical tree) and filtering
704// =====================================================================================
705
706static int string_array_length(char **list)
707{
708 int length = 0;
709 for(; *list; list++) length++;
710 return length;
711}
712
713// NULL-terminated array of path components (drops the leading empty root component on POSIX).
714static char **split_path(const char *path)
715{
716 if(IS_NULL_PTR(path) || !*path) return NULL;
717
718 char **result;
719 char **tokens = g_strsplit(path, G_DIR_SEPARATOR_S, -1);
720
721#ifdef _WIN32
722 if(!(g_ascii_isalpha(tokens[0][0]) && tokens[0][strlen(tokens[0]) - 1] == ':'))
723 {
724 g_strfreev(tokens);
725 tokens = NULL;
726 }
727 result = tokens;
728#else
729 const unsigned int size = g_strv_length(tokens);
730 result = malloc(sizeof(char *) * size);
731 for(unsigned int i = 0; i < size; i++) result[i] = tokens[i + 1];
732 dt_free(tokens[0]);
733 dt_free(tokens);
734#endif
735 return result;
736}
737
743
744static void free_tuple(gpointer data)
745{
746 name_key_tuple_t *tuple = (name_key_tuple_t *)data;
747 dt_free(tuple->name);
748 dt_free(tuple->collate_key);
749 dt_free(tuple);
750}
751
752static gint sort_folder_tag(gconstpointer a, gconstpointer b)
753{
754 const name_key_tuple_t *ta = (const name_key_tuple_t *)a;
755 const name_key_tuple_t *tb = (const name_key_tuple_t *)b;
756 return g_strcmp0(ta->collate_key, tb->collate_key);
757}
758
759// Sort key so that "not tagged" & "darktable|" come first, sub-tags directly behind their parent.
760static char *tag_collate_key(char *tag)
761{
762 const size_t len = strlen(tag);
763 char *result = g_malloc(len + 2);
764 if(!g_strcmp0(tag, _("not tagged")))
765 *result = '\1';
766 else if(g_str_has_prefix(tag, "darktable|"))
767 *result = '\2';
768 else
769 *result = '\3';
770 memcpy(result + 1, tag, len + 1);
771 for(char *iter = result + 1; *iter; iter++)
772 if(*iter == '|') *iter = '\1';
773 return result;
774}
775
776static void tree_count_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
777 GtkTreeIter *iter, gpointer data)
778{
779 gchar *name;
780 guint count;
781 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_TEXT, &name, DT_LIB_COLLECT_COL_COUNT, &count, -1);
782 if(!count)
783 g_object_set(renderer, "text", name, NULL);
784 else
785 {
786 gchar *coltext = g_strdup_printf("%s (%d)", name, count);
787 g_object_set(renderer, "text", coltext, NULL);
788 dt_free(coltext);
789 }
790 dt_free(name);
791}
792
793// ---- search filtering (list) ----
794static gboolean list_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
795{
797 gchar *str = NULL;
798 gboolean visible = FALSE;
799 gboolean was_visible;
800 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &was_visible, -1);
801
802 gchar *haystack = g_utf8_strdown(str, -1);
803 const gchar *needle = dr->searchstring;
804 const int property = _combo_get_active_collection(dr->combo);
805
807 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_RATING)
808 {
809 visible = TRUE;
810 gchar *operator, * number, *number2;
811 dt_collection_split_operator_number(needle, &number, &number2, &operator);
812 if(number)
813 {
814 const float nb1 = g_strtod(number, NULL);
815 const float nb2 = g_strtod(haystack, NULL);
816 if(operator&& strcmp(operator, ">") == 0)
817 visible = (nb2 > nb1);
818 else if(operator&& strcmp(operator, ">=") == 0)
819 visible = (nb2 >= nb1);
820 else if(operator&& strcmp(operator, "<") == 0)
821 visible = (nb2 < nb1);
822 else if(operator&& strcmp(operator, "<=") == 0)
823 visible = (nb2 <= nb1);
824 else if(operator&& strcmp(operator, "<>") == 0)
825 visible = (nb1 != nb2);
826 else if(operator&& number2 && strcmp(operator, "[]") == 0)
827 {
828 const float nb3 = g_strtod(number2, NULL);
829 visible = (nb2 >= nb1 && nb2 <= nb3);
830 }
831 else
832 visible = (nb1 == nb2);
833 }
834 dt_free(operator);
835 dt_free(number);
836 dt_free(number2);
837 }
838 else if(property == DT_COLLECTION_PROP_FILENAME && strchr(needle, ',') != NULL)
839 {
840 GList *list = dt_util_str_to_glist(",", needle);
841 for(const GList *l = list; l; l = g_list_next(l))
842 {
843 const char *name = (char *)l->data;
844 if((visible = (g_strrstr(haystack, name + (name[0] == '%')) != NULL))) break;
845 }
846 g_list_free_full(list, dt_free_gpointer);
847 }
848 else
849 {
850 if(needle[0] == '%') needle++;
851 if(!needle[0])
852 visible = TRUE;
853 else if(!needle[1])
854 visible = (strchr(haystack, needle[0]) != NULL);
855 else
856 visible = (g_strrstr(haystack, needle) != NULL);
857 }
858
859 dt_free(haystack);
860 dt_free(str);
861 if(visible != was_visible)
862 gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
863 return FALSE;
864}
865
866// ---- search filtering (tree) ----
867static gboolean tree_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
868{
870 gchar *str = NULL;
871 gboolean cur_state, visible;
872 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &cur_state, -1);
873
874 if(dr->typing == FALSE && !cur_state)
875 visible = TRUE;
876 else
877 {
878 gchar *haystack = g_utf8_strdown(str, -1),
879 *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
880 visible = (g_strrstr(haystack, needle) != NULL);
881 dt_free(haystack);
882 dt_free(needle);
883 }
884 dt_free(str);
885 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
886 return FALSE;
887}
888
889static gboolean tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
890{
891 gboolean state;
892 GtkTreeIter parent, child = *iter;
893 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_VISIBLE, &state, -1);
894 if(!state) return FALSE;
895 while(gtk_tree_model_iter_parent(model, &parent, &child))
896 {
897 gtk_tree_store_set(GTK_TREE_STORE(model), &parent, DT_LIB_COLLECT_COL_VISIBLE, TRUE, -1);
898 child = parent;
899 }
900 return FALSE;
901}
902
903static void tree_set_visibility(GtkTreeModel *model, gpointer data)
904{
905 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_match_string, data);
906 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_reveal_func, NULL);
907}
908
909// Turn a partial date string ("2021", "2021:06:15", "2021:06:15 13:00", ...) into a comparable
910// 14-digit YYYYMMDDHHMMSS number, keeping only digits (separator-agnostic) and padding the
911// unspecified low-order part with `pad`. Pad '0' yields the earliest instant of the prefix,
912// pad '9' an upper bound past its latest instant.
913static guint64 _date_key(const char *s, char pad)
914{
915 char digits[15];
916 int n = 0;
917 for(const char *p = s; p && *p && n < 14; p++)
918 if(g_ascii_isdigit(*p)) digits[n++] = *p;
919 while(n < 14) digits[n++] = pad;
920 digits[14] = '\0';
921 return g_ascii_strtoull(digits, NULL, 10);
922}
923
924typedef struct _date_range_t
925{
926 guint64 lo, hi; // inclusive bounds as _date_key() numbers
928
929// Reduce a date tree to the nodes whose date prefix overlaps [lo;hi]. A node's prefix spans
930// [node_lo;node_hi]; it overlaps the range iff node_hi >= lo AND node_lo <= hi. Pair this with
931// tree_reveal_func() so the visible leaves' ancestors stay visible too.
932static gboolean tree_range_visible(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
933{
934 const _date_range_t *r = (const _date_range_t *)data;
935 gchar *str = NULL;
936 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
937 const guint64 node_lo = _date_key(str, '0');
938 const guint64 node_hi = _date_key(str, '9');
939 dt_free(str);
940 const gboolean visible = (node_hi >= r->lo) && (node_lo <= r->hi);
941 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
942 return FALSE;
943}
944
945static gboolean list_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
946{
949 gchar *str = NULL;
950 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
951
952 gchar *haystack = g_utf8_strdown(str, -1);
953 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
954 if(strcmp(haystack, needle) == 0)
955 {
956 gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), path);
957 gtk_tree_view_scroll_to_cell(d->view, path, NULL, FALSE, 0.2, 0);
958 }
959 dt_free(haystack);
960 dt_free(needle);
961 dt_free(str);
962 return FALSE;
963}
964
965static gboolean range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
966{
967 _range_t *range = (_range_t *)data;
968 gchar *str = NULL;
969 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
970
971 gchar *haystack = g_utf8_strdown(str, -1);
972 gchar *needle = range->path1 ? g_utf8_strdown(range->stop, -1) : g_utf8_strdown(range->start, -1);
973 if(strcmp(haystack, needle) == 0)
974 {
975 if(range->path1)
976 {
977 range->path2 = gtk_tree_path_copy(path);
978 dt_free(haystack);
979 dt_free(needle);
980 dt_free(str);
981 return TRUE;
982 }
983 else
984 range->path1 = gtk_tree_path_copy(path);
985 }
986 dt_free(haystack);
987 dt_free(needle);
988 dt_free(str);
989 return FALSE;
990}
991
992static gboolean tree_expand(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
993{
996 gchar *str = NULL, *txt = NULL;
997 gboolean startwildcard = FALSE, expanded = FALSE;
998 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_TEXT, &txt, -1);
999
1000 gchar *haystack = g_utf8_strdown(str, -1);
1001 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
1002 gchar *txt2 = g_utf8_strdown(txt, -1);
1003 const int property = _combo_get_active_collection(dr->combo);
1004
1005 // While typing, or right after a view/tab switch that preserved a query, we want to actively
1006 // reveal the matching node(s); on a plain refresh we leave the tree where the user left it.
1007 const gboolean reveal = dr->typing || dr->reveal;
1008
1009 if(g_str_has_prefix(needle, "%")) startwildcard = TRUE;
1010 if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0';
1011 if(g_str_has_suffix(haystack, "%")) haystack[strlen(haystack) - 1] = '\0';
1012 if(property == DT_COLLECTION_PROP_TAG || property == DT_COLLECTION_PROP_GEOTAGGING)
1013 {
1014 if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0'; // hierarchy + sub
1015 if(g_str_has_suffix(needle, "|")) needle[strlen(needle) - 1] = '\0';
1016 if(g_str_has_suffix(haystack, "|")) haystack[strlen(haystack) - 1] = '\0';
1017 }
1018 else if(property == DT_COLLECTION_PROP_FOLDERS)
1019 {
1020 if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0';
1021 if(g_str_has_suffix(needle, "/")) needle[strlen(needle) - 1] = '\0';
1022 if(g_str_has_suffix(haystack, "/")) haystack[strlen(haystack) - 1] = '\0';
1023 }
1024 else if(DT_COLLECTION_PROP_DAY == property || is_time_property(property))
1025 {
1026 if(g_str_has_suffix(needle, ":")) needle[strlen(needle) - 1] = '\0';
1027 if(g_str_has_suffix(haystack, ":")) haystack[strlen(haystack) - 1] = '\0';
1028 }
1029
1030 if(reveal && g_strrstr(txt2, needle) != NULL)
1031 {
1032 gtk_tree_view_expand_to_path(d->view, path);
1033 expanded = TRUE;
1034 }
1035
1036 if(strlen(needle) == 0)
1037 {
1038 // keep collapsed
1039 }
1040 else if(strcmp(haystack, needle) == 0)
1041 {
1042 gtk_tree_view_expand_to_path(d->view, path);
1043 gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), path);
1044 gtk_tree_view_scroll_to_cell(d->view, path, NULL, FALSE, 0.2, 0);
1045 expanded = TRUE;
1046 }
1047 else if(startwildcard && g_strrstr(haystack, needle + 1) != NULL)
1048 {
1049 gtk_tree_view_expand_to_path(d->view, path);
1050 expanded = TRUE;
1051 }
1052 else if((reveal || property != DT_COLLECTION_PROP_FOLDERS) && g_str_has_prefix(haystack, needle))
1053 {
1054 gtk_tree_view_expand_to_path(d->view, path);
1055 expanded = TRUE;
1056 }
1057
1058 dt_free(haystack);
1059 dt_free(needle);
1060 dt_free(txt2);
1061 dt_free(str);
1062 dt_free(txt);
1063 return expanded;
1064}
1065
1066// Walk down a folder tree through single-child nodes and return the path of the deepest such node
1067// (the unique common root), so the filtered model can hide the redundant leading folders. Returns
1068// NULL when there is nothing to collapse. Stops descending at a node that is itself a film-roll.
1069static GtkTreePath *_folders_root_collapse_path(GtkTreeModel *model)
1070{
1071 GtkTreeIter child, iter;
1072 int level = 0;
1073 while(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) > 0)
1074 {
1075 if(level > 0)
1076 {
1077 gchar *pth = NULL;
1078 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &pth, -1);
1079 const int id = dt_film_get_id(pth); // is this folder a known film-roll?
1080 dt_free(pth);
1081 if(id != -1)
1082 {
1083 if(!gtk_tree_model_iter_parent(model, &child, &iter)) level = 0;
1084 iter = child;
1085 break;
1086 }
1087 }
1088 if(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) != 1) break;
1089 gtk_tree_model_iter_children(model, &child, level > 0 ? &iter : NULL);
1090 iter = child;
1091 level++;
1092 }
1093 if(level <= 0) return NULL;
1094
1095 if(gtk_tree_model_iter_n_children(model, &iter) == 0 && gtk_tree_model_iter_parent(model, &child, &iter))
1096 return gtk_tree_model_get_path(model, &child);
1097 return gtk_tree_model_get_path(model, &iter);
1098}
1099
1100// Build the filtered model; for folders, collapse a unique common root into the virtual root.
1101static GtkTreeModel *_create_filtered_model(GtkTreeModel *model, dt_lib_collect_rule_t *dr)
1102{
1105 : NULL;
1106 GtkTreeModel *filter = gtk_tree_model_filter_new(model, path);
1107 gtk_tree_path_free(path);
1108 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(filter), DT_LIB_COLLECT_COL_VISIBLE);
1109 return filter;
1110}
1111
1112static const char *UNCATEGORIZED_TAG = N_("uncategorized");
1113
1114// --- preserve tree expansion across a rebuild so the user keeps their place ---
1115static void _collect_expanded_cb(GtkTreeView *view, GtkTreePath *path, gpointer data)
1116{
1117 GHashTable *set = (GHashTable *)data;
1118 GtkTreeModel *model = gtk_tree_view_get_model(view);
1119 GtkTreeIter it;
1120 if(gtk_tree_model_get_iter(model, &it, path))
1121 {
1122 gchar *p = NULL;
1123 gtk_tree_model_get(model, &it, DT_LIB_COLLECT_COL_PATH, &p, -1);
1124 if(p) g_hash_table_add(set, p); // hash table takes ownership of p
1125 }
1126}
1127
1133
1134static gboolean _restore_expanded_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1135{
1136 _expand_ctx_t *c = (_expand_ctx_t *)data;
1137 gchar *p = NULL;
1138 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &p, -1);
1139 if(p && g_hash_table_contains(c->set, p)) gtk_tree_view_expand_to_path(c->d->view, path);
1140 dt_free(p);
1141 return FALSE;
1142}
1143
1144// Split a tree value into its path components for the active property.
1145static char **_split_tree_name(int property, const char *name)
1146{
1147 if(property == DT_COLLECTION_PROP_FOLDERS) return split_path(name);
1148 if(property == DT_COLLECTION_PROP_DAY) return g_strsplit(name, ":", -1);
1149 if(is_time_property(property)) return g_strsplit_set(name, ": ", 4);
1150 return g_strsplit(name, "|", -1);
1151}
1152
1153// Add `count` to every ancestor of `leaf` so parent folders/dates show the total beneath them
1154// (fixes #537 for the displayed count).
1155static void _propagate_count_to_ancestors(GtkTreeStore *store, GtkTreeIter *leaf, int count)
1156{
1157 GtkTreeModel *model = GTK_TREE_MODEL(store);
1158 GtkTreeIter parent, child = *leaf;
1159 while(gtk_tree_model_iter_parent(model, &parent, &child))
1160 {
1161 guint parentcount;
1162 gtk_tree_model_get(model, &parent, DT_LIB_COLLECT_COL_COUNT, &parentcount, -1);
1163 gtk_tree_store_set(store, &parent, DT_LIB_COLLECT_COL_COUNT, count + parentcount, -1);
1164 child = parent;
1165 }
1166}
1167
1168// A top-level tag with no children of its own is filed under a synthetic "uncategorized" node
1169// (created lazily). Returns TRUE when `name` was filed this way, so the caller skips it.
1170static gboolean _maybe_file_uncategorized(GtkTreeStore *store, const char *name, const char *next_name_raw,
1171 GtkTreeIter *uncategorized, guint *index, int count)
1172{
1173 if(strchr(name, '|') != NULL) return FALSE; // has a hierarchy of its own
1174
1175 char *next_name = g_strdup(next_name_raw ? next_name_raw : "");
1176 if(strlen(next_name) >= strlen(name) + 1 && next_name[strlen(name)] == '|') next_name[strlen(name)] = '\0';
1177 const gboolean leaf_toplevel = g_strcmp0(next_name, name) && g_strcmp0(name, _("not tagged"));
1178 dt_free(next_name);
1179 if(!leaf_toplevel) return FALSE;
1180
1181 if(!uncategorized->stamp)
1182 {
1183 gtk_tree_store_insert_with_values(store, uncategorized, NULL, -1, DT_LIB_COLLECT_COL_TEXT,
1186 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1187 (*index)++;
1188 }
1189 GtkTreeIter temp;
1190 gtk_tree_store_insert_with_values(store, &temp, uncategorized, 0, DT_LIB_COLLECT_COL_TEXT, name,
1193 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1194 (*index)++;
1195 return TRUE;
1196}
1197
1198// Pull the raw values from the SQL engine and sort them ourselves: sqlite knows nothing about path
1199// separators, so we order by a path-aware collate key and build the tree by hand. Caller frees with
1200// g_list_free_full(list, free_tuple).
1201static GList *_collect_sorted_tree_names(int property, int rule)
1202{
1203 GList *sorted_names = NULL;
1204 GList *values = dt_collection_get_property_values(property, rule);
1205 for(GList *v = values; v; v = g_list_next(v))
1206 {
1208 char *name = g_strdup(nv->name ? nv->name : "");
1209 gchar *collate_key;
1210 if(property == DT_COLLECTION_PROP_FOLDERS)
1211 {
1212 char *name_folded = g_utf8_casefold(name, -1);
1213 char *name_folded_slash = g_strconcat(name_folded, G_DIR_SEPARATOR_S, NULL);
1214 collate_key = g_utf8_collate_key_for_filename(name_folded_slash, -1);
1215 dt_free(name_folded_slash);
1216 dt_free(name_folded);
1217 }
1218 else
1219 collate_key = tag_collate_key(name);
1220
1221 name_key_tuple_t *tuple = (name_key_tuple_t *)malloc(sizeof(name_key_tuple_t));
1222 tuple->name = name;
1223 tuple->collate_key = collate_key;
1224 tuple->count = nv->count;
1225 tuple->status = (property == DT_COLLECTION_PROP_FOLDERS) ? nv->status : -1;
1226 sorted_names = g_list_prepend(sorted_names, tuple);
1227 }
1228 g_list_free_full(values, dt_collection_name_value_free);
1229
1230 sorted_names = g_list_sort(sorted_names, sort_folder_tag);
1231 if(!dt_conf_get_bool("plugins/collect/descending")) sorted_names = g_list_reverse(sorted_names);
1232 return sorted_names;
1233}
1234
1235// Build the hierarchical tree store from the pre-sorted names. Each name is split into path
1236// components; consecutive names share their common prefix, so we only insert the new tail under
1237// the right parent (which is why the input must be path-sorted).
1238static void _build_tree_store(GtkTreeStore *store, int property, GList *sorted_names,
1239 gboolean no_uncategorized, const char *format_separator)
1240{
1241 GtkTreeModel *model = GTK_TREE_MODEL(store);
1242 GtkTreeIter uncategorized = { 0 };
1243 char **last_tokens = NULL;
1244 int last_tokens_length = 0;
1245 GtkTreeIter last_parent = { 0 };
1246 guint index = 0;
1247
1248 for(GList *names = sorted_names; names; names = g_list_next(names))
1249 {
1250 name_key_tuple_t *tuple = (name_key_tuple_t *)names->data;
1251 char *name = tuple->name;
1252 const int count = tuple->count;
1253 const int status = tuple->status;
1254 if(IS_NULL_PTR(name)) continue;
1255
1256 const char *next_name = names->next ? ((name_key_tuple_t *)names->next->data)->name : NULL;
1257 if(!no_uncategorized && _maybe_file_uncategorized(store, name, next_name, &uncategorized, &index, count))
1258 continue;
1259
1260 char **tokens = _split_tree_name(property, name);
1261 if(IS_NULL_PTR(tokens)) continue;
1262
1263 GtkTreeIter parent = last_parent;
1264 const int tokens_length = string_array_length(tokens);
1265 int common_length = 0;
1266 if(last_tokens)
1267 {
1268 while(tokens[common_length] && last_tokens[common_length]
1269 && !g_strcmp0(tokens[common_length], last_tokens[common_length]))
1270 common_length++;
1271 for(int i = common_length; i < last_tokens_length; i++)
1272 {
1273 gtk_tree_model_iter_parent(model, &parent, &last_parent);
1274 last_parent = parent;
1275 }
1276 }
1277
1278 char *pth = NULL;
1279#ifndef _WIN32
1280 if(property == DT_COLLECTION_PROP_FOLDERS) pth = g_strdup("/");
1281#endif
1282 for(int i = 0; i < common_length; i++) pth = dt_util_dstrcat(pth, format_separator, tokens[i]);
1283
1284 for(char **token = &tokens[common_length]; *token; token++)
1285 {
1286 GtkTreeIter iter;
1287 pth = dt_util_dstrcat(pth, format_separator, *token);
1288 if(is_time_property(property) && !*(token + 1)) pth[10] = ' ';
1289
1290 gchar *pth2 = g_strdup(pth);
1291 pth2[strlen(pth2) - 1] = '\0';
1292 const gboolean leaf = !*(token + 1);
1293 gtk_tree_store_insert_with_values(store, &iter, common_length > 0 ? &parent : NULL, 0,
1296 (leaf ? count : 0), DT_LIB_COLLECT_COL_INDEX, index,
1297 DT_LIB_COLLECT_COL_UNREACHABLE, (leaf ? !status : 0),
1298 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1299 index++;
1300 const gboolean recursive_count = property == DT_COLLECTION_PROP_DAY || is_time_property(property)
1301 || property == DT_COLLECTION_PROP_FOLDERS;
1302 if(recursive_count && leaf) _propagate_count_to_ancestors(store, &iter, count);
1303 common_length++;
1304 parent = iter;
1305 dt_free(pth2);
1306 }
1307 dt_free(pth);
1308
1309 if(last_tokens) g_strfreev(last_tokens);
1310 last_tokens = tokens;
1311 last_parent = parent;
1312 last_tokens_length = tokens_length;
1313 }
1314 g_strfreev(last_tokens);
1315}
1316
1317// Hierarchical (tree) properties: folders, tags, geotags, day, date-times.
1319{
1321 const int property = _combo_get_active_collection(dr->combo);
1322 char *format_separator = "";
1323
1324 switch(property)
1325 {
1327 format_separator = "%s" G_DIR_SEPARATOR_S;
1328 break;
1331 format_separator = "%s|";
1332 break;
1339 format_separator = "%s:";
1340 break;
1341 }
1342
1343 set_properties(dr);
1344
1345 GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->treefilter));
1346 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
1347 GTK_SORT_ASCENDING);
1348
1349 GHashTable *saved_expanded = NULL; // expanded node paths, captured across a rebuild
1350
1351 if(d->view_rule != property)
1352 {
1353 // remember which nodes were expanded so the rebuild doesn't lose the user's place
1354 saved_expanded = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1355 gtk_tree_view_map_expanded_rows(d->view, _collect_expanded_cb, saved_expanded);
1356
1357 g_object_ref(model);
1358 g_object_unref(d->treefilter);
1359 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL);
1360 gtk_tree_store_clear(GTK_TREE_STORE(model));
1361 gtk_widget_hide(GTK_WIDGET(d->view));
1362
1363 const gboolean no_uncategorized = (property == DT_COLLECTION_PROP_TAG)
1364 ? dt_conf_get_bool("plugins/lighttable/tagging/no_uncategorized")
1365 : TRUE;
1366 GList *sorted_names = _collect_sorted_tree_names(property, dr->num);
1367 _build_tree_store(GTK_TREE_STORE(model), property, sorted_names, no_uncategorized, format_separator);
1368 g_list_free_full(sorted_names, free_tuple);
1369
1370 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP);
1371 d->treefilter = _create_filtered_model(model, dr);
1372
1373 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view));
1374 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
1375
1376 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->treefilter);
1377 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
1378 gtk_widget_show_all(GTK_WIDGET(d->view));
1379
1380 g_object_unref(model);
1381 d->view_rule = property;
1382 }
1383
1384 // A [a;b] range on a numeric/date tree property reduces the tree to the in-range nodes (like
1385 // a text search reduces folder/filename lists), instead of a substring filter that — since no
1386 // node literally contains "[a;b]" — would hide everything.
1387 _range_t *range = NULL;
1388 if(item_is_numeric(property))
1389 {
1390 GRegex *regex = g_regex_new("^\\s*\\[\\s*(.*)\\s*;\\s*(.*)\\s*\\]\\s*$", 0, 0, NULL);
1391 GMatchInfo *match_info;
1392 g_regex_match_full(regex, gtk_entry_get_text(GTK_ENTRY(dr->text)), -1, 0, 0, &match_info, NULL);
1393 if(g_match_info_get_match_count(match_info) == 3)
1394 {
1395 range = (_range_t *)calloc(1, sizeof(_range_t));
1396 range->start = g_match_info_fetch(match_info, 2); // inverted: dates are reverse-ordered
1397 range->stop = g_match_info_fetch(match_info, 1);
1398 }
1399 g_match_info_free(match_info);
1400 g_regex_unref(regex);
1401 }
1402
1403 if(range)
1404 {
1405 // restrict the visible nodes to the dates within [start;stop] (bounds taken order-agnostic)
1406 const guint64 a_lo = _date_key(range->start, '0'), a_hi = _date_key(range->start, '9');
1407 const guint64 b_lo = _date_key(range->stop, '0'), b_hi = _date_key(range->stop, '9');
1408 _date_range_t dvr = { MIN(a_lo, b_lo), MAX(a_hi, b_hi) };
1409 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_range_visible, &dvr);
1410 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_reveal_func, NULL);
1411 }
1412 else if(dr->typing)
1414
1415 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view));
1416
1417 // Active search (text or range) collapses, then we expand just the matches. When merely
1418 // refreshing/browsing, restore the previous expansion so the view stays where the user was.
1419 if(range)
1420 gtk_tree_view_expand_all(d->view); // reveal the reduced in-range set
1421 else if(dr->typing)
1422 gtk_tree_view_collapse_all(d->view);
1423 else if(saved_expanded)
1424 {
1425 _expand_ctx_t ctx = { d, saved_expanded };
1426 gtk_tree_model_foreach(d->treefilter, _restore_expanded_cb, &ctx);
1427 }
1428 if(saved_expanded) g_hash_table_destroy(saved_expanded);
1429
1430 if(range)
1431 {
1432 // also select the boundary rows when the typed bounds match actual leaves
1433 gtk_tree_model_foreach(d->treefilter, (GtkTreeModelForeachFunc)range_select, range);
1434 if(range->path1 && range->path2)
1435 gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), range->path1, range->path2);
1436 dt_free(range->start);
1437 dt_free(range->stop);
1438 gtk_tree_path_free(range->path1);
1439 gtk_tree_path_free(range->path2);
1440 dt_free(range);
1441 }
1442 else
1443 gtk_tree_model_foreach(d->treefilter, (GtkTreeModelForeachFunc)tree_expand, dr);
1444}
1445
1446// Flat (list) properties: film-rolls, camera, lens, filename, rating, metadata, ...
1448{
1450 const int property = _combo_get_active_collection(dr->combo);
1451
1452 set_properties(dr);
1453
1454 GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->listfilter));
1455 if(d->view_rule != property)
1456 {
1457 GtkTreeIter iter;
1458 g_object_unref(d->listfilter);
1459 g_object_ref(model);
1460 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL);
1461 gtk_list_store_clear(GTK_LIST_STORE(model));
1462 gtk_widget_hide(GTK_WIDGET(d->view));
1463
1464 GList *values = dt_collection_get_property_values(property, dr->num);
1465 for(GList *v = values; v; v = g_list_next(v))
1466 {
1468 if(IS_NULL_PTR(nv->name)) continue;
1469 const char *value = nv->name;
1470 // film-rolls show a shortened folder name but keep the full path for queries/management
1471 const char *display = (property == DT_COLLECTION_PROP_FILMROLL) ? dt_image_film_roll_name(value) : value;
1472 const int unreachable = (property == DT_COLLECTION_PROP_FILMROLL) ? (nv->status == 0) : 0;
1473
1474 gchar *text = g_strdup(value);
1475 gchar *ptr = text;
1476 while(!g_utf8_validate(ptr, -1, (const gchar **)&ptr)) ptr[0] = '?';
1477 gchar *escaped_text = g_markup_escape_text(text, -1);
1478
1479 gtk_list_store_append(GTK_LIST_STORE(model), &iter);
1480 gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_LIB_COLLECT_COL_TEXT, display, DT_LIB_COLLECT_COL_ID,
1483 DT_LIB_COLLECT_COL_UNREACHABLE, unreachable, DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL,
1484 -1);
1485 dt_free(text);
1486 dt_free(escaped_text);
1487 }
1488 g_list_free_full(values, dt_collection_name_value_free);
1489
1490 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP);
1491 d->listfilter = _create_filtered_model(model, dr);
1492
1493 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view));
1494 const gboolean multi = item_is_numeric(property) || property == DT_COLLECTION_PROP_FILMROLL;
1495 gtk_tree_selection_set_mode(selection, multi ? GTK_SELECTION_MULTIPLE : GTK_SELECTION_SINGLE);
1496
1497 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->listfilter);
1498 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
1499 gtk_widget_show_all(GTK_WIDGET(d->view));
1500 g_object_unref(model);
1501 d->view_rule = property;
1502 }
1503
1504 // restrict to matching entries while typing
1505 if(dr->typing
1506 && (property == DT_COLLECTION_PROP_CAMERA || property == DT_COLLECTION_PROP_FILENAME
1507 || property == DT_COLLECTION_PROP_FILMROLL || property == DT_COLLECTION_PROP_LENS
1509 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_MODULE
1510 || property == DT_COLLECTION_PROP_ORDER || property == DT_COLLECTION_PROP_RATING
1511 || (property >= DT_COLLECTION_PROP_METADATA
1513 {
1514 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
1515 if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0';
1516 dr->searchstring = needle;
1517 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)list_match_string, dr);
1518 dr->searchstring = NULL;
1519 dt_free(needle);
1520 }
1521 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view));
1522
1523 if(item_is_numeric(property))
1524 {
1525 GRegex *regex = g_regex_new("^\\s*\\[\\s*(.*)\\s*;\\s*(.*)\\s*\\]\\s*$", 0, 0, NULL);
1526 GMatchInfo *match_info;
1527 g_regex_match_full(regex, gtk_entry_get_text(GTK_ENTRY(dr->text)), -1, 0, 0, &match_info, NULL);
1528 const int match_count = g_match_info_get_match_count(match_info);
1529 if(match_count == 3)
1530 {
1531 _range_t *range = (_range_t *)calloc(1, sizeof(_range_t));
1532 range->start = g_match_info_fetch(match_info, 1);
1533 range->stop = g_match_info_fetch(match_info, 2);
1534 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)range_select, range);
1535 if(range->path1 && range->path2)
1536 gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), range->path1, range->path2);
1537 dt_free(range->start);
1538 dt_free(range->stop);
1539 gtk_tree_path_free(range->path1);
1540 gtk_tree_path_free(range->path2);
1541 dt_free(range);
1542 }
1543 else
1544 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr);
1545 g_match_info_free(match_info);
1546 g_regex_unref(regex);
1547 }
1548 else
1549 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr);
1550}
1551
1553{
1554 const int property = _combo_get_active_collection(dr->combo);
1555 if(item_is_tree(property))
1556 _populate_tree(dr);
1557 else
1558 _populate_list(dr);
1559 dr->reveal = FALSE; // one-shot: consumed by this rebuild
1560}
1561
1562// =====================================================================================
1563// Section 5 — clicking a value row -> write the rule text and refresh the collection
1564// =====================================================================================
1565
1566// Append the tag/geotag hierarchy suffix chosen by the modifier keys. Consumes and replaces
1567// `text`: ctrl -> sub-hierarchies only ("|%"), plain -> this hierarchy + sub ("*"), shift -> the
1568// exact node (no suffix).
1569static gchar *_decorate_hierarchy(gchar *text, GdkEventButton *event)
1570{
1571 if(event && dt_modifier_is(event->state, GDK_CONTROL_MASK))
1572 {
1573 gchar *n = g_strconcat(text, "|%", NULL);
1574 dt_free(text);
1575 return n;
1576 }
1577 if(!event || !dt_modifier_is(event->state, GDK_SHIFT_MASK))
1578 {
1579 gchar *n = g_strconcat(text, "*", NULL);
1580 dt_free(text);
1581 return n;
1582 }
1583 return text;
1584}
1585
1586// Clicking a leaf tag in the first rule adopts that tag's saved sort order. Returns TRUE (with
1587// *order filled) when an order change should be signalled.
1588static gboolean _adopt_tag_order(const char *text, int *order)
1589{
1590 const uint32_t tagid = dt_tag_get_tag_id_by_name(text);
1591 if(!tagid)
1592 {
1594 return FALSE;
1595 }
1596 uint32_t sort = DT_COLLECTION_SORT_NONE;
1597 gboolean descending = FALSE;
1598 if(dt_tag_get_tag_order_by_id(tagid, &sort, &descending))
1599 *order = sort | (descending ? DT_COLLECTION_ORDER_FLAG : 0);
1600 else
1601 {
1604 }
1606 return TRUE;
1607}
1608
1609static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d)
1610{
1611 GtkTreeIter iter;
1612 GtkTreeModel *model = NULL;
1613 GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
1614 const int n_selected = gtk_tree_selection_count_selected_rows(selection);
1615 if(n_selected < 1) return;
1616
1617 GList *sels = gtk_tree_selection_get_selected_rows(selection, &model);
1618 GtkTreePath *path1 = (GtkTreePath *)sels->data;
1619 if(!gtk_tree_model_get_iter(model, &iter, path1))
1620 {
1621 g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free);
1622 return;
1623 }
1624
1626 dt_lib_collect_rule_t *active_rule = get_active_rule(d);
1627 active_rule->typing = FALSE;
1628 const int item = d->view_rule;
1629
1630 gchar *text;
1631 gboolean order_request = FALSE;
1632 int order = 0;
1633 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1);
1634
1635 if(text && strlen(text) > 0)
1636 {
1637 if(n_selected > 1 && item_is_numeric(item))
1638 {
1639 // range selection [a;b]
1640 GtkTreeIter iter2;
1641 GtkTreePath *path2 = (GtkTreePath *)g_list_last(sels)->data;
1642 if(gtk_tree_model_get_iter(model, &iter2, path2))
1643 {
1644 gchar *text2;
1645 gtk_tree_model_get(model, &iter2, DT_LIB_COLLECT_COL_PATH, &text2, -1);
1646 gchar *n_text = (item == DT_COLLECTION_PROP_DAY || is_time_property(item))
1647 ? g_strdup_printf("[%s;%s]", text2, text) // dates are reverse-ordered
1648 : g_strdup_printf("[%s;%s]", text, text2);
1649 dt_free(text);
1650 dt_free(text2);
1651 text = n_text;
1652 }
1653 }
1654 else if(item == DT_COLLECTION_PROP_FOLDERS)
1655 {
1656 // recursion is driven by the explicit "include sub-folders" checkbox (#537)
1657 if(d->recursive_check && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->recursive_check)))
1658 {
1659 gchar *n_text = g_strconcat(text, "*", NULL);
1660 dt_free(text);
1661 text = n_text;
1662 }
1663 }
1664 else if(item == DT_COLLECTION_PROP_TAG || item == DT_COLLECTION_PROP_GEOTAGGING)
1665 {
1666 if(gtk_tree_model_iter_has_child(model, &iter))
1667 text = _decorate_hierarchy(text, event);
1668 else if(item == DT_COLLECTION_PROP_TAG && active_rule == d->rule && g_strcmp0(text, _("not tagged")))
1669 order_request = _adopt_tag_order(text, &order);
1670 }
1671 else
1672 _combo_set_active_collection(active_rule->combo, item);
1673 }
1674 g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free);
1675
1676 g_signal_handlers_block_matched(active_rule->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
1677 gtk_entry_set_text(GTK_ENTRY(active_rule->text), text);
1678 gtk_editable_set_position(GTK_EDITABLE(active_rule->text), -1);
1679 g_signal_handlers_unblock_matched(active_rule->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
1680 dt_free(text);
1681
1682 // properties whose value list is unaffected by the new selection only need the conf written;
1683 // the others need the value list refreshed to reflect the new search string.
1687 || item == DT_COLLECTION_PROP_GROUPING)
1688 set_properties(active_rule);
1689 else
1690 update_view(active_rule);
1691
1693 _commit_quiet();
1696}
1697
1698// =====================================================================================
1699// Section 6 — management & admin actions (extensible right-click framework)
1700// =====================================================================================
1701
1703{
1705}
1706
1708{
1709 d->view_rule = -1;
1711}
1712
1713// one selected value row
1714typedef struct collect_row_t
1715{
1716 gchar *path; // DT_LIB_COLLECT_COL_PATH (folder path / tag path)
1717 gint id; // DT_LIB_COLLECT_COL_ID (film_roll id / tag id)
1719
1720static void _free_row(gpointer p)
1721{
1723 dt_free(r->path);
1724 dt_free(r);
1725}
1726
1728{
1729 GtkTreeModel *model = NULL;
1730 GList *paths = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(d->view), &model);
1731 GList *out = NULL;
1732 for(GList *l = paths; l; l = g_list_next(l))
1733 {
1734 GtkTreeIter it;
1735 if(gtk_tree_model_get_iter(model, &it, (GtkTreePath *)l->data))
1736 {
1737 collect_row_t *r = g_malloc0(sizeof(collect_row_t));
1738 gtk_tree_model_get(model, &it, DT_LIB_COLLECT_COL_PATH, &r->path, DT_LIB_COLLECT_COL_ID, &r->id, -1);
1739 out = g_list_prepend(out, r);
1740 }
1741 }
1742 g_list_free_full(paths, (GDestroyNotify)gtk_tree_path_free);
1743 return g_list_reverse(out);
1744}
1745
1746// Map selected rows to the de-duplicated set of matching image ids, reusing the SQL engine.
1747// Foundation for any bulk operation (export, pre-render thumbnails, ...). Caller g_list_free.
1748static GList *_rows_to_imgids(int property, GList *rows, gboolean recursive)
1749{
1750 GHashTable *seen = g_hash_table_new(g_direct_hash, g_direct_equal);
1751 GList *out = NULL;
1752 for(GList *l = rows; l; l = g_list_next(l))
1753 {
1754 collect_row_t *r = (collect_row_t *)l->data;
1755 gchar *text;
1756 if(item_is_folder(property))
1757 text = recursive ? g_strconcat(r->path, "*", NULL) : g_strdup(r->path);
1758 else
1759 text = g_strdup(r->path);
1760 // folder rows always query through FOLDERS (supports the recursive '*' suffix)
1761 const int prop = item_is_folder(property) ? DT_COLLECTION_PROP_FOLDERS : property;
1762 GList *ids = dt_collection_get_images_for_rule(prop, text);
1763 dt_free(text);
1764 for(GList *i = ids; i; i = g_list_next(i))
1765 if(!g_hash_table_contains(seen, i->data))
1766 {
1767 g_hash_table_add(seen, i->data);
1768 out = g_list_prepend(out, i->data);
1769 }
1770 g_list_free(ids);
1771 }
1772 g_hash_table_destroy(seen);
1773 return g_list_reverse(out);
1774}
1775
1776// ---- small dialog helpers ----
1777static gboolean _confirm(const char *title, const char *message)
1778{
1780 GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION,
1781 GTK_BUTTONS_YES_NO, "%s", message);
1782 gtk_window_set_title(GTK_WINDOW(dialog), title);
1783#ifdef GDK_WINDOWING_QUARTZ
1785#endif
1786 const gint res = gtk_dialog_run(GTK_DIALOG(dialog));
1787 gtk_widget_destroy(dialog);
1788 return res == GTK_RESPONSE_YES;
1789}
1790
1791static gchar *_ask_text(const char *title, const char *initial)
1792{
1794 GtkWidget *dialog
1795 = gtk_dialog_new_with_buttons(title, GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_cancel"),
1796 GTK_RESPONSE_CANCEL, _("_ok"), GTK_RESPONSE_ACCEPT, NULL);
1797 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1798 GtkWidget *entry = gtk_entry_new();
1799 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
1800 if(initial) gtk_entry_set_text(GTK_ENTRY(entry), initial);
1801 gtk_box_pack_start(GTK_BOX(area), entry, TRUE, TRUE, 0);
1802 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
1803 g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL);
1804 gtk_widget_show_all(dialog);
1805#ifdef GDK_WINDOWING_QUARTZ
1807#endif
1808 gchar *result = NULL;
1809 if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
1810 {
1811 const gchar *t = gtk_entry_get_text(GTK_ENTRY(entry));
1812 if(t && *t) result = g_strdup(t);
1813 }
1814 gtk_widget_destroy(dialog);
1815 return result;
1816}
1817
1818// ---- actions ----
1819static void _act_folders_remove(dt_lib_collect_t *d, GList *rows)
1820{
1821 // Select the images of exactly the chosen folders (non-recursive: we must not silently pull
1822 // in a whole parent subtree), then hand them to dt_control_remove_images(), which already
1823 // prompts "remove from library vs trash files".
1824 GList *imgids = _rows_to_imgids(DT_COLLECTION_PROP_FOLDERS, rows, FALSE);
1825 if(!imgids) return;
1828 g_list_free(imgids);
1830}
1831
1832static void _act_folders_relocate(dt_lib_collect_t *d, GList *rows)
1833{
1835 const int n = g_list_length(rows);
1836 const gboolean single = (n == 1);
1837 collect_row_t *first = (collect_row_t *)rows->data;
1838
1839 GtkFileChooserNative *fc = gtk_file_chooser_native_new(
1840 single ? _("select the new location of this folder") : _("select the new parent folder"), GTK_WINDOW(win),
1841 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_open"), _("_cancel"));
1842 if(single && first->path) gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc), first->path);
1843
1844 if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(fc)) == GTK_RESPONSE_ACCEPT)
1845 {
1846 gchar *uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(fc));
1847 gchar *chosen = g_filename_from_uri(uri, NULL, NULL);
1848 dt_free(uri);
1849 if(chosen)
1850 {
1851 for(GList *l = rows; l; l = g_list_next(l))
1852 {
1853 collect_row_t *r = (collect_row_t *)l->data;
1854 if(IS_NULL_PTR(r->path)) continue;
1855 if(single)
1856 dt_film_relocate(r->path, chosen);
1857 else
1858 {
1859 gchar *base = g_path_get_basename(r->path);
1860 gchar *dest = g_build_filename(chosen, base, NULL);
1861 dt_film_relocate(r->path, dest);
1862 dt_free(base);
1863 dt_free(dest);
1864 }
1865 }
1870 dt_free(chosen);
1871 }
1872 else
1873 dt_control_log(_("problem selecting new path for the folder"));
1874 }
1875 g_object_unref(fc);
1876}
1877
1878static void _act_tags_remove(dt_lib_collect_t *d, GList *rows)
1879{
1880 const int n = g_list_length(rows);
1881 gchar *msg = g_strdup_printf(ngettext("Delete %d tag and detach it from all images?",
1882 "Delete %d tags and detach them from all images?", n),
1883 n);
1884 const gboolean ok = _confirm(_("delete tags"), msg);
1885 dt_free(msg);
1886 if(!ok) return;
1887
1888 for(GList *l = rows; l; l = g_list_next(l))
1889 {
1890 collect_row_t *r = (collect_row_t *)l->data;
1891 // tree rows don't carry the real tag id (the enumeration uses a placeholder), so resolve
1892 // it from the full tag path
1893 const guint tagid = IS_NULL_PTR(r->path) ? 0 : dt_tag_get_tag_id_by_name(r->path);
1894 if(tagid) dt_tag_remove(tagid, TRUE);
1895 }
1898}
1899
1900static void _act_tag_rename(dt_lib_collect_t *d, GList *rows)
1901{
1902 collect_row_t *r = (collect_row_t *)rows->data;
1903 if(IS_NULL_PTR(r->path)) return;
1904 const guint tagid = dt_tag_get_tag_id_by_name(r->path);
1905 if(!tagid) return;
1906 gchar *newname = _ask_text(_("rename tag"), r->path);
1907 if(newname)
1908 {
1909 dt_tag_rename(tagid, newname);
1910 dt_free(newname);
1913 }
1914}
1915
1916// ---- pre-render thumbnails of the matching image set (background job) ----
1918{
1919 GList *imgids; // owned: imgids to render
1922
1923static void _prerender_free(void *p)
1924{
1926 g_list_free(pr->imgids);
1927 g_free(pr);
1928}
1929
1930// Fill the on-disk mipmap cache for every imgid, largest size first (smaller sizes are then
1931// downscaled from it rather than recomputed). Mirrors the "preload" job in gui/actions/run.c,
1932// but works on an explicit imgid list so it never touches the user's selection.
1933static int32_t _prerender_job(dt_job_t *job)
1934{
1936 const dt_mipmap_size_t max = p->max_size;
1937 const int n = g_list_length(p->imgids);
1938 const float total = (n > 0) ? (float)(n * (max + 1)) : 1.0f;
1939 int done = 0;
1940
1941 for(GList *l = p->imgids; l && dt_control_job_get_state(job) != DT_JOB_STATE_CANCELLED; l = g_list_next(l))
1942 {
1943 const int32_t imgid = GPOINTER_TO_INT(l->data);
1945 {
1946 char filename[PATH_MAX] = { 0 };
1948 if(!dt_util_test_image_file(filename)) // skip thumbnails already on disc
1949 {
1953 }
1954 dt_control_job_set_progress(job, (float)(++done) / total);
1955 }
1956 dt_mimap_cache_evict(darktable.mipmap_cache, imgid); // flush to disc, free RAM
1957 }
1958 return 0;
1959}
1960
1961static void _act_prerender(dt_lib_collect_t *d, GList *rows)
1962{
1963 // recursive for folders so a parent folder renders its whole subtree
1964 GList *imgids = _rows_to_imgids(d->view_rule, rows, TRUE);
1965 if(!imgids) return;
1966 collect_prerender_t *p = g_malloc0(sizeof(collect_prerender_t));
1967 p->imgids = imgids; // takes ownership
1968 p->max_size = DT_MIPMAP_2;
1969 dt_job_t *job = dt_control_job_create(&_prerender_job, "prerender collection thumbnails");
1971 dt_control_job_add_progress(job, _("pre-rendering thumbnails"), TRUE);
1973}
1974
1975// ---- action table: add a bulk operation by adding a row here ----
1976static gboolean _en_folders(int property, int n)
1977{
1978 return item_is_folder(property);
1979}
1980static gboolean _en_tags(int property, int n)
1981{
1982 return item_is_tag(property);
1983}
1984static gboolean _en_tag_single(int property, int n)
1985{
1986 return item_is_tag(property) && n == 1;
1987}
1988static gboolean _en_any(int property, int n)
1989{
1990 return item_is_folder(property) || item_is_tag(property);
1991}
1992
1993typedef struct collect_action_t
1994{
1995 const char *label;
1996 gboolean multi; // allow more than one selected row
1997 gboolean (*enabled)(int property, int n);
1998 void (*run)(dt_lib_collect_t *d, GList *rows);
2000
2001static const collect_action_t ACTIONS[] = {
2002 { N_("remove from library..."), TRUE, _en_folders, _act_folders_remove },
2003 { N_("relocate..."), TRUE, _en_folders, _act_folders_relocate },
2004 { N_("delete tag(s)..."), TRUE, _en_tags, _act_tags_remove },
2005 { N_("rename tag..."), FALSE, _en_tag_single, _act_tag_rename },
2006 { N_("pre-render thumbnails"), TRUE, _en_any, _act_prerender },
2007};
2008
2009static void _action_activate(GtkMenuItem *mi, dt_lib_collect_t *d)
2010{
2011 const collect_action_t *act = g_object_get_data(G_OBJECT(mi), "collect-action");
2012 GList *rows = _selected_rows(d);
2013 if(rows) act->run(d, rows);
2014 g_list_free_full(rows, _free_row);
2015}
2016
2017static void _show_context_menu(dt_lib_collect_t *d, GdkEventButton *event)
2018{
2019 const int property = d->view_rule;
2020 const int n = gtk_tree_selection_count_selected_rows(gtk_tree_view_get_selection(d->view));
2021 if(n < 1) return;
2022
2023 GtkWidget *menu = gtk_menu_new();
2024 int shown = 0;
2025 for(size_t i = 0; i < G_N_ELEMENTS(ACTIONS); i++)
2026 {
2027 const collect_action_t *act = &ACTIONS[i];
2028 if(!act->enabled(property, n)) continue;
2029 if(!act->multi && n > 1) continue;
2030 GtkWidget *mi = gtk_menu_item_new_with_label(_(act->label));
2031 g_object_set_data(G_OBJECT(mi), "collect-action", (gpointer)act);
2032 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_action_activate), d);
2033 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2034 shown++;
2035 }
2036 if(shown)
2037 {
2038 gtk_widget_show_all(menu);
2039 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2040 }
2041 else
2042 gtk_widget_destroy(menu);
2043}
2044
2045// =====================================================================================
2046// Section 7 — view & widget events
2047// =====================================================================================
2048
2049// Drag & drop target: images dragged from the lighttable thumbtable (DND_TARGET_IMGID carries an
2050// array of uint32_t imgids). Dropping on a folder row physically moves the files into it; dropping
2051// on a tag row attaches the tag.
2052static gboolean _drop_move_to_folder(dt_lib_collect_t *d, const char *folder, GList *imgs)
2053{
2054 if(IS_NULL_PTR(folder) || !*folder || IS_NULL_PTR(imgs)) return FALSE;
2055 const int n = g_list_length(imgs);
2056 gchar *msg = g_strdup_printf(ngettext("Physically move %d image to\n%s ?\n\nFiles are moved on disk.",
2057 "Physically move %d images to\n%s ?\n\nFiles are moved on disk.", n),
2058 n, folder);
2059 const gboolean ok = _confirm(_("move images"), msg);
2060 g_free(msg);
2061 if(!ok) return FALSE;
2062
2063 dt_film_t film;
2064 dt_film_init(&film);
2065 dt_film_new(&film, folder); // create-or-fetch the film roll for that folder
2066 const int32_t filmid = film.id;
2067 dt_film_cleanup(&film);
2068 if(filmid <= 0)
2069 {
2070 dt_control_log(_("could not access the destination folder"));
2071 return FALSE;
2072 }
2073
2074 int moved = 0;
2075 for(GList *l = imgs; l; l = g_list_next(l))
2076 if(dt_image_move(GPOINTER_TO_INT(l->data), filmid) != -1) moved++;
2077
2078 if(moved)
2079 {
2085 }
2086 return moved > 0;
2087}
2088
2089static gboolean _drop_attach_tag(dt_lib_collect_t *d, const char *tagpath, GList *imgs)
2090{
2091 // tree rows carry a placeholder id, so resolve the real tag id from the full path
2092 const guint tagid = (IS_NULL_PTR(tagpath) || !*tagpath) ? 0 : dt_tag_get_tag_id_by_name(tagpath);
2093 if(!tagid) return FALSE;
2094 dt_tag_attach_images(tagid, imgs, TRUE);
2098 return TRUE;
2099}
2100
2101// Perform the drop of `sel` (a uint32_t imgid array) onto the row under (x, y). Returns success.
2102static gboolean _do_drop(dt_lib_collect_t *d, GtkTreeView *tree, gint x, gint y, GtkSelectionData *sel)
2103{
2104 const gboolean to_tag = item_is_tag(d->view_rule);
2105 if(!(to_tag || item_is_folder(d->view_rule))) return FALSE;
2106
2107 const int imgs_nb = gtk_selection_data_get_length(sel) / (int)sizeof(uint32_t);
2108 if(imgs_nb <= 0) return FALSE;
2109
2110 GtkTreePath *path = NULL;
2111 if(!gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL)) return FALSE;
2112
2113 GtkTreeModel *model = gtk_tree_view_get_model(tree);
2114 GtkTreeIter iter;
2115 gboolean ok = FALSE;
2116 if(gtk_tree_model_get_iter(model, &iter, path))
2117 {
2118 const uint32_t *imgt = (const uint32_t *)gtk_selection_data_get_data(sel);
2119 GList *imgs = NULL;
2120 for(int i = 0; i < imgs_nb; i++) imgs = g_list_prepend(imgs, GINT_TO_POINTER((int)imgt[i]));
2121
2122 gchar *rowpath = NULL;
2123 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &rowpath, -1);
2124 ok = to_tag ? _drop_attach_tag(d, rowpath, imgs) : _drop_move_to_folder(d, rowpath, imgs);
2125 dt_free(rowpath);
2126 g_list_free(imgs);
2127 }
2128 gtk_tree_path_free(path);
2129 return ok;
2130}
2131
2132static void _view_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
2133 GtkSelectionData *selection_data, guint target_type, guint time,
2135{
2136 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2137 g_signal_stop_emission_by_name(tree, "drag-data-received"); // bypass GtkTreeView's own DnD
2138 const gboolean success = (target_type == DND_TARGET_IMGID && !IS_NULL_PTR(selection_data))
2139 && _do_drop(d, tree, x, y, selection_data);
2140 gtk_drag_finish(context, success, FALSE, time);
2141}
2142
2143static gboolean _view_button_pressed(GtkWidget *treeview, GdkEventButton *event, dt_lib_collect_t *d)
2144{
2145 // We only special-case right-click to raise the management menu; left clicks / expander
2146 // toggles use GtkTreeView's default handling, and activation goes through "row-activated".
2147 if(event->button != 3 || event->type != GDK_BUTTON_PRESS) return FALSE;
2148 if(!(item_is_folder(d->view_rule) || item_is_tag(d->view_rule))) return FALSE;
2149
2150 GtkTreeView *view = GTK_TREE_VIEW(treeview);
2151 GtkTreeSelection *sel = gtk_tree_view_get_selection(view);
2152
2153 // Right-clicking a row outside the current selection re-selects just that row; right-clicking
2154 // within an existing multi-selection keeps it so the batch actions apply to all of it.
2155 GtkTreePath *path = NULL;
2156 if(gtk_tree_view_get_path_at_pos(view, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL) && path)
2157 {
2158 if(!gtk_tree_selection_path_is_selected(sel, path))
2159 {
2160 gtk_tree_selection_unselect_all(sel);
2161 gtk_tree_selection_select_path(sel, path);
2162 }
2163 gtk_tree_path_free(path);
2164 }
2165
2166 // Show the menu whenever something is selected, even if the click missed a precise cell.
2167 if(gtk_tree_selection_count_selected_rows(sel) < 1) return FALSE;
2168 _show_context_menu(d, event);
2169 return TRUE;
2170}
2171
2172static gboolean _view_popup_menu(GtkWidget *treeview, dt_lib_collect_t *d)
2173{
2174 if(!(item_is_folder(d->view_rule) || item_is_tag(d->view_rule))) return FALSE;
2175 _show_context_menu(d, NULL);
2176 return TRUE;
2177}
2178
2179static void _view_row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, dt_lib_collect_t *d)
2180{
2181 GdkEvent *ev = gtk_get_current_event(); // carries ctrl/shift state for tag hierarchy clicks
2182 // only a button event has ->state at the layout row_activated() expects; for key activation
2183 // (Enter) pass NULL so we fall back to the default (plain-click) behaviour.
2184 GdkEventButton *be
2185 = (ev && (ev->type == GDK_BUTTON_PRESS || ev->type == GDK_2BUTTON_PRESS || ev->type == GDK_BUTTON_RELEASE))
2186 ? (GdkEventButton *)ev
2187 : NULL;
2188 row_activated(view, path, be, d);
2189 if(ev) gdk_event_free(ev);
2190}
2191
2192static void _view_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, dt_lib_collect_t *d)
2193{
2194 if(d->view_rule != DT_COLLECTION_PROP_FOLDERS) return;
2195 gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.0, 0.0);
2196}
2197
2198// Document the search syntax of the active property, on both the entry and its combo. Imported
2199// from upstream so the wildcards / operators / ranges stay discoverable.
2201{
2202 const int property = _combo_get_active_collection(dr->combo);
2203
2205 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_EXPOSURE)
2206 gtk_widget_set_tooltip_text(dr->text, _("use <, <=, >, >=, <>, =, [;] as operators"));
2207 else if(property == DT_COLLECTION_PROP_RATING)
2208 gtk_widget_set_tooltip_text(dr->text, _("use <, <=, >, >=, <>, =, [;] as operators\n"
2209 "star rating: 0-5\n"
2210 "rejected images: -1"));
2211 else if(property == DT_COLLECTION_PROP_DAY || is_time_property(property))
2212 gtk_widget_set_tooltip_text(dr->text,
2213 _("use <, <=, >, >=, <>, =, [;] as operators\n"
2214 "type dates in the form: YYYY:MM:DD hh:mm:ss.sss (only the year is mandatory)"));
2215 else if(property == DT_COLLECTION_PROP_FILENAME)
2216 /* xgettext:no-c-format */
2217 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard and `,' to separate values"));
2218 else if(property == DT_COLLECTION_PROP_TAG)
2219 /* xgettext:no-c-format */
2220 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard\n"
2221 "click to include hierarchy + sub-hierarchies (suffix `*')\n"
2222 "shift+click to include only the current hierarchy (no suffix)\n"
2223 "ctrl+click to include only sub-hierarchies (suffix `|%')"));
2224 else if(property == DT_COLLECTION_PROP_GEOTAGGING)
2225 /* xgettext:no-c-format */
2226 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard\n"
2227 "click to include location + sub-locations (suffix `*')\n"
2228 "shift+click to include only the current location (no suffix)\n"
2229 "ctrl+click to include only sub-locations (suffix `|%')"));
2230 else if(property == DT_COLLECTION_PROP_FOLDERS)
2231 /* xgettext:no-c-format */
2232 gtk_widget_set_tooltip_text(dr->text,
2233 _("use `%' as wildcard and append `*' to match sub-folders"));
2234 else
2235 /* xgettext:no-c-format */
2236 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard"));
2237
2238 gchar *tip = gtk_widget_get_tooltip_text(dr->text);
2239 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->combo), tip);
2240 dt_free(tip);
2241}
2242
2243// Show the operator combo only for properties that support comparison operators.
2245{
2246 gtk_widget_set_visible(dr->op_combo, item_is_numeric(_combo_get_active_collection(dr->combo)));
2247}
2248
2250{
2251 if(darktable.gui->reset) return;
2253 c->active_rule = dr->num;
2254 set_properties(dr);
2255 c->view_rule = -1;
2256 _commit_colllection(); // signal-driven refresh, like combo_changed
2257}
2258
2260{
2261 if(darktable.gui->reset) return;
2263 const int previous = _rule_get_item(dr->num); // conf still holds the old property
2264 const int property = _combo_get_active_collection(dr->combo);
2265
2266 c->active_rule = dr->num;
2267 dr->typing = FALSE;
2268
2269 // Clear the search text when switching to an unrelated property; folder<->folder (List/Tree)
2270 // and tag<->tag keep their text since the search string is transferable.
2271 const gboolean transferable
2272 = (item_is_folder(previous) && item_is_folder(property)) || (item_is_tag(previous) && item_is_tag(property));
2273 if(!transferable)
2274 {
2275 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2276 gtk_entry_set_text(GTK_ENTRY(dr->text), "");
2277 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2278
2279 // start the new property from the default "=" operator instead of silently carrying over the
2280 // previous property's operator (which would prefix the now-empty value with e.g. ">")
2281 g_signal_handlers_block_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
2282 gtk_combo_box_set_active(GTK_COMBO_BOX(dr->op_combo), 0);
2283 g_signal_handlers_unblock_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
2284 }
2285
2286 // On the Folders tab the property combo is the List/Tree toggle: keep the sub-folders
2287 // checkbox visible only for the hierarchical (Tree = FOLDERS) view, and the "sort by"
2288 // selector only for the flat film-roll List (the folder Tree is always path-sorted).
2289 if(c->folders_controls && gtk_widget_get_visible(c->folders_controls))
2290 {
2291 gtk_widget_set_visible(c->recursive_check, property == DT_COLLECTION_PROP_FOLDERS);
2292 gtk_widget_set_visible(c->sort_by, property == DT_COLLECTION_PROP_FILMROLL);
2293 gtk_widget_set_visible(c->folder_levels, property == DT_COLLECTION_PROP_FILMROLL);
2294 }
2295
2296 _set_tooltip(dr);
2297 _update_op_combo(dr);
2298 set_properties(dr);
2299
2300 // when the query carried over (e.g. List <-> Tree, or folder <-> film-roll), unfold the rebuilt
2301 // tree to it instead of leaving the user on a collapsed view
2302 dr->reveal = transferable;
2303
2304 // Signal-driven refresh: committing rebuilds where_ext first, then collection_updated ->
2305 // _lib_collect_gui_update rebuilds the value list against the fresh constraints. (Doing the
2306 // rebuild ourselves before committing would use a stale where_ext and not refresh.)
2307 c->view_rule = -1;
2309}
2310
2311static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr)
2312{
2313 dr->typing = TRUE;
2315
2316 // keep the Folders "include sub-folders" checkbox in sync with a *, % or |% typed by hand
2317 if(d->recursive_check && _combo_get_active_collection(dr->combo) == DT_COLLECTION_PROP_FOLDERS)
2318 {
2319 const gchar *t = gtk_entry_get_text(GTK_ENTRY(dr->text));
2320 const gboolean recursive = g_str_has_suffix(t, "*") || g_str_has_suffix(t, "%");
2321 g_signal_handlers_block_matched(d->recursive_check, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, d);
2322 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->recursive_check), recursive);
2323 g_signal_handlers_unblock_matched(d->recursive_check, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, d);
2324 }
2325 update_view(dr);
2326}
2327
2329{
2330 update_view(dr);
2332 const int property = _combo_get_active_collection(dr->combo);
2333
2334 // for flat lists, pressing enter with a single remaining match selects it
2335 if(!item_is_tree(property))
2336 {
2337 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(c->view));
2338 if(gtk_tree_model_iter_n_children(model, NULL) == 1)
2339 {
2340 GtkTreeIter iter;
2341 if(gtk_tree_model_get_iter_first(model, &iter))
2342 {
2343 gchar *text;
2344 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1);
2345 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2346 gtk_entry_set_text(GTK_ENTRY(dr->text), text);
2347 gtk_editable_set_position(GTK_EDITABLE(dr->text), -1);
2348 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2349 dt_free(text);
2350 update_view(dr);
2351 }
2352 }
2353 }
2354 _commit_quiet();
2355 dr->typing = FALSE;
2357}
2358
2359static gboolean _entry_focus_in(GtkWidget *w, GdkEventFocus *event, dt_lib_collect_rule_t *dr)
2360{
2363 return FALSE;
2364}
2365
2366// ---- Folders inline controls ----
2367static void _recursive_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2368{
2369 if(darktable.gui->reset) return;
2372
2373 gchar *t = g_strdup(gtk_entry_get_text(GTK_ENTRY(dr->text)));
2374 // strip any trailing recursion markers, then re-append a single '*' if recursion is wanted
2375 while(g_str_has_suffix(t, "*") || g_str_has_suffix(t, "%") || g_str_has_suffix(t, "|")) t[strlen(t) - 1] = '\0';
2376 gchar *n = gtk_toggle_button_get_active(b) ? g_strconcat(t, "*", NULL) : g_strdup(t);
2377 dt_free(t);
2378
2379 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2380 gtk_entry_set_text(GTK_ENTRY(dr->text), n);
2381 gtk_editable_set_position(GTK_EDITABLE(dr->text), -1);
2382 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2383 dt_free(n);
2384
2385 set_properties(dr);
2386 _commit_quiet();
2387}
2388
2389static void _sort_dir_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2390{
2391 const gboolean desc = gtk_toggle_button_get_active(b);
2393 desc ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP, NULL);
2394 gtk_widget_queue_draw(GTK_WIDGET(b));
2395 if(darktable.gui->reset) return;
2396 dt_conf_set_bool("plugins/collect/descending", desc);
2398}
2399
2401{
2402 if(darktable.gui->reset) return;
2403 dt_conf_set_string("plugins/collect/filmroll_sort", dt_bauhaus_combobox_get(combo) == 0 ? "folder" : "id");
2405}
2406
2407// Surfaced settings that used to live in the hidden preferences popup (TODO).
2409{
2410 if(darktable.gui->reset) return;
2411 dt_conf_set_int("show_folder_levels", (int)gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin)));
2413}
2414
2415static void _no_uncategorized_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2416{
2417 if(darktable.gui->reset) return;
2418 dt_conf_set_bool("plugins/lighttable/tagging/no_uncategorized", gtk_toggle_button_get_active(b));
2420}
2421
2422// ---- Queries raw-SQL escape ----
2423static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d);
2425{
2429 _rule_set_string(0, gtk_entry_get_text(GTK_ENTRY(entry)));
2430 d->active_rule = 0;
2432}
2433
2434// ---- Queries rule +/- management ----
2435static void menuitem_mode(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2436{
2437 const int active = _rules_count();
2438 if(active < MAX_RULES)
2439 {
2440 const dt_lib_collect_mode_t mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "menuitem_mode"));
2441 _rule_set_mode(active, mode);
2442 _rule_set_string(active, "");
2444 _rules_set_count(active + 1);
2446 c->active_rule = active;
2447 c->view_rule = -1;
2448 }
2450}
2451
2452static void menuitem_mode_change(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2453{
2454 const int num = dr->num + 1;
2455 if(num < MAX_RULES && num > 0)
2456 _rule_set_mode(num, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "menuitem_mode")));
2458 c->view_rule = -1;
2460}
2461
2462static void menuitem_clear(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2463{
2464 const int active = _rules_count();
2466 if(active > 1)
2467 {
2468 _rules_set_count(active - 1);
2469 if(c->active_rule >= active - 1) c->active_rule = active - 2;
2470 }
2471 else
2472 {
2475 _rule_set_string(0, "");
2476 dr->typing = FALSE;
2477 }
2478 // shift the rules below the removed one up by one
2479 for(int i = dr->num; i < MAX_RULES - 1; i++)
2480 {
2481 gchar *string = _rule_get_string(i + 1);
2482 if(string)
2483 {
2486 _rule_set_string(i, string);
2487 dt_free(string);
2488 }
2489 }
2490 c->view_rule = -1;
2492}
2493
2494static gboolean popup_button_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_collect_rule_t *dr)
2495{
2496 if(event->button != 1) return FALSE;
2497
2498 GtkWidget *menu = gtk_menu_new();
2499 GtkWidget *mi;
2500 const int active = _rules_count();
2501
2502 mi = gtk_menu_item_new_with_label(_("clear this rule"));
2503 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2504 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_clear), dr);
2505
2506 if(dr->num == active - 1)
2507 {
2508 mi = gtk_menu_item_new_with_label(_("narrow down search"));
2509 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND));
2510 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2511 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2512
2513 mi = gtk_menu_item_new_with_label(_("add more images"));
2514 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_OR));
2515 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2516 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2517
2518 mi = gtk_menu_item_new_with_label(_("exclude images"));
2519 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND_NOT));
2520 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2521 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2522 }
2523 else if(dr->num < active - 1)
2524 {
2525 mi = gtk_menu_item_new_with_label(_("change to: and"));
2526 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND));
2527 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2528 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2529
2530 mi = gtk_menu_item_new_with_label(_("change to: or"));
2531 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_OR));
2532 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2533 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2534
2535 mi = gtk_menu_item_new_with_label(_("change to: except"));
2536 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND_NOT));
2537 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2538 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2539 }
2540
2541 gtk_widget_show_all(GTK_WIDGET(menu));
2542 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2543 return TRUE;
2544}
2545
2546// =====================================================================================
2547// Section 9 — tab configuration (one shared value view, reconfigured per tab)
2548// =====================================================================================
2549
2550static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self);
2551
2552static void _combo_as_view_toggle(GtkWidget *combo) // Folders: List / Tree
2553{
2555 dt_bauhaus_widget_set_label(combo, _("View"));
2558 GUINT_TO_POINTER(DT_COLLECTION_PROP_FILMROLL + 1), NULL, TRUE);
2560 GUINT_TO_POINTER(DT_COLLECTION_PROP_FOLDERS + 1), NULL, TRUE);
2561}
2562
2563static void _combo_as_collections(GtkWidget *combo) // Collections: tags only
2564{
2566 dt_bauhaus_widget_set_label(combo, _("View"));
2569 GUINT_TO_POINTER(DT_COLLECTION_PROP_TAG + 1), NULL, TRUE);
2570}
2571
2572static void _combo_as_full(GtkWidget *combo) // Queries: every property
2573{
2575 dt_bauhaus_widget_set_label(combo, NULL);
2578}
2579
2580static void _set_rule_button(dt_lib_collect_rule_t *dr, gboolean last, gboolean active)
2581{
2582 if(last)
2583 {
2584 gtk_button_set_label(GTK_BUTTON(dr->button), "-");
2585 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule"));
2586 }
2587 else if(active)
2588 {
2589 gtk_button_set_label(GTK_BUTTON(dr->button), "+");
2590 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule or add new rules"));
2591 }
2592 else
2593 {
2594 const int mode = _rule_get_mode(dr->num + 1);
2595 gtk_button_set_label(GTK_BUTTON(dr->button), mode == DT_LIB_COLLECT_MODE_AND ? _("AND")
2596 : mode == DT_LIB_COLLECT_MODE_OR ? _("OR")
2597 : _("AND NOT"));
2598 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule"));
2599 }
2600}
2601
2603{
2604 for(int i = 0; i < MAX_RULES; i++)
2605 {
2606 gtk_widget_set_no_show_all(d->rule[i].hbox, TRUE);
2607 gtk_widget_hide(d->rule[i].hbox);
2608 }
2609 gtk_widget_set_no_show_all(d->folders_controls, TRUE);
2610 gtk_widget_hide(d->folders_controls);
2611 gtk_widget_set_no_show_all(d->collections_controls, TRUE);
2612 gtk_widget_hide(d->collections_controls);
2613 gtk_widget_set_no_show_all(d->raw_box, TRUE);
2614 gtk_widget_hide(d->raw_box);
2615 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
2616 gtk_widget_show(GTK_WIDGET(d->view));
2617}
2618
2620{
2621 ++darktable.gui->reset;
2623
2624 if(tab == TAB_FOLDERS)
2625 {
2627 int item = _rule_get_item(0);
2628 if(!item_is_folder(item))
2629 {
2630 _rule_set_string(0, ""); // coming from a different property family
2632 _rule_set_item(0, item);
2633 }
2634 _combo_as_view_toggle(d->rule[0].combo);
2635 _combo_set_active_collection(d->rule[0].combo, item);
2636 get_properties(&d->rule[0]);
2637 gtk_widget_set_no_show_all(d->rule[0].hbox, FALSE);
2638 gtk_widget_show_all(d->rule[0].hbox);
2639 gtk_widget_show(d->rule[0].combo);
2640 gtk_widget_hide(d->rule[0].button); // adding rules only makes sense on the Queries tab
2641 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[0].text), _("Search a folder..."));
2642 _set_tooltip(&d->rule[0]);
2643 _update_op_combo(&d->rule[0]);
2644
2645 gtk_widget_set_no_show_all(d->folders_controls, FALSE);
2646 gtk_widget_show_all(d->folders_controls);
2647 gtk_widget_set_visible(d->recursive_check, item == DT_COLLECTION_PROP_FOLDERS);
2648 const gboolean desc = dt_conf_get_bool("plugins/collect/descending");
2649 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->sort_dir), desc);
2651 desc ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP, NULL);
2653 d->sort_by, g_strcmp0(dt_conf_get_string_const("plugins/collect/filmroll_sort"), "id") == 0 ? 1 : 0);
2654 // "sort by name/id" and "folder levels" only affect the flat film-roll List; the folder
2655 // Tree is always path-sorted and shows full paths, so hide them there for consistency (TODO).
2656 gtk_widget_set_visible(d->sort_by, item == DT_COLLECTION_PROP_FILMROLL);
2657 gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->folder_levels),
2658 CLAMP(dt_conf_get_int("show_folder_levels"), 1, 5));
2659 gtk_widget_set_visible(d->folder_levels, item == DT_COLLECTION_PROP_FILMROLL);
2660 const gchar *t = gtk_entry_get_text(GTK_ENTRY(d->rule[0].text));
2661 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->recursive_check),
2662 g_str_has_suffix(t, "*") || g_str_has_suffix(t, "%"));
2663 d->active_rule = 0;
2664 }
2665 else if(tab == TAB_COLLECTIONS)
2666 {
2670 _combo_as_collections(d->rule[0].combo);
2672 get_properties(&d->rule[0]);
2673 gtk_widget_set_no_show_all(d->rule[0].hbox, FALSE);
2674 gtk_widget_show_all(d->rule[0].hbox);
2675 gtk_widget_hide(d->rule[0].combo); // single option, no need to show it
2676 gtk_widget_hide(d->rule[0].button); // adding rules only makes sense on the Queries tab
2677 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[0].text), _("Search a collection..."));
2678 _set_tooltip(&d->rule[0]);
2679 _update_op_combo(&d->rule[0]);
2680
2681 gtk_widget_set_no_show_all(d->collections_controls, FALSE);
2682 gtk_widget_show_all(d->collections_controls);
2683 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->no_uncategorized),
2684 dt_conf_get_bool("plugins/lighttable/tagging/no_uncategorized"));
2685 d->active_rule = 0;
2686 }
2687 else // TAB_QUERIES
2688 {
2690 const gboolean raw = (_rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2691 gtk_widget_set_no_show_all(d->raw_box, FALSE);
2692 gtk_widget_show_all(d->raw_box);
2693 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->raw_check), raw);
2694
2695 if(raw)
2696 {
2697 gchar *s = _rule_get_string(0);
2698 gtk_entry_set_text(GTK_ENTRY(d->raw_entry), s ? s : "");
2699 dt_free(s);
2700 gtk_widget_show(d->raw_entry);
2701 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), TRUE);
2702 gtk_widget_hide(GTK_WIDGET(d->view));
2703 d->active_rule = 0;
2704 }
2705 else
2706 {
2707 gtk_widget_hide(d->raw_entry);
2708 for(int i = 0; i <= d->active_rule; i++)
2709 {
2710 _combo_as_full(d->rule[i].combo);
2711 get_properties(&d->rule[i]);
2712 gtk_widget_set_no_show_all(d->rule[i].hbox, FALSE);
2713 gtk_widget_show_all(d->rule[i].hbox);
2714 gtk_widget_show(d->rule[i].combo);
2715 gtk_widget_show(d->rule[i].button); // rule +/- management, Queries tab only
2716 _set_rule_button(&d->rule[i], i == MAX_RULES - 1, i == d->active_rule);
2717 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[i].text), _("Search..."));
2718 _set_tooltip(&d->rule[i]);
2719 _update_op_combo(&d->rule[i]);
2720 }
2721 }
2722 }
2723 --darktable.gui->reset;
2724}
2725
2726static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2727{
2728 if(darktable.gui->reset) return;
2729 if(gtk_toggle_button_get_active(b))
2730 {
2733 _rule_set_string(0, gtk_entry_get_text(GTK_ENTRY(d->raw_entry)));
2734 d->active_rule = 0;
2735 }
2737 {
2739 _rule_set_string(0, "");
2740 }
2741 d->view_rule = -1;
2743 if(!gtk_toggle_button_get_active(b)) update_view(get_active_rule(d)); // raw mode has no value list
2744 _commit_quiet();
2745}
2746
2747static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self)
2748{
2750 dt_conf_set_int("plugins/lighttable/collect/tab", page_num);
2751 if(page_num == TAB_FOLDERS || page_num == TAB_COLLECTIONS) _rules_set_count(1);
2752
2753 const gboolean raw = (page_num == TAB_QUERIES && _rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2754 _configure_tab(d, page_num);
2755 d->view_rule = -1;
2756 if(!raw)
2757 {
2758 // _configure_tab kept the query whenever the destination field is compatible; unfold the tree
2759 // to it (a no-op when the field was incompatible and the entry was cleared)
2762 }
2763 // raw SQL mode has no value list
2764 // NB: switching tabs only reconfigures the GUI and rebuilds the value list; it must NOT
2765 // re-run the collection query (that rebuilds the whole lighttable and was the slow path).
2766 // The collection updates when the user actually clicks a value or edits a rule.
2767}
2768
2770{
2772 if(d->view_rule != -1) return; // nothing changed since the last build
2773
2774 ++darktable.gui->reset;
2776
2777 dt_collect_tab_t tab;
2778 if(item_is_folder(_rule_get_item(0)) && d->nb_rules == 1)
2779 tab = TAB_FOLDERS;
2780 else if(item_is_tag(_rule_get_item(0)) && d->nb_rules == 1)
2781 tab = TAB_COLLECTIONS;
2782 else
2783 tab = TAB_QUERIES;
2784 dt_conf_set_int("plugins/lighttable/collect/tab", tab);
2785
2786 g_signal_handlers_block_matched(d->notebook, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _on_tab_switch, NULL);
2787 gtk_notebook_set_current_page(GTK_NOTEBOOK(d->notebook), tab);
2788 g_signal_handlers_unblock_matched(d->notebook, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _on_tab_switch, NULL);
2789
2790 const gboolean raw = (tab == TAB_QUERIES && _rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2791 _configure_tab(d, tab);
2792 if(!raw) update_view(get_active_rule(d)); // raw SQL mode has no value list
2793 --darktable.gui->reset;
2794}
2795
2796// =====================================================================================
2797// Section 10 — signals
2798// =====================================================================================
2799
2800static void collection_updated(gpointer instance, dt_collection_change_t query_change,
2801 dt_collection_properties_t changed_property, gpointer imgs, int next, gpointer self)
2802{
2803 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2804 d->view_rule = -1;
2806
2807 // On a pure reload (no query change) only rebuild if a property we display actually changed.
2808 gboolean refresh = TRUE;
2809 if(query_change == DT_COLLECTION_CHANGE_RELOAD && changed_property != DT_COLLECTION_PROP_UNDEF)
2810 {
2811 refresh = FALSE;
2812 for(int i = 0; i <= d->active_rule; i++)
2813 if(_combo_get_active_collection(d->rule[i].combo) == changed_property)
2814 {
2815 refresh = TRUE;
2816 break;
2817 }
2818 }
2819 if(refresh) _lib_collect_gui_update(self);
2820}
2821
2822static void filmrolls_updated(gpointer instance, gpointer self)
2823{
2824 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2825 d->view_rule = -1;
2827}
2828
2829static void filmrolls_removed(gpointer instance, gpointer self)
2830{
2831 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2832 d->view_rule = -1;
2835}
2836
2841
2842static void tag_changed(gpointer instance, gpointer self)
2843{
2844 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2845 gboolean uses_tag = FALSE;
2846 for(int i = 0; i < d->nb_rules; i++)
2848 {
2849 uses_tag = TRUE;
2850 break;
2851 }
2852
2853 d->view_rule = -1;
2855 if(uses_tag)
2856 {
2862 }
2864}
2865
2866static void geotag_changed(gpointer instance, GList *imgs, const int locid, gpointer self)
2867{
2868 if(locid) return; // not our concern
2869 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2871 {
2872 d->view_rule = -1;
2878 NULL);
2881 }
2882}
2883
2898
2899#ifdef _WIN32
2900static void _mount_changed(GVolumeMonitor *volume_monitor, GMount *mount, dt_lib_module_t *self)
2901#else
2902static void _mount_changed(GUnixMountMonitor *monitor, dt_lib_module_t *self)
2903#endif
2904{
2908 {
2909 d->view_rule = -1;
2911 }
2912}
2913
2914// =====================================================================================
2915// Section 11 — preferences popup, construction & teardown
2916// =====================================================================================
2917
2918// NB: the old hidden "preferences..." popup has been retired (TODO). Every collect setting it
2919// exposed now lives in the front widget: filmroll_sort / descending / folder_levels on the
2920// Folders tab, no_uncategorized on the Collections tab.
2921
2923{
2924 dt_lib_collect_t *d = (dt_lib_collect_t *)calloc(1, sizeof(dt_lib_collect_t));
2925 self->data = (void *)d;
2926 self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2927
2928 d->active_rule = 0;
2929 d->nb_rules = 0;
2930 d->view_rule = -1;
2931 d->params = (dt_lib_collect_params_t *)malloc(sizeof(dt_lib_collect_params_t));
2932
2933 // notebook: only its tab-bar is used; the content below is shared and reconfigured per tab
2934 d->notebook = GTK_WIDGET(dt_ui_notebook_new());
2935 dt_gui_add_class(d->notebook, "empty");
2936 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Folders"), _("Browse and manage the folders known to Ansel"));
2937 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Collections"), _("Browse and manage tags"));
2938 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Queries"), _("Build arbitrary collections"));
2939 gtk_widget_show_all(d->notebook);
2940 gtk_box_pack_start(GTK_BOX(self->widget), d->notebook, TRUE, TRUE, 0);
2941 gtk_notebook_set_scrollable(GTK_NOTEBOOK(d->notebook), TRUE);
2942 g_signal_connect(G_OBJECT(d->notebook), "switch_page", G_CALLBACK(_on_tab_switch), self);
2943
2944 // one rule row per possible rule (only the relevant ones are shown per tab)
2945 for(int i = 0; i < MAX_RULES; i++)
2946 {
2947 d->rule[i].num = i;
2948 d->rule[i].typing = FALSE;
2949 d->rule[i].lib_collect = (void *)d;
2950
2951 GtkBox *box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
2952 d->rule[i].hbox = GTK_WIDGET(box);
2953 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
2954 gtk_widget_set_name(GTK_WIDGET(box), "lib-dtbutton");
2955
2958 _populate_collect_combo(d->rule[i].combo);
2959 g_signal_connect(G_OBJECT(d->rule[i].combo), "value-changed", G_CALLBACK(combo_changed), d->rule + i);
2960 gtk_box_pack_start(box, d->rule[i].combo, FALSE, FALSE, 0);
2961
2962 GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING));
2963 gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2964
2965 // comparison-operator selector, shown only for numeric/date/rating properties
2966 d->rule[i].op_combo = gtk_combo_box_text_new();
2967 for(int o = 0; o < COLLECT_N_OPS; o++)
2968 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(d->rule[i].op_combo), OP_LABELS[o]);
2969 gtk_combo_box_set_active(GTK_COMBO_BOX(d->rule[i].op_combo), 0);
2970 gtk_widget_set_no_show_all(d->rule[i].op_combo, TRUE);
2971 gtk_widget_set_tooltip_text(d->rule[i].op_combo, _("comparison operator"));
2972 g_signal_connect(G_OBJECT(d->rule[i].op_combo), "changed", G_CALLBACK(_op_changed), d->rule + i);
2973 gtk_box_pack_start(hbox, d->rule[i].op_combo, FALSE, FALSE, 0);
2974
2975 GtkWidget *w = gtk_search_entry_new();
2977 d->rule[i].text = w;
2978 gtk_widget_add_events(w, GDK_FOCUS_CHANGE_MASK | GDK_KEY_PRESS_MASK);
2979 gtk_entry_set_placeholder_text(GTK_ENTRY(w), _("Search..."));
2980 g_signal_connect(G_OBJECT(w), "focus-in-event", G_CALLBACK(_entry_focus_in), d->rule + i);
2981 g_signal_connect(G_OBJECT(w), "changed", G_CALLBACK(entry_changed), d->rule + i);
2982 g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(entry_activated), d->rule + i);
2983 gtk_widget_set_name(GTK_WIDGET(w), "lib-collect-entry");
2984 gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
2985 gtk_entry_set_width_chars(GTK_ENTRY(w), 5);
2986
2987 d->rule[i].button = gtk_button_new();
2988 gtk_widget_set_events(d->rule[i].button, GDK_BUTTON_PRESS_MASK);
2989 g_signal_connect(G_OBJECT(d->rule[i].button), "button-press-event", G_CALLBACK(popup_button_callback),
2990 d->rule + i);
2991 gtk_box_pack_start(hbox, d->rule[i].button, FALSE, FALSE, 0);
2992 }
2993
2994 // Folders inline controls (sort + recursion), shown only on the Folders tab
2995 d->folders_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2996 d->recursive_check = gtk_check_button_new_with_label(_("include sub-folders"));
2997 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->recursive_check), TRUE);
2998 g_signal_connect(G_OBJECT(d->recursive_check), "toggled", G_CALLBACK(_recursive_toggled), d);
2999 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->recursive_check, FALSE, FALSE, 0);
3000
3002 dt_bauhaus_widget_set_label(d->sort_by, _("sort by"));
3003 dt_bauhaus_combobox_add(d->sort_by, _("name"));
3004 dt_bauhaus_combobox_add(d->sort_by, _("id"));
3005 // bauhaus widgets render at their own (short) natural height; in this horizontal row they would
3006 // otherwise stick to the top and sit higher than the native spin/toggle siblings, so center them
3007 gtk_widget_set_valign(d->sort_by, GTK_ALIGN_CENTER);
3008 g_signal_connect(G_OBJECT(d->sort_by), "value-changed", G_CALLBACK(_sort_by_changed), d);
3009 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->sort_by, TRUE, TRUE, 0);
3010
3012 dt_gui_add_class(d->sort_dir, "dt_ignore_fg_state");
3013 gtk_widget_set_tooltip_text(d->sort_dir, _("toggle ascending / descending order"));
3014 g_signal_connect(G_OBJECT(d->sort_dir), "toggled", G_CALLBACK(_sort_dir_toggled), d);
3015 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->sort_dir, FALSE, FALSE, 0);
3016
3017 d->folder_levels = gtk_spin_button_new_with_range(1, 5, 1);
3018 gtk_widget_set_tooltip_text(d->folder_levels,
3019 _("number of folder levels to show in film-roll names, from the right"));
3020 g_signal_connect(G_OBJECT(d->folder_levels), "value-changed", G_CALLBACK(_folder_levels_changed), d);
3021 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->folder_levels, FALSE, FALSE, 0);
3022 gtk_box_pack_start(GTK_BOX(self->widget), d->folders_controls, FALSE, FALSE, 0);
3023
3024 // Collections inline controls, shown only on the Collections tab
3025 d->collections_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3026 d->no_uncategorized = gtk_check_button_new_with_label(_("no 'uncategorized' group"));
3027 gtk_widget_set_tooltip_text(d->no_uncategorized,
3028 _("do not group childless tags under an 'uncategorized' entry"));
3029 g_signal_connect(G_OBJECT(d->no_uncategorized), "toggled", G_CALLBACK(_no_uncategorized_toggled), d);
3030 gtk_box_pack_start(GTK_BOX(d->collections_controls), d->no_uncategorized, FALSE, FALSE, 0);
3031 gtk_box_pack_start(GTK_BOX(self->widget), d->collections_controls, FALSE, FALSE, 0);
3032
3033 // Queries raw-SQL escape, shown only on the Queries tab
3034 d->raw_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3035 d->raw_check = gtk_check_button_new_with_label(_("edit as raw SQL"));
3036 g_signal_connect(G_OBJECT(d->raw_check), "toggled", G_CALLBACK(_raw_toggled), d);
3037 gtk_box_pack_start(GTK_BOX(d->raw_box), d->raw_check, FALSE, FALSE, 0);
3038 d->raw_entry = gtk_entry_new();
3039 gtk_entry_set_placeholder_text(GTK_ENTRY(d->raw_entry),
3040 _("SQL WHERE expression, e.g. iso > 800 AND lens LIKE '%50mm%'"));
3042 g_signal_connect(G_OBJECT(d->raw_entry), "activate", G_CALLBACK(_raw_entry_activated), d);
3043 gtk_box_pack_start(GTK_BOX(d->raw_box), d->raw_entry, FALSE, FALSE, 0);
3044 gtk_box_pack_start(GTK_BOX(self->widget), d->raw_box, FALSE, FALSE, 0);
3045
3046 // shared value view
3047 GtkTreeView *view = GTK_TREE_VIEW(gtk_tree_view_new());
3048 d->view = view;
3049 gtk_tree_view_set_headers_visible(view, FALSE);
3050 gtk_tree_view_set_activate_on_single_click(view, TRUE);
3051 gtk_widget_set_can_focus(GTK_WIDGET(view), TRUE);
3052 g_signal_connect(G_OBJECT(view), "button-press-event", G_CALLBACK(_view_button_pressed), d);
3053 g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(_view_popup_menu), d);
3054 g_signal_connect(G_OBJECT(view), "row-activated", G_CALLBACK(_view_row_activated), d);
3055 g_signal_connect(G_OBJECT(view), "row-expanded", G_CALLBACK(_view_row_expanded), d);
3056
3057 // accept images dragged from the lighttable thumbtable: drop on a folder row to move the
3058 // files there, on a tag row to attach the tag (handled in _view_drag_data_received)
3059 gtk_drag_dest_set(GTK_WIDGET(view), GTK_DEST_DEFAULT_ALL, target_list_internal, n_targets_internal,
3060 GDK_ACTION_MOVE);
3061 g_signal_connect(G_OBJECT(view), "drag-data-received", G_CALLBACK(_view_drag_data_received), d);
3062
3063 GtkTreeViewColumn *col = gtk_tree_view_column_new();
3064 gtk_tree_view_append_column(view, col);
3065 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
3066 gtk_tree_view_column_pack_start(col, renderer, TRUE);
3067 gtk_tree_view_column_set_cell_data_func(col, renderer, tree_count_show, NULL, NULL);
3068 gtk_tree_view_column_add_attribute(col, renderer, "weight", DT_LIB_COLLECT_COL_FONT);
3069 g_object_set(renderer, "strikethrough", TRUE, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
3070 gtk_tree_view_column_add_attribute(col, renderer, "strikethrough-set", DT_LIB_COLLECT_COL_UNREACHABLE);
3071
3072 GtkTreeModel *listmodel = GTK_TREE_MODEL(
3073 gtk_list_store_new(DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING,
3074 G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INT));
3075 d->listfilter = gtk_tree_model_filter_new(listmodel, NULL);
3076 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(d->listfilter), DT_LIB_COLLECT_COL_VISIBLE);
3077
3078 GtkTreeModel *treemodel = GTK_TREE_MODEL(
3079 gtk_tree_store_new(DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING,
3080 G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INT));
3081 d->treefilter = gtk_tree_model_filter_new(treemodel, NULL);
3082 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(d->treefilter), DT_LIB_COLLECT_COL_VISIBLE);
3083 g_object_unref(treemodel);
3084
3085 // Static height: the collection list refreshes on selection/act-on, so a fixed user-set size keeps
3086 // the side-panel layout from jumping. Defaults to ~200px until the user drags the grip.
3087 gtk_box_pack_start(GTK_BOX(self->widget),
3088 dt_ui_scroll_wrap(GTK_WIDGET(view), 200, "plugins/lighttable/collect/windowheight",
3090 TRUE, TRUE, 0);
3091
3092 // proxy used by other code to force a refresh
3095
3097
3098#ifdef _WIN32
3099 d->vmonitor = g_volume_monitor_get();
3100 g_signal_connect(G_OBJECT(d->vmonitor), "mount-changed", G_CALLBACK(_mount_changed), self);
3101 g_signal_connect(G_OBJECT(d->vmonitor), "mount-added", G_CALLBACK(_mount_changed), self);
3102#else
3103 d->vmonitor = g_unix_mount_monitor_get();
3104 g_signal_connect(G_OBJECT(d->vmonitor), "mounts-changed", G_CALLBACK(_mount_changed), self);
3105#endif
3106
3108 self);
3110 self);
3112 self);
3114 self);
3118 self);
3119}
3120
3122{
3123 if(IS_NULL_PTR(self->data)) return;
3125
3134
3135 dt_free(d->params);
3136 g_object_unref(d->treefilter);
3137 g_object_unref(d->listfilter);
3138 g_object_unref(d->vmonitor);
3139 dt_free(self->data);
3140}
3141
3142// clang-format off
3143// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3144// vim: shiftwidth=2 expandtab tabstop=2 cindent
3145// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3146// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_bauhaus_combobox_clear(GtkWidget *widget)
Definition bauhaus.c:2189
void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
Definition bauhaus.c:2092
gboolean dt_bauhaus_combobox_set_from_value(GtkWidget *widget, int value)
Definition bauhaus.c:2330
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2347
void dt_bauhaus_combobox_add_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align, gpointer data, void(free_func)(void *data), gboolean sensitive)
Definition bauhaus.c:2038
gpointer dt_bauhaus_combobox_get_data(GtkWidget *widget)
Definition bauhaus.c:2179
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2301
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1653
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1842
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:2016
@ DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT
Definition bauhaus.h:125
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
static void get_properties(dt_lib_collect_rule_t *dr)
Definition collect.c:519
static gboolean _confirm(const char *title, const char *message)
Definition collect.c:1777
void gui_reset(dt_lib_module_t *self)
Definition collect.c:626
int set_params(dt_lib_module_t *self, const void *params, int size)
Definition collect.c:610
static void _set_tooltip(dt_lib_collect_rule_t *dr)
Definition collect.c:2200
static void _hide_all_widgets(dt_lib_collect_t *d)
Definition collect.c:2602
static gboolean tree_expand(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:992
static void metadata_changed(gpointer instance, int type, gpointer self)
Definition collect.c:2884
static void _folder_levels_changed(GtkWidget *spin, dt_lib_collect_t *d)
Definition collect.c:2408
void * get_params(dt_lib_module_t *self, int *size)
Definition collect.c:601
static void _collect_expanded_cb(GtkTreeView *view, GtkTreePath *path, gpointer data)
Definition collect.c:1115
static int _rule_get_mode(int n)
Definition collect.c:471
dt_lib_collect_cols_t
Definition collect.c:173
@ DT_LIB_COLLECT_COL_TOOLTIP
Definition collect.c:176
@ DT_LIB_COLLECT_COL_ID
Definition collect.c:175
@ DT_LIB_COLLECT_COL_COUNT
Definition collect.c:180
@ DT_LIB_COLLECT_COL_UNREACHABLE
Definition collect.c:179
@ DT_LIB_COLLECT_NUM_COLS
Definition collect.c:183
@ DT_LIB_COLLECT_COL_FONT
Definition collect.c:182
@ DT_LIB_COLLECT_COL_INDEX
Definition collect.c:181
@ DT_LIB_COLLECT_COL_PATH
Definition collect.c:177
@ DT_LIB_COLLECT_COL_TEXT
Definition collect.c:174
@ DT_LIB_COLLECT_COL_VISIBLE
Definition collect.c:178
static void _view_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, dt_lib_collect_t *d)
Definition collect.c:2132
void * legacy_params(struct dt_lib_module_t *self, const void *const old_params, const size_t old_params_size, const int old_version, int *new_version, size_t *new_size)
Definition collect.c:378
static void _populate_collect_combo(GtkWidget *w)
Definition collect.c:655
static void _mount_changed(GUnixMountMonitor *monitor, dt_lib_module_t *self)
Definition collect.c:2902
static void geotag_changed(gpointer instance, GList *imgs, const int locid, gpointer self)
Definition collect.c:2866
#define PARAM_STRING_SIZE
Definition collect.c:163
static const char * UNCATEGORIZED_TAG
Definition collect.c:1112
static void _free_row(gpointer p)
Definition collect.c:1720
static int _rules_count()
Definition collect.c:447
static GList * _collect_sorted_tree_names(int property, int rule)
Definition collect.c:1201
#define COLLECT_N_OPS
Definition collect.c:312
static void menuitem_clear(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2462
static void get_number_of_rules(dt_lib_collect_t *d)
Definition collect.c:578
static void _action_activate(GtkMenuItem *mi, dt_lib_collect_t *d)
Definition collect.c:2009
static void _sort_dir_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2389
static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self)
Definition collect.c:2747
static void _raw_entry_activated(GtkWidget *entry, dt_lib_collect_t *d)
Definition collect.c:2424
static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2726
static char ** _split_tree_name(int property, const char *name)
Definition collect.c:1145
static void _configure_tab(dt_lib_collect_t *d, dt_collect_tab_t tab)
Definition collect.c:2619
static void _view_row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, dt_lib_collect_t *d)
Definition collect.c:2179
static void _rule_set_mode(int n, int mode)
Definition collect.c:478
static int32_t _prerender_job(dt_job_t *job)
Definition collect.c:1933
static void _split_operator(const char *text, int *op_idx, const char **value)
Definition collect.c:315
static gboolean item_is_tree(int item)
Definition collect.c:302
static gboolean _drop_move_to_folder(dt_lib_collect_t *d, const char *folder, GList *imgs)
Definition collect.c:2052
static void tag_changed(gpointer instance, gpointer self)
Definition collect.c:2842
static gboolean _view_button_pressed(GtkWidget *treeview, GdkEventButton *event, dt_lib_collect_t *d)
Definition collect.c:2143
static gboolean _en_tags(int property, int n)
Definition collect.c:1980
static void _build_tree_store(GtkTreeStore *store, int property, GList *sorted_names, gboolean no_uncategorized, const char *format_separator)
Definition collect.c:1238
static gboolean list_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:794
static int string_array_length(char **list)
Definition collect.c:706
static gboolean item_is_tag(int item)
Definition collect.c:289
static void _lib_collect_gui_update(dt_lib_module_t *self)
Definition collect.c:2769
static void _force_refresh(dt_lib_collect_t *d)
Definition collect.c:1707
static void _op_changed(GtkWidget *w, dt_lib_collect_rule_t *dr)
Definition collect.c:2249
static gboolean _view_popup_menu(GtkWidget *treeview, dt_lib_collect_t *d)
Definition collect.c:2172
static void _combo_as_view_toggle(GtkWidget *combo)
Definition collect.c:2552
static gboolean tree_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:867
void gui_cleanup(dt_lib_module_t *self)
Definition collect.c:3121
static void _populate_list(dt_lib_collect_rule_t *dr)
Definition collect.c:1447
static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d)
Definition collect.c:1609
static void tree_count_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
Definition collect.c:776
static void _show_context_menu(dt_lib_collect_t *d, GdkEventButton *event)
Definition collect.c:2017
static gboolean _drop_attach_tag(dt_lib_collect_t *d, const char *tagpath, GList *imgs)
Definition collect.c:2089
static GList * _selected_rows(dt_lib_collect_t *d)
Definition collect.c:1727
static char * tag_collate_key(char *tag)
Definition collect.c:760
static const char *const OP_LABELS[]
Definition collect.c:311
static gboolean _do_drop(dt_lib_collect_t *d, GtkTreeView *tree, gint x, gint y, GtkSelectionData *sel)
Definition collect.c:2102
static void _act_tags_remove(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1878
static void _no_uncategorized_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2415
static gboolean _combo_set_active_collection(GtkWidget *combo, const int property)
Definition collect.c:648
static void _lib_collect_update_params(dt_lib_collect_t *d)
Definition collect.c:585
dt_collect_tab_t
Definition collect.c:166
@ TAB_FOLDERS
Definition collect.c:167
@ TAB_QUERIES
Definition collect.c:169
@ TAB_COLLECTIONS
Definition collect.c:168
static gint sort_folder_tag(gconstpointer a, gconstpointer b)
Definition collect.c:752
static void menuitem_mode_change(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2452
static void entry_activated(GtkWidget *entry, dt_lib_collect_rule_t *dr)
Definition collect.c:2328
static gboolean item_is_folder(int item)
Definition collect.c:284
void init_presets(dt_lib_module_t *self)
Definition collect.c:390
static void _rules_set_count(int n)
Definition collect.c:452
static GList * _rows_to_imgids(int property, GList *rows, gboolean recursive)
Definition collect.c:1748
static void _act_folders_remove(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1819
static void _combo_as_collections(GtkWidget *combo)
Definition collect.c:2563
static void _view_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, dt_lib_collect_t *d)
Definition collect.c:2192
static void _prerender_free(void *p)
Definition collect.c:1923
static dt_lib_collect_t * get_collect(dt_lib_collect_rule_t *r)
Definition collect.c:568
#define ADD_COLLECT_ENTRY(value)
uint32_t container(dt_lib_module_t *self)
Definition collect.c:368
static gboolean _entry_focus_in(GtkWidget *w, GdkEventFocus *event, dt_lib_collect_rule_t *dr)
Definition collect.c:2359
static int is_time_property(int property)
Definition collect.c:277
static dt_lib_module_t * _self()
Definition collect.c:1702
static gboolean _restore_expanded_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1134
static void set_properties(dt_lib_collect_rule_t *dr)
Definition collect.c:501
static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr)
Definition collect.c:2311
static void _act_tag_rename(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1900
static void filmrolls_removed(gpointer instance, gpointer self)
Definition collect.c:2829
static const collect_action_t ACTIONS[]
Definition collect.c:2001
static gboolean range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:965
static void menuitem_mode(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2435
static int _rule_get_item(int n)
Definition collect.c:457
static void collection_updated(gpointer instance, dt_collection_change_t query_change, dt_collection_properties_t changed_property, gpointer imgs, int next, gpointer self)
Definition collect.c:2800
static void preferences_changed(gpointer instance, gpointer self)
Definition collect.c:2837
static void _act_folders_relocate(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1832
static void _rule_set_item(int n, int item)
Definition collect.c:464
static gboolean popup_button_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_collect_rule_t *dr)
Definition collect.c:2494
void gui_init(dt_lib_module_t *self)
Definition collect.c:2922
static GtkTreeModel * _create_filtered_model(GtkTreeModel *model, dt_lib_collect_rule_t *dr)
Definition collect.c:1101
int position()
Definition collect.c:373
static void _rule_set_string(int n, const char *s)
Definition collect.c:492
static void _commit_colllection()
Definition collect.c:553
const char ** views(dt_lib_module_t *self)
Definition collect.c:362
static gboolean _maybe_file_uncategorized(GtkTreeStore *store, const char *name, const char *next_name_raw, GtkTreeIter *uncategorized, guint *index, int count)
Definition collect.c:1170
static gboolean list_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:945
static gboolean _en_folders(int property, int n)
Definition collect.c:1976
static void combo_changed(GtkWidget *combo, dt_lib_collect_rule_t *dr)
Definition collect.c:2259
static void tree_set_visibility(GtkTreeModel *model, gpointer data)
Definition collect.c:903
static void _set_rule_button(dt_lib_collect_rule_t *dr, gboolean last, gboolean active)
Definition collect.c:2580
static gchar * _rule_get_string(int n)
Definition collect.c:485
static gchar * _ask_text(const char *title, const char *initial)
Definition collect.c:1791
static dt_lib_collect_rule_t * get_active_rule(dt_lib_collect_t *d)
Definition collect.c:573
static void filmrolls_updated(gpointer instance, gpointer self)
Definition collect.c:2822
static void _act_prerender(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1961
static gboolean _en_any(int property, int n)
Definition collect.c:1988
static void _commit_quiet()
Definition collect.c:559
static void _update_op_combo(dt_lib_collect_rule_t *dr)
Definition collect.c:2244
static void _recursive_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2367
static const char *const OP_TOKENS[]
Definition collect.c:310
static gboolean item_is_numeric(int item)
Definition collect.c:294
static GtkTreePath * _folders_root_collapse_path(GtkTreeModel *model)
Definition collect.c:1069
static gboolean tree_range_visible(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:932
static gboolean tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:889
static gboolean _en_tag_single(int property, int n)
Definition collect.c:1984
static void update_view(dt_lib_collect_rule_t *dr)
Definition collect.c:1552
static void free_tuple(gpointer data)
Definition collect.c:744
static void _propagate_count_to_ancestors(GtkTreeStore *store, GtkTreeIter *leaf, int count)
Definition collect.c:1155
static gboolean _adopt_tag_order(const char *text, int *order)
Definition collect.c:1588
static void _sort_by_changed(GtkWidget *combo, dt_lib_collect_t *d)
Definition collect.c:2400
#define CLEAR_PARAMS(r)
static void _combo_as_full(GtkWidget *combo)
Definition collect.c:2572
static char ** split_path(const char *path)
Definition collect.c:714
static guint64 _date_key(const char *s, char pad)
Definition collect.c:913
#define MAX_RULES
Definition collect.c:162
static int _combo_get_active_collection(GtkWidget *combo)
Definition collect.c:643
static gchar * _decorate_hierarchy(gchar *text, GdkEventButton *event)
Definition collect.c:1569
static void _populate_tree(dt_lib_collect_rule_t *dr)
Definition collect.c:1318
dt_lib_collect_mode_t
Definition collect.h:36
@ DT_LIB_COLLECT_MODE_OR
Definition collect.h:38
@ DT_LIB_COLLECT_MODE_AND_NOT
Definition collect.h:39
@ DT_LIB_COLLECT_MODE_AND
Definition collect.h:37
void dt_collection_update_query(const dt_collection_t *collection, dt_collection_change_t query_change, dt_collection_properties_t changed_property, GList *list)
void dt_collection_set_query_flags(const dt_collection_t *collection, dt_collection_query_flags_t flags)
Definition collection.c:588
void dt_collection_memory_update()
Definition collection.c:201
void dt_collection_split_operator_number(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:924
GList * dt_collection_get_images_for_rule(const dt_collection_properties_t property, const char *text)
void dt_collection_set_tag_id(dt_collection_t *collection, const uint32_t tagid)
Definition collection.c:632
void dt_collection_name_value_free(gpointer value)
GList * dt_collection_get_property_values(const dt_collection_properties_t property, const int rule)
dt_collection_properties_t
Definition collection.h:107
@ DT_COLLECTION_PROP_EXPOSURE
Definition collection.h:115
@ DT_COLLECTION_PROP_MODULE
Definition collection.h:134
@ DT_COLLECTION_PROP_RATING
Definition collection.h:136
@ DT_COLLECTION_PROP_QUERY
Definition collection.h:138
@ DT_COLLECTION_PROP_TIME
Definition collection.h:120
@ DT_COLLECTION_PROP_METADATA
Definition collection.h:129
@ DT_COLLECTION_PROP_GROUPING
Definition collection.h:130
@ DT_COLLECTION_PROP_TAG
Definition collection.h:127
@ DT_COLLECTION_PROP_FILMROLL
Definition collection.h:108
@ DT_COLLECTION_PROP_LENS
Definition collection.h:113
@ DT_COLLECTION_PROP_CAMERA
Definition collection.h:112
@ DT_COLLECTION_PROP_LOCAL_COPY
Definition collection.h:131
@ DT_COLLECTION_PROP_GEOTAGGING
Definition collection.h:126
@ DT_COLLECTION_PROP_FILENAME
Definition collection.h:110
@ DT_COLLECTION_PROP_ISO
Definition collection.h:117
@ DT_COLLECTION_PROP_COLORLABEL
Definition collection.h:128
@ DT_COLLECTION_PROP_UNDEF
Definition collection.h:142
@ DT_COLLECTION_PROP_DAY
Definition collection.h:119
@ DT_COLLECTION_PROP_ORDER
Definition collection.h:135
@ DT_COLLECTION_PROP_FOLDERS
Definition collection.h:109
@ DT_COLLECTION_PROP_APERTURE
Definition collection.h:114
@ DT_COLLECTION_PROP_IMPORT_TIMESTAMP
Definition collection.h:121
@ DT_COLLECTION_PROP_FOCAL_LENGTH
Definition collection.h:116
@ DT_COLLECTION_PROP_EXPORT_TIMESTAMP
Definition collection.h:123
@ DT_COLLECTION_PROP_CHANGE_TIMESTAMP
Definition collection.h:122
@ DT_COLLECTION_PROP_HISTORY
Definition collection.h:133
@ DT_COLLECTION_PROP_PRINT_TIMESTAMP
Definition collection.h:124
#define DT_COLLECTION_ORDER_FLAG
Definition collection.h:103
#define COLLECTION_QUERY_FULL
Definition collection.h:63
dt_collection_change_t
Definition collection.h:147
@ DT_COLLECTION_CHANGE_RELOAD
Definition collection.h:151
@ DT_COLLECTION_CHANGE_NEW_QUERY
Definition collection.h:149
@ DT_COLLECTION_SORT_FILENAME
Definition collection.h:89
@ DT_COLLECTION_SORT_NONE
Definition collection.h:88
const float max
const dt_colormatrix_t dt_aligned_pixel_t out
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
int32_t dt_image_move(const int32_t imgid, const int32_t filmid)
const char * dt_image_film_roll_name(const char *path)
void dt_image_synch_xmp(const int selected)
const char * dt_metadata_get_name(const uint32_t keyid)
int type
int dt_metadata_get_type(const uint32_t keyid)
dt_metadata_t dt_metadata_get_keyid_by_display_order(const uint32_t order)
char * name
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
void dt_control_log(const char *msg,...)
Definition control.c:761
void dt_control_queue_redraw_center()
request redraw of center window. This redraws the center view within a gdk critical section to preven...
Definition control.c:861
gboolean dt_control_remove_images()
uint32_t view(const dt_view_t *self)
Definition darkroom.c:227
darktable_t darktable
Definition darktable.c:181
#define DT_MODULE(MODVER)
Definition darktable.h:140
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:463
#define dt_free(ptr)
Definition darktable.h:456
static const dt_aligned_pixel_simd_t value
Definition darktable.h:577
static gboolean dt_modifier_is(const GdkModifierType state, const GdkModifierType desired_modifier_mask)
Definition darktable.h:893
#define PATH_MAX
Definition darktable.h:1062
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:281
int store(dt_imageio_module_storage_t *self, dt_imageio_module_data_t *sdata, const int32_t imgid, dt_imageio_module_format_t *format, dt_imageio_module_data_t *fdata, const int num, const int total, const gboolean high_quality, const gboolean export_masks, dt_colorspaces_color_profile_type_t icc_type, const gchar *icc_filename, dt_iop_color_intent_t icc_intent, dt_export_metadata_t *metadata)
Definition disk.c:252
@ DND_TARGET_IMGID
static const GtkTargetEntry target_list_internal[]
static const guint n_targets_internal
void dtgtk_cairo_paint_sortby(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
@ CPF_DIRECTION_UP
Definition dtgtk/paint.h:61
@ CPF_DIRECTION_DOWN
Definition dtgtk/paint.h:62
void dt_film_init(dt_film_t *film)
Definition film.c:70
void dt_film_relocate(const char *old_path, const char *new_path)
Definition film.c:455
int dt_film_new(dt_film_t *film, const char *directory)
Definition film.c:161
int32_t dt_film_get_id(const char *folder)
Definition film.c:111
void dt_film_set_folder_status()
Definition film.c:514
void dt_film_cleanup(dt_film_t *film)
Definition film.c:80
GtkWidget * dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip)
Definition gtk.c:2259
GtkNotebook * dt_ui_notebook_new()
Definition gtk.c:2254
void dt_gui_refocus_center()
Definition gtk.c:3234
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
Definition gtk.c:2713
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnects accels when a text or search entry gets the focus, and reconnects them when it looses it....
Definition gtk.c:3225
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
@ DT_UI_RESIZE_STATIC
Definition gtk.h:264
#define DT_GUI_BOX_SPACING
Definition gtk.h:109
#define DT_GUI_MODULE(x)
const char * model
static const float x
const int t
const float v
dt_job_state_t dt_control_job_get_state(_dt_job_t *job)
Definition jobs.c:103
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:135
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:405
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:129
void dt_control_job_set_progress(dt_job_t *job, double value)
Definition jobs.c:626
void dt_control_job_add_progress(dt_job_t *job, const char *message, gboolean cancellable)
Definition jobs.c:612
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:112
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:55
@ DT_JOB_STATE_CANCELLED
Definition jobs.h:46
gboolean dt_handle_dialog_enter(GtkWidget *widget, GdkEventKey *event, gpointer data)
Definition lib.c:1551
void dt_lib_presets_add(const char *name, const char *plugin_name, const int32_t version, const void *params, const int32_t params_size, gboolean readonly)
Definition lib.c:1353
float *const restrict const size_t k
@ DT_METADATA_FLAG_HIDDEN
Definition metadata.h:74
@ DT_METADATA_NUMBER
Definition metadata.h:52
@ DT_METADATA_TYPE_INTERNAL
Definition metadata.h:60
@ DT_METADATA_SIGNAL_SHOWN
Definition metadata.h:66
@ DT_METADATA_SIGNAL_HIDDEN
Definition metadata.h:67
void dt_mipmap_get_cache_filename(char path[PATH_MAX], const dt_mipmap_cache_t *cache, dt_mipmap_size_t mip, const int32_t imgid)
void dt_mimap_cache_evict(dt_mipmap_cache_t *cache, const int32_t imgid)
size_t size
Definition mipmap_cache.c:3
#define dt_mipmap_cache_get(A, B, C, D, E, F)
@ DT_MIPMAP_BLOCKING
#define dt_mipmap_cache_release(A, B)
dt_mipmap_size_t
@ DT_MIPMAP_0
@ DT_MIPMAP_2
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:104
void dt_selection_select_list(struct dt_selection_t *selection, const GList *const l)
Definition selection.c:320
void dt_selection_clear(dt_selection_t *selection)
Definition selection.c:266
void dt_control_signal_unblock_by_func(const struct dt_control_signal_t *ctlsig, GCallback cb, gpointer user_data)
Definition signal.c:481
void dt_control_signal_block_by_func(const struct dt_control_signal_t *ctlsig, GCallback cb, gpointer user_data)
Definition signal.c:476
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:368
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:347
@ DT_SIGNAL_METADATA_CHANGED
This signal is raised when metadata status (shown/hidden) or value has changed.
Definition signal.h:139
@ DT_SIGNAL_IMAGES_ORDER_CHANGE
This signal is raised to request image order change.
Definition signal.h:150
@ DT_SIGNAL_FILMROLLS_CHANGED
This signal is raised when a filmroll is deleted/changed but not imported.
Definition signal.h:156
@ DT_SIGNAL_GEOTAG_CHANGED
This signal is raised when a geotag is added/deleted/changed
Definition signal.h:136
@ DT_SIGNAL_PREFERENCES_CHANGE
This signal is raised after preferences have been changed no parameters no return.
Definition signal.h:273
@ DT_SIGNAL_FILMROLLS_REMOVED
This signal is raised only when a filmroll is removed.
Definition signal.h:159
@ DT_SIGNAL_TAG_CHANGED
This signal is raised when a tag is added/deleted/changed
Definition signal.h:130
@ DT_SIGNAL_COLLECTION_CHANGED
This signal is raised when collection changed. To avoid leaking the list, dt_collection_t is connecte...
Definition signal.h:122
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:357
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
const float r
guint64 hi
Definition collect.c:926
guint64 lo
Definition collect.c:926
GHashTable * set
Definition collect.c:1131
dt_lib_collect_t * d
Definition collect.c:1130
gchar * start
Definition collect.c:253
gchar * stop
Definition collect.c:254
GtkTreePath * path1
Definition collect.c:255
GtkTreePath * path2
Definition collect.c:256
gboolean(* enabled)(int property, int n)
Definition collect.c:1997
gboolean multi
Definition collect.c:1996
void(* run)(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1998
const char * label
Definition collect.c:1995
dt_mipmap_size_t max_size
Definition collect.c:1920
gchar * path
Definition collect.c:1716
struct dt_gui_gtk_t * gui
Definition darktable.h:775
struct dt_collection_t * collection
Definition darktable.h:781
struct dt_mipmap_cache_t * mipmap_cache
Definition darktable.h:776
struct dt_selection_t * selection
Definition darktable.h:782
struct dt_control_signal_t * signals
Definition darktable.h:774
struct dt_bauhaus_t * bauhaus
Definition darktable.h:778
struct dt_view_manager_t * view_manager
Definition darktable.h:772
struct dt_control_t * control
Definition darktable.h:773
int32_t id
Definition film.h:45
int32_t reset
Definition gtk.h:172
dt_ui_t * ui
Definition gtk.h:164
dt_lib_collect_params_rule_t rule[10]
Definition collect.c:248
GtkWidget * hbox
Definition collect.c:189
GtkWidget * op_combo
Definition collect.c:191
GtkWidget * text
Definition collect.c:192
GtkWidget * combo
Definition collect.c:190
GtkWidget * button
Definition collect.c:193
GtkWidget * recursive_check
Definition collect.c:216
struct dt_lib_collect_params_t * params
Definition collect.c:230
GtkWidget * collections_controls
Definition collect.c:222
GtkTreeModel * treefilter
Definition collect.c:211
GtkWidget * sort_by
Definition collect.c:218
dt_lib_collect_rule_t rule[10]
Definition collect.c:202
GtkTreeModel * listfilter
Definition collect.c:212
GtkWidget * raw_box
Definition collect.c:226
GtkWidget * notebook
Definition collect.c:203
GtkWidget * no_uncategorized
Definition collect.c:223
GtkWidget * folder_levels
Definition collect.c:219
GtkWidget * raw_check
Definition collect.c:227
GUnixMountMonitor * vmonitor
Definition collect.c:234
GtkWidget * raw_entry
Definition collect.c:228
GtkWidget * sort_dir
Definition collect.c:217
GtkWidget * folders_controls
Definition collect.c:215
GtkTreeView * view
Definition collect.c:208
char plugin_name[128]
Definition lib.h:82
GModule *void * data
Definition lib.h:80
GtkWidget * widget
Definition lib.h:84
struct dt_view_manager_t::@67 proxy
struct dt_view_manager_t::@67::@69 module_collect
struct dt_lib_module_t *void(* update)(struct dt_lib_module_t *)
Definition view.h:238
char * collate_key
Definition collect.c:740
gboolean dt_tag_attach_images(const guint tagid, const GList *img, const gboolean undo_on)
Definition tags.c:463
void dt_tag_set_tag_order_by_id(const uint32_t tagid, const uint32_t sort, const gboolean descending)
Definition tags.c:1911
gboolean dt_tag_get_tag_order_by_id(const uint32_t tagid, uint32_t *sort, gboolean *descending)
Definition tags.c:1863
void dt_tag_rename(const guint tagid, const gchar *new_tagname)
Definition tags.c:340
guint dt_tag_remove(const guint tagid, gboolean final)
Definition tags.c:236
uint32_t dt_tag_get_tag_id_by_name(const char *const name)
Definition tags.c:1891
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
void dtgtk_togglebutton_set_paint(GtkDarktableToggleButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
GtkWidget * dtgtk_togglebutton_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
#define DTGTK_TOGGLEBUTTON(obj)
GList * dt_util_str_to_glist(const gchar *separator, const gchar *text)
Definition utility.c:830
gboolean dt_util_test_image_file(const char *filename)
Definition utility.c:318
gchar * dt_util_dstrcat(gchar *str, const gchar *format,...)
Definition utility.c:95
@ DT_UI_CONTAINER_PANEL_LEFT_CENTER