/* This file is part of darktable, Copyright (C) 2010-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/metadata.h" #include "common/collection.h" #include "common/darktable.h" #include "common/debug.h" #include "control/signal.h" #include "dtgtk/button.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/gtkentry.h" #include "gui/metadata_tags.h" #include "libs/lib.h" #include "libs/lib_api.h" #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif #include DT_MODULE(5) typedef enum dt_metadata_pref_cols_t { DT_METADATA_PREF_COL_KEY, // key DT_METADATA_PREF_COL_TAGNAME, // xmp tag name DT_METADATA_PREF_COL_NAME, // displayed name DT_METADATA_PREF_COL_VISIBLE, // visibility DT_METADATA_PREF_COL_PRIVATE, // do not export DT_METADATA_PREF_NUM_COLS } dt_metadata_pref_cols_t; typedef struct dt_lib_metadata_t { GHashTable *metadata_texts; GHashTable *metadata_counts; GList *setting_names; GtkWidget *grid, *button_box, *apply_button, *cancel_button, *delete_button; GtkWidget *dialog; GtkListStore *liststore; GtkTreeView *tree_view; GList *last_act_on; GList *metadata_to_delete; int num_grid_rows; gboolean needs_rebuild; } dt_lib_metadata_t; const char *name(dt_lib_module_t *self) { return _("metadata editor"); } const char *description(dt_lib_module_t *self) { return _("modify text metadata fields of\n" "the currently selected images"); } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_RIGHT_CENTER; } static gchar *_get_buffer_text(GtkTextView *textview) { GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); GtkTextIter start, end; gtk_text_buffer_get_bounds(buffer, &start, &end); return gtk_text_buffer_get_text(buffer, &start, &end, TRUE); } static gboolean _is_leave_unchanged(GtkTextView *textview) { return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(textview), "tv_multiple")); } static gboolean _set_leave_unchanged(GtkTextView *textview, GtkWidget *label) { gchar *text = _get_buffer_text(textview); const gchar *text_orig = (gchar *)g_object_get_data(G_OBJECT(textview), "text_orig"); const gboolean leave_unchanged = _is_leave_unchanged(textview); const gboolean this_changed = text_orig && !leave_unchanged ? strcmp(text, text_orig) : text[0] != 0; g_free(text); gtk_widget_set_name(label, this_changed ? "dt-metadata-changed" : NULL); gtk_container_foreach(GTK_CONTAINER(textview), (GtkCallback)gtk_widget_set_visible, GINT_TO_POINTER(leave_unchanged && !this_changed)); return this_changed; } static GtkLabel *_get_label_from_grid_at_row(const uint32_t row, dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 0, row); GtkLabel *label = g_object_get_data(G_OBJECT(cell), "label"); return label; } static GtkTextView *_get_textview_from_grid_at_row(const uint32_t row, dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, row); GtkTextView *textview = g_object_get_data(G_OBJECT(cell), "textview"); return textview; } static void _textbuffer_changed(GtkTextBuffer *buffer, dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; DT_GUARD_GUI_UPDATE(); gboolean changed = FALSE; for(unsigned int row = 0; row < d->num_grid_rows; row++) { GtkLabel *label = _get_label_from_grid_at_row(row, self); GtkTextView *textview = _get_textview_from_grid_at_row(row, self); if(label) changed |= _set_leave_unchanged(textview, GTK_WIDGET(label)); } gtk_widget_set_sensitive(d->button_box, changed); } static GtkTextView *_get_textview_by_key(const uint32_t key, dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; // find textview by metadata key for(int row = 0; row < d->num_grid_rows; row++) { GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, row); const uint32_t metadata_key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(cell), "key")); if(metadata_key == key) { GtkTextView *textview = g_object_get_data(G_OBJECT(cell), "textview"); return textview; } } return NULL; } static void _clear_grid(dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; for(unsigned int row = 0; row < d->num_grid_rows; row++) { GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, row); GtkTextView *textview = g_object_get_data(G_OBJECT(cell), "textview"); g_object_set_data(G_OBJECT(textview), "tv_multiple", GINT_TO_POINTER(FALSE)); GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); gtk_text_buffer_set_text(buffer, "", -1); gchar *text_orig = g_object_get_data(G_OBJECT(textview), "text_orig"); g_free(text_orig); g_object_set_data(G_OBJECT(textview), "text_orig", g_strdup("")); } } static void _fill_textview(gpointer key, gpointer value, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; const uint32_t metadata_key = GPOINTER_TO_INT(key); GList *texts = value; const int count = GPOINTER_TO_INT(g_hash_table_lookup(d->metadata_counts, GINT_TO_POINTER(key))); GtkTextView *textview = _get_textview_by_key(metadata_key, self); if(textview) { g_object_set_data(G_OBJECT(textview), "tv_multiple", GINT_TO_POINTER(count == 1)); GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); if(count <= 1) gtk_text_buffer_set_text(buffer, "", -1); else { gchar *text = texts? texts->data : ""; gtk_text_buffer_set_text(buffer, text, -1); gchar *text_orig = (gchar *)g_object_get_data(G_OBJECT(textview), "text_orig"); g_free(text_orig); g_object_set_data(G_OBJECT(textview), "text_orig", g_strdup(text)); } } } static void _write_metadata(dt_lib_module_t *self); static void _free_list_entry(gpointer key, gpointer value, gpointer user_data) { g_list_free_full((GList *)value, g_free); if(user_data) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)user_data; g_hash_table_replace(d->metadata_texts, key, NULL); } } static void _reset_count_entry(gpointer key, gpointer value, gpointer user_data) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)user_data; g_hash_table_replace(d->metadata_counts, key, GINT_TO_POINTER(0)); } void gui_update(dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; GList *imgs = dt_act_on_get_images(FALSE, FALSE, FALSE); // first we want to make sure the list of images to act on has changed // this is not the case if mouse hover change but still stay in selection for ex. if(imgs && d->last_act_on && dt_list_length_equal(imgs, d->last_act_on)) { gboolean changed = FALSE; GList *l = d->last_act_on; GList *ll = (GList *)imgs; while(l && ll) { if(GPOINTER_TO_INT(l->data) != GPOINTER_TO_INT(ll->data)) { changed = TRUE; break; } l = g_list_next(l); ll = g_list_next(ll); } if(!changed) { g_list_free(imgs); return; } } _write_metadata(self); d->last_act_on = imgs; // using dt_metadata_get() is not possible here. we want to do all // this in a single pass, everything else takes ages. gchar *images = dt_act_on_get_query(FALSE); const uint32_t imgs_count = g_list_length((GList *)imgs); g_hash_table_foreach(d->metadata_texts, _free_list_entry, d); g_hash_table_foreach(d->metadata_counts, _reset_count_entry, d); if(images) { GList *counts = dt_metadata_get_counts(images); g_free(images); for(GList *iter = counts; iter; iter = iter->next) { const dt_metadata_count_t *c = iter->data; GList *texts = g_hash_table_lookup(d->metadata_texts, GINT_TO_POINTER(c->key)); texts = g_list_prepend(texts, g_strdup(c->value)); g_hash_table_replace(d->metadata_texts, GINT_TO_POINTER(c->key), texts); g_hash_table_replace(d->metadata_counts, GINT_TO_POINTER(c->key), GINT_TO_POINTER((c->count == imgs_count) ? 2 : 1)); } g_list_free_full(counts, dt_metadata_count_free); } DT_ENTER_GUI_UPDATE(); _clear_grid(self); g_hash_table_foreach(d->metadata_texts, _fill_textview, self); DT_LEAVE_GUI_UPDATE(); _textbuffer_changed(NULL, self); gtk_widget_set_sensitive(d->grid, imgs_count > 0); } static void _image_selection_changed_callback(gpointer instance, dt_lib_module_t *self) { dt_lib_gui_queue_update(self); } static void _collection_updated_callback(gpointer instance, const dt_collection_change_t query_change, const dt_collection_properties_t changed_property, gpointer imgs, const int next, dt_lib_module_t *self) { dt_lib_gui_queue_update(self); } static void _append_kv(GList **l, const gchar *key, const gchar *value) { *l = g_list_append(*l, (gchar *)key); *l = g_list_append(*l, (gchar *)value); } static void _metadata_set_list(const int i, GList **key_value, dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, i); GtkTextView *textview = g_object_get_data(G_OBJECT(cell), "textview"); if(!textview) return; const char *tagname = (char *)g_object_get_data(G_OBJECT(textview), "tagname"); gchar *metadata = _get_buffer_text(GTK_TEXT_VIEW(textview)); const gchar *text_orig = g_object_get_data(G_OBJECT(textview), "text_orig"); const gboolean this_changed = text_orig && !_is_leave_unchanged(textview) ? strcmp(metadata, text_orig) : metadata[0] != 0; if(this_changed) _append_kv(key_value, tagname, metadata); else g_free(metadata); } static void _write_metadata(dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; GList *key_value = NULL; for(unsigned int i = 0; i < d->num_grid_rows; i++) _metadata_set_list(i, &key_value, self); if(key_value && d->last_act_on) { dt_gui_cursor_set_busy(); dt_metadata_set_list(d->last_act_on, key_value, TRUE); // NOTE: we free only one item out of two for(GList *l = key_value; l; l = l->next->next) g_free(l->next->data); g_list_free(key_value); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_METADATA_CHANGED, DT_METADATA_SIGNAL_NEW_VALUE); dt_image_synch_xmps(d->last_act_on); dt_gui_cursor_clear_busy(); } g_list_free(d->last_act_on); d->last_act_on = NULL; dt_lib_gui_queue_update(self); } static void _apply_button_clicked(GtkButton *button, dt_lib_module_t *self) { _write_metadata(self); gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL); } static void _cancel_button_clicked(GtkButton *button, dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; g_list_free(d->last_act_on); d->last_act_on = NULL; dt_lib_gui_queue_update(self); gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL); } static gboolean _key_pressed_cb(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, dt_lib_module_t *self) { GtkWidget *textview = dt_gui_get_widget(controller); dt_lib_metadata_t *d = self->data; switch(keyval) { case GDK_KEY_Return: case GDK_KEY_KP_Enter: if(!dt_modifier_is(state, GDK_CONTROL_MASK)) { gtk_button_clicked(GTK_BUTTON(d->apply_button)); return TRUE; } break; case GDK_KEY_Escape: if(dt_modifier_is(state, 0)) { gtk_button_clicked(GTK_BUTTON(d->cancel_button)); return TRUE; } break; default: break; } GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); #if GTK_CHECK_VERSION(4, 0, 0) /* GTK4: GtkTextView's internal key controller does the IM filtering * itself; gtk_text_view_im_context_filter_keypress() is gone. Let * the event propagate so the textview handles it natively. */ (void)event; return FALSE; #else const gboolean handled = gtk_text_view_im_context_filter_keypress(GTK_TEXT_VIEW(textview), (GdkEventKey *)event); gdk_event_free(event); return handled; #endif } static gboolean _textview_focus(GtkWidget *widget, GtkDirectionType d, gpointer user_data) { GtkWidget *target = g_object_get_data (G_OBJECT(widget), d == GTK_DIR_TAB_FORWARD ? "meta_next" : "meta_prev"); gtk_widget_grab_focus(target); return TRUE; } int position(const dt_lib_module_t *self) { return 510; } static void _update_layout(dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; GtkWidget *first = NULL, *previous = NULL; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; const gboolean visible = !metadata->internal && metadata->visible; int row = 0; while(row < d->num_grid_rows) { GtkWidget *lb_cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 0, row); GtkWidget *tv_cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, row); const uint32_t key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tv_cell), "key")); if(key == metadata->key) { gtk_widget_set_visible(lb_cell, visible); gtk_widget_set_visible(tv_cell, visible); GtkWidget *label = g_object_get_data(G_OBJECT(lb_cell), "label"); gtk_label_set_label(GTK_LABEL(label), metadata->name); if(visible) { GtkWidget *current = g_object_get_data(G_OBJECT(tv_cell), "textview"); _set_leave_unchanged(GTK_TEXT_VIEW(current), label); if(!first) first = previous = current; g_object_set_data(G_OBJECT(previous), "meta_next", current); g_object_set_data(G_OBJECT(current), "meta_prev", previous); g_object_set_data(G_OBJECT(current), "meta_next", first); g_object_set_data(G_OBJECT(first), "meta_prev", current); previous = current; } break; } row++; } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); } void gui_reset(dt_lib_module_t *self) { DT_ENTER_GUI_UPDATE(); dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; if(!metadata->internal && metadata->visible) { GtkTextView *textview = _get_textview_by_key(metadata->key, self); GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); gtk_text_buffer_set_text(buffer, "", -1); } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); DT_LEAVE_GUI_UPDATE(); _write_metadata(self); } static void _set_text_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(user_data)); gtk_text_buffer_set_text(buffer, g_variant_get_string(parameter, NULL), -1); } // while the selected images disagree on a field, a right-click offers their // values instead of the text view's own context menu static void _textview_secondary_pressed(GtkGestureSingle *gesture, const int n_press, const double x, const double y, dt_lib_module_t *self) { GtkWidget *textview = dt_gui_get_widget(gesture); if(!_is_leave_unchanged(GTK_TEXT_VIEW(textview))) return; const dt_lib_metadata_t *d = self->data; const uint32_t key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(textview), "key")); GList *texts = g_hash_table_lookup(d->metadata_texts, GINT_TO_POINTER(key)); if(!texts) return; // keeps the text view's own gesture from opening its context menu as well dt_gui_claim(gesture); if(!gtk_widget_get_action_group(textview, "metadata")) { const GActionEntry entries[] = { { "set-text", _set_text_activated, "s" } }; GSimpleActionGroup *group = g_simple_action_group_new(); g_action_map_add_action_entries(G_ACTION_MAP(group), entries, G_N_ELEMENTS(entries), textview); gtk_widget_insert_action_group(textview, "metadata", G_ACTION_GROUP(group)); g_object_unref(group); } GMenu *menu = g_menu_new(); for(GList *item = texts; item; item = item->next) { gchar *label = dt_str_unmnemonic(item->data); GMenuItem *mi = g_menu_item_new(label, NULL); g_free(label); g_menu_item_set_action_and_target_value(mi, "metadata.set-text", g_variant_new_string(item->data)); g_menu_append_item(menu, mi); g_object_unref(mi); } GtkWidget *popover = dt_gui_popover_menu_from_model(textview, menu); g_object_unref(menu); const GdkRectangle rect = { x, y, 1, 1 }; gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect); gtk_popover_popup(GTK_POPOVER(popover)); } static void _metadata_reset_cb(GtkGestureSingle *gesture, int n_press, double x, double y, GtkWidget *widget) { if(n_press >= 2) { g_object_set_data(G_OBJECT(widget), "tv_multiple", GINT_TO_POINTER(FALSE)); GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if(gtk_text_buffer_get_char_count(buffer)) gtk_text_buffer_set_text(buffer, "", -1); else g_signal_emit_by_name(G_OBJECT(buffer), "changed"); // even if unchanged } } static void _add_grid_row(dt_metadata_t *metadata, int row, dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; GtkGrid *grid = GTK_GRID(d->grid); gtk_grid_insert_row(GTK_GRID(d->grid), row); GtkWidget *label = dt_ui_label_new(metadata->name); gtk_widget_set_halign(label, GTK_ALIGN_FILL); gtk_widget_set_valign(label, GTK_ALIGN_START); GtkWidget *labelev = gtk_event_box_new(); gtk_widget_set_tooltip_text(labelev, _("double-click to reset")); gtk_widget_add_events(labelev, GDK_BUTTON_PRESS_MASK); gtk_container_add(GTK_CONTAINER(labelev), label); g_object_set_data(G_OBJECT(labelev), "label", label); gtk_grid_attach(grid, labelev, 0, row, 1, 1); GtkWidget *textview = gtk_text_view_new(); dt_action_define(DT_ACTION(self), NULL, metadata->name, textview, &dt_action_def_entry); gtk_widget_set_tooltip_text(textview, _("metadata text" "\nctrl+enter inserts a new line (caution, may not be compatible with standard metadata)" "\nif selected images have different metadata" "\nin that case, right-click gives the possibility to choose one of them" "\nescape to exit the popup window")); GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview)); g_object_set_data(G_OBJECT(textview), "tv_multiple", GINT_TO_POINTER(FALSE)); g_object_set_data(G_OBJECT(textview), "text_orig", NULL); g_object_set_data(G_OBJECT(textview), "key", GINT_TO_POINTER(metadata->key)); g_object_set_data(G_OBJECT(textview), "tagname", metadata->tagname); GtkWidget *unchanged = gtk_label_new(_("")); gtk_widget_set_name(unchanged, "dt-metadata-multi"); gtk_widget_set_visible(unchanged, FALSE); gtk_text_view_add_child_in_window(GTK_TEXT_VIEW(textview), unchanged, GTK_TEXT_WINDOW_WIDGET, 0, 0); gchar *setting_name = g_strdup_printf("plugins/lighttable/metadata/%s_text_height", dt_metadata_get_tag_subkey(metadata->tagname)); GtkWidget *swindow = dt_ui_resize_wrap(GTK_WIDGET(textview), 100, setting_name); d->setting_names = g_list_append(d->setting_names, setting_name); g_object_set_data(G_OBJECT(swindow), "key", GINT_TO_POINTER(metadata->key)); g_object_set_data(G_OBJECT(swindow), "textview", textview); gtk_grid_attach(grid, swindow, 1, row, 1, 1); gtk_widget_set_hexpand(swindow, TRUE); //workaround for a Gtk issue where the textview does not wrap correctly //while resizing the panel or typing into the widget //reported upstream to https://gitlab.gnome.org/GNOME/gtk/-/issues/4042 //see also discussions on https://github.com/darktable-org/darktable/pull/10584 GtkScrolledWindow *realsw = GTK_SCROLLED_WINDOW(gtk_widget_get_parent(textview)); gtk_scrolled_window_set_policy(realsw, GTK_POLICY_EXTERNAL, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_min_content_width(realsw, DT_PIXEL_APPLY_DPI(30)); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR); gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(textview), FALSE); gtk_widget_add_events(textview, GDK_FOCUS_CHANGE_MASK | GDK_ENTER_NOTIFY_MASK); dt_gui_connect_key(textview, _key_pressed_cb, self); g_signal_connect(textview, "focus", G_CALLBACK(_textview_focus), self); GtkGestureSingle *secondary = dt_gui_connect_click(textview, _textview_secondary_pressed, NULL, self); gtk_gesture_single_set_button(secondary, GDK_BUTTON_SECONDARY); gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(secondary), GTK_PHASE_TARGET); dt_gui_connect_click_all(labelev, _metadata_reset_cb, NULL, textview); g_signal_connect(buffer, "changed", G_CALLBACK(_textbuffer_changed), self); gtk_widget_set_hexpand(textview, TRUE); gtk_widget_set_vexpand(textview, TRUE); d->num_grid_rows++; } static void _toggled_callback(gchar *path_str, gpointer user_data, const int column) { GtkListStore *store = (GtkListStore *)user_data; GtkTreeIter iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gboolean toggle; gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter, path); gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, column, &toggle, -1); gtk_list_store_set(store, &iter, column, !toggle, -1); gtk_tree_path_free(path); } static void _visible_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data) { _toggled_callback(path_str, user_data, DT_METADATA_PREF_COL_VISIBLE); } static void _private_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data) { _toggled_callback(path_str, user_data, DT_METADATA_PREF_COL_PRIVATE); } static void _display_name_edited_callback(GtkCellRenderer *renderer, gchar *path_str, gchar *new_text, gpointer user_data) { GtkListStore *store = (GtkListStore *)user_data; GtkTreeIter iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter, path); gtk_list_store_set(store, &iter, DT_METADATA_PREF_COL_NAME, new_text, -1); gtk_tree_path_free(path); } static gboolean _find_metadata_iter_per_text(GtkTreeModel *model, GtkTreeIter *iter, gint col, const char *text) { if(!text) return FALSE; GtkTreeIter it; gboolean valid = gtk_tree_model_get_iter_first(model, &it); char *name; while(valid) { gtk_tree_model_get(model, &it, col, &name, -1); const gboolean found = g_strcmp0(text, name) == 0; g_free(name); if(found) { if(iter) *iter = it; return TRUE; } valid = gtk_tree_model_iter_next(model, &it); } return FALSE; } static void _add_selected_metadata(gchar *tagname, dt_lib_metadata_t *d) { GtkTreeIter iter; if(!_find_metadata_iter_per_text(GTK_TREE_MODEL(d->liststore), NULL, DT_METADATA_PREF_COL_TAGNAME, tagname)) { gtk_list_store_insert_with_values(d->liststore, &iter, -1, DT_METADATA_PREF_COL_KEY, -1, // -1 indicates a new entry DT_METADATA_PREF_COL_TAGNAME, tagname, DT_METADATA_PREF_COL_NAME, dt_metadata_get_tag_subkey(tagname), DT_METADATA_PREF_COL_VISIBLE, TRUE, DT_METADATA_PREF_COL_PRIVATE, FALSE, -1); GtkTreeSelection *selection = gtk_tree_view_get_selection(d->tree_view); gtk_tree_selection_select_iter(selection, &iter); } g_free(tagname); } static void _metadata_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, dt_lib_metadata_t *d) { gchar *tagname = dt_metadata_tags_get_selected(); _add_selected_metadata(tagname, d); } // dialog to add metadata tag into the formula list static void _add_tag_button_clicked(GtkButton *button, dt_lib_metadata_t *d) { GtkWidget *dialog = dt_metadata_tags_dialog(d->dialog, _metadata_activated, d); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); #endif gtk_widget_show_all(dialog); while(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { gchar *tagname = dt_metadata_tags_get_selected(); _add_selected_metadata(tagname, d); } gtk_widget_destroy(dialog); } static void _delete_tag_button_clicked(GtkButton *button, dt_lib_metadata_t *d) { GtkTreeIter iter; GtkTreeModel *model = GTK_TREE_MODEL(d->liststore); GtkTreeSelection *selection = gtk_tree_view_get_selection(d->tree_view); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { uint32_t key; gtk_tree_model_get(model, &iter, DT_METADATA_PREF_COL_KEY, &key, -1); if(key != -1) d->metadata_to_delete = g_list_prepend(d->metadata_to_delete, GINT_TO_POINTER(key)); gtk_list_store_remove(d->liststore, &iter); } } static void _drag_data_inserted(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, dt_lib_metadata_t *d) { d->needs_rebuild = TRUE; } static void _fill_grid(dt_lib_module_t *self) { dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; for(int32_t i = d->num_grid_rows - 1; i >= 0; i--) gtk_grid_remove_row(GTK_GRID(d->grid), i); g_hash_table_foreach(d->metadata_texts, _free_list_entry, NULL); g_hash_table_destroy(d->metadata_texts); g_hash_table_destroy(d->metadata_counts); d->metadata_texts = g_hash_table_new(NULL, NULL); d->metadata_counts = g_hash_table_new(NULL, NULL); d->metadata_to_delete = NULL; d->num_grid_rows = 0; gtk_widget_set_no_show_all(d->grid, FALSE); int row = 0; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; if(metadata->internal) continue; g_hash_table_insert(d->metadata_texts, GINT_TO_POINTER(metadata->key), NULL); g_hash_table_insert(d->metadata_counts, GINT_TO_POINTER(metadata->key), GINT_TO_POINTER(0)); _add_grid_row(metadata, row, self); row++; } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); gtk_widget_show_all(d->grid); gtk_widget_set_no_show_all(d->grid, TRUE); dt_lib_gui_queue_update(self); } static void _menuitem_preferences(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; dt_lib_metadata_t *d = (dt_lib_metadata_t *)self->data; GtkCellEditable *active_editable = NULL; g_list_free(d->metadata_to_delete); d->metadata_to_delete = NULL; GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkWidget *dialog = gtk_dialog_new_with_buttons(_("metadata settings"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); d->dialog = dialog; gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_gui_dialog_add_help(GTK_DIALOG(dialog), "metadata_preferences"); dt_gui_dialog_restore_size(GTK_DIALOG(dialog), "metadata"); dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); GtkListStore *store = gtk_list_store_new(DT_METADATA_PREF_NUM_COLS, G_TYPE_INT, // key G_TYPE_STRING, // xmp tag name G_TYPE_STRING, // displayed name G_TYPE_BOOLEAN, // visibility G_TYPE_BOOLEAN); // do not export d->liststore = store; GtkTreeModel *model = GTK_TREE_MODEL(store); GtkTreeIter iter; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *metadata_iter = dt_metadata_get_list(); metadata_iter; metadata_iter = metadata_iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)metadata_iter->data; if(!metadata->internal) { gtk_list_store_insert_with_values(store, NULL, -1, DT_METADATA_PREF_COL_KEY, metadata->key, DT_METADATA_PREF_COL_TAGNAME, metadata->tagname, DT_METADATA_PREF_COL_NAME, metadata->name, DT_METADATA_PREF_COL_VISIBLE, metadata->visible, DT_METADATA_PREF_COL_PRIVATE, metadata->priv, -1); } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); GtkWidget *view = gtk_tree_view_new_with_model(model); d->tree_view = GTK_TREE_VIEW(view); g_object_unref(model); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes (_("XMP tag name"), renderer, "text", DT_METADATA_PREF_COL_TAGNAME, NULL); gtk_tree_view_column_set_expand(column, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); renderer = gtk_cell_renderer_text_new(); column = gtk_tree_view_column_new_with_attributes (_("display name"), renderer, "text", DT_METADATA_PREF_COL_NAME, NULL); g_object_set(renderer, "editable", TRUE, NULL); g_signal_connect(G_OBJECT(renderer), "edited", G_CALLBACK(_display_name_edited_callback), store); dt_gui_commit_on_focus_loss(renderer, &active_editable); gtk_tree_view_column_set_expand(column, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); renderer = gtk_cell_renderer_toggle_new(); g_signal_connect(renderer, "toggled", G_CALLBACK(_visible_toggled_callback), store); column = gtk_tree_view_column_new_with_attributes (_("visible"), renderer, "active", DT_METADATA_PREF_COL_VISIBLE, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); GtkTreePath *first = gtk_tree_path_new_first (); gtk_tree_view_set_cursor(GTK_TREE_VIEW(view), first, column, FALSE); gtk_tree_path_free(first); gtk_widget_set_tooltip_text(view, _("drag and drop one row at a time until you get the desired order")); // drag & drop gtk_tree_view_set_reorderable(GTK_TREE_VIEW(view), TRUE); g_signal_connect(G_OBJECT(model), "row-inserted", G_CALLBACK(_drag_data_inserted), d); GtkWidget *header = gtk_tree_view_column_get_button(column); gtk_widget_set_tooltip_text(header, _("tick if the corresponding metadata is of interest for you" "\nit will be visible from metadata editor, collection and import module" "\nit will be also exported")); renderer = gtk_cell_renderer_toggle_new(); g_signal_connect(renderer, "toggled", G_CALLBACK(_private_toggled_callback), store); column = gtk_tree_view_column_new_with_attributes(_("private"), renderer, "active", DT_METADATA_PREF_COL_PRIVATE, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); header = gtk_tree_view_column_get_button(column); gtk_widget_set_tooltip_text(header, _("tick if you want to keep this information private" " (not exported with images)")); GtkWidget *w = dt_gui_scroll_wrap(view); gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); GtkWidget *plus = dtgtk_button_new(dtgtk_cairo_paint_plus_simple, 0, NULL); gtk_widget_set_tooltip_text(plus, _("add metadata tags")); g_signal_connect(G_OBJECT(plus), "clicked", G_CALLBACK(_add_tag_button_clicked), (gpointer)d); GtkWidget *minus = dtgtk_button_new(dtgtk_cairo_paint_minus_simple, 0, NULL); gtk_widget_set_tooltip_text(minus, _("delete metadata tag")); g_signal_connect(G_OBJECT(minus), "clicked", G_CALLBACK(_delete_tag_button_clicked), (gpointer)d); d->delete_button = minus; #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); #endif dt_gui_dialog_add(GTK_DIALOG(dialog), w, dt_gui_hbox(dt_gui_expand(dt_gui_align_right(minus)), plus)); gtk_widget_show_all(dialog); d->needs_rebuild = FALSE; int res = gtk_dialog_run(GTK_DIALOG(dialog)); if(res == GTK_RESPONSE_ACCEPT) { if(active_editable) gtk_cell_editable_editing_done(active_editable); // delete metadata gboolean confirm_delete = TRUE; if(d->metadata_to_delete) { // check for images with that metadata assigned const uint32_t count = dt_metadata_count_assigned(d->metadata_to_delete); if(count > 0) { confirm_delete = dt_gui_show_yes_no_dialog( _("delete metadata"), "", _("you are about to delete metadata which is currently assigned to images.\n" "the assignments will be removed.")); } if(!confirm_delete) goto finish; dt_metadata_remove_keys(d->metadata_to_delete); dt_pthread_mutex_lock(&darktable.metadata_threadsafe); // remove the metadata from the variables substitution list for(GList *key_iter = d->metadata_to_delete; key_iter; key_iter = key_iter->next) { uint32_t keyid = GPOINTER_TO_INT(key_iter->data); dt_metadata_t *metadata = dt_metadata_get_metadata_by_keyid(keyid); if(metadata) dt_gtkentry_variables_remove_metadata(metadata); } // re-initialze the metadata list dt_metadata_init(); dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); g_list_free(d->metadata_to_delete); d->metadata_to_delete = NULL; d->needs_rebuild = TRUE; } // process the remaining list gboolean valid = gtk_tree_model_get_iter_first(model, &iter); uint32_t display_order = 0; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); while(valid) { int32_t key = 0; gchar *tagname = NULL; gchar *name = NULL; gboolean visible = FALSE; gboolean private = FALSE; gtk_tree_model_get(model, &iter, DT_METADATA_PREF_COL_KEY, &key, DT_METADATA_PREF_COL_TAGNAME, &tagname, DT_METADATA_PREF_COL_NAME, &name, DT_METADATA_PREF_COL_VISIBLE, &visible, DT_METADATA_PREF_COL_PRIVATE, &private, -1); if(key == -1) { // new metadata entry dt_metadata_t *md = calloc(1, sizeof(dt_metadata_t)); md->tagname = g_strdup(tagname); md->name = g_strdup(name); md->internal = FALSE; md->visible = visible; md->priv = private; md->display_order = display_order; dt_metadata_add_metadata(md); dt_gtkentry_variables_add_metadata(md); d->needs_rebuild = TRUE; } else { // update dt_metadata_update_metadata(&(dt_metadata_t){ .key = key, .name = name, .visible = visible, .priv = private, .display_order = display_order }); dt_metadata_t *metadata = dt_metadata_get_metadata_by_keyid(key); g_free(metadata->name); metadata->name = g_strdup(name); metadata->visible = visible; metadata->priv = private; metadata->display_order = display_order; } g_free(tagname); g_free(name); display_order++; valid = gtk_tree_model_iter_next(model, &iter); } dt_metadata_sort(); dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); if(d->needs_rebuild) _fill_grid(self); _update_layout(self); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_METADATA_CHANGED, DT_METADATA_SIGNAL_PREF_CHANGED); } finish: gtk_widget_destroy(dialog); } void set_preferences(GMenu *menu, GActionGroup *action_group, dt_lib_module_t *self) { GSimpleAction *action = g_simple_action_new("preferences", NULL); g_signal_connect(action, "activate", G_CALLBACK(_menuitem_preferences), self); g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action)); g_menu_append(menu, _("preferences..."), "presets.preferences"); } void gui_init(dt_lib_module_t *self) { dt_lib_metadata_t *d = calloc(1, sizeof(dt_lib_metadata_t)); self->data = (void *)d; d->metadata_texts = g_hash_table_new(NULL, NULL); d->metadata_counts = g_hash_table_new(NULL, NULL); d->metadata_to_delete = NULL; GtkWidget *grid = gtk_grid_new(); d->grid = grid; gtk_grid_set_row_spacing(GTK_GRID(grid), DT_PIXEL_APPLY_DPI(0)); gtk_grid_set_column_spacing(GTK_GRID(grid), DT_PIXEL_APPLY_DPI(10)); d->apply_button = dt_action_button_new(self, N_("apply"), _apply_button_clicked, self, _("write metadata for selected images"), 0, 0); d->cancel_button = dt_action_button_new(self, N_("cancel"), _cancel_button_clicked, self, _("ignore changed metadata"), 0, 0); d->button_box = dt_gui_hbox(d->apply_button, d->cancel_button); self->widget = dt_gui_vbox(grid, d->button_box); _fill_grid(self); /* lets signup for mouse over image change signals */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _image_selection_changed_callback); // and 2 other interesting signals: DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _image_selection_changed_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _collection_updated_callback); gtk_widget_show_all(d->grid); gtk_widget_set_no_show_all(d->grid, TRUE); _update_layout(self); } void gui_cleanup(dt_lib_module_t *self) { dt_lib_metadata_t *d = self->data; for(unsigned int row = 0; row < d->num_grid_rows; row++) { GtkWidget *cell = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, row); GtkTextView *textview = g_object_get_data(G_OBJECT(cell), "textview"); gchar *text_orig = g_object_get_data(G_OBJECT(textview), "text_orig"); g_free(text_orig); g_object_set_data(G_OBJECT(textview), "text_orig", NULL); } g_list_free_full(d->setting_names, g_free); g_list_free(d->last_act_on); g_hash_table_foreach(d->metadata_texts, _free_list_entry, NULL); g_hash_table_destroy(d->metadata_texts); g_hash_table_destroy(d->metadata_counts); free(self->data); self->data = NULL; } static void add_rights_preset(dt_lib_module_t *self, char *name, char *string) { const char *tagname = "Xmp.dc.rights"; const uint32_t tagname_len = strlen(tagname) + 1; const uint32_t string_len = strlen(string) + 1; const unsigned int params_size = tagname_len + string_len; char *params = calloc(sizeof(char), params_size); if(params) { size_t pos = 0; memcpy(params + pos, tagname, tagname_len); pos += tagname_len; memcpy(params + pos, string, string_len); dt_lib_presets_add(name, self->plugin_name, self->version(), params, params_size, TRUE, 0); free(params); } } void init_presets(dt_lib_module_t *self) { add_rights_preset(self, _("CC BY"), _("Creative Commons Attribution (CC BY)")); add_rights_preset(self, _("CC BY-SA"), _("Creative Commons Attribution-ShareAlike (CC BY-SA)")); add_rights_preset(self, _("CC BY-ND"), _("Creative Commons Attribution-NoDerivs (CC BY-ND)")); add_rights_preset(self, _("CC BY-NC"), _("Creative Commons Attribution-NonCommercial (CC BY-NC)")); add_rights_preset(self, _("CC BY-NC-SA"), _("Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)")); add_rights_preset(self, _("CC BY-NC-ND"), _("Creative Commons Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)")); add_rights_preset(self, _("all rights reserved"), _("all rights reserved")); } static size_t _build_tag(char *buf, const char *tagname, const char **tagname_ptr, size_t *tagname_len_ptr, const char **text_ptr, size_t *text_len_ptr) { *tagname_ptr = tagname; *tagname_len_ptr = strlen(tagname) + 1; *text_ptr = buf; *text_len_ptr = strlen(buf) + 1; return *text_len_ptr; } 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) { if(old_version == 1) { const size_t new_params_size = old_params_size + 1; char *new_params = calloc(sizeof(char), new_params_size); const char *buf = (const char *)old_params; enum dt_metadata_v1_t { DT_METADATA_XMP_DC_CREATOR_V1, DT_METADATA_XMP_DC_PUBLISHER_V1, DT_METADATA_XMP_DC_TITLE_V1, DT_METADATA_XMP_DC_DESCRIPTION_V1, DT_METADATA_XMP_DC_RIGHTS_V1, DT_METADATA_XMP_ACDSEE_NOTES_V1, DT_METADATA_XMP_VERSION_NAME_V1, DT_METADATA_XMP_IMAGE_ID_V1, DT_METADATA_XMP_PRESERVED_FILENAME_V1, DT_METADATA_NUMBER_V1 }; // \0<description>\0<rights>\0<creator>\0<publisher> const char *metadata[DT_METADATA_NUMBER_V1]; size_t metadata_len[DT_METADATA_NUMBER_V1]; for(unsigned int i = 0; i < DT_METADATA_NUMBER_V1; i++) { metadata[i] = buf; if(!metadata[i]) { free(new_params); return NULL; } metadata_len[i] = strlen(metadata[i]) + 1; buf += metadata_len[i]; } // <creator>\0<publisher>\0<title>\0<description>\0<rights> size_t pos = 0; memcpy(new_params + pos, metadata[3], metadata_len[3]); pos += metadata_len[3]; memcpy(new_params + pos, metadata[4], metadata_len[4]); pos += metadata_len[4]; memcpy(new_params + pos, metadata[0], metadata_len[0]); pos += metadata_len[0]; memcpy(new_params + pos, metadata[1], metadata_len[1]); pos += metadata_len[1]; memcpy(new_params + pos, metadata[2], metadata_len[2]); *new_size = new_params_size; *new_version = 2; return new_params; } else if(old_version == 2) { const size_t new_params_size = old_params_size + 1; char *new_params = calloc(sizeof(char), new_params_size); memcpy(new_params, old_params, old_params_size); *new_size = new_params_size; *new_version = 3; return new_params; } else if(old_version == 3) { const size_t new_params_size = old_params_size + 1; char *new_params = calloc(sizeof(char), new_params_size); memcpy(new_params, old_params, old_params_size); *new_size = new_params_size; *new_version = 4; return new_params; } else if(old_version == 4) { enum dt_metadata_v4_t { DT_METADATA_XMP_DC_CREATOR_V4, DT_METADATA_XMP_DC_PUBLISHER_V4, DT_METADATA_XMP_DC_TITLE_V4, DT_METADATA_XMP_DC_DESCRIPTION_V4, DT_METADATA_XMP_DC_RIGHTS_V4, DT_METADATA_XMP_ACDSEE_NOTES_V4, DT_METADATA_XMP_VERSION_NAME_V4, DT_METADATA_XMP_IMAGE_ID_V4, DT_METADATA_XMP_PRESERVED_FILENAME_V4, DT_METADATA_NUMBER_V4 }; // new format: // <tagname>\0<tagvalue>\0<tagname>\0</tagvalue>\0 ... const char **metadata_tagnames = calloc(DT_METADATA_NUMBER_V4, sizeof(char *)); size_t *metadata_tagname_len = calloc(DT_METADATA_NUMBER_V4, sizeof(size_t)); const char **metadata_texts = calloc(DT_METADATA_NUMBER_V4, sizeof(char *)); size_t *metadata_len = calloc(DT_METADATA_NUMBER_V4, sizeof(size_t)); char *buf = (char *)old_params; int i = 0; // creator if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.dc.creator", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // publisher if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.dc.publisher", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // title if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.dc.title", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // description if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.dc.description", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // rights if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.dc.rights", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // notes if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.acdsee.notes", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // version name if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.darktable.version_name", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // preserved filename if(strlen(buf) > 0) { buf += _build_tag(buf, "Xmp.xmpMM.PreservedFileName", &metadata_tagnames[i], &metadata_tagname_len[i], &metadata_texts[i], &metadata_len[i]); i++; } else buf++; // calculate new buffer size size_t new_params_size = 0; for(int j = 0; j < i; j++) new_params_size += metadata_tagname_len[j] + metadata_len[j]; char *new_params = calloc(sizeof(char), new_params_size); size_t pos = 0; for(int j = 0; j < i; j++) { memcpy(new_params + pos, metadata_tagnames[j], metadata_tagname_len[j]); pos += metadata_tagname_len[j]; memcpy(new_params + pos, metadata_texts[j], metadata_len[j]); pos += metadata_len[j]; } free(metadata_tagname_len); free(metadata_tagnames); free(metadata_texts); free(metadata_len); *new_size = new_params_size; *new_version = 5; return new_params; } return NULL; } void *get_params(dt_lib_module_t *self, int *size) { *size = 0; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); const int metadata_nb = g_list_length(dt_metadata_get_list()); char **metadata_tagnames = calloc(metadata_nb, sizeof(char *)); int32_t *metadata_tagname_len = calloc(metadata_nb, sizeof(int32_t)); char **metadata_texts = calloc(metadata_nb, sizeof(char *)); int32_t *metadata_len = calloc(metadata_nb, sizeof(int32_t)); int i = 0; for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; if(metadata->internal) continue; GtkTextView *textview = _get_textview_by_key(metadata->key, self); GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); GtkTextIter start, end; gtk_text_buffer_get_bounds(buffer, &start, &end); gchar *tagtext = gtk_text_buffer_get_text(buffer, &start, &end, TRUE); if(*tagtext != '\0') { const gchar *tagname = g_object_get_data(G_OBJECT(textview), "tagname"); metadata_tagnames[i] = g_strdup(tagname); metadata_tagname_len[i] = strlen(metadata_tagnames[i]) + 1; metadata_texts[i] = tagtext; metadata_len[i] = strlen(metadata_texts[i]) + 1; *size = *size + metadata_tagname_len[i] + metadata_len[i]; i++; } else g_free(tagtext); } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); char *params = malloc(*size); int pos = 0; for(int j = 0; j < i; j++) { memcpy(params + pos, metadata_tagnames[j], metadata_tagname_len[j]); pos += metadata_tagname_len[j]; memcpy(params + pos, metadata_texts[j], metadata_len[j]); pos += metadata_len[j]; g_free(metadata_texts[j]); } free(metadata_tagname_len); free(metadata_tagnames); free(metadata_texts); free(metadata_len); g_assert(pos == *size); return params; } // WARNING: also change src/libs/import.c when changing this! int set_params(dt_lib_module_t *self, const void *params, int size) { if(!params) return 1; dt_lib_metadata_t *d = self->data; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); const int metadata_nb = g_list_length(dt_metadata_get_list()); dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); char **metadata_tagnames = calloc(metadata_nb, sizeof(char *)); int32_t *metadata_tagname_len = calloc(metadata_nb, sizeof(int32_t)); char **metadata_texts = calloc(metadata_nb, sizeof(char *)); int32_t *metadata_len = calloc(metadata_nb, sizeof(int32_t)); char *buf = (char *)params; size_t pos = 0; int i = 0; while(pos < size) { char *tagname = buf + pos; pos += strlen(tagname) + 1; char *text = buf + pos; pos += strlen(text) + 1; metadata_tagnames[i] = tagname; metadata_tagname_len[i] = strlen(tagname) + 1; metadata_texts[i] = text; metadata_len[i] = strlen(text) + 1; i++; } if(pos != size) { free(metadata_tagname_len); free(metadata_tagnames); free(metadata_texts); free(metadata_len); return 1; } GList *key_value = NULL; for(unsigned int j = 0; j < i; j++) { _append_kv(&key_value, metadata_tagnames[j], metadata_texts[j]); } free(metadata_tagname_len); free(metadata_tagnames); free(metadata_texts); free(metadata_len); GList *imgs = dt_act_on_get_images(FALSE, TRUE, FALSE); dt_metadata_set_list(imgs, key_value, TRUE); g_list_free(key_value); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE); dt_image_synch_xmps(imgs); g_list_free(imgs); // force the ui refresh to update the info from preset g_list_free(d->last_act_on); d->last_act_on = NULL; dt_lib_gui_queue_update(self); return 0; } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on