1 /*
2 This file is part of darktable,
3 Copyright (C) 2017-2021 darktable developers.
4
5 darktable 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 darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "common/undo.h"
20 #include "common/collection.h"
21 #include "common/darktable.h"
22 #include "common/image.h"
23 #include "control/control.h"
24 #include <glib.h> // for GList, gpointer, g_list_prepend
25 #include <stdlib.h> // for NULL, malloc, free
26 #include <sys/time.h>
27
28 const double MAX_TIME_PERIOD = 0.5; // in second
29
30 typedef struct dt_undo_item_t
31 {
32 gpointer user_data;
33 dt_undo_type_t type;
34 dt_undo_data_t data;
35 double ts;
36 gboolean is_group;
37 void (*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, dt_undo_action_t action, GList **imgs);
38 void (*free_data)(gpointer data);
39 } dt_undo_item_t;
40
dt_undo_init(void)41 dt_undo_t *dt_undo_init(void)
42 {
43 dt_undo_t *udata = malloc(sizeof(dt_undo_t));
44 udata->undo_list = NULL;
45 udata->redo_list = NULL;
46 udata->disable_next = FALSE;
47 udata->locked = FALSE;
48 dt_pthread_mutex_init(&udata->mutex, NULL);
49 udata->group = DT_UNDO_NONE;
50 udata->group_indent = 0;
51 dt_print(DT_DEBUG_UNDO, "[undo] init\n");
52 return udata;
53 }
54
55 #define LOCK \
56 dt_pthread_mutex_lock(&self->mutex); self->locked = TRUE
57
58 #define UNLOCK \
59 self->locked = FALSE; dt_pthread_mutex_unlock(&self->mutex)
60
dt_undo_disable_next(dt_undo_t * self)61 void dt_undo_disable_next(dt_undo_t *self)
62 {
63 self->disable_next = TRUE;
64 dt_print(DT_DEBUG_UNDO, "[undo] disable next\n");
65 }
66
dt_undo_cleanup(dt_undo_t * self)67 void dt_undo_cleanup(dt_undo_t *self)
68 {
69 dt_undo_clear(self, DT_UNDO_ALL);
70 dt_pthread_mutex_destroy(&self->mutex);
71 }
72
_free_undo_data(void * p)73 static void _free_undo_data(void *p)
74 {
75 dt_undo_item_t *item = (dt_undo_item_t *)p;
76 if (item->free_data) item->free_data(item->data);
77 free(item);
78 }
79
_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))80 static void _undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data,
81 gboolean is_group,
82 void (*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item, dt_undo_action_t action, GList **imgs),
83 void (*free_data)(gpointer data))
84 {
85 if(!self) return;
86
87 if(self->disable_next)
88 {
89 if(free_data) free_data(data);
90 self->disable_next = FALSE;
91 }
92 else
93 {
94 // do not block, if an undo record is asked and there is a lock it means that this call has been done in un
95 // undo/redo callback. We just skip this event.
96
97 if(!self->locked)
98 {
99 LOCK;
100
101 dt_undo_item_t *item = malloc(sizeof(dt_undo_item_t));
102
103 item->user_data = user_data;
104 item->type = type;
105 item->data = data;
106 item->undo = undo;
107 item->free_data = free_data;
108 item->ts = dt_get_wtime();
109 item->is_group = is_group;
110
111 self->undo_list = g_list_prepend(self->undo_list, (gpointer)item);
112
113 // recording an undo data invalidate all the redo
114 g_list_free_full(self->redo_list, _free_undo_data);
115 self->redo_list = NULL;
116
117 dt_print(DT_DEBUG_UNDO, "[undo] record for type %d (length %d)\n",
118 type, g_list_length(self->undo_list));
119
120 UNLOCK;
121 }
122 }
123 }
124
dt_undo_start_group(dt_undo_t * self,dt_undo_type_t type)125 void dt_undo_start_group(dt_undo_t *self, dt_undo_type_t type)
126 {
127 if(!self) return;
128
129 if(self->group == DT_UNDO_NONE)
130 {
131 dt_print(DT_DEBUG_UNDO, "[undo] start group for type %d\n", type);
132 self->group = type;
133 self->group_indent = 1;
134 _undo_record(self, NULL, type, NULL, TRUE, NULL, NULL);
135 }
136 else
137 self->group_indent++;
138 }
139
dt_undo_end_group(dt_undo_t * self)140 void dt_undo_end_group(dt_undo_t *self)
141 {
142 if(!self) return;
143
144 assert(self->group_indent>0);
145 self->group_indent--;
146 if(self->group_indent == 0)
147 {
148 _undo_record(self, NULL, self->group, NULL, TRUE, NULL, NULL);
149 dt_print(DT_DEBUG_UNDO, "[undo] end group for type %d\n", self->group);
150 self->group = DT_UNDO_NONE;
151 }
152 }
153
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))154 void dt_undo_record(dt_undo_t *self, gpointer user_data, dt_undo_type_t type, dt_undo_data_t data,
155 void (*undo)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item, dt_undo_action_t action, GList **imgs),
156 void (*free_data)(gpointer data))
157 {
158 _undo_record(self, user_data, type, data, FALSE, undo, free_data);
159 }
160
_images_list_cmp(gconstpointer a,gconstpointer b)161 gint _images_list_cmp(gconstpointer a, gconstpointer b)
162 {
163 return GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b);
164 }
165
_undo_do_undo_redo(dt_undo_t * self,uint32_t filter,dt_undo_action_t action)166 static void _undo_do_undo_redo(dt_undo_t *self, uint32_t filter, dt_undo_action_t action)
167 {
168 if(!self) return;
169
170 LOCK;
171
172 // we take/remove item from the FROM list and add them into the TO list:
173 GList **from = action == DT_ACTION_UNDO ? &self->undo_list : &self->redo_list;
174 GList **to = action == DT_ACTION_UNDO ? &self->redo_list : &self->undo_list;
175
176 GList *imgs = NULL;
177
178 // check for first item that is matching the given pattern
179
180 dt_print(DT_DEBUG_UNDO, "[undo] action %s for %d (from length %d -> to length %d)\n",
181 action == DT_ACTION_UNDO?"UNDO":"DO", filter, g_list_length(*from), g_list_length(*to));
182
183 for(GList *l = *from; l; l = g_list_next(l))
184 {
185 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
186
187 if(item->type & filter)
188 {
189 if(item->is_group)
190 {
191 gboolean is_group = FALSE;
192
193 GList *next = g_list_next(l);
194
195 // first move the group item into the TO list
196 *from = g_list_remove(*from, item);
197 *to = g_list_prepend(*to, item);
198
199 while((l = next) && !is_group)
200 {
201 item = (dt_undo_item_t *)l->data;
202 next = g_list_next(l);
203
204 // first remove element from FROM list
205 *from = g_list_remove(*from, item);
206
207 // callback with undo or redo data
208 if(item->is_group)
209 is_group = TRUE;
210 else
211 item->undo(item->user_data, item->type, item->data, action, &imgs);
212
213 // add old position back into the TO list
214 *to = g_list_prepend(*to, item);
215 }
216 }
217 else
218 {
219 const double first_item_ts = item->ts;
220 gboolean in_group = FALSE;
221
222 // when found, handle all items of the same type and in the same time period
223
224 do
225 {
226 GList *next = g_list_next(l);
227
228 // first remove element from FROM list
229 *from = g_list_remove(*from, item);
230
231 if(item->is_group)
232 in_group = !in_group;
233 else
234 // callback with redo or redo data
235 item->undo(item->user_data, item->type, item->data, action, &imgs);
236
237 // add old position back into the TO list
238 *to = g_list_prepend(*to, item);
239
240 l = next;
241 if (l) item = (dt_undo_item_t *)l->data;
242 } while (l && (item->type & filter) && (in_group || (fabs(item->ts - first_item_ts) < MAX_TIME_PERIOD)));
243 }
244
245 break;
246 }
247 }
248 UNLOCK;
249
250 if(imgs)
251 {
252 imgs = g_list_sort(imgs, _images_list_cmp);
253 // remove duplicates
254 for(const GList *img = imgs; img; img = g_list_next(img))
255 while(img->next && img->data == img->next->data)
256 imgs = g_list_delete_link(imgs, img->next);
257 // udpate xmp for updated images
258
259 dt_image_synch_xmps(imgs);
260 }
261
262 dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, imgs);
263 }
264
dt_undo_do_redo(dt_undo_t * self,uint32_t filter)265 void dt_undo_do_redo(dt_undo_t *self, uint32_t filter)
266 {
267 _undo_do_undo_redo(self, filter, DT_ACTION_REDO);
268 }
269
dt_undo_do_undo(dt_undo_t * self,uint32_t filter)270 void dt_undo_do_undo(dt_undo_t *self, uint32_t filter)
271 {
272 _undo_do_undo_redo(self, filter, DT_ACTION_UNDO);
273 }
274
_undo_clear_list(GList ** list,uint32_t filter)275 static void _undo_clear_list(GList **list, uint32_t filter)
276 {
277 // check for first item that is matching the given pattern
278
279 GList *next;
280 for(GList *l = *list; l; l = next)
281 {
282 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
283 next = g_list_next(l); // get next node now, because we may delete the current one
284 if(item->type & filter)
285 {
286 // remove this element
287 *list = g_list_remove(*list, item);
288 _free_undo_data((void *)item);
289 }
290 };
291
292 dt_print(DT_DEBUG_UNDO, "[undo] clear list for %d (length %d)\n",
293 filter, g_list_length(*list));
294 }
295
dt_undo_clear(dt_undo_t * self,uint32_t filter)296 void dt_undo_clear(dt_undo_t *self, uint32_t filter)
297 {
298 if(!self) return;
299
300 LOCK;
301 _undo_clear_list(&self->undo_list, filter);
302 _undo_clear_list(&self->redo_list, filter);
303 self->undo_list = NULL;
304 self->redo_list = NULL;
305 self->disable_next = FALSE;
306 UNLOCK;
307 }
308
_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))309 static void _undo_iterate(GList *list, uint32_t filter, gpointer user_data,
310 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
311 {
312 // check for first item that is matching the given pattern
313 for(GList *l = list; l; l = g_list_next(l))
314 {
315 dt_undo_item_t *item = (dt_undo_item_t *)l->data;
316 if(!item->is_group && (item->type & filter))
317 {
318 apply(user_data, item->type, item->data);
319 }
320 };
321 }
322
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))323 void dt_undo_iterate_internal(dt_undo_t *self, uint32_t filter, gpointer user_data,
324 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
325 {
326 if(!self) return;
327
328 _undo_iterate(self->undo_list, filter, user_data, apply);
329 _undo_iterate(self->redo_list, filter, user_data, apply);
330 }
331
332
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))333 void dt_undo_iterate(dt_undo_t *self, uint32_t filter, gpointer user_data,
334 void (*apply)(gpointer user_data, dt_undo_type_t type, dt_undo_data_t item))
335 {
336 if(!self) return;
337
338 LOCK;
339 dt_undo_iterate_internal(self, filter, user_data, apply);
340 UNLOCK;
341 }
342
343 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
344 // vim: shiftwidth=2 expandtab tabstop=2 cindent
345 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
346