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