Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
tagging.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2010-2011, 2013 Henrik Andersson.
4 Copyright (C) 2010-2011, 2013-2014 johannes hanika.
5 Copyright (C) 2010 Stuart Henderson.
6 Copyright (C) 2010-2017 Tobias Ellinghaus.
7 Copyright (C) 2011 Antony Dovgal.
8 Copyright (C) 2011, 2013 Jérémy Rosen.
9 Copyright (C) 2011 Robert Bieber.
10 Copyright (C) 2012 Richard Wonka.
11 Copyright (C) 2013 José Carlos García Sogo.
12 Copyright (C) 2013-2016 Roman Lebedev.
13 Copyright (C) 2013 Simon Spannagel.
14 Copyright (C) 2014 Pascal de Bruijn.
15 Copyright (C) 2017-2018, 2020 parafin.
16 Copyright (C) 2018 August Schwerdfeger.
17 Copyright (C) 2018, 2023 Maurizio Paglia.
18 Copyright (C) 2018 rawfiner.
19 Copyright (C) 2019 Alexis Mousset.
20 Copyright (C) 2019, 2022-2023, 2025 Aurélien PIERRE.
21 Copyright (C) 2019 Edgardo Hoszowski.
22 Copyright (C) 2019-2022 Pascal Obry.
23 Copyright (C) 2019-2022 Philippe Weyland.
24 Copyright (C) 2019 Sam Smith.
25 Copyright (C) 2020-2022 Aldric Renaudin.
26 Copyright (C) 2020-2022 Diederik Ter Rahe.
27 Copyright (C) 2020-2021 Hubert Kowalski.
28 Copyright (C) 2020 Marco.
29 Copyright (C) 2020-2021 Ralf Brown.
30 Copyright (C) 2021 Harald.
31 Copyright (C) 2021 luzpaz.
32 Copyright (C) 2021 Marco Carrarini.
33 Copyright (C) 2022 Martin Bařinka.
34 Copyright (C) 2022 Nicolas Auffray.
35 Copyright (C) 2026 Guillaume Stutin.
36
37 darktable is free software: you can redistribute it and/or modify
38 it under the terms of the GNU General Public License as published by
39 the Free Software Foundation, either version 3 of the License, or
40 (at your option) any later version.
41
42 darktable is distributed in the hope that it will be useful,
43 but WITHOUT ANY WARRANTY; without even the implied warranty of
44 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
45 GNU General Public License for more details.
46
47 You should have received a copy of the GNU General Public License
48 along with darktable. If not, see <http://www.gnu.org/licenses/>.
49*/
50#include "common/collection.h"
51#include "common/act_on.h"
53#include "common/selection.h"
54#include "system/macros.h"
55#include "system/mem_alloc.h"
57#include "control/signal.h"
58#include "common/utility.h"
59#include "widgets/gdkkeys.h"
60#include "metadata/tags.h"
61#include "common/conf.h"
62#include "control/control.h"
63#include "widgets/bauhaus.h"
64#include "widgets/button.h"
66
67#include "gui/application.h"
69#include "gui/drag_and_drop.h"
70#include "libs/lib.h"
72#include "libs/lib_api.h"
73#ifdef GDK_WINDOWING_QUARTZ
74#include "osx/osx.h"
75#endif
76#include <gdk/gdkkeysyms.h>
77#include "widgets/dialog.h"
78#include "widgets/label.h"
79#include "widgets/scroll_wrap.h"
80#include "widgets/widget_settings.h" // dt_gui_widget_freeze(), dt_gui_widgets_suppressed()
81
82#define FLOATING_ENTRY_WIDTH DT_PIXEL_APPLY_DPI(150)
83
84DT_MODULE(1)
85
86typedef struct dt_lib_tagging_t
87{
88 char keyword[1024];
89 GtkEntry *entry, *dict_entry;
91 GtkTreeView *attached_view, *dictionary_view;
92 GtkWidget *validate_button, *new_button, *import_button, *export_button;
93 GtkWidget *toggle_tree_button, *toggle_suggestion_button;
94 GtkWidget *attached_view_combo, *sort_combo, *dttags_check; // sidebar attached-list controls
95 GtkListStore *attached_liststore, *dictionary_liststore, *completion_store;
96 GtkTreeStore *attached_treestore, *dictionary_treestore;
97 GtkTreeModelFilter *dictionary_listfilter, *dictionary_treefilter;
98 GHashTable *collection_tags;
101 gboolean tree_flag, suggestion_flag, sort_count_flag, hide_path_flag, dttags_flag;
102 gboolean attached_tree_flag; // sidebar attached view: TRUE = tree, FALSE = list
104 char *last_tag;
105 struct
106 {
107 gchar *tagname;
108 GtkTreePath *path, *lastpath;
109 int expand_timeout, scroll_timeout, last_y;
110 gboolean root, tag_source;
111 } drag;
113
114typedef struct dt_tag_op_t
115{
116 gint tagid;
119 gboolean tree_flag;
121
134
141
142static void _save_last_tag_used(const char *tags, dt_lib_tagging_t *d);
144static void _size_recent_tags_list();
145static void _entry_clear_suppressed(GtkEntry *entry, dt_lib_module_t *self);
146static gboolean _enter_key_pressed(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self);
147static gboolean _lib_tagging_tag_redo_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
148 GdkModifierType mods, gpointer user_data);
149static gboolean _lib_tagging_tag_show_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
150 GdkModifierType mods, gpointer user_data);
151
152const char *name(struct dt_lib_module_t *self)
153{
154 return _("Tags");
155}
156
157const char **views(dt_lib_module_t *self)
158{
159 static const char *v[] = {"lighttable", "map", "studio_capture", NULL};
160 return v;
161}
162
164{
166}
167
168static gboolean _is_user_tag(GtkTreeModel *model, GtkTreeIter *iter)
169{
170 char *path;
171 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
172 const gboolean user_tag = !g_str_has_prefix(path, "darktable|") || g_str_has_prefix(path, "darktable|style|");
173 dt_free(path);
174 return user_tag;
175}
176
177static void _unselect_all_in_view(GtkTreeView *view)
178{
179 GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
180 gtk_tree_selection_unselect_all(selection);
181}
182
183// MULTIPLE-selection-safe replacement for gtk_tree_selection_get_selected():
184// returns the first selected row. gtk_tree_selection_get_selected() asserts
185// (Gtk-CRITICAL) on GTK_SELECTION_MULTIPLE views, which both our views now are.
186static gboolean _selection_get_first(GtkTreeSelection *selection, GtkTreeModel **model, GtkTreeIter *iter)
187{
188 GList *rows = gtk_tree_selection_get_selected_rows(selection, model);
189 gboolean found = FALSE;
190 if(rows)
191 {
192 found = gtk_tree_model_get_iter(*model, iter, (GtkTreePath *)rows->data);
193 g_list_free_full(rows, (GDestroyNotify)gtk_tree_path_free);
194 }
195 return found;
196}
197
198// kept as a hook for callers that want to refresh after a selection/act-on change;
199// the attach/detach buttons it used to drive are gone (tagging is done from the entry,
200// detaching from per-row icons or the right-click menu)
205
206static void _propagate_sel_to_parents(GtkTreeModel *model, GtkTreeIter *iter)
207{
208 guint sel;
209 GtkTreeIter parent, child = *iter;
210 while(gtk_tree_model_iter_parent(model, &parent, &child))
211 {
212 gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
213 if(sel == DT_TS_NO_IMAGE)
214 gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
216 child = parent;
217 }
218}
219
220static gboolean _set_matching_tag_visibility(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, dt_lib_module_t *self)
221{
223 gboolean visible;
224 gchar *tagname = NULL;
225 gchar *synonyms = NULL;
226 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &tagname, DT_LIB_TAGGING_COL_SYNONYM, &synonyms, -1);
227 if(!d->keyword[0])
228 visible = TRUE;
229 else
230 {
231 if(synonyms && synonyms[0]) tagname = dt_util_dstrcat(tagname, ", %s", synonyms);
232 gchar *haystack = g_utf8_strdown(tagname, -1);
233 gchar *needle = g_utf8_strdown(d->keyword, -1);
234 visible = (g_strrstr(haystack, needle) != NULL);
235 dt_free(haystack);
236 dt_free(needle);
237 }
238 if(d->tree_flag)
239 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_TAGGING_COL_VISIBLE, visible, -1);
240 else
241 gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_TAGGING_COL_VISIBLE, visible, -1);
242 dt_free(tagname);
243 dt_free(synonyms);
244 return FALSE;
245}
246
247static gboolean _tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
248{
249 gboolean state;
250 GtkTreeIter parent, child = *iter;
251
252 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_VISIBLE, &state, -1);
253 if(!state) return FALSE;
254
255 while(gtk_tree_model_iter_parent(model, &parent, &child))
256 {
257 gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_VISIBLE, &state, -1);
258 gtk_tree_store_set(GTK_TREE_STORE(model), &parent, DT_LIB_TAGGING_COL_VISIBLE, TRUE, -1);
259 child = parent;
260 }
261 return FALSE;
262}
263
264static void _sort_attached_list(dt_lib_module_t *self, gboolean force)
265{
267 if(d->attached_tree_flag)
268 {
269 // tree view: order by full path so the hierarchy lines up
270 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->attached_treestore), DT_TAG_SORT_PATH_ID, GTK_SORT_ASCENDING);
271 return;
272 }
273 if(force && d->sort_count_flag)
274 {
275 // ugly but when sorted by count _tree_tagname_show() is not triggered
276 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->attached_liststore), DT_TAG_SORT_NAME_ID, GTK_SORT_ASCENDING);
277 }
278 const gint sort = d->sort_count_flag ? DT_TAG_SORT_COUNT_ID : DT_TAG_SORT_NAME_ID;
279 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->attached_liststore), sort, GTK_SORT_ASCENDING);
280}
281
282static void _sort_dictionary_list(dt_lib_module_t *self, gboolean force)
283{
285 if(!d->tree_flag)
286 {
287 if(force && d->sort_count_flag)
288 {
289 // ugly but when sorted by count _tree_tagname_show() is not triggered
290 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_liststore), DT_TAG_SORT_NAME_ID, GTK_SORT_ASCENDING);
291 }
292 const gint sort = d->sort_count_flag ? DT_TAG_SORT_COUNT_ID : d->hide_path_flag ? DT_TAG_SORT_NAME_ID : DT_TAG_SORT_PATH_ID;
293 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_liststore), sort, GTK_SORT_ASCENDING);
294 }
295 else
296 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_treestore), DT_TAG_SORT_PATH_ID, GTK_SORT_ASCENDING);
297}
298
299// find a tag on the tree
300static gboolean _find_tag_iter_tagname(GtkTreeModel *model, GtkTreeIter *iter,
301 const char *tagname, const gboolean needle)
302{
303 gboolean found = FALSE;
304 if(IS_NULL_PTR(tagname)) return found;
305 char *path;
306 do
307 {
308 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
309 if(needle)
310 {
311 gchar *haystack = g_utf8_strdown(path, -1);
312 found = g_strstr_len(haystack, strlen(haystack), tagname) != NULL;
313 dt_free(haystack);
314 }
315 else
316 found = !g_strcmp0(tagname, path);
317 dt_free(path);
318 if(found) return found;
319 GtkTreeIter child, parent = *iter;
320 if(gtk_tree_model_iter_children(model, &child, &parent))
321 {
322 found = _find_tag_iter_tagname(model, &child, tagname, needle);
323 if(found)
324 {
325 *iter = child;
326 return found;
327 }
328 }
329 } while(gtk_tree_model_iter_next(model, iter));
330 return found;
331}
332
333static void _show_iter_on_view(GtkTreeView *view, GtkTreeIter *iter, const gboolean select)
334{
335 GtkTreeModel *model = gtk_tree_view_get_model(view);
336 GtkTreePath *path = gtk_tree_model_get_path(model, iter);
337 gtk_tree_view_expand_to_path(view, path);
338 gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.5, 0.5);
339 gtk_tree_path_free(path);
340 if(select)
341 {
342 GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
343 gtk_tree_selection_select_iter(selection, iter);
344 }
345}
346
347// make the tag visible on view
348static void _show_tag_on_view(GtkTreeView *view, const char *tagname,
349 const gboolean needle, const gboolean select)
350{
351 if(tagname)
352 {
353 char *lt = g_strdup(tagname);
354 char *t = g_strstrip(lt);
355 GtkTreeIter iter;
356 GtkTreeModel *model = gtk_tree_view_get_model(view);
357 if(gtk_tree_model_get_iter_first(model, &iter))
358 {
359 if(_find_tag_iter_tagname(model, &iter, t, needle))
360 _show_iter_on_view(view, &iter, select);
361 }
362 dt_free(lt);
363 }
364}
365
366static void _show_keyword_on_view(GtkTreeView *view, const char *keyword, const gboolean select)
367{
368 gchar *needle = g_utf8_strdown(keyword, -1);
369 _show_tag_on_view(view, needle, TRUE, select);
370 dt_free(needle);
371}
372
373static gboolean _select_previous_user_attached_tag(const int index, GtkTreeView *view)
374{
375 if(index == 0)
376 return FALSE;
377 GtkTreeIter iter;
378 GtkTreeModel *model = gtk_tree_view_get_model(view);
379 gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
380 int i = 0;
381 for(; valid && i < index -1; i++)
382 valid = gtk_tree_model_iter_next(model, &iter);
383 for(; valid; valid = gtk_tree_model_iter_previous(model, &iter))
384 {
385 if(_is_user_tag(model, &iter))
386 {
388 return TRUE;
389 }
390 }
391 return FALSE;
392}
393
394static gboolean _select_next_user_attached_tag(const int index, GtkTreeView *view)
395{
396 GtkTreeIter iter;
397 GtkTreeModel *model = gtk_tree_view_get_model(view);
398 gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
399 int i = 0;
400 for(; valid && i < index; i++)
401 valid = gtk_tree_model_iter_next(model, &iter);
402 for(; valid; valid = gtk_tree_model_iter_next(model, &iter))
403 {
404 if(_is_user_tag(model, &iter))
405 {
407 return TRUE;
408 }
409 }
410 if(index)
412 return FALSE;
413}
414
415static void _init_treeview(dt_lib_module_t *self, const int which)
416{
418 GList *tags = NULL;
419 uint32_t count;
420 GtkTreeIter iter;
421 GtkTreeView *view;
422 GtkTreeModel *store;
423 GtkTreeModel *model;
424 gboolean no_sel = FALSE;
425
426 // are we building a hierarchical tree (split on '|') or a flat list?
427 const gboolean build_tree = which ? d->tree_flag : d->attached_tree_flag;
428
429 if(which == 0) // tags of selected images
430 {
431 const int imgsel = dt_control_get_mouse_over_id();
432 no_sel = imgsel > 0 || dt_selection_get_length(dt_selection_get_global()) == 1;
433 count = dt_tag_get_attached(imgsel, &tags, d->dttags_flag ? FALSE : TRUE);
434 view = d->attached_view;
435 // the attached view shows its store directly (no filter), so model == store
436 store = build_tree ? GTK_TREE_MODEL(d->attached_treestore) : GTK_TREE_MODEL(d->attached_liststore);
437 model = store;
438 }
439 else // dictionary_view tags of typed text
440 {
441 if(!d->tree_flag && d->suggestion_flag)
442 count = dt_tag_get_suggestions(&tags);
443 else
444 count = dt_tag_get_with_usage(&tags);
445 view = d->dictionary_view;
446 model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
447 if(d->tree_flag)
448 store = GTK_TREE_MODEL(d->dictionary_treestore);
449 else
450 store = GTK_TREE_MODEL(d->dictionary_liststore);
451 }
452 g_object_ref(model);
453 gtk_tree_view_set_model(GTK_TREE_VIEW(view), NULL);
454
455 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING);
456 if(build_tree)
457 {
458 gtk_tree_store_clear(GTK_TREE_STORE(store));
459 {
460 char **last_tokens = NULL;
461 int last_tokens_length = 0;
462 GtkTreeIter last_parent = { 0 };
463 GList *sorted_tags = dt_sort_tag(tags, 0); // ordered by full tag name
464 tags = sorted_tags;
465 for(GList *taglist = tags; taglist; taglist = g_list_next(taglist))
466 {
467 const gchar *tag = ((dt_tag_t *)taglist->data)->tag;
468 if(IS_NULL_PTR(tag)) continue;
469 char **tokens;
470 tokens = g_strsplit(tag, "|", -1);
471 if(tokens)
472 {
473 // find the number of common parts at the beginning of tokens and last_tokens
474 GtkTreeIter parent = last_parent;
475 const int tokens_length = g_strv_length(tokens);
476 int common_length = 0;
477 if(last_tokens)
478 {
479 while(tokens[common_length] && last_tokens[common_length] &&
480 !g_strcmp0(tokens[common_length], last_tokens[common_length]))
481 {
482 common_length++;
483 }
484
485 // point parent iter to where the entries should be added
486 for(int i = common_length; i < last_tokens_length; i++)
487 {
488 gtk_tree_model_iter_parent(GTK_TREE_MODEL(store), &parent, &last_parent);
489 last_parent = parent;
490 }
491 }
492
493 // insert everything from tokens past the common part
494 char *pth = NULL;
495 for(int i = 0; i < common_length; i++)
496 pth = dt_util_dstrcat(pth, "%s|", tokens[i]);
497
498 for(char **token = &tokens[common_length]; *token; token++)
499 {
500 pth = dt_util_dstrcat(pth, "%s|", *token);
501 gchar *pth2 = g_strdup(pth);
502 pth2[strlen(pth2) - 1] = '\0';
503 gtk_tree_store_insert(GTK_TREE_STORE(store), &iter, common_length > 0 ? &parent : NULL, -1);
504 gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
506 DT_LIB_TAGGING_COL_ID, (token == &tokens[tokens_length-1]) ?
507 ((dt_tag_t *)taglist->data)->id : 0,
509 DT_LIB_TAGGING_COL_COUNT, (token == &tokens[tokens_length-1]) ?
510 ((dt_tag_t *)taglist->data)->count : 0,
511 DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)taglist->data)->select,
512 DT_LIB_TAGGING_COL_FLAGS, ((dt_tag_t *)taglist->data)->flags,
513 DT_LIB_TAGGING_COL_SYNONYM, ((dt_tag_t *)taglist->data)->synonym,
515 -1);
516 if(((dt_tag_t *)taglist->data)->select)
517 _propagate_sel_to_parents(GTK_TREE_MODEL(store), &iter);
518 common_length++;
519 parent = iter;
520 dt_free(pth2);
521 }
522 dt_free(pth);
523
524 // remember things for the next round
525 if(last_tokens) g_strfreev(last_tokens);
526 last_tokens = tokens;
527 last_parent = parent;
528 last_tokens_length = tokens_length;
529 }
530 }
531 g_strfreev(last_tokens);
532 }
533 if(which && d->keyword[0]) // keyword filtering only applies to the dictionary
534 {
535 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
536 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_tree_reveal_func, NULL);
537 gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
538 }
539 else gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
540 g_object_unref(model);
541 }
542 else
543 {
544 gtk_list_store_clear(GTK_LIST_STORE(store));
545 if(count > 0 && tags)
546 {
547 for (GList *tag = tags; tag; tag = g_list_next(tag))
548 {
549 const char *subtag = g_strrstr(((dt_tag_t *)tag->data)->tag, "|");
550 gtk_list_store_append(GTK_LIST_STORE(store), &iter);
551 gtk_list_store_set(GTK_LIST_STORE(store), &iter,
552 DT_LIB_TAGGING_COL_TAG, !subtag ? ((dt_tag_t *)tag->data)->tag : subtag + 1,
553 DT_LIB_TAGGING_COL_ID, ((dt_tag_t *)tag->data)->id,
554 DT_LIB_TAGGING_COL_PATH, ((dt_tag_t *)tag->data)->tag,
555 DT_LIB_TAGGING_COL_COUNT, ((dt_tag_t *)tag->data)->count,
557 ((dt_tag_t *)tag->data)->select,
558 DT_LIB_TAGGING_COL_FLAGS, ((dt_tag_t *)tag->data)->flags,
559 DT_LIB_TAGGING_COL_SYNONYM, ((dt_tag_t *)tag->data)->synonym,
561 -1);
562 }
563 }
564 if(which && d->keyword[0])
565 {
566 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
567 }
568 gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
569 g_object_unref(model);
570 }
571 if(which)
573 else
575 // the attached tree is meant to display the tags, not to be browsed, so reveal everything
576 if(!which && d->attached_tree_flag)
577 gtk_tree_view_expand_all(GTK_TREE_VIEW(d->attached_view));
578 // Free result...
579 dt_tag_free_result(&tags);
580}
581
582static void _tree_tagname_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer,
583 GtkTreeModel *model, GtkTreeIter *iter,
584 gpointer data, gboolean dictionary_view)
585{
586 dt_lib_module_t *self = (dt_lib_module_t *)data;
588 guint tagid;
589 gchar *name;
590 gchar *path;
591 guint count;
592 gchar *coltext;
593 gint flags;
594
595 gtk_tree_model_get(model, iter,
596 DT_LIB_TAGGING_COL_ID, &tagid,
600 DT_LIB_TAGGING_COL_PATH, &path, -1);
601 // tree view shows the leaf token only; list view shows the whole "a|b|c" path
602 const gboolean hide = dictionary_view ? (d->tree_flag ? TRUE : d->hide_path_flag) : d->attached_tree_flag;
603 const gboolean istag = !(flags & DT_TF_CATEGORY) && tagid;
604 if((dictionary_view && !count) || (!dictionary_view && count <= 1))
605 {
606 coltext = g_markup_printf_escaped(istag ? "%s" : "<i>%s</i>", hide ? name : path);
607 }
608 else
609 {
610 coltext = g_markup_printf_escaped(istag ? "%s (%d)" : "<i>%s</i> (%d)", hide ? name : path, count);
611 }
612 g_object_set(renderer, "markup", coltext, NULL);
613 dt_free(coltext);
614 dt_free(name);
615 dt_free(path);
616}
617
618static void _tree_tagname_show_attached(GtkTreeViewColumn *col, GtkCellRenderer *renderer,
619 GtkTreeModel *model, GtkTreeIter *iter,
620 gpointer data)
621{
622 _tree_tagname_show(col, renderer, model, iter, data, 0);
623}
624
625static void _tree_tagname_show_dictionary(GtkTreeViewColumn *col, GtkCellRenderer *renderer,
626 GtkTreeModel *model, GtkTreeIter *iter,
627 gpointer data)
628{
629 _tree_tagname_show(col, renderer, model, iter, data, 1);
630}
631
632
634{
635 _init_treeview(self, 0);
637}
638
639static void _lib_tagging_redraw_callback(gpointer instance, dt_lib_module_t *self)
640{
641 //if(dt_lib_gui_get_expanded(self)) // segfaults internally, for some reason
643}
644
645static void _lib_tagging_tags_changed_callback(gpointer instance, dt_lib_module_t *self)
646{
648 _init_treeview(self, 0);
649 _init_treeview(self, 1);
650 // the set of known tags may have changed (created / deleted / renamed)
651 dt_tagging_completion_refresh(d->completion_store);
653}
654
655static void _collection_updated_callback(gpointer instance, dt_collection_change_t query_change,
656 dt_collection_properties_t changed_property, gpointer imgs, int next,
657 dt_lib_module_t *self)
658{
660 d->collection[0] = '\0';
661 // the tags suggested on an empty entry follow the current collection
664}
665
667{
669 // when collection is on tag any attach & detach becomes very slow
670 // speeding up when jumping from tag collection to the other
671 // the cost is that tag collection doesn't reflect the tag changes real time
672 if(!d->collection[0])
673 {
674 // raises change only for other modules
680 }
681}
682
683// find a tag on the tree
684static gboolean _find_tag_iter_tagid(GtkTreeModel *model, GtkTreeIter *iter, const gint tagid)
685{
686 gint tag;
687 do
688 {
689 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_ID, &tag, -1);
690 if(tag == tagid)
691 {
692 return TRUE;
693 }
694 GtkTreeIter child, parent = *iter;
695 if(gtk_tree_model_iter_children(model, &child, &parent))
696 if(_find_tag_iter_tagid(model, &child, tagid))
697 {
698 *iter = child;
699 return TRUE;
700 }
701 } while (gtk_tree_model_iter_next(model, iter));
702 return FALSE;
703}
704
705// calculate the indeterminated state (1) where needed on the tree
706static void _calculate_sel_on_path(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
707{
708 GtkTreeIter child, parent = *iter;
709 do
710 {
711 gint sel = DT_TS_NO_IMAGE;
712 gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
713 if(sel == DT_TS_ALL_IMAGES)
714 {
716 }
717 if(gtk_tree_model_iter_children(model, &child, &parent))
719 } while (!root && gtk_tree_model_iter_next(model, &parent));
720}
721
722// reset the indeterminated selection (1) on the tree
723static void _reset_sel_on_path(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
724{
725 GtkTreeIter child, parent = *iter;
726 do
727 {
728 if(gtk_tree_model_iter_children(model, &child, &parent))
729 {
730 gint sel = DT_TS_NO_IMAGE;
731 gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
732 if(sel == DT_TS_SOME_IMAGES)
733 {
734 gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
736 }
738 }
739 } while (!root && gtk_tree_model_iter_next(model, &parent));
740}
741
742// reset all selection (1 & 2) on the tree
743static void _reset_sel_on_path_full(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
744{
745 GtkTreeIter child, parent = *iter;
746 do
747 {
748 if(GTK_IS_TREE_STORE(model))
749 {
750 gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
752 if(gtk_tree_model_iter_children(model, &child, &parent))
754 }
755 else
756 {
757 gtk_list_store_set(GTK_LIST_STORE(model), &parent,
759 }
760 } while (!root && gtk_tree_model_iter_next(model, &parent));
761}
762
763// try to find a node fully attached (2) which is the root of the update loop. If not the full tree will be used
764static void _find_root_iter_iter(GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *parent)
765{
766 guint sel;
767 GtkTreeIter child = *iter;
768 while (gtk_tree_model_iter_parent(model, parent, &child))
769 {
770 gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
771 if(sel == DT_TS_ALL_IMAGES)
772 {
773 char *path = NULL;
774 gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_PATH, &path, -1);
775 dt_free(path);
776 return; // no need to go further
777 }
778 child = *parent;
779 }
780 *parent = child; // last before root
781 char *path = NULL;
782 gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_PATH, &path, -1);
783 dt_free(path);
784}
785
786// with tag detach update the tree selection
787static void _calculate_sel_on_tree(GtkTreeModel *model, GtkTreeIter *iter)
788{
789 GtkTreeIter parent;
790 if(iter)
791 {
792 // only on sub-tree
793 _find_root_iter_iter(model, iter, &parent);
794 _reset_sel_on_path(model, &parent, TRUE);
796 }
797 else
798 {
799 // on full tree
800 if(gtk_tree_model_get_iter_first(model, &parent))
801 {
802 _reset_sel_on_path(model, &parent, FALSE);
804 }
805 }
806}
807
808// get the new selected images and update the tree selection
809static void _update_sel_on_tree(GtkTreeModel *model)
810{
811 GList *tags = NULL;
812 dt_tag_get_attached(-1, &tags, TRUE);
813 GtkTreeIter parent;
814 if(gtk_tree_model_get_iter_first(model, &parent))
815 {
817 for (GList *tag = tags; tag; tag = g_list_next(tag))
818 {
819 GtkTreeIter iter = parent;
820 if(_find_tag_iter_tagid(model, &iter, ((dt_tag_t *)tag->data)->id))
821 {
822 if(GTK_IS_TREE_STORE(model))
823 {
824 gtk_tree_store_set(GTK_TREE_STORE(model), &iter,
825 DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)tag->data)->select, -1);
827 }
828 else
829 {
830 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
831 DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)tag->data)->select, -1);
832 }
833 }
834 }
835 }
836 if(tags)
837 dt_tag_free_result(&tags);
838}
839
840// delete a tag in the tree (tree or list)
841// delete a branch of the tag tree
842static void _delete_tree_path(GtkTreeModel *model, GtkTreeIter *iter, gboolean root, gboolean tree)
843{
844 if(tree) // the treeview is tree. It handles the hierarchy itself (parent / child)
845 {
846 GtkTreeIter child, parent = *iter;
847 gboolean valid = TRUE;
848 do
849 {
850 if(gtk_tree_model_iter_children(model, &child, &parent))
851 _delete_tree_path(model, &child, FALSE, tree);
852 GtkTreeIter tobedel = parent;
853 valid = gtk_tree_model_iter_next(model, &parent);
854 if(root)
855 {
856 gtk_tree_store_set(GTK_TREE_STORE(model), &tobedel,
859
860 char *path2 = NULL;
861 gtk_tree_model_get(model, &tobedel, DT_LIB_TAGGING_COL_PATH, &path2, -1);
862 dt_free(path2);
863
864 _calculate_sel_on_tree(model, &tobedel);
865 }
866 char *path = NULL;
867 gtk_tree_model_get(model, &tobedel, DT_LIB_TAGGING_COL_PATH, &path, -1);
868 dt_free(path);
869 gtk_tree_store_remove(GTK_TREE_STORE(model), &tobedel);
870 } while (!root && valid);
871 }
872 else // treeview is a list. The hierarchy of tags is found with the root (left part) of tagname
873 {
874 GtkTreeIter child;
875 char *path = NULL;
876 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
877 guint pathlen = strlen(path);
878 gboolean valid = gtk_tree_model_get_iter_first(model, &child);
879 while(valid)
880 {
881 char *path2 = NULL;
882 gtk_tree_model_get(model, &child, DT_LIB_TAGGING_COL_PATH, &path2, -1);
883 GtkTreeIter tobedel = child;
884 valid = gtk_tree_model_iter_next (model, &child);
885 if(strlen(path2) >= pathlen)
886 {
887 char letter = path2[pathlen];
888 path2[pathlen] = '\0';
889 if(g_strcmp0(path, path2) == 0)
890 {
891 path2[pathlen] = letter;
892 gtk_list_store_remove(GTK_LIST_STORE(model), &tobedel);
893 }
894 }
895 dt_free(path2);
896 }
897 dt_free(path);
898 }
899}
900
901static void _lib_selection_changed_callback(gpointer instance, dt_lib_module_t *self)
902{
904 _init_treeview(self, 0);
905 if(!d->tree_flag && d->suggestion_flag)
906 {
907 _init_treeview(self, 1);
908 }
909 else
910 _update_sel_on_tree(d->tree_flag ? GTK_TREE_MODEL(d->dictionary_treestore)
911 : GTK_TREE_MODEL(d->dictionary_liststore));
912
914}
915
917{
919 const gchar *beg = g_strrstr(gtk_entry_get_text(d->dict_entry), ",");
920
921 if(IS_NULL_PTR(beg))
922 beg = gtk_entry_get_text(d->dict_entry);
923 else
924 {
925 if(*beg == ',') beg++;
926 if(*beg == ' ') beg++;
927 }
928 g_strlcpy(d->keyword, beg, sizeof(d->keyword));
929}
930
931static gboolean _update_tag_name_per_name(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, dt_tag_op_t *to)
932{
933 char *tagname;
934 char *newtagname = to->newtagname;
935 char *oldtagname = to->oldtagname;
936 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &tagname, -1);
937 if(g_str_has_prefix(tagname, oldtagname))
938 {
939 if(strlen(tagname) == strlen(oldtagname))
940 {
941 // rename the tag itself
942 if(to->tree_flag)
943 {
944 char *subtag = g_strrstr(to->newtagname, "|");
945 subtag = (!subtag) ? newtagname : subtag + 1;
946 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_TAGGING_COL_PATH,
947 newtagname, DT_LIB_TAGGING_COL_TAG, subtag, -1);
948 }
949 else
950 {
951 gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_TAGGING_COL_PATH,
952 newtagname, DT_LIB_TAGGING_COL_TAG, newtagname, -1);
953 }
954 }
955 else if(strlen(tagname) > strlen(oldtagname) && tagname[strlen(oldtagname)] == '|')
956 {
957 // rename similar path
958 char *newpath = g_strconcat(newtagname, &tagname[strlen(oldtagname)] , NULL);
959 if(to->tree_flag)
960 {
961 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_TAGGING_COL_PATH,
962 newpath, -1);
963 }
964 else
965 {
966 gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_TAGGING_COL_PATH,
967 newpath, DT_LIB_TAGGING_COL_TAG, newpath, -1);
968 }
969 dt_free(newpath);
970 }
971 }
972 dt_free(tagname);
973 return FALSE;
974}
975
976static void _update_attached_count(const int tagid, GtkTreeView *view, const gboolean tree_flag)
977{
978 const guint count = dt_tag_images_count(tagid);
979 GtkTreeModel *model = gtk_tree_view_get_model(view);
980 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
981 GtkTreeIter iter;
982 if(gtk_tree_model_get_iter_first(store, &iter))
983 {
984 if(_find_tag_iter_tagid(store, &iter, tagid))
985 {
986 if(tree_flag)
987 {
988 gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
991 _calculate_sel_on_tree(GTK_TREE_MODEL(store), &iter);
992 }
993 else
994 {
995 gtk_list_store_set(GTK_LIST_STORE(store), &iter,
998 }
999 }
1000 }
1001}
1002
1004{
1005
1006}
1007
1009{
1010 char *params = NULL;
1011 *size = 0;
1012 GList *tags = NULL;
1013 const guint count = dt_tag_get_attached(-1, &tags, TRUE);
1014
1015 if(count)
1016 {
1017 for(GList *taglist = tags; taglist; taglist = g_list_next(taglist))
1018 {
1019 params = dt_util_dstrcat(params, "%d,", ((dt_tag_t *)taglist->data)->id);
1020 }
1021 dt_tag_free_result(&tags);
1022 if(IS_NULL_PTR(params))
1023 return NULL;
1024 *size = strlen(params);
1025 params[*size-1]='\0';
1026 }
1027 return params;
1028}
1029
1030int set_params(dt_lib_module_t *self, const void *params, int size)
1031{
1032 if(IS_NULL_PTR(params) || !size) return 1;
1034
1035 const char *buf = (char *)params;
1036 if(buf && buf[0])
1037 {
1038 gchar **tokens = g_strsplit(buf, ",", 0);
1039 if(tokens)
1040 {
1041 GList *tags = NULL;
1042 gchar **entry = tokens;
1043 while(*entry)
1044 {
1045 const guint tagid = strtoul(*entry, NULL, 0);
1046 tags = g_list_prepend(tags, GINT_TO_POINTER(tagid));
1047 entry++;
1048 }
1049 g_strfreev(tokens);
1050 GList *imgs = dt_act_on_get_images();
1051 dt_tag_set_tags(tags, imgs, TRUE, FALSE, TRUE);
1052 g_list_free(imgs);
1053 imgs = NULL;
1054 gboolean change = FALSE;
1055 for(GList *tag = tags; tag; tag = g_list_next(tag))
1056 {
1057 _update_attached_count(GPOINTER_TO_INT(tag->data), d->dictionary_view, d->tree_flag);
1058 change = TRUE;
1059 }
1060
1061 if(change)
1062 {
1063 _init_treeview(self, 0);
1066 }
1067 g_list_free(tags);
1068 tags = NULL;
1069 }
1070 }
1071 return 0;
1072}
1073
1074// collect the tag ids of all currently selected rows of view (skips non-tag nodes)
1075static GList *_selected_tagids(GtkTreeView *view)
1076{
1077 GList *ids = NULL;
1078 GtkTreeModel *model = NULL;
1079 GList *rows = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(view), &model);
1080 for(GList *r = rows; r; r = g_list_next(r))
1081 {
1082 GtkTreeIter iter;
1083 if(gtk_tree_model_get_iter(model, &iter, (GtkTreePath *)r->data))
1084 {
1085 guint tagid = 0;
1086 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1087 if(tagid > 0) ids = g_list_prepend(ids, GUINT_TO_POINTER(tagid));
1088 }
1089 }
1090 g_list_free_full(rows, (GDestroyNotify)gtk_tree_path_free);
1091 return ids;
1092}
1093
1094// detach the given tag ids from the images to act on, then refresh the views
1095static void _detach_tagids(GList *tagids, dt_lib_module_t *self)
1096{
1098 if(IS_NULL_PTR(tagids)) return;
1099 GList *imgs = dt_act_on_get_images();
1100 if(IS_NULL_PTR(imgs)) return;
1101
1102 gboolean changed = FALSE;
1103 for(GList *t = tagids; t; t = g_list_next(t))
1104 {
1105 const guint tagid = GPOINTER_TO_UINT(t->data);
1106 if(tagid <= 0) continue;
1107 GList *affected = dt_tag_get_images_from_list(imgs, tagid);
1108 if(affected)
1109 {
1110 if(dt_tag_detach_images(tagid, affected, TRUE)) changed = TRUE;
1111 dt_image_synch_xmps(affected);
1112 g_list_free(affected);
1113 }
1114 }
1115 g_list_free(imgs);
1116
1117 _init_treeview(self, 0);
1118 _init_treeview(self, 1);
1119 dt_tagging_completion_refresh(d->completion_store);
1121 if(changed) _raise_signal_tag_changed(self);
1122}
1123
1125{
1127 GtkTreeIter iter;
1128 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
1129 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->attached_view));
1130 if(!_selection_get_first(selection, &model, &iter))
1131 return;
1132 guint tagid;
1133 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1134 if(tagid <= 0) return;
1135
1136 // attach tag on images to act on
1137 const gboolean res = dt_tag_attach(tagid, -1, TRUE, TRUE);
1138
1141
1142 _init_treeview(self, 0);
1143
1144 const uint32_t count = dt_tag_images_count(tagid);
1145 model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
1146 if(gtk_tree_model_get_iter_first(model, &iter))
1147 {
1148 if(_find_tag_iter_tagid(model, &iter, tagid))
1149 {
1150 GtkTreeIter store_iter;
1151 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
1152 gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
1153 &store_iter, &iter);
1154 if(d->tree_flag)
1155 {
1156 gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter, DT_LIB_TAGGING_COL_COUNT, count, -1);
1157 }
1158 else
1159 {
1160 gtk_list_store_set(GTK_LIST_STORE(store), &store_iter, DT_LIB_TAGGING_COL_COUNT, count, -1);
1161 }
1162 }
1163 }
1164
1165 if(res)
1166 {
1169 }
1170}
1171
1173{
1175 GList *tagids = _selected_tagids(d->attached_view);
1176 _detach_tagids(tagids, self);
1177 g_list_free(tagids);
1178}
1179
1180// detach every (user) tag currently listed in the attached view
1182{
1184 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
1185 GtkTreeIter iter;
1186 GList *tagids = NULL;
1187 if(gtk_tree_model_get_iter_first(model, &iter))
1188 {
1189 do
1190 {
1191 if(!_is_user_tag(model, &iter)) continue; // never touch Ansel internal tags
1192 guint tagid = 0;
1193 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1194 if(tagid > 0) tagids = g_list_prepend(tagids, GUINT_TO_POINTER(tagid));
1195 } while(gtk_tree_model_iter_next(model, &iter));
1196 }
1197 _detach_tagids(tagids, self);
1198 g_list_free(tagids);
1199}
1200
1201static void _pop_menu_attached(GtkWidget *treeview, GdkEventButton *event, dt_lib_module_t *self)
1202{
1204 GtkWidget *menu, *menuitem;
1205 menu = gtk_menu_new();
1206
1207 GtkTreeIter iter;
1208 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
1209 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->attached_view));
1210 const gint sel_cnt = gtk_tree_selection_count_selected_rows(selection);
1211
1212 // "attach to all" only makes sense for a single partially-attached tag
1213 if(sel_cnt == 1 && _selection_get_first(selection, &model, &iter))
1214 {
1215 guint sel;
1216 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_SEL, &sel, -1);
1217 if(sel == DT_TS_SOME_IMAGES)
1218 {
1219 menuitem = gtk_menu_item_new_with_label(_("attach tag to all"));
1220 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_attached_attach_to_all, self);
1221 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
1222 menuitem = gtk_separator_menu_item_new();
1223 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
1224 }
1225 }
1226
1227 if(sel_cnt > 0)
1228 {
1229 menuitem = gtk_menu_item_new_with_label(sel_cnt > 1 ? _("detach tags") : _("detach tag"));
1230 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
1231 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_attached_detach, self);
1232 }
1233
1234 menuitem = gtk_menu_item_new_with_label(_("detach all tags"));
1235 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
1236 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_attached_detach_all, self);
1237
1238 gtk_widget_show_all(GTK_WIDGET(menu));
1239
1240 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
1241}
1242
1243// detach the single tag of the row whose per-row "delete" icon was clicked
1244static gboolean _attached_delete_icon_activated(GtkCellRenderer *cell, GdkEvent *event, GtkWidget *treeview,
1245 const gchar *path_str, GdkRectangle *background,
1246 GdkRectangle *cell_area, GtkCellRendererState flags,
1247 gpointer user_data)
1248{
1249 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1250 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
1251 GtkTreePath *path = gtk_tree_path_new_from_string(path_str);
1252 GtkTreeIter iter;
1253 if(path && gtk_tree_model_get_iter(model, &iter, path))
1254 {
1255 guint tagid = 0;
1256 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1257 if(tagid > 0)
1258 {
1259 GList *one = g_list_prepend(NULL, GUINT_TO_POINTER(tagid));
1260 _detach_tagids(one, self);
1261 g_list_free(one);
1262 }
1263 }
1264 if(path) gtk_tree_path_free(path);
1265 return TRUE;
1266}
1267
1268// only real user tags get a per-row delete icon (not Ansel internal tags,
1269// nor the free/parent nodes that appear in tree view, which have no tag id)
1270static void _attached_delete_icon_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer,
1271 GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
1272{
1273 guint tagid = 0;
1274 gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1275 g_object_set(renderer, "icon-name", "edit-delete-symbolic",
1276 "visible", (tagid > 0) && _is_user_tag(model, iter), NULL);
1277}
1278
1279static gboolean _click_on_view_attached(GtkWidget *view, GdkEventButton *event, dt_lib_module_t *self)
1280{
1282 _unselect_all_in_view(d->dictionary_view);
1283
1284 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
1285 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
1286 GtkTreePath *path = NULL;
1287 const gboolean on_row =
1288 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL);
1289
1290 gboolean valid_tag = FALSE;
1291 GtkTreeIter iter;
1292 if(on_row && gtk_tree_model_get_iter(model, &iter, path))
1293 valid_tag = _is_user_tag(model, &iter); // not an Ansel internal tag
1294
1295 // right-click: always offer the menu, even on empty space (so "detach all tags" is reachable)
1296 if(event->type == GDK_BUTTON_PRESS && event->button == 3)
1297 {
1298 if(valid_tag)
1299 {
1300 // keep an existing multi-selection so the menu can act on it in bulk
1301 if(!gtk_tree_selection_path_is_selected(selection, path))
1302 {
1303 gtk_tree_selection_unselect_all(selection);
1304 gtk_tree_selection_select_path(selection, path);
1305 }
1306 }
1307 else
1308 gtk_tree_selection_unselect_all(selection); // clicked empty space / an Ansel tag
1309 _pop_menu_attached(view, event, self);
1310 if(path) gtk_tree_path_free(path);
1311 return TRUE;
1312 }
1313
1314 // double-click on a user tag: detach that single tag
1315 if(event->type == GDK_2BUTTON_PRESS && event->button == 1 && valid_tag)
1316 {
1317 guint tagid = 0;
1318 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1319 if(tagid > 0)
1320 {
1321 GList *one = g_list_prepend(NULL, GUINT_TO_POINTER(tagid));
1322 _detach_tagids(one, self);
1323 g_list_free(one);
1324 }
1325 if(path) gtk_tree_path_free(path);
1326 return TRUE;
1327 }
1328
1329 // single click on an Ansel internal tag: prevent selecting it
1330 if(event->type == GDK_BUTTON_PRESS && event->button == 1 && on_row && !valid_tag)
1331 {
1332 gtk_tree_path_free(path);
1333 return TRUE;
1334 }
1335
1336 if(path) gtk_tree_path_free(path);
1337 return FALSE;
1338}
1339
1340static gboolean _attached_key_pressed(GtkWidget *view, GdkEventKey *event, dt_lib_module_t *self)
1341{
1343 _unselect_all_in_view(d->dictionary_view);
1344
1345 guint key = dt_keys_mainpad_alternatives(event->keyval);
1346 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
1347 if(key == GDK_KEY_Delete && gtk_tree_selection_count_selected_rows(selection) > 0)
1348 {
1349 GList *tagids = _selected_tagids(GTK_TREE_VIEW(view));
1350 _detach_tagids(tagids, self);
1351 g_list_free(tagids);
1352 return TRUE;
1353 }
1354 if(key == GDK_KEY_Tab)
1355 {
1356 gtk_tree_selection_unselect_all(selection);
1357 gtk_widget_grab_focus(GTK_WIDGET(d->entry));
1358 return TRUE;
1359 }
1360 else if(key == GDK_KEY_ISO_Left_Tab)
1361 {
1362 gtk_tree_selection_unselect_all(selection);
1363 return TRUE;
1364 }
1365 return FALSE;
1366}
1367
1368// create the tag typed in src and attach it to the current selection
1369static void _create_tag_from_entry(dt_lib_module_t *self, GtkEntry *src)
1370{
1372 const gchar *tag = gtk_entry_get_text(src);
1373 if(IS_NULL_PTR(tag) || tag[0] == '\0') return;
1374
1376 const gboolean res = dt_tag_attach_string_list(tag, imgs, TRUE);
1377 if(res) dt_image_synch_xmps(imgs);
1378 g_list_free(imgs);
1379 imgs = NULL;
1380
1382 _save_last_tag_used(tag, d);
1383
1384 _entry_clear_suppressed(src, self);
1385
1386 _init_treeview(self, 0);
1387 _init_treeview(self, 1);
1388 // _raise_signal_tag_changed() may block the tags-changed callback, so refresh
1389 // the autocompletion explicitly to pick up the freshly created tag
1390 dt_tagging_completion_refresh(d->completion_store);
1392 char *tagname = strrchr(d->last_tag, ',');
1394 _show_tag_on_view(GTK_TREE_VIEW(d->dictionary_view),
1395 tagname ? tagname + 1 : d->last_tag, FALSE, TRUE);
1396}
1397
1398// "new" button in the management popup: create from its search entry
1399static void _new_button_clicked(GtkButton *button, dt_lib_module_t *self)
1400{
1402 _create_tag_from_entry(self, d->dict_entry);
1403}
1404
1405/* Clear a tag entry without its own key handler taking the programmatic change for user input.
1406 *
1407 * dt_gui_widget_freeze() (widgets/widget_settings.h) is the application-wide way to say that, and
1408 * the one this file already uses everywhere else: it raises the suppression depth for the rest of
1409 * the enclosing scope and drops it on every exit path, while each widget callback opens by asking
1410 * dt_gui_widgets_suppressed(). dt_tagging_entry_clear() puts _enter_key_pressed() back after the
1411 * completion's key handler, which reattaching the completion connects last; the dictionary entry
1412 * has no key handler, so nothing is reconnected there. */
1413static void _entry_clear_suppressed(GtkEntry *entry, dt_lib_module_t *self)
1414{
1416 dt_tagging_entry_clear(entry, G_CALLBACK(_enter_key_pressed), self);
1417}
1418
1419static gboolean _enter_key_pressed(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self)
1420{
1421 // A programmatic clear of the entry is not the user pressing a key. FALSE, not TRUE: a
1422 // suppressed callback declines the event rather than swallowing it.
1423 if(dt_gui_widgets_suppressed()) return FALSE;
1424
1426 guint key = dt_keys_mainpad_alternatives(event->keyval);
1427 switch(key)
1428 {
1429 case GDK_KEY_Return:
1430 _create_tag_from_entry(self, d->entry);
1431 break;
1432 case GDK_KEY_Escape:
1433 gtk_window_set_focus(GTK_WINDOW(dt_gui_main_window()), NULL);
1434 break;
1435 case GDK_KEY_ISO_Left_Tab:
1436 {
1437 if(_select_next_user_attached_tag(0, d->attached_view))
1438 {
1439 _entry_clear_suppressed(d->entry, self);
1440 gtk_widget_grab_focus(GTK_WIDGET(d->attached_view));
1441 }
1442 return TRUE;
1443 }
1444 default:
1445 break;
1446 }
1447 return FALSE;
1448}
1449
1450static void _entry_clear_icon(GtkEntry *entry, const GtkEntryIconPosition position,
1451 GdkEvent *event, dt_lib_module_t *self)
1452{
1453 if(position != GTK_ENTRY_ICON_SECONDARY) return;
1454
1455 _entry_clear_suppressed(entry, self);
1456 gtk_widget_grab_focus(GTK_WIDGET(entry));
1457}
1458
1459
1460static void _tag_name_changed(GtkEntry *entry, dt_lib_module_t *self)
1461{
1463 _set_keyword(self);
1464 GtkTreeModel *model = gtk_tree_view_get_model(d->dictionary_view);
1465 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
1466 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
1467 if(d->tree_flag && d->keyword[0])
1468 {
1469 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_tree_reveal_func, NULL);
1470 _show_keyword_on_view(d->dictionary_view, d->keyword, FALSE);
1471 }
1472}
1473
1475{
1477
1478 int res = GTK_RESPONSE_YES;
1479
1480 char *tagname;
1481 gint tagid;
1482 gchar *text;
1483 GtkWidget *label;
1484 GtkTreeIter iter;
1485 GtkTreeModel *model = NULL;
1486 GtkTreeView *view = d->dictionary_view;
1487 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
1488 if(!_selection_get_first(selection, &model, &iter)) return;
1489
1490 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
1491 DT_LIB_TAGGING_COL_ID, &tagid, -1);
1492
1493 gint tag_count = 0;
1494 gint img_count = 0;
1495 dt_tag_count_tags_images(tagname, &tag_count, &img_count);
1496 if(tag_count == 0) return;
1497
1499 GtkWidget *dialog = gtk_dialog_new_with_buttons( _("delete node?"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT,
1500 _("cancel"), GTK_RESPONSE_NONE, _("delete"), GTK_RESPONSE_YES, NULL);
1501 gtk_window_set_default_size(GTK_WINDOW(dialog), 300, -1);
1502 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1503 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1504 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1505 gtk_container_add(GTK_CONTAINER(area), vbox);
1506 text = g_strdup_printf(_("selected: %s"), tagname);
1507 label = gtk_label_new(text);
1508 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 0);
1509 dt_free(text);
1510
1511 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1512 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, TRUE, 0);
1513 text = g_strdup_printf(ngettext("<u>%d</u> tag will be deleted", "<u>%d</u> tags will be deleted", tag_count), tag_count);
1514 label = gtk_label_new(NULL);
1515 gtk_label_set_markup(GTK_LABEL(label), text);
1516 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1517 dt_free(text);
1518 text = g_strdup_printf(ngettext("<u>%d</u> image will be updated", "<u>%d</u> images will be updated", img_count), img_count);
1519 label = gtk_label_new(NULL);
1520 gtk_label_set_markup(GTK_LABEL(label), text);
1521 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1522 dt_free(text);
1523
1524#ifdef GDK_WINDOWING_QUARTZ
1526#endif
1527 gtk_widget_show_all(dialog);
1528
1529 res = gtk_dialog_run(GTK_DIALOG(dialog));
1530 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1531 gtk_widget_destroy(dialog);
1532 dt_gui_refocus_parent(dialog_parent);
1533 if(res != GTK_RESPONSE_YES)
1534 {
1535 dt_free(tagname);
1536 return;
1537 }
1538
1539 GList *tag_family = NULL;
1540 GList *tagged_images = NULL;
1541 dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
1542
1544 tag_count = dt_tag_remove_list(tag_family);
1546 dt_control_log(_("%d tags removed"), tag_count);
1547
1548 GtkTreeIter store_iter;
1549 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
1550 gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
1551 &store_iter, &iter);
1552 _delete_tree_path(GTK_TREE_MODEL(store), &store_iter, TRUE, d->tree_flag);
1553 _init_treeview(self, 0);
1554
1555 dt_tag_free_result(&tag_family);
1556 dt_image_synch_xmps(tagged_images);
1557 g_list_free(tagged_images);
1558 tagged_images = NULL;
1559 dt_tagging_completion_refresh(d->completion_store);
1562 dt_free(tagname);
1563}
1564
1565// create tag allows the user to create a single tag, which can be an element of the hierarchy or not
1567{
1569
1570 char *tagname;
1571 char *path;
1572 gint tagid;
1573 gchar *text;
1574 GtkWidget *label;
1575 GtkTreeIter iter;
1576 GtkTreeModel *model = NULL;
1577 GtkTreeView *view = d->dictionary_view;
1578 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
1579 if(!_selection_get_first(selection, &model, &iter)) return;
1580
1581 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_TAG, &tagname,
1583
1585 GtkWidget *dialog = gtk_dialog_new_with_buttons(_("create tag"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT,
1586 _("cancel"), GTK_RESPONSE_NONE, _("save"), GTK_RESPONSE_YES, NULL);
1587 gtk_window_set_default_size(GTK_WINDOW(dialog), 300, -1);
1588 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1589 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1590 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1591 gtk_container_add(GTK_CONTAINER(area), vbox);
1592
1593 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1594 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, TRUE, 0);
1595 label = gtk_label_new(_("name: "));
1596 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1597 GtkWidget *entry = gtk_entry_new();
1599 gtk_box_pack_end(GTK_BOX(box), entry, TRUE, TRUE, 0);
1600
1601 GtkWidget *category;
1602 GtkWidget *private;
1603 GtkWidget *parent;
1604 GtkTextBuffer *buffer = NULL;
1605 GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1606 gtk_box_pack_start(GTK_BOX(vbox), vbox2, FALSE, TRUE, 0);
1607
1608 text = g_strdup_printf(_("add to: \"%s\" "), path);
1609 parent = gtk_check_button_new_with_label(text);
1610 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(parent), TRUE);
1611 gtk_box_pack_end(GTK_BOX(vbox2), parent, FALSE, TRUE, 0);
1612 dt_free(text);
1613
1614 category = gtk_check_button_new_with_label(_("category"));
1615 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(category), FALSE);
1616 gtk_box_pack_end(GTK_BOX(vbox2), category, FALSE, TRUE, 0);
1617 private = gtk_check_button_new_with_label(_("private"));
1618 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(private), FALSE);
1619 gtk_box_pack_end(GTK_BOX(vbox2), private, FALSE, TRUE, 0);
1620
1621 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1622 gtk_box_pack_end(GTK_BOX(vbox), box, TRUE, TRUE, 0);
1623 label = gtk_label_new(_("synonyms: "));
1624 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1625 GtkWidget *synonyms = gtk_text_view_new();
1627 dt_gui_textview_set_padding(GTK_TEXT_VIEW(synonyms));
1628 gtk_box_pack_end(GTK_BOX(box), synonyms, TRUE, TRUE, 0);
1629 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(synonyms), GTK_WRAP_WORD);
1630 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(synonyms));
1631
1632#ifdef GDK_WINDOWING_QUARTZ
1634#endif
1635 gtk_widget_show_all(dialog);
1636
1637 if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
1638 {
1639 const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
1640 char *message = NULL;
1641 if(!newtag[0])
1642 message = _("empty tag is not allowed, aborting");
1643 char *new_tagname = NULL;
1644 const gboolean root = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(parent));
1645 if(!root)
1646 {
1647 new_tagname = g_strdup(path);
1648 new_tagname = dt_util_dstrcat(new_tagname, "|%s", newtag);
1649 }
1650 else new_tagname = g_strdup(newtag);
1651
1652 if(dt_tag_exists(new_tagname, NULL))
1653 message = _("tag name already exists. aborting.");
1654 if(message)
1655 {
1656 GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL,
1657 GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
1658 gtk_dialog_run(GTK_DIALOG(warning_dialog));
1659 gtk_widget_destroy(warning_dialog);
1660 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1661 gtk_widget_destroy(dialog);
1662 dt_gui_refocus_parent(dialog_parent);
1663 dt_free(tagname);
1664 return;
1665 }
1666 guint new_tagid = 0;
1667 if(dt_tag_new(new_tagname, &new_tagid))
1668 {
1669 const gint new_flags = ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(category)) ? DT_TF_CATEGORY : 0) |
1670 (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(private)) ? DT_TF_PRIVATE : 0));
1671 if(new_tagid) dt_tag_set_flags(new_tagid, new_flags);
1672 GtkTextIter start, end;
1673 gtk_text_buffer_get_start_iter(buffer, &start);
1674 gtk_text_buffer_get_end_iter(buffer, &end);
1675 gchar *new_synonyms_list = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
1676 if(new_tagid && new_synonyms_list && new_synonyms_list[0])
1677 dt_tag_set_synonyms(new_tagid, new_synonyms_list);
1678 dt_free(new_synonyms_list);
1679 _init_treeview(self, 1);
1680 dt_tagging_completion_refresh(d->completion_store);
1682 _show_tag_on_view(view, new_tagname, FALSE, TRUE);
1683 }
1684 dt_free(new_tagname);
1685 }
1686 _init_treeview(self, 0);
1687 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1688 gtk_widget_destroy(dialog);
1689 dt_gui_refocus_parent(dialog_parent);
1690 dt_free(tagname);
1691}
1692
1693// edit tag allows the user to rename a single tag, which can be an element of the hierarchy and change other parameters
1695{
1697
1698 char *tagname;
1699 char *synonyms_list;
1700 gint tagid;
1701 gchar *text;
1702 GtkWidget *label;
1703 GtkTreeIter iter;
1704 GtkTreeModel *model = NULL;
1705 GtkTreeView *view = d->dictionary_view;
1706 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
1707 if(!_selection_get_first(selection, &model, &iter)) return;
1708
1709 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
1710 DT_LIB_TAGGING_COL_SYNONYM, &synonyms_list, DT_LIB_TAGGING_COL_ID, &tagid, -1);
1711 char *subtag = g_strrstr(tagname, "|");
1712 if(subtag) subtag = subtag + 1;
1713 gint tag_count;
1714 gint img_count;
1715 dt_tag_count_tags_images(tagname, &tag_count, &img_count);
1716 if(tag_count == 0)
1717 {
1718 dt_free(tagname);
1719 dt_free(synonyms_list);
1720 return;
1721 }
1722
1724 GtkWidget *dialog = gtk_dialog_new_with_buttons(_("edit"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT,
1725 _("cancel"), GTK_RESPONSE_NONE, _("save"), GTK_RESPONSE_YES, NULL);
1726 gtk_window_set_default_size(GTK_WINDOW(dialog), 300, -1);
1727 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1728 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1729 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1730 gtk_container_add(GTK_CONTAINER(area), vbox);
1731 text = g_strdup_printf(_("selected: %s"), tagname);
1732 label = gtk_label_new(text);
1733 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 0);
1734 dt_free(text);
1735
1736 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1737 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, TRUE, 0);
1738 text = g_strdup_printf(ngettext("<u>%d</u> tag will be updated", "<u>%d</u> tags will be updated", tag_count), tag_count);
1739 label = gtk_label_new(NULL);
1740 gtk_label_set_markup(GTK_LABEL(label), text);
1741 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1742 dt_free(text);
1743 text = g_strdup_printf(ngettext("<u>%d</u> image will be updated", "<u>%d</u> images will be updated", img_count), img_count);
1744 label = gtk_label_new(NULL);
1745 gtk_label_set_markup(GTK_LABEL(label), text);
1746 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1747 dt_free(text);
1748
1749 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1750 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, TRUE, 0);
1751 label = gtk_label_new(_("name: "));
1752 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1753 GtkWidget *entry = gtk_entry_new();
1755 gtk_entry_set_text(GTK_ENTRY(entry), subtag ? subtag : tagname);
1756 gtk_box_pack_end(GTK_BOX(box), entry, TRUE, TRUE, 0);
1757
1758 gint flags = 0;
1759 GtkWidget *category = NULL;
1760 GtkWidget *private = gtk_check_button_new_with_label(_("private"));
1761 GtkTextBuffer *buffer = NULL;
1762
1763 if(tagid)
1764 {
1765 GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1766 gtk_box_pack_start(GTK_BOX(vbox), vbox2, FALSE, TRUE, 0);
1767 flags = dt_tag_get_flags(tagid);
1768 category = gtk_check_button_new_with_label(_("category"));
1769 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(category), flags & DT_TF_CATEGORY);
1770 gtk_box_pack_end(GTK_BOX(vbox2), category, FALSE, TRUE, 0);
1771 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(private), flags & DT_TF_PRIVATE);
1772 gtk_box_pack_end(GTK_BOX(vbox2), private, FALSE, TRUE, 0);
1773
1774 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1775 gtk_box_pack_end(GTK_BOX(vbox), box, TRUE, TRUE, 0);
1776 label = gtk_label_new(_("synonyms: "));
1777 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
1778 GtkWidget *synonyms = gtk_text_view_new();
1780 dt_gui_textview_set_padding(GTK_TEXT_VIEW(synonyms));
1781 gtk_box_pack_end(GTK_BOX(box), synonyms, TRUE, TRUE, 0);
1782 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(synonyms), GTK_WRAP_WORD);
1783 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(synonyms));
1784 if(synonyms_list) gtk_text_buffer_set_text(buffer, synonyms_list, -1);
1785 }
1786
1787#ifdef GDK_WINDOWING_QUARTZ
1789#endif
1790 gtk_widget_show_all(dialog);
1791
1792 if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
1793 {
1794 const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
1795 if(g_strcmp0(newtag, subtag ? subtag : tagname) != 0)
1796 {
1797 // tag name has changed
1798 char *message = NULL;
1799 if(!newtag[0])
1800 message = _("empty tag is not allowed, aborting");
1801 if(strchr(newtag, '|') != 0)
1802 message = _("'|' character is not allowed for renaming tag.\nto modify the hierarchy use rename path instead. Aborting.");
1803 if(message)
1804 {
1805 GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL,
1806 GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
1807 gtk_dialog_run(GTK_DIALOG(warning_dialog));
1808 gtk_widget_destroy(warning_dialog);
1809 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1810 gtk_widget_destroy(dialog);
1811 dt_gui_refocus_parent(dialog_parent);
1812 dt_free(tagname);
1813 return;
1814 }
1815
1816 GList *tag_family = NULL;
1817 GList *tagged_images = NULL;
1818 dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
1819
1820 const int tagname_len = strlen(tagname);
1821 char *new_prefix_tag;
1822 if(subtag)
1823 {
1824 const int subtag_len = strlen(subtag);
1825 const char letter = tagname[tagname_len - subtag_len];
1826 tagname[tagname_len - subtag_len] = '\0';
1827 new_prefix_tag = g_strconcat(tagname, newtag, NULL);
1828 tagname[tagname_len - subtag_len] = letter;
1829 }
1830 else
1831 new_prefix_tag = (char *)newtag;
1832
1833 // check if one of the new tagnames already exists.
1834 gboolean tagname_exists = FALSE;
1835 for (GList *taglist = tag_family; taglist && !tagname_exists; taglist = g_list_next(taglist))
1836 {
1837 char *new_tagname = g_strconcat(new_prefix_tag, &((dt_tag_t *)taglist->data)->tag[tagname_len], NULL);
1838 tagname_exists = dt_tag_exists(new_tagname, NULL);
1839 if(tagname_exists)
1840 {
1841 GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL,
1842 GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
1843 _("at least one new tag name (%s) already exists, aborting"), new_tagname);
1844 gtk_dialog_run(GTK_DIALOG(warning_dialog));
1845 gtk_widget_destroy(warning_dialog);
1846 dt_free(new_tagname);
1847 if(subtag)
1848 {
1849 dt_free(new_prefix_tag);
1850 }
1851 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1852 gtk_widget_destroy(dialog);
1853 dt_gui_refocus_parent(dialog_parent);
1854 dt_free(tagname);
1855 return;
1856 };
1857 dt_free(new_tagname);
1858 }
1859
1860 // rename related tags
1861 for (GList *taglist = tag_family; taglist; taglist = g_list_next(taglist))
1862 {
1863 char *new_tagname = g_strconcat(new_prefix_tag, &((dt_tag_t *)taglist->data)->tag[tagname_len], NULL);
1864 dt_tag_rename(((dt_tag_t *)taglist->data)->id, new_tagname);
1865 dt_free(new_tagname);
1866 }
1867
1868 // update the store
1869 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
1870 dt_tag_op_t *to = g_malloc(sizeof(dt_tag_op_t));
1871 to->tree_flag = d->tree_flag;
1872 to->oldtagname = tagname;
1873 to->newtagname = new_prefix_tag;
1874 gint sort_column;
1875 GtkSortType sort_order;
1876 gtk_tree_sortable_get_sort_column_id(GTK_TREE_SORTABLE(store), &sort_column, &sort_order);
1877 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING);
1878 gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_update_tag_name_per_name, to);
1879 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), sort_column, sort_order);
1880 dt_free(to);
1881 if(subtag)
1882 {
1883 dt_free(new_prefix_tag);
1884 }
1885
1886 dt_tagging_completion_refresh(d->completion_store);
1889 dt_tag_free_result(&tag_family);
1890 dt_image_synch_xmps(tagged_images);
1891 g_list_free(tagged_images);
1892 tagged_images = NULL;
1893 }
1894
1895 if(tagid)
1896 {
1897 gint new_flags = ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(category)) ? DT_TF_CATEGORY : 0) |
1898 (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(private)) ? DT_TF_PRIVATE : 0));
1899 GtkTextIter start, end;
1900 gtk_text_buffer_get_start_iter(buffer, &start);
1901 gtk_text_buffer_get_end_iter(buffer, &end);
1902 gchar *new_synonyms_list = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
1903 // refresh iter
1904 _selection_get_first(selection, &model, &iter);
1905 GtkTreeIter store_iter;
1906 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
1907 gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
1908 &store_iter, &iter);
1909 if(new_flags != (flags & (DT_TF_CATEGORY | DT_TF_PRIVATE)))
1910 {
1911 new_flags = (flags & ~(DT_TF_CATEGORY | DT_TF_PRIVATE)) | new_flags;
1912 dt_tag_set_flags(tagid, new_flags);
1913 if(!d->tree_flag)
1914 gtk_list_store_set(GTK_LIST_STORE(store), &store_iter, DT_LIB_TAGGING_COL_FLAGS, new_flags, -1);
1915 else
1916 gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter, DT_LIB_TAGGING_COL_FLAGS, new_flags, -1);
1917 }
1918 if(new_synonyms_list && g_strcmp0(synonyms_list, new_synonyms_list) != 0)
1919 {
1920 dt_tag_set_synonyms(tagid, new_synonyms_list);
1921 if(!d->tree_flag)
1922 gtk_list_store_set(GTK_LIST_STORE(store), &store_iter, DT_LIB_TAGGING_COL_SYNONYM, new_synonyms_list, -1);
1923 else
1924 gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter, DT_LIB_TAGGING_COL_SYNONYM, new_synonyms_list, -1);
1925 }
1926 dt_free(new_synonyms_list);
1927 }
1928 }
1929 _init_treeview(self, 0);
1930 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1931 gtk_widget_destroy(dialog);
1932 dt_gui_refocus_parent(dialog_parent);
1933 dt_free(synonyms_list);
1934 dt_free(tagname);
1935}
1936
1937static gboolean _apply_rename_path(GtkWidget *dialog, const char *tagname,
1938 const char *newtag, dt_lib_module_t *self)
1939{
1941
1942 gboolean success = FALSE;
1943 GList *tag_family = NULL;
1944 GList *tagged_images = NULL;
1945 dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
1946
1947 // check if one of the new tagnames already exists.
1948 const int tagname_len = strlen(tagname);
1949 gboolean tagname_exists = FALSE;
1950 for(GList *taglist = tag_family; taglist && !tagname_exists; taglist = g_list_next(taglist))
1951 {
1952 char *new_tagname = g_strconcat(newtag, &((dt_tag_t *)taglist->data)->tag[tagname_len], NULL);
1953 tagname_exists = dt_tag_exists(new_tagname, NULL);
1954 if(tagname_exists)
1955 {
1956 GtkWidget *win;
1957 if(IS_NULL_PTR(dialog))
1958 win = dt_gui_main_window();
1959 else
1960 win = dialog;
1961
1962 GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(win), GTK_DIALOG_MODAL,
1963 GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
1964 _("at least one new tagname (%s) already exists, aborting."), new_tagname);
1965 gtk_dialog_run(GTK_DIALOG(warning_dialog));
1966 gtk_widget_destroy(warning_dialog);
1967 }
1968 dt_free(new_tagname);
1969 }
1970
1971 if(!tagname_exists)
1972 {
1973 for (GList *taglist = tag_family; taglist; taglist = g_list_next(taglist))
1974 {
1975 char *new_tagname = g_strconcat(newtag, &((dt_tag_t *)taglist->data)->tag[tagname_len], NULL);
1976 dt_tag_rename(((dt_tag_t *)taglist->data)->id, new_tagname);
1977 dt_free(new_tagname);
1978 }
1979 _init_treeview(self, 0);
1980 _init_treeview(self, 1);
1981 dt_tagging_completion_refresh(d->completion_store);
1983 dt_image_synch_xmps(tagged_images);
1985 _show_tag_on_view(d->dictionary_view, newtag, FALSE, TRUE);
1986 success = TRUE;
1987 }
1988 dt_tag_free_result(&tag_family);
1989 g_list_free(tagged_images);
1990 tagged_images = NULL;
1991
1992 return success;
1993}
1994
1995// rename path allows the user to redefine a hierarchy
1997{
1999
2000 char *tagname;
2001 gint tagid;
2002 gchar *text;
2003 GtkWidget *label;
2004 GtkTreeIter iter;
2005 GtkTreeModel *model = NULL;
2006 GtkTreeView *view = d->dictionary_view;
2007 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
2008 if(!_selection_get_first(selection, &model, &iter)) return;
2009
2010 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
2011 DT_LIB_TAGGING_COL_ID, &tagid, -1);
2012
2013 gint tag_count = 0;
2014 gint img_count = 0;
2015 dt_tag_count_tags_images(tagname, &tag_count, &img_count);
2016 if(tag_count == 0) return;
2017
2019 GtkWidget *dialog = gtk_dialog_new_with_buttons(_("change path"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT,
2020 _("cancel"), GTK_RESPONSE_NONE, _("save"), GTK_RESPONSE_YES, NULL);
2021 gtk_window_set_default_size(GTK_WINDOW(dialog), 300, -1);
2022 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
2023 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2024 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
2025 gtk_container_add(GTK_CONTAINER(area), vbox);
2026 text = g_strdup_printf(_("selected: %s"), tagname);
2027 label = gtk_label_new(text);
2028 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 0);
2029 dt_free(text);
2030
2031 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2032 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, TRUE, 0);
2033 text = g_strdup_printf(ngettext("<u>%d</u> tag will be updated", "<u>%d</u> tags will be updated", tag_count), tag_count);
2034 label = gtk_label_new(NULL);
2035 gtk_label_set_markup(GTK_LABEL(label), text);
2036 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
2037 dt_free(text);
2038 text = g_strdup_printf(ngettext("<u>%d</u> image will be updated", "<u>%d</u> images will be updated", img_count), img_count);
2039 label = gtk_label_new(NULL);
2040 gtk_label_set_markup(GTK_LABEL(label), text);
2041 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 0);
2042 dt_free(text);
2043
2044 GtkWidget *entry = gtk_entry_new();
2046 gtk_entry_set_text(GTK_ENTRY(entry), tagname);
2047 gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, TRUE, 0);
2048
2049#ifdef GDK_WINDOWING_QUARTZ
2051#endif
2052 gtk_widget_show_all(dialog);
2053
2054 if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
2055 {
2056 const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
2057 if(g_strcmp0(newtag, tagname) == 0)
2058 return; // no change
2059 char *message = NULL;
2060 if(!newtag[0])
2061 message = _("empty tag is not allowed, aborting");
2062 if(strchr(newtag, '|') == &newtag[0] || strchr(newtag, '|') == &newtag[strlen(newtag)-1] || strstr(newtag, "||"))
2063 message = _("'|' misplaced, empty tag is not allowed, aborting");
2064 if(message)
2065 {
2066 GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL,
2067 GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
2068 gtk_dialog_run(GTK_DIALOG(warning_dialog));
2069 gtk_widget_destroy(warning_dialog);
2070 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
2071 gtk_widget_destroy(dialog);
2072 dt_gui_refocus_parent(dialog_parent);
2073 dt_free(tagname);
2074 return;
2075 }
2076 _apply_rename_path(dialog, tagname, newtag, self);
2077 }
2078 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
2079 gtk_widget_destroy(dialog);
2080 dt_gui_refocus_parent(dialog_parent);
2081 dt_free(tagname);
2082}
2083
2085{
2087 GtkTreeIter iter;
2088 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
2089 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
2090 if(_selection_get_first(selection, &model, &iter))
2091 {
2092 char *path;
2093 guint count;
2094 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &path, DT_LIB_TAGGING_COL_COUNT, &count, -1);
2095 if(count)
2096 {
2097 if(!d->collection[0]) dt_collection_serialize(d->collection, 4096);
2098 gchar *tag_collection = g_strdup_printf("1:0:%d:%s$", DT_COLLECTION_PROP_TAG, path);
2100 dt_collection_deserialize(tag_collection);
2102 dt_free(tag_collection);
2103 }
2104 dt_free(path);
2105 }
2106}
2107
2109{
2111 if(d->collection[0])
2112 {
2114 dt_collection_deserialize(d->collection);
2116 d->collection[0] = '\0';
2117 }
2118}
2119
2121{
2123 GtkTreeIter iter;
2124 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
2125 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
2126 if(_selection_get_first(selection, &model, &iter))
2127 {
2128 char *tag;
2129 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tag, -1);
2130 gtk_entry_set_text(d->dict_entry, tag);
2131 dt_free(tag);
2132 gtk_entry_grab_focus_without_selecting(d->dict_entry);
2133 }
2134}
2135
2136// delete the given tag ids from the database (after a single confirmation), then refresh
2137static void _delete_tagids(GList *tagids, dt_lib_module_t *self)
2138{
2140 if(IS_NULL_PTR(tagids)) return;
2141 const guint nb = g_list_length(tagids);
2142
2143 // dry-run removal (FALSE) to count how many images would lose a tag
2144 guint img_count = 0;
2145 for(GList *t = tagids; t; t = g_list_next(t))
2146 img_count += dt_tag_remove(GPOINTER_TO_UINT(t->data), FALSE);
2147
2148 if(img_count > 0 || dt_conf_get_bool("plugins/lighttable/tagging/ask_before_delete_tag"))
2149 {
2151 GtkWidget *dialog = gtk_dialog_new_with_buttons(_("delete tag?"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT,
2152 _("cancel"), GTK_RESPONSE_NONE, _("delete"), GTK_RESPONSE_YES, NULL);
2153 gtk_window_set_default_size(GTK_WINDOW(dialog), 300, -1);
2154 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
2155 gchar *text = g_strdup_printf(ngettext("do you really want to delete %d tag?\n%d image is assigned these tags!",
2156 "do you really want to delete %d tags?\n%d images are assigned these tags!",
2157 img_count), nb, img_count);
2158 GtkWidget *label = gtk_label_new(text);
2159 gtk_container_set_border_width(GTK_CONTAINER(area), 8);
2160 gtk_container_add(GTK_CONTAINER(area), label);
2161 dt_free(text);
2162#ifdef GDK_WINDOWING_QUARTZ
2164#endif
2165 gtk_widget_show_all(dialog);
2166 const int res = gtk_dialog_run(GTK_DIALOG(dialog));
2167 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
2168 gtk_widget_destroy(dialog);
2169 dt_gui_refocus_parent(dialog_parent);
2170 if(res != GTK_RESPONSE_YES) return;
2171 }
2172
2173 GList *affected = NULL;
2174 for(GList *t = tagids; t; t = g_list_next(t))
2175 {
2176 const guint tagid = GPOINTER_TO_UINT(t->data);
2177 affected = g_list_concat(affected, dt_tag_get_images(tagid));
2178 gchar *name = dt_tag_get_name(tagid);
2179 dt_tag_remove(tagid, TRUE);
2180 if(name)
2181 {
2182 dt_control_log(_("tag %s removed"), name);
2183 dt_free(name);
2184 }
2185 }
2186
2187 _init_treeview(self, 0);
2188 _init_treeview(self, 1);
2189 dt_tagging_completion_refresh(d->completion_store);
2191 dt_image_synch_xmps(affected);
2192 g_list_free(affected);
2194}
2195
2197{
2199 GList *tagids = _selected_tagids(d->dictionary_view);
2200 _delete_tagids(tagids, self);
2201 g_list_free(tagids);
2202}
2203
2205{
2207 char *tagname;
2208 guint new_tagid;
2209
2210 GtkTreeIter iter;
2211 GtkTreeModel *model = NULL;
2212 GtkTreeView *view = d->dictionary_view;
2213 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
2214 if(!_selection_get_first(selection, &model, &iter)) return;
2215
2216 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname, -1);
2217
2218 dt_tag_new(tagname, &new_tagid);
2219 dt_control_log(_("tag %s created"), tagname);
2220
2221 _init_treeview(self, 1);
2222 dt_tagging_completion_refresh(d->completion_store);
2224 _show_tag_on_view(d->dictionary_view, tagname, FALSE, TRUE);
2225 dt_free(tagname);
2226
2227}
2228
2229static void _pop_menu_dictionary(GtkWidget *treeview, GdkEventButton *event, dt_lib_module_t *self)
2230{
2232 GtkTreeIter iter, child;
2233 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
2234 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
2235 const gint sel_cnt = gtk_tree_selection_count_selected_rows(selection);
2236 if(sel_cnt == 0) return;
2237
2238 GtkWidget *menu = gtk_menu_new();
2239 GtkWidget *menuitem;
2240
2241 // with several tags selected, only bulk CRUD makes sense
2242 if(sel_cnt > 1)
2243 {
2244 menuitem = gtk_menu_item_new_with_label(_("delete tags"));
2245 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_delete_selected, self);
2246 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2247 gtk_widget_show_all(GTK_WIDGET(menu));
2248 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2249 return;
2250 }
2251
2252 if(_selection_get_first(selection, &model, &iter))
2253 {
2254 guint tagid, count;
2255 gtk_tree_model_get(model, &iter,
2256 DT_LIB_TAGGING_COL_ID, &tagid,
2257 DT_LIB_TAGGING_COL_COUNT, &count, -1);
2258
2259 if(d->tree_flag || !d->suggestion_flag)
2260 {
2261 menuitem = gtk_menu_item_new_with_label(_("create tag..."));
2262 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2263 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_create_tag, self);
2264
2265 if(tagid)
2266 {
2267 menuitem = gtk_menu_item_new_with_label(_("delete tag"));
2268 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2269 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_delete_selected, self);
2270 }
2271
2272 if(gtk_tree_model_iter_children(model, &child, &iter))
2273 {
2274 menuitem = gtk_menu_item_new_with_label(_("delete node"));
2275 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2276 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_delete_node, self);
2277 }
2278
2279 menuitem = gtk_menu_item_new_with_label(_("edit..."));
2280 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2281 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_edit_tag, self);
2282
2283 }
2284
2285 if(d->tree_flag)
2286 {
2287 menuitem = gtk_menu_item_new_with_label(_("change path..."));
2288 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2289 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_change_path, self);
2290 }
2291
2292 if(d->tree_flag && !tagid)
2293 {
2294 menuitem = gtk_separator_menu_item_new();
2295 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2296
2297 menuitem = gtk_menu_item_new_with_label(_("set as a tag"));
2298 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2299 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_set_as_tag, self);
2300 }
2301
2302 if(!d->suggestion_flag)
2303 {
2304 menuitem = gtk_separator_menu_item_new();
2305 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2306 }
2307
2308 menuitem = gtk_menu_item_new_with_label(_("copy to entry"));
2309 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_copy_tag, self);
2310 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2311
2312 if(d->collection[0])
2313 {
2314 char *collection = g_malloc(4096);
2315 dt_collection_serialize(collection, 4096);
2316 if(g_strcmp0(d->collection, collection) == 0) d->collection[0] = '\0';
2317 dt_free(collection);
2318 }
2319 if(count || d->collection[0])
2320 {
2321 menuitem = gtk_separator_menu_item_new();
2322 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2323 if(count)
2324 {
2325 menuitem = gtk_menu_item_new_with_label(_("go to tag collection"));
2326 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_goto_tag_collection, self);
2327 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2328 }
2329 if(d->collection[0])
2330 {
2331 menuitem = gtk_menu_item_new_with_label(_("go back to work"));
2332 g_signal_connect(menuitem, "activate", (GCallback)_pop_menu_dictionary_goto_collection_back, self);
2333 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2334 }
2335 }
2336 gtk_widget_show_all(GTK_WIDGET(menu));
2337
2338 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
2339 }
2340}
2341
2342static gboolean _click_on_view_dictionary(GtkWidget *view, GdkEventButton *event, dt_lib_module_t *self)
2343{
2345 _unselect_all_in_view(d->attached_view);
2346
2347 const int button_pressed = (event->type == GDK_BUTTON_PRESS) ? event->button : 0;
2348 const gboolean shift_pressed = dt_modifier_is(event->state, GDK_SHIFT_MASK);
2349 if((button_pressed == 3) // contextual menu
2350 || (d->tree_flag && button_pressed == 1)) // expand & drag
2351 {
2352 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
2353 GtkTreePath *path = NULL;
2354 // Get tree path for row that was clicked
2355 if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
2356 {
2357 if(d->tree_flag && button_pressed == 1 && !shift_pressed)
2358 {
2359 GtkTreeModel *model = gtk_tree_view_get_model(d->dictionary_view);
2360 GtkTreeIter iter;
2361 gtk_tree_model_get_iter(model, &iter, path);
2362 char *tagname;
2363 gtk_tree_model_get(model, &iter,
2364 DT_LIB_TAGGING_COL_PATH, &tagname, -1);
2365 if(d->drag.tagname)
2366 {
2367 dt_free(d->drag.tagname);
2368 }
2369 d->drag.tagname = tagname;
2370 if(d->drag.path) gtk_tree_path_free(d->drag.path);
2371 d->drag.path = path;
2372 if(d->drag.lastpath) gtk_tree_path_free(d->drag.lastpath);
2373 d->drag.lastpath = NULL;
2374 return FALSE;
2375 }
2376 else if(button_pressed == 3)
2377 {
2378 // keep an existing multi-selection so the menu can act on it in bulk
2379 if(!gtk_tree_selection_path_is_selected(selection, path))
2380 {
2381 gtk_tree_selection_unselect_all(selection);
2382 gtk_tree_selection_select_path(selection, path);
2383 }
2384 _pop_menu_dictionary(view, event, self);
2385 gtk_tree_path_free(path);
2386 return TRUE;
2387 }
2388 else if(d->tree_flag && button_pressed == 1 && shift_pressed)
2389 {
2390 gtk_tree_selection_select_path(selection, path);
2391 gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path, TRUE);
2392 gtk_tree_path_free(path);
2393 return TRUE;
2394 }
2395 }
2396 gtk_tree_path_free(path);
2397 }
2398 return FALSE;
2399}
2400
2401static gboolean _dictionary_key_pressed(GtkWidget *view, GdkEventKey *event, dt_lib_module_t *self)
2402{
2404 _unselect_all_in_view(d->attached_view);
2405 GtkTreeIter iter;
2406 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
2407 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
2408 gboolean res = FALSE;
2409 guint key = dt_keys_mainpad_alternatives(event->keyval);
2410
2411 if(_selection_get_first(selection, &model, &iter))
2412 {
2413 GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
2414 switch(key)
2415 {
2416 case GDK_KEY_Left:
2417 if(path)
2418 {
2419 if(dt_modifier_is(event->state, GDK_SHIFT_MASK))
2420 gtk_tree_view_collapse_all(GTK_TREE_VIEW(view));
2421 else
2422 gtk_tree_view_collapse_row(GTK_TREE_VIEW(view), path);
2423 res = TRUE;
2424 }
2425 break;
2426 case GDK_KEY_Right:
2427 if(path)
2428 {
2429 gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path,
2430 dt_modifier_is(event->state, GDK_SHIFT_MASK));
2431 res = TRUE;
2432 }
2433 break;
2434 default:
2435 break;
2436 }
2437 gtk_tree_path_free(path);
2438 }
2439 if(event->keyval == GDK_KEY_Tab)
2440 {
2441 gtk_tree_selection_unselect_all(selection);
2442 res = TRUE;
2443 }
2444 else if(event->keyval == GDK_KEY_ISO_Left_Tab)
2445 {
2446 gtk_tree_selection_unselect_all(selection);
2447 gtk_widget_grab_focus(GTK_WIDGET(d->dict_entry));
2448 res = TRUE;
2449 }
2450 return res;
2451}
2452
2453static gboolean _row_tooltip_setup(GtkWidget *treeview, gint x, gint y, gboolean kb_mode,
2454 GtkTooltip* tooltip, dt_lib_module_t *self)
2455{
2456 gboolean res = FALSE;
2457 GtkTreePath *path = NULL;
2458 // Get tree path mouse position
2459 if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview), x, y, &path, NULL, NULL, NULL))
2460 {
2461 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
2462 GtkTreeIter iter;
2463 if(gtk_tree_model_get_iter(model, &iter, path))
2464 {
2465 char *tagname;
2466 guint tagid;
2467 guint flags;
2468 char *synonyms = NULL;
2469 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, DT_LIB_TAGGING_COL_TAG, &tagname,
2471 if(tagid)
2472 {
2473 if((flags & DT_TF_PRIVATE) || (synonyms && synonyms[0]))
2474 {
2475 gchar *text = g_strdup_printf(_("%s"), tagname);
2476 text = dt_util_dstrcat(text, " %s\n", (flags & DT_TF_PRIVATE) ? _("(private)") : "");
2477 text = dt_util_dstrcat(text, "synonyms: %s", (synonyms && synonyms[0]) ? synonyms : " - ");
2478 gtk_tooltip_set_text(tooltip, text);
2479 dt_free(text);
2480 res = TRUE;
2481 }
2482 }
2483 dt_free(synonyms);
2484 dt_free(tagname);
2485 }
2486 }
2487 gtk_tree_path_free(path);
2488
2489 return res;
2490}
2491
2492static void _import_button_clicked(GtkButton *button, dt_lib_module_t *self)
2493{
2494 const char *last_dirname = dt_conf_get_string_const("plugins/lighttable/tagging/last_import_export_location");
2495 if(IS_NULL_PTR(last_dirname) || !*last_dirname)
2496 {
2497 last_dirname = g_get_home_dir();
2498 }
2499
2501 GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
2502 _("select a keyword file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
2503 _("_import"), _("_cancel"));
2504
2505 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
2506 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);
2507
2508 if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
2509 {
2510 char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
2511 char *dirname = g_path_get_dirname(filename);
2512 dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
2513 uint32_t count = dt_tag_import(filename);
2514 dt_control_log(_("%u tags imported"), count);
2515 dt_free(filename);
2516 dt_free(dirname);
2517 }
2518
2519 g_object_unref(filechooser);
2520 _init_treeview(self, 1);
2521}
2522
2523static void _export_button_clicked(GtkButton *button, dt_lib_module_t *self)
2524{
2525 GDateTime *now = g_date_time_new_now_local();
2526 char *export_filename = g_date_time_format(now, "darktable_tags_%F_%H-%M.txt");
2527 const char *last_dirname = dt_conf_get_string_const("plugins/lighttable/tagging/last_import_export_location");
2528 if(IS_NULL_PTR(last_dirname) || !*last_dirname)
2529 {
2530 last_dirname = g_get_home_dir();
2531 }
2532
2534 GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
2535 _("select file to export to"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_SAVE,
2536 _("_export"), _("_cancel"));
2537
2538 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(filechooser), TRUE);
2539 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
2540 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(filechooser), export_filename);
2541
2542 if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
2543 {
2544 char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
2545 char *dirname = g_path_get_dirname(filename);
2546 dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
2547 const uint32_t count = dt_tag_export(filename);
2548 dt_control_log(_("%u tags exported"), count);
2549 dt_free(filename);
2550 dt_free(dirname);
2551 }
2552
2553 g_date_time_unref(now);
2554 dt_free(export_filename);
2555 g_object_unref(filechooser);
2556}
2557
2559{
2561 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
2562
2564
2565 d->suggestion_flag = dt_conf_get_bool("plugins/lighttable/tagging/nosuggestion");
2566 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_suggestion_button), d->suggestion_flag);
2567 d->tree_flag = dt_conf_get_bool("plugins/lighttable/tagging/treeview");
2568 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_tree_button), d->tree_flag);
2569
2570 if(d->tree_flag)
2571 {
2572 if(model == GTK_TREE_MODEL(d->dictionary_listfilter))
2573 {
2574 g_object_ref(model);
2575 gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), NULL);
2576 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
2577 gtk_list_store_clear(GTK_LIST_STORE(store));
2578 gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), GTK_TREE_MODEL(d->dictionary_treefilter));
2579 g_object_unref(d->dictionary_treefilter);
2580 }
2581 gtk_widget_set_sensitive(GTK_WIDGET(d->toggle_suggestion_button), FALSE);
2582 }
2583 else
2584 {
2585 if(model == GTK_TREE_MODEL(d->dictionary_treefilter))
2586 {
2587 g_object_ref(model);
2588 gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), NULL);
2589 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
2590 gtk_tree_store_clear(GTK_TREE_STORE(store));
2591 gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), GTK_TREE_MODEL(d->dictionary_listfilter));
2592 g_object_unref(d->dictionary_listfilter);
2593 }
2594 gtk_widget_set_sensitive(GTK_WIDGET(d->toggle_suggestion_button), TRUE);
2595 }
2596
2597 // drag & drop
2598 if(d->tree_flag)
2599 gtk_drag_source_set(GTK_WIDGET(d->dictionary_view), GDK_BUTTON1_MASK,
2600 target_list_tags, n_targets_tags, GDK_ACTION_MOVE);
2601 else
2602 gtk_drag_source_unset(GTK_WIDGET(d->dictionary_view));
2603
2604 d->sort_count_flag = dt_conf_get_bool("plugins/lighttable/tagging/listsortedbycount");
2605 dt_bauhaus_combobox_set(d->sort_combo, d->sort_count_flag ? 1 : 0);
2606
2607 // controls the dictionary list rendering (leaf vs full path); the sidebar attached
2608 // view has its own tree/list "view" combobox below
2609 d->hide_path_flag = dt_conf_get_bool("plugins/lighttable/tagging/hidehierarchy");
2610
2611 d->attached_tree_flag = dt_conf_get_bool("plugins/lighttable/tagging/attached_treeview");
2612 dt_bauhaus_combobox_set(d->attached_view_combo, d->attached_tree_flag ? 1 : 0);
2613
2614 d->dttags_flag = dt_conf_get_bool("plugins/lighttable/tagging/dttags");
2615 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->dttags_check), d->dttags_flag);
2616
2618}
2619
2620static void _toggle_suggestion_button_callback(GtkToggleButton *source, dt_lib_module_t *self)
2621{
2622 if(dt_gui_widgets_suppressed()) return;
2623 const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/nosuggestion");
2624 dt_conf_set_bool("plugins/lighttable/tagging/nosuggestion", new_state);
2625 _update_layout(self);
2626 _init_treeview(self, 1);
2627}
2628
2629static void _toggle_tree_button_callback(GtkToggleButton *source, dt_lib_module_t *self)
2630{
2631 if(dt_gui_widgets_suppressed()) return;
2632 const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/treeview");
2633 dt_conf_set_bool("plugins/lighttable/tagging/treeview", new_state);
2634 _update_layout(self);
2635 _init_treeview(self, 1);
2636}
2637
2638static gint _sort_tree_count_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
2639{
2640 guint count_a = 0;
2641 guint count_b = 0;
2642 gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_COUNT, &count_a, -1);
2643 gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_COUNT, &count_b, -1);
2644 return (count_b - count_a);
2645}
2646
2647static gint _sort_tree_tag_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
2648{
2649 char *tag_a = NULL;
2650 char *tag_b = NULL;
2651 gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_TAG, &tag_a, -1);
2652 gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_TAG, &tag_b, -1);
2653 if(IS_NULL_PTR(tag_a)) tag_a = g_strdup("");
2654 if(IS_NULL_PTR(tag_b)) tag_b = g_strdup("");
2655 const gboolean sort = g_strcmp0(tag_a, tag_b);
2656 dt_free(tag_a);
2657 dt_free(tag_b);
2658 return sort;
2659}
2660
2661static gint _sort_tree_path_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
2662{
2663 char *tag_a = NULL;
2664 char *tag_b = NULL;
2665 gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_PATH, &tag_a, -1);
2666 gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_PATH, &tag_b, -1);
2667 if(tag_a)
2668 {
2669 for(char *letter = tag_a; *letter; letter++)
2670 if(*letter == '|') *letter = '\1';
2671 }
2672 else
2673 tag_a = g_strdup("");
2674
2675 if(tag_b)
2676 {
2677 for(char *letter = tag_b; *letter; letter++)
2678 if(*letter == '|') *letter = '\1';
2679 }
2680 else
2681 tag_b = g_strdup("");
2682
2683 const gboolean sort = g_strcmp0(tag_a, tag_b);
2684 dt_free(tag_a);
2685 dt_free(tag_b);
2686 return sort;
2687}
2688
2689// "sort" combobox in the sidebar: by name (0) or by count (1)
2691{
2692 if(dt_gui_widgets_suppressed()) return;
2693 dt_conf_set_bool("plugins/lighttable/tagging/listsortedbycount", dt_bauhaus_combobox_get(combo) == 1);
2694 _update_layout(self);
2697}
2698
2699// "view" combobox in the sidebar: attached list (0) or attached tree (1)
2701{
2702 if(dt_gui_widgets_suppressed()) return;
2703 dt_conf_set_bool("plugins/lighttable/tagging/attached_treeview", dt_bauhaus_combobox_get(combo) == 1);
2704 _update_layout(self);
2705 _init_treeview(self, 0);
2706}
2707
2708// "show system tags" checkbox in the sidebar
2709static void _dttags_check_toggled(GtkToggleButton *source, dt_lib_module_t *self)
2710{
2711 if(dt_gui_widgets_suppressed()) return;
2713 d->dttags_flag = gtk_toggle_button_get_active(source);
2714 dt_conf_set_bool("plugins/lighttable/tagging/dttags", d->dttags_flag);
2715 _init_treeview(self, 0);
2716}
2717
2719{
2721 // clear entry boxes and query
2722 _entry_clear_suppressed(d->entry, self);
2723 _entry_clear_suppressed(d->dict_entry, self);
2724 _set_keyword(self);
2725 _init_treeview(self, 1);
2727}
2728
2730{
2731 return 3;
2732}
2733
2734// Single-column model feeding the tag entry autocompletion (full tag path).
2736
2737// (Re)build the set of tags used by the current collection. Used to seed the
2738// entry autocompletion when nothing has been typed yet.
2740{
2742 if(!d->collection_tags) return;
2743 g_hash_table_remove_all(d->collection_tags);
2744 GList *tags = NULL;
2746 for(GList *t = tags; t; t = g_list_next(t))
2747 {
2748 const dt_tag_t *tag = (const dt_tag_t *)t->data;
2749 if(IS_NULL_PTR(tag->tag)) continue;
2750 g_hash_table_add(d->collection_tags, g_strdup(tag->tag));
2751 }
2752 dt_tag_free_result(&tags);
2753}
2754
2755static gboolean _completion_match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter,
2756 gpointer user_data)
2757{
2758 gboolean res = FALSE;
2759 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
2760 dt_lib_tagging_t *d = self ? (dt_lib_tagging_t *)self->data : NULL;
2761
2762 GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
2763
2764 if(!GTK_IS_EDITABLE(e))
2765 {
2766 return FALSE;
2767 }
2768
2769 const gint cur_pos = gtk_editable_get_position(e);
2770 const gboolean onLastTag = (g_strstr_len(&key[cur_pos], -1, ",") == NULL);
2771 if(!onLastTag)
2772 {
2773 return FALSE;
2774 }
2775
2776 GtkTreeModel *model = gtk_entry_completion_get_model(completion);
2777 const int column = gtk_entry_completion_get_text_column(completion);
2778
2779 if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING)
2780 {
2781 return FALSE;
2782 }
2783
2784 const gchar *lastTag = g_strrstr(key, ",");
2785 if(!IS_NULL_PTR(lastTag))
2786 {
2787 lastTag++;
2788 }
2789 else
2790 {
2791 lastTag = key;
2792 }
2793
2794 char *tag = NULL;
2795 gtk_tree_model_get(model, iter, column, &tag, -1);
2796
2797 // nothing typed yet on the active tag token: default the suggestions to the
2798 // tags used by the current collection opened in lighttable
2799 if(lastTag[0] == '\0')
2800 {
2801 res = (d && d->collection_tags && tag) ? g_hash_table_contains(d->collection_tags, tag) : FALSE;
2802 dt_free(tag);
2803 return res;
2804 }
2805
2806 if(tag)
2807 {
2808 char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
2809 if(normalized)
2810 {
2811 char *casefold = g_utf8_casefold(normalized, -1);
2812 if(casefold)
2813 {
2814 res = g_strstr_len(casefold, -1, lastTag) != NULL;
2815 }
2816 dt_free(casefold);
2817 }
2818 dt_free(normalized);
2819 dt_free(tag);
2820 }
2821
2822 return res;
2823}
2824
2825static void _tree_selection_changed(GtkTreeSelection *treeselection, gpointer data)
2826{
2828}
2829
2831{
2833 if(!d->drag.root) return;
2834 GtkTreeModel *model = GTK_TREE_MODEL(d->dictionary_treestore);
2835 GtkTreeIter iter;
2836 gtk_tree_model_get_iter_first(model, &iter);
2837 char *name;
2838 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &name, -1);
2839 if(name && name[0] == '\0')
2840 gtk_tree_store_remove(d->dictionary_treestore, &iter);
2841 dt_free(name);
2842 d->drag.root = FALSE;
2843}
2844
2845static void _event_dnd_begin(GtkWidget *widget, GdkDragContext *context, dt_lib_module_t *self)
2846{
2848 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2849
2850 if(d->drag.path)
2851 {
2852 cairo_surface_t *row_pix = gtk_tree_view_create_row_drag_icon(tree, d->drag.path);
2853 // FIXME: try to put the anchor point on left bottom corner as for images
2854// cairo_surface_set_device_offset(row_pix,
2855// // the + 1 is for the black border in the icon
2856// - (double)(cairo_image_surface_get_width(row_pix) + 1),
2857// - (double)(cairo_image_surface_get_height(row_pix)+ 1));
2858 gtk_drag_set_icon_surface(context, row_pix);
2859 cairo_surface_destroy(row_pix);
2860 gtk_tree_path_free(d->drag.path);
2861 d->drag.path = NULL;
2862 d->drag.tag_source = TRUE;
2863 }
2864}
2865
2866static void _event_dnd_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data,
2867 const guint target_type, const guint time, dt_lib_module_t *self)
2868{
2869 if(target_type == DND_TARGET_TAG)
2870 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
2871 _DWORD, NULL, 0);
2872}
2873
2874static void _event_dnd_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
2875 GtkSelectionData *selection_data, guint target_type, guint time,
2876 dt_lib_module_t *self)
2877{
2879 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2880 // disable the default handler
2881 g_signal_stop_emission_by_name(tree, "drag-data-received");
2882 gboolean success = FALSE;
2883
2884 if(target_type == DND_TARGET_TAG)
2885 {
2886 GtkTreePath *path = NULL;
2887 if(gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
2888 {
2889 char *name;
2890 GtkTreeModel *model = gtk_tree_view_get_model(tree);
2891 GtkTreeIter iter;
2892
2893 gtk_tree_model_get_iter(model, &iter, path);
2894 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &name, -1);
2895 _dnd_clear_root(self);
2896 const gboolean root = (name && name[0] == '\0');
2897 char *leave = g_strrstr(d->drag.tagname, "|");
2898 if(leave) leave++;
2899 name = dt_util_dstrcat(name, "%s%s", root ? "" : "|", leave ? leave : d->drag.tagname);
2900 _apply_rename_path(NULL, d->drag.tagname, name, self);
2901
2902 dt_free(name);
2903 dt_free(d->drag.tagname);
2904 gtk_tree_path_free(path); // release result of gtk_tree_view_get_path_at_pos above
2905 success = TRUE;
2906 }
2907 }
2908 else if((target_type == DND_TARGET_IMGID) && (!IS_NULL_PTR(selection_data)))
2909 {
2910 GtkTreePath *path = NULL;
2911 const int imgs_nb = gtk_selection_data_get_length(selection_data) / sizeof(uint32_t);
2912 if(imgs_nb && gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
2913 {
2914 const uint32_t *imgt = (uint32_t *)gtk_selection_data_get_data(selection_data);
2915 GList *imgs = NULL;
2916 for(int i = 0; i < imgs_nb; i++)
2917 {
2918 imgs = g_list_prepend(imgs, GINT_TO_POINTER(imgt[i]));
2919 }
2920 GtkTreeModel *model = gtk_tree_view_get_model(tree);
2921 GtkTreeIter iter;
2922 gtk_tree_model_get_iter(model, &iter, path);
2923 gint tagid;
2924 gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
2925 if(tagid)
2926 dt_tag_attach_images(tagid, imgs, TRUE);
2927 g_list_free(imgs);
2928 imgs = NULL;
2929 _update_attached_count(tagid, d->dictionary_view, d->tree_flag);
2930 _init_treeview(self, 0);
2933 gtk_tree_path_free(path); // release result of gtk_tree_view_get_path_at_pos above
2934 success = TRUE;
2935 }
2936 }
2937 gtk_drag_finish(context, success, FALSE, time);
2938}
2939
2941{
2943 if(d->drag.lastpath)
2944 {
2945 gtk_tree_view_expand_row(d->dictionary_view, d->drag.lastpath, FALSE);
2946 }
2947 return FALSE;
2948}
2949
2951{
2953 if(d->drag.scroll_timeout)
2954 {
2955 GdkRectangle visible;
2956 gtk_tree_view_get_visible_rect(d->dictionary_view, &visible);
2957 gint top_ty;
2958 gtk_tree_view_convert_bin_window_to_tree_coords(d->dictionary_view,
2959 0, 0, NULL, &top_ty);
2960
2961 // temporary drop root
2962 if(d->drag.tag_source && !d->drag.root && d->drag.last_y < 5 && top_ty < 1)
2963 {
2964 GtkTreeIter iter;
2965 gtk_tree_store_prepend(d->dictionary_treestore, &iter, NULL);
2966 gtk_tree_store_set(d->dictionary_treestore, &iter,
2967 DT_LIB_TAGGING_COL_TAG, _("drop to root"),
2974 -1);
2975 d->drag.root = TRUE;
2976 }
2977 else if(d->drag.root && d->drag.last_y >= 20)
2978 _dnd_clear_root(self);
2979
2980 if(d->drag.last_y < 5)
2981 {
2982 // scroll up
2983 gtk_tree_view_scroll_to_point(d->dictionary_view, 0, top_ty - 25 < 0 ? 0 : top_ty - 25);
2984 }
2985 else if(d->drag.last_y > visible.height - 5)
2986 // scroll down
2987 gtk_tree_view_scroll_to_point(d->dictionary_view, 0, top_ty + 25);
2988 return TRUE;
2989 }
2990 return FALSE;
2991}
2992
2993static gboolean _event_dnd_motion(GtkWidget *widget, GdkDragContext *context,
2994 gint x, gint y, guint time, dt_lib_module_t *self)
2995{
2997 GtkTreeView *tree = GTK_TREE_VIEW(widget);
2998 GtkTreePath *path = NULL;
2999
3000 if(gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
3001 {
3002 if(IS_NULL_PTR(d->drag.lastpath) || ((d->drag.lastpath) && gtk_tree_path_compare(d->drag.lastpath, path) != 0))
3003 {
3004 GtkTreeViewColumn *col = gtk_tree_view_get_column(d->dictionary_view, 0);
3005 const int sel_width = gtk_tree_view_column_get_width(col);
3006 if(x >= sel_width)
3007 {
3008 if(!gtk_tree_view_row_expanded(tree, path))
3009 d->drag.expand_timeout = g_timeout_add(200, (GSourceFunc)_dnd_expand_timeout, self);
3010 }
3011 }
3012
3013 GtkTreeSelection *selection = gtk_tree_view_get_selection(d->dictionary_view);
3014 gtk_tree_selection_select_path(selection, path);
3015 d->drag.last_y = y;
3016 if(d->drag.scroll_timeout == 0)
3017 {
3018 d->drag.scroll_timeout = g_timeout_add(100, (GSourceFunc)_dnd_scroll_timeout, self);
3019 }
3020 }
3021
3022 // FIXME - no row highlihted... workartound with selection above
3023// gtk_tree_view_set_drag_dest_row(tree, path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
3024
3025 if(d->drag.lastpath)
3026 gtk_tree_path_free(d->drag.lastpath);
3027 d->drag.lastpath = path;
3028
3029 return TRUE;
3030}
3031
3032static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, dt_lib_module_t *self)
3033{
3035 // FIXME (see gtk_tree_view_set_drag_dest_row above)
3036// gtk_tree_view_set_drag_dest_row(d->dictionary_view, NULL, GTK_TREE_VIEW_DROP_BEFORE);
3037 GtkTreeSelection *selection = gtk_tree_view_get_selection(d->dictionary_view);
3038 gtk_tree_selection_unselect_all(selection);
3039 if(d->drag.scroll_timeout)
3040 g_source_remove(d->drag.scroll_timeout);
3041 d->drag.scroll_timeout = 0;
3042 d->drag.tag_source = FALSE;
3043 _dnd_clear_root(self);
3044}
3045
3046// open the tag management window (from the module's gear menu)
3047static void _show_manage_window(GtkMenuItem *menuitem, dt_lib_module_t *self)
3048{
3050 gtk_widget_show_all(d->manage_window);
3051 gtk_window_present(GTK_WINDOW(d->manage_window));
3052}
3053
3054// after a suggestion setting changed, rebuild the dictionary if it shows suggestions
3056{
3058 if(!d->tree_flag && d->suggestion_flag)
3059 _init_treeview(self, 1);
3060}
3061
3062static void _confidence_changed(GtkSpinButton *spin, dt_lib_module_t *self)
3063{
3064 if(dt_gui_widgets_suppressed()) return;
3065 dt_conf_set_int("plugins/lighttable/tagging/confidence", gtk_spin_button_get_value_as_int(spin));
3067}
3068
3069static void _recent_tags_changed(GtkSpinButton *spin, dt_lib_module_t *self)
3070{
3071 if(dt_gui_widgets_suppressed()) return;
3072 dt_conf_set_int("plugins/lighttable/tagging/nb_recent_tags", gtk_spin_button_get_value_as_int(spin));
3075}
3076
3077// validate button next to the entry: attach the typed/picked tag (same as pressing Enter)
3078static void _validate_button_clicked(GtkButton *button, dt_lib_module_t *self)
3079{
3081 _create_tag_from_entry(self, d->entry);
3082}
3083
3085{
3086 dt_lib_tagging_t *d = (dt_lib_tagging_t *)calloc(sizeof(dt_lib_tagging_t),1);
3087 self->data = (void *)d;
3088 d->last_tag = NULL;
3089 self->timeout_handle = 0;
3090
3091 self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3092
3093 GtkBox *box, *hbox;
3094 GtkWidget *button;
3095 GtkWidget *w;
3096 GtkTreeView *view;
3097 GtkTreeModel *model;
3098 GtkListStore *liststore;
3099 GtkTreeStore *treestore;
3100 GtkTreeViewColumn *col;
3101 GtkCellRenderer *renderer;
3102
3103 // ── line 1: "view" (list/tree) and "sort" (name/count) comboboxes ──
3104 hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING));
3105
3106 // "view" combobox: render the attached list above as a flat list, or as a hierarchical tree
3107 d->attached_view_combo = dt_bauhaus_combobox_new(dt_bauhaus_get_global(), DT_GUI_MODULE(NULL));
3108 dt_bauhaus_widget_set_label(d->attached_view_combo, _("view"));
3109 dt_bauhaus_combobox_add(d->attached_view_combo, _("list"));
3110 dt_bauhaus_combobox_add(d->attached_view_combo, _("tree"));
3112 gtk_widget_set_valign(d->attached_view_combo, GTK_ALIGN_CENTER);
3113 gtk_widget_set_tooltip_text(d->attached_view_combo,
3114 _("show the attached tags as a flat list or as a hierarchical tree"));
3115 g_signal_connect(G_OBJECT(d->attached_view_combo), "value-changed", G_CALLBACK(_attached_view_combo_changed), self);
3116 gtk_box_pack_start(hbox, d->attached_view_combo, TRUE, TRUE, 0);
3117
3118 // "sort" combobox: by name or by image count
3120 dt_bauhaus_widget_set_label(d->sort_combo, _("Sort by"));
3121 dt_bauhaus_combobox_add(d->sort_combo, _("name"));
3122 dt_bauhaus_combobox_add(d->sort_combo, _("count"));
3124 gtk_widget_set_valign(d->sort_combo, GTK_ALIGN_CENTER);
3125 gtk_widget_set_tooltip_text(d->sort_combo, _("sort the tags by name or by image count"));
3126 g_signal_connect(G_OBJECT(d->sort_combo), "value-changed", G_CALLBACK(_sort_combo_changed), self);
3127 gtk_box_pack_start(hbox, d->sort_combo, TRUE, TRUE, 0);
3128
3129 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hbox), FALSE, TRUE, 0);
3130
3131 // ── line 3: scrolled list / tree of attached tags ──
3132 // (packed here in code, but reordered below to sit under the entry — see the reorder call)
3133 box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
3134
3135 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
3136 view = GTK_TREE_VIEW(gtk_tree_view_new());
3137 // Static: the attached-tags list tracks the hovered/selected images, so keep a fixed height.
3138 w = dt_ui_scroll_wrap(GTK_WIDGET(view), 200, "plugins/lighttable/tagging/heightattachedwindow",
3140 gtk_box_pack_start(box, w, TRUE, TRUE, 0);
3141 d->attached_view = view;
3142 gtk_tree_view_set_enable_search(view, FALSE);
3143 gtk_tree_view_set_headers_visible(view, FALSE);
3144 liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING,
3145 G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN);
3146 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_PATH_ID,
3147 (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
3148 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_NAME_ID,
3149 (GtkTreeIterCompareFunc)_sort_tree_tag_func, self, NULL);
3150 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_COUNT_ID,
3151 (GtkTreeIterCompareFunc)_sort_tree_count_func, self, NULL);
3152 d->attached_liststore = liststore; // kept alive by d's ref (the view alternates between the two stores)
3153 // tree variant of the attached view, used when the "view" combobox is set to "tree"
3154 treestore = gtk_tree_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING,
3155 G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN);
3156 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(treestore), DT_TAG_SORT_PATH_ID,
3157 (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
3158 d->attached_treestore = treestore;
3159 g_object_set(G_OBJECT(view), "has-tooltip", TRUE, NULL);
3160 g_signal_connect(G_OBJECT(view), "query-tooltip", G_CALLBACK(_row_tooltip_setup), (gpointer)self);
3161
3162 col = gtk_tree_view_column_new();
3163 gtk_tree_view_append_column(view, col);
3164 renderer = gtk_cell_renderer_text_new();
3165 g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
3166 gtk_tree_view_column_pack_start(col, renderer, TRUE);
3167 gtk_tree_view_column_set_cell_data_func(col, renderer, _tree_tagname_show_attached, (gpointer)self, NULL);
3168 gtk_tree_view_column_set_expand(col, TRUE);
3169 gtk_tree_view_set_expander_column(view, col);
3170
3171 // per-row "delete" icon to detach a single tag in one click
3172 col = gtk_tree_view_column_new();
3173 gtk_tree_view_append_column(view, col);
3174 renderer = dtgtk_cell_renderer_button_new();
3175 g_object_set(renderer, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
3176 gtk_tree_view_column_pack_end(col, renderer, FALSE);
3177 gtk_tree_view_column_set_cell_data_func(col, renderer, _attached_delete_icon_show, NULL, NULL);
3178 g_signal_connect(renderer, "activate", G_CALLBACK(_attached_delete_icon_activated), (gpointer)self);
3179
3180 gtk_tree_selection_set_mode(gtk_tree_view_get_selection(view), GTK_SELECTION_MULTIPLE);
3181 gtk_tree_view_set_model(view, GTK_TREE_MODEL(liststore));
3182 // NB: liststore and treestore are not unref'd here — d keeps a ref on each so they
3183 // both survive while the view switches between list and tree mode (freed in gui_cleanup)
3184 gtk_widget_set_tooltip_text(GTK_WIDGET(view), _("attached tags,"
3185 "\nclick the trash icon, press Delete or double-click to detach"
3186 "\nselect several tags and right-click to detach them in bulk,"
3187 "\npress Tab to give the focus to entry,"
3188 "\nctrl-wheel scroll to resize the window"));
3189 g_signal_connect(G_OBJECT(view), "button-press-event", G_CALLBACK(_click_on_view_attached), (gpointer)self);
3190 g_signal_connect(G_OBJECT(view), "key-press-event", G_CALLBACK(_attached_key_pressed), (gpointer)self);
3191 g_signal_connect(gtk_tree_view_get_selection(view), "changed", G_CALLBACK(_tree_selection_changed), self);
3192
3193#define NEW_TOGGLE_BUTTON(paint, callback, tooltip, action) \
3194 button = dtgtk_togglebutton_new(paint, 0, NULL); \
3195 gtk_widget_set_tooltip_text(button, tooltip); \
3196 gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, TRUE, 0); \
3197 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(callback), self); \
3198
3199 // ── line 2: tag input entry (with autocompletion) ──
3200 box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
3201 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), FALSE, TRUE, 0);
3202 // the attached-tags list is built before this entry, so move the entry up to index 1
3203 // (just under the combos at index 0): final order is combos, entry, attached list, checkbox
3204 gtk_box_reorder_child(GTK_BOX(self->widget), GTK_WIDGET(box), 1);
3205
3206 hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING));
3207
3208 w = gtk_entry_new();
3210 gtk_entry_set_text(GTK_ENTRY(w), "");
3211 gtk_entry_set_width_chars(GTK_ENTRY(w), 0);
3212 // no widget tooltip here: it would pop up over the autocompletion list and hide it
3213 gtk_entry_set_placeholder_text(GTK_ENTRY(w), _("enter or pick a tag, Enter to attach"));
3214 gtk_entry_set_icon_from_icon_name(GTK_ENTRY(w), GTK_ENTRY_ICON_SECONDARY, "edit-clear-symbolic");
3215 gtk_entry_set_icon_tooltip_text(GTK_ENTRY(w), GTK_ENTRY_ICON_SECONDARY, _("clear entry"));
3216 g_signal_connect(G_OBJECT(w), "icon-release", G_CALLBACK(_entry_clear_icon), self);
3217 gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
3218 gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_RELEASE_MASK);
3219 d->entry = GTK_ENTRY(w);
3220
3221 // validate button: attach the typed/picked tag without having to hit Enter
3222 d->validate_button = dtgtk_button_new(dtgtk_cairo_paint_check_mark, 0, NULL);
3223 gtk_widget_set_tooltip_text(d->validate_button, _("attach this tag to the selected images"));
3224 gtk_box_pack_end(GTK_BOX(hbox), d->validate_button, FALSE, TRUE, 0);
3225 g_signal_connect(G_OBJECT(d->validate_button), "clicked", G_CALLBACK(_validate_button_clicked), (gpointer)self);
3226 gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
3227
3228 // autocompletion: every known tag feeds the model, suggestions default to the
3229 // current collection's tags when the entry holds a single token (see _completion_match_func).
3230 // minimum key length 1: an empty entry shows no popup, so the list does not hover over the
3231 // attached-tags view (e.g. right after a tag was attached and the entry cleared).
3232 d->completion_store = gtk_list_store_new(DT_COMPL_NUM_COLS, G_TYPE_STRING);
3233 {
3234 GtkEntryCompletion *completion = gtk_entry_completion_new();
3235 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(d->completion_store));
3236 gtk_entry_completion_set_text_column(completion, DT_COMPL_COL_PATH);
3237 gtk_entry_completion_set_inline_completion(completion, TRUE);
3238 gtk_entry_completion_set_popup_completion(completion, TRUE);
3239 gtk_entry_completion_set_popup_set_width(completion, FALSE);
3240 gtk_entry_completion_set_minimum_key_length(completion, 1);
3241 g_signal_connect(G_OBJECT(completion), "match-selected",
3242 G_CALLBACK(dt_tagging_completion_match_selected), self);
3243 gtk_entry_completion_set_match_func(completion, _completion_match_func, self, NULL);
3244 gtk_entry_set_completion(d->entry, completion);
3245 g_object_unref(completion);
3246 }
3247 g_object_unref(d->completion_store); // kept alive by the completion above
3248 // connected after gtk_entry_set_completion() so GtkEntryCompletion's own Return-key
3249 // handling (accepting a keyboard-highlighted popup match into the entry text) runs
3250 // before ours: otherwise a tag gets created from the raw typed prefix instead of the
3251 // selected suggestion (#905). dt_tagging_entry_clear() keeps that order when it reattaches the completion.
3252 g_signal_connect(G_OBJECT(d->entry), "key-press-event", G_CALLBACK(_enter_key_pressed), self);
3253
3254 // ── tag management popup window (persistent, hidden until requested) ────
3255 d->manage_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
3256#ifdef GDK_WINDOWING_QUARTZ
3257 dt_osx_disallow_fullscreen(d->manage_window);
3258#endif
3259 gtk_window_set_title(GTK_WINDOW(d->manage_window), _("manage tags"));
3260 gtk_window_set_transient_for(GTK_WINDOW(d->manage_window), GTK_WINDOW(dt_gui_main_window()));
3261 gtk_window_set_default_size(GTK_WINDOW(d->manage_window), DT_PIXEL_APPLY_DPI(400), DT_PIXEL_APPLY_DPI(600));
3262 // hide instead of destroy so the dictionary widgets/state persist across openings
3263 g_signal_connect(G_OBJECT(d->manage_window), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
3264
3265 box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
3266 gtk_container_set_border_width(GTK_CONTAINER(box), DT_PIXEL_APPLY_DPI(8));
3267 gtk_container_add(GTK_CONTAINER(d->manage_window), GTK_WIDGET(box));
3268
3269 // search entry to filter the dictionary
3270 w = gtk_entry_new();
3272 gtk_entry_set_text(GTK_ENTRY(w), "");
3273 gtk_entry_set_width_chars(GTK_ENTRY(w), 0);
3274 gtk_entry_set_placeholder_text(GTK_ENTRY(w), _("type to filter the tag dictionary below"));
3275 gtk_entry_set_icon_from_icon_name(GTK_ENTRY(w), GTK_ENTRY_ICON_SECONDARY, "edit-clear-symbolic");
3276 gtk_entry_set_icon_tooltip_text(GTK_ENTRY(w), GTK_ENTRY_ICON_SECONDARY, _("clear entry"));
3277 g_signal_connect(G_OBJECT(w), "icon-release", G_CALLBACK(_entry_clear_icon), self);
3278 gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_RELEASE_MASK);
3279 g_signal_connect(G_OBJECT(w), "changed", G_CALLBACK(_tag_name_changed), (gpointer)self);
3280 d->dict_entry = GTK_ENTRY(w);
3281 gtk_box_pack_start(box, w, FALSE, TRUE, 0);
3282
3283 // dictionary_view tree view
3284 view = GTK_TREE_VIEW(gtk_tree_view_new());
3285 // Static: keep a fixed height for stability alongside the attached-tags list above.
3286 w = dt_ui_scroll_wrap(GTK_WIDGET(view), 200, "plugins/lighttable/tagging/heightdictionarywindow",
3288 gtk_box_pack_start(box, w, TRUE, TRUE, 0);
3289 d->dictionary_view = view;
3290 gtk_tree_view_set_enable_search(view, FALSE);
3291 gtk_tree_view_set_headers_visible(view, FALSE);
3292 liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING,
3293 G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN);
3294 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_PATH_ID,
3295 (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
3296 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_NAME_ID,
3297 (GtkTreeIterCompareFunc)_sort_tree_tag_func, self, NULL);
3298 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(liststore), DT_TAG_SORT_COUNT_ID,
3299 (GtkTreeIterCompareFunc)_sort_tree_count_func, self, NULL);
3300 d->dictionary_liststore = liststore;
3301 model = gtk_tree_model_filter_new(GTK_TREE_MODEL(liststore), NULL);
3302 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model), DT_LIB_TAGGING_COL_VISIBLE);
3303 d->dictionary_listfilter = GTK_TREE_MODEL_FILTER(model);
3304 treestore = gtk_tree_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING,
3305 G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN);
3306 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(treestore), DT_TAG_SORT_PATH_ID,
3307 (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
3308 d->dictionary_treestore = treestore;
3309 model = gtk_tree_model_filter_new(GTK_TREE_MODEL(treestore), NULL);
3310 gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model), DT_LIB_TAGGING_COL_VISIBLE);
3311 d->dictionary_treefilter = GTK_TREE_MODEL_FILTER(model);
3312
3313 col = gtk_tree_view_column_new();
3314 gtk_tree_view_append_column(view, col);
3315 renderer = gtk_cell_renderer_text_new();
3316 g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
3317 gtk_tree_view_column_pack_start(col, renderer, TRUE);
3318 gtk_tree_view_column_set_cell_data_func(col, renderer, _tree_tagname_show_dictionary, (gpointer)self, NULL);
3319 gtk_tree_view_set_expander_column(view, col);
3320
3321 gtk_tree_selection_set_mode(gtk_tree_view_get_selection(view), GTK_SELECTION_MULTIPLE);
3322 gtk_widget_set_tooltip_text(GTK_WIDGET(view), _("tag dictionary,"
3323 "\nright-click to create, edit, delete tags,"
3324 "\nselect several tags and right-click to delete them in bulk,"
3325 "\nshift+click to fully expand the selected tag,"
3326 "\nctrl-scroll to resize the window"));
3327
3328 g_signal_connect(G_OBJECT(view), "button-press-event", G_CALLBACK(_click_on_view_dictionary), (gpointer)self);
3329 g_signal_connect(G_OBJECT(view), "key-press-event", G_CALLBACK(_dictionary_key_pressed), (gpointer)self);
3330 gtk_tree_view_set_model(view, GTK_TREE_MODEL(d->dictionary_listfilter));
3331 g_object_unref(d->dictionary_listfilter);
3332 g_object_set(G_OBJECT(view), "has-tooltip", TRUE, NULL);
3333 g_signal_connect(G_OBJECT(view), "query-tooltip", G_CALLBACK(_row_tooltip_setup), (gpointer)self);
3334 g_signal_connect(gtk_tree_view_get_selection(view), "changed", G_CALLBACK(_tree_selection_changed), self);
3335
3336 // drag & drop
3337 {
3338 d->drag.path = NULL;
3339 d->drag.tagname = NULL;
3340 d->drag.scroll_timeout = 0;
3341 d->drag.expand_timeout = 0;
3342 d->drag.root = FALSE;
3343 d->drag.tag_source = FALSE;
3344 gtk_drag_dest_set(GTK_WIDGET(d->dictionary_view), GTK_DEST_DEFAULT_ALL,
3345 target_list_tags_dest, n_targets_tags_dest, GDK_ACTION_MOVE);
3346 g_signal_connect(d->dictionary_view, "drag-data-get", G_CALLBACK(_event_dnd_get), self);
3347 g_signal_connect(d->dictionary_view, "drag-data-received", G_CALLBACK(_event_dnd_received), self);
3348 g_signal_connect_after(d->dictionary_view, "drag-begin", G_CALLBACK(_event_dnd_begin), self);
3349 g_signal_connect_after(d->dictionary_view, "drag-end", G_CALLBACK(_event_dnd_end), self);
3350 g_signal_connect(d->dictionary_view, "drag-motion", G_CALLBACK(_event_dnd_motion), self);
3351 }
3352
3353 // buttons
3354 hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING));
3355
3356 d->new_button = dt_action_button_new(self, N_("new"), _new_button_clicked, self, _("create a new tag with the\nname you entered"), 0, 0);
3357 gtk_box_pack_start(hbox, d->new_button, TRUE, TRUE, 0);
3358
3359 d->import_button = dt_action_button_new(self, N_("import..."), _import_button_clicked, self, _("import tags from a Lightroom keyword file"), 0, 0);
3360 gtk_box_pack_start(hbox, d->import_button, TRUE, TRUE, 0);
3361
3362 d->export_button = dt_action_button_new(self, N_("export..."), _export_button_clicked, self, _("export all tags to a Lightroom keyword file"), 0, 0);
3363 gtk_box_pack_start(hbox, d->export_button, TRUE, TRUE, 0);
3364
3366 _("toggle list / tree view"), N_("tree"));
3368 _("toggle list with / without suggestion"), N_("suggestion"));
3369
3370 gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
3371
3372 // ── tag-suggestion settings (formerly the module "preferences" dialog), applied live ──
3373 gtk_box_pack_start(box, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, DT_PIXEL_APPLY_DPI(4));
3374 {
3375 GtkWidget *grid = gtk_grid_new();
3376 gtk_grid_set_row_spacing(GTK_GRID(grid), DT_GUI_BOX_SPACING);
3377 gtk_grid_set_column_spacing(GTK_GRID(grid), DT_GUI_BOX_SPACING);
3378
3379 GtkWidget *lbl = gtk_label_new(_("suggested tags level of confidence"));
3380 gtk_widget_set_halign(lbl, GTK_ALIGN_START);
3381 gtk_widget_set_hexpand(lbl, TRUE);
3382 GtkWidget *spin = gtk_spin_button_new_with_range(0, 100, 1);
3383 gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), dt_conf_get_int("plugins/lighttable/tagging/confidence"));
3384 gtk_widget_set_tooltip_text(spin, _("level of confidence to include a tag in the suggestions list:"
3385 "\n0: all associated tags, 99: 99% matching tags,"
3386 "\n100: only the recently-used tags (faster)"));
3387 g_signal_connect(G_OBJECT(spin), "value-changed", G_CALLBACK(_confidence_changed), self);
3388 gtk_grid_attach(GTK_GRID(grid), lbl, 0, 0, 1, 1);
3389 gtk_grid_attach(GTK_GRID(grid), spin, 1, 0, 1, 1);
3390
3391 GtkWidget *lbl2 = gtk_label_new(_("number of recently attached tags"));
3392 gtk_widget_set_halign(lbl2, GTK_ALIGN_START);
3393 gtk_widget_set_hexpand(lbl2, TRUE);
3394 GtkWidget *spin2 = gtk_spin_button_new_with_range(-1, 1000, 1);
3395 gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin2), dt_conf_get_int("plugins/lighttable/tagging/nb_recent_tags"));
3396 gtk_widget_set_tooltip_text(spin2, _("number of recently attached tags shown in the suggestions list"
3397 "\n(-1 disables the recent-tags list)"));
3398 g_signal_connect(G_OBJECT(spin2), "value-changed", G_CALLBACK(_recent_tags_changed), self);
3399 gtk_grid_attach(GTK_GRID(grid), lbl2, 0, 1, 1, 1);
3400 gtk_grid_attach(GTK_GRID(grid), spin2, 1, 1, 1, 1);
3401
3402 gtk_box_pack_start(box, grid, FALSE, FALSE, 0);
3403 }
3404
3405 // "show system tags" checkbox: reveal the tags Ansel manages automatically (Ansel|…)
3406 d->dttags_check = gtk_check_button_new_with_label(_("Show system tags"));
3407 gtk_widget_set_tooltip_text(d->dttags_check, _("show the tags automatically attached by Ansel (names starting with “Ansel|”)"));
3408 d->dttags_flag = FALSE;
3409 g_signal_connect(G_OBJECT(d->dttags_check), "toggled", G_CALLBACK(_dttags_check_toggled), self);
3410 gtk_box_pack_start(GTK_BOX(self->widget), d->dttags_check, FALSE, TRUE, 0);
3411
3412#undef NEW_TOGGLE_BUTTON
3413
3414 /* connect to mouse over id */
3416 G_CALLBACK(_lib_tagging_redraw_callback), self);
3418 G_CALLBACK(_lib_tagging_tags_changed_callback), self);
3420 G_CALLBACK(_lib_selection_changed_callback), self);
3422 G_CALLBACK(_collection_updated_callback), self);
3423
3424 d->collection = g_malloc(4096);
3425 d->collection_tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
3426 _update_layout(self);
3427 _init_treeview(self, 0);
3428 _set_keyword(self);
3429 _init_treeview(self, 1);
3431 dt_tagging_completion_refresh(d->completion_store);
3433
3435 dt_gui_get_accels()->lighttable_accels, N_("Lighttable/Tags"),
3436 N_("Tag"), GDK_KEY_t, DT_PRIMARY_MASK, FALSE,
3437 _("Opens the quick tagging entry"));
3439 dt_gui_get_accels()->lighttable_accels, N_("Lighttable/Tags"),
3440 N_("Redo last tag"), GDK_KEY_t, GDK_MOD1_MASK, FALSE,
3441 _("Re-applies the last used tag"));
3443 dt_gui_get_accels()->map_accels, N_("Map/Tags"),
3444 N_("Tag"), GDK_KEY_t, DT_PRIMARY_MASK, FALSE,
3445 _("Opens the quick tagging entry"));
3447 dt_gui_get_accels()->map_accels, N_("Map/Tags"),
3448 N_("Redo last tag"), GDK_KEY_t, GDK_MOD1_MASK, FALSE,
3449 _("Re-applies the last used tag"));
3450}
3451
3453{
3454 if(IS_NULL_PTR(self->data)) return;
3457
3458 gchar *path = dt_accels_build_path(N_("Lighttable/Tags"), N_("Tag"));
3460 dt_free(path);
3461 path = dt_accels_build_path(N_("Lighttable/Tags"), N_("Redo last tag"));
3463 dt_free(path);
3464 path = dt_accels_build_path(N_("Map/Tags"), N_("Tag"));
3466 dt_free(path);
3467 path = dt_accels_build_path(N_("Map/Tags"), N_("Redo last tag"));
3469 dt_free(path);
3470
3475 if(d->manage_window) gtk_widget_destroy(d->manage_window);
3476 if(d->collection_tags) g_hash_table_destroy(d->collection_tags);
3477 // d kept its own ref on both attached stores so the view could switch between them
3478 if(d->attached_liststore) g_object_unref(d->attached_liststore);
3479 if(d->attached_treestore) g_object_unref(d->attached_treestore);
3480 dt_free(d->collection);
3481 if(d->drag.tagname)
3482 {
3483 dt_free(d->drag.tagname);
3484 }
3485 if(d->drag.path) gtk_tree_path_free(d->drag.path);
3486 dt_free(self->data);
3487}
3488
3489// http://stackoverflow.com/questions/4631388/transparent-floating-gtkentry
3490static gboolean _lib_tagging_tag_key_press(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self)
3491{
3493 guint key = dt_keys_mainpad_alternatives(event->keyval);
3494
3495 switch(key)
3496 {
3497 case GDK_KEY_Escape:
3498 g_list_free(d->floating_tag_imgs);
3499 d->floating_tag_imgs = NULL;
3500 gtk_widget_destroy(d->floating_tag_window);
3501 gtk_window_present(GTK_WINDOW(dt_gui_main_window()));
3502 return TRUE;
3503 case GDK_KEY_Tab:
3504 return TRUE;
3505 case GDK_KEY_Return:
3506 {
3507 const gchar *tag = gtk_entry_get_text(GTK_ENTRY(entry));
3508 const gboolean res = dt_tag_attach_string_list(tag, d->floating_tag_imgs, TRUE);
3509 if(res) dt_image_synch_xmps(d->floating_tag_imgs);
3510 g_list_free(d->floating_tag_imgs);
3511 d->floating_tag_imgs = NULL;
3512
3514 _save_last_tag_used(tag, d);
3515
3516 _init_treeview(self, 0);
3517 _init_treeview(self, 1);
3518 dt_tagging_completion_refresh(d->completion_store);
3520 gtk_widget_destroy(d->floating_tag_window);
3521 gtk_window_present(GTK_WINDOW(dt_gui_main_window()));
3523
3524 return TRUE;
3525 }
3526 }
3527 return FALSE; /* event not handled */
3528}
3529
3530static gboolean _lib_tagging_tag_destroy(GtkWidget *widget, GdkEvent *event, gpointer user_data)
3531{
3532 gtk_widget_destroy(GTK_WIDGET(user_data));
3533 return FALSE;
3534}
3535
3536
3540static gboolean _lib_tagging_tag_redo_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
3541 GdkModifierType mods, gpointer user_data)
3542{
3543 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
3545
3546 if(d->last_tag)
3547 {
3548 GList *imgs = dt_act_on_get_images();
3549 const gboolean res = dt_tag_attach_string_list(d->last_tag, imgs, TRUE);
3550 if(res) dt_image_synch_xmps(imgs);
3551 g_list_free(imgs);
3552 imgs = NULL;
3553 _init_treeview(self, 0);
3554 _init_treeview(self, 1);
3555 dt_tagging_completion_refresh(d->completion_store);
3558 }
3559
3560 return TRUE;
3561}
3562
3567static gboolean _lib_tagging_tag_show_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
3568 GdkModifierType mods, gpointer user_data)
3569{
3570 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
3572 if(d->tree_flag)
3573 {
3574 dt_control_log(_("tag shortcut is not active with tag tree view. please switch to list view"));
3575 return TRUE; // doesn't work properly with tree treeview
3576 }
3577
3578 d->floating_tag_imgs = dt_act_on_get_images();
3579 gint x, y;
3580 gint px, py, w, h;
3582 GtkWidget *center = dt_gui_center_widget();
3583 gdk_window_get_origin(gtk_widget_get_window(center), &px, &py);
3584
3585 w = gdk_window_get_width(gtk_widget_get_window(center));
3586 h = gdk_window_get_height(gtk_widget_get_window(center));
3587
3588 x = px + 0.5 * (w - FLOATING_ENTRY_WIDTH);
3589 y = py + h - 50;
3590
3591 /* put the floating box at the mouse pointer */
3592 // gint pointerx, pointery;
3593 // GdkDevice *device =
3594 // gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gtk_widget_get_display(widget)));
3595 // gdk_window_get_device_position (gtk_widget_get_window (widget), device, &pointerx, &pointery, NULL);
3596 // x = px + pointerx + 1;
3597 // y = py + pointery + 1;
3598
3599 d->floating_tag_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
3600#ifdef GDK_WINDOWING_QUARTZ
3601 dt_osx_disallow_fullscreen(d->floating_tag_window);
3602#endif
3603 /* stackoverflow.com/questions/1925568/how-to-give-keyboard-focus-to-a-pop-up-gtk-window */
3604 gtk_widget_set_can_focus(d->floating_tag_window, TRUE);
3605 gtk_window_set_decorated(GTK_WINDOW(d->floating_tag_window), FALSE);
3606 gtk_window_set_type_hint(GTK_WINDOW(d->floating_tag_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
3607 gtk_window_set_transient_for(GTK_WINDOW(d->floating_tag_window), GTK_WINDOW(window));
3608 gtk_widget_set_opacity(d->floating_tag_window, 0.8);
3609 gtk_window_move(GTK_WINDOW(d->floating_tag_window), x, y);
3610
3611 GtkWidget *entry = gtk_entry_new();
3613 gtk_widget_set_size_request(entry, FLOATING_ENTRY_WIDTH, -1);
3614 gtk_widget_add_events(entry, GDK_FOCUS_CHANGE_MASK);
3615
3616 GtkEntryCompletion *completion = gtk_entry_completion_new();
3617 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(d->completion_store));
3618 gtk_entry_completion_set_text_column(completion, DT_COMPL_COL_PATH);
3619 gtk_entry_completion_set_inline_completion(completion, TRUE);
3620 gtk_entry_completion_set_popup_set_width(completion, FALSE);
3621 gtk_entry_completion_set_minimum_key_length(completion, 0);
3622 g_signal_connect(G_OBJECT(completion), "match-selected",
3623 G_CALLBACK(dt_tagging_completion_match_selected), self);
3624 gtk_entry_completion_set_match_func(completion, _completion_match_func, self, NULL);
3625 gtk_entry_set_completion(GTK_ENTRY(entry), completion);
3626
3627 gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
3628 gtk_container_add(GTK_CONTAINER(d->floating_tag_window), entry);
3629 g_signal_connect(entry, "focus-out-event", G_CALLBACK(_lib_tagging_tag_destroy), d->floating_tag_window);
3630 g_signal_connect(entry, "key-press-event", G_CALLBACK(_lib_tagging_tag_key_press), self);
3631
3632 gtk_widget_show_all(d->floating_tag_window);
3633 gtk_widget_grab_focus(entry);
3634 gtk_window_present(GTK_WINDOW(d->floating_tag_window));
3635
3636 return TRUE;
3637}
3638
3640{
3641 const int length = dt_conf_get_int("plugins/lighttable/tagging/nb_recent_tags");
3642 if(length == -1) return length;
3643 else if(length >= 10/2) return length * 2;
3644 else return 10;
3645}
3646
3648{
3649 const char *list = dt_conf_get_string_const("plugins/lighttable/tagging/recent_tags");
3650 if(!list[0])
3651 return;
3652 const int length = _get_recent_tags_list_length();
3653 if(length == -1)
3654 {
3655 dt_conf_set_string("plugins/lighttable/tagging/recent_tags", "");
3656 return;
3657 }
3658
3659 char *p = (char *)list;
3660 int nb = 1;
3661 for(;*p != '\0'; p++)
3662 {
3663 if(*p == ',') nb++;
3664 }
3665
3666 if(nb > length)
3667 {
3668 nb -= length;
3669 char *list2 = g_strdup(list);
3670 for(; nb > 0; nb--)
3671 {
3672 p = g_strrstr(list2, "','");
3673 if(p) *p = '\0';
3674 }
3675 dt_conf_set_string("plugins/lighttable/tagging/recent_tags", list2);
3676 dt_free(list2);
3677 }
3678}
3679
3680// the module's gear menu opens the tag management window, which now also hosts the
3681// tag-suggestion settings that used to live in a separate "preferences" dialog
3682void set_preferences(void *menu, dt_lib_module_t *self)
3683{
3684 GtkWidget *mi = gtk_menu_item_new_with_label(_("manage tags..."));
3685 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_show_manage_window), self);
3686 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
3687}
3688
3689static void _save_last_tag_used(const char *tagnames, dt_lib_tagging_t *d)
3690{
3691 dt_free(d->last_tag);
3692 d->last_tag = g_strdup(tagnames);
3693
3694 const int nb_recent = _get_recent_tags_list_length();
3695
3696 if(nb_recent != -1)
3697 {
3698 GList *ntags = dt_util_str_to_glist(",", tagnames);
3699 if(ntags)
3700 {
3701 const char *sl = dt_conf_get_string_const("plugins/lighttable/tagging/recent_tags");
3702 // use "','" instead of "," to use the list as is in the query (dt_tag_get_suggestions)
3703 GList *tags = dt_util_str_to_glist("','", sl);
3704 for(GList *tag = ntags; tag; tag = g_list_next(tag))
3705 {
3706 char *escaped = sqlite3_mprintf("%q", (char *)tag->data);
3707 GList *found = g_list_find_custom(tags, escaped, (GCompareFunc)g_strcmp0);
3708 if(found)
3709 {
3710 tags = g_list_remove_link(tags, found);
3711 dt_free(found->data);
3712 g_list_free(found);
3713 found = NULL;
3714 }
3715 tags = g_list_prepend(tags, g_strdup(escaped));
3716 sqlite3_free(escaped);
3717 }
3718 g_list_free_full(ntags, dt_free_gpointer);
3719 ntags = NULL;
3720
3721 char *nsl = dt_util_glist_to_str("','", tags);
3722 dt_conf_set_string("plugins/lighttable/tagging/recent_tags", nsl);
3723 dt_free(nsl);
3724 if(g_list_length(tags) > nb_recent)
3726 g_list_free_full(tags, dt_free_gpointer);
3727 tags = NULL;
3728 }
3729 }
3730}
3731
3732// clang-format off
3733// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3734// vim: shiftwidth=2 expandtab tabstop=2 cindent
3735// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3736// clang-format on
gchar * dt_accels_build_path(const gchar *scope, const gchar *feature)
Build the accel path of feature under scope.
void dt_accels_new_action_shortcut(dt_accels_t *accels, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gpointer data, GtkWidget *target_widget, GtkAccelGroup *accel_group, const gchar *action_scope, const gchar *action_name, guint key_val, GdkModifierType accel_mods, const gboolean lock, const char *description)
Register a new shortcut for a generic action, setting up its path, default keys and accel group....
void dt_accels_remove_accel(dt_accels_t *accels, const char *path, gpointer data)
Recursively remove all accels for all shortcuts containing path. This is unneeded for accels attached...
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
GList * dt_act_on_get_images()
Definition act_on.c:38
GtkWidget * dt_gui_main_window(void)
struct dt_accels_t * dt_gui_get_accels(void)
GtkWidget * dt_gui_center_widget(void)
int button_pressed(struct dt_iop_module_t *self, double x, double y, double pressure, int which, int type, uint32_t state)
Definition ashift.c:4508
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
Definition bauhaus.c:1903
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2139
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2093
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1504
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1701
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:1827
@ DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT
Definition bauhaus.h:127
GtkWidget * dtgtk_button_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
Definition button.c:135
void dt_collection_deserialize(const char *buf)
Definition collection.c:763
int dt_collection_serialize(char *buf, int bufsize)
Definition collection.c:720
dt_collection_properties_t
Definition collection.h:120
@ DT_COLLECTION_PROP_TAG
Definition collection.h:140
dt_collection_change_t
Definition collection.h:160
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
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
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.
void dt_image_synch_xmps(const GList *img)
void dt_image_synch_xmp(const int selected)
int32_t dt_control_get_mouse_over_id()
Definition control.c:1032
void dt_control_log(const char *msg,...)
Definition control.c:839
void leave(dt_view_t *self)
Definition darkroom.c:2139
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:649
void dt_gui_refocus_parent(GtkWindow *parent)
Definition dialog.c:64
GtkWidget * window
GtkTreeStore * store
its model, owned by the view
GtkWidget * view
the tree view
#define _DWORD
@ DND_TARGET_IMGID
@ DND_TARGET_TAG
static const GtkTargetEntry target_list_tags_dest[]
static const guint n_targets_tags
static const guint n_targets_tags_dest
static const GtkTargetEntry target_list_tags[]
const int res
Definition dtpthread.h:351
dt_lib_tagging_cols_t
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:118
#define DT_GUI_MODULE(x)
GtkCellRenderer * dtgtk_cell_renderer_button_new(void)
const char * tooltip
Definition image.h:319
const char * model
void dt_gui_textview_set_padding(GtkTextView *textview)
Apply the standard recessed-input text padding to a GtkTextView.
Definition label.c:149
static const dt_iop_drawlayer_params_t * params
Definition layers.h:80
void dt_lib_cancel_postponed_update(dt_lib_module_t *mod)
Definition lib.c:1320
GtkWidget * dt_action_button_new(dt_lib_module_t *self, const gchar *label, gpointer callback, gpointer data, const gchar *tooltip, guint accel_key, GdkModifierType mods)
Definition lib.c:1347
void dt_lib_queue_postponed_update(dt_lib_module_t *mod, void(*update_fn)(dt_lib_module_t *self))
Definition lib.c:1308
#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
char * key
size_t size
Definition mipmap_cache.c:3
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
#define DT_MODULE(MODVER)
Instantiate the version handshake every module must export. Use once, at file scope,...
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:105
const char * name
Definition pdf.h:90
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, const char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
@ DT_UI_RESIZE_STATIC
Definition scroll_wrap.h:47
struct dt_selection_t * dt_selection_get_global(void)
Definition selection.c:80
int dt_selection_get_length(struct dt_selection_t *selection)
Definition selection.c:193
GList * dt_selection_get_list(struct dt_selection_t *selection)
Definition selection.c:185
void dt_control_signal_unblock_by_func(const struct dt_control_signal_t *ctlsig, GCallback cb, gpointer user_data)
Definition signal.c:541
void dt_control_signal_block_by_func(const struct dt_control_signal_t *ctlsig, GCallback cb, gpointer user_data)
Definition signal.c:535
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:411
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:390
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE
This signal is raised when mouse hovers over image thumbs both on lighttable and in the filmstrip....
Definition signal.h:63
@ DT_SIGNAL_TAG_CHANGED
This signal is raised when a tag is added/deleted/changed
Definition signal.h:134
@ DT_SIGNAL_SELECTION_CHANGED
This signal is raised when the selection is changed no param, no returned value.
Definition signal.h:131
@ DT_SIGNAL_COLLECTION_CHANGED
This signal is raised when collection changed. To avoid leaking the list, dt_collection_t is connecte...
Definition signal.h:126
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
const float uint32_t state[4]
const float r
GModule *void * data
Definition lib.h:79
GtkWidget * widget
Definition lib.h:83
guint timeout_handle
Definition lib.h:89
gchar * tagname
Definition tagging.c:107
GtkWidget * floating_tag_window
Definition tagging.c:99
GtkWidget * manage_window
Definition tagging.c:90
gboolean dttags_flag
Definition tagging.c:101
GtkListStore * attached_liststore
Definition tagging.c:95
GList * floating_tag_imgs
Definition tagging.c:100
GtkWidget * toggle_suggestion_button
Definition tagging.c:93
GtkTreeStore * attached_treestore
Definition tagging.c:96
GtkWidget * export_button
Definition tagging.c:92
char * collection
Definition tagging.c:103
GtkWidget * attached_view_combo
Definition tagging.c:94
GtkTreePath * lastpath
Definition tagging.c:108
GtkTreeModelFilter * dictionary_listfilter
Definition tagging.c:97
GHashTable * collection_tags
Definition tagging.c:98
gboolean attached_tree_flag
Definition tagging.c:102
GtkTreeView * attached_view
Definition tagging.c:91
GtkEntry * dict_entry
Definition tagging.c:89
gboolean root
Definition tagging.c:110
char * last_tag
Definition tagging.c:104
gboolean tree_flag
Definition tagging.c:119
char * newtagname
Definition tagging.c:117
gint tagid
Definition tagging.c:116
char * oldtagname
Definition tagging.c:118
static void _lib_tagging_redraw_callback(gpointer instance, dt_lib_module_t *self)
Definition tagging.c:639
static gboolean _attached_key_pressed(GtkWidget *view, GdkEventKey *event, dt_lib_module_t *self)
Definition tagging.c:1340
static gint _sort_tree_tag_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
Definition tagging.c:2647
static gint _sort_tree_count_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
Definition tagging.c:2638
static void _export_button_clicked(GtkButton *button, dt_lib_module_t *self)
Definition tagging.c:2523
static void _entry_clear_icon(GtkEntry *entry, const GtkEntryIconPosition position, GdkEvent *event, dt_lib_module_t *self)
Definition tagging.c:1450
static void _entry_clear_suppressed(GtkEntry *entry, dt_lib_module_t *self)
Definition tagging.c:1413
void gui_reset(dt_lib_module_t *self)
Definition tagging.c:2718
static gboolean _enter_key_pressed(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self)
Definition tagging.c:1419
int set_params(dt_lib_module_t *self, const void *params, int size)
Definition tagging.c:1030
static void _lib_tagging_tags_changed_callback(gpointer instance, dt_lib_module_t *self)
Definition tagging.c:645
static GList * _selected_tagids(GtkTreeView *view)
Definition tagging.c:1075
static gboolean _dnd_scroll_timeout(dt_lib_module_t *self)
Definition tagging.c:2950
static gboolean _select_previous_user_attached_tag(const int index, GtkTreeView *view)
Definition tagging.c:373
static void _pop_menu_dictionary_delete_node(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1474
void * get_params(dt_lib_module_t *self, int *size)
Definition tagging.c:1008
static void _tree_tagname_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data, gboolean dictionary_view)
Definition tagging.c:582
void set_preferences(void *menu, dt_lib_module_t *self)
Definition tagging.c:3682
static void _pop_menu_attached_detach(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1172
static gboolean _find_tag_iter_tagname(GtkTreeModel *model, GtkTreeIter *iter, const char *tagname, const gboolean needle)
Definition tagging.c:300
static gint _sort_tree_path_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self)
Definition tagging.c:2661
static void _toggle_tree_button_callback(GtkToggleButton *source, dt_lib_module_t *self)
Definition tagging.c:2629
static gboolean _set_matching_tag_visibility(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, dt_lib_module_t *self)
Definition tagging.c:220
static void _update_sel_on_tree(GtkTreeModel *model)
Definition tagging.c:809
static void _sort_combo_changed(GtkWidget *combo, dt_lib_module_t *self)
Definition tagging.c:2690
static void _validate_button_clicked(GtkButton *button, dt_lib_module_t *self)
Definition tagging.c:3078
static gboolean _row_tooltip_setup(GtkWidget *treeview, gint x, gint y, gboolean kb_mode, GtkTooltip *tooltip, dt_lib_module_t *self)
Definition tagging.c:2453
static gboolean _dictionary_key_pressed(GtkWidget *view, GdkEventKey *event, dt_lib_module_t *self)
Definition tagging.c:2401
static void _refresh_collection_tags(dt_lib_module_t *self)
Definition tagging.c:2739
static gboolean _update_tag_name_per_name(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, dt_tag_op_t *to)
Definition tagging.c:931
static void _event_dnd_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, const guint target_type, const guint time, dt_lib_module_t *self)
Definition tagging.c:2866
static void _new_button_clicked(GtkButton *button, dt_lib_module_t *self)
Definition tagging.c:1399
static void _dnd_clear_root(dt_lib_module_t *self)
Definition tagging.c:2830
static void _tag_name_changed(GtkEntry *entry, dt_lib_module_t *self)
Definition tagging.c:1460
static gboolean _lib_tagging_tag_redo_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval, GdkModifierType mods, gpointer user_data)
Re-apply the most recent tag expression to the current target images.
Definition tagging.c:3540
static void _detach_tagids(GList *tagids, dt_lib_module_t *self)
Definition tagging.c:1095
static void _update_attached_count(const int tagid, GtkTreeView *view, const gboolean tree_flag)
Definition tagging.c:976
#define NEW_TOGGLE_BUTTON(paint, callback, tooltip, action)
static void _unselect_all_in_view(GtkTreeView *view)
Definition tagging.c:177
static void _import_button_clicked(GtkButton *button, dt_lib_module_t *self)
Definition tagging.c:2492
static void _pop_menu_dictionary_change_path(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1996
static gboolean _tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
Definition tagging.c:247
static void _size_recent_tags_list()
Definition tagging.c:3647
static void _propagate_sel_to_parents(GtkTreeModel *model, GtkTreeIter *iter)
Definition tagging.c:206
static void _update_layout(dt_lib_module_t *self)
Definition tagging.c:2558
static void _create_tag_from_entry(dt_lib_module_t *self, GtkEntry *src)
Definition tagging.c:1369
static void _pop_menu_dictionary_delete_selected(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:2196
static void _tree_tagname_show_attached(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
Definition tagging.c:618
void gui_cleanup(dt_lib_module_t *self)
Definition tagging.c:3452
static void _attached_delete_icon_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
Definition tagging.c:1270
static void _pop_menu_dictionary_edit_tag(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1694
static gboolean _select_next_user_attached_tag(const int index, GtkTreeView *view)
Definition tagging.c:394
static void _show_manage_window(GtkMenuItem *menuitem, dt_lib_module_t *self)
Definition tagging.c:3047
static void _pop_menu_attached_attach_to_all(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1124
static void _pop_menu_dictionary(GtkWidget *treeview, GdkEventButton *event, dt_lib_module_t *self)
Definition tagging.c:2229
static void _set_keyword(dt_lib_module_t *self)
Definition tagging.c:916
static void _reset_sel_on_path_full(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
Definition tagging.c:743
static void _dttags_check_toggled(GtkToggleButton *source, dt_lib_module_t *self)
Definition tagging.c:2709
static void _sort_attached_list(dt_lib_module_t *self, gboolean force)
Definition tagging.c:264
static void _event_dnd_begin(GtkWidget *widget, GdkDragContext *context, dt_lib_module_t *self)
Definition tagging.c:2845
static void _recent_tags_changed(GtkSpinButton *spin, dt_lib_module_t *self)
Definition tagging.c:3069
static void _show_keyword_on_view(GtkTreeView *view, const char *keyword, const gboolean select)
Definition tagging.c:366
static gboolean _apply_rename_path(GtkWidget *dialog, const char *tagname, const char *newtag, dt_lib_module_t *self)
Definition tagging.c:1937
static void _calculate_sel_on_tree(GtkTreeModel *model, GtkTreeIter *iter)
Definition tagging.c:787
static void _lib_selection_changed_callback(gpointer instance, dt_lib_module_t *self)
Definition tagging.c:901
static void _find_root_iter_iter(GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *parent)
Definition tagging.c:764
void init_presets(dt_lib_module_t *self)
Definition tagging.c:1003
static void _pop_menu_dictionary_goto_collection_back(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:2108
static gboolean _completion_match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer user_data)
Definition tagging.c:2755
dt_tag_sort_id
Definition tagging.c:136
@ DT_TAG_SORT_COUNT_ID
Definition tagging.c:139
@ DT_TAG_SORT_NAME_ID
Definition tagging.c:138
@ DT_TAG_SORT_PATH_ID
Definition tagging.c:137
static void _pop_menu_dictionary_set_as_tag(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:2204
static gboolean _is_user_tag(GtkTreeModel *model, GtkTreeIter *iter)
Definition tagging.c:168
static void _pop_menu_dictionary_create_tag(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1566
static void _attached_view_combo_changed(GtkWidget *combo, dt_lib_module_t *self)
Definition tagging.c:2700
static void _show_tag_on_view(GtkTreeView *view, const char *tagname, const gboolean needle, const gboolean select)
Definition tagging.c:348
static void _confidence_changed(GtkSpinButton *spin, dt_lib_module_t *self)
Definition tagging.c:3062
static void _init_treeview(dt_lib_module_t *self, const int which)
Definition tagging.c:415
static void _collection_updated_callback(gpointer instance, dt_collection_change_t query_change, dt_collection_properties_t changed_property, gpointer imgs, int next, dt_lib_module_t *self)
Definition tagging.c:655
uint32_t container(dt_lib_module_t *self)
Definition tagging.c:163
static void _postponed_update(dt_lib_module_t *self)
Definition tagging.c:633
dt_lib_tagging_cols_t
Definition tagging.c:123
@ DT_LIB_TAGGING_COL_SYNONYM
Definition tagging.c:127
@ DT_LIB_TAGGING_NUM_COLS
Definition tagging.c:132
@ DT_LIB_TAGGING_COL_VISIBLE
Definition tagging.c:131
@ DT_LIB_TAGGING_COL_PATH
Definition tagging.c:126
@ DT_LIB_TAGGING_COL_COUNT
Definition tagging.c:128
@ DT_LIB_TAGGING_COL_TAG
Definition tagging.c:124
@ DT_LIB_TAGGING_COL_ID
Definition tagging.c:125
@ DT_LIB_TAGGING_COL_FLAGS
Definition tagging.c:130
@ DT_LIB_TAGGING_COL_SEL
Definition tagging.c:129
#define FLOATING_ENTRY_WIDTH
Definition tagging.c:82
static void _reset_sel_on_path(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
Definition tagging.c:723
static void _pop_menu_attached_detach_all(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:1181
static gboolean _selection_get_first(GtkTreeSelection *selection, GtkTreeModel **model, GtkTreeIter *iter)
Definition tagging.c:186
static void _delete_tagids(GList *tagids, dt_lib_module_t *self)
Definition tagging.c:2137
static int _get_recent_tags_list_length()
Definition tagging.c:3639
static void _refresh_suggestions(dt_lib_module_t *self)
Definition tagging.c:3055
static void _toggle_suggestion_button_callback(GtkToggleButton *source, dt_lib_module_t *self)
Definition tagging.c:2620
static void _event_dnd_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, dt_lib_module_t *self)
Definition tagging.c:2874
static void _tree_selection_changed(GtkTreeSelection *treeselection, gpointer data)
Definition tagging.c:2825
static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, dt_lib_module_t *self)
Definition tagging.c:3032
static void _pop_menu_dictionary_goto_tag_collection(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:2084
void gui_init(dt_lib_module_t *self)
Definition tagging.c:3084
int position()
Definition tagging.c:2729
static gboolean _lib_tagging_tag_key_press(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self)
Definition tagging.c:3490
static gboolean _lib_tagging_tag_show_accel(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval, GdkModifierType mods, gpointer user_data)
Open the floating quick-tag entry used by the historical tagging shortcut, now registered in the acti...
Definition tagging.c:3567
static void _delete_tree_path(GtkTreeModel *model, GtkTreeIter *iter, gboolean root, gboolean tree)
Definition tagging.c:842
const char ** views(dt_lib_module_t *self)
Definition tagging.c:157
static gboolean _lib_tagging_tag_destroy(GtkWidget *widget, GdkEvent *event, gpointer user_data)
Definition tagging.c:3530
static void _calculate_sel_on_path(GtkTreeModel *model, GtkTreeIter *iter, const gboolean root)
Definition tagging.c:706
static gboolean _attached_delete_icon_activated(GtkCellRenderer *cell, GdkEvent *event, GtkWidget *treeview, const gchar *path_str, GdkRectangle *background, GdkRectangle *cell_area, GtkCellRendererState flags, gpointer user_data)
Definition tagging.c:1244
static void _tree_tagname_show_dictionary(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
Definition tagging.c:625
static gboolean _click_on_view_dictionary(GtkWidget *view, GdkEventButton *event, dt_lib_module_t *self)
Definition tagging.c:2342
static gboolean _dnd_expand_timeout(dt_lib_module_t *self)
Definition tagging.c:2940
static void _raise_signal_tag_changed(dt_lib_module_t *self)
Definition tagging.c:666
static void _save_last_tag_used(const char *tags, dt_lib_tagging_t *d)
Definition tagging.c:3689
@ DT_COMPL_NUM_COLS
Definition tagging.c:2735
@ DT_COMPL_COL_PATH
Definition tagging.c:2735
static gboolean _event_dnd_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, dt_lib_module_t *self)
Definition tagging.c:2993
static void _pop_menu_attached(GtkWidget *treeview, GdkEventButton *event, dt_lib_module_t *self)
Definition tagging.c:1201
static void _pop_menu_dictionary_copy_tag(GtkWidget *menuitem, dt_lib_module_t *self)
Definition tagging.c:2120
static gboolean _find_tag_iter_tagid(GtkTreeModel *model, GtkTreeIter *iter, const gint tagid)
Definition tagging.c:684
static void _update_atdetach_buttons(dt_lib_module_t *self)
Definition tagging.c:201
static void _sort_dictionary_list(dt_lib_module_t *self, gboolean force)
Definition tagging.c:282
static void _show_iter_on_view(GtkTreeView *view, GtkTreeIter *iter, const gboolean select)
Definition tagging.c:333
static gboolean _click_on_view_attached(GtkWidget *view, GdkEventButton *event, dt_lib_module_t *self)
Definition tagging.c:1279
gboolean dt_tagging_completion_match_selected(GtkEntryCompletion *completion, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
void dt_tagging_completion_refresh(GtkListStore *store)
void dt_tagging_entry_clear(GtkEntry *entry, GCallback key_handler, gpointer user_data)
GList * dt_tag_get_images_from_list(const GList *img, const gint tagid)
Definition tags.c:834
GList * dt_tag_get_images(const gint tagid)
Definition tags.c:829
void dt_tag_get_tags_images(const gchar *keyword, GList **tag_list, GList **img_list)
Definition tags.c:880
guint dt_tag_remove_list(GList *tag_list)
Definition tags.c:211
gboolean dt_tag_attach(const guint tagid, const int32_t imgid, const gboolean undo_on, const gboolean group_on)
Definition tags.c:377
gboolean dt_tag_set_tags(const GList *tags, const GList *img, const gboolean ignore_dt_tags, const gboolean clear_on, const gboolean undo_on)
Definition tags.c:398
uint32_t dt_tag_get_suggestions(GList **result)
Definition tags.c:852
uint32_t dt_tag_get_attached(const int32_t imgid, GList **result, const gboolean ignore_dt_tags)
Definition tags.c:515
gboolean dt_tag_attach_images(const guint tagid, const GList *img, const gboolean undo_on)
Definition tags.c:355
gint dt_tag_get_flags(gint tagid)
Definition tags.c:970
gboolean dt_tag_attach_string_list(const gchar *tags, const GList *img, const gboolean undo_on)
Definition tags.c:418
uint32_t dt_tag_images_count(gint tagid)
Definition tags.c:885
gboolean dt_tag_new(const char *name, guint *tagid)
Definition tags.c:164
uint32_t dt_tag_get_collection_tags(GList **result)
Definition tags.c:910
void dt_tag_rename(const guint tagid, const gchar *new_tagname)
Definition tags.c:247
gchar * dt_tag_get_name(const guint tagid)
Definition tags.c:242
uint32_t dt_tag_import(const char *filename)
Definition tags.c:1025
void dt_tag_set_flags(gint tagid, gint flags)
Definition tags.c:975
guint dt_tag_remove(const guint tagid, gboolean final)
Definition tags.c:195
gboolean dt_tag_detach_images(const guint tagid, const GList *img, const gboolean undo_on)
Definition tags.c:462
void dt_tag_free_result(GList **result)
Definition tags.c:1003
GList * dt_sort_tag(GList *tags, gint sort_type)
Definition tags.c:596
gboolean dt_tag_exists(const char *name, guint *tagid)
Definition tags.c:255
void dt_tag_set_synonyms(gint tagid, gchar *synonyms_entry)
Definition tags.c:961
uint32_t dt_tag_export(const char *filename)
Definition tags.c:1160
void dt_tag_count_tags_images(const gchar *keyword, int *tag_count, int *img_count)
Definition tags.c:875
uint32_t dt_tag_get_with_usage(GList **result)
Definition tags.c:890
@ DT_TS_ALL_IMAGES
Definition tags.h:76
@ DT_TS_NO_IMAGE
Definition tags.h:74
@ DT_TS_SOME_IMAGES
Definition tags.h:75
@ DT_TF_CATEGORY
Definition tags.h:64
@ DT_TF_PRIVATE
Definition tags.h:65
GList * dt_util_str_to_glist(const gchar *separator, const gchar *text)
Definition utility.c:856
gchar * dt_util_glist_to_str(const gchar *separator, GList *items)
Definition utility.c:193
gchar * dt_util_dstrcat(gchar *str, const gchar *format,...)
Definition utility.c:122
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_widget_freeze()
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dtgtk_cairo_paint_treelist(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
void dtgtk_cairo_paint_check_mark(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
void dtgtk_cairo_paint_plus_simple(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
@ DT_UI_CONTAINER_PANEL_LEFT_CENTER