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