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