src/libs/duplicate.c
Rama master · réplica de lectura del repositorio original.
/*
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 <http://www.gnu.org/licenses/>.
*/
#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