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 // GTK delivers the button-release that ends a drag-and-drop onto a row as a normal click
211 // too, which row-activated (activate-on-single-click) would otherwise turn into a new/replaced
212 // collection filter rule on top of the tag/folder just dropped on (#905) -- ignore it while set.
214
215 GtkTreeModel *treefilter;
216 GtkTreeModel *listfilter;
217
218 // Folders-tab inline controls
219 GtkWidget *folders_controls; // hbox holding the widgets below
220 GtkWidget *recursive_check; // "include sub-folders" -> recursive<N> conf (see collect.c doc)
221 GtkWidget *sort_dir; // ascending/descending toggle
222 GtkWidget *sort_by; // film-roll sort key (id / folder name)
223 GtkWidget *folder_levels; // show_folder_levels: levels shown in film-roll names (List only)
224
225 // Rule 0's last folder/film-roll selection (item + string + recursive), remembered across
226 // visits to other tabs so _configure_tab() can restore it when returning to Folders -- unless
227 // a real commit happened for a different property in the meantime (see _commit_colllection()).
232
233 // Collections-tab inline controls
234 GtkWidget *collections_controls; // hbox holding the widget below
235 GtkWidget *no_uncategorized; // "no 'uncategorized' group" for childless tags
236
237 // Queries-tab raw SQL escape
241
243#ifdef _WIN32
244 GVolumeMonitor *vmonitor;
245#else
246 GUnixMountMonitor *vmonitor;
247#endif
249
251{
252 uint32_t item : 16;
253 uint32_t mode : 16;
254 char string[PARAM_STRING_SIZE];
256
262
263typedef struct _range_t
264{
265 gchar *start;
266 gchar *stop;
267 GtkTreePath *path1;
268 GtkTreePath *path2;
270
271// ---- forward declarations ----
273static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr);
274static void combo_changed(GtkWidget *combo, dt_lib_collect_rule_t *dr);
275static void collection_updated(gpointer instance, dt_collection_change_t query_change,
276 dt_collection_properties_t changed_property, gpointer imgs, int next,
277 gpointer self);
278static void tag_changed(gpointer instance, gpointer self);
279static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d);
280static void update_view(dt_lib_collect_rule_t *dr);
281static void _populate_collect_combo(GtkWidget *w);
282static int _combo_get_active_collection(GtkWidget *combo);
283static gboolean _combo_set_active_collection(GtkWidget *combo, const int property);
284static void _op_changed(GtkWidget *w, dt_lib_collect_rule_t *dr);
286static dt_lib_module_t *_self();
287
288// =====================================================================================
289// Section 0 — property predicates
290// =====================================================================================
291
292static int is_time_property(int property)
293{
294 return property == DT_COLLECTION_PROP_TIME || property == DT_COLLECTION_PROP_IMPORT_TIMESTAMP
297}
298
299static gboolean item_is_folder(int item)
300{
302}
303
304static gboolean item_is_tag(int item)
305{
306 return item == DT_COLLECTION_PROP_TAG;
307}
308
309static gboolean item_is_numeric(int item)
310{
314}
315
316// A property displayed as a hierarchical tree (vs a flat list).
317static gboolean item_is_tree(int item)
318{
319 return item == DT_COLLECTION_PROP_FOLDERS || item == DT_COLLECTION_PROP_TAG
321}
322
323// Comparison operators offered by the operator combo for numeric/date/rating properties.
324// OP_TOKENS is the prefix written into the rule string; OP_LABELS is what the user sees.
325static const char *const OP_TOKENS[] = { "", "<", "<=", ">", ">=", "<>" };
326static const char *const OP_LABELS[] = { "=", "<", "≤", ">", "≥", "≠" };
327#define COLLECT_N_OPS ((int)G_N_ELEMENTS(OP_TOKENS))
328
329// Split a rule string into a leading operator index (into OP_TOKENS) and the remaining value.
330static void _split_operator(const char *text, int *op_idx, const char **value)
331{
332 *op_idx = 0;
333 *value = text ? text : "";
334 if(IS_NULL_PTR(text) || text[0] == '[') return; // a [a;b] range carries no leading operator
335 if(g_str_has_prefix(text, "<="))
336 {
337 *op_idx = 2;
338 *value = text + 2;
339 }
340 else if(g_str_has_prefix(text, ">="))
341 {
342 *op_idx = 4;
343 *value = text + 2;
344 }
345 else if(g_str_has_prefix(text, "<>"))
346 {
347 *op_idx = 5;
348 *value = text + 2;
349 }
350 else if(g_str_has_prefix(text, "<"))
351 {
352 *op_idx = 1;
353 *value = text + 1;
354 }
355 else if(g_str_has_prefix(text, ">"))
356 {
357 *op_idx = 3;
358 *value = text + 1;
359 }
360 else if(g_str_has_prefix(text, "="))
361 {
362 *op_idx = 0;
363 *value = text + 1;
364 }
365 while(**value == ' ') (*value)++;
366}
367
368// =====================================================================================
369// Section 1 — module identity & presets (conf-only; unchanged contract)
370// =====================================================================================
371
372const char *name(struct dt_lib_module_t *self)
373{
374 return _("Library");
375}
376
377const char **views(dt_lib_module_t *self)
378{
379 static const char *v[] = { "lighttable", "map", "print", NULL };
380 return v;
381}
382
384{
386}
387
389{
390 return 400;
391}
392
393void *legacy_params(struct dt_lib_module_t *self, const void *const old_params, const size_t old_params_size,
394 const int old_version, int *new_version, size_t *new_size)
395{
396 if(old_version == 1 || old_version == 2)
397 {
398 // v1->v2 and v2->v3 only reordered/extended the property enum; presets store the property
399 // index. Rather than carry the historical remap tables, drop incompatible presets.
400 return NULL;
401 }
402 return NULL;
403}
404
406{
408
409#define CLEAR_PARAMS(r) \
410 { \
411 memset(&params, 0, sizeof(params)); \
412 params.rules = 1; \
413 params.rule[0].mode = 0; \
414 params.rule[0].item = r; \
415 }
416
417 GDateTime *now = g_date_time_new_now_local();
418 char *datetime_today = g_date_time_format(now, "%Y:%m:%d");
419 GDateTime *gdt = g_date_time_add_days(now, -1);
420 char *datetime_24hrs = g_date_time_format(gdt, "> %Y:%m:%d %H:%M");
421 g_date_time_unref(gdt);
422 gdt = g_date_time_add_days(now, -30);
423 char *datetime_30d = g_date_time_format(gdt, "> %Y:%m:%d");
424 g_date_time_unref(gdt);
425 g_date_time_unref(now);
426
428 g_strlcpy(params.rule[0].string, datetime_today, PARAM_STRING_SIZE);
429 dt_lib_presets_add(_("imported: today"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
430
432 g_strlcpy(params.rule[0].string, datetime_24hrs, PARAM_STRING_SIZE);
433 dt_lib_presets_add(_("imported: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
434
436 g_strlcpy(params.rule[0].string, datetime_30d, PARAM_STRING_SIZE);
437 dt_lib_presets_add(_("imported: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params),
438 TRUE);
439
441 g_strlcpy(params.rule[0].string, datetime_today, PARAM_STRING_SIZE);
442 dt_lib_presets_add(_("taken: today"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
443
445 g_strlcpy(params.rule[0].string, datetime_24hrs, PARAM_STRING_SIZE);
446 dt_lib_presets_add(_("taken: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
447
449 g_strlcpy(params.rule[0].string, datetime_30d, PARAM_STRING_SIZE);
450 dt_lib_presets_add(_("taken: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params), TRUE);
451
452 dt_free(datetime_today);
453 dt_free(datetime_24hrs);
454 dt_free(datetime_30d);
455#undef CLEAR_PARAMS
456}
457
458// =====================================================================================
459// Section 2 — conf layer (the single source of truth read by the SQL engine)
460// =====================================================================================
461
462static int _rules_count()
463{
464 return CLAMP(dt_conf_get_int("plugins/lighttable/collect/num_rules"), 1, MAX_RULES);
465}
466
467static void _rules_set_count(int n)
468{
469 dt_conf_set_int("plugins/lighttable/collect/num_rules", CLAMP(n, 1, MAX_RULES));
470}
471
472static int _rule_get_item(int n)
473{
474 char k[64];
475 snprintf(k, sizeof(k), "plugins/lighttable/collect/item%1d", n);
476 return dt_conf_get_int(k);
477}
478
479static void _rule_set_item(int n, int item)
480{
481 char k[64];
482 snprintf(k, sizeof(k), "plugins/lighttable/collect/item%1d", n);
483 dt_conf_set_int(k, item);
484}
485
486static int _rule_get_mode(int n)
487{
488 char k[64];
489 snprintf(k, sizeof(k), "plugins/lighttable/collect/mode%1d", n);
490 return dt_conf_get_int(k);
491}
492
493static void _rule_set_mode(int n, int mode)
494{
495 char k[64];
496 snprintf(k, sizeof(k), "plugins/lighttable/collect/mode%1d", n);
497 dt_conf_set_int(k, mode);
498}
499
500static gboolean _rule_get_recursive(int n)
501{
502 char k[64];
503 snprintf(k, sizeof(k), "plugins/lighttable/collect/recursive%1d", n);
504 return dt_conf_get_bool(k);
505}
506
507static void _rule_set_recursive(int n, gboolean recursive)
508{
509 char k[64];
510 snprintf(k, sizeof(k), "plugins/lighttable/collect/recursive%1d", n);
511 dt_conf_set_bool(k, recursive);
512}
513
514static gchar *_rule_get_string(int n)
515{
516 char k[64];
517 snprintf(k, sizeof(k), "plugins/lighttable/collect/string%1d", n);
518 gchar *s = dt_conf_get_string(k);
519 // Backward compat: pre-recursive-flag conf/presets encoded "include sub-folders" as a trailing
520 // '*' on the string itself. Migrate it into the dedicated flag the first time this rule is
521 // read after upgrading; every read after this one sees a clean string.
522 if(s && _rule_get_item(n) == DT_COLLECTION_PROP_FOLDERS && g_str_has_suffix(s, "*"))
523 {
524 s[strlen(s) - 1] = '\0';
527 }
528 return s;
529}
530
531static void _rule_set_string(int n, const char *s)
532{
533 char k[64];
534 snprintf(k, sizeof(k), "plugins/lighttable/collect/string%1d", n);
535 dt_conf_set_string(k, s ? s : "");
536}
537
538// Push the GUI state of one rule (combo property + operator + entry text) into conf. For
539// numeric/date/rating properties the chosen operator is prepended to the value.
541{
542 const int property = _combo_get_active_collection(dr->combo);
543 const char *val = gtk_entry_get_text(GTK_ENTRY(dr->text));
544 gchar *s;
545 if(item_is_numeric(property) && val[0] != '[') // a [a;b] range carries its own operator
546 {
547 const int idx = CLAMP(gtk_combo_box_get_active(GTK_COMBO_BOX(dr->op_combo)), 0, COLLECT_N_OPS - 1);
548 s = g_strconcat(OP_TOKENS[idx], val, NULL);
549 }
550 else
551 s = g_strdup(val);
552 _rule_set_string(dr->num, s);
553 dt_free(s);
554 _rule_set_item(dr->num, property);
555
556 // Only the Folders tab's own rule actually owns recursive_check -- the Queries tab reuses the
557 // same rule slots and can have a FOLDERS rule with the checkbox hidden; writing its state there
558 // would stamp an unrelated rule with the Folders tab's leftover checkbox value.
560 const gboolean folders_visible = d->folders_controls && gtk_widget_get_visible(d->folders_controls);
561 if(property == DT_COLLECTION_PROP_FOLDERS && folders_visible)
562 _rule_set_recursive(dr->num, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->recursive_check)));
563}
564
565// Pull the conf state of one rule back into the GUI (without firing the change handlers).
567{
569 const int property = _combo_get_active_collection(dr->combo);
570 gchar *text = _rule_get_string(dr->num);
571 if(text)
572 {
573 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
574 g_signal_handlers_block_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
575
576 if(item_is_numeric(property))
577 {
578 int idx;
579 const char *val;
580 _split_operator(text, &idx, &val);
581 gtk_combo_box_set_active(GTK_COMBO_BOX(dr->op_combo), idx);
582 gtk_entry_set_text(GTK_ENTRY(dr->text), val);
583 }
584 else
585 {
586 gtk_entry_set_text(GTK_ENTRY(dr->text), text);
587 }
588
589 gtk_editable_set_position(GTK_EDITABLE(dr->text), -1);
590 dr->typing = FALSE;
591
592 g_signal_handlers_unblock_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
593 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
594 dt_free(text);
595 }
596}
597
598// Rebuild the collection query from the conf rules and refresh the lighttable.
599// Note: _commit() would conflict with msys64/ucrt64/include/io.h namespace on Windows.
601{
602 // Every real, user-triggered change to what's being filtered goes through here (mere tab
603 // switching does not, see the NB in _on_tab_switch) -- so this is the one place that can tell
604 // "the Folders tab's selection is still the active filter" from "something else superseded
605 // it", regardless of which handler/tab actually triggered the commit.
608 {
609 dt_free(d->last_folders_string);
610 d->last_folders_item = _rule_get_item(0);
611 d->last_folders_string = _rule_get_string(0);
612 d->last_folders_recursive = _rule_get_recursive(0);
613 d->has_last_folders = TRUE;
614 }
615 else
616 d->has_last_folders = FALSE; // a real filter change elsewhere supersedes the remembered folder
617
619}
620
621// Like _commit_colllection() but without bouncing back into our own collection_updated() handler.
630
632{
633 return (dt_lib_collect_t *)r->lib_collect;
634}
635
637{
638 return d->rule + d->active_rule;
639}
640
642{
643 d->nb_rules = _rules_count();
644 d->active_rule = CLAMP(d->active_rule, 0, d->nb_rules - 1);
645}
646
647// get_params/set_params/gui_reset rely on the conf layer above, so they stay tiny.
649{
650 dt_lib_collect_params_t *p = d->params;
651 memset(p, 0, sizeof(dt_lib_collect_params_t));
652 const int n = _rules_count();
653 for(int i = 0; i < n; i++)
654 {
655 p->rule[i].item = _rule_get_item(i);
656 p->rule[i].mode = _rule_get_mode(i);
657 gchar *s = _rule_get_string(i); // may lazily migrate a legacy '*' + set recursive(i) as a side effect
658 // Presets fold the recursive flag back into the trailing '*' this format has always used
659 // (same trick as dt_collection_serialize/deserialize) -- no separate field, no version bump.
660 if(p->rule[i].item == DT_COLLECTION_PROP_FOLDERS && _rule_get_recursive(i) && s && s[0] != '\0'
661 && !g_str_has_suffix(s, "*") && !g_str_has_suffix(s, "%"))
662 {
663 gchar *s2 = g_strconcat(s, "*", NULL);
664 dt_free(s);
665 s = s2;
666 }
667 if(s) g_strlcpy(p->rule[i].string, s, PARAM_STRING_SIZE);
668 dt_free(s);
669 }
670 p->rules = n;
671}
672
674{
676 *size = sizeof(dt_lib_collect_params_t);
677 void *p = malloc(*size);
678 memcpy(p, ((dt_lib_collect_t *)self->data)->params, *size);
679 return p;
680}
681
682int set_params(dt_lib_module_t *self, const void *params, int size)
683{
685 for(uint32_t i = 0; i < p->rules; i++)
686 {
687 _rule_set_item(i, p->rule[i].item);
688 _rule_set_mode(i, p->rule[i].mode);
689 _rule_set_string(i, p->rule[i].string);
690 }
691 _rules_set_count(p->rules);
692 // _rule_get_string()'s lazy migration (triggered below) reads any trailing '*' back into
693 // recursive<N> -- no explicit _rule_set_recursive() needed here.
697 return 0;
698}
699
713
714// =====================================================================================
715// Section 3 — property combo
716// =====================================================================================
717
719{
720 return GPOINTER_TO_UINT(dt_bauhaus_combobox_get_data(combo)) - 1;
721}
722
723static gboolean _combo_set_active_collection(GtkWidget *combo, const int property)
724{
725 const gboolean found = dt_bauhaus_combobox_set_from_value(combo, property + 1);
727 return found;
728}
729
731{
732#define ADD_COLLECT_ENTRY(value) \
733 dt_bauhaus_combobox_add_full(w, dt_collection_name(value), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, \
734 GUINT_TO_POINTER(value + 1), NULL, TRUE)
735
739
741 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
742 {
743 const uint32_t keyid = dt_metadata_get_keyid_by_display_order(i);
744 const gchar *name_ = dt_metadata_get_name(keyid);
745 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name_);
746 const gboolean hidden = dt_conf_get_int(setting) & DT_METADATA_FLAG_HIDDEN;
747 dt_free(setting);
748 const int meta_type = dt_metadata_get_type(keyid);
750 }
754
761
768
774#undef ADD_COLLECT_ENTRY
775}
776
777// =====================================================================================
778// Section 4 — model population (flat list + hierarchical tree) and filtering
779// =====================================================================================
780
781static int string_array_length(char **list)
782{
783 int length = 0;
784 for(; *list; list++) length++;
785 return length;
786}
787
788// NULL-terminated array of path components (drops the leading empty root component on POSIX).
789static char **split_path(const char *path)
790{
791 if(IS_NULL_PTR(path) || !*path) return NULL;
792
793 char **result;
794 char **tokens = g_strsplit(path, G_DIR_SEPARATOR_S, -1);
795
796#ifdef _WIN32
797 if(!(g_ascii_isalpha(tokens[0][0]) && tokens[0][strlen(tokens[0]) - 1] == ':'))
798 {
799 g_strfreev(tokens);
800 tokens = NULL;
801 }
802 result = tokens;
803#else
804 const unsigned int size = g_strv_length(tokens);
805 result = malloc(sizeof(char *) * size);
806 for(unsigned int i = 0; i < size; i++) result[i] = tokens[i + 1];
807 dt_free(tokens[0]);
808 dt_free(tokens);
809#endif
810 return result;
811}
812
818
819static void free_tuple(gpointer data)
820{
821 name_key_tuple_t *tuple = (name_key_tuple_t *)data;
822 dt_free(tuple->name);
823 dt_free(tuple->collate_key);
824 dt_free(tuple);
825}
826
827static gint sort_folder_tag(gconstpointer a, gconstpointer b)
828{
829 const name_key_tuple_t *ta = (const name_key_tuple_t *)a;
830 const name_key_tuple_t *tb = (const name_key_tuple_t *)b;
831 return g_strcmp0(ta->collate_key, tb->collate_key);
832}
833
834// Sort key so that "not tagged" & "darktable|" come first, sub-tags directly behind their parent.
835// Tag names are displayed with their raw case, so the key is folded here to keep sorting
836// case-insensitive/alphabetical instead of splitting on ASCII case (all-uppercase first).
837static char *tag_collate_key(char *tag)
838{
839 gchar *folded = g_utf8_casefold(tag, -1);
840 const size_t len = strlen(folded);
841 char *result = g_malloc(len + 2);
842 if(!g_strcmp0(tag, _("not tagged")))
843 *result = '\1';
844 else if(g_str_has_prefix(tag, "darktable|"))
845 *result = '\2';
846 else
847 *result = '\3';
848 memcpy(result + 1, folded, len + 1);
849 for(char *iter = result + 1; *iter; iter++)
850 if(*iter == '|') *iter = '\1';
851 g_free(folded);
852 return result;
853}
854
855static void tree_count_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
856 GtkTreeIter *iter, gpointer data)
857{
858 gchar *name;
859 guint count;
860 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_TEXT, &name, DT_LIB_COLLECT_COL_COUNT, &count, -1);
861 if(!count)
862 g_object_set(renderer, "text", name, NULL);
863 else
864 {
865 gchar *coltext = g_strdup_printf("%s (%d)", name, count);
866 g_object_set(renderer, "text", coltext, NULL);
867 dt_free(coltext);
868 }
869 dt_free(name);
870}
871
872// ---- search filtering (list) ----
873static gboolean list_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
874{
876 gchar *str = NULL;
877 gboolean visible = FALSE;
878 gboolean was_visible;
879 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &was_visible, -1);
880
881 gchar *haystack = g_utf8_strdown(str, -1);
882 const gchar *needle = dr->searchstring;
883 const int property = _combo_get_active_collection(dr->combo);
884
886 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_RATING)
887 {
888 visible = TRUE;
889 gchar *operator, * number, *number2;
890 dt_collection_split_operator_number(needle, &number, &number2, &operator);
891 if(number)
892 {
893 const float nb1 = g_strtod(number, NULL);
894 const float nb2 = g_strtod(haystack, NULL);
895 if(operator&& strcmp(operator, ">") == 0)
896 visible = (nb2 > nb1);
897 else if(operator&& strcmp(operator, ">=") == 0)
898 visible = (nb2 >= nb1);
899 else if(operator&& strcmp(operator, "<") == 0)
900 visible = (nb2 < nb1);
901 else if(operator&& strcmp(operator, "<=") == 0)
902 visible = (nb2 <= nb1);
903 else if(operator&& strcmp(operator, "<>") == 0)
904 visible = (nb1 != nb2);
905 else if(operator&& number2 && strcmp(operator, "[]") == 0)
906 {
907 const float nb3 = g_strtod(number2, NULL);
908 visible = (nb2 >= nb1 && nb2 <= nb3);
909 }
910 else
911 visible = (nb1 == nb2);
912 }
913 dt_free(operator);
914 dt_free(number);
915 dt_free(number2);
916 }
917 else if(property == DT_COLLECTION_PROP_FILENAME && strchr(needle, ',') != NULL)
918 {
919 GList *list = dt_util_str_to_glist(",", needle);
920 for(const GList *l = list; l; l = g_list_next(l))
921 {
922 const char *name = (char *)l->data;
923 if((visible = (g_strrstr(haystack, name + (name[0] == '%')) != NULL))) break;
924 }
925 g_list_free_full(list, dt_free_gpointer);
926 }
927 else
928 {
929 if(needle[0] == '%') needle++;
930 if(!needle[0])
931 visible = TRUE;
932 else if(!needle[1])
933 visible = (strchr(haystack, needle[0]) != NULL);
934 else
935 visible = (g_strrstr(haystack, needle) != NULL);
936 }
937
938 dt_free(haystack);
939 dt_free(str);
940 if(visible != was_visible)
941 gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
942 return FALSE;
943}
944
945// Strip the trailing recursion/wildcard marker(s) so a search string compares against a plain
946// node path/name, not a literal `*`/`%`/`|`/`:` that no real node ever contains verbatim. Shared
947// by every tree-shaped property matcher (tree_match_string, tree_expand) so they agree on what
948// "this node matches the current search text" means.
949static void _strip_recursion_marker(gchar *needle, gchar *haystack, int property)
950{
951 if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0';
952 if(g_str_has_suffix(haystack, "%")) haystack[strlen(haystack) - 1] = '\0';
953 if(property == DT_COLLECTION_PROP_TAG || property == DT_COLLECTION_PROP_GEOTAGGING)
954 {
955 if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0'; // hierarchy + sub
956 if(g_str_has_suffix(needle, "|")) needle[strlen(needle) - 1] = '\0';
957 if(g_str_has_suffix(haystack, "|")) haystack[strlen(haystack) - 1] = '\0';
958 }
959 else if(property == DT_COLLECTION_PROP_FOLDERS)
960 {
961 if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0';
962 if(g_str_has_suffix(needle, "/")) needle[strlen(needle) - 1] = '\0';
963 if(g_str_has_suffix(haystack, "/")) haystack[strlen(haystack) - 1] = '\0';
964 }
965 else if(DT_COLLECTION_PROP_DAY == property || is_time_property(property))
966 {
967 if(g_str_has_suffix(needle, ":")) needle[strlen(needle) - 1] = '\0';
968 if(g_str_has_suffix(haystack, ":")) haystack[strlen(haystack) - 1] = '\0';
969 }
970}
971
972// ---- search filtering (tree) ----
973static gboolean tree_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
974{
976 gchar *str = NULL;
977 gboolean cur_state, visible;
978 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &cur_state, -1);
979
980 if(dr->typing == FALSE && !cur_state)
981 visible = TRUE;
982 else
983 {
984 gchar *haystack = g_utf8_strdown(str, -1),
985 *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
987 visible = (g_strrstr(haystack, needle) != NULL);
988 dt_free(haystack);
989 dt_free(needle);
990 }
991 dt_free(str);
992 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
993 return FALSE;
994}
995
996static gboolean tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
997{
998 gboolean state;
999 GtkTreeIter parent, child = *iter;
1000 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_VISIBLE, &state, -1);
1001 if(!state) return FALSE;
1002 while(gtk_tree_model_iter_parent(model, &parent, &child))
1003 {
1004 gtk_tree_store_set(GTK_TREE_STORE(model), &parent, DT_LIB_COLLECT_COL_VISIBLE, TRUE, -1);
1005 child = parent;
1006 }
1007 return FALSE;
1008}
1009
1010static void tree_set_visibility(GtkTreeModel *model, gpointer data)
1011{
1012 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_match_string, data);
1013 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_reveal_func, NULL);
1014}
1015
1016// Turn a partial date string ("2021", "2021:06:15", "2021:06:15 13:00", ...) into a comparable
1017// 14-digit YYYYMMDDHHMMSS number, keeping only digits (separator-agnostic) and padding the
1018// unspecified low-order part with `pad`. Pad '0' yields the earliest instant of the prefix,
1019// pad '9' an upper bound past its latest instant.
1020static guint64 _date_key(const char *s, char pad)
1021{
1022 char digits[15];
1023 int n = 0;
1024 for(const char *p = s; p && *p && n < 14; p++)
1025 if(g_ascii_isdigit(*p)) digits[n++] = *p;
1026 while(n < 14) digits[n++] = pad;
1027 digits[14] = '\0';
1028 return g_ascii_strtoull(digits, NULL, 10);
1029}
1030
1031typedef struct _date_range_t
1032{
1033 guint64 lo, hi; // inclusive bounds as _date_key() numbers
1035
1036// Reduce a date tree to the nodes whose date prefix overlaps [lo;hi]. A node's prefix spans
1037// [node_lo;node_hi]; it overlaps the range iff node_hi >= lo AND node_lo <= hi. Pair this with
1038// tree_reveal_func() so the visible leaves' ancestors stay visible too.
1039static gboolean tree_range_visible(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1040{
1041 const _date_range_t *r = (const _date_range_t *)data;
1042 gchar *str = NULL;
1043 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
1044 const guint64 node_lo = _date_key(str, '0');
1045 const guint64 node_hi = _date_key(str, '9');
1046 dt_free(str);
1047 const gboolean visible = (node_hi >= r->lo) && (node_lo <= r->hi);
1048 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1);
1049 return FALSE;
1050}
1051
1052static gboolean list_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1053{
1056 gchar *str = NULL;
1057 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
1058
1059 gchar *haystack = g_utf8_strdown(str, -1);
1060 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
1061 if(strcmp(haystack, needle) == 0)
1062 {
1063 gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), path);
1064 gtk_tree_view_scroll_to_cell(d->view, path, NULL, FALSE, 0.2, 0);
1065 }
1066 dt_free(haystack);
1067 dt_free(needle);
1068 dt_free(str);
1069 return FALSE;
1070}
1071
1072static gboolean range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1073{
1074 _range_t *range = (_range_t *)data;
1075 gchar *str = NULL;
1076 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1);
1077
1078 gchar *haystack = g_utf8_strdown(str, -1);
1079 gchar *needle = range->path1 ? g_utf8_strdown(range->stop, -1) : g_utf8_strdown(range->start, -1);
1080 if(strcmp(haystack, needle) == 0)
1081 {
1082 if(range->path1)
1083 {
1084 range->path2 = gtk_tree_path_copy(path);
1085 dt_free(haystack);
1086 dt_free(needle);
1087 dt_free(str);
1088 return TRUE;
1089 }
1090 else
1091 range->path1 = gtk_tree_path_copy(path);
1092 }
1093 dt_free(haystack);
1094 dt_free(needle);
1095 dt_free(str);
1096 return FALSE;
1097}
1098
1099static gboolean tree_expand(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1100{
1103 gchar *str = NULL, *txt = NULL;
1104 gboolean startwildcard = FALSE, expanded = FALSE;
1105 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_TEXT, &txt, -1);
1106
1107 gchar *haystack = g_utf8_strdown(str, -1);
1108 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
1109 gchar *txt2 = g_utf8_strdown(txt, -1);
1110 const int property = _combo_get_active_collection(dr->combo);
1111
1112 // While typing, or right after a view/tab switch that preserved a query, we want to actively
1113 // reveal the matching node(s); on a plain refresh we leave the tree where the user left it.
1114 const gboolean reveal = dr->typing || dr->reveal;
1115
1116 if(g_str_has_prefix(needle, "%")) startwildcard = TRUE;
1117 _strip_recursion_marker(needle, haystack, property);
1118
1119 if(reveal && g_strrstr(txt2, needle) != NULL)
1120 {
1121 gtk_tree_view_expand_to_path(d->view, path);
1122 expanded = TRUE;
1123 }
1124
1125 if(strlen(needle) == 0)
1126 {
1127 // keep collapsed
1128 }
1129 else if(strcmp(haystack, needle) == 0)
1130 {
1131 gtk_tree_view_expand_to_path(d->view, path);
1132 gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), path);
1133 gtk_tree_view_scroll_to_cell(d->view, path, NULL, FALSE, 0.2, 0);
1134 expanded = TRUE;
1135 }
1136 else if(startwildcard && g_strrstr(haystack, needle + 1) != NULL)
1137 {
1138 gtk_tree_view_expand_to_path(d->view, path);
1139 expanded = TRUE;
1140 }
1141 else if((reveal || property != DT_COLLECTION_PROP_FOLDERS) && g_str_has_prefix(haystack, needle))
1142 {
1143 gtk_tree_view_expand_to_path(d->view, path);
1144 expanded = TRUE;
1145 }
1146
1147 dt_free(haystack);
1148 dt_free(needle);
1149 dt_free(txt2);
1150 dt_free(str);
1151 dt_free(txt);
1152 return expanded;
1153}
1154
1155// Walk down a folder tree through single-child nodes and return the path of the deepest such node
1156// (the unique common root), so the filtered model can hide the redundant leading folders. Returns
1157// NULL when there is nothing to collapse. Stops descending at a node that is itself a film-roll.
1158static GtkTreePath *_folders_root_collapse_path(GtkTreeModel *model)
1159{
1160 GtkTreeIter child, iter;
1161 int level = 0;
1162 while(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) > 0)
1163 {
1164 if(level > 0)
1165 {
1166 gchar *pth = NULL;
1167 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &pth, -1);
1168 const int id = dt_film_get_id(pth); // is this folder a known film-roll?
1169 dt_free(pth);
1170 if(id != -1)
1171 {
1172 if(!gtk_tree_model_iter_parent(model, &child, &iter)) level = 0;
1173 iter = child;
1174 break;
1175 }
1176 }
1177 if(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) != 1) break;
1178 gtk_tree_model_iter_children(model, &child, level > 0 ? &iter : NULL);
1179 iter = child;
1180 level++;
1181 }
1182 if(level <= 0) return NULL;
1183
1184 if(gtk_tree_model_iter_n_children(model, &iter) == 0 && gtk_tree_model_iter_parent(model, &child, &iter))
1185 return gtk_tree_model_get_path(model, &child);
1186 return gtk_tree_model_get_path(model, &iter);
1187}
1188
1189// Build the filtered model; for folders, collapse a unique common root into the virtual root.
1190static GtkTreeModel *_create_filtered_model(GtkTreeModel *model, dt_lib_collect_rule_t *dr)
1191{
1194 : NULL;
1195 GtkTreeModel *filter = gtk_tree_model_filter_new(model, path);
1196 gtk_tree_path_free(path);
1197 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(filter), DT_LIB_COLLECT_COL_VISIBLE);
1198 return filter;
1199}
1200
1201static const char *UNCATEGORIZED_TAG = N_("uncategorized");
1202
1203// --- preserve tree expansion across a rebuild so the user keeps their place ---
1204static void _collect_expanded_cb(GtkTreeView *view, GtkTreePath *path, gpointer data)
1205{
1206 GHashTable *set = (GHashTable *)data;
1207 GtkTreeModel *model = gtk_tree_view_get_model(view);
1208 GtkTreeIter it;
1209 if(gtk_tree_model_get_iter(model, &it, path))
1210 {
1211 gchar *p = NULL;
1212 gtk_tree_model_get(model, &it, DT_LIB_COLLECT_COL_PATH, &p, -1);
1213 if(p) g_hash_table_add(set, p); // hash table takes ownership of p
1214 }
1215}
1216
1222
1223static gboolean _restore_expanded_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1224{
1225 _expand_ctx_t *c = (_expand_ctx_t *)data;
1226 gchar *p = NULL;
1227 gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &p, -1);
1228 if(p && g_hash_table_contains(c->set, p)) gtk_tree_view_expand_to_path(c->d->view, path);
1229 dt_free(p);
1230 return FALSE;
1231}
1232
1233// Split a tree value into its path components for the active property.
1234static char **_split_tree_name(int property, const char *name)
1235{
1236 if(property == DT_COLLECTION_PROP_FOLDERS) return split_path(name);
1237 if(property == DT_COLLECTION_PROP_DAY) return g_strsplit(name, ":", -1);
1238 if(is_time_property(property)) return g_strsplit_set(name, ": ", 4);
1239 return g_strsplit(name, "|", -1);
1240}
1241
1242// Add `count` to every ancestor of `leaf` so parent folders/dates show the total beneath them
1243// (fixes #537 for the displayed count).
1244static void _propagate_count_to_ancestors(GtkTreeStore *store, GtkTreeIter *leaf, int count)
1245{
1246 GtkTreeModel *model = GTK_TREE_MODEL(store);
1247 GtkTreeIter parent, child = *leaf;
1248 while(gtk_tree_model_iter_parent(model, &parent, &child))
1249 {
1250 guint parentcount;
1251 gtk_tree_model_get(model, &parent, DT_LIB_COLLECT_COL_COUNT, &parentcount, -1);
1252 gtk_tree_store_set(store, &parent, DT_LIB_COLLECT_COL_COUNT, count + parentcount, -1);
1253 child = parent;
1254 }
1255}
1256
1257// A top-level tag with no children of its own is filed under a synthetic "uncategorized" node
1258// (created lazily). Returns TRUE when `name` was filed this way, so the caller skips it.
1259static gboolean _maybe_file_uncategorized(GtkTreeStore *store, const char *name, const char *next_name_raw,
1260 GtkTreeIter *uncategorized, guint *index, int count)
1261{
1262 if(strchr(name, '|') != NULL) return FALSE; // has a hierarchy of its own
1263
1264 char *next_name = g_strdup(next_name_raw ? next_name_raw : "");
1265 if(strlen(next_name) >= strlen(name) + 1 && next_name[strlen(name)] == '|') next_name[strlen(name)] = '\0';
1266 const gboolean leaf_toplevel = g_strcmp0(next_name, name) && g_strcmp0(name, _("not tagged"));
1267 dt_free(next_name);
1268 if(!leaf_toplevel) return FALSE;
1269
1270 if(!uncategorized->stamp)
1271 {
1272 gtk_tree_store_insert_with_values(store, uncategorized, NULL, -1, DT_LIB_COLLECT_COL_TEXT,
1275 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1276 (*index)++;
1277 }
1278 GtkTreeIter temp;
1279 gtk_tree_store_insert_with_values(store, &temp, uncategorized, 0, DT_LIB_COLLECT_COL_TEXT, name,
1282 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1283 (*index)++;
1284 return TRUE;
1285}
1286
1287// Pull the raw values from the SQL engine and sort them ourselves: sqlite knows nothing about path
1288// separators, so we order by a path-aware collate key and build the tree by hand. Caller frees with
1289// g_list_free_full(list, free_tuple).
1290static GList *_collect_sorted_tree_names(int property, int rule)
1291{
1292 GList *sorted_names = NULL;
1293 GList *values = dt_collection_get_property_values(property, rule);
1294 for(GList *v = values; v; v = g_list_next(v))
1295 {
1297 char *name = g_strdup(nv->name ? nv->name : "");
1298 gchar *collate_key;
1299 if(property == DT_COLLECTION_PROP_FOLDERS)
1300 {
1301 char *name_folded = g_utf8_casefold(name, -1);
1302 char *name_folded_slash = g_strconcat(name_folded, G_DIR_SEPARATOR_S, NULL);
1303 collate_key = g_utf8_collate_key_for_filename(name_folded_slash, -1);
1304 dt_free(name_folded_slash);
1305 dt_free(name_folded);
1306 }
1307 else
1308 collate_key = tag_collate_key(name);
1309
1310 name_key_tuple_t *tuple = (name_key_tuple_t *)malloc(sizeof(name_key_tuple_t));
1311 tuple->name = name;
1312 tuple->collate_key = collate_key;
1313 tuple->count = nv->count;
1314 tuple->status = (property == DT_COLLECTION_PROP_FOLDERS) ? nv->status : -1;
1315 sorted_names = g_list_prepend(sorted_names, tuple);
1316 }
1317 g_list_free_full(values, dt_collection_name_value_free);
1318
1319 sorted_names = g_list_sort(sorted_names, sort_folder_tag);
1320 if(!dt_conf_get_bool("plugins/collect/descending")) sorted_names = g_list_reverse(sorted_names);
1321 return sorted_names;
1322}
1323
1324// Build the hierarchical tree store from the pre-sorted names. Each name is split into path
1325// components; consecutive names share their common prefix, so we only insert the new tail under
1326// the right parent (which is why the input must be path-sorted).
1327static void _build_tree_store(GtkTreeStore *store, int property, GList *sorted_names,
1328 gboolean no_uncategorized, const char *format_separator)
1329{
1330 GtkTreeModel *model = GTK_TREE_MODEL(store);
1331 GtkTreeIter uncategorized = { 0 };
1332 char **last_tokens = NULL;
1333 int last_tokens_length = 0;
1334 GtkTreeIter last_parent = { 0 };
1335 guint index = 0;
1336
1337 for(GList *names = sorted_names; names; names = g_list_next(names))
1338 {
1339 name_key_tuple_t *tuple = (name_key_tuple_t *)names->data;
1340 char *name = tuple->name;
1341 const int count = tuple->count;
1342 const int status = tuple->status;
1343 if(IS_NULL_PTR(name)) continue;
1344
1345 const char *next_name = names->next ? ((name_key_tuple_t *)names->next->data)->name : NULL;
1346 if(!no_uncategorized && _maybe_file_uncategorized(store, name, next_name, &uncategorized, &index, count))
1347 continue;
1348
1349 char **tokens = _split_tree_name(property, name);
1350 if(IS_NULL_PTR(tokens)) continue;
1351
1352 GtkTreeIter parent = last_parent;
1353 const int tokens_length = string_array_length(tokens);
1354 int common_length = 0;
1355 if(last_tokens)
1356 {
1357 while(tokens[common_length] && last_tokens[common_length]
1358 && !g_strcmp0(tokens[common_length], last_tokens[common_length]))
1359 common_length++;
1360 for(int i = common_length; i < last_tokens_length; i++)
1361 {
1362 gtk_tree_model_iter_parent(model, &parent, &last_parent);
1363 last_parent = parent;
1364 }
1365 }
1366
1367 char *pth = NULL;
1368#ifndef _WIN32
1369 if(property == DT_COLLECTION_PROP_FOLDERS) pth = g_strdup("/");
1370#endif
1371 for(int i = 0; i < common_length; i++) pth = dt_util_dstrcat(pth, format_separator, tokens[i]);
1372
1373 for(char **token = &tokens[common_length]; *token; token++)
1374 {
1375 GtkTreeIter iter;
1376 pth = dt_util_dstrcat(pth, format_separator, *token);
1377 if(is_time_property(property) && !*(token + 1)) pth[10] = ' ';
1378
1379 gchar *pth2 = g_strdup(pth);
1380 pth2[strlen(pth2) - 1] = '\0';
1381 const gboolean leaf = !*(token + 1);
1382 gtk_tree_store_insert_with_values(store, &iter, common_length > 0 ? &parent : NULL, 0,
1385 (leaf ? count : 0), DT_LIB_COLLECT_COL_INDEX, index,
1386 DT_LIB_COLLECT_COL_UNREACHABLE, (leaf ? !status : 0),
1387 DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL, -1);
1388 index++;
1389 const gboolean recursive_count = property == DT_COLLECTION_PROP_DAY || is_time_property(property)
1390 || property == DT_COLLECTION_PROP_FOLDERS;
1391 if(recursive_count && leaf) _propagate_count_to_ancestors(store, &iter, count);
1392 common_length++;
1393 parent = iter;
1394 dt_free(pth2);
1395 }
1396 dt_free(pth);
1397
1398 if(last_tokens) g_strfreev(last_tokens);
1399 last_tokens = tokens;
1400 last_parent = parent;
1401 last_tokens_length = tokens_length;
1402 }
1403 g_strfreev(last_tokens);
1404}
1405
1406// Hierarchical (tree) properties: folders, tags, geotags, day, date-times.
1408{
1410 const int property = _combo_get_active_collection(dr->combo);
1411 char *format_separator = "";
1412
1413 switch(property)
1414 {
1416 format_separator = "%s" G_DIR_SEPARATOR_S;
1417 break;
1420 format_separator = "%s|";
1421 break;
1428 format_separator = "%s:";
1429 break;
1430 }
1431
1432 set_properties(dr);
1433
1434 GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->treefilter));
1435 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
1436 GTK_SORT_ASCENDING);
1437
1438 GHashTable *saved_expanded = NULL; // expanded node paths, captured across a rebuild
1439
1440 if(d->view_rule != property)
1441 {
1442 // remember which nodes were expanded so the rebuild doesn't lose the user's place
1443 saved_expanded = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1444 gtk_tree_view_map_expanded_rows(d->view, _collect_expanded_cb, saved_expanded);
1445
1446 g_object_ref(model);
1447 g_object_unref(d->treefilter);
1448 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL);
1449 gtk_tree_store_clear(GTK_TREE_STORE(model));
1450 gtk_widget_hide(GTK_WIDGET(d->view));
1451
1452 const gboolean no_uncategorized = (property == DT_COLLECTION_PROP_TAG)
1453 ? dt_conf_get_bool("plugins/lighttable/tagging/no_uncategorized")
1454 : TRUE;
1455 GList *sorted_names = _collect_sorted_tree_names(property, dr->num);
1456 _build_tree_store(GTK_TREE_STORE(model), property, sorted_names, no_uncategorized, format_separator);
1457 g_list_free_full(sorted_names, free_tuple);
1458
1459 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP);
1460 d->treefilter = _create_filtered_model(model, dr);
1461
1462 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view));
1463 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
1464
1465 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->treefilter);
1466 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
1467 gtk_widget_show_all(GTK_WIDGET(d->view));
1468
1469 g_object_unref(model);
1470 d->view_rule = property;
1471 }
1472
1473 // A [a;b] range on a numeric/date tree property reduces the tree to the in-range nodes (like
1474 // a text search reduces folder/filename lists), instead of a substring filter that — since no
1475 // node literally contains "[a;b]" — would hide everything.
1476 _range_t *range = NULL;
1477 if(item_is_numeric(property))
1478 {
1479 GRegex *regex = g_regex_new("^\\s*\\[\\s*(.*)\\s*;\\s*(.*)\\s*\\]\\s*$", 0, 0, NULL);
1480 GMatchInfo *match_info;
1481 g_regex_match_full(regex, gtk_entry_get_text(GTK_ENTRY(dr->text)), -1, 0, 0, &match_info, NULL);
1482 if(g_match_info_get_match_count(match_info) == 3)
1483 {
1484 range = (_range_t *)calloc(1, sizeof(_range_t));
1485 range->start = g_match_info_fetch(match_info, 2); // inverted: dates are reverse-ordered
1486 range->stop = g_match_info_fetch(match_info, 1);
1487 }
1488 g_match_info_free(match_info);
1489 g_regex_unref(regex);
1490 }
1491
1492 if(range)
1493 {
1494 // restrict the visible nodes to the dates within [start;stop] (bounds taken order-agnostic)
1495 const guint64 a_lo = _date_key(range->start, '0'), a_hi = _date_key(range->start, '9');
1496 const guint64 b_lo = _date_key(range->stop, '0'), b_hi = _date_key(range->stop, '9');
1497 _date_range_t dvr = { MIN(a_lo, b_lo), MAX(a_hi, b_hi) };
1498 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_range_visible, &dvr);
1499 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_reveal_func, NULL);
1500 }
1501 else if(dr->typing)
1503
1504 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view));
1505
1506 // Active search (text or range) collapses, then we expand just the matches. When merely
1507 // refreshing/browsing, restore the previous expansion so the view stays where the user was.
1508 if(range)
1509 gtk_tree_view_expand_all(d->view); // reveal the reduced in-range set
1510 else if(dr->typing)
1511 gtk_tree_view_collapse_all(d->view);
1512 else if(saved_expanded)
1513 {
1514 _expand_ctx_t ctx = { d, saved_expanded };
1515 gtk_tree_model_foreach(d->treefilter, _restore_expanded_cb, &ctx);
1516 }
1517 if(saved_expanded) g_hash_table_destroy(saved_expanded);
1518
1519 if(range)
1520 {
1521 // also select the boundary rows when the typed bounds match actual leaves
1522 gtk_tree_model_foreach(d->treefilter, (GtkTreeModelForeachFunc)range_select, range);
1523 if(range->path1 && range->path2)
1524 gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), range->path1, range->path2);
1525 dt_free(range->start);
1526 dt_free(range->stop);
1527 gtk_tree_path_free(range->path1);
1528 gtk_tree_path_free(range->path2);
1529 dt_free(range);
1530 }
1531 else
1532 gtk_tree_model_foreach(d->treefilter, (GtkTreeModelForeachFunc)tree_expand, dr);
1533}
1534
1535// Flat (list) properties: film-rolls, camera, lens, filename, rating, metadata, ...
1537{
1539 const int property = _combo_get_active_collection(dr->combo);
1540
1541 set_properties(dr);
1542
1543 GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->listfilter));
1544 if(d->view_rule != property)
1545 {
1546 GtkTreeIter iter;
1547 g_object_unref(d->listfilter);
1548 g_object_ref(model);
1549 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL);
1550 gtk_list_store_clear(GTK_LIST_STORE(model));
1551 gtk_widget_hide(GTK_WIDGET(d->view));
1552
1553 GList *values = dt_collection_get_property_values(property, dr->num);
1554 for(GList *v = values; v; v = g_list_next(v))
1555 {
1557 if(IS_NULL_PTR(nv->name)) continue;
1558 const char *value = nv->name;
1559 // film-rolls show a shortened folder name but keep the full path for queries/management
1560 const char *display = (property == DT_COLLECTION_PROP_FILMROLL) ? dt_image_film_roll_name(value) : value;
1561 const int unreachable = (property == DT_COLLECTION_PROP_FILMROLL) ? (nv->status == 0) : 0;
1562
1563 gchar *text = g_strdup(value);
1564 gchar *ptr = text;
1565 while(!g_utf8_validate(ptr, -1, (const gchar **)&ptr)) ptr[0] = '?';
1566 gchar *escaped_text = g_markup_escape_text(text, -1);
1567
1568 gtk_list_store_append(GTK_LIST_STORE(model), &iter);
1569 gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_LIB_COLLECT_COL_TEXT, display, DT_LIB_COLLECT_COL_ID,
1572 DT_LIB_COLLECT_COL_UNREACHABLE, unreachable, DT_LIB_COLLECT_COL_FONT, PANGO_WEIGHT_NORMAL,
1573 -1);
1574 dt_free(text);
1575 dt_free(escaped_text);
1576 }
1577 g_list_free_full(values, dt_collection_name_value_free);
1578
1579 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP);
1580 d->listfilter = _create_filtered_model(model, dr);
1581
1582 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view));
1583 const gboolean multi = item_is_numeric(property) || property == DT_COLLECTION_PROP_FILMROLL;
1584 gtk_tree_selection_set_mode(selection, multi ? GTK_SELECTION_MULTIPLE : GTK_SELECTION_SINGLE);
1585
1586 gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->listfilter);
1587 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
1588 gtk_widget_show_all(GTK_WIDGET(d->view));
1589 g_object_unref(model);
1590 d->view_rule = property;
1591 }
1592
1593 // restrict to matching entries while typing
1594 if(dr->typing
1595 && (property == DT_COLLECTION_PROP_CAMERA || property == DT_COLLECTION_PROP_FILENAME
1596 || property == DT_COLLECTION_PROP_FILMROLL || property == DT_COLLECTION_PROP_LENS
1598 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_MODULE
1599 || property == DT_COLLECTION_PROP_ORDER || property == DT_COLLECTION_PROP_RATING
1600 || (property >= DT_COLLECTION_PROP_METADATA
1602 {
1603 gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1);
1604 if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0';
1605 dr->searchstring = needle;
1606 gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)list_match_string, dr);
1607 dr->searchstring = NULL;
1608 dt_free(needle);
1609 }
1610 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view));
1611
1612 if(item_is_numeric(property))
1613 {
1614 GRegex *regex = g_regex_new("^\\s*\\[\\s*(.*)\\s*;\\s*(.*)\\s*\\]\\s*$", 0, 0, NULL);
1615 GMatchInfo *match_info;
1616 g_regex_match_full(regex, gtk_entry_get_text(GTK_ENTRY(dr->text)), -1, 0, 0, &match_info, NULL);
1617 const int match_count = g_match_info_get_match_count(match_info);
1618 if(match_count == 3)
1619 {
1620 _range_t *range = (_range_t *)calloc(1, sizeof(_range_t));
1621 range->start = g_match_info_fetch(match_info, 1);
1622 range->stop = g_match_info_fetch(match_info, 2);
1623 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)range_select, range);
1624 if(range->path1 && range->path2)
1625 gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), range->path1, range->path2);
1626 dt_free(range->start);
1627 dt_free(range->stop);
1628 gtk_tree_path_free(range->path1);
1629 gtk_tree_path_free(range->path2);
1630 dt_free(range);
1631 }
1632 else
1633 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr);
1634 g_match_info_free(match_info);
1635 g_regex_unref(regex);
1636 }
1637 else
1638 gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr);
1639}
1640
1642{
1643 const int property = _combo_get_active_collection(dr->combo);
1644 if(item_is_tree(property))
1645 _populate_tree(dr);
1646 else
1647 _populate_list(dr);
1648 dr->reveal = FALSE; // one-shot: consumed by this rebuild
1649}
1650
1651// =====================================================================================
1652// Section 5 — clicking a value row -> write the rule text and refresh the collection
1653// =====================================================================================
1654
1655// Append the tag/geotag hierarchy suffix chosen by the modifier keys. Consumes and replaces
1656// `text`: ctrl -> sub-hierarchies only ("|%"), plain -> this hierarchy + sub ("*"), shift -> the
1657// exact node (no suffix).
1658static gchar *_decorate_hierarchy(gchar *text, GdkEventButton *event)
1659{
1660 if(event && dt_modifier_is(event->state, GDK_CONTROL_MASK))
1661 {
1662 gchar *n = g_strconcat(text, "|%", NULL);
1663 dt_free(text);
1664 return n;
1665 }
1666 if(!event || !dt_modifier_is(event->state, GDK_SHIFT_MASK))
1667 {
1668 gchar *n = g_strconcat(text, "*", NULL);
1669 dt_free(text);
1670 return n;
1671 }
1672 return text;
1673}
1674
1675// Clicking a leaf tag in the first rule adopts that tag's saved sort order. Returns TRUE (with
1676// *order filled) when an order change should be signalled.
1677static gboolean _adopt_tag_order(const char *text, int *order)
1678{
1679 const uint32_t tagid = dt_tag_get_tag_id_by_name(text);
1680 if(!tagid)
1681 {
1683 return FALSE;
1684 }
1685 uint32_t sort = DT_COLLECTION_SORT_NONE;
1686 gboolean descending = FALSE;
1687 if(dt_tag_get_tag_order_by_id(tagid, &sort, &descending))
1688 *order = sort | (descending ? DT_COLLECTION_ORDER_FLAG : 0);
1689 else
1690 {
1693 }
1695 return TRUE;
1696}
1697
1698static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d)
1699{
1700 GtkTreeIter iter;
1701 GtkTreeModel *model = NULL;
1702 GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
1703 const int n_selected = gtk_tree_selection_count_selected_rows(selection);
1704 if(n_selected < 1) return;
1705
1706 GList *sels = gtk_tree_selection_get_selected_rows(selection, &model);
1707 GtkTreePath *path1 = (GtkTreePath *)sels->data;
1708 if(!gtk_tree_model_get_iter(model, &iter, path1))
1709 {
1710 g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free);
1711 return;
1712 }
1713
1715 dt_lib_collect_rule_t *active_rule = get_active_rule(d);
1716 active_rule->typing = FALSE;
1717 const int item = _combo_get_active_collection(active_rule->combo);
1718
1719 gchar *text;
1720 gboolean order_request = FALSE;
1721 int order = 0;
1722 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1);
1723
1724 if(text && strlen(text) > 0)
1725 {
1726 if(n_selected > 1 && item_is_numeric(item))
1727 {
1728 // range selection [a;b]
1729 GtkTreeIter iter2;
1730 GtkTreePath *path2 = (GtkTreePath *)g_list_last(sels)->data;
1731 if(gtk_tree_model_get_iter(model, &iter2, path2))
1732 {
1733 gchar *text2;
1734 gtk_tree_model_get(model, &iter2, DT_LIB_COLLECT_COL_PATH, &text2, -1);
1735 gchar *n_text = (item == DT_COLLECTION_PROP_DAY || is_time_property(item))
1736 ? g_strdup_printf("[%s;%s]", text2, text) // dates are reverse-ordered
1737 : g_strdup_printf("[%s;%s]", text, text2);
1738 dt_free(text);
1739 dt_free(text2);
1740 text = n_text;
1741 }
1742 }
1743 else if(item == DT_COLLECTION_PROP_FOLDERS)
1744 {
1745 // Recursion is a persisted flag (recursive_check / recursive{N} conf), independent of
1746 // which folder was clicked -- nothing to do here, set_properties() persists it already.
1747 }
1748 else if(item == DT_COLLECTION_PROP_TAG || item == DT_COLLECTION_PROP_GEOTAGGING)
1749 {
1750 if(gtk_tree_model_iter_has_child(model, &iter))
1751 text = _decorate_hierarchy(text, event);
1752 else if(item == DT_COLLECTION_PROP_TAG && active_rule == d->rule && g_strcmp0(text, _("not tagged")))
1753 order_request = _adopt_tag_order(text, &order);
1754 }
1755 else
1756 _combo_set_active_collection(active_rule->combo, item);
1757 }
1758 g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free);
1759
1760 g_signal_handlers_block_matched(active_rule->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
1761 gtk_entry_set_text(GTK_ENTRY(active_rule->text), text);
1762 gtk_editable_set_position(GTK_EDITABLE(active_rule->text), -1);
1763 g_signal_handlers_unblock_matched(active_rule->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
1764 dt_free(text);
1765
1766 // properties whose value list is unaffected by the new selection only need the conf written;
1767 // the others need the value list refreshed to reflect the new search string.
1771 || item == DT_COLLECTION_PROP_GROUPING)
1772 set_properties(active_rule);
1773 else
1774 update_view(active_rule);
1775
1777 _commit_quiet();
1780}
1781
1782// =====================================================================================
1783// Section 6 — management & admin actions (extensible right-click framework)
1784// =====================================================================================
1785
1787{
1789}
1790
1792{
1793 d->view_rule = -1;
1795}
1796
1797// one selected value row
1798typedef struct collect_row_t
1799{
1800 gchar *path; // DT_LIB_COLLECT_COL_PATH (folder path / tag path)
1801 gint id; // DT_LIB_COLLECT_COL_ID (film_roll id / tag id)
1803
1804static void _free_row(gpointer p)
1805{
1807 dt_free(r->path);
1808 dt_free(r);
1809}
1810
1812{
1813 GtkTreeModel *model = NULL;
1814 GList *paths = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(d->view), &model);
1815 GList *out = NULL;
1816 for(GList *l = paths; l; l = g_list_next(l))
1817 {
1818 GtkTreeIter it;
1819 if(gtk_tree_model_get_iter(model, &it, (GtkTreePath *)l->data))
1820 {
1821 collect_row_t *r = g_malloc0(sizeof(collect_row_t));
1822 gtk_tree_model_get(model, &it, DT_LIB_COLLECT_COL_PATH, &r->path, DT_LIB_COLLECT_COL_ID, &r->id, -1);
1823 out = g_list_prepend(out, r);
1824 }
1825 }
1826 g_list_free_full(paths, (GDestroyNotify)gtk_tree_path_free);
1827 return g_list_reverse(out);
1828}
1829
1830// Map selected rows to the de-duplicated set of matching image ids, reusing the SQL engine.
1831// Foundation for any bulk operation (export, pre-render thumbnails, ...). Caller g_list_free.
1832static GList *_rows_to_imgids(int property, GList *rows, gboolean recursive)
1833{
1834 GHashTable *seen = g_hash_table_new(g_direct_hash, g_direct_equal);
1835 GList *out = NULL;
1836 for(GList *l = rows; l; l = g_list_next(l))
1837 {
1838 collect_row_t *r = (collect_row_t *)l->data;
1839 // folder rows always query through FOLDERS
1840 const int prop = item_is_folder(property) ? DT_COLLECTION_PROP_FOLDERS : property;
1841 GList *ids = dt_collection_get_images_for_rule(prop, r->path, item_is_folder(property) && recursive);
1842 for(GList *i = ids; i; i = g_list_next(i))
1843 if(!g_hash_table_contains(seen, i->data))
1844 {
1845 g_hash_table_add(seen, i->data);
1846 out = g_list_prepend(out, i->data);
1847 }
1848 g_list_free(ids);
1849 }
1850 g_hash_table_destroy(seen);
1851 return g_list_reverse(out);
1852}
1853
1854// ---- small dialog helpers ----
1855static gboolean _confirm(const char *title, const char *message)
1856{
1858 GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION,
1859 GTK_BUTTONS_YES_NO, "%s", message);
1860 gtk_window_set_title(GTK_WINDOW(dialog), title);
1861#ifdef GDK_WINDOWING_QUARTZ
1863#endif
1864 const gint res = gtk_dialog_run(GTK_DIALOG(dialog));
1865 gtk_widget_destroy(dialog);
1866 return res == GTK_RESPONSE_YES;
1867}
1868
1869static gchar *_ask_text(const char *title, const char *initial)
1870{
1872 GtkWidget *dialog
1873 = gtk_dialog_new_with_buttons(title, GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_cancel"),
1874 GTK_RESPONSE_CANCEL, _("_ok"), GTK_RESPONSE_ACCEPT, NULL);
1875 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1876 GtkWidget *entry = gtk_entry_new();
1877 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
1878 if(initial) gtk_entry_set_text(GTK_ENTRY(entry), initial);
1879 gtk_box_pack_start(GTK_BOX(area), entry, TRUE, TRUE, 0);
1880 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
1881 g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL);
1882 gtk_widget_show_all(dialog);
1883#ifdef GDK_WINDOWING_QUARTZ
1885#endif
1886 gchar *result = NULL;
1887 if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
1888 {
1889 const gchar *t = gtk_entry_get_text(GTK_ENTRY(entry));
1890 if(t && *t) result = g_strdup(t);
1891 }
1892 gtk_widget_destroy(dialog);
1893 return result;
1894}
1895
1896// ---- actions ----
1897static void _act_folders_remove(dt_lib_collect_t *d, GList *rows)
1898{
1899 // Recursive, like _act_prerender() below: a folder node with only subfolder children (no
1900 // images directly in it) has nothing to match non-recursively, so a non-recursive lookup here
1901 // silently no-ops on any such parent folder -- most of a real, nested film-roll tree. The
1902 // subtree count is shown to the user in dt_control_remove_images()'s "remove N images?"
1903 // confirmation before anything is touched, so recursion doesn't skip that safety net.
1904 GList *imgids = _rows_to_imgids(DT_COLLECTION_PROP_FOLDERS, rows, TRUE);
1905 if(!imgids) return;
1908 g_list_free(imgids);
1910}
1911
1912static void _act_folders_relocate(dt_lib_collect_t *d, GList *rows)
1913{
1915 const int n = g_list_length(rows);
1916 const gboolean single = (n == 1);
1917 collect_row_t *first = (collect_row_t *)rows->data;
1918
1919 GtkFileChooserNative *fc = gtk_file_chooser_native_new(
1920 single ? _("select the new location of this folder") : _("select the new parent folder"), GTK_WINDOW(win),
1921 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_open"), _("_cancel"));
1922 if(single && first->path) gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc), first->path);
1923
1924 if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(fc)) == GTK_RESPONSE_ACCEPT)
1925 {
1926 gchar *uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(fc));
1927 gchar *chosen = g_filename_from_uri(uri, NULL, NULL);
1928 dt_free(uri);
1929 if(chosen)
1930 {
1931 for(GList *l = rows; l; l = g_list_next(l))
1932 {
1933 collect_row_t *r = (collect_row_t *)l->data;
1934 if(IS_NULL_PTR(r->path)) continue;
1935 if(single)
1936 dt_film_relocate(r->path, chosen);
1937 else
1938 {
1939 gchar *base = g_path_get_basename(r->path);
1940 gchar *dest = g_build_filename(chosen, base, NULL);
1941 dt_film_relocate(r->path, dest);
1942 dt_free(base);
1943 dt_free(dest);
1944 }
1945 }
1950 dt_free(chosen);
1951 }
1952 else
1953 dt_control_log(_("problem selecting new path for the folder"));
1954 }
1955 g_object_unref(fc);
1956}
1957
1958static void _act_tags_remove(dt_lib_collect_t *d, GList *rows)
1959{
1960 const int n = g_list_length(rows);
1961 gchar *msg = g_strdup_printf(ngettext("Delete %d tag and detach it from all images?",
1962 "Delete %d tags and detach them from all images?", n),
1963 n);
1964 const gboolean ok = _confirm(_("delete tags"), msg);
1965 dt_free(msg);
1966 if(!ok) return;
1967
1968 for(GList *l = rows; l; l = g_list_next(l))
1969 {
1970 collect_row_t *r = (collect_row_t *)l->data;
1971 // tree rows don't carry the real tag id (the enumeration uses a placeholder), so resolve
1972 // it from the full tag path
1973 const guint tagid = IS_NULL_PTR(r->path) ? 0 : dt_tag_get_tag_id_by_name(r->path);
1974 if(tagid) dt_tag_remove(tagid, TRUE);
1975 }
1978}
1979
1980static void _act_tag_rename(dt_lib_collect_t *d, GList *rows)
1981{
1982 collect_row_t *r = (collect_row_t *)rows->data;
1983 if(IS_NULL_PTR(r->path)) return;
1984 const guint tagid = dt_tag_get_tag_id_by_name(r->path);
1985 if(!tagid) return;
1986 gchar *newname = _ask_text(_("rename tag"), r->path);
1987 if(newname)
1988 {
1989 dt_tag_rename(tagid, newname);
1990 dt_free(newname);
1993 }
1994}
1995
1996// ---- pre-render thumbnails of the matching image set (background job) ----
1998{
1999 GList *imgids; // owned: imgids to render
2002
2003static void _prerender_free(void *p)
2004{
2006 g_list_free(pr->imgids);
2007 g_free(pr);
2008}
2009
2010// Fill the on-disk mipmap cache for every imgid, largest size first (smaller sizes are then
2011// downscaled from it rather than recomputed). Mirrors the "preload" job in gui/actions/run.c,
2012// but works on an explicit imgid list so it never touches the user's selection.
2013static int32_t _prerender_job(dt_job_t *job)
2014{
2016 const dt_mipmap_size_t max = p->max_size;
2017 const int n = g_list_length(p->imgids);
2018 const float total = (n > 0) ? (float)(n * (max + 1)) : 1.0f;
2019 int done = 0;
2020
2021 for(GList *l = p->imgids; l && dt_control_job_get_state(job) != DT_JOB_STATE_CANCELLED; l = g_list_next(l))
2022 {
2023 const int32_t imgid = GPOINTER_TO_INT(l->data);
2025 {
2026 char filename[PATH_MAX] = { 0 };
2028 if(!dt_util_test_image_file(filename)) // skip thumbnails already on disc
2029 {
2033 }
2034 dt_control_job_set_progress(job, (float)(++done) / total);
2035 }
2036 dt_mimap_cache_evict(darktable.mipmap_cache, imgid); // flush to disc, free RAM
2037 }
2038 return 0;
2039}
2040
2041static void _act_prerender(dt_lib_collect_t *d, GList *rows)
2042{
2043 // recursive for folders so a parent folder renders its whole subtree
2044 GList *imgids = _rows_to_imgids(d->view_rule, rows, TRUE);
2045 if(!imgids) return;
2046 collect_prerender_t *p = g_malloc0(sizeof(collect_prerender_t));
2047 p->imgids = imgids; // takes ownership
2048 p->max_size = DT_MIPMAP_2;
2049 dt_job_t *job = dt_control_job_create(&_prerender_job, "prerender collection thumbnails");
2051 dt_control_job_add_progress(job, _("pre-rendering thumbnails"), TRUE);
2053}
2054
2055// ---- action table: add a bulk operation by adding a row here ----
2056static gboolean _en_folders(int property, int n)
2057{
2058 return item_is_folder(property);
2059}
2060// Unlike _en_folders(), also require Lighttable: dt_control_remove_images() unconditionally
2061// refuses to run outside DT_VIEW_LIGHTTABLE (control/jobs/control_jobs.c), the same restriction
2062// the global "File > Remove from library" menu enforces by graying itself out
2063// (has_active_image_in_lighttable(), gui/actions/menu.c). This panel is also shown in the map and
2064// print views (see views() above), where the context-menu action would otherwise appear enabled
2065// but silently do nothing beyond a log message.
2066static gboolean _en_folders_lighttable(int property, int n)
2067{
2069 return item_is_folder(property) && cv && cv->view(cv) == DT_VIEW_LIGHTTABLE;
2070}
2071static gboolean _en_tags(int property, int n)
2072{
2073 return item_is_tag(property);
2074}
2075static gboolean _en_tag_single(int property, int n)
2076{
2077 return item_is_tag(property) && n == 1;
2078}
2079static gboolean _en_any(int property, int n)
2080{
2081 return item_is_folder(property) || item_is_tag(property);
2082}
2083
2084typedef struct collect_action_t
2085{
2086 const char *label;
2087 gboolean multi; // allow more than one selected row
2088 gboolean (*enabled)(int property, int n);
2089 void (*run)(dt_lib_collect_t *d, GList *rows);
2091
2092static const collect_action_t ACTIONS[] = {
2093 { N_("remove from library..."), TRUE, _en_folders_lighttable, _act_folders_remove },
2094 { N_("relocate..."), TRUE, _en_folders, _act_folders_relocate },
2095 { N_("delete tag(s)..."), TRUE, _en_tags, _act_tags_remove },
2096 { N_("rename tag..."), FALSE, _en_tag_single, _act_tag_rename },
2097 { N_("pre-render thumbnails"), TRUE, _en_any, _act_prerender },
2098};
2099
2100static void _action_activate(GtkMenuItem *mi, dt_lib_collect_t *d)
2101{
2102 const collect_action_t *act = g_object_get_data(G_OBJECT(mi), "collect-action");
2103 GList *rows = _selected_rows(d);
2104 if(rows) act->run(d, rows);
2105 g_list_free_full(rows, _free_row);
2106}
2107
2108static void _show_context_menu(dt_lib_collect_t *d, GdkEventButton *event)
2109{
2110 const int property = d->view_rule;
2111 const int n = gtk_tree_selection_count_selected_rows(gtk_tree_view_get_selection(d->view));
2112 if(n < 1) return;
2113
2114 GtkWidget *menu = gtk_menu_new();
2115 int shown = 0;
2116 for(size_t i = 0; i < G_N_ELEMENTS(ACTIONS); i++)
2117 {
2118 const collect_action_t *act = &ACTIONS[i];
2119 if(!act->enabled(property, n)) continue;
2120 if(!act->multi && n > 1) continue;
2121 GtkWidget *mi = gtk_menu_item_new_with_label(_(act->label));
2122 g_object_set_data(G_OBJECT(mi), "collect-action", (gpointer)act);
2123 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_action_activate), d);
2124 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2125 shown++;
2126 }
2127 if(shown)
2128 {
2129 gtk_widget_show_all(menu);
2130 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2131 }
2132 else
2133 gtk_widget_destroy(menu);
2134}
2135
2136// =====================================================================================
2137// Section 7 — view & widget events
2138// =====================================================================================
2139
2140// Drag & drop target: images dragged from the lighttable thumbtable (DND_TARGET_IMGID carries an
2141// array of uint32_t imgids). Dropping on a folder row physically moves the files into it; dropping
2142// on a tag row attaches the tag.
2143static gboolean _drop_move_to_folder(dt_lib_collect_t *d, const char *folder, GList *imgs)
2144{
2145 if(IS_NULL_PTR(folder) || !*folder || IS_NULL_PTR(imgs)) return FALSE;
2146 const int n = g_list_length(imgs);
2147 gchar *msg = g_strdup_printf(ngettext("Physically move %d image to\n%s ?\n\nFiles are moved on disk.",
2148 "Physically move %d images to\n%s ?\n\nFiles are moved on disk.", n),
2149 n, folder);
2150 const gboolean ok = _confirm(_("move images"), msg);
2151 g_free(msg);
2152 if(!ok) return FALSE;
2153
2154 dt_film_t film;
2155 dt_film_init(&film);
2156 dt_film_new(&film, folder); // create-or-fetch the film roll for that folder
2157 const int32_t filmid = film.id;
2158 dt_film_cleanup(&film);
2159 if(filmid <= 0)
2160 {
2161 dt_control_log(_("could not access the destination folder"));
2162 return FALSE;
2163 }
2164
2165 int moved = 0;
2166 for(GList *l = imgs; l; l = g_list_next(l))
2167 if(dt_image_move(GPOINTER_TO_INT(l->data), filmid) != -1) moved++;
2168
2169 if(moved)
2170 {
2176 }
2177 return moved > 0;
2178}
2179
2180static gboolean _drop_attach_tag(dt_lib_collect_t *d, const char *tagpath, GList *imgs)
2181{
2182 // tree rows carry a placeholder id, so resolve the real tag id from the full path
2183 const guint tagid = (IS_NULL_PTR(tagpath) || !*tagpath) ? 0 : dt_tag_get_tag_id_by_name(tagpath);
2184 if(!tagid) return FALSE;
2185 dt_tag_attach_images(tagid, imgs, TRUE);
2187
2188 // DAM use case (#905): dragging images onto a tag is a pure metadata write and must NOT
2189 // re-filter the current collection -- the dropped images stay put so the user can keep
2190 // tagging from the same view. Our own tag_changed() would otherwise re-run the collection
2191 // query whenever the active rule collects by tag (turning the view into "all images carrying
2192 // this tag", i.e. the `%`/mixed-view bug), so block it for the duration of the broadcast.
2193 // Other modules (tagging, metadata) still receive DT_SIGNAL_TAG_CHANGED and refresh.
2194 dt_lib_module_t *self = _self();
2198
2199 _force_refresh(d); // refresh the tag counts in the treeview, without touching the collection
2200 return TRUE;
2201}
2202
2203// Perform the drop of `sel` (a uint32_t imgid array) onto the row under (x, y). Returns success.
2204static gboolean _do_drop(dt_lib_collect_t *d, GtkTreeView *tree, gint x, gint y, GtkSelectionData *sel)
2205{
2206 const gboolean to_tag = item_is_tag(d->view_rule);
2207 if(!(to_tag || item_is_folder(d->view_rule))) return FALSE;
2208
2209 const int imgs_nb = gtk_selection_data_get_length(sel) / (int)sizeof(uint32_t);
2210 if(imgs_nb <= 0) return FALSE;
2211
2212 GtkTreePath *path = NULL;
2213 if(!gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL)) return FALSE;
2214
2215 GtkTreeModel *model = gtk_tree_view_get_model(tree);
2216 GtkTreeIter iter;
2217 gboolean ok = FALSE;
2218 if(gtk_tree_model_get_iter(model, &iter, path))
2219 {
2220 const uint32_t *imgt = (const uint32_t *)gtk_selection_data_get_data(sel);
2221 GList *imgs = NULL;
2222 for(int i = 0; i < imgs_nb; i++) imgs = g_list_prepend(imgs, GINT_TO_POINTER((int)imgt[i]));
2223
2224 gchar *rowpath = NULL;
2225 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &rowpath, -1);
2226 ok = to_tag ? _drop_attach_tag(d, rowpath, imgs) : _drop_move_to_folder(d, rowpath, imgs);
2227 dt_free(rowpath);
2228 g_list_free(imgs);
2229 }
2230 gtk_tree_path_free(path);
2231 return ok;
2232}
2233
2234static gboolean _clear_suppress_row_activated(gpointer d)
2235{
2236 ((dt_lib_collect_t *)d)->suppress_row_activated = FALSE;
2237 return FALSE; // one-shot
2238}
2239
2240// Highlight the row under the pointer as the drop target via GtkTreeView's native drag-dest
2241// mechanism: gtk_tree_view_set_drag_dest_row() flags the row with the GTK_STATE_FLAG_DROP_ACTIVE
2242// state, which the theme styles through `treeview.view:drop(active)` (see ansel.css). Restricted
2243// to folder/tag rows since those are the only valid drop targets (_do_drop).
2244static gboolean _view_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time,
2246{
2247 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2248 GtkTreePath *path = NULL;
2249 if((item_is_folder(d->view_rule) || item_is_tag(d->view_rule))
2250 && gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
2251 {
2252 gtk_tree_view_set_drag_dest_row(tree, path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
2253 gtk_tree_path_free(path);
2254 gdk_drag_status(context, GDK_ACTION_MOVE, time);
2255 }
2256 else
2257 {
2258 gtk_tree_view_set_drag_dest_row(tree, NULL, 0);
2259 gdk_drag_status(context, 0, time);
2260 }
2261 return TRUE;
2262}
2263
2264static void _view_drag_leave(GtkWidget *widget, GdkDragContext *context, guint time, dt_lib_collect_t *d)
2265{
2266 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), NULL, 0);
2267}
2268
2269static void _view_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
2270 GtkSelectionData *selection_data, guint target_type, guint time,
2272{
2273 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2274 g_signal_stop_emission_by_name(tree, "drag-data-received"); // bypass GtkTreeView's own DnD
2275 gtk_tree_view_set_drag_dest_row(tree, NULL, 0); // clear the drop-target highlight
2276 // the button-release ending the drag still reaches the treeview as an ordinary click; with
2277 // activate-on-single-click that would otherwise add/replace a collection filter rule for the
2278 // row the images were just dropped on (#905). Cleared on idle, after that synthetic click.
2279 d->suppress_row_activated = TRUE;
2280 g_idle_add(_clear_suppress_row_activated, d);
2281 const gboolean success = (target_type == DND_TARGET_IMGID && !IS_NULL_PTR(selection_data))
2282 && _do_drop(d, tree, x, y, selection_data);
2283 gtk_drag_finish(context, success, FALSE, time);
2284}
2285
2286static gboolean _view_button_pressed(GtkWidget *treeview, GdkEventButton *event, dt_lib_collect_t *d)
2287{
2288 // We only special-case right-click to raise the management menu; left clicks / expander
2289 // toggles use GtkTreeView's default handling, and activation goes through "row-activated".
2290 if(event->button != 3 || event->type != GDK_BUTTON_PRESS) return FALSE;
2291 if(!(item_is_folder(d->view_rule) || item_is_tag(d->view_rule))) return FALSE;
2292
2293 GtkTreeView *view = GTK_TREE_VIEW(treeview);
2294 GtkTreeSelection *sel = gtk_tree_view_get_selection(view);
2295
2296 // Right-clicking a row outside the current selection re-selects just that row; right-clicking
2297 // within an existing multi-selection keeps it so the batch actions apply to all of it.
2298 GtkTreePath *path = NULL;
2299 if(gtk_tree_view_get_path_at_pos(view, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL) && path)
2300 {
2301 if(!gtk_tree_selection_path_is_selected(sel, path))
2302 {
2303 gtk_tree_selection_unselect_all(sel);
2304 gtk_tree_selection_select_path(sel, path);
2305 }
2306 gtk_tree_path_free(path);
2307 }
2308
2309 // Show the menu whenever something is selected, even if the click missed a precise cell.
2310 if(gtk_tree_selection_count_selected_rows(sel) < 1) return FALSE;
2311 _show_context_menu(d, event);
2312 return TRUE;
2313}
2314
2315static gboolean _view_popup_menu(GtkWidget *treeview, dt_lib_collect_t *d)
2316{
2317 if(!(item_is_folder(d->view_rule) || item_is_tag(d->view_rule))) return FALSE;
2318 _show_context_menu(d, NULL);
2319 return TRUE;
2320}
2321
2322static void _view_row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, dt_lib_collect_t *d)
2323{
2324 if(d->suppress_row_activated) return; // synthetic click from a drag-and-drop release (#905)
2325
2326 GdkEvent *ev = gtk_get_current_event(); // carries ctrl/shift state for tag hierarchy clicks
2327 // only a button event has ->state at the layout row_activated() expects; for key activation
2328 // (Enter) pass NULL so we fall back to the default (plain-click) behaviour.
2329 GdkEventButton *be
2330 = (ev && (ev->type == GDK_BUTTON_PRESS || ev->type == GDK_2BUTTON_PRESS || ev->type == GDK_BUTTON_RELEASE))
2331 ? (GdkEventButton *)ev
2332 : NULL;
2333 row_activated(view, path, be, d);
2334 if(ev) gdk_event_free(ev);
2335}
2336
2337static void _view_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, dt_lib_collect_t *d)
2338{
2339 if(d->view_rule != DT_COLLECTION_PROP_FOLDERS) return;
2340 gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.0, 0.0);
2341}
2342
2343// Document the search syntax of the active property, on both the entry and its combo. Imported
2344// from upstream so the wildcards / operators / ranges stay discoverable.
2346{
2347 const int property = _combo_get_active_collection(dr->combo);
2348
2350 || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_EXPOSURE)
2351 gtk_widget_set_tooltip_text(dr->text, _("use <, <=, >, >=, <>, =, [;] as operators"));
2352 else if(property == DT_COLLECTION_PROP_RATING)
2353 gtk_widget_set_tooltip_text(dr->text, _("use <, <=, >, >=, <>, =, [;] as operators\n"
2354 "star rating: 0-5\n"
2355 "rejected images: -1"));
2356 else if(property == DT_COLLECTION_PROP_DAY || is_time_property(property))
2357 gtk_widget_set_tooltip_text(dr->text,
2358 _("use <, <=, >, >=, <>, =, [;] as operators\n"
2359 "type dates in the form: YYYY:MM:DD hh:mm:ss.sss (only the year is mandatory)"));
2360 else if(property == DT_COLLECTION_PROP_FILENAME)
2361 /* xgettext:no-c-format */
2362 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard and `,' to separate values"));
2363 else if(property == DT_COLLECTION_PROP_TAG)
2364 /* xgettext:no-c-format */
2365 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard\n"
2366 "click to include hierarchy + sub-hierarchies (suffix `*')\n"
2367 "shift+click to include only the current hierarchy (no suffix)\n"
2368 "ctrl+click to include only sub-hierarchies (suffix `|%')"));
2369 else if(property == DT_COLLECTION_PROP_GEOTAGGING)
2370 /* xgettext:no-c-format */
2371 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard\n"
2372 "click to include location + sub-locations (suffix `*')\n"
2373 "shift+click to include only the current location (no suffix)\n"
2374 "ctrl+click to include only sub-locations (suffix `|%')"));
2375 else if(property == DT_COLLECTION_PROP_FOLDERS)
2376 /* xgettext:no-c-format */
2377 gtk_widget_set_tooltip_text(dr->text,
2378 _("use `%' as wildcard and append `*' to match sub-folders"));
2379 else
2380 /* xgettext:no-c-format */
2381 gtk_widget_set_tooltip_text(dr->text, _("use `%' as wildcard"));
2382
2383 gchar *tip = gtk_widget_get_tooltip_text(dr->text);
2384 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->combo), tip);
2385 dt_free(tip);
2386}
2387
2388// Show the operator combo only for properties that support comparison operators.
2390{
2391 gtk_widget_set_visible(dr->op_combo, item_is_numeric(_combo_get_active_collection(dr->combo)));
2392}
2393
2395{
2396 if(dt_gui_widgets_suppressed()) return;
2398 c->active_rule = dr->num;
2399 set_properties(dr);
2400 c->view_rule = -1;
2401 _commit_colllection(); // signal-driven refresh, like combo_changed
2402}
2403
2405{
2406 if(dt_gui_widgets_suppressed()) return;
2408 const int previous = _rule_get_item(dr->num); // conf still holds the old property
2409 const int property = _combo_get_active_collection(dr->combo);
2410
2411 c->active_rule = dr->num;
2412 dr->typing = FALSE;
2413
2414 // Clear the search text when switching to an unrelated property; folder<->folder (List/Tree)
2415 // and tag<->tag keep their text since the search string is transferable.
2416 const gboolean transferable
2417 = (item_is_folder(previous) && item_is_folder(property)) || (item_is_tag(previous) && item_is_tag(property));
2418 if(!transferable)
2419 {
2420 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2421 gtk_entry_set_text(GTK_ENTRY(dr->text), "");
2422 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2423
2424 // start the new property from the default "=" operator instead of silently carrying over the
2425 // previous property's operator (which would prefix the now-empty value with e.g. ">")
2426 g_signal_handlers_block_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
2427 gtk_combo_box_set_active(GTK_COMBO_BOX(dr->op_combo), 0);
2428 g_signal_handlers_unblock_matched(dr->op_combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _op_changed, NULL);
2429 }
2430
2431 // On the Folders tab the property combo is the List/Tree toggle: keep the sub-folders
2432 // checkbox visible only for the hierarchical (Tree = FOLDERS) view, and the "sort by"
2433 // selector only for the flat film-roll List (the folder Tree is always path-sorted).
2434 if(c->folders_controls && gtk_widget_get_visible(c->folders_controls))
2435 {
2436 gtk_widget_set_visible(c->recursive_check, property == DT_COLLECTION_PROP_FOLDERS);
2437 gtk_widget_set_visible(c->sort_by, property == DT_COLLECTION_PROP_FILMROLL);
2438 gtk_widget_set_visible(c->folder_levels, property == DT_COLLECTION_PROP_FILMROLL);
2439 }
2440
2441 _set_tooltip(dr);
2442 _update_op_combo(dr);
2443 set_properties(dr);
2444
2445 // when the query carried over (e.g. List <-> Tree, or folder <-> film-roll), unfold the rebuilt
2446 // tree to it instead of leaving the user on a collapsed view
2447 dr->reveal = transferable;
2448
2449 // Signal-driven refresh: committing rebuilds where_ext first, then collection_updated ->
2450 // _lib_collect_gui_update rebuilds the value list against the fresh constraints. (Doing the
2451 // rebuild ourselves before committing would use a stale where_ext and not refresh.)
2452 c->view_rule = -1;
2454}
2455
2456static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr)
2457{
2458 dr->typing = TRUE;
2459 // The "include sub-folders" checkbox is a persisted flag now, not derived from the text --
2460 // typing here must never feed back into it, or a plain edit of the search text would silently
2461 // flip recursion on/off.
2462 update_view(dr);
2463}
2464
2466{
2467 update_view(dr);
2469 const int property = _combo_get_active_collection(dr->combo);
2470
2471 // for flat lists, pressing enter with a single remaining match selects it
2472 if(!item_is_tree(property))
2473 {
2474 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(c->view));
2475 if(gtk_tree_model_iter_n_children(model, NULL) == 1)
2476 {
2477 GtkTreeIter iter;
2478 if(gtk_tree_model_get_iter_first(model, &iter))
2479 {
2480 gchar *text;
2481 gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1);
2482 g_signal_handlers_block_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2483 gtk_entry_set_text(GTK_ENTRY(dr->text), text);
2484 gtk_editable_set_position(GTK_EDITABLE(dr->text), -1);
2485 g_signal_handlers_unblock_matched(dr->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL);
2486 dt_free(text);
2487 update_view(dr);
2488 }
2489 }
2490 }
2491 _commit_quiet();
2492 dr->typing = FALSE;
2494}
2495
2496static gboolean _entry_focus_in(GtkWidget *w, GdkEventFocus *event, dt_lib_collect_rule_t *dr)
2497{
2500 return FALSE;
2501}
2502
2503// ---- Folders inline controls ----
2504static void _recursive_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2505{
2506 if(dt_gui_widgets_suppressed()) return;
2509
2510 // Recursion is a persisted flag now (recursive{N} conf), not a marker on the search text --
2511 // set_properties() reads the checkbox itself and writes it, nothing to do to dr->text here.
2512 set_properties(dr);
2513 _commit_quiet();
2514}
2515
2516static void _sort_dir_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2517{
2518 const gboolean desc = gtk_toggle_button_get_active(b);
2520 desc ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP, NULL);
2521 gtk_widget_queue_draw(GTK_WIDGET(b));
2522 if(dt_gui_widgets_suppressed()) return;
2523 dt_conf_set_bool("plugins/collect/descending", desc);
2525}
2526
2528{
2529 if(dt_gui_widgets_suppressed()) return;
2530 dt_conf_set_string("plugins/collect/filmroll_sort", dt_bauhaus_combobox_get(combo) == 0 ? "folder" : "id");
2532}
2533
2534// Surfaced settings that used to live in the hidden preferences popup (TODO).
2536{
2537 if(dt_gui_widgets_suppressed()) return;
2538 dt_conf_set_int("show_folder_levels", (int)gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin)));
2540}
2541
2542static void _no_uncategorized_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2543{
2544 if(dt_gui_widgets_suppressed()) return;
2545 dt_conf_set_bool("plugins/lighttable/tagging/no_uncategorized", gtk_toggle_button_get_active(b));
2547}
2548
2549// ---- Queries raw-SQL escape ----
2550static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d);
2552{
2556 _rule_set_string(0, gtk_entry_get_text(GTK_ENTRY(entry)));
2557 d->active_rule = 0;
2559}
2560
2561// ---- Queries rule +/- management ----
2562static void menuitem_mode(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2563{
2564 const int active = _rules_count();
2565 if(active < MAX_RULES)
2566 {
2567 const dt_lib_collect_mode_t mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "menuitem_mode"));
2568 _rule_set_mode(active, mode);
2569 _rule_set_string(active, "");
2571 _rules_set_count(active + 1);
2573 c->active_rule = active;
2574 c->view_rule = -1;
2575 }
2577}
2578
2579static void menuitem_mode_change(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2580{
2581 const int num = dr->num + 1;
2582 if(num < MAX_RULES && num > 0)
2583 _rule_set_mode(num, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "menuitem_mode")));
2585 c->view_rule = -1;
2587}
2588
2589static void menuitem_clear(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
2590{
2591 const int active = _rules_count();
2593 if(active > 1)
2594 {
2595 _rules_set_count(active - 1);
2596 if(c->active_rule >= active - 1) c->active_rule = active - 2;
2597 }
2598 else
2599 {
2602 _rule_set_string(0, "");
2603 dr->typing = FALSE;
2604 }
2605 // shift the rules below the removed one up by one
2606 for(int i = dr->num; i < MAX_RULES - 1; i++)
2607 {
2608 gchar *string = _rule_get_string(i + 1);
2609 if(string)
2610 {
2613 _rule_set_string(i, string);
2614 dt_free(string);
2615 }
2616 }
2617 c->view_rule = -1;
2619}
2620
2621static gboolean popup_button_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_collect_rule_t *dr)
2622{
2623 if(event->button != 1) return FALSE;
2624
2625 GtkWidget *menu = gtk_menu_new();
2626 GtkWidget *mi;
2627 const int active = _rules_count();
2628
2629 mi = gtk_menu_item_new_with_label(_("clear this rule"));
2630 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2631 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_clear), dr);
2632
2633 if(dr->num == active - 1)
2634 {
2635 mi = gtk_menu_item_new_with_label(_("narrow down search"));
2636 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND));
2637 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2638 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2639
2640 mi = gtk_menu_item_new_with_label(_("add more images"));
2641 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_OR));
2642 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2643 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2644
2645 mi = gtk_menu_item_new_with_label(_("exclude images"));
2646 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND_NOT));
2647 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2648 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode), dr);
2649 }
2650 else if(dr->num < active - 1)
2651 {
2652 mi = gtk_menu_item_new_with_label(_("change to: and"));
2653 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND));
2654 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2655 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2656
2657 mi = gtk_menu_item_new_with_label(_("change to: or"));
2658 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_OR));
2659 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2660 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2661
2662 mi = gtk_menu_item_new_with_label(_("change to: except"));
2663 g_object_set_data(G_OBJECT(mi), "menuitem_mode", GINT_TO_POINTER(DT_LIB_COLLECT_MODE_AND_NOT));
2664 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
2665 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(menuitem_mode_change), dr);
2666 }
2667
2668 gtk_widget_show_all(GTK_WIDGET(menu));
2669 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2670 return TRUE;
2671}
2672
2673// =====================================================================================
2674// Section 9 — tab configuration (one shared value view, reconfigured per tab)
2675// =====================================================================================
2676
2677static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self);
2678
2679static void _combo_as_view_toggle(GtkWidget *combo) // Folders: List / Tree
2680{
2682 dt_bauhaus_widget_set_label(combo, _("View"));
2685 GUINT_TO_POINTER(DT_COLLECTION_PROP_FILMROLL + 1), NULL, TRUE);
2687 GUINT_TO_POINTER(DT_COLLECTION_PROP_FOLDERS + 1), NULL, TRUE);
2688}
2689
2690static void _combo_as_collections(GtkWidget *combo) // Collections: tags only
2691{
2693 dt_bauhaus_widget_set_label(combo, _("View"));
2696 GUINT_TO_POINTER(DT_COLLECTION_PROP_TAG + 1), NULL, TRUE);
2697}
2698
2699static void _combo_as_full(GtkWidget *combo) // Queries: every property
2700{
2702 dt_bauhaus_widget_set_label(combo, NULL);
2705}
2706
2707static void _set_rule_button(dt_lib_collect_rule_t *dr, gboolean last, gboolean active)
2708{
2709 if(last)
2710 {
2711 gtk_button_set_label(GTK_BUTTON(dr->button), "-");
2712 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule"));
2713 }
2714 else if(active)
2715 {
2716 gtk_button_set_label(GTK_BUTTON(dr->button), "+");
2717 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule or add new rules"));
2718 }
2719 else
2720 {
2721 const int mode = _rule_get_mode(dr->num + 1);
2722 gtk_button_set_label(GTK_BUTTON(dr->button), mode == DT_LIB_COLLECT_MODE_AND ? _("AND")
2723 : mode == DT_LIB_COLLECT_MODE_OR ? _("OR")
2724 : _("AND NOT"));
2725 gtk_widget_set_tooltip_text(GTK_WIDGET(dr->button), _("clear this rule"));
2726 }
2727}
2728
2730{
2731 for(int i = 0; i < MAX_RULES; i++)
2732 {
2733 gtk_widget_set_no_show_all(d->rule[i].hbox, TRUE);
2734 gtk_widget_hide(d->rule[i].hbox);
2735 }
2736 gtk_widget_set_no_show_all(d->folders_controls, TRUE);
2737 gtk_widget_hide(d->folders_controls);
2738 gtk_widget_set_no_show_all(d->collections_controls, TRUE);
2739 gtk_widget_hide(d->collections_controls);
2740 gtk_widget_set_no_show_all(d->raw_box, TRUE);
2741 gtk_widget_hide(d->raw_box);
2742 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE);
2743 gtk_widget_show(GTK_WIDGET(d->view));
2744}
2745
2747{
2750
2751 if(tab == TAB_FOLDERS)
2752 {
2754 int item = _rule_get_item(0);
2755 if(!item_is_folder(item))
2756 {
2757 // Coming from a different property family: restore the last real folder selection if
2758 // nothing else has actually been committed since (has_last_folders), instead of always
2759 // resetting to an empty search -- merely visiting another tab doesn't count as a change.
2760 if(d->has_last_folders)
2761 {
2762 item = d->last_folders_item;
2763 _rule_set_item(0, item);
2764 _rule_set_string(0, d->last_folders_string ? d->last_folders_string : "");
2765 _rule_set_recursive(0, d->last_folders_recursive);
2766 }
2767 else
2768 {
2769 _rule_set_string(0, "");
2771 _rule_set_item(0, item);
2772 }
2773 }
2774 _combo_as_view_toggle(d->rule[0].combo);
2775 _combo_set_active_collection(d->rule[0].combo, item);
2776 get_properties(&d->rule[0]);
2777 gtk_widget_set_no_show_all(d->rule[0].hbox, FALSE);
2778 gtk_widget_show_all(d->rule[0].hbox);
2779 gtk_widget_show(d->rule[0].combo);
2780 gtk_widget_hide(d->rule[0].button); // adding rules only makes sense on the Queries tab
2781 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[0].text), _("Search a folder..."));
2782 _set_tooltip(&d->rule[0]);
2783 _update_op_combo(&d->rule[0]);
2784
2785 gtk_widget_set_no_show_all(d->folders_controls, FALSE);
2786 gtk_widget_show_all(d->folders_controls);
2787 gtk_widget_set_visible(d->recursive_check, item == DT_COLLECTION_PROP_FOLDERS);
2788 const gboolean desc = dt_conf_get_bool("plugins/collect/descending");
2789 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->sort_dir), desc);
2791 desc ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP, NULL);
2793 d->sort_by, g_strcmp0(dt_conf_get_string_const("plugins/collect/filmroll_sort"), "id") == 0 ? 1 : 0);
2794 // "sort by name/id" and "folder levels" only affect the flat film-roll List; the folder
2795 // Tree is always path-sorted and shows full paths, so hide them there for consistency (TODO).
2796 gtk_widget_set_visible(d->sort_by, item == DT_COLLECTION_PROP_FILMROLL);
2797 gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->folder_levels),
2798 CLAMP(dt_conf_get_int("show_folder_levels"), 1, 5));
2799 gtk_widget_set_visible(d->folder_levels, item == DT_COLLECTION_PROP_FILMROLL);
2800 // Restore the checkbox from its own persisted flag (get_properties() above already ran any
2801 // pending legacy-'*' migration for this rule). If this fires "toggled" (old GTK state !=
2802 // restored value), _recursive_toggled() just writes the same value straight back -- harmless,
2803 // not a real feedback loop, since it always re-derives from the checkbox's now-correct state.
2804 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->recursive_check), _rule_get_recursive(0));
2805 d->active_rule = 0;
2806 }
2807 else if(tab == TAB_COLLECTIONS)
2808 {
2812 _combo_as_collections(d->rule[0].combo);
2814 get_properties(&d->rule[0]);
2815 gtk_widget_set_no_show_all(d->rule[0].hbox, FALSE);
2816 gtk_widget_show_all(d->rule[0].hbox);
2817 gtk_widget_hide(d->rule[0].combo); // single option, no need to show it
2818 gtk_widget_hide(d->rule[0].button); // adding rules only makes sense on the Queries tab
2819 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[0].text), _("Search a collection..."));
2820 _set_tooltip(&d->rule[0]);
2821 _update_op_combo(&d->rule[0]);
2822
2823 gtk_widget_set_no_show_all(d->collections_controls, FALSE);
2824 gtk_widget_show_all(d->collections_controls);
2825 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->no_uncategorized),
2826 dt_conf_get_bool("plugins/lighttable/tagging/no_uncategorized"));
2827 d->active_rule = 0;
2828 }
2829 else // TAB_QUERIES
2830 {
2832 const gboolean raw = (_rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2833 gtk_widget_set_no_show_all(d->raw_box, FALSE);
2834 gtk_widget_show_all(d->raw_box);
2835 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->raw_check), raw);
2836
2837 if(raw)
2838 {
2839 gchar *s = _rule_get_string(0);
2840 gtk_entry_set_text(GTK_ENTRY(d->raw_entry), s ? s : "");
2841 dt_free(s);
2842 gtk_widget_show(d->raw_entry);
2843 gtk_widget_set_no_show_all(GTK_WIDGET(d->view), TRUE);
2844 gtk_widget_hide(GTK_WIDGET(d->view));
2845 d->active_rule = 0;
2846 }
2847 else
2848 {
2849 gtk_widget_hide(d->raw_entry);
2850 for(int i = 0; i <= d->active_rule; i++)
2851 {
2852 _combo_as_full(d->rule[i].combo);
2853 get_properties(&d->rule[i]);
2854 gtk_widget_set_no_show_all(d->rule[i].hbox, FALSE);
2855 gtk_widget_show_all(d->rule[i].hbox);
2856 gtk_widget_show(d->rule[i].combo);
2857 gtk_widget_show(d->rule[i].button); // rule +/- management, Queries tab only
2858 _set_rule_button(&d->rule[i], i == MAX_RULES - 1, i == d->active_rule);
2859 gtk_entry_set_placeholder_text(GTK_ENTRY(d->rule[i].text), _("Search..."));
2860 _set_tooltip(&d->rule[i]);
2861 _update_op_combo(&d->rule[i]);
2862 }
2863 }
2864 }
2866}
2867
2868static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
2869{
2870 if(dt_gui_widgets_suppressed()) return;
2871 if(gtk_toggle_button_get_active(b))
2872 {
2875 _rule_set_string(0, gtk_entry_get_text(GTK_ENTRY(d->raw_entry)));
2876 d->active_rule = 0;
2877 }
2879 {
2881 _rule_set_string(0, "");
2882 }
2883 d->view_rule = -1;
2885 if(!gtk_toggle_button_get_active(b)) update_view(get_active_rule(d)); // raw mode has no value list
2886 _commit_quiet();
2887}
2888
2889static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self)
2890{
2892 dt_conf_set_int("plugins/lighttable/collect/tab", page_num);
2893 if(page_num == TAB_FOLDERS || page_num == TAB_COLLECTIONS) _rules_set_count(1);
2894
2895 const gboolean raw = (page_num == TAB_QUERIES && _rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2896 _configure_tab(d, page_num);
2897 d->view_rule = -1;
2898 if(!raw)
2899 {
2900 // _configure_tab kept the query whenever the destination field is compatible; unfold the tree
2901 // to it (a no-op when the field was incompatible and the entry was cleared)
2904 }
2905 // raw SQL mode has no value list
2906 // NB: switching tabs only reconfigures the GUI and rebuilds the value list; it must NOT
2907 // re-run the collection query (that rebuilds the whole lighttable and was the slow path).
2908 // The collection updates when the user actually clicks a value or edits a rule.
2909}
2910
2912{
2914 if(d->view_rule != -1) return; // nothing changed since the last build
2915
2918
2919 dt_collect_tab_t tab;
2920 if(item_is_folder(_rule_get_item(0)) && d->nb_rules == 1)
2921 tab = TAB_FOLDERS;
2922 else if(item_is_tag(_rule_get_item(0)) && d->nb_rules == 1)
2923 tab = TAB_COLLECTIONS;
2924 else
2925 tab = TAB_QUERIES;
2926 dt_conf_set_int("plugins/lighttable/collect/tab", tab);
2927
2928 g_signal_handlers_block_matched(d->notebook, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _on_tab_switch, NULL);
2929 gtk_notebook_set_current_page(GTK_NOTEBOOK(d->notebook), tab);
2930 g_signal_handlers_unblock_matched(d->notebook, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _on_tab_switch, NULL);
2931
2932 const gboolean raw = (tab == TAB_QUERIES && _rule_get_item(0) == DT_COLLECTION_PROP_QUERY);
2933 _configure_tab(d, tab);
2934 if(!raw) update_view(get_active_rule(d)); // raw SQL mode has no value list
2936}
2937
2938// =====================================================================================
2939// Section 10 — signals
2940// =====================================================================================
2941
2942static void collection_updated(gpointer instance, dt_collection_change_t query_change,
2943 dt_collection_properties_t changed_property, gpointer imgs, int next, gpointer self)
2944{
2945 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2946 d->view_rule = -1;
2948
2949 // On a pure reload (no query change) only rebuild if a property we display actually changed.
2950 gboolean refresh = TRUE;
2951 if(query_change == DT_COLLECTION_CHANGE_RELOAD && changed_property != DT_COLLECTION_PROP_UNDEF)
2952 {
2953 refresh = FALSE;
2954 for(int i = 0; i <= d->active_rule; i++)
2955 if(_combo_get_active_collection(d->rule[i].combo) == changed_property)
2956 {
2957 refresh = TRUE;
2958 break;
2959 }
2960 }
2961 if(refresh) _lib_collect_gui_update(self);
2962}
2963
2964static void filmrolls_updated(gpointer instance, gpointer self)
2965{
2966 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2967 d->view_rule = -1;
2969}
2970
2971static void filmrolls_removed(gpointer instance, gpointer self)
2972{
2973 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2974 d->view_rule = -1;
2977}
2978
2983
2984static void tag_changed(gpointer instance, gpointer self)
2985{
2986 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
2987 gboolean uses_tag = FALSE;
2988 for(int i = 0; i < d->nb_rules; i++)
2990 {
2991 uses_tag = TRUE;
2992 break;
2993 }
2994
2995 d->view_rule = -1;
2997 if(uses_tag)
2998 {
3004 }
3006}
3007
3008static void geotag_changed(gpointer instance, GList *imgs, const int locid, gpointer self)
3009{
3010 if(locid) return; // not our concern
3011 dt_lib_collect_t *d = (dt_lib_collect_t *)((dt_lib_module_t *)self)->data;
3013 {
3014 d->view_rule = -1;
3020 NULL);
3023 }
3024}
3025
3040
3041#ifdef _WIN32
3042static void _mount_changed(GVolumeMonitor *volume_monitor, GMount *mount, dt_lib_module_t *self)
3043#else
3044static void _mount_changed(GUnixMountMonitor *monitor, dt_lib_module_t *self)
3045#endif
3046{
3050 {
3051 d->view_rule = -1;
3053 }
3054}
3055
3056// =====================================================================================
3057// Section 11 — preferences popup, construction & teardown
3058// =====================================================================================
3059
3060// NB: the old hidden "preferences..." popup has been retired (TODO). Every collect setting it
3061// exposed now lives in the front widget: filmroll_sort / descending / folder_levels on the
3062// Folders tab, no_uncategorized on the Collections tab.
3063
3065{
3066 dt_lib_collect_t *d = (dt_lib_collect_t *)calloc(1, sizeof(dt_lib_collect_t));
3067 self->data = (void *)d;
3068 self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3069
3070 d->active_rule = 0;
3071 d->nb_rules = 0;
3072 d->view_rule = -1;
3073 d->params = (dt_lib_collect_params_t *)malloc(sizeof(dt_lib_collect_params_t));
3074
3075 // notebook: only its tab-bar is used; the content below is shared and reconfigured per tab
3076 d->notebook = GTK_WIDGET(dt_ui_notebook_new());
3077 dt_gui_add_class(d->notebook, "empty");
3078 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Folders"), _("Browse and manage the folders known to Ansel"));
3079 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Collections"), _("Browse and manage tags"));
3080 dt_ui_notebook_page(GTK_NOTEBOOK(d->notebook), _("Queries"), _("Build arbitrary collections"));
3081 gtk_widget_show_all(d->notebook);
3082 gtk_box_pack_start(GTK_BOX(self->widget), d->notebook, TRUE, TRUE, 0);
3083 gtk_notebook_set_scrollable(GTK_NOTEBOOK(d->notebook), TRUE);
3084 g_signal_connect(G_OBJECT(d->notebook), "switch_page", G_CALLBACK(_on_tab_switch), self);
3085
3086 // one rule row per possible rule (only the relevant ones are shown per tab)
3087 for(int i = 0; i < MAX_RULES; i++)
3088 {
3089 d->rule[i].num = i;
3090 d->rule[i].typing = FALSE;
3091 d->rule[i].lib_collect = (void *)d;
3092
3093 GtkBox *box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
3094 d->rule[i].hbox = GTK_WIDGET(box);
3095 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
3096 gtk_widget_set_name(GTK_WIDGET(box), "lib-dtbutton");
3097
3100 _populate_collect_combo(d->rule[i].combo);
3101 g_signal_connect(G_OBJECT(d->rule[i].combo), "value-changed", G_CALLBACK(combo_changed), d->rule + i);
3102 gtk_box_pack_start(box, d->rule[i].combo, FALSE, FALSE, 0);
3103
3104 GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING));
3105 gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, FALSE, 0);
3106
3107 // comparison-operator selector, shown only for numeric/date/rating properties
3108 d->rule[i].op_combo = gtk_combo_box_text_new();
3109 for(int o = 0; o < COLLECT_N_OPS; o++)
3110 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(d->rule[i].op_combo), OP_LABELS[o]);
3111 gtk_combo_box_set_active(GTK_COMBO_BOX(d->rule[i].op_combo), 0);
3112 gtk_widget_set_no_show_all(d->rule[i].op_combo, TRUE);
3113 gtk_widget_set_tooltip_text(d->rule[i].op_combo, _("comparison operator"));
3114 g_signal_connect(G_OBJECT(d->rule[i].op_combo), "changed", G_CALLBACK(_op_changed), d->rule + i);
3115 gtk_box_pack_start(hbox, d->rule[i].op_combo, FALSE, FALSE, 0);
3116
3117 GtkWidget *w = gtk_search_entry_new();
3119 d->rule[i].text = w;
3120 gtk_widget_add_events(w, GDK_FOCUS_CHANGE_MASK | GDK_KEY_PRESS_MASK);
3121 gtk_entry_set_placeholder_text(GTK_ENTRY(w), _("Search..."));
3122 g_signal_connect(G_OBJECT(w), "focus-in-event", G_CALLBACK(_entry_focus_in), d->rule + i);
3123 g_signal_connect(G_OBJECT(w), "changed", G_CALLBACK(entry_changed), d->rule + i);
3124 g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(entry_activated), d->rule + i);
3125 gtk_widget_set_name(GTK_WIDGET(w), "lib-collect-entry");
3126 gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
3127 gtk_entry_set_width_chars(GTK_ENTRY(w), 5);
3128
3129 d->rule[i].button = gtk_button_new();
3130 gtk_widget_set_events(d->rule[i].button, GDK_BUTTON_PRESS_MASK);
3131 g_signal_connect(G_OBJECT(d->rule[i].button), "button-press-event", G_CALLBACK(popup_button_callback),
3132 d->rule + i);
3133 gtk_box_pack_start(hbox, d->rule[i].button, FALSE, FALSE, 0);
3134 }
3135
3136 // Folders inline controls (sort + recursion), shown only on the Folders tab
3137 d->folders_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3138 d->recursive_check = gtk_check_button_new_with_label(_("include sub-folders"));
3139 // Real value comes from _configure_tab() -> _rule_get_recursive(0) before first paint (the
3140 // startup _lib_collect_gui_update() call below always runs it); this is just a harmless seed.
3141 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->recursive_check), FALSE);
3142 g_signal_connect(G_OBJECT(d->recursive_check), "toggled", G_CALLBACK(_recursive_toggled), d);
3143 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->recursive_check, FALSE, FALSE, 0);
3144
3146 dt_bauhaus_widget_set_label(d->sort_by, _("sort by"));
3147 dt_bauhaus_combobox_add(d->sort_by, _("name"));
3148 dt_bauhaus_combobox_add(d->sort_by, _("id"));
3149 // bauhaus widgets render at their own (short) natural height; in this horizontal row they would
3150 // otherwise stick to the top and sit higher than the native spin/toggle siblings, so center them
3151 gtk_widget_set_valign(d->sort_by, GTK_ALIGN_CENTER);
3152 g_signal_connect(G_OBJECT(d->sort_by), "value-changed", G_CALLBACK(_sort_by_changed), d);
3153 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->sort_by, TRUE, TRUE, 0);
3154
3156 dt_gui_add_class(d->sort_dir, "dt_ignore_fg_state");
3157 gtk_widget_set_tooltip_text(d->sort_dir, _("toggle ascending / descending order"));
3158 g_signal_connect(G_OBJECT(d->sort_dir), "toggled", G_CALLBACK(_sort_dir_toggled), d);
3159 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->sort_dir, FALSE, FALSE, 0);
3160
3161 d->folder_levels = gtk_spin_button_new_with_range(1, 5, 1);
3162 gtk_widget_set_tooltip_text(d->folder_levels,
3163 _("number of folder levels to show in film-roll names, from the right"));
3164 g_signal_connect(G_OBJECT(d->folder_levels), "value-changed", G_CALLBACK(_folder_levels_changed), d);
3165 gtk_box_pack_start(GTK_BOX(d->folders_controls), d->folder_levels, FALSE, FALSE, 0);
3166 gtk_box_pack_start(GTK_BOX(self->widget), d->folders_controls, FALSE, FALSE, 0);
3167
3168 // Collections inline controls, shown only on the Collections tab
3169 d->collections_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3170 d->no_uncategorized = gtk_check_button_new_with_label(_("no 'uncategorized' group"));
3171 gtk_widget_set_tooltip_text(d->no_uncategorized,
3172 _("do not group childless tags under an 'uncategorized' entry"));
3173 g_signal_connect(G_OBJECT(d->no_uncategorized), "toggled", G_CALLBACK(_no_uncategorized_toggled), d);
3174 gtk_box_pack_start(GTK_BOX(d->collections_controls), d->no_uncategorized, FALSE, FALSE, 0);
3175 gtk_box_pack_start(GTK_BOX(self->widget), d->collections_controls, FALSE, FALSE, 0);
3176
3177 // Queries raw-SQL escape, shown only on the Queries tab
3178 d->raw_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3179 d->raw_check = gtk_check_button_new_with_label(_("edit as raw SQL"));
3180 g_signal_connect(G_OBJECT(d->raw_check), "toggled", G_CALLBACK(_raw_toggled), d);
3181 gtk_box_pack_start(GTK_BOX(d->raw_box), d->raw_check, FALSE, FALSE, 0);
3182 d->raw_entry = gtk_entry_new();
3183 gtk_entry_set_placeholder_text(GTK_ENTRY(d->raw_entry),
3184 _("SQL WHERE expression, e.g. iso > 800 AND lens LIKE '%50mm%'"));
3186 g_signal_connect(G_OBJECT(d->raw_entry), "activate", G_CALLBACK(_raw_entry_activated), d);
3187 gtk_box_pack_start(GTK_BOX(d->raw_box), d->raw_entry, FALSE, FALSE, 0);
3188 gtk_box_pack_start(GTK_BOX(self->widget), d->raw_box, FALSE, FALSE, 0);
3189
3190 // shared value view
3191 GtkTreeView *view = GTK_TREE_VIEW(gtk_tree_view_new());
3192 d->view = view;
3193 gtk_tree_view_set_headers_visible(view, FALSE);
3194 gtk_tree_view_set_activate_on_single_click(view, TRUE);
3195 gtk_widget_set_can_focus(GTK_WIDGET(view), TRUE);
3196 g_signal_connect(G_OBJECT(view), "button-press-event", G_CALLBACK(_view_button_pressed), d);
3197 g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(_view_popup_menu), d);
3198 g_signal_connect(G_OBJECT(view), "row-activated", G_CALLBACK(_view_row_activated), d);
3199 g_signal_connect(G_OBJECT(view), "row-expanded", G_CALLBACK(_view_row_expanded), d);
3200
3201 // accept images dragged from the lighttable thumbtable: drop on a folder row to move the
3202 // files there, on a tag row to attach the tag (handled in _view_drag_data_received). Motion
3203 // and highlighting are driven manually (_view_drag_motion/_view_drag_leave) so only the row
3204 // under the pointer gets the native drop highlight, not the whole widget.
3205 gtk_drag_dest_set(GTK_WIDGET(view), GTK_DEST_DEFAULT_DROP, target_list_internal, n_targets_internal,
3206 GDK_ACTION_MOVE);
3207 g_signal_connect(G_OBJECT(view), "drag-motion", G_CALLBACK(_view_drag_motion), d);
3208 g_signal_connect(G_OBJECT(view), "drag-leave", G_CALLBACK(_view_drag_leave), d);
3209 g_signal_connect(G_OBJECT(view), "drag-data-received", G_CALLBACK(_view_drag_data_received), d);
3210
3211 GtkTreeViewColumn *col = gtk_tree_view_column_new();
3212 gtk_tree_view_append_column(view, col);
3213 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
3214 gtk_tree_view_column_pack_start(col, renderer, TRUE);
3215 gtk_tree_view_column_set_cell_data_func(col, renderer, tree_count_show, NULL, NULL);
3216 gtk_tree_view_column_add_attribute(col, renderer, "weight", DT_LIB_COLLECT_COL_FONT);
3217 g_object_set(renderer, "strikethrough", TRUE, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
3218 gtk_tree_view_column_add_attribute(col, renderer, "strikethrough-set", DT_LIB_COLLECT_COL_UNREACHABLE);
3219
3220 GtkTreeModel *listmodel = GTK_TREE_MODEL(
3221 gtk_list_store_new(DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING,
3222 G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INT));
3223 d->listfilter = gtk_tree_model_filter_new(listmodel, NULL);
3224 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(d->listfilter), DT_LIB_COLLECT_COL_VISIBLE);
3225
3226 GtkTreeModel *treemodel = GTK_TREE_MODEL(
3227 gtk_tree_store_new(DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING,
3228 G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INT));
3229 d->treefilter = gtk_tree_model_filter_new(treemodel, NULL);
3230 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(d->treefilter), DT_LIB_COLLECT_COL_VISIBLE);
3231 g_object_unref(treemodel);
3232
3233 // Static height: the collection list refreshes on selection/act-on, so a fixed user-set size keeps
3234 // the side-panel layout from jumping. Defaults to ~200px until the user drags the grip.
3235 gtk_box_pack_start(GTK_BOX(self->widget),
3236 dt_ui_scroll_wrap(GTK_WIDGET(view), 200, "plugins/lighttable/collect/windowheight",
3238 TRUE, TRUE, 0);
3239
3240 // proxy used by other code to force a refresh
3243
3245
3246#ifdef _WIN32
3247 d->vmonitor = g_volume_monitor_get();
3248 g_signal_connect(G_OBJECT(d->vmonitor), "mount-changed", G_CALLBACK(_mount_changed), self);
3249 g_signal_connect(G_OBJECT(d->vmonitor), "mount-added", G_CALLBACK(_mount_changed), self);
3250#else
3251 d->vmonitor = g_unix_mount_monitor_get();
3252 g_signal_connect(G_OBJECT(d->vmonitor), "mounts-changed", G_CALLBACK(_mount_changed), self);
3253#endif
3254
3256 self);
3258 self);
3260 self);
3262 self);
3266 self);
3267}
3268
3270{
3271 if(IS_NULL_PTR(self->data)) return;
3273
3282
3283 dt_free(d->params);
3284 dt_free(d->last_folders_string);
3285 g_object_unref(d->treefilter);
3286 g_object_unref(d->listfilter);
3287 g_object_unref(d->vmonitor);
3288 dt_free(self->data);
3289}
3290
3291// clang-format off
3292// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3293// vim: shiftwidth=2 expandtab tabstop=2 cindent
3294// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3295// 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:2192
void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
Definition bauhaus.c:2095
gboolean dt_bauhaus_combobox_set_from_value(GtkWidget *widget, int value)
Definition bauhaus.c:2333
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2350
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:2041
gpointer dt_bauhaus_combobox_get_data(GtkWidget *widget)
Definition bauhaus.c:2182
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2304
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1656
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1845
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:2019
@ 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:566
static gboolean _confirm(const char *title, const char *message)
Definition collect.c:1855
void gui_reset(dt_lib_module_t *self)
Definition collect.c:700
int set_params(dt_lib_module_t *self, const void *params, int size)
Definition collect.c:682
static void _set_tooltip(dt_lib_collect_rule_t *dr)
Definition collect.c:2345
static void _hide_all_widgets(dt_lib_collect_t *d)
Definition collect.c:2729
static gboolean tree_expand(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1099
static void metadata_changed(gpointer instance, int type, gpointer self)
Definition collect.c:3026
static void _folder_levels_changed(GtkWidget *spin, dt_lib_collect_t *d)
Definition collect.c:2535
void * get_params(dt_lib_module_t *self, int *size)
Definition collect.c:673
static void _collect_expanded_cb(GtkTreeView *view, GtkTreePath *path, gpointer data)
Definition collect.c:1204
static int _rule_get_mode(int n)
Definition collect.c:486
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:2269
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:393
static void _populate_collect_combo(GtkWidget *w)
Definition collect.c:730
static void _mount_changed(GUnixMountMonitor *monitor, dt_lib_module_t *self)
Definition collect.c:3044
static void geotag_changed(gpointer instance, GList *imgs, const int locid, gpointer self)
Definition collect.c:3008
#define PARAM_STRING_SIZE
Definition collect.c:163
static const char * UNCATEGORIZED_TAG
Definition collect.c:1201
static void _free_row(gpointer p)
Definition collect.c:1804
static int _rules_count()
Definition collect.c:462
static GList * _collect_sorted_tree_names(int property, int rule)
Definition collect.c:1290
#define COLLECT_N_OPS
Definition collect.c:327
static void menuitem_clear(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2589
static void get_number_of_rules(dt_lib_collect_t *d)
Definition collect.c:641
static void _view_drag_leave(GtkWidget *widget, GdkDragContext *context, guint time, dt_lib_collect_t *d)
Definition collect.c:2264
static void _action_activate(GtkMenuItem *mi, dt_lib_collect_t *d)
Definition collect.c:2100
static void _sort_dir_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2516
static void _on_tab_switch(GtkNotebook *nb, GtkWidget *page, guint page_num, dt_lib_module_t *self)
Definition collect.c:2889
static void _raw_entry_activated(GtkWidget *entry, dt_lib_collect_t *d)
Definition collect.c:2551
static void _raw_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2868
static char ** _split_tree_name(int property, const char *name)
Definition collect.c:1234
static void _configure_tab(dt_lib_collect_t *d, dt_collect_tab_t tab)
Definition collect.c:2746
static void _view_row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, dt_lib_collect_t *d)
Definition collect.c:2322
static void _rule_set_mode(int n, int mode)
Definition collect.c:493
static int32_t _prerender_job(dt_job_t *job)
Definition collect.c:2013
static void _split_operator(const char *text, int *op_idx, const char **value)
Definition collect.c:330
static gboolean item_is_tree(int item)
Definition collect.c:317
static gboolean _drop_move_to_folder(dt_lib_collect_t *d, const char *folder, GList *imgs)
Definition collect.c:2143
static void tag_changed(gpointer instance, gpointer self)
Definition collect.c:2984
static gboolean _view_button_pressed(GtkWidget *treeview, GdkEventButton *event, dt_lib_collect_t *d)
Definition collect.c:2286
static gboolean _en_tags(int property, int n)
Definition collect.c:2071
static void _build_tree_store(GtkTreeStore *store, int property, GList *sorted_names, gboolean no_uncategorized, const char *format_separator)
Definition collect.c:1327
static gboolean list_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:873
static int string_array_length(char **list)
Definition collect.c:781
static gboolean item_is_tag(int item)
Definition collect.c:304
static gboolean _view_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, dt_lib_collect_t *d)
Definition collect.c:2244
static void _lib_collect_gui_update(dt_lib_module_t *self)
Definition collect.c:2911
static gboolean _rule_get_recursive(int n)
Definition collect.c:500
static void _force_refresh(dt_lib_collect_t *d)
Definition collect.c:1791
static void _op_changed(GtkWidget *w, dt_lib_collect_rule_t *dr)
Definition collect.c:2394
static gboolean _view_popup_menu(GtkWidget *treeview, dt_lib_collect_t *d)
Definition collect.c:2315
static void _combo_as_view_toggle(GtkWidget *combo)
Definition collect.c:2679
static gboolean tree_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:973
void gui_cleanup(dt_lib_module_t *self)
Definition collect.c:3269
static void _populate_list(dt_lib_collect_rule_t *dr)
Definition collect.c:1536
static void row_activated(GtkTreeView *view, GtkTreePath *path, GdkEventButton *event, dt_lib_collect_t *d)
Definition collect.c:1698
static void tree_count_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
Definition collect.c:855
static void _show_context_menu(dt_lib_collect_t *d, GdkEventButton *event)
Definition collect.c:2108
static gboolean _drop_attach_tag(dt_lib_collect_t *d, const char *tagpath, GList *imgs)
Definition collect.c:2180
static GList * _selected_rows(dt_lib_collect_t *d)
Definition collect.c:1811
static char * tag_collate_key(char *tag)
Definition collect.c:837
static const char *const OP_LABELS[]
Definition collect.c:326
static gboolean _do_drop(dt_lib_collect_t *d, GtkTreeView *tree, gint x, gint y, GtkSelectionData *sel)
Definition collect.c:2204
static void _act_tags_remove(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1958
static void _no_uncategorized_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2542
static gboolean _clear_suppress_row_activated(gpointer d)
Definition collect.c:2234
static gboolean _combo_set_active_collection(GtkWidget *combo, const int property)
Definition collect.c:723
static void _lib_collect_update_params(dt_lib_collect_t *d)
Definition collect.c:648
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:827
static void _rule_set_recursive(int n, gboolean recursive)
Definition collect.c:507
static void menuitem_mode_change(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2579
static void entry_activated(GtkWidget *entry, dt_lib_collect_rule_t *dr)
Definition collect.c:2465
static gboolean item_is_folder(int item)
Definition collect.c:299
void init_presets(dt_lib_module_t *self)
Definition collect.c:405
static void _rules_set_count(int n)
Definition collect.c:467
static GList * _rows_to_imgids(int property, GList *rows, gboolean recursive)
Definition collect.c:1832
static void _act_folders_remove(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1897
static void _combo_as_collections(GtkWidget *combo)
Definition collect.c:2690
static void _view_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, dt_lib_collect_t *d)
Definition collect.c:2337
static void _prerender_free(void *p)
Definition collect.c:2003
static dt_lib_collect_t * get_collect(dt_lib_collect_rule_t *r)
Definition collect.c:631
#define ADD_COLLECT_ENTRY(value)
uint32_t container(dt_lib_module_t *self)
Definition collect.c:383
static gboolean _entry_focus_in(GtkWidget *w, GdkEventFocus *event, dt_lib_collect_rule_t *dr)
Definition collect.c:2496
static int is_time_property(int property)
Definition collect.c:292
static dt_lib_module_t * _self()
Definition collect.c:1786
static gboolean _restore_expanded_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1223
static void set_properties(dt_lib_collect_rule_t *dr)
Definition collect.c:540
static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr)
Definition collect.c:2456
static void _act_tag_rename(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1980
static void filmrolls_removed(gpointer instance, gpointer self)
Definition collect.c:2971
static const collect_action_t ACTIONS[]
Definition collect.c:2092
static gboolean range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1072
static void menuitem_mode(GtkMenuItem *menuitem, dt_lib_collect_rule_t *dr)
Definition collect.c:2562
static int _rule_get_item(int n)
Definition collect.c:472
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:2942
static void preferences_changed(gpointer instance, gpointer self)
Definition collect.c:2979
static void _act_folders_relocate(dt_lib_collect_t *d, GList *rows)
Definition collect.c:1912
static void _rule_set_item(int n, int item)
Definition collect.c:479
static gboolean popup_button_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_collect_rule_t *dr)
Definition collect.c:2621
void gui_init(dt_lib_module_t *self)
Definition collect.c:3064
static GtkTreeModel * _create_filtered_model(GtkTreeModel *model, dt_lib_collect_rule_t *dr)
Definition collect.c:1190
int position()
Definition collect.c:388
static void _rule_set_string(int n, const char *s)
Definition collect.c:531
static void _commit_colllection()
Definition collect.c:600
const char ** views(dt_lib_module_t *self)
Definition collect.c:377
static gboolean _maybe_file_uncategorized(GtkTreeStore *store, const char *name, const char *next_name_raw, GtkTreeIter *uncategorized, guint *index, int count)
Definition collect.c:1259
static gboolean list_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1052
static gboolean _en_folders(int property, int n)
Definition collect.c:2056
static void combo_changed(GtkWidget *combo, dt_lib_collect_rule_t *dr)
Definition collect.c:2404
static void tree_set_visibility(GtkTreeModel *model, gpointer data)
Definition collect.c:1010
static void _set_rule_button(dt_lib_collect_rule_t *dr, gboolean last, gboolean active)
Definition collect.c:2707
static gchar * _rule_get_string(int n)
Definition collect.c:514
static gchar * _ask_text(const char *title, const char *initial)
Definition collect.c:1869
static dt_lib_collect_rule_t * get_active_rule(dt_lib_collect_t *d)
Definition collect.c:636
static void filmrolls_updated(gpointer instance, gpointer self)
Definition collect.c:2964
static void _strip_recursion_marker(gchar *needle, gchar *haystack, int property)
Definition collect.c:949
static void _act_prerender(dt_lib_collect_t *d, GList *rows)
Definition collect.c:2041
static gboolean _en_any(int property, int n)
Definition collect.c:2079
static void _commit_quiet()
Definition collect.c:622
static void _update_op_combo(dt_lib_collect_rule_t *dr)
Definition collect.c:2389
static void _recursive_toggled(GtkToggleButton *b, dt_lib_collect_t *d)
Definition collect.c:2504
static const char *const OP_TOKENS[]
Definition collect.c:325
static gboolean item_is_numeric(int item)
Definition collect.c:309
static GtkTreePath * _folders_root_collapse_path(GtkTreeModel *model)
Definition collect.c:1158
static gboolean tree_range_visible(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:1039
static gboolean tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition collect.c:996
static gboolean _en_tag_single(int property, int n)
Definition collect.c:2075
static void update_view(dt_lib_collect_rule_t *dr)
Definition collect.c:1641
static gboolean _en_folders_lighttable(int property, int n)
Definition collect.c:2066
static void free_tuple(gpointer data)
Definition collect.c:819
static void _propagate_count_to_ancestors(GtkTreeStore *store, GtkTreeIter *leaf, int count)
Definition collect.c:1244
static gboolean _adopt_tag_order(const char *text, int *order)
Definition collect.c:1677
static void _sort_by_changed(GtkWidget *combo, dt_lib_collect_t *d)
Definition collect.c:2527
#define CLEAR_PARAMS(r)
static void _combo_as_full(GtkWidget *combo)
Definition collect.c:2699
static char ** split_path(const char *path)
Definition collect.c:789
static guint64 _date_key(const char *s, char pad)
Definition collect.c:1020
#define MAX_RULES
Definition collect.c:162
static int _combo_get_active_collection(GtkWidget *combo)
Definition collect.c:718
static gchar * _decorate_hierarchy(gchar *text, GdkEventButton *event)
Definition collect.c:1658
static void _populate_tree(dt_lib_collect_rule_t *dr)
Definition collect.c:1407
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, gboolean recursive)
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:777
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:877
gboolean dt_control_remove_images()
uint32_t view(const dt_view_t *self)
Definition darkroom.c:228
darktable_t darktable
Definition darktable.c:183
#define dt_gui_freeze_begin()
Definition darktable.h:900
#define dt_gui_freeze_end()
Definition darktable.h:901
#define DT_MODULE(MODVER)
Definition darktable.h:140
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:485
#define dt_free(ptr)
Definition darktable.h:478
void void void gboolean dt_gui_widgets_suppressed(void)
Definition gtk.c:194
static const dt_aligned_pixel_simd_t value
Definition darktable.h:599
static gboolean dt_modifier_is(const GdkModifierType state, const GdkModifierType desired_modifier_mask)
Definition darktable.h:955
#define PATH_MAX
Definition darktable.h:1189
#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:293
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
static const GtkTargetEntry target_list_internal[]
static const guint n_targets_internal
@ DND_TARGET_IMGID
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:2470
GtkNotebook * dt_ui_notebook_new()
Definition gtk.c:2465
void dt_gui_refocus_center()
Definition gtk.c:3482
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:2945
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:3473
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:270
#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:502
void dt_control_signal_block_by_func(const struct dt_control_signal_t *ctlsig, GCallback cb, gpointer user_data)
Definition signal.c:497
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:387
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:366
@ 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:376
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
const float r
guint64 hi
Definition collect.c:1033
guint64 lo
Definition collect.c:1033
GHashTable * set
Definition collect.c:1220
dt_lib_collect_t * d
Definition collect.c:1219
gchar * start
Definition collect.c:265
gchar * stop
Definition collect.c:266
GtkTreePath * path1
Definition collect.c:267
GtkTreePath * path2
Definition collect.c:268
gboolean(* enabled)(int property, int n)
Definition collect.c:2088
gboolean multi
Definition collect.c:2087
void(* run)(dt_lib_collect_t *d, GList *rows)
Definition collect.c:2089
const char * label
Definition collect.c:2086
dt_mipmap_size_t max_size
Definition collect.c:2000
gchar * path
Definition collect.c:1800
struct dt_gui_gtk_t * gui
Definition darktable.h:803
struct dt_collection_t * collection
Definition darktable.h:809
struct dt_mipmap_cache_t * mipmap_cache
Definition darktable.h:804
struct dt_selection_t * selection
Definition darktable.h:810
struct dt_control_signal_t * signals
Definition darktable.h:802
struct dt_bauhaus_t * bauhaus
Definition darktable.h:806
struct dt_view_manager_t * view_manager
Definition darktable.h:800
struct dt_control_t * control
Definition darktable.h:801
int32_t id
Definition film.h:45
dt_ui_t * ui
Definition gtk.h:165
dt_lib_collect_params_rule_t rule[10]
Definition collect.c:260
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:220
struct dt_lib_collect_params_t * params
Definition collect.c:242
GtkWidget * collections_controls
Definition collect.c:234
GtkTreeModel * treefilter
Definition collect.c:215
GtkWidget * sort_by
Definition collect.c:222
dt_lib_collect_rule_t rule[10]
Definition collect.c:202
GtkTreeModel * listfilter
Definition collect.c:216
GtkWidget * raw_box
Definition collect.c:238
GtkWidget * notebook
Definition collect.c:203
GtkWidget * no_uncategorized
Definition collect.c:235
GtkWidget * folder_levels
Definition collect.c:223
gchar * last_folders_string
Definition collect.c:230
GtkWidget * raw_check
Definition collect.c:239
GUnixMountMonitor * vmonitor
Definition collect.c:246
gboolean has_last_folders
Definition collect.c:228
gboolean last_folders_recursive
Definition collect.c:231
GtkWidget * raw_entry
Definition collect.c:240
GtkWidget * sort_dir
Definition collect.c:221
int last_folders_item
Definition collect.c:229
GtkWidget * folders_controls
Definition collect.c:219
GtkTreeView * view
Definition collect.c:208
gboolean suppress_row_activated
Definition collect.c:213
char plugin_name[128]
Definition lib.h:82
GModule *void * data
Definition lib.h:80
GtkWidget * widget
Definition lib.h:84
dt_view_t * current_view
Definition view.h:202
struct dt_view_manager_t::@69 proxy
struct dt_lib_module_t *void(* update)(struct dt_lib_module_t *)
Definition view.h:239
struct dt_view_manager_t::@69::@71 module_collect
char * collate_key
Definition collect.c:815
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_VIEW_LIGHTTABLE
Definition view.h:77
@ DT_UI_CONTAINER_PANEL_LEFT_CENTER