Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
history_actions.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
20#include "history/notify.h"
21
22#include "common/collection.h"
23#include "common/xmp_sidecar.h"
24#include "history/history.h"
26#include "common/image.h"
27#include "caches/image_cache.h"
28#include "common/styles.h"
29#include "common/undo.h"
30#include "common/conf.h"
31#include "develop/dev_history.h"
32#include "develop/develop.h"
33#include "develop/imageop.h"
34
35#ifdef GDK_WINDOWING_QUARTZ
36#include "osx/osx.h"
37#endif
38
39/* The history clipboard. Was a member of dt_view_manager_t, which the view manager never
40 * touched; zero-initialised there by its calloc, and zero-initialised here by being static. */
42
47
48static void _history_action_finalize_list(const GList *list, const gboolean changed)
49{
50 if(!changed) return;
51
54}
55
56typedef gboolean (*dt_history_action_fn)(const int32_t imgid, void *user_data);
57
58static gboolean _history_action_on_list_with_undo(const GList *list, dt_history_action_fn action, void *user_data,
59 const gboolean use_undo)
60{
61 if(IS_NULL_PTR(list)) return FALSE;
62
64 gboolean changed = FALSE;
65 for(const GList *l = list; l; l = g_list_next(l))
66 {
67 const int32_t imgid = GPOINTER_TO_INT(l->data);
68 changed |= action(imgid, user_data);
69 }
71
72 _history_action_finalize_list(list, changed);
73 return changed;
74}
75
76static gboolean _history_action_on_list(const GList *list, dt_history_action_fn action, void *user_data)
77{
78 return _history_action_on_list_with_undo(list, action, user_data, TRUE);
79}
80
91static GList *_get_user_mod_list(dt_develop_t *dev_src, GList *ops, gboolean copy_full)
92{
93 GList *mod_list = NULL;
94
95 if(ops)
96 {
97 dt_print(DT_DEBUG_PARAMS, "[_history_copy_and_paste_on_image_merge] pasting selected IOP\n");
98
99 // copy only selected history entries
100 for(const GList *l = g_list_last(ops); l; l = g_list_previous(l))
101 {
102 const unsigned int num = GPOINTER_TO_UINT(l->data);
103 const dt_dev_history_item_t *hist = NULL;
104 for(GList *h = g_list_last(dev_src->history); h; h = g_list_previous(h))
105 {
106 const dt_dev_history_item_t *item = (dt_dev_history_item_t *)h->data;
107 if(item && item->num == (int)num)
108 {
109 hist = item;
110 break;
111 }
112 }
113
114 if(hist)
115 {
116 dt_iop_module_t *mod = hist->module;
117 if(mod && !dt_iop_is_hidden(mod))
118 {
119 dt_print(DT_DEBUG_HISTORY, "selected for copy/pasting : module %20s, multiprio %i", mod->op, mod->multi_priority);
120
121 mod_list = g_list_prepend(mod_list, mod);
122 }
123 }
124 }
125 }
126 else
127 {
128 dt_print(DT_DEBUG_PARAMS, "[_history_copy_and_paste_on_image_merge] pasting all IOP\n");
129
130 // we will copy all modules
131 for(GList *modules_src = g_list_first(dev_src->iop); modules_src; modules_src = g_list_next(modules_src))
132 {
133 dt_iop_module_t *mod_src = (dt_iop_module_t *)(modules_src->data);
134
135 // copy from history only if
136 if((dt_dev_history_get_first_item_by_module(dev_src->history, mod_src) != NULL) // module is in history of source image
137 && !dt_iop_is_hidden(mod_src) // hidden modules are technical and special
138 && (copy_full || !dt_history_module_skip_copy(mod_src->flags()))
139 )
140 {
141 // Note: we prepend to GList because it's more efficient
142 mod_list = g_list_prepend(mod_list, mod_src);
143 }
144 }
145 }
146
147 return g_list_reverse(mod_list);
148}
149
162static int _history_copy_and_paste_on_image_merge(int32_t imgid, int32_t dest_imgid, GList *ops,
163 const gboolean copy_full, const dt_history_merge_strategy_t mode,
164 const gboolean copy_iop_order, dt_hm_batch_state_t *batch)
165{
166 // Init source history + pipeline
167 dt_develop_t _dev_src = { 0 };
168 dt_develop_t *dev_src = &_dev_src;
169 dt_dev_init(dev_src, FALSE);
170 dt_dev_reload_history_items(dev_src, imgid);
171
172 int ret_val = 0;
173
174 if(mode == DT_HISTORY_MERGE_REPLACE)
175 {
176 // Dumb mode : keep dev_src intact but swap destination imgid and image info into it.
177 //
178 // NOTE: when pasting from LDR/JPEG onto RAW images, the source iop-order list might not be compatible
179 // with the destination pipeline. We need to re-init the iop-order list for the destination image and
180 // reload module defaults for the destination image, but we must not apply auto-presets.
181 ret_val = dt_dev_replace_history_on_image(dev_src, dest_imgid, TRUE, "_history_copy_and_paste_on_image_merge");
182 }
183 else
184 {
185 // Merge
186 GList *mod_list = _get_user_mod_list(dev_src, ops, copy_full);
187 ret_val = dt_dev_merge_history_into_image(dev_src, dest_imgid, mod_list,
188 copy_iop_order, mode,
189 dt_conf_get_bool("history/paste_instances"), NULL, batch);
190 g_list_free(mod_list);
191 mod_list = NULL;
192 }
193 dt_dev_cleanup(dev_src);
194
195 return ret_val;
196}
197
198gboolean dt_history_copy_and_paste_on_image(const int32_t imgid, const int32_t dest_imgid, GList *ops,
199 const gboolean copy_full, const dt_history_merge_strategy_t mode,
200 const gboolean copy_iop_order, dt_hm_batch_state_t *batch)
201{
202 if(imgid == dest_imgid) return 1;
203
204 if(imgid == UNKNOWN_IMAGE)
205 {
206 dt_history_message(_("you need to copy history from an image before you paste it onto another"));
207 return 1;
208 }
209
211 hist->imgid = dest_imgid;
213
214 int ret_val = _history_copy_and_paste_on_image_merge(imgid, dest_imgid, ops, copy_full, mode, copy_iop_order,
215 batch);
216
220
221 return ret_val;
222}
223
224gboolean dt_history_copy(int32_t imgid)
225{
226 // note that this routine does not copy anything, it just setup the copy_paste proxy
227 // with the needed information that will be used while pasting.
228
229 if(imgid <= 0) return FALSE;
230
232
233 return TRUE;
234}
235
240
241static gboolean _history_paste_apply(const int32_t imgid, void *user_data)
242{
243 _paste_action_ctx_t *ctx = (_paste_action_ctx_t *)user_data;
245 if(copy_paste->copied_imageid <= 0) return FALSE;
246 if(imgid <= 0) return FALSE;
247
248 const gboolean pasted = dt_history_copy_and_paste_on_image(copy_paste->copied_imageid,
249 imgid,
250 copy_paste->selops,
251 FALSE,
252 dt_conf_get_int("history/paste/mode"),
253 dt_conf_get_bool("history/paste/copy_iop_order"),
254 ctx ? &ctx->batch : NULL) == 0;
255 return pasted;
256}
257
258gboolean dt_history_paste_on_image(const int32_t imgid)
259{
260 return _history_paste_apply(imgid, NULL); // NULL batch → always show report popup
261}
262
263gboolean dt_history_paste_on_list(const GList *list)
264{
265 if(dt_history_copy_paste_get()->copied_imageid <= 0) return FALSE;
266 _paste_action_ctx_t ctx = { 0 };
267 const gboolean changed = _history_action_on_list(list, _history_paste_apply, &ctx);
269 return changed;
270}
271
272static gboolean _history_paste_parts_apply(const int32_t imgid, void *user_data)
273{
274 _paste_action_ctx_t *ctx = (_paste_action_ctx_t *)user_data;
276 if(copy_paste->copied_imageid <= 0) return FALSE;
277 if(IS_NULL_PTR(copy_paste->selops)) return FALSE;
278 if(imgid <= 0) return FALSE;
279
280 const gboolean pasted = dt_history_copy_and_paste_on_image(copy_paste->copied_imageid,
281 imgid,
282 copy_paste->selops,
283 FALSE,
284 dt_conf_get_int("history/paste/mode"),
285 dt_conf_get_bool("history/paste/copy_iop_order"),
286 ctx ? &ctx->batch : NULL) == 0;
287 return pasted;
288}
289
290gboolean dt_history_paste_parts_on_image(const int32_t imgid)
291{
292 return _history_paste_parts_apply(imgid, NULL); // NULL batch → always show report popup
293}
294
295gboolean dt_history_paste_parts_on_list(const GList *list)
296{
298 if(copy_paste->copied_imageid <= 0) return FALSE;
299 if(IS_NULL_PTR(copy_paste->selops))
300 return FALSE;
301 _paste_action_ctx_t ctx = { 0 };
302 const gboolean changed = _history_action_on_list(list, _history_paste_parts_apply, &ctx);
304 return changed;
305}
306
307static gboolean _history_compress_apply(const int32_t imgid, void *user_data)
308{
309 (void)user_data;
310 dt_print(DT_DEBUG_HISTORY, "[dt_history_compress_on_image] compressing history for image %i\n", imgid);
311 if(imgid <= 0) return FALSE;
312
313 dt_develop_t dev;
314 dt_dev_init(&dev, FALSE);
315 dt_dev_reload_history_items(&dev, imgid);
317 dt_dev_write_history_ext(&dev, imgid);
318 dt_dev_cleanup(&dev);
319 return TRUE;
320}
321
322void dt_history_compress_on_image(const int32_t imgid)
323{
324 _history_compress_apply(imgid, NULL);
325}
326
327int dt_history_compress_on_list(const GList *imgs)
328{
330 return 0;
331}
332
338
339static gboolean _history_load_and_apply_apply(const int32_t imgid, void *user_data)
340{
342 dt_image_t *img = dt_image_cache_get(imgid, 'w');
343 if(IS_NULL_PTR(img)) return FALSE;
344
346 hist->imgid = imgid;
348
349 if(dt_exif_xmp_read(img, params->filename, params->history_only))
350 {
352 // ugly but if not history_only => called from crawler - do not write the xmp
353 params->history_only ? DT_IMAGE_CACHE_SAFE : DT_IMAGE_CACHE_RELAXED);
354 return FALSE;
355 }
356
360
362 // ugly but if not history_only => called from crawler - do not write the xmp
363 params->history_only ? DT_IMAGE_CACHE_SAFE : DT_IMAGE_CACHE_RELAXED);
364
365 // Loading an XMP rewrites the image history straight into the DB and image cache, bypassing
366 // the darkroom/paste path that goes through dt_dev_history_notify_change(). Nothing invalidates
367 // the rendered thumbnail, so it keeps showing the pre-XMP development (issue #861).
368 // The cached metadata (history_items, ratings...) is refreshed by the DT_SIGNAL_IMAGE_INFO_CHANGED
369 // raised in _history_action_finalize_list(), but the pixels are not: reload metadata, invalidate
370 // the mipmap and refresh the thumbnail (lighttable context, so refresh the filmstrip too).
372
373 return TRUE;
374}
375
376int dt_history_load_and_apply(const int32_t imgid, gchar *filename, int history_only)
377{
378 dt_history_load_params_t params = { .filename = filename, .history_only = history_only };
379 return _history_load_and_apply_apply(imgid, &params) ? 0 : 1;
380}
381
382int dt_history_load_and_apply_on_image(int32_t imgid, gchar *filename, int history_only)
383{
384 return dt_history_load_and_apply(imgid, filename, history_only);
385}
386
387int dt_history_load_and_apply_on_list(gchar *filename, const GList *list)
388{
389 dt_history_load_params_t params = { .filename = filename, .history_only = 1 };
390 const gboolean changed = _history_action_on_list(list, _history_load_and_apply_apply, &params);
391 return changed ? 0 : 1;
392}
393
398
399static gboolean _history_delete_apply(const int32_t imgid, void *user_data)
400{
401 if(imgid <= 0) return FALSE;
402
403 const dt_history_delete_params_t *params = (dt_history_delete_params_t *)user_data;
404 const gboolean undo = params ? params->undo : TRUE;
405
406 dt_undo_lt_history_t *hist = NULL;
407 if(undo)
408 {
410 hist->imgid = imgid;
412 }
413
415
416 if(undo)
417 {
421 }
422
423 return TRUE;
424}
425
426gboolean dt_history_delete_on_list(const GList *list, gboolean undo)
427{
428 dt_history_delete_params_t params = { .undo = undo };
429 return _history_action_on_list_with_undo(list, _history_delete_apply, &params, undo);
430}
431
440
441static gboolean _history_style_apply(const int32_t imgid, void *user_data)
442{
444 if(IS_NULL_PTR(params) || params->style_id == 0 || IS_NULL_PTR(params->name) || !*params->name) return FALSE;
445
446 int32_t newimgid = imgid;
447 if(params->duplicate)
448 {
449 // Defer exposing the duplicate to the collection/lighttable grid until its history is
450 // copied below -- otherwise the grid can generate and cache a thumbnail from the
451 // still-historyless row before the copy ever runs.
452 newimgid = dt_image_duplicate_no_reload(imgid);
453 if(newimgid == UNKNOWN_IMAGE) return FALSE;
454
455 // Structural copy of original history into the duplicate; no merge report needed here.
456 const gboolean pasted = dt_history_copy_and_paste_on_image(imgid, newimgid, NULL, TRUE, params->mode,
457 dt_conf_get_bool("history/style/copy_iop_order"),
458 NULL) == 0;
459
461
462 return pasted;
463 }
464
466 hist->imgid = newimgid;
468
469 const int ret_val = dt_styles_apply_to_image_merge(params->name, params->style_id, newimgid, params->mode,
470 &params->batch);
471
475
476 const gboolean changed = (ret_val == 0);
477 return changed;
478}
479
480gboolean dt_history_style_on_image(const int32_t imgid, const char *name, const gboolean duplicate)
481{
482 if(IS_NULL_PTR(name) || !*name) return FALSE;
483
485 .name = name,
486 .style_id = dt_styles_get_id_by_name(name),
487 .duplicate = duplicate,
488 .mode = dt_conf_get_int("history/style/mode"),
489 .batch = { .decision = DT_HM_BATCH_UNDECIDED },
490 };
491 if(params.style_id == 0) return FALSE;
492
493 const gboolean changed = _history_style_apply(imgid, &params);
494 dt_hm_batch_state_cleanup(&params.batch);
495 return changed;
496}
497
498gboolean dt_history_style_on_list(const GList *list, const char *name, const gboolean duplicate)
499{
500 if(IS_NULL_PTR(name) || !*name) return FALSE;
501
503 .name = name,
504 .style_id = dt_styles_get_id_by_name(name),
505 .duplicate = duplicate,
506 .mode = dt_conf_get_int("history/style/mode"),
507 .batch = { .decision = DT_HM_BATCH_UNDECIDED },
508 };
509 if(params.style_id == 0) return FALSE;
510
511 const gboolean changed = _history_action_on_list(list, _history_style_apply, &params);
512 dt_hm_batch_state_cleanup(&params.batch);
513 return changed;
514}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
void dt_collection_update_query(const dt_collection_t *collection, dt_collection_change_t query_change, dt_collection_properties_t changed_property, GList *list)
Definition collection.c:837
dt_collection_t * dt_collection_get_global(void)
Definition collection.c:138
@ DT_COLLECTION_PROP_UNDEF
Definition collection.h:155
@ DT_COLLECTION_CHANGE_RELOAD
Definition collection.h:164
int dt_conf_get_bool(const char *name)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
int32_t dt_image_duplicate_no_reload(const int32_t imgid)
void dt_image_history_changed(const int32_t imgid, const gboolean refresh_filmstrip)
void dt_dev_history_compress_or_truncate(dt_develop_t *dev) REQUIRES_SHARED(dev -> history_mutex)
Compress history if history_end is at top, otherwise truncate.
int dt_dev_replace_history_on_image(dt_develop_t *dev_src, const int32_t dest_imgid, const gboolean reload_defaults, const char *msg)
Replace an image history with the content of dev_src.
int dt_dev_merge_history_into_image(dt_develop_t *dev_src, int32_t dest_imgid, const GList *mod_list, gboolean merge_iop_order, const dt_history_merge_strategy_t mode, const gboolean paste_instances, const char *source_label, dt_hm_batch_state_t *batch)
Merge a list of modules into a destination image history via dt_history_merge().
gboolean dt_history_module_skip_copy(const int flags)
Determine whether a module should be skipped during history copy.
gboolean dt_dev_reload_history_items(dt_develop_t *dev, const int32_t imgid)
Reload history from DB and rebuild pipelines/GUI state.
dt_dev_history_item_t * dt_dev_history_get_first_item_by_module(GList *history_list, dt_iop_module_t *module)
Find the first history item referencing a module.
void dt_dev_write_history_ext(dt_develop_t *dev, const int32_t imgid) REQUIRES_SHARED(dev -> history_mutex)
Write dev->history to DB and XMP for a given image id.
void dt_dev_cleanup(dt_develop_t *dev)
Definition develop.c:237
void dt_dev_init(dt_develop_t *dev, int32_t gui_attached)
Definition develop.c:143
void dt_history_delete_on_image_ext(int32_t imgid, gboolean undo)
void dt_history_message(const char *format,...)
void dt_history_changed_images(const GList *imgs)
void dt_history_changed(const dt_history_change_t what)
Raise it. Internal to this code.
Everything the history, styles and presets code says to the outside world: messages for the user,...
@ DT_HISTORY_CHANGE_TAGS
static gboolean _history_action_on_list_with_undo(const GList *list, dt_history_action_fn action, void *user_data, const gboolean use_undo)
gboolean dt_history_paste_on_list(const GList *list)
static gboolean _history_paste_apply(const int32_t imgid, void *user_data)
int dt_history_load_and_apply(const int32_t imgid, gchar *filename, int history_only)
gboolean dt_history_copy(int32_t imgid)
static gboolean _history_style_apply(const int32_t imgid, void *user_data)
gboolean dt_history_paste_parts_on_list(const GList *list)
static gboolean _history_paste_parts_apply(const int32_t imgid, void *user_data)
static GList * _get_user_mod_list(dt_develop_t *dev_src, GList *ops, gboolean copy_full)
Build a module list to copy based on selected history indices.
int dt_history_load_and_apply_on_image(int32_t imgid, gchar *filename, int history_only)
dt_history_copy_item_t * dt_history_copy_paste_get(void)
The history clipboard: what "copy history" put there, for "paste" to read.
static gboolean _history_action_on_list(const GList *list, dt_history_action_fn action, void *user_data)
gboolean dt_history_delete_on_list(const GList *list, gboolean undo)
gboolean dt_history_paste_on_image(const int32_t imgid)
gboolean dt_history_style_on_image(const int32_t imgid, const char *name, const gboolean duplicate)
static gboolean _history_load_and_apply_apply(const int32_t imgid, void *user_data)
static int _history_copy_and_paste_on_image_merge(int32_t imgid, int32_t dest_imgid, GList *ops, const gboolean copy_full, const dt_history_merge_strategy_t mode, const gboolean copy_iop_order, dt_hm_batch_state_t *batch)
Copy/merge history between images using the merge pipeline.
static void _history_action_finalize_list(const GList *list, const gboolean changed)
static dt_history_copy_item_t _copy_paste
int dt_history_load_and_apply_on_list(gchar *filename, const GList *list)
int dt_history_compress_on_list(const GList *imgs)
static gboolean _history_delete_apply(const int32_t imgid, void *user_data)
gboolean dt_history_paste_parts_on_image(const int32_t imgid)
gboolean dt_history_copy_and_paste_on_image(const int32_t imgid, const int32_t dest_imgid, GList *ops, const gboolean copy_full, const dt_history_merge_strategy_t mode, const gboolean copy_iop_order, dt_hm_batch_state_t *batch)
static gboolean _history_compress_apply(const int32_t imgid, void *user_data)
void dt_history_compress_on_image(const int32_t imgid)
gboolean dt_history_style_on_list(const GList *list, const char *name, const gboolean duplicate)
gboolean(* dt_history_action_fn)(const int32_t imgid, void *user_data)
void dt_hm_batch_state_cleanup(dt_hm_batch_state_t *batch)
Release resources held by a batch state (the cached order). Safe to call on a zeroed state.
@ DT_HM_BATCH_UNDECIDED
dt_history_merge_strategy_t
@ DT_HISTORY_MERGE_REPLACE
void dt_history_snapshot_undo_create(const int32_t imgid, int *snap_id, int *history_end)
void dt_history_snapshot_undo_lt_history_data_free(gpointer data)
dt_undo_lt_history_t * dt_history_snapshot_item_init(void)
void dt_history_snapshot_undo_pop(gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, dt_undo_action_t action, GList **imgs)
#define UNKNOWN_IMAGE
Definition image.h:78
void dt_image_cache_write_release(dt_image_t *img, dt_image_cache_write_mode_t mode)
dt_image_t * dt_image_cache_get(const int32_t imgid, char mode)
@ DT_IMAGE_CACHE_RELAXED
Definition image_cache.h:50
@ DT_IMAGE_CACHE_SAFE
Definition image_cache.h:48
gboolean dt_iop_is_hidden(dt_iop_module_t *module)
Definition imageop.c:736
@ DT_DEBUG_HISTORY
Definition logging.h:75
@ DT_DEBUG_PARAMS
Definition logging.h:71
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#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
const char * name
Definition pdf.h:90
int dt_styles_apply_to_image_merge(const char *name, const int style_id, const int32_t newimgid, const dt_history_merge_strategy_t mode, dt_hm_batch_state_t *batch)
int32_t dt_styles_get_id_by_name(const char *name)
dt_hm_batch_state_t batch
GList * iop
Definition develop.h:269
GList * history
Definition develop.h:259
dt_history_merge_strategy_t mode
dt_hm_batch_state_t batch
dt_dev_operation_t op
Definition imageop.h:259
void dt_undo_end_group(dt_undo_t *self)
Definition undo.c:149
void dt_undo_start_group(dt_undo_t *self, dt_undo_type_t type)
Definition undo.c:134
void dt_undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, void(*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item, dt_undo_action_t action, GList **imgs), void(*free_data)(gpointer data))
Definition undo.c:163
@ DT_UNDO_LT_HISTORY
Definition undo.h:49
dt_undo_t * dt_undo_get_global(void)
Definition darktable.c:611
void * dt_undo_data_t
Definition undo.h:70
int dt_exif_xmp_read(dt_image_t *img, const char *filename, const int history_only)
The XMP document that carries an image's development: history stack, mask shapes, module order,...