Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
undo.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2013, 2016-2017, 2019-2021 Pascal Obry.
4 Copyright (C) 2014, 2016-2017 Tobias Ellinghaus.
5 Copyright (C) 2015-2017 Roman Lebedev.
6 Copyright (C) 2019 Edgardo Hoszowski.
7 Copyright (C) 2019 Heiko Bauke.
8 Copyright (C) 2019 Philippe Weyland.
9 Copyright (C) 2020-2021 Aldric Renaudin.
10 Copyright (C) 2021 Ralf Brown.
11 Copyright (C) 2022 Martin Bařinka.
12 Copyright (C) 2023, 2025 Aurélien PIERRE.
13
14 darktable is free software: you can redistribute it and/or modify
15 it under the terms of the GNU General Public License as published by
16 the Free Software Foundation, either version 3 of the License, or
17 (at your option) any later version.
18
19 darktable is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 GNU General Public License for more details.
23
24 You should have received a copy of the GNU General Public License
25 along with darktable. If not, see <http://www.gnu.org/licenses/>.
26*/
27
28#include "common/undo.h"
29#include "common/image.h"
30#include "common/logging.h"
31#include "system/macros.h"
32#include "system/mem_alloc.h"
33#include "common/times.h"
34#include <glib.h> // for GList, gpointer, g_list_prepend
35#include <stdlib.h> // for NULL, malloc, free
36
37const double MAX_TIME_PERIOD = 0.5; // in second
38
49
51{
52 dt_undo_t *udata = malloc(sizeof(dt_undo_t));
53 udata->undo_list = NULL;
54 udata->redo_list = NULL;
55 udata->disable_next = FALSE;
56 udata->locked = FALSE;
57 dt_pthread_mutex_init(&udata->mutex, NULL);
58 udata->group = DT_UNDO_NONE;
59 udata->group_indent = 0;
60 dt_print(DT_DEBUG_UNDO, "[undo] init\n");
61 return udata;
62}
63
64#define LOCK \
65 dt_pthread_mutex_lock(&self->mutex); self->locked = TRUE
66
67#define UNLOCK \
68 self->locked = FALSE; dt_pthread_mutex_unlock(&self->mutex)
69
71{
72 self->disable_next = TRUE;
73 dt_print(DT_DEBUG_UNDO, "[undo] disable next\n");
74}
75
81
82static void _free_undo_data(void *p)
83{
85 if (item->free_data) item->free_data(item->data);
86 dt_free(item);
87}
88
89static void _undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data,
90 gboolean is_group,
91 void (*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item, dt_undo_action_t action, GList **imgs),
92 void (*free_data)(gpointer data))
93{
94 if(IS_NULL_PTR(self)) return;
95
96 if(self->disable_next)
97 {
98 if(free_data) free_data(data);
99 self->disable_next = FALSE;
100 }
101 else
102 {
103 // do not block, if an undo record is asked and there is a lock it means that this call has been done in un
104 // undo/redo callback. We just skip this event.
105
106 if(!self->locked)
107 {
108 LOCK;
109
110 dt_undo_item_t *item = malloc(sizeof(dt_undo_item_t));
111
112 item->user_data = user_data;
113 item->type = type;
114 item->data = data;
115 item->undo = undo;
116 item->free_data = free_data;
117 item->ts = dt_get_wtime();
118 item->is_group = is_group;
119
120 self->undo_list = g_list_prepend(self->undo_list, (gpointer)item);
121
122 // recording an undo data invalidate all the redo
123 g_list_free_full(self->redo_list, _free_undo_data);
124 self->redo_list = NULL;
125
126 dt_print(DT_DEBUG_UNDO, "[undo] record for type %d (length %d)\n",
127 type, g_list_length(self->undo_list));
128
129 UNLOCK;
130 }
131 }
132}
133
135{
136 if(IS_NULL_PTR(self)) return;
137
138 if(self->group == DT_UNDO_NONE)
139 {
140 dt_print(DT_DEBUG_UNDO, "[undo] start group for type %d\n", type);
141 self->group = type;
142 self->group_indent = 1;
143 _undo_record(self, NULL, type, NULL, TRUE, NULL, NULL);
144 }
145 else
146 self->group_indent++;
147}
148
150{
151 if(IS_NULL_PTR(self)) return;
152
153 assert(self->group_indent>0);
154 self->group_indent--;
155 if(self->group_indent == 0)
156 {
157 _undo_record(self, NULL, self->group, NULL, TRUE, NULL, NULL);
158 dt_print(DT_DEBUG_UNDO, "[undo] end group for type %d\n", self->group);
159 self->group = DT_UNDO_NONE;
160 }
161}
162
163void dt_undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data,
164 void (*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item, dt_undo_action_t action, GList **imgs),
165 void (*free_data)(gpointer data))
166{
167 _undo_record(self, user_data, type, data, FALSE, undo, free_data);
168}
169
170gint _images_list_cmp(gconstpointer a, gconstpointer b)
171{
172 return GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b);
173}
174
175static void _undo_do_undo_redo(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
176{
177 if(IS_NULL_PTR(self)) return;
178
179 LOCK;
180
181 // we take/remove item from the FROM list and add them into the TO list:
182 GList **from = action == DT_ACTION_UNDO ? &self->undo_list : &self->redo_list;
183 GList **to = action == DT_ACTION_UNDO ? &self->redo_list : &self->undo_list;
184
185 GList *imgs = NULL;
186
187 // check for first item that is matching the given pattern
188
189 dt_print(DT_DEBUG_UNDO, "[undo] action %s for %d (from length %d -> to length %d)\n",
190 action == DT_ACTION_UNDO?"UNDO":"DO", filter, g_list_length(*from), g_list_length(*to));
191
192 for(GList *l = *from; l; l = g_list_next(l))
193 {
194 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
195
196 if(item->type & filter)
197 {
198 if(item->is_group)
199 {
200 gboolean is_group = FALSE;
201
202 GList *next = g_list_next(l);
203
204 // first move the group item into the TO list
205 *from = g_list_remove(*from, item);
206 *to = g_list_prepend(*to, item);
207
208 while((l = next) && !is_group)
209 {
210 item = (dt_undo_item_t *)l->data;
211 next = g_list_next(l);
212
213 // first remove element from FROM list
214 *from = g_list_remove(*from, item);
215
216 // callback with undo or redo data
217 if(item->is_group)
218 is_group = TRUE;
219 else
220 item->undo(item->user_data, item->type, item->data, action, &imgs);
221
222 // add old position back into the TO list
223 *to = g_list_prepend(*to, item);
224 }
225 }
226 else
227 {
228 const double first_item_ts = item->ts;
229 gboolean in_group = FALSE;
230
231 // when found, handle all items of the same type and in the same time period
232
233 do
234 {
235 GList *next = g_list_next(l);
236
237 // first remove element from FROM list
238 *from = g_list_remove(*from, item);
239
240 if(item->is_group)
241 in_group = !in_group;
242 else
243 // callback with redo or redo data
244 item->undo(item->user_data, item->type, item->data, action, &imgs);
245
246 // add old position back into the TO list
247 *to = g_list_prepend(*to, item);
248
249 l = next;
250 if (l) item = (dt_undo_item_t *)l->data;
251 } while (l && (item->type & filter) && (in_group || (fabs(item->ts - first_item_ts) < MAX_TIME_PERIOD)));
252 }
253
254 break;
255 }
256 }
257 UNLOCK;
258
259 if(imgs)
260 {
261 imgs = g_list_sort(imgs, _images_list_cmp);
262 // remove duplicates
263 for(const GList *img = imgs; img; img = g_list_next(img))
264 while(img->next && img->data == img->next->data)
265 imgs = g_list_delete_link(imgs, img->next);
266 // udpate xmp for updated images
267
269 }
270}
271
272void dt_undo_do_redo(dt_undo_t *self, uint32_t filter)
273{
274 _undo_do_undo_redo(self, filter, DT_ACTION_REDO);
275}
276
277void dt_undo_do_undo(dt_undo_t *self, uint32_t filter)
278{
279 _undo_do_undo_redo(self, filter, DT_ACTION_UNDO);
280}
281
282static int _do_undo_list_length(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
283{
284 if(IS_NULL_PTR(self)) return 0;
285 int count = 0;
286
287 LOCK;
288
289 GList **from = action == DT_ACTION_UNDO ? &self->undo_list : &self->redo_list;
290
291 for(GList *l = *from; l; l = g_list_next(l))
292 {
293 const dt_undo_item_t *item = (const dt_undo_item_t *)l->data;
294 if(item->type & filter) count++;
295 }
296
297 UNLOCK;
298
299 return count;
300}
301
302static gboolean _is_do_undo_list_populated(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
303{
304 if(IS_NULL_PTR(self)) return FALSE;
305 gboolean found_something = FALSE;
306
307 LOCK;
308
309 GList **from = action == DT_ACTION_UNDO ? &self->undo_list : &self->redo_list;
310
311 for(GList *l = *from; l; l = g_list_next(l))
312 {
313 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
314 if(item->type & filter)
315 {
316 found_something = TRUE;
317 break;
318 }
319 }
320
321 UNLOCK;
322
323 return found_something;
324}
325
326gboolean dt_is_undo_list_populated(dt_undo_t *self, uint32_t filter)
327{
328 return _is_do_undo_list_populated(self, filter, DT_ACTION_UNDO);
329}
330
331gboolean dt_is_redo_list_populated(dt_undo_t *self, uint32_t filter)
332{
333 return _is_do_undo_list_populated(self, filter, DT_ACTION_REDO);
334}
335
336int dt_undo_list_length(dt_undo_t *self, uint32_t filter)
337{
338 return _do_undo_list_length(self, filter, DT_ACTION_UNDO);
339}
340
341int dt_redo_list_length(dt_undo_t *self, uint32_t filter)
342{
343 return _do_undo_list_length(self, filter, DT_ACTION_REDO);
344}
345
346static void _undo_clear_list(GList **list, uint32_t filter)
347{
348 // check for first item that is matching the given pattern
349
350 GList *next;
351 for(GList *l = *list; l; l = next)
352 {
353 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
354 next = g_list_next(l); // get next node now, because we may delete the current one
355 if(item->type & filter)
356 {
357 // remove this element
358 *list = g_list_remove(*list, item);
359 _free_undo_data((void *)item);
360 }
361 };
362
363 dt_print(DT_DEBUG_UNDO, "[undo] clear list for %d (length %d)\n",
364 filter, g_list_length(*list));
365}
366
367void dt_undo_clear(dt_undo_t *self, uint32_t filter)
368{
369 if(IS_NULL_PTR(self)) return;
370
371 LOCK;
372 _undo_clear_list(&self->undo_list, filter);
373 _undo_clear_list(&self->redo_list, filter);
374 self->undo_list = NULL;
375 self->redo_list = NULL;
376 self->disable_next = FALSE;
377 UNLOCK;
378}
379
380static void _undo_iterate(GList *list, uint32_t filter, gpointer user_data,
381 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
382{
383 // check for first item that is matching the given pattern
384 for(GList *l = list; l; l = g_list_next(l))
385 {
386 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
387 if(!item->is_group && (item->type & filter))
388 {
389 apply(user_data, item->type, item->data);
390 }
391 };
392}
393
394void dt_undo_iterate_internal(dt_undo_t *self, uint32_t filter, gpointer user_data,
395 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
396{
397 if(IS_NULL_PTR(self)) return;
398
399 _undo_iterate(self->undo_list, filter, user_data, apply);
400 _undo_iterate(self->redo_list, filter, user_data, apply);
401}
402
403
404void dt_undo_iterate(dt_undo_t *self, uint32_t filter, gpointer user_data,
405 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
406{
407 if(IS_NULL_PTR(self)) return;
408
409 LOCK;
410 dt_undo_iterate_internal(self, filter, user_data, apply);
411 UNLOCK;
412}
413
414// clang-format off
415// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
416// vim: shiftwidth=2 expandtab tabstop=2 cindent
417// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
418// clang-format on
#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_image_synch_xmps(const GList *img)
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Initialise a mutex. With mutexattr NULL – which is how 54 of the 56 call sites in this tree spell it ...
Definition dtpthread.h:104
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:132
_lib_location_type_t type
Definition location.c:1
@ DT_DEBUG_UNDO
Definition logging.h:69
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
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
void(* free_data)(gpointer data)
Definition undo.c:47
void(* undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, dt_undo_action_t action, GList **imgs)
Definition undo.c:46
dt_undo_data_t data
Definition undo.c:43
double ts
Definition undo.c:44
gboolean is_group
Definition undo.c:45
gpointer user_data
Definition undo.c:41
dt_undo_type_t type
Definition undo.c:42
dt_pthread_mutex_t mutex
Definition undo.h:77
gboolean disable_next
Definition undo.h:79
gboolean locked
Definition undo.h:78
int group_indent
Definition undo.h:76
GList * undo_list
Definition undo.h:74
GList * redo_list
Definition undo.h:74
dt_undo_type_t group
Definition undo.h:75
static double dt_get_wtime(void)
Definition times.h:43
#define LOCK
Definition undo.c:64
void dt_undo_disable_next(dt_undo_t *self)
Definition undo.c:70
int dt_redo_list_length(dt_undo_t *self, uint32_t filter)
Definition undo.c:341
void dt_undo_do_redo(dt_undo_t *self, uint32_t filter)
Definition undo.c:272
static void _undo_iterate(GList *list, uint32_t filter, gpointer user_data, void(*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
Definition undo.c:380
gboolean dt_is_redo_list_populated(dt_undo_t *self, uint32_t filter)
Definition undo.c:331
static void _undo_clear_list(GList **list, uint32_t filter)
Definition undo.c:346
void dt_undo_end_group(dt_undo_t *self)
Definition undo.c:149
static void _undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, gboolean is_group, 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:89
gint _images_list_cmp(gconstpointer a, gconstpointer b)
Definition undo.c:170
const double MAX_TIME_PERIOD
Definition undo.c:37
int dt_undo_list_length(dt_undo_t *self, uint32_t filter)
How many records match filter, so a caller can tell a one-record undo from a batch of a thousand....
Definition undo.c:336
void dt_undo_cleanup(dt_undo_t *self)
Definition undo.c:76
void dt_undo_clear(dt_undo_t *self, uint32_t filter)
Definition undo.c:367
void dt_undo_iterate_internal(dt_undo_t *self, uint32_t filter, gpointer user_data, void(*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
Definition undo.c:394
void dt_undo_iterate(dt_undo_t *self, uint32_t filter, gpointer user_data, void(*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
Definition undo.c:404
void dt_undo_start_group(dt_undo_t *self, dt_undo_type_t type)
Definition undo.c:134
static void _free_undo_data(void *p)
Definition undo.c:82
static gboolean _is_do_undo_list_populated(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
Definition undo.c:302
#define UNLOCK
Definition undo.c:67
void dt_undo_do_undo(dt_undo_t *self, uint32_t filter)
Definition undo.c:277
static int _do_undo_list_length(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
Definition undo.c:282
static void _undo_do_undo_redo(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
Definition undo.c:175
dt_undo_t * dt_undo_init(void)
Definition undo.c:50
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
gboolean dt_is_undo_list_populated(dt_undo_t *self, uint32_t filter)
Definition undo.c:326
dt_undo_type_t
Definition undo.h:40
@ DT_UNDO_NONE
Definition undo.h:41
@ DT_UNDO_ALL
Definition undo.h:61
void * dt_undo_data_t
Definition undo.h:70
dt_undo_action_t
Definition undo.h:65
@ DT_ACTION_REDO
Definition undo.h:67
@ DT_ACTION_UNDO
Definition undo.h:66