Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
libs/metadata.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2010-2017 Tobias Ellinghaus.
4 Copyright (C) 2011 Alexandre Prokoudine.
5 Copyright (C) 2011 Antony Dovgal.
6 Copyright (C) 2011 Brian Teague.
7 Copyright (C) 2011, 2013 Henrik Andersson.
8 Copyright (C) 2011, 2013 johannes hanika.
9 Copyright (C) 2011 Jérémy Rosen.
10 Copyright (C) 2011 Robert Bieber.
11 Copyright (C) 2012 Richard Wonka.
12 Copyright (C) 2013 José Carlos García Sogo.
13 Copyright (C) 2013-2016 Roman Lebedev.
14 Copyright (C) 2013 Thomas Pryds.
15 Copyright (C) 2014, 2019-2021 Pascal Obry.
16 Copyright (C) 2018, 2020 Heiko Bauke.
17 Copyright (C) 2018 Maurizio Paglia.
18 Copyright (C) 2018 rawfiner.
19 Copyright (C) 2019, 2022-2023, 2025-2026 Aurélien PIERRE.
20 Copyright (C) 2019-2022 Philippe Weyland.
21 Copyright (C) 2019 Sam Smith.
22 Copyright (C) 2020-2021 Aldric Renaudin.
23 Copyright (C) 2020-2021 Chris Elston.
24 Copyright (C) 2020-2022 Diederik Ter Rahe.
25 Copyright (C) 2020-2021 Hubert Kowalski.
26 Copyright (C) 2020 Marco.
27 Copyright (C) 2020 parafin.
28 Copyright (C) 2020-2021 Ralf Brown.
29 Copyright (C) 2021 luzpaz.
30 Copyright (C) 2021 Vincent THOMAS.
31 Copyright (C) 2022 Martin Bařinka.
32
33 darktable is free software: you can redistribute it and/or modify
34 it under the terms of the GNU General Public License as published by
35 the Free Software Foundation, either version 3 of the License, or
36 (at your option) any later version.
37
38 darktable is distributed in the hope that it will be useful,
39 but WITHOUT ANY WARRANTY; without even the implied warranty of
40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41 GNU General Public License for more details.
42
43 You should have received a copy of the GNU General Public License
44 along with darktable. If not, see <http://www.gnu.org/licenses/>.
45*/
46
47#include "metadata/metadata.h"
48#include "common/act_on.h"
49#include "system/macros.h"
50#include "system/mem_alloc.h"
52#include "widgets/gdkkeys.h"
55#include "common/conf.h"
56#include "widgets/label.h"
59#include "control/signal.h"
60
61#include "gui/application.h"
62#include "libs/lib.h"
63#include "libs/lib_api.h"
64#ifdef GDK_WINDOWING_QUARTZ
65#include "osx/osx.h"
66#endif
67#include <gdk/gdkkeysyms.h>
68
69DT_MODULE(3)
70
79
88
89const char *name(struct dt_lib_module_t *self)
90{
91 return _("Metadata");
92}
93
94const char **views(dt_lib_module_t *self)
95{
96 static const char *v[] = {"lighttable", "tethering", "studio_capture", NULL};
97 return v;
98}
99
101{
103}
104
105void _textbuffer_changed(GtkTextBuffer *textbuffer, dt_lib_module_t *self);
106static void _write_metadata(GtkTextView *textview, dt_lib_module_t *self);
107
108static gboolean _is_leave_unchanged(GtkTextView *textview)
109{
110 return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(textview), "tv_multiple"));
111}
112
113static gchar *_get_buffer_text(GtkTextView *textview)
114{
115 GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
116 GtkTextIter start, end;
117 gtk_text_buffer_get_bounds(buffer, &start, &end);
118 return gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
119}
120
121static void _text_set_all_selected(GtkTextView *textview, const gboolean selected)
122{
123 GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
124 GtkTextIter start, end;
125 gtk_text_buffer_get_bounds(buffer, &start, &end);
126 gtk_text_buffer_select_range(buffer, selected ? &start : &end, &end);
127}
128
129static void _text_set_italic(GtkTextView *textview, const gboolean italic)
130{
131 GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
132 GtkTextIter start, end;
133 gtk_text_buffer_get_bounds(buffer, &start, &end);
134 if(italic)
135 gtk_text_buffer_apply_tag_by_name(buffer, "italic", &start, &end);
136 else
137 gtk_text_buffer_remove_tag_by_name(buffer, "italic", &start, &end);
138}
139
140static void _set_text_buffer(GtkTextBuffer *buffer, const char *text)
141{
142 g_signal_handlers_block_matched(buffer, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _textbuffer_changed, NULL);
143 gtk_text_buffer_set_text(buffer, text, -1);
144 g_signal_handlers_unblock_matched(buffer, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _textbuffer_changed, NULL);
145}
146
147static void _fill_text_view(const uint32_t i, const uint32_t count, dt_lib_module_t *self)
148{
150 gboolean multi = FALSE;
151
152 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->textview[i]);
153 if(count == 0) // no metadata value
154 {
155 _set_text_buffer(buffer, "");
156 }
157 else if(count == 1) // images with different metadata values
158 {
159 _set_text_buffer(buffer, _("<leave unchanged>"));
160 multi = TRUE;
161 }
162 else // one or several images with the same metadata value
163 {
164 _set_text_buffer(buffer, (char *)d->metadata_list[i]->data);
165 }
166 g_object_set_data(G_OBJECT(d->textview[i]), "tv_multiple", GINT_TO_POINTER(multi));
167 _text_set_italic(d->textview[i], multi);
168}
169
170/* The two per-key arrays _update() is filling, plus the selection size the row count is
171 * compared against. */
178
179static void _selected_metadata_row(void *user_data, const int keyid, const char *value,
180 const uint32_t count)
181{
183
184 // an empty or absent value is not a value: the panel would show a blank entry for it
185 if(IS_NULL_PTR(value) || value[0] == '\0') return;
186 if(keyid < 0 || (uint32_t)keyid >= DT_METADATA_NUMBER) return;
187
188 // count == imgs_count means every selected image agrees on this one value
189 ctx->metadata_count[keyid] = (count == ctx->imgs_count) ? 2 : 1;
190 ctx->metadata[keyid] = g_list_append(ctx->metadata[keyid], g_strdup(value));
191}
192
193/* Write the field being typed in, if any, to the images the panel shows. What was typed is
194 * written as soon as it stops being edited -- a click elsewhere, another image, Tab or Enter --
195 * the way any text field behaves; only Escape discards it. */
197{
199 if(!d->editing) return;
200
201 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
202 {
203 if(!IS_NULL_PTR(d->textview[i]) && gtk_widget_has_focus(GTK_WIDGET(d->textview[i])))
204 {
205 _write_metadata(d->textview[i], self);
206 return;
207 }
208 }
209}
210
211static void _refresh(dt_lib_module_t *self);
212
213/* Follow the images to act on: when they change, commit the field being edited to the images
214 * it was typed for, then show the new ones. */
215static void _update(dt_lib_module_t *self)
216{
219
220 GList *imgs = dt_act_on_get_images();
221
222 // first we want to make sure the list of images to act on has changed
223 // this is not the case if mouse hover change but still stay in selection for ex.
224 if(IS_NULL_PTR(imgs) && IS_NULL_PTR(d->last_act_on)) return;
225 if(imgs && d->last_act_on)
226 {
227 const GList *l = d->last_act_on;
228 const GList *ll = imgs;
229 while(l && ll && GPOINTER_TO_INT(l->data) == GPOINTER_TO_INT(ll->data))
230 {
231 l = g_list_next(l);
232 ll = g_list_next(ll);
233 }
234 // The same images only if both lists end together: one being the start of the other -- a
235 // selection grown by a click -- is a change.
236 if(IS_NULL_PTR(l) && IS_NULL_PTR(ll))
237 {
238 g_list_free(imgs);
239 return;
240 }
241 }
242
243 // A field still being typed in belongs to the images it was typed for: write it to them
244 // before the panel moves on, as a click elsewhere would.
246
247 g_list_free(d->last_act_on);
248 d->last_act_on = imgs;
249 _refresh(self);
250}
251
252/* Read what the images the panel shows hold into its fields. Called when they change, and after
253 * a write, since a field shared by several images may have become one common value. */
254static void _refresh(dt_lib_module_t *self)
255{
257
258 GList *metadata[DT_METADATA_NUMBER];
259 uint32_t metadata_count[DT_METADATA_NUMBER];
260
261 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
262 {
263 metadata[i] = NULL;
264 metadata_count[i] = 0;
265 }
266
267 // using dt_metadata_get() is not possible here. we want to do all this in a single pass, everything else
268 // takes ages.
269 const uint32_t imgs_count = g_list_length(d->last_act_on);
270
271 if(imgs_count > 0)
272 {
273 _selected_metadata_ctx_t ctx = { .metadata = metadata, .metadata_count = metadata_count,
274 .imgs_count = imgs_count };
276 }
277
278 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
279 {
280 const uint32_t keyid = dt_metadata_get_keyid_by_display_order(i);
282 continue;
283 g_list_free_full(d->metadata_list[i], dt_free_gpointer);
284 d->metadata_list[i] = metadata[keyid];
285 metadata[keyid] = NULL;
286 _fill_text_view(i, metadata_count[keyid], self);
287 }
288
289 for(unsigned int key = 0; key < DT_METADATA_NUMBER; key++)
290 {
291 g_list_free_full(metadata[key], dt_free_gpointer);
292 metadata[key] = NULL;
293 }
294
295 gtk_widget_set_sensitive(GTK_WIDGET(d->apply_button), imgs_count > 0);
296}
297
298static void _image_selection_changed_callback(gpointer instance, dt_lib_module_t *self)
299{
300 _update(self);
301}
302
303static void _append_kv(GList **l, const gchar *key, const gchar *value)
304{
305 *l = g_list_append(*l, (gchar *)key);
306 *l = g_list_append(*l, (gchar *)value);
307}
308
309static void _metadata_set_list(const int i, GList **key_value, dt_lib_metadata_t *d)
310{
311 const uint32_t keyid = dt_metadata_get_keyid_by_display_order(i);
313 return;
314 gchar *metadata = _get_buffer_text(GTK_TEXT_VIEW(d->textview[i]));
315 if(metadata && !_is_leave_unchanged(GTK_TEXT_VIEW(d->textview[i])))
316 _append_kv(key_value, dt_metadata_get_key(keyid), metadata);
317}
318
319static void _write_metadata(GtkTextView *textview, dt_lib_module_t *self)
320{
322 // First: a focus-out raised while this runs must not write the same field a second time.
323 d->editing = FALSE;
324
325 GList *key_value = NULL;
326 if(textview)
327 {
328 const int i = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(textview), "tv_index"));
329 _metadata_set_list(i, &key_value, d);
330 }
331 else
332 {
333 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
334 _metadata_set_list(i, &key_value, d);
335 }
336
337 // The images the panel shows, not the selection: they are the ones whose values were edited,
338 // and by the time a click on another image commits the field, the selection has moved on.
339 const GList *imgs = d->last_act_on;
340 if(!IS_NULL_PTR(imgs) && !IS_NULL_PTR(key_value))
341 {
342 dt_metadata_set_list(imgs, key_value, TRUE);
346 }
347
348 for(GList *l = key_value; l; l = l->next)
349 {
350 l = l->next;
351 dt_free(l->data); // metadata value
352 }
353 g_list_free(key_value);
354 key_value = NULL;
355
356 _refresh(self);
357}
358
359static void _apply_button_clicked(GtkButton *button, dt_lib_module_t *self)
360{
361 _write_metadata(NULL, self);
362}
363
364static gboolean _key_pressed(GtkWidget *textview, GdkEventKey *event, dt_lib_module_t *self)
365{
367
368 guint key = dt_keys_mainpad_alternatives(event->keyval);
369
370 if(dt_modifier_is(event->state, DT_PRIMARY_MASK))
371 {
372 switch(key)
373 {
374 case GDK_KEY_Return:
375 // insert new line
376 event->state &= ~DT_PRIMARY_MASK;
377 d->editing = TRUE;
378 break;
379 default:
380 break;
381 }
382 }
383 else
384 {
385 switch(key)
386 {
387 case GDK_KEY_Return:
388 _write_metadata(GTK_TEXT_VIEW(textview), self);
389 _text_set_all_selected(GTK_TEXT_VIEW(textview), FALSE);
390 return TRUE;
391 break;
392 case GDK_KEY_Tab:
393 case GDK_KEY_ISO_Left_Tab:
394 _write_metadata(GTK_TEXT_VIEW(textview), self);
395 break;
396 case GDK_KEY_Escape:
397 {
398 if(dt_modifier_is(event->state, 0))
399 {
400 // Discard: cleared before the focus leaves, so the focus-out writes nothing, then the
401 // fields get back what the images hold.
402 d->editing = FALSE;
403 _refresh(self);
404 gtk_window_set_focus(GTK_WINDOW(dt_gui_main_window()), NULL);
405 return TRUE;
406 }
407 break;
408 }
409 default:
410 break;
411 }
412 }
413
414 return gtk_text_view_im_context_filter_keypress(GTK_TEXT_VIEW(textview), event);
415}
416
417void _textbuffer_changed(GtkTextBuffer *textbuffer, dt_lib_module_t *self)
418{
420 d->editing = TRUE;
421 GtkTextView *textview = GINT_TO_POINTER(g_object_get_data(G_OBJECT(textbuffer), "buffer_tv"));
422 g_object_set_data(G_OBJECT(textview), "tv_multiple", GINT_TO_POINTER(FALSE));
423}
424
425gboolean _textview_focus(GtkWidget *widget, GtkDirectionType d, gpointer user_data)
426{
427 GtkWidget *target = g_object_get_data(G_OBJECT(widget), d == GTK_DIR_TAB_FORWARD ? "meta_next" : "meta_prev");
428 gtk_widget_grab_focus(target);
429 return TRUE;
430}
431
432static gboolean _got_focus(GtkWidget *textview, dt_lib_module_t *self)
433{
435 if(!d->editing)
436 {
437 if(_is_leave_unchanged(GTK_TEXT_VIEW(textview)))
438 {
439 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
440 _set_text_buffer(buffer, "");
441 _text_set_italic(GTK_TEXT_VIEW(textview), FALSE);
442 }
443 _text_set_all_selected(GTK_TEXT_VIEW(textview), TRUE);
444 }
445 return TRUE;
446}
447
448static gboolean _lost_focus(GtkWidget *textview, GdkEventFocus *event, dt_lib_module_t *self)
449{
451 // Clicking elsewhere validates, as in any text field: what was typed is written. Escape is the
452 // way to discard it, and clears the flag before the focus leaves.
453 if(d->editing) _write_metadata(GTK_TEXT_VIEW(textview), self);
454 d->editing = FALSE;
455 if(_is_leave_unchanged(GTK_TEXT_VIEW(textview)))
456 {
457 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
458 _set_text_buffer(buffer, _("<leave unchanged>"));
459 _text_set_italic(GTK_TEXT_VIEW(textview), TRUE);
460 }
461 return FALSE;
462}
463
465{
466 return 4;
467}
468
470{
472
473 GtkWidget *first = NULL, *previous = NULL;
474 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
475 {
477 continue;
480 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name);
481 const gboolean hidden = type == DT_METADATA_TYPE_INTERNAL ||
483 dt_free(setting);
484
485 GtkWidget *label = gtk_grid_get_child_at(GTK_GRID(self->widget), 0, i);
486 gtk_widget_set_visible(label, !hidden);
487 GtkWidget *current = GTK_WIDGET(d->textview[i]);
488 gtk_widget_set_visible(gtk_widget_get_parent(current), !hidden);
489
490 if(!hidden)
491 {
492 if(IS_NULL_PTR(first)) first = previous = current;
493
494 g_object_set_data(G_OBJECT(previous), "meta_next", current);
495 g_object_set_data(G_OBJECT(current), "meta_prev", previous);
496
497 g_object_set_data(G_OBJECT(current), "meta_next", first);
498 g_object_set_data(G_OBJECT(first), "meta_prev", current);
499
500 previous = current;
501 }
502 }
503}
504
506{
508 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
509 {
511 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name);
512 const gboolean hidden = dt_conf_get_int(setting) & DT_METADATA_FLAG_HIDDEN;
513 dt_free(setting);
515 // we don't want to lose hidden information
516 if(!hidden && type != DT_METADATA_TYPE_INTERNAL)
517 {
518 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->textview[i]);
519 _set_text_buffer(buffer, "");
520 _text_set_italic(d->textview[i], FALSE);
521 }
522 }
523 _write_metadata(NULL, self);
524}
525
526static void _toggled_callback(gchar *path_str, gpointer user_data, const int column)
527{
528 GtkListStore *store = (GtkListStore *)user_data;
529 GtkTreeIter iter;
530 GtkTreePath *path = gtk_tree_path_new_from_string(path_str);
531 gboolean toggle;
532
533 gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter, path);
534 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, column, &toggle, -1);
535 gtk_list_store_set(store, &iter, column, !toggle, -1);
536
537 gtk_tree_path_free(path);
538}
539
540static void _visible_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data)
541{
543}
544
545static void _private_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data)
546{
548}
549
550void _menuitem_preferences(GtkMenuItem *menuitem, dt_lib_module_t *self)
551{
553 GtkWidget *dialog = gtk_dialog_new_with_buttons(_("metadata settings"), GTK_WINDOW(win),
554 GTK_DIALOG_DESTROY_WITH_PARENT, _("default"), GTK_RESPONSE_YES,
555 _("cancel"), GTK_RESPONSE_NONE, _("save"), GTK_RESPONSE_ACCEPT, NULL);
556 g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL);
557 GtkWidget *area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
558
559 GtkWidget *w = gtk_scrolled_window_new(NULL, NULL);
560 gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100));
561 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
562 dt_gui_add_class(w, "dt_recessed_scroll");
563 gtk_box_pack_start(GTK_BOX(area), w, TRUE, TRUE, 0);
564
565 GtkListStore *store = gtk_list_store_new(DT_METADATA_PREF_NUM_COLS,
566 G_TYPE_INT, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN);
567 GtkTreeModel *model = GTK_TREE_MODEL(store);
568 GtkTreeIter iter;
569
571 gboolean visible[DT_METADATA_NUMBER];
572 gboolean private[DT_METADATA_NUMBER];
573 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
574 {
577 {
579 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name[i]);
580 const uint32_t flag = dt_conf_get_int(setting);
581 dt_free(setting);
582 visible[i] = !(flag & DT_METADATA_FLAG_HIDDEN);
583 private[i] = flag & DT_METADATA_FLAG_PRIVATE;
584 gtk_list_store_append(store, &iter);
585 gtk_list_store_set(store, &iter,
590 -1);
591 }
592 }
593
594 GtkWidget *view = gtk_tree_view_new_with_model(model);
595 g_object_unref(model);
596 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
597 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("metadata"), renderer,
598 "text", DT_METADATA_PREF_COL_NAME, NULL);
599 gtk_tree_view_column_set_expand(column, TRUE);
600 gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
601 renderer = gtk_cell_renderer_toggle_new();
602 g_signal_connect(renderer, "toggled", G_CALLBACK(_visible_toggled_callback), store);
603 column = gtk_tree_view_column_new_with_attributes(_("visible"), renderer,
604 "active", DT_METADATA_PREF_COL_VISIBLE, NULL);
605 gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
606 GtkWidget *header = gtk_tree_view_column_get_button(column);
607 gtk_widget_set_tooltip_text(header,
608 _("tick if the corresponding metadata is of interest for you"
609 "\nit will be visible from metadata editor, collection and import module"
610 "\nit will be also exported"));
611 renderer = gtk_cell_renderer_toggle_new();
612 g_signal_connect(renderer, "toggled", G_CALLBACK(_private_toggled_callback), store);
613 column = gtk_tree_view_column_new_with_attributes(_("private"), renderer,
614 "active", DT_METADATA_PREF_COL_PRIVATE, NULL);
615 gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
616 header = gtk_tree_view_column_get_button(column);
617 gtk_widget_set_tooltip_text(header,
618 _("tick if you want to keep this information private (not exported with images)"));
619
620 gtk_container_add(GTK_CONTAINER(w), view);
621
622#ifdef GDK_WINDOWING_QUARTZ
624#endif
625 gtk_widget_show_all(dialog);
626
627 int res = gtk_dialog_run(GTK_DIALOG(dialog));
628 while(res == GTK_RESPONSE_YES)
629 {
630 gtk_tree_model_get_iter_first(model, &iter);
631 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
632 {
633 // mimic dt_metadata_init() without saving at this stage
636 {
637 gtk_list_store_set(store, &iter,
640 -1);
641 gtk_tree_model_iter_next(model, &iter);
642 }
643 }
644 res = gtk_dialog_run(GTK_DIALOG(dialog));
645 }
646
647 if(res == GTK_RESPONSE_ACCEPT)
648 {
649 gboolean meta_signal = FALSE;
650 gboolean meta_remove = FALSE;
651 gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
652 while(valid)
653 {
654 gboolean new_visible;
655 gboolean new_private;
656 uint32_t i;
657 gtk_tree_model_get(model, &iter,
659 DT_METADATA_PREF_COL_VISIBLE, &new_visible,
660 DT_METADATA_PREF_COL_PRIVATE, &new_private,
661 -1);
663 {
664 gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", name[i]);
665 uint32_t flag = dt_conf_get_int(setting);
666 if(new_visible != visible[i])
667 {
668 flag = !new_visible ? flag | DT_METADATA_FLAG_HIDDEN : flag & ~DT_METADATA_FLAG_HIDDEN;
669 meta_signal = TRUE;
670 meta_remove = !new_visible ? TRUE : meta_remove;
671 }
672 if(new_private != private[i])
673 {
674 flag = new_private ? flag | DT_METADATA_FLAG_PRIVATE : flag & ~DT_METADATA_FLAG_PRIVATE;
675 }
676 dt_conf_set_int(setting, flag);
677 dt_free(setting);
678 }
679 valid = gtk_tree_model_iter_next(model, &iter);
680 }
681 if(meta_signal)
684 }
685 _update_layout(self);
686 gtk_widget_destroy(dialog);
687}
688
689void set_preferences(void *menu, dt_lib_module_t *self)
690{
691 GtkWidget *mi = gtk_menu_item_new_with_label(_("preferences..."));
692 g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_preferences), self);
693 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
694}
695
696void _menu_line_activated(GtkMenuItem *menuitem, GtkTextView *textview)
697{
698 GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
699 gtk_text_buffer_set_text(buffer, gtk_label_get_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(menuitem)))), -1);
700}
701
702static void _populate_popup_multi(GtkTextView *textview, GtkWidget *popup, dt_lib_module_t *self)
703{
704 const dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data;
705
706 // get grid line number
707 const int i = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(textview), "tv_index"));
708
709 if (!d->metadata_list[i] || !_is_leave_unchanged(GTK_TEXT_VIEW(textview))) return;
710
711 gtk_menu_shell_append(GTK_MENU_SHELL(popup),gtk_separator_menu_item_new());
712
713 for(GList *item = d->metadata_list[i]; item; item = g_list_next(item))
714 {
715 GtkWidget *new_line = gtk_menu_item_new_with_label(item->data);
716 g_signal_connect(G_OBJECT(new_line), "activate", G_CALLBACK(_menu_line_activated), textview);
717 gtk_menu_shell_append(GTK_MENU_SHELL(popup), new_line);
718 }
719 gtk_widget_show_all(popup);
720}
721
722static gboolean _metadata_reset(GtkWidget *label, GdkEventButton *event, GtkWidget *widget)
723{
724 if(event->type == GDK_2BUTTON_PRESS)
725 {
726 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
727 _set_text_buffer(buffer, "");
728
729 GdkEventKey e = {0};
730 e.type = GDK_KEY_PRESS;
731 e.keyval = GDK_KEY_Return;
732 e.send_event = TRUE;
733 e.window = gtk_text_view_get_window(GTK_TEXT_VIEW(widget), GTK_TEXT_WINDOW_TEXT);
734 gboolean ret_val;
735 g_signal_emit_by_name(G_OBJECT(widget), "key-press-event", &e, &ret_val);
736 }
737 return FALSE;
738}
739
741{
743 self->data = (void *)d;
744
745 self->timeout_handle = 0;
746
747 GtkGrid *grid = GTK_GRID(gtk_grid_new());
748 self->widget = GTK_WIDGET(grid);
749 gtk_grid_set_row_spacing(grid, DT_GUI_BOX_SPACING);
750 gtk_grid_set_column_spacing(grid, DT_GUI_BOX_SPACING);
751
752 for(int i = 0; i < DT_METADATA_NUMBER; i++)
753 {
755 continue;
757 GtkWidget *labelev = gtk_event_box_new();
758 gtk_widget_add_events(labelev, GDK_BUTTON_PRESS_MASK);
759 gtk_container_add(GTK_CONTAINER(labelev), label);
760 gtk_grid_attach(grid, labelev, 0, i, 1, 1);
761 gtk_widget_set_tooltip_text(GTK_WIDGET(label),
762 _("metadata text. ctrl-wheel scroll to resize the text box"
763 "\n ctrl-enter inserts a new line (caution, may not be compatible with standard metadata)."
764 "\nif <leave unchanged> selected images have different metadata."
765 "\nin that case, right-click gives the possibility to choose one of them."
766 "\npress escape to exit the popup window"));
767
768 GtkWidget *textview = gtk_text_view_new();
769 dt_gui_textview_set_padding(GTK_TEXT_VIEW(textview));
770
771 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
772 g_object_set_data(G_OBJECT(buffer), "buffer_tv", GINT_TO_POINTER(textview));
773 g_object_set_data(G_OBJECT(textview), "tv_index", GINT_TO_POINTER(i));
774 g_object_set_data(G_OBJECT(textview), "tv_multiple", GINT_TO_POINTER(FALSE));
775 gtk_text_buffer_create_tag(gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview)),
776 "italic", "style", PANGO_STYLE_ITALIC, NULL);
777
778 gtk_grid_attach(grid, textview, 1, i, 1, 1);
779 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR);
780 gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(textview), FALSE);
781 gtk_widget_add_events(textview, GDK_FOCUS_CHANGE_MASK);
782
783 g_signal_connect(textview, "key-press-event", G_CALLBACK(_key_pressed), self);
784 /* After _key_pressed, not before: handlers run in connection order, and the text-input one
785 * takes Escape to hand the focus back and stops the emission. Escape would then reach this
786 * module only as a focus-out -- which writes the field -- instead of the discard it means. */
788 g_signal_connect(textview, "focus", G_CALLBACK(_textview_focus), self);
789 g_signal_connect(textview, "populate-popup", G_CALLBACK(_populate_popup_multi), self);
790 g_signal_connect(textview, "grab-focus", G_CALLBACK(_got_focus), self);
791 g_signal_connect(textview, "focus-out-event", G_CALLBACK(_lost_focus), self);
792 g_signal_connect(labelev, "button-press-event", G_CALLBACK(_metadata_reset), textview);
793 g_signal_connect(buffer, "changed", G_CALLBACK(_textbuffer_changed), self);
794
795 d->textview[i] = GTK_TEXT_VIEW(textview);
796 gtk_widget_set_hexpand(textview, TRUE);
797 gtk_widget_set_vexpand(textview, TRUE);
798 }
799
800 // apply button
801 d->apply_button = dt_action_button_new(self, N_("apply"), _apply_button_clicked, self,
802 _("write metadata for selected images"), 0, 0);
803
804 gtk_grid_attach(GTK_GRID(self->widget), GTK_WIDGET(d->apply_button), 0, DT_METADATA_NUMBER, 2, 1);
805
806 // and 2 other interesting signals:
808 G_CALLBACK(_image_selection_changed_callback), self);
809
810 gtk_widget_show_all(self->widget);
811 gtk_widget_set_no_show_all(self->widget, TRUE);
812
813 _update(self);
814 _update_layout(self);
815}
816
818{
819 if(IS_NULL_PTR(self->data)) return;
823
824 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
825 {
827 continue;
828 g_signal_handlers_block_by_func(d->textview[i], _lost_focus, self);
829 g_list_free_full(d->metadata_list[i], dt_free_gpointer);
830 d->metadata_list[i] = NULL;
831 }
832 g_list_free(d->last_act_on);
833 d->last_act_on = NULL;
834
835 dt_free(self->data);
836}
837
838static void add_rights_preset(dt_lib_module_t *self, char *name, char *string)
839{
840 // to be adjusted the nb of metadata items changes
841 const unsigned int metadata_nb = dt_metadata_get_nb_user_metadata();
842 const unsigned int params_size = strlen(string) + metadata_nb;
843
844 char *params = calloc(sizeof(char), params_size);
845 memcpy(params + 4, string, params_size - metadata_nb);
846 dt_lib_presets_add(name, self->plugin_name, self->version(), params, params_size, TRUE);
847 dt_free(params);
848}
849
851{
852 // <title>\0<description>\0<rights>\0<creator>\0<publisher>
853
854 add_rights_preset(self, _("CC BY"), _("Creative Commons Attribution (CC BY)"));
855 add_rights_preset(self, _("CC BY-SA"), _("Creative Commons Attribution-ShareAlike (CC BY-SA)"));
856 add_rights_preset(self, _("CC BY-ND"), _("Creative Commons Attribution-NoDerivs (CC BY-ND)"));
857 add_rights_preset(self, _("CC BY-NC"), _("Creative Commons Attribution-NonCommercial (CC BY-NC)"));
858 add_rights_preset(self, _("CC BY-NC-SA"),
859 _("Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)"));
860 add_rights_preset(self, _("CC BY-NC-ND"),
861 _("Creative Commons Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)"));
862 add_rights_preset(self, _("all rights reserved"), _("all rights reserved"));
863}
864
865void *legacy_params(dt_lib_module_t *self, const void *const old_params, const size_t old_params_size,
866 const int old_version, int *new_version, size_t *new_size)
867{
868 if(old_version == 1)
869 {
870 const size_t new_params_size = old_params_size + 1;
871 char *new_params = calloc(sizeof(char), new_params_size);
872
873 const char *buf = (const char *)old_params;
874
875 // <title>\0<description>\0<rights>\0<creator>\0<publisher>
876 const char *metadata[DT_METADATA_NUMBER];
877 size_t metadata_len[DT_METADATA_NUMBER];
878 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
879 {
880 metadata[i] = buf;
881 if(!metadata[i])
882 {
883 dt_free(new_params);
884 return NULL;
885 }
886 metadata_len[i] = strlen(metadata[i]) + 1;
887 buf += metadata_len[i];
888 }
889
890 // <creator>\0<publisher>\0<title>\0<description>\0<rights>
891 size_t pos = 0;
892 memcpy(new_params + pos, metadata[3], metadata_len[3]);
893 pos += metadata_len[3];
894 memcpy(new_params + pos, metadata[4], metadata_len[4]);
895 pos += metadata_len[4];
896 memcpy(new_params + pos, metadata[0], metadata_len[0]);
897 pos += metadata_len[0];
898 memcpy(new_params + pos, metadata[1], metadata_len[1]);
899 pos += metadata_len[1];
900 memcpy(new_params + pos, metadata[2], metadata_len[2]);
901
902 *new_size = new_params_size;
903 *new_version = 2;
904 return new_params;
905 }
906 else if(old_version == 2)
907 {
908 const size_t new_params_size = old_params_size + 1;
909 char *new_params = calloc(sizeof(char), new_params_size);
910
911 memcpy(new_params, old_params, old_params_size);
912
913 *new_size = new_params_size;
914 *new_version = 3;
915 return new_params;
916 }
917 return NULL;
918}
919
921{
923
924 *size = 0;
925 char *metadata[DT_METADATA_NUMBER];
926 int32_t metadata_len[DT_METADATA_NUMBER];
927
928 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
929 {
931 continue;
932 const uint32_t keyid = dt_metadata_get_keyid_by_display_order(i);
933 GtkTextBuffer *buffer = gtk_text_view_get_buffer((GtkTextView *)d->textview[i]);
934 GtkTextIter start, end;
935 gtk_text_buffer_get_bounds(buffer, &start, &end);
936 metadata[keyid] = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
937 if(!metadata[keyid]) metadata[keyid] = g_strdup("");
938 metadata_len[keyid] = strlen(metadata[keyid]) + 1;
939 *size = *size + metadata_len[keyid];
940 }
941
942 char *params = (char *)malloc(*size);
943
944 int pos = 0;
945
946 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
947 {
949 continue;
950 memcpy(params + pos, metadata[i], metadata_len[i]);
951 pos += metadata_len[i];
952 dt_free(metadata[i]);
953 }
954
955 g_assert(pos == *size);
956
957 return params;
958}
959
960// WARNING: also change src/libs/import.c when changing this!
961int set_params(dt_lib_module_t *self, const void *params, int size)
962{
963 if(IS_NULL_PTR(params)) return 1;
965
966 char *buf = (char *)params;
967 char *metadata[DT_METADATA_NUMBER];
968 uint32_t metadata_len[DT_METADATA_NUMBER];
969 uint32_t total_len = 0;
970 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
971 {
973 continue;
974 metadata[i] = buf;
975 if(IS_NULL_PTR(metadata[i])) return 1;
976 metadata_len[i] = strlen(metadata[i]) + 1;
977 buf += metadata_len[i];
978 total_len += metadata_len[i];
979 }
980
981 if(size != total_len)
982 return 1;
983
984 GList *key_value = NULL;
985
986 for(unsigned int i = 0; i < DT_METADATA_NUMBER; i++)
987 {
989 continue;
990 if(metadata[i][0] != '\0') _append_kv(&key_value, dt_metadata_get_key(i), metadata[i]);
991 }
992
993 GList *imgs = dt_act_on_get_images();
994 dt_metadata_set_list(imgs, key_value, TRUE);
995
996 g_list_free(key_value);
997 key_value = NULL;
998
1000 g_list_free(imgs);
1001 imgs = NULL;
1002 // force the ui refresh to update the info from preset
1003 g_list_free(d->last_act_on);
1004 d->last_act_on = NULL;
1005 _update(self);
1006 return 0;
1007}
1008// clang-format off
1009// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1010// vim: shiftwidth=2 expandtab tabstop=2 cindent
1011// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1012// clang-format on
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnect accels while a text or search entry has the focus, and reconnect them when it loses it....
Handle default and user-set shortcuts (accelerators)
#define DT_PRIMARY_MASK
GList * dt_act_on_get_images()
Definition act_on.c:38
GtkWidget * dt_gui_main_window(void)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
size_t params_size(dt_imageio_module_format_t *self)
Definition avif.c:571
const float v
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
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_image_synch_xmps(const GList *img)
GtkTreeStore * store
its model, owned by the view
GHashTable * selected
set of checked row labels, mirrored to conf on every change
GtkWidget * view
the tree view
const int res
Definition dtpthread.h:351
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:118
const char flag
Definition image.h:311
const char * model
GtkWidget * dt_ui_label_new(const gchar *str)
Definition label.c:125
void dt_gui_textview_set_padding(GtkTextView *textview)
Apply the standard recessed-input text padding to a GtkTextView.
Definition label.c:149
gboolean dt_handle_dialog_enter(GtkWidget *widget, GdkEventKey *event, gpointer data)
Definition lib.c:1324
void dt_lib_cancel_postponed_update(dt_lib_module_t *mod)
Definition lib.c:1309
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:1336
void dt_lib_presets_add(const char *name, const char *plugin_name, const int32_t version, const void *params, const int32_t params_size, gboolean readonly)
Definition lib.c:1152
void gui_reset(dt_lib_module_t *self)
void _menuitem_preferences(GtkMenuItem *menuitem, dt_lib_module_t *self)
static void _commit_pending_edit(dt_lib_module_t *self)
int set_params(dt_lib_module_t *self, const void *params, int size)
static void _update(dt_lib_module_t *self)
void * get_params(dt_lib_module_t *self, int *size)
static void add_rights_preset(dt_lib_module_t *self, char *name, char *string)
void set_preferences(void *menu, dt_lib_module_t *self)
gboolean _textview_focus(GtkWidget *widget, GtkDirectionType d, gpointer user_data)
static gboolean _metadata_reset(GtkWidget *label, GdkEventButton *event, GtkWidget *widget)
static void _toggled_callback(gchar *path_str, gpointer user_data, const int column)
static void _selected_metadata_row(void *user_data, const int keyid, const char *value, const uint32_t count)
static gchar * _get_buffer_text(GtkTextView *textview)
static gboolean _key_pressed(GtkWidget *textview, GdkEventKey *event, dt_lib_module_t *self)
static void _write_metadata(GtkTextView *textview, dt_lib_module_t *self)
static void _apply_button_clicked(GtkButton *button, dt_lib_module_t *self)
static void _update_layout(dt_lib_module_t *self)
static void _refresh(dt_lib_module_t *self)
void gui_cleanup(dt_lib_module_t *self)
dt_metadata_pref_cols_t
@ DT_METADATA_PREF_COL_INDEX
@ DT_METADATA_PREF_NUM_COLS
@ DT_METADATA_PREF_COL_VISIBLE
@ DT_METADATA_PREF_COL_PRIVATE
@ DT_METADATA_PREF_COL_NAME
void _textbuffer_changed(GtkTextBuffer *textbuffer, dt_lib_module_t *self)
static gboolean _is_leave_unchanged(GtkTextView *textview)
static void _set_text_buffer(GtkTextBuffer *buffer, const char *text)
static void _text_set_all_selected(GtkTextView *textview, const gboolean selected)
void _menu_line_activated(GtkMenuItem *menuitem, GtkTextView *textview)
static void _populate_popup_multi(GtkTextView *textview, GtkWidget *popup, dt_lib_module_t *self)
static void _text_set_italic(GtkTextView *textview, const gboolean italic)
static void _metadata_set_list(const int i, GList **key_value, dt_lib_metadata_t *d)
void init_presets(dt_lib_module_t *self)
static gboolean _lost_focus(GtkWidget *textview, GdkEventFocus *event, dt_lib_module_t *self)
static void _private_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data)
uint32_t container(dt_lib_module_t *self)
void gui_init(dt_lib_module_t *self)
int position()
const char ** views(dt_lib_module_t *self)
static void _image_selection_changed_callback(gpointer instance, dt_lib_module_t *self)
void * legacy_params(dt_lib_module_t *self, const void *const old_params, const size_t old_params_size, const int old_version, int *new_version, size_t *new_size)
static void _visible_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data)
static void _fill_text_view(const uint32_t i, const uint32_t count, dt_lib_module_t *self)
static void _append_kv(GList **l, const gchar *key, const gchar *value)
static gboolean _got_focus(GtkWidget *textview, dt_lib_module_t *self)
_lib_location_type_t type
Definition location.c:1
#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
const char * dt_metadata_get_key(const uint32_t keyid)
const char * dt_metadata_get_name_by_display_order(const uint32_t order)
void dt_metadata_set_list(const GList *imgs, GList *key_value, const gboolean undo_on)
unsigned int dt_metadata_get_nb_user_metadata()
int dt_metadata_get_type_by_display_order(const uint32_t order)
int dt_metadata_get_type(const uint32_t keyid)
dt_metadata_t dt_metadata_get_keyid_by_display_order(const uint32_t order)
@ DT_METADATA_FLAG_HIDDEN
Definition metadata.h:79
@ DT_METADATA_FLAG_PRIVATE
Definition metadata.h:80
@ DT_METADATA_NUMBER
Definition metadata.h:57
@ DT_METADATA_TYPE_OPTIONAL
Definition metadata.h:64
@ DT_METADATA_TYPE_INTERNAL
Definition metadata.h:65
@ DT_METADATA_SIGNAL_SHOWN
Definition metadata.h:71
@ DT_METADATA_SIGNAL_NEW_VALUE
Definition metadata.h:73
@ DT_METADATA_SIGNAL_HIDDEN
Definition metadata.h:72
void dt_metadata_repository_foreach_selected(dt_metadata_repository_selected_cb cb, void *user_data)
Every distinct (key, value) pair across main.selected_images, ordered by value.
main.meta_data: the free-text image metadata (title, description, creator, publisher,...
size_t size
Definition mipmap_cache.c:3
#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
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:407
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:386
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:616
@ DT_SIGNAL_METADATA_CHANGED
This signal is raised when metadata status (shown/hidden) or value has changed.
Definition signal.h:142
@ DT_SIGNAL_SELECTION_CHANGED
This signal is raised when the selection is changed no param, no returned value.
Definition signal.h:130
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:396
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
GtkTextView * textview[DT_METADATA_NUMBER]
GList * metadata_list[DT_METADATA_NUMBER]
GtkWidget * apply_button
char plugin_name[128]
Definition lib.h:81
GModule *void * data
Definition lib.h:79
GtkWidget * widget
Definition lib.h:83
guint timeout_handle
Definition lib.h:89
static gboolean dt_modifier_is(GdkModifierType state, const GdkModifierType desired_modifier_mask)
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
@ DT_UI_CONTAINER_PANEL_LEFT_CENTER