1 /*
2     This file is part of darktable,
3     Copyright (C) 2015-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/collection.h"
20 #include "common/darktable.h"
21 #include "common/debug.h"
22 #include "common/history.h"
23 #include "common/metadata.h"
24 #include "common/mipmap_cache.h"
25 #include "common/selection.h"
26 #include "common/styles.h"
27 #include "control/conf.h"
28 #include "control/control.h"
29 #include "develop/develop.h"
30 #include "dtgtk/thumbnail.h"
31 #include "gui/accelerators.h"
32 #include "gui/gtk.h"
33 #include "gui/styles.h"
34 #include "libs/lib.h"
35 
36 #define DUPLICATE_COMPARE_SIZE 40
37 
38 DT_MODULE(1)
39 
40 typedef struct dt_lib_duplicate_t
41 {
42   GtkWidget *duplicate_box;
43   int imgid;
44   gboolean busy;
45   int cur_final_width;
46   int cur_final_height;
47   int32_t preview_width;
48   int32_t preview_height;
49   gboolean allow_zoom;
50 
51   cairo_surface_t *preview_surf;
52   float preview_zoom;
53   int preview_id;
54 
55   GList *thumbs;
56 } dt_lib_duplicate_t;
57 
name(dt_lib_module_t * self)58 const char *name(dt_lib_module_t *self)
59 {
60   return _("duplicate manager");
61 }
62 
views(dt_lib_module_t * self)63 const char **views(dt_lib_module_t *self)
64 {
65   static const char *v[] = {"darkroom", NULL};
66   return v;
67 }
68 
container(dt_lib_module_t * self)69 uint32_t container(dt_lib_module_t *self)
70 {
71   return DT_UI_CONTAINER_PANEL_LEFT_CENTER;
72 }
73 
position()74 int position()
75 {
76   return 850;
77 }
78 
79 static void _lib_duplicate_init_callback(gpointer instance, dt_lib_module_t *self);
80 
_lib_duplicate_caption_out_callback(GtkWidget * widget,GdkEvent * event,dt_lib_module_t * self)81 static gboolean _lib_duplicate_caption_out_callback(GtkWidget *widget, GdkEvent *event, dt_lib_module_t *self)
82 {
83   const int imgid = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget),"imgid"));
84 
85   // we write the content of the textbox to the caption field
86   dt_metadata_set(imgid, "Xmp.darktable.version_name", gtk_entry_get_text(GTK_ENTRY(widget)), FALSE);
87   dt_image_synch_xmp(imgid);
88 
89   return FALSE;
90 }
91 
_lib_duplicate_new_clicked_callback(GtkWidget * widget,GdkEventButton * event,dt_lib_module_t * self)92 static void _lib_duplicate_new_clicked_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
93 {
94   const int imgid = darktable.develop->image_storage.id;
95   const int newid = dt_image_duplicate(imgid);
96   if (newid <= 0) return;
97   dt_history_delete_on_image(newid);
98   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_TAG_CHANGED);
99   dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL);
100   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, newid);
101 }
_lib_duplicate_duplicate_clicked_callback(GtkWidget * widget,GdkEventButton * event,dt_lib_module_t * self)102 static void _lib_duplicate_duplicate_clicked_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
103 {
104   const int imgid = darktable.develop->image_storage.id;
105   const int newid = dt_image_duplicate(imgid);
106   if (newid <= 0) return;
107   dt_history_copy_and_paste_on_image(imgid, newid, FALSE, NULL, TRUE, TRUE);
108   dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL);
109   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, newid);
110 }
111 
_lib_duplicate_delete(GtkButton * button,dt_lib_module_t * self)112 static void _lib_duplicate_delete(GtkButton *button, dt_lib_module_t *self)
113 {
114   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
115   const int imgid = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "imgid"));
116 
117   if(imgid == darktable.develop->image_storage.id)
118   {
119     // we find the duplicate image to show now
120     for(GList *l = d->thumbs; l; l = g_list_next(l))
121     {
122       dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
123       if(thumb->imgid == imgid)
124       {
125         GList *l2 = g_list_next(l);
126         if(!l2) l2 = g_list_previous(l);
127         if(l2)
128         {
129           dt_thumbnail_t *th2 = (dt_thumbnail_t *)l2->data;
130           DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, th2->imgid);
131           break;
132         }
133       }
134     }
135   }
136 
137   // and we remove the image
138   dt_control_delete_image(imgid);
139   dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF,
140                              g_list_prepend(NULL, GINT_TO_POINTER(imgid)));
141 }
142 
_lib_duplicate_thumb_press_callback(GtkWidget * widget,GdkEventButton * event,dt_lib_module_t * self)143 static void _lib_duplicate_thumb_press_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
144 {
145   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
146   dt_thumbnail_t *thumb = (dt_thumbnail_t *)g_object_get_data(G_OBJECT(widget), "thumb");
147   const int imgid = thumb->imgid;
148 
149   if(event->button == 1)
150   {
151     if(event->type == GDK_BUTTON_PRESS)
152     {
153       dt_develop_t *dev = darktable.develop;
154       if(!dev) return;
155 
156       dt_dev_invalidate(dev);
157       dt_control_queue_redraw_center();
158 
159       dt_dev_invalidate(darktable.develop);
160 
161       d->imgid = imgid;
162       int fw, fh;
163       fw = fh = 0;
164       dt_image_get_final_size(imgid, &fw, &fh);
165       if(d->cur_final_width <= 0)
166         dt_image_get_final_size(dev->image_storage.id, &d->cur_final_width, &d->cur_final_height);
167       d->allow_zoom
168           = (d->cur_final_width - fw < DUPLICATE_COMPARE_SIZE && d->cur_final_width - fw > -DUPLICATE_COMPARE_SIZE
169              && d->cur_final_height - fh < DUPLICATE_COMPARE_SIZE
170              && d->cur_final_height - fh > -DUPLICATE_COMPARE_SIZE);
171       dt_control_queue_redraw_center();
172     }
173     else if(event->type == GDK_2BUTTON_PRESS)
174     {
175       // let's switch to the new image
176       DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, imgid);
177     }
178   }
179 }
180 
_lib_duplicate_thumb_release_callback(GtkWidget * widget,GdkEventButton * event,dt_lib_module_t * self)181 static void _lib_duplicate_thumb_release_callback(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
182 {
183   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
184 
185   d->imgid = 0;
186   if(d->busy)
187   {
188     dt_control_log_busy_leave();
189     dt_control_toast_busy_leave();
190   }
191   d->busy = FALSE;
192   dt_control_queue_redraw_center();
193 }
194 
view_leave(struct dt_lib_module_t * self,struct dt_view_t * old_view,struct dt_view_t * new_view)195 void view_leave(struct dt_lib_module_t *self, struct dt_view_t *old_view, struct dt_view_t *new_view)
196 {
197   // we leave the view. Let's destroy preview surf if any
198   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
199   if(d->preview_surf)
200   {
201     cairo_surface_destroy(d->preview_surf);
202     d->preview_surf = NULL;
203   }
204 }
gui_post_expose(dt_lib_module_t * self,cairo_t * cri,int32_t width,int32_t height,int32_t pointerx,int32_t pointery)205 void gui_post_expose(dt_lib_module_t *self, cairo_t *cri, int32_t width, int32_t height, int32_t pointerx, int32_t pointery)
206 {
207   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
208   if (d->imgid == 0) return;
209   dt_develop_t *dev = darktable.develop;
210   if(!dev->preview_pipe->backbuf || dev->preview_status != DT_DEV_PIXELPIPE_VALID) return;
211 
212   // use the same resolution as main previem image to avoid blur
213   float img_wd, img_ht;
214   if(d->allow_zoom)
215   {
216     img_wd = dev->preview_pipe->backbuf_width;
217     img_ht = dev->preview_pipe->backbuf_height;
218   }
219   else
220   {
221     int w2, h2;
222     dt_image_get_final_size(d->imgid, &w2, &h2);
223     img_wd = w2;
224     img_ht = h2;
225   }
226 
227   const int32_t tb = darktable.develop->border_size;
228 
229   // we rescale the sizes to the screen size
230   if (img_ht * (width - 2 * tb) > img_wd * (height - 2 * tb))
231   {
232     img_wd = img_wd*(height - 2 * tb)/img_ht;
233     img_ht = (height - 2 * tb);
234   }
235   else
236   {
237     img_ht = img_ht*(width - 2 * tb)/img_wd;
238     img_wd = (width - 2 * tb);
239   }
240 
241   // Get the resizing from borders - only to check validity of mipmap cache size
242   float zoom_ratio = 1.f;
243   if(dev->iso_12646.enabled)
244   {
245     if(img_wd - 2 * tb < img_ht - 2 * tb)
246       zoom_ratio = (img_ht - 2 * tb) / img_ht;
247     else
248       zoom_ratio = (img_wd - 2 * tb) / img_wd;
249   }
250 
251   // if image have too different sizes, we show the full preview not zoomed
252   float nz = 1.0f;
253   if(d->allow_zoom)
254   {
255     const int closeup = dt_control_get_dev_closeup();
256     const dt_dev_zoom_t zoom = dt_control_get_dev_zoom();
257     const float min_scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_FIT, 1 << closeup, 0);
258     const float cur_scale = dt_dev_get_zoom_scale(dev, zoom, 1 << closeup, 0);
259     // if cur_scale is >=2.0f (200%) we disable preview as it can hit cairo size limits without warnings
260     if(cur_scale >= 2.0f)
261     {
262       /* xgettext:no-c-format */
263       dt_control_log(_("preview is only possible for zoom lower than 200%%..."));
264       return;
265     }
266     nz = cur_scale / min_scale;
267   }
268 
269   // if not cached, load or reload a mipmap
270   dt_view_surface_value_t res = DT_VIEW_SURFACE_OK;
271   if(d->preview_id != d->imgid || d->preview_zoom != nz * zoom_ratio || !d->preview_surf
272      || d->preview_width != width || d->preview_height != height)
273   {
274     d->preview_width = width;
275     d->preview_height = height;
276 
277     res = dt_view_image_get_surface(d->imgid, img_wd * nz, img_ht * nz, &d->preview_surf, TRUE);
278 
279     if(res == DT_VIEW_SURFACE_OK)
280     {
281       d->preview_id = d->imgid;
282       d->preview_zoom = nz * zoom_ratio; //  only to check validity of mipmap cache size
283     }
284   }
285 
286   // if ready, we draw the surface
287   if(d->preview_surf)
288   {
289     cairo_save(cri);
290     // force middle grey in background
291     if(dev->iso_12646.enabled)
292       cairo_set_source_rgb(cri, 0.5, 0.5, 0.5);
293     else
294       dt_gui_gtk_set_source_rgb(cri, DT_GUI_COLOR_DARKROOM_BG);
295 
296     // draw background
297     cairo_paint(cri);
298 
299     // move coordinates according to margin
300     float wd, ht;
301     if(d->allow_zoom)
302     {
303       wd = dev->pipe->output_backbuf_width / darktable.gui->ppd;
304       ht = dev->pipe->output_backbuf_height / darktable.gui->ppd;
305     }
306     else
307     {
308       wd = img_wd / darktable.gui->ppd;
309       ht = img_ht / darktable.gui->ppd;
310     }
311     const float margin_left = ceilf(.5f * (width - wd));
312     const float margin_top = ceilf(.5f * (height - ht));
313     cairo_translate(cri, margin_left, margin_top);
314 
315     if(dev->iso_12646.enabled)
316     {
317       // draw the white frame around picture
318       cairo_rectangle(cri, -tb / 3., -tb / 3., wd + 2. * tb / 3., ht + 2. * tb / 3.);
319       cairo_set_source_rgb(cri, 1., 1., 1.);
320       cairo_fill(cri);
321     }
322 
323     // finally, draw the image
324     cairo_rectangle(cri, 0, 0, wd, ht);
325     cairo_clip_preserve(cri);
326 
327     const float scaler = 1.0f / darktable.gui->ppd_thb;
328     cairo_scale(cri, scaler, scaler);
329 
330 
331     if(d->allow_zoom)
332     {
333       // compute the surface pixel shift to match reference image FIXME!
334       const float zoom_y = dt_control_get_dev_zoom_y();
335       const float zoom_x = dt_control_get_dev_zoom_x();
336       const float dx = -floorf(zoom_x * (img_wd)*nz + img_wd * nz / 2. - width / 2.) - margin_left;
337       const float dy = -floorf(zoom_y * (img_ht)*nz + img_ht * nz / 2. - height / 2.) - margin_top;
338       cairo_set_source_surface(cri, d->preview_surf, dx / scaler, dy / scaler);
339     }
340     else
341       cairo_set_source_surface(cri, d->preview_surf, 0, 0);
342 
343     cairo_pattern_set_filter(cairo_get_source(cri), (darktable.gui->filter_image == CAIRO_FILTER_FAST)
344       ? CAIRO_FILTER_GOOD : darktable.gui->filter_image) ;
345     cairo_paint(cri);
346 
347     cairo_restore(cri);
348   }
349 
350   if(res != DT_VIEW_SURFACE_OK)
351   {
352     if(!d->busy)
353     {
354       dt_control_log_busy_enter();
355       dt_control_toast_busy_enter();
356     }
357     d->busy = TRUE;
358   }
359   else
360   {
361     if(d->busy)
362     {
363       dt_control_log_busy_leave();
364       dt_control_toast_busy_leave();
365     }
366     d->busy = FALSE;
367   }
368 }
369 
_thumb_remove(gpointer user_data)370 static void _thumb_remove(gpointer user_data)
371 {
372   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
373   gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(thumb->w_main)), thumb->w_main);
374   dt_thumbnail_destroy(thumb);
375 }
376 
_lib_duplicate_init_callback(gpointer instance,dt_lib_module_t * self)377 static void _lib_duplicate_init_callback(gpointer instance, dt_lib_module_t *self)
378 {
379   //block signals to avoid concurrent calls
380   dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(_lib_duplicate_init_callback), self);
381 
382   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
383 
384   d->imgid = 0;
385   // we drop the preview if any
386   if(d->preview_surf)
387   {
388     cairo_surface_destroy(d->preview_surf);
389     d->preview_surf = NULL;
390   }
391   // we drop all the thumbs
392   g_list_free_full(d->thumbs, _thumb_remove);
393   d->thumbs = NULL;
394   // and the other widgets too
395   dt_gui_container_destroy_children(GTK_CONTAINER(d->duplicate_box));
396   // retrieve all the versions of the image
397   sqlite3_stmt *stmt;
398   dt_develop_t *dev = darktable.develop;
399 
400   int count = 0;
401 
402   // we get a summarize of all versions of the image
403   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
404                               "SELECT i.version, i.id, m.value"
405                               " FROM images AS i"
406                               " LEFT JOIN meta_data AS m ON m.id = i.id AND m.key = ?3"
407                               " WHERE film_id = ?1 AND filename = ?2"
408                               " ORDER BY i.version",
409                               -1, &stmt, NULL);
410   DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, dev->image_storage.film_id);
411   DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, dev->image_storage.filename, -1, SQLITE_TRANSIENT);
412   DT_DEBUG_SQLITE3_BIND_INT(stmt, 3, DT_METADATA_XMP_VERSION_NAME);
413 
414   GtkWidget *bt = NULL;
415 
416   while(sqlite3_step(stmt) == SQLITE_ROW)
417   {
418     GtkWidget *hb = gtk_grid_new();
419     const int imgid = sqlite3_column_int(stmt, 1);
420 
421     GtkStyleContext *context = gtk_widget_get_style_context(hb);
422     gtk_style_context_add_class(context, "dt_overlays_always");
423 
424     dt_thumbnail_t *thumb = dt_thumbnail_new(100, 100, IMG_TO_FIT, imgid, -1, DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL,
425                                              DT_THUMBNAIL_CONTAINER_LIGHTTABLE, TRUE);
426     thumb->sel_mode = DT_THUMBNAIL_SEL_MODE_DISABLED;
427     thumb->disable_mouseover = TRUE;
428     thumb->disable_actions = TRUE;
429     dt_thumbnail_set_mouseover(thumb, imgid == dev->image_storage.id);
430 
431     if (imgid != dev->image_storage.id)
432     {
433       g_signal_connect(G_OBJECT(thumb->w_main), "button-press-event",
434                        G_CALLBACK(_lib_duplicate_thumb_press_callback), self);
435       g_signal_connect(G_OBJECT(thumb->w_main), "button-release-event",
436                        G_CALLBACK(_lib_duplicate_thumb_release_callback), self);
437     }
438 
439     gchar chl[256];
440     gchar *path = (gchar *)sqlite3_column_text(stmt, 2);
441     g_snprintf(chl, sizeof(chl), "%d", sqlite3_column_int(stmt, 0));
442 
443     GtkWidget *tb = gtk_entry_new();
444     if(path) gtk_entry_set_text(GTK_ENTRY(tb), path);
445     gtk_entry_set_width_chars(GTK_ENTRY(tb), 0);
446     gtk_widget_set_hexpand(tb, TRUE);
447     g_object_set_data (G_OBJECT(tb), "imgid", GINT_TO_POINTER(imgid));
448     g_signal_connect(G_OBJECT(tb), "focus-out-event", G_CALLBACK(_lib_duplicate_caption_out_callback), self);
449     dt_gui_key_accel_block_on_focus_connect(GTK_WIDGET(tb));
450     GtkWidget *lb = gtk_label_new (g_strdup(chl));
451     gtk_widget_set_hexpand(lb, TRUE);
452     bt = dtgtk_button_new(dtgtk_cairo_paint_cancel, CPF_STYLE_FLAT, NULL);
453 //    gtk_widget_set_halign(bt, GTK_ALIGN_END);
454     g_object_set_data(G_OBJECT(bt), "imgid", GINT_TO_POINTER(imgid));
455     g_signal_connect(G_OBJECT(bt), "clicked", G_CALLBACK(_lib_duplicate_delete), self);
456 
457     gtk_grid_attach(GTK_GRID(hb), thumb->w_main, 0, 0, 1, 2);
458     gtk_grid_attach(GTK_GRID(hb), bt, 2, 0, 1, 1);
459     gtk_grid_attach(GTK_GRID(hb), lb, 1, 0, 1, 1);
460     gtk_grid_attach(GTK_GRID(hb), tb, 1, 1, 2, 1);
461 
462     gtk_widget_show_all(hb);
463 
464     gtk_box_pack_start(GTK_BOX(d->duplicate_box), hb, FALSE, FALSE, 0);
465     d->thumbs = g_list_append(d->thumbs, thumb);
466     count++;
467   }
468   sqlite3_finalize (stmt);
469 
470   gtk_widget_show(d->duplicate_box);
471 
472   // we have a single image, do not allow it to be removed so hide last bt
473   if(count==1)
474   {
475     gtk_widget_set_sensitive(bt, FALSE);
476     gtk_widget_set_visible(bt, FALSE);
477   }
478 
479   // and reset the final size of the current image
480   if(dev->image_storage.id >= 0)
481   {
482     d->cur_final_width = 0;
483     d->cur_final_height = 0;
484   }
485 
486   dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(_lib_duplicate_init_callback), self); //unblock signals
487 }
488 
_lib_duplicate_collection_changed(gpointer instance,dt_collection_change_t query_change,dt_collection_properties_t changed_property,gpointer imgs,int next,dt_lib_module_t * self)489 static void _lib_duplicate_collection_changed(gpointer instance, dt_collection_change_t query_change,
490                                               dt_collection_properties_t changed_property, gpointer imgs, int next,
491                                               dt_lib_module_t *self)
492 {
493   _lib_duplicate_init_callback(instance, self);
494 }
495 
_lib_duplicate_mipmap_updated_callback(gpointer instance,int imgid,dt_lib_module_t * self)496 static void _lib_duplicate_mipmap_updated_callback(gpointer instance, int imgid, dt_lib_module_t *self)
497 {
498   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
499   // we reset the final size of the current image
500   if(imgid > 0 && darktable.develop->image_storage.id == imgid)
501   {
502     d->cur_final_width = 0;
503     d->cur_final_height = 0;
504   }
505 
506   gtk_widget_queue_draw(d->duplicate_box);
507   dt_control_queue_redraw_center();
508 }
_lib_duplicate_preview_updated_callback(gpointer instance,dt_lib_module_t * self)509 static void _lib_duplicate_preview_updated_callback(gpointer instance, dt_lib_module_t *self)
510 {
511   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)self->data;
512   // we reset the final size of the current image
513   if(darktable.develop->image_storage.id >= 0)
514   {
515     d->cur_final_width = 0;
516     d->cur_final_height = 0;
517   }
518 
519   gtk_widget_queue_draw (d->duplicate_box);
520   dt_control_queue_redraw_center();
521 }
522 
gui_init(dt_lib_module_t * self)523 void gui_init(dt_lib_module_t *self)
524 {
525   /* initialize ui widgets */
526   dt_lib_duplicate_t *d = (dt_lib_duplicate_t *)g_malloc0(sizeof(dt_lib_duplicate_t));
527   self->data = (void *)d;
528 
529   d->imgid = 0;
530   d->preview_surf = NULL;
531   d->preview_zoom = 1.0;
532   d->preview_width = 0;
533   d->preview_height = 0;
534 
535   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
536   GtkStyleContext *context = gtk_widget_get_style_context(self->widget);
537   gtk_style_context_add_class(context, "duplicate-ui");
538   dt_gui_add_help_link(self->widget, dt_get_help_url(self->plugin_name));
539 
540   d->duplicate_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
541 
542   GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
543   GtkWidget *bt = dt_ui_button_new(_("original"), _("create a 'virgin' duplicate of the image without any development"), NULL);
544   g_signal_connect(G_OBJECT(bt), "clicked", G_CALLBACK(_lib_duplicate_new_clicked_callback), self);
545   gtk_box_pack_end(GTK_BOX(hb), bt, TRUE, TRUE, 0);
546   bt = dt_ui_button_new(_("duplicate"), _("create a duplicate of the image with same history stack"), (char *)NULL);
547   g_signal_connect(G_OBJECT(bt), "clicked", G_CALLBACK(_lib_duplicate_duplicate_clicked_callback), self);
548   gtk_box_pack_end(GTK_BOX(hb), bt, TRUE, TRUE, 0);
549 
550   /* add duplicate list and buttonbox to widget */
551   gtk_box_pack_start(GTK_BOX(self->widget),
552                      dt_ui_scroll_wrap(d->duplicate_box, 1, "plugins/darkroom/duplicate/windowheight"), TRUE, TRUE, 0);
553   gtk_box_pack_start(GTK_BOX(self->widget), hb, TRUE, TRUE, 0);
554 
555   gtk_widget_show_all(self->widget);
556 
557   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_IMAGE_CHANGED, G_CALLBACK(_lib_duplicate_init_callback), self);
558   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_INITIALIZE, G_CALLBACK(_lib_duplicate_init_callback), self);
559   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_COLLECTION_CHANGED,
560                             G_CALLBACK(_lib_duplicate_collection_changed), self);
561   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_MIPMAP_UPDATED, G_CALLBACK(_lib_duplicate_mipmap_updated_callback), (gpointer)self);
562   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED,
563                             G_CALLBACK(_lib_duplicate_preview_updated_callback), self);
564 }
565 
gui_cleanup(dt_lib_module_t * self)566 void gui_cleanup(dt_lib_module_t *self)
567 {
568   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_lib_duplicate_init_callback), self);
569   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_lib_duplicate_mipmap_updated_callback), self);
570   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_lib_duplicate_preview_updated_callback), self);
571   g_free(self->data);
572   self->data = NULL;
573 }
574 
575 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
576 // vim: shiftwidth=2 expandtab tabstop=2 cindent
577 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-space on;
578