/* This file is part of darktable, Copyright (C) 2015-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/gdk_event_utils.h" #include "common/collection.h" #include "common/darktable.h" #include "common/debug.h" #include "common/history.h" #include "common/image.h" #include "common/metadata.h" #include "common/mipmap_cache.h" #include "common/selection.h" #include "common/styles.h" #include "control/conf.h" #include "control/control.h" #include "develop/develop.h" #include "dtgtk/thumbnail.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/styles.h" #include "libs/lib.h" #define DUPLICATE_COMPARE_SIZE 40 DT_MODULE(1) typedef struct dt_lib_duplicate_t { GtkWidget *duplicate_box; dt_imgid_t imgid; uint8_t *buf; float scale; size_t buf_width; size_t buf_height; dt_dev_zoom_pos_t zoom_pos; dt_view_context_t view_ctx; int preview_id; GList *thumbs; } dt_lib_duplicate_t; const char *name(dt_lib_module_t *self) { return _("duplicate manager"); } const char *description(dt_lib_module_t *self) { return _("create/rename/remove multiple\n" "edits of the current image"); } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_DARKROOM; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_LEFT_CENTER; } int position(const dt_lib_module_t *self) { return 850; } static void _lib_duplicate_init_callback(gpointer instance, dt_lib_module_t *self); static void _lib_duplicate_caption_out_callback(GtkEventController *controller, dt_lib_module_t *self) { GtkWidget *widget = dt_gui_get_widget(controller); const dt_imgid_t imgid = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget),"imgid")); // we write the content of the textbox to the caption field dt_metadata_set(imgid, "Xmp.darktable.version_name", gtk_entry_get_text(GTK_ENTRY(widget)), FALSE); dt_image_synch_xmp(imgid); } // create a 'virgin' duplicate of imgid, discarding the source history stack static void _lib_duplicate_original(const dt_imgid_t imgid) { const dt_imgid_t newid = dt_image_duplicate(imgid); if(!dt_is_valid_imgid(newid)) return; dt_history_delete_on_image(newid); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_TAG_CHANGED); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, newid); } // create a duplicate of imgid with the same history stack static void _lib_duplicate_duplicate(const dt_imgid_t imgid) { const dt_imgid_t newid = dt_image_duplicate(imgid); if(!dt_is_valid_imgid(newid)) return; dt_history_copy_and_paste_on_image(imgid, newid, FALSE, NULL, TRUE, TRUE, TRUE); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, newid); } // per-row buttons: act on the duplicate the button belongs to static void _lib_duplicate_original_clicked_callback(GtkButton *button, dt_lib_module_t *self) { _lib_duplicate_original(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "imgid"))); } static void _lib_duplicate_duplicate_clicked_callback(GtkButton *button, dt_lib_module_t *self) { _lib_duplicate_duplicate(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "imgid"))); } // actions (keyboard shortcuts): act on the currently selected duplicate static void _lib_duplicate_original_action_callback(dt_action_t *action) { _lib_duplicate_original(darktable.develop->image_storage.id); } static void _lib_duplicate_duplicate_action_callback(dt_action_t *action) { _lib_duplicate_duplicate(darktable.develop->image_storage.id); } static void _lib_duplicate_delete(GtkButton *button, dt_lib_module_t *self) { dt_lib_duplicate_t *d = self->data; const dt_imgid_t imgid = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "imgid")); if(imgid == darktable.develop->image_storage.id) { // we find the duplicate image to show now for(GList *l = d->thumbs; l; l = g_list_next(l)) { dt_thumbnail_t *thumb = l->data; if(thumb->imgid == imgid) { GList *l2 = g_list_next(l); if(!l2) l2 = g_list_previous(l); if(l2) { dt_thumbnail_t *th2 = (dt_thumbnail_t *)l2->data; DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, th2->imgid); break; } } } } // and we remove the image dt_control_delete_duplicate(imgid); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, g_list_prepend(NULL, GINT_TO_POINTER(imgid))); } static void _lib_duplicate_thumb_press_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self) { dt_lib_duplicate_t *d = self->data; GtkWidget *widget = dt_gui_get_widget(gesture); dt_thumbnail_t *thumb = (dt_thumbnail_t *)g_object_get_data(G_OBJECT(widget), "thumb"); const dt_imgid_t imgid = thumb->imgid; if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { if(n_press == 1) { d->imgid = imgid; dt_control_queue_redraw_center(); } else if(n_press >= 2) { // let's switch to the new image DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, imgid); } } } static void _lib_duplicate_thumb_release_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self) { dt_lib_duplicate_t *d = self->data; d->imgid = NO_IMGID; dt_control_queue_redraw_center(); } void view_leave(struct dt_lib_module_t *self, struct dt_view_t *old_view, struct dt_view_t *new_view) { // we leave the view. Let's destroy preview surf if any dt_lib_duplicate_t *d = self->data; if(d->buf) { dt_free_align(d->buf); d->buf = NULL; } } void gui_post_expose(dt_lib_module_t *self, cairo_t *cri, const int32_t width, const int32_t height, const int32_t pointerx, const int32_t pointery) { dt_lib_duplicate_t *d = self->data; if(!dt_is_valid_imgid(d->imgid)) return; const gboolean view_ok = dt_view_check_context_hash(&d->view_ctx); if(!view_ok || d->preview_id != d->imgid) { if(d->buf) dt_free_align(d->buf); dt_dev_image(d->imgid, width, height, -1, &d->buf, &d->scale, &d->buf_width, &d->buf_height, d->zoom_pos, -1, NULL, DT_DEVICE_NONE, FALSE, FALSE); d->preview_id = d->imgid; } if(d->buf) dt_view_paint_surface(cri, width, height, &darktable.develop->full, DT_WINDOW_MAIN, d->buf, d->scale, d->buf_width, d->buf_height, d->zoom_pos); } static void _thumb_remove(gpointer user_data) { dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data; gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(thumb->w_main)), thumb->w_main); dt_thumbnail_destroy(thumb); } static void _lib_duplicate_init_callback(gpointer instance, dt_lib_module_t *self) { //block signals to avoid concurrent calls dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(_lib_duplicate_init_callback), self); dt_lib_duplicate_t *d = self->data; d->imgid = NO_IMGID; // we drop the preview if any if(d->buf) { dt_free_align(d->buf); d->buf = NULL; } // we drop all the thumbs g_list_free_full(d->thumbs, _thumb_remove); d->thumbs = NULL; // and the other widgets too dt_gui_container_destroy_children(GTK_CONTAINER(d->duplicate_box)); // retrieve all the versions of the image dt_develop_t *dev = darktable.develop; int count = 0; // we get a summarize of all versions of the image GList *versions = dt_metadata_get_versions(dev->image_storage.film_id, dev->image_storage.filename); GtkWidget *bt = NULL; for(GList *iter = versions; iter; iter = iter->next) { const dt_metadata_version_t *version = iter->data; GtkWidget *hb = gtk_grid_new(); const dt_imgid_t imgid = version->imgid; dt_gui_add_class(hb, "dt_overlays_always"); dt_thumbnail_t *thumb = dt_thumbnail_new(100, 100, IMG_TO_FIT, imgid, -1, DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL, DT_THUMBNAIL_CONTAINER_DUPLICATE, TRUE, DT_THUMBNAIL_SELECTION_UNSELECTED); thumb->sel_mode = DT_THUMBNAIL_SEL_MODE_DISABLED; thumb->disable_mouseover = TRUE; thumb->disable_actions = TRUE; dt_thumbnail_set_mouseover(thumb, imgid == dev->image_storage.id); if(imgid != dev->image_storage.id) { dt_gui_connect_click(thumb->w_main, _lib_duplicate_thumb_press_callback, _lib_duplicate_thumb_release_callback, self); } gchar chl[256]; const gchar *path = version->name; g_snprintf(chl, sizeof(chl), "%d", version->version); GtkWidget *tb = dt_ui_entry_new(0); if(path) gtk_entry_set_text(GTK_ENTRY(tb), path); gtk_widget_set_hexpand(tb, TRUE); g_object_set_data (G_OBJECT(tb), "imgid", GINT_TO_POINTER(imgid)); gtk_widget_add_events(tb, GDK_FOCUS_CHANGE_MASK); dt_gui_connect_focus(tb, NULL, _lib_duplicate_caption_out_callback, self); GtkWidget *lb = gtk_label_new(chl); gtk_widget_set_hexpand(lb, TRUE); // duplicate this image with its history stack GtkWidget *bt_dup = dtgtk_button_new_full(dtgtk_cairo_paint_multiinstance, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("create a duplicate of this image with same history stack"), .clicked_cb = G_CALLBACK(_lib_duplicate_duplicate_clicked_callback), .clicked_data = self, }); dt_gui_add_class(bt_dup, "dt_duplicate_duplicate"); g_object_set_data(G_OBJECT(bt_dup), "imgid", GINT_TO_POINTER(imgid)); // create a 'virgin' duplicate of this image, discarding the source history stack GtkWidget *bt_orig = dtgtk_button_new_full(dtgtk_cairo_paint_plus, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("create a 'virgin' duplicate, discarding the source history stack"), .clicked_cb = G_CALLBACK(_lib_duplicate_original_clicked_callback), .clicked_data = self, }); dt_gui_add_class(bt_orig, "dt_duplicate_original"); g_object_set_data(G_OBJECT(bt_orig), "imgid", GINT_TO_POINTER(imgid)); bt = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("delete this duplicate"), .clicked_cb = G_CALLBACK(_lib_duplicate_delete), .clicked_data = self, }); dt_gui_add_class(bt, "dt_duplicate_delete"); g_object_set_data(G_OBJECT(bt), "imgid", GINT_TO_POINTER(imgid)); /* Keep the number and actions in a single top row. The delete slot is * retained when its button is unavailable, so the two active buttons do * not move when switching between images with and without duplicates. * See https://github.com/darktable-org/darktable/pull/21955 for the * discussion of reserving this space to avoid sidebar width changes. */ GtkWidget *delete_slot = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); dt_gui_add_class(delete_slot, "dt_duplicate_delete_slot"); gtk_box_pack_start(GTK_BOX(delete_slot), bt, FALSE, FALSE, 0); GtkWidget *buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); dt_gui_add_class(buttons, "dt_duplicate_actions"); gtk_box_pack_start(GTK_BOX(buttons), delete_slot, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(buttons), bt_dup, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(buttons), bt_orig, FALSE, FALSE, 0); GtkWidget *top = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start(GTK_BOX(top), lb, TRUE, TRUE, 0); gtk_box_pack_end(GTK_BOX(top), buttons, FALSE, FALSE, 0); gtk_grid_attach(GTK_GRID(hb), thumb->w_main, 0, 0, 1, 2); gtk_grid_attach(GTK_GRID(hb), top, 1, 0, 4, 1); gtk_grid_attach(GTK_GRID(hb), tb, 1, 1, 4, 1); gtk_widget_show(hb); gtk_widget_show(top); gtk_widget_show(buttons); gtk_widget_show(delete_slot); gtk_widget_show(lb); gtk_widget_show(tb); gtk_widget_show(bt_dup); gtk_widget_show(bt_orig); gtk_widget_show(bt); dt_gui_box_add(d->duplicate_box, hb); d->thumbs = g_list_append(d->thumbs, thumb); count++; } g_list_free_full(versions, dt_metadata_version_free); gtk_widget_show(d->duplicate_box); // We have a single image, so the delete action is not available. Keep its // slot allocated so adding/removing duplicate versions does not move the // other actions in the row. if(count == 1) { gtk_widget_set_sensitive(bt, FALSE); gtk_widget_hide(bt); } //unblock signals dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(_lib_duplicate_init_callback), self); } static void _lib_duplicate_collection_changed(gpointer instance, dt_collection_change_t query_change, dt_collection_properties_t changed_property, gpointer imgs, const int next, dt_lib_module_t *self) { _lib_duplicate_init_callback(instance, self); } static void _lib_duplicate_mipmap_updated_callback(gpointer instance, const dt_imgid_t imgid, dt_lib_module_t *self) { dt_lib_duplicate_t *d = self->data; gtk_widget_queue_draw(d->duplicate_box); dt_control_queue_redraw_center(); } static void _lib_duplicate_preview_updated_callback(gpointer instance, dt_lib_module_t *self) { dt_lib_duplicate_t *d = self->data; gtk_widget_queue_draw (d->duplicate_box); dt_control_queue_redraw_center(); } void gui_init(dt_lib_module_t *self) { /* initialize ui widgets */ dt_lib_duplicate_t *d = self->data = g_malloc0(sizeof(dt_lib_duplicate_t)); d->imgid = NO_IMGID; d->buf = NULL; d->view_ctx = 0; d->duplicate_box = dt_gui_vbox(); self->widget = dt_gui_vbox(dt_ui_resize_wrap(d->duplicate_box, 1, "plugins/darkroom/duplicate/windowheight")); dt_gui_add_class(self->widget, "dt_duplicate_ui"); dt_act_on_set_class(self->widget); gtk_widget_show_all(self->widget); gtk_widget_set_sensitive(self->widget, !dt_check_gimpmode_ok("file")); // the per-duplicate buttons act on their own row; these actions act on the // currently selected duplicate, retaining the previous bottom-button behaviour dt_action_register( DT_ACTION(self), N_("duplicate"), _lib_duplicate_duplicate_action_callback, 0, 0); dt_action_register( DT_ACTION(self), N_("original"), _lib_duplicate_original_action_callback, 0, 0); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_IMAGE_CHANGED, _lib_duplicate_init_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_INITIALIZE, _lib_duplicate_init_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _lib_duplicate_collection_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MIPMAP_UPDATED, _lib_duplicate_mipmap_updated_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _lib_duplicate_preview_updated_callback); } void gui_cleanup(dt_lib_module_t *self) { g_free(self->data); self->data = NULL; } // 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