1 /*
2     This file is part of darktable,
3     Copyright (C) 2019-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 /** this is the thumbnail class for the lighttable module.  */
19 #include "dtgtk/thumbnail.h"
20 
21 #include "bauhaus/bauhaus.h"
22 #include "common/collection.h"
23 #include "common/debug.h"
24 #include "common/focus.h"
25 #include "common/focus_peaking.h"
26 #include "common/grouping.h"
27 #include "common/image_cache.h"
28 #include "common/ratings.h"
29 #include "common/selection.h"
30 #include "common/variables.h"
31 #include "control/control.h"
32 #include "dtgtk/button.h"
33 #include "dtgtk/icon.h"
34 #include "dtgtk/thumbnail_btn.h"
35 #include "gui/drag_and_drop.h"
36 #include "gui/accelerators.h"
37 #include "views/view.h"
38 
39 static void _thumb_resize_overlays(dt_thumbnail_t *thumb);
40 
_set_flag(GtkWidget * w,GtkStateFlags flag,gboolean over)41 static void _set_flag(GtkWidget *w, GtkStateFlags flag, gboolean over)
42 {
43   int flags = gtk_widget_get_state_flags(w);
44   if(over)
45     flags |= flag;
46   else
47     flags &= ~flag;
48 
49   gtk_widget_set_state_flags(w, flags, TRUE);
50 }
51 
52 // create a new extended infos line from strach
_thumb_update_extended_infos_line(dt_thumbnail_t * thumb)53 static void _thumb_update_extended_infos_line(dt_thumbnail_t *thumb)
54 {
55   gchar *pattern = dt_conf_get_string("plugins/lighttable/extended_pattern");
56   // we compute the info line (we reuse the function used in export to disk)
57   char input_dir[1024] = { 0 };
58   gboolean from_cache = TRUE;
59   dt_image_full_path(thumb->imgid, input_dir, sizeof(input_dir), &from_cache);
60 
61   dt_variables_params_t *vp;
62   dt_variables_params_init(&vp);
63 
64   vp->filename = input_dir;
65   vp->jobcode = "infos";
66   vp->imgid = thumb->imgid;
67   vp->sequence = 0;
68   vp->escape_markup = TRUE;
69 
70   if(thumb->info_line) g_free(thumb->info_line);
71   thumb->info_line = dt_variables_expand(vp, pattern, TRUE);
72 
73   dt_variables_params_destroy(vp);
74 
75   g_free(pattern);
76 }
77 
_image_update_group_tooltip(dt_thumbnail_t * thumb)78 static void _image_update_group_tooltip(dt_thumbnail_t *thumb)
79 {
80   if(!thumb->w_group) return;
81   if(!thumb->is_grouped)
82   {
83     gtk_widget_set_has_tooltip(thumb->w_group, FALSE);
84     return;
85   }
86 
87   gchar *tt = NULL;
88   int nb = 0;
89 
90   // the group leader
91   if(thumb->imgid == thumb->groupid)
92     tt = g_strdup_printf("\n<b>%s (%s)</b>", _("current"), _("leader"));
93   else
94   {
95     const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->groupid, 'r');
96     if(img)
97     {
98       tt = g_strdup_printf("\n<b>%s (%s)</b>", img->filename, _("leader"));
99       dt_image_cache_read_release(darktable.image_cache, img);
100     }
101   }
102 
103   // and the other images
104   sqlite3_stmt *stmt;
105   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
106                               "SELECT id, version, filename FROM main.images WHERE group_id = ?1", -1, &stmt,
107                               NULL);
108   DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, thumb->groupid);
109   while(sqlite3_step(stmt) == SQLITE_ROW)
110   {
111     nb++;
112     const int id = sqlite3_column_int(stmt, 0);
113     const int v = sqlite3_column_int(stmt, 1);
114 
115     if(id != thumb->groupid)
116     {
117       if(id == thumb->imgid)
118         tt = dt_util_dstrcat(tt, "\n%s", _("current"));
119       else
120       {
121         tt = dt_util_dstrcat(tt, "\n%s", sqlite3_column_text(stmt, 2));
122         if(v > 0) tt = dt_util_dstrcat(tt, " v%d", v);
123       }
124     }
125   }
126   sqlite3_finalize(stmt);
127 
128   // and the number of grouped images
129   gchar *ttf = g_strdup_printf("%d %s\n%s", nb, _("grouped images"), tt);
130   g_free(tt);
131 
132   // let's apply the tooltip
133   gtk_widget_set_tooltip_markup(thumb->w_group, ttf);
134   g_free(ttf);
135 }
136 
_thumb_update_rating_class(dt_thumbnail_t * thumb)137 static void _thumb_update_rating_class(dt_thumbnail_t *thumb)
138 {
139   if(!thumb->w_main) return;
140 
141   GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_main);
142   for(int i = DT_VIEW_DESERT; i <= DT_VIEW_REJECT; i++)
143   {
144     gchar *cn = g_strdup_printf("dt_thumbnail_rating_%d", i);
145     if(thumb->rating == i)
146       gtk_style_context_add_class(context, cn);
147     else
148       gtk_style_context_remove_class(context, cn);
149     g_free(cn);
150   }
151 }
152 
_image_get_infos(dt_thumbnail_t * thumb)153 static void _image_get_infos(dt_thumbnail_t *thumb)
154 {
155   if(thumb->imgid <= 0) return;
156   if(thumb->over == DT_THUMBNAIL_OVERLAYS_NONE) return;
157 
158   // we only get here infos that might change, others(exif, ...) are cached on widget creation
159 
160   const int old_rating = thumb->rating;
161   thumb->rating = 0;
162   const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->imgid, 'r');
163   if(img)
164   {
165     thumb->has_localcopy = (img->flags & DT_IMAGE_LOCAL_COPY);
166     thumb->rating = img->flags & DT_IMAGE_REJECTED ? DT_VIEW_REJECT : (img->flags & DT_VIEW_RATINGS_MASK);
167     thumb->is_bw = dt_image_monochrome_flags(img);
168     thumb->is_bw_flow = dt_image_use_monochrome_workflow(img);
169     thumb->is_hdr = dt_image_is_hdr(img);
170 
171     thumb->groupid = img->group_id;
172 
173     dt_image_cache_read_release(darktable.image_cache, img);
174   }
175   // if the rating as changed, update the rejected
176   if(old_rating != thumb->rating)
177   {
178     _thumb_update_rating_class(thumb);
179   }
180 
181   // colorlabels
182   thumb->colorlabels = 0;
183   DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.get_color);
184   DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.get_color);
185   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_color, 1, thumb->imgid);
186   while(sqlite3_step(darktable.view_manager->statements.get_color) == SQLITE_ROW)
187   {
188     const int col = sqlite3_column_int(darktable.view_manager->statements.get_color, 0);
189     // we reuse CPF_* flags, as we'll pass them to the paint fct after
190     if(col == 0)
191       thumb->colorlabels |= CPF_DIRECTION_UP;
192     else if(col == 1)
193       thumb->colorlabels |= CPF_DIRECTION_DOWN;
194     else if(col == 2)
195       thumb->colorlabels |= CPF_DIRECTION_LEFT;
196     else if(col == 3)
197       thumb->colorlabels |= CPF_DIRECTION_RIGHT;
198     else if(col == 4)
199       thumb->colorlabels |= CPF_BG_TRANSPARENT;
200   }
201   if(thumb->w_color)
202   {
203     GtkDarktableThumbnailBtn *btn = (GtkDarktableThumbnailBtn *)thumb->w_color;
204     btn->icon_flags = thumb->colorlabels;
205   }
206 
207   // altered
208   thumb->is_altered = dt_image_altered(thumb->imgid);
209 
210   // grouping
211   DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.get_grouped);
212   DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.get_grouped);
213   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_grouped, 1, thumb->imgid);
214   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.get_grouped, 2, thumb->imgid);
215   thumb->is_grouped = (sqlite3_step(darktable.view_manager->statements.get_grouped) == SQLITE_ROW);
216 
217   // grouping tooltip
218   _image_update_group_tooltip(thumb);
219 }
220 
_thumb_expose_again(gpointer user_data)221 static gboolean _thumb_expose_again(gpointer user_data)
222 {
223   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
224   if(!thumb) return FALSE;
225   gpointer w_image = thumb->w_image;
226   if(!w_image || !GTK_IS_WIDGET(w_image)) return FALSE;
227 
228   thumb->expose_again_timeout_id = 0;
229   gtk_widget_queue_draw((GtkWidget *)w_image);
230   return FALSE;
231 }
232 
_thumb_set_image_size(dt_thumbnail_t * thumb,int image_w,int image_h)233 static void _thumb_set_image_size(dt_thumbnail_t *thumb, int image_w, int image_h)
234 {
235   int imgbox_w = 0;
236   int imgbox_h = 0;
237   gtk_widget_get_size_request(thumb->w_image_box, &imgbox_w, &imgbox_h);
238 
239   gtk_widget_set_size_request(thumb->w_image, MIN(image_w, imgbox_w), MIN(image_h, imgbox_h));
240 }
241 
_thumb_draw_image(dt_thumbnail_t * thumb,cairo_t * cr)242 static void _thumb_draw_image(dt_thumbnail_t *thumb, cairo_t *cr)
243 {
244   if(!thumb->w_image) return;
245 
246   // we draw the image
247   GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_image);
248   int w = 0;
249   int h = 0;
250   gtk_widget_get_size_request(thumb->w_image, &w, &h);
251 
252   // Safety check to avoid possible error
253   if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) >= 1)
254   {
255     cairo_save(cr);
256     const float scaler = 1.0f / darktable.gui->ppd_thb;
257     cairo_scale(cr, scaler, scaler);
258 
259     cairo_set_source_surface(cr, thumb->img_surf, thumb->zoomx * darktable.gui->ppd,
260                              thumb->zoomy * darktable.gui->ppd);
261 
262     // get the transparency value
263     GdkRGBA im_color;
264     gtk_style_context_get_color(context, gtk_widget_get_state_flags(thumb->w_image), &im_color);
265     cairo_paint_with_alpha(cr, im_color.alpha);
266 
267     // and eventually the image border
268     gtk_render_frame(context, cr, 0, 0, w * darktable.gui->ppd_thb, h * darktable.gui->ppd_thb);
269     cairo_restore(cr);
270   }
271 
272   // if needed we draw the working msg too
273   if(thumb->busy)
274   {
275     dt_control_draw_busy_msg(cr, w, h);
276   }
277 }
278 
_thumb_retrieve_margins(dt_thumbnail_t * thumb)279 static void _thumb_retrieve_margins(dt_thumbnail_t *thumb)
280 {
281   if(thumb->img_margin) gtk_border_free(thumb->img_margin);
282   // we retrieve image margins from css
283   GtkStateFlags state = gtk_widget_get_state_flags(thumb->w_image);
284   thumb->img_margin = gtk_border_new();
285   GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_image);
286   gtk_style_context_get_margin(context, state, thumb->img_margin);
287 
288   // and we apply it to the thumb size
289   int width, height;
290   gtk_widget_get_size_request(thumb->w_main, &width, &height);
291   thumb->img_margin->left = MAX(0, thumb->img_margin->left * width / 1000);
292   thumb->img_margin->top = MAX(0, thumb->img_margin->top * height / 1000);
293   thumb->img_margin->right = MAX(0, thumb->img_margin->right * width / 1000);
294   thumb->img_margin->bottom = MAX(0, thumb->img_margin->bottom * height / 1000);
295 }
296 
_thumb_write_extension(dt_thumbnail_t * thumb)297 static void _thumb_write_extension(dt_thumbnail_t *thumb)
298 {
299   // fill the file extension label
300   const char *ext = thumb->filename + strlen(thumb->filename);
301   while(ext > thumb->filename && *ext != '.') ext--;
302   ext++;
303   gchar *uext = dt_view_extend_modes_str(ext, thumb->is_hdr, thumb->is_bw, thumb->is_bw_flow);
304   gtk_label_set_text(GTK_LABEL(thumb->w_ext), uext);
305   g_free(uext);
306 }
307 
_event_cursor_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)308 static gboolean _event_cursor_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
309 {
310   if(!user_data || !widget) return TRUE;
311   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
312 
313   GtkStateFlags state = gtk_widget_get_state_flags(thumb->w_cursor);
314   GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_cursor);
315   GdkRGBA col;
316   gtk_style_context_get_color(context, state, &col);
317 
318   cairo_set_source_rgba(cr, col.red, col.green, col.blue, col.alpha);
319   cairo_line_to(cr, gtk_widget_get_allocated_width(widget), 0);
320   cairo_line_to(cr, gtk_widget_get_allocated_width(widget) / 2, gtk_widget_get_allocated_height(widget));
321   cairo_line_to(cr, 0, 0);
322   cairo_close_path(cr);
323   cairo_fill(cr);
324 
325   return TRUE;
326 }
327 
328 // zoom_ratio is 0-1 based, where 0 is "img to fit" and 1 "zoom to 100%". returns a thumb->zoom value
_zoom_ratio_to_thumb_zoom(float zoom_ratio,float zoom_100)329 static float _zoom_ratio_to_thumb_zoom(float zoom_ratio, float zoom_100)
330 {
331   return (zoom_100 - 1) * zoom_ratio + 1;
332 }
333 
334 // converts a thumb->zoom value based on it's zoom_100 (max value) to a 0-1 based zoom_ratio.
_thumb_zoom_to_zoom_ratio(float zoom,float zoom_100)335 static float _thumb_zoom_to_zoom_ratio(float zoom, float zoom_100)
336 {
337   return (zoom - 1) / (zoom_100 - 1);
338 }
339 
340 // given max_width & max_height, the width and height is calculated to fit an image in a "img to fit" mode
341 // (everything is visible)
_get_dimensions_for_img_to_fit(dt_thumbnail_t * thumb,int max_width,int max_height,float * width,float * height)342 static void _get_dimensions_for_img_to_fit(dt_thumbnail_t *thumb, int max_width, int max_height, float *width,
343                                            float *height)
344 {
345   float iw = max_width;
346   float ih = max_height;
347 
348   // we can't rely on img->aspect_ratio as the value is round to 1 decimal, so not enough accurate
349   // so we compute it from the larger available mipmap
350   float ar = 0.0f;
351   for(int k = DT_MIPMAP_7; k >= DT_MIPMAP_0; k--)
352   {
353     dt_mipmap_buffer_t tmp;
354     dt_mipmap_cache_get(darktable.mipmap_cache, &tmp, thumb->imgid, k, DT_MIPMAP_TESTLOCK, 'r');
355     if(tmp.buf)
356     {
357       const int mipw = tmp.width;
358       const int miph = tmp.height;
359       dt_mipmap_cache_release(darktable.mipmap_cache, &tmp);
360       if(mipw > 0 && miph > 0)
361       {
362         ar = (float)mipw / miph;
363         break;
364       }
365     }
366   }
367 
368   if(ar < 0.001)
369   {
370     // let's try with the aspect_ratio store in image structure, even if it's less accurate
371     const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->imgid, 'r');
372     if(img)
373     {
374       ar = img->aspect_ratio;
375       dt_image_cache_read_release(darktable.image_cache, img);
376     }
377   }
378 
379   if(ar > 0.001)
380   {
381     // we have a valid ratio, let's apply it
382     if(ar < 1.0)
383       iw = ih * ar;
384     else
385       ih = iw / ar;
386     // rescale to ensure it stay in thumbnails bounds
387     const float scale = fminf(1.0, fminf((float)max_width / iw, (float)max_height / ih));
388     iw *= scale;
389     ih *= scale;
390   }
391 
392   *width = iw;
393   *height = ih;
394 }
395 
396 // retrieves image zoom100 and final_width/final_height to calculate the dimensions of the zoomed image.
_get_dimensions_for_zoomed_img(dt_thumbnail_t * thumb,int max_width,int max_height,float zoom_ratio,float * width,float * height)397 static void _get_dimensions_for_zoomed_img(dt_thumbnail_t *thumb, int max_width, int max_height, float zoom_ratio,
398                                            float *width, float *height)
399 {
400   float iw = max_width;
401   float ih = max_height;
402   // we need to get proper dimensions for the image to determine the image_w size.
403   // calling dt_thumbnail_get_zoom100 is used to get the max zoom, but also to ensure that final_width and
404   // height are available.
405   const float zoom_100 = dt_thumbnail_get_zoom100(thumb);
406   const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->imgid, 'r');
407   if(img)
408   {
409     if(img->final_width > 0 && img->final_height > 0)
410     {
411       iw = img->final_width;
412       ih = img->final_height;
413     }
414     dt_image_cache_read_release(darktable.image_cache, img);
415   }
416 
417   // scale first to "img to fit", then apply the zoom ratio to get the resulting final (zoomed) image
418   // dimensions, while making sure to still fit in the imagebox.
419   const float scale_to_fit = fminf((float)max_width / iw, (float)max_height / ih);
420   thumb->zoom = _zoom_ratio_to_thumb_zoom(zoom_ratio, zoom_100);
421   *width = MIN(iw * scale_to_fit * thumb->zoom, max_width);
422   *height = MIN(ih * scale_to_fit * thumb->zoom, max_height);
423 }
424 
_thumb_set_image_area(dt_thumbnail_t * thumb,float zoom_ratio)425 static void _thumb_set_image_area(dt_thumbnail_t *thumb, float zoom_ratio)
426 {
427   // let's ensure we have the right margins
428   _thumb_retrieve_margins(thumb);
429 
430   int image_w, image_h;
431   int posy = 0;
432   if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL || thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED)
433   {
434     image_w = thumb->width - thumb->img_margin->left - thumb->img_margin->right;
435     int w = 0;
436     int h = 0;
437     gtk_widget_get_size_request(thumb->w_bottom_eb, &w, &h);
438     image_h = thumb->height - MAX(0, h);
439     gtk_widget_get_size_request(thumb->w_altered, &w, &h);
440     if(!thumb->zoomable)
441     {
442       posy = h + gtk_widget_get_margin_top(thumb->w_altered);
443       image_h -= posy;
444     }
445     else
446       image_h -= thumb->img_margin->bottom;
447     image_h -= thumb->img_margin->top;
448     posy += thumb->img_margin->top;
449   }
450   else if(thumb->over == DT_THUMBNAIL_OVERLAYS_MIXED)
451   {
452     image_w = thumb->width - thumb->img_margin->left - thumb->img_margin->right;
453     int w = 0;
454     int h = 0;
455     gtk_widget_get_size_request(thumb->w_reject, &w, &h);
456     image_h = thumb->height - (h + gtk_widget_get_margin_bottom(thumb->w_reject));
457     gtk_widget_get_size_request(thumb->w_altered, &w, &h);
458     posy = h + gtk_widget_get_margin_top(thumb->w_altered);
459     image_h -= posy;
460     image_h -= thumb->img_margin->top + thumb->img_margin->bottom;
461     posy += thumb->img_margin->top;
462   }
463   else
464   {
465     image_w = thumb->width - thumb->img_margin->left - thumb->img_margin->right;
466     image_h = thumb->height - thumb->img_margin->top - thumb->img_margin->bottom;
467     posy = thumb->img_margin->top;
468   }
469 
470   // we check that the image drawing area is not greater than the box
471   int wi = 0;
472   int hi = 0;
473   gtk_widget_get_size_request(thumb->w_image, &wi, &hi);
474   if(wi <= 0 || hi <= 0)
475   {
476     // we arrive here if we are inside the creation process
477     float iw = image_w;
478     float ih = image_h;
479 
480     if(zoom_ratio == IMG_TO_FIT)
481       _get_dimensions_for_img_to_fit(thumb, image_w, image_h, &iw, &ih);
482     else
483       _get_dimensions_for_zoomed_img(thumb, image_w, image_h, zoom_ratio, &iw, &ih);
484 
485     gtk_widget_set_size_request(thumb->w_image, iw, ih);
486   }
487   else
488   {
489     const float scale = fminf((float)image_w / wi, (float)image_h / hi);
490     if(scale < 1.0f) gtk_widget_set_size_request(thumb->w_image, wi * scale, hi * scale);
491   }
492 
493   // and we set the size and margins of the imagebox
494   gtk_widget_set_size_request(thumb->w_image_box, image_w, image_h);
495   gtk_widget_set_margin_start(thumb->w_image_box, thumb->img_margin->left);
496   gtk_widget_set_margin_top(thumb->w_image_box, posy);
497 }
498 
_event_image_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)499 static gboolean _event_image_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
500 {
501   if(!user_data) return TRUE;
502   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
503   if(thumb->imgid <= 0)
504   {
505     dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_LIGHTTABLE_BG);
506     cairo_paint(cr);
507     return TRUE;
508   }
509 
510   // if we have a rgbbuf but the thumb is not anymore the darkroom main one
511   dt_develop_t *dev = darktable.develop;
512   const dt_view_t *v = dt_view_manager_get_current_view(darktable.view_manager);
513   if(thumb->img_surf_preview
514      && (v->view(v) != DT_VIEW_DARKROOM || !dev->preview_pipe->output_backbuf
515          || dev->preview_pipe->output_imgid != thumb->imgid))
516   {
517     if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) > 0)
518       cairo_surface_destroy(thumb->img_surf);
519     thumb->img_surf = NULL;
520     thumb->img_surf_dirty = TRUE;
521   }
522 
523   // if image surface has no more ref. let's samitize it's value to NULL
524   if(thumb->img_surf
525      && cairo_surface_get_reference_count(thumb->img_surf) < 1)
526     thumb->img_surf = NULL;
527 
528   // if we don't have it in memory, we want the image surface
529   dt_view_surface_value_t res = DT_VIEW_SURFACE_OK;
530   if(!thumb->img_surf || thumb->img_surf_dirty)
531   {
532     int image_w = 0;
533     int image_h = 0;
534     _thumb_set_image_area(thumb, IMG_TO_FIT);
535     gtk_widget_get_size_request(thumb->w_image_box, &image_w, &image_h);
536 
537     if(v->view(v) == DT_VIEW_DARKROOM
538        && dev->preview_pipe->output_imgid == thumb->imgid
539        && dev->preview_pipe->output_backbuf)
540     {
541       // the current thumb is the one currently developed in darkroom
542       // better use the preview buffer for surface, in order to stay in sync
543       if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) > 0)
544         cairo_surface_destroy(thumb->img_surf);
545       thumb->img_surf = NULL;
546 
547       // get new surface with preview image
548       const int buf_width = dev->preview_pipe->output_backbuf_width;
549       const int buf_height = dev->preview_pipe->output_backbuf_height;
550       uint8_t *rgbbuf = g_malloc0(sizeof(unsigned char) * 4 * buf_width * buf_height);
551 
552       dt_pthread_mutex_t *mutex = &dev->preview_pipe->backbuf_mutex;
553       dt_pthread_mutex_lock(mutex);
554       memcpy(rgbbuf, dev->preview_pipe->output_backbuf, sizeof(unsigned char) * 4 * buf_width * buf_height);
555       dt_pthread_mutex_unlock(mutex);
556 
557       const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, buf_width);
558       cairo_surface_t *tmp_surface
559           = cairo_image_surface_create_for_data(rgbbuf, CAIRO_FORMAT_RGB24, buf_width, buf_height, stride);
560 
561       // copy preview image into final surface
562       if(tmp_surface)
563       {
564         float scale = fminf(image_w / (float)buf_width, image_h / (float)buf_height) * darktable.gui->ppd_thb;
565         const int img_width = roundf(buf_width * scale);
566         const int img_height = roundf(buf_height * scale);
567         scale = fmaxf(img_width / (float)buf_width, img_height / (float)buf_height);
568         thumb->img_surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, img_width, img_height);
569         cairo_t *cr2 = cairo_create(thumb->img_surf);
570         cairo_scale(cr2, scale, scale);
571 
572         cairo_set_source_surface(cr2, tmp_surface, 0, 0);
573         // set filter no nearest:
574         // in skull mode, we want to see big pixels.
575         // in 1 iir mode for the right mip, we want to see exactly what the pipe gave us, 1:1 pixel for pixel.
576         // in between, filtering just makes stuff go unsharp.
577         if((buf_width <= 8 && buf_height <= 8) || fabsf(scale - 1.0f) < 0.01f)
578           cairo_pattern_set_filter(cairo_get_source(cr2), CAIRO_FILTER_NEAREST);
579         else
580           cairo_pattern_set_filter(cairo_get_source(cr2), darktable.gui->filter_image);
581 
582         cairo_paint(cr2);
583 
584         if(darktable.gui->show_focus_peaking)
585         {
586           cairo_save(cr2);
587           cairo_scale(cr2, 1.0f/scale, 1.0f/scale);
588           dt_focuspeaking(cr2, img_width, img_height, cairo_image_surface_get_data(thumb->img_surf),
589                           cairo_image_surface_get_width(thumb->img_surf),
590                           cairo_image_surface_get_height(thumb->img_surf));
591           cairo_restore(cr2);
592         }
593 
594         cairo_surface_destroy(tmp_surface);
595         cairo_destroy(cr2);
596       }
597       if(rgbbuf) g_free(rgbbuf);
598     }
599     else
600     {
601       cairo_surface_t *img_surf = NULL;
602       if(thumb->zoomable)
603       {
604         if(thumb->zoom > 1.0f)
605           thumb->zoom = MIN(thumb->zoom, dt_thumbnail_get_zoom100(thumb));
606         res = dt_view_image_get_surface(thumb->imgid, image_w * thumb->zoom, image_h * thumb->zoom, &img_surf, FALSE);
607       }
608       else
609       {
610         res = dt_view_image_get_surface(thumb->imgid, image_w, image_h, &img_surf, FALSE);
611       }
612 
613       if(res == DT_VIEW_SURFACE_OK || res == DT_VIEW_SURFACE_SMALLER)
614       {
615         // if we succeed to get an image (even a smaller one)
616         cairo_surface_t *tmp_surf = thumb->img_surf;
617         thumb->img_surf = img_surf;
618         if(tmp_surf && cairo_surface_get_reference_count(tmp_surf) > 0)
619           cairo_surface_destroy(tmp_surf);
620       }
621     }
622 
623     if(thumb->img_surf)
624     {
625       thumb->img_width = cairo_image_surface_get_width(thumb->img_surf);
626       thumb->img_height = cairo_image_surface_get_height(thumb->img_surf);
627       // and we want to resize the imagebox to fit in the imagearea
628       const int imgbox_w = MIN(image_w, thumb->img_width / darktable.gui->ppd_thb);
629       const int imgbox_h = MIN(image_h, thumb->img_height / darktable.gui->ppd_thb);
630       // we record the imagebox size before the change
631       int hh = 0;
632       int ww = 0;
633       gtk_widget_get_size_request(thumb->w_image, &ww, &hh);
634       // and we set the new size of the imagebox
635       _thumb_set_image_size(thumb, imgbox_w, imgbox_h);
636       // the imagebox size may have been slightly sanitized, so we get it again
637       int nhi = 0;
638       int nwi = 0;
639       gtk_widget_get_size_request(thumb->w_image, &nwi, &nhi);
640 
641       // panning value need to be adjusted if the imagebox size as changed
642       thumb->zoomx = thumb->zoomx + (nwi - ww) / 2.0;
643       thumb->zoomy = thumb->zoomy + (nhi - hh) / 2.0;
644       // let's sanitize and apply panning values as we are sure the zoomed image is loaded now
645       // here we have to make sure to properly align according to ppd
646       thumb->zoomx
647           = CLAMP(thumb->zoomx, (nwi * darktable.gui->ppd_thb - thumb->img_width) / darktable.gui->ppd_thb, 0);
648       thumb->zoomy
649           = CLAMP(thumb->zoomy, (nhi * darktable.gui->ppd_thb - thumb->img_height) / darktable.gui->ppd_thb, 0);
650 
651       // for overlay block, we need to resize it
652       if(thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
653         _thumb_resize_overlays(thumb);
654     }
655 
656     // if we don't have the right size of the image now, we reload it again
657     if(res != DT_VIEW_SURFACE_OK)
658     {
659       thumb->busy = TRUE;
660       if(!thumb->expose_again_timeout_id)
661         thumb->expose_again_timeout_id = g_timeout_add(250, _thumb_expose_again, thumb);
662     }
663 
664     // if needed we compute and draw here the big rectangle to show focused areas
665     if(res == DT_VIEW_SURFACE_OK && thumb->display_focus)
666     {
667       uint8_t *full_res_thumb = NULL;
668       int32_t full_res_thumb_wd, full_res_thumb_ht;
669       dt_colorspaces_color_profile_type_t color_space;
670       char path[PATH_MAX] = { 0 };
671       gboolean from_cache = TRUE;
672       dt_image_full_path(thumb->imgid, path, sizeof(path), &from_cache);
673       if(!dt_imageio_large_thumbnail(path, &full_res_thumb, &full_res_thumb_wd, &full_res_thumb_ht, &color_space))
674       {
675         // we look for focus areas
676         dt_focus_cluster_t full_res_focus[49];
677         const int frows = 5, fcols = 5;
678         dt_focus_create_clusters(full_res_focus, frows, fcols, full_res_thumb, full_res_thumb_wd,
679                                  full_res_thumb_ht);
680         // and we draw them on the image
681         cairo_t *cri = cairo_create(thumb->img_surf);
682         dt_focus_draw_clusters(cri, cairo_image_surface_get_width(thumb->img_surf),
683                                cairo_image_surface_get_height(thumb->img_surf), thumb->imgid, full_res_thumb_wd,
684                                full_res_thumb_ht, full_res_focus, frows, fcols, 1.0, 0, 0);
685         cairo_destroy(cri);
686       }
687       dt_free_align(full_res_thumb);
688     }
689 
690 
691     // here we are sure to have the right imagesurface
692     if(res == DT_VIEW_SURFACE_OK)
693     {
694       thumb->img_surf_dirty = FALSE;
695       thumb->busy = FALSE;
696     }
697 
698     // and we can also set the zooming level if needed
699     if(res == DT_VIEW_SURFACE_OK
700        && thumb->zoomable
701        && thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
702     {
703       if(thumb->zoom_100 < 1.0 || thumb->zoom <= 1.0f)
704       {
705         gtk_label_set_text(GTK_LABEL(thumb->w_zoom), _("fit"));
706       }
707       else
708       {
709         gchar *z = g_strdup_printf("%.0f%%", thumb->zoom * 100.0 / thumb->zoom_100);
710         gtk_label_set_text(GTK_LABEL(thumb->w_zoom), z);
711         g_free(z);
712       }
713     }
714   }
715 
716   _thumb_draw_image(thumb, cr);
717 
718   return TRUE;
719 }
720 
_thumb_update_icons(dt_thumbnail_t * thumb)721 static void _thumb_update_icons(dt_thumbnail_t *thumb)
722 {
723   gtk_widget_set_visible(thumb->w_local_copy, thumb->has_localcopy);
724   gtk_widget_set_visible(thumb->w_altered, thumb->is_altered);
725   gtk_widget_set_visible(thumb->w_group, thumb->is_grouped);
726   gtk_widget_set_visible(thumb->w_audio, thumb->has_audio);
727   gtk_widget_set_visible(thumb->w_color, thumb->colorlabels != 0);
728   gtk_widget_set_visible(thumb->w_zoom_eb, (thumb->zoomable && thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK));
729   gtk_widget_show(thumb->w_bottom_eb);
730   gtk_widget_show(thumb->w_reject);
731   gtk_widget_show(thumb->w_ext);
732   gtk_widget_show(thumb->w_cursor);
733   for(int i = 0; i < MAX_STARS; i++) gtk_widget_show(thumb->w_stars[i]);
734 
735   _set_flag(thumb->w_main, GTK_STATE_FLAG_PRELIGHT, thumb->mouse_over);
736   _set_flag(thumb->w_main, GTK_STATE_FLAG_ACTIVE, thumb->active);
737 
738   _set_flag(thumb->w_reject, GTK_STATE_FLAG_ACTIVE, (thumb->rating == DT_VIEW_REJECT));
739   for(int i = 0; i < MAX_STARS; i++)
740     _set_flag(thumb->w_stars[i], GTK_STATE_FLAG_ACTIVE, (thumb->rating > i && thumb->rating < DT_VIEW_REJECT));
741   _set_flag(thumb->w_group, GTK_STATE_FLAG_ACTIVE, (thumb->imgid == thumb->groupid));
742 
743   _set_flag(thumb->w_main, GTK_STATE_FLAG_SELECTED, thumb->selected);
744 
745   // and the tooltip
746   gchar *pattern = dt_conf_get_string("plugins/lighttable/thumbnail_tooltip_pattern");
747   if(!thumb->tooltip || strcmp(pattern, "") == 0)
748   {
749     gtk_widget_set_has_tooltip(thumb->w_main, FALSE);
750   }
751   else
752   {
753     // we compute the tooltip (we reuse the function used in export to disk)
754     char input_dir[1024] = { 0 };
755     gboolean from_cache = TRUE;
756     dt_image_full_path(thumb->imgid, input_dir, sizeof(input_dir), &from_cache);
757 
758     dt_variables_params_t *vp;
759     dt_variables_params_init(&vp);
760 
761     vp->filename = input_dir;
762     vp->jobcode = "infos";
763     vp->imgid = thumb->imgid;
764     vp->sequence = 0;
765     vp->escape_markup = TRUE;
766 
767     gchar *msg = dt_variables_expand(vp, pattern, TRUE);
768 
769     dt_variables_params_destroy(vp);
770 
771     // we change the label
772     gtk_widget_set_tooltip_markup(thumb->w_main, msg);
773 
774     g_free(msg);
775   }
776   g_free(pattern);
777 
778   // we recompte the history tooltip if needed
779   thumb->is_altered = dt_image_altered(thumb->imgid);
780   gtk_widget_set_visible(thumb->w_altered, thumb->is_altered);
781   if(thumb->is_altered)
782   {
783     char *tooltip = dt_history_get_items_as_string(thumb->imgid);
784     if(tooltip)
785     {
786       gtk_widget_set_tooltip_text(thumb->w_altered, tooltip);
787       g_free(tooltip);
788     }
789   }
790 }
791 
_thumbs_hide_overlays(gpointer user_data)792 static gboolean _thumbs_hide_overlays(gpointer user_data)
793 {
794   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
795   thumb->overlay_timeout_id = 0;
796   // if the mouse is inside the infos block, we don't hide them
797   if(gtk_widget_get_state_flags(thumb->w_bottom_eb) & GTK_STATE_FLAG_PRELIGHT) return FALSE;
798 
799   gtk_widget_hide(thumb->w_bottom_eb);
800   gtk_widget_hide(thumb->w_reject);
801   for(int i = 0; i < MAX_STARS; i++) gtk_widget_hide(thumb->w_stars[i]);
802   gtk_widget_hide(thumb->w_color);
803   gtk_widget_hide(thumb->w_local_copy);
804   gtk_widget_hide(thumb->w_altered);
805   gtk_widget_hide(thumb->w_group);
806   gtk_widget_hide(thumb->w_audio);
807   gtk_widget_hide(thumb->w_zoom_eb);
808   gtk_widget_hide(thumb->w_ext);
809   return G_SOURCE_REMOVE;
810 }
_thumbs_show_overlays(gpointer user_data)811 static gboolean _thumbs_show_overlays(gpointer user_data)
812 {
813   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
814   _thumb_update_icons(thumb);
815   return G_SOURCE_REMOVE;
816 }
817 
_event_main_motion(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)818 static gboolean _event_main_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
819 {
820   if(!user_data) return TRUE;
821   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
822 
823   // first, we hide the block overlays after a delay if the mouse hasn't move
824   if(thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
825   {
826     if(thumb->overlay_timeout_id > 0)
827     {
828       g_source_remove(thumb->overlay_timeout_id);
829       thumb->overlay_timeout_id = 0;
830     }
831     _thumbs_show_overlays(thumb);
832     if(thumb->overlay_timeout_duration >= 0)
833     {
834       thumb->overlay_timeout_id
835           = g_timeout_add_seconds(thumb->overlay_timeout_duration, _thumbs_hide_overlays, thumb);
836     }
837   }
838 
839   if(!thumb->mouse_over && !thumb->disable_mouseover) dt_control_set_mouse_over_id(thumb->imgid);
840   return FALSE;
841 }
842 
_event_main_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)843 static gboolean _event_main_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
844 {
845   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
846   if(event->button == 1
847      && ((event->type == GDK_2BUTTON_PRESS && !thumb->single_click)
848          || (event->type == GDK_BUTTON_PRESS && dt_modifier_is(event->state, 0) && thumb->single_click)))
849   {
850     dt_control_set_mouse_over_id(thumb->imgid); // to ensure we haven't lost imgid during double-click
851   }
852   return FALSE;
853 }
_event_main_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)854 static gboolean _event_main_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
855 {
856   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
857 
858   if(event->button == 1 && !thumb->moved && thumb->sel_mode != DT_THUMBNAIL_SEL_MODE_DISABLED)
859   {
860     if(dt_modifier_is(event->state, 0) && thumb->sel_mode != DT_THUMBNAIL_SEL_MODE_MOD_ONLY)
861       dt_selection_select_single(darktable.selection, thumb->imgid);
862     else if(dt_modifier_is(event->state, GDK_MOD1_MASK))
863       dt_selection_select_single(darktable.selection, thumb->imgid);
864     else if(dt_modifier_is(event->state, GDK_CONTROL_MASK))
865       dt_selection_toggle(darktable.selection, thumb->imgid);
866     else if(dt_modifier_is(event->state, GDK_SHIFT_MASK))
867       dt_selection_select_range(darktable.selection, thumb->imgid);
868   }
869   return FALSE;
870 }
871 
_event_rating_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)872 static gboolean _event_rating_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
873 {
874   return TRUE;
875 }
_event_rating_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)876 static gboolean _event_rating_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
877 {
878   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
879   if(thumb->disable_actions) return FALSE;
880   if(dtgtk_thumbnail_btn_is_hidden(widget)) return FALSE;
881 
882   if(event->button == 1 && !thumb->moved)
883   {
884     dt_view_image_over_t rating = DT_VIEW_DESERT;
885     if(widget == thumb->w_reject)
886       rating = DT_VIEW_REJECT;
887     else if(widget == thumb->w_stars[0])
888       rating = DT_VIEW_STAR_1;
889     else if(widget == thumb->w_stars[1])
890       rating = DT_VIEW_STAR_2;
891     else if(widget == thumb->w_stars[2])
892       rating = DT_VIEW_STAR_3;
893     else if(widget == thumb->w_stars[3])
894       rating = DT_VIEW_STAR_4;
895     else if(widget == thumb->w_stars[4])
896       rating = DT_VIEW_STAR_5;
897 
898     if(rating != DT_VIEW_DESERT)
899     {
900       dt_ratings_apply_on_image(thumb->imgid, rating, TRUE, TRUE, TRUE);
901       dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_RATING,
902                                  g_list_prepend(NULL, GINT_TO_POINTER(thumb->imgid)));
903     }
904   }
905   return TRUE;
906 }
907 
_event_grouping_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)908 static gboolean _event_grouping_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
909 {
910   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
911   if(thumb->disable_actions) return FALSE;
912   if(dtgtk_thumbnail_btn_is_hidden(widget)) return FALSE;
913 
914   if(event->button == 1 && !thumb->moved)
915   {
916     //TODO: will succeed if either or *both* of Shift and Control are pressed.  Do we want this?
917     if(dt_modifier_is(event->state, GDK_SHIFT_MASK) | dt_modifier_is(event->state, GDK_CONTROL_MASK))
918     {
919       // just add the whole group to the selection. TODO: make this also work for collapsed groups.
920       sqlite3_stmt *stmt;
921       DT_DEBUG_SQLITE3_PREPARE_V2(
922           dt_database_get(darktable.db),
923           "INSERT OR IGNORE INTO main.selected_images SELECT id FROM main.images WHERE group_id = ?1", -1, &stmt,
924           NULL);
925       DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, thumb->groupid);
926       sqlite3_step(stmt);
927       sqlite3_finalize(stmt);
928     }
929     else if(!darktable.gui->grouping
930             || thumb->groupid == darktable.gui->expanded_group_id) // the group is already expanded, so ...
931     {
932       if(thumb->imgid == darktable.gui->expanded_group_id && darktable.gui->grouping) // ... collapse it
933         darktable.gui->expanded_group_id = -1;
934       else // ... make the image the new representative of the group
935         darktable.gui->expanded_group_id = dt_grouping_change_representative(thumb->imgid);
936     }
937     else // expand the group
938       darktable.gui->expanded_group_id = thumb->groupid;
939     dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_GROUPING,
940                                NULL);
941   }
942   return FALSE;
943 }
944 
_event_audio_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)945 static gboolean _event_audio_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
946 {
947   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
948   if(thumb->disable_actions) return FALSE;
949   if(dtgtk_thumbnail_btn_is_hidden(widget)) return FALSE;
950 
951   if(event->button == 1 && !thumb->moved)
952   {
953     gboolean start_audio = TRUE;
954     if(darktable.view_manager->audio.audio_player_id != -1)
955     {
956       // don't start the audio for the image we just killed it for
957       if(darktable.view_manager->audio.audio_player_id == thumb->imgid) start_audio = FALSE;
958       dt_view_audio_stop(darktable.view_manager);
959     }
960 
961     if(start_audio)
962     {
963       dt_view_audio_start(darktable.view_manager, thumb->imgid);
964     }
965   }
966   return FALSE;
967 }
968 
969 // this is called each time the images info change
_dt_image_info_changed_callback(gpointer instance,gpointer imgs,gpointer user_data)970 static void _dt_image_info_changed_callback(gpointer instance, gpointer imgs, gpointer user_data)
971 {
972   if(!user_data || !imgs) return;
973   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
974   for(const GList *i = imgs; i; i = g_list_next(i))
975   {
976     if(GPOINTER_TO_INT(i->data) == thumb->imgid)
977     {
978       dt_thumbnail_update_infos(thumb);
979       break;
980     }
981   }
982 }
983 
984 // this is called each time collected images change
985 // we only use this because the image infos may have changed
_dt_collection_changed_callback(gpointer instance,dt_collection_change_t query_change,dt_collection_properties_t changed_property,gpointer imgs,const int next,gpointer user_data)986 static void _dt_collection_changed_callback(gpointer instance, dt_collection_change_t query_change,
987                                             dt_collection_properties_t changed_property, gpointer imgs,
988                                             const int next, gpointer user_data)
989 {
990   if(!user_data || !imgs) return;
991   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
992   for(const GList *i = imgs; i; i = g_list_next(i))
993   {
994     if(GPOINTER_TO_INT(i->data) == thumb->imgid)
995     {
996       dt_thumbnail_update_infos(thumb);
997       break;
998     }
999   }
1000 }
1001 
dt_thumbnail_update_selection(dt_thumbnail_t * thumb)1002 void dt_thumbnail_update_selection(dt_thumbnail_t *thumb)
1003 {
1004   if(!thumb) return;
1005   if(!gtk_widget_is_visible(thumb->w_main)) return;
1006 
1007   gboolean selected = FALSE;
1008   /* clear and reset statements */
1009   DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
1010   DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);
1011   /* bind imgid to prepared statements */
1012   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, thumb->imgid);
1013   /* lets check if imgid is selected */
1014   if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW) selected = TRUE;
1015 
1016   // if there's a change, update the thumb
1017   if(selected != thumb->selected)
1018   {
1019     thumb->selected = selected;
1020     _thumb_update_icons(thumb);
1021     gtk_widget_queue_draw(thumb->w_main);
1022   }
1023 }
1024 
_dt_selection_changed_callback(gpointer instance,gpointer user_data)1025 static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
1026 {
1027   if(!user_data) return;
1028   dt_thumbnail_update_selection((dt_thumbnail_t *)user_data);
1029 }
1030 
_dt_active_images_callback(gpointer instance,gpointer user_data)1031 static void _dt_active_images_callback(gpointer instance, gpointer user_data)
1032 {
1033   if(!user_data) return;
1034   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1035   if(!thumb) return;
1036 
1037   gboolean active = FALSE;
1038   for(GSList *l = darktable.view_manager->active_images; l; l = g_slist_next(l))
1039   {
1040     int id = GPOINTER_TO_INT(l->data);
1041     if(id == thumb->imgid)
1042     {
1043       active = TRUE;
1044       break;
1045     }
1046   }
1047 
1048   // if there's a change, update the thumb
1049   if(active != thumb->active)
1050   {
1051     thumb->active = active;
1052     if(gtk_widget_is_visible(thumb->w_main))
1053     {
1054       _thumb_update_icons(thumb);
1055       gtk_widget_queue_draw(thumb->w_main);
1056     }
1057   }
1058 }
1059 
_dt_preview_updated_callback(gpointer instance,gpointer user_data)1060 static void _dt_preview_updated_callback(gpointer instance, gpointer user_data)
1061 {
1062   if(!user_data) return;
1063   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1064   if(!thumb) return;
1065   if(!gtk_widget_is_visible(thumb->w_main)) return;
1066 
1067   const dt_view_t *v = dt_view_manager_get_current_view(darktable.view_manager);
1068   if(v->view(v) == DT_VIEW_DARKROOM && darktable.develop->preview_pipe->output_imgid == thumb->imgid
1069      && darktable.develop->preview_pipe->output_backbuf)
1070   {
1071     // reset surface
1072     thumb->img_surf_dirty = TRUE;
1073     gtk_widget_queue_draw(thumb->w_main);
1074   }
1075 }
1076 
_dt_mipmaps_updated_callback(gpointer instance,int imgid,gpointer user_data)1077 static void _dt_mipmaps_updated_callback(gpointer instance, int imgid, gpointer user_data)
1078 {
1079   if(!user_data) return;
1080   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1081   if(!thumb || (imgid > 0 && thumb->imgid != imgid)) return;
1082 
1083   // we recompte the history tooltip if needed
1084   thumb->is_altered = dt_image_altered(thumb->imgid);
1085   gtk_widget_set_visible(thumb->w_altered, thumb->is_altered);
1086   if(thumb->is_altered)
1087   {
1088     char *tooltip = dt_history_get_items_as_string(thumb->imgid);
1089     if(tooltip)
1090     {
1091       gtk_widget_set_tooltip_text(thumb->w_altered, tooltip);
1092       g_free(tooltip);
1093     }
1094   }
1095 
1096   // reset surface
1097   thumb->img_surf_dirty = TRUE;
1098   gtk_widget_queue_draw(thumb->w_main);
1099 }
1100 
_event_box_enter_leave(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1101 static gboolean _event_box_enter_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1102 {
1103   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1104   // if we leave for ancestor, that means we leave for blank thumbtable area
1105   if(event->type == GDK_LEAVE_NOTIFY && event->detail == GDK_NOTIFY_ANCESTOR) dt_control_set_mouse_over_id(-1);
1106 
1107   if(!thumb->mouse_over && event->type == GDK_ENTER_NOTIFY && !thumb->disable_mouseover)
1108     dt_control_set_mouse_over_id(thumb->imgid);
1109   _set_flag(widget, GTK_STATE_FLAG_PRELIGHT, (event->type == GDK_ENTER_NOTIFY));
1110   _set_flag(thumb->w_image_box, GTK_STATE_FLAG_PRELIGHT, (event->type == GDK_ENTER_NOTIFY));
1111   return FALSE;
1112 }
1113 
_event_image_enter_leave(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1114 static gboolean _event_image_enter_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1115 {
1116   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1117   _set_flag(thumb->w_image_box, GTK_STATE_FLAG_PRELIGHT, (event->type == GDK_ENTER_NOTIFY));
1118   return FALSE;
1119 }
1120 
_event_btn_enter_leave(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1121 static gboolean _event_btn_enter_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1122 {
1123   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1124 
1125   if(event->type == GDK_ENTER_NOTIFY && widget == thumb->w_reject) darktable.control->element = DT_VIEW_REJECT;
1126 
1127   // if we leave for ancestor, that means we leave for blank thumbtable area
1128   if(event->type == GDK_LEAVE_NOTIFY && event->detail == GDK_NOTIFY_ANCESTOR) dt_control_set_mouse_over_id(-1);
1129 
1130   if(thumb->disable_actions) return TRUE;
1131   if(event->type == GDK_ENTER_NOTIFY) _set_flag(thumb->w_image_box, GTK_STATE_FLAG_PRELIGHT, TRUE);
1132   return FALSE;
1133 }
1134 
_event_star_enter(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1135 static gboolean _event_star_enter(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1136 {
1137   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1138   if(thumb->disable_actions) return TRUE;
1139   if(!thumb->mouse_over && !thumb->disable_mouseover) dt_control_set_mouse_over_id(thumb->imgid);
1140   _set_flag(thumb->w_bottom_eb, GTK_STATE_FLAG_PRELIGHT, TRUE);
1141   _set_flag(thumb->w_image_box, GTK_STATE_FLAG_PRELIGHT, TRUE);
1142 
1143   // we prelight all stars before the current one
1144   gboolean pre = TRUE;
1145   for(int i = 0; i < MAX_STARS; i++)
1146   {
1147     _set_flag(thumb->w_stars[i], GTK_STATE_FLAG_PRELIGHT, pre);
1148     gtk_widget_queue_draw(thumb->w_stars[i]);
1149     if(thumb->w_stars[i] == widget)
1150     {
1151       darktable.control->element = i + 1;
1152       pre = FALSE;
1153     }
1154   }
1155   return TRUE;
1156 }
_event_star_leave(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1157 static gboolean _event_star_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1158 {
1159   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
1160   // if we leave for ancestor, that means we leave for blank thumbtable area
1161   if(event->type == GDK_LEAVE_NOTIFY && event->detail == GDK_NOTIFY_ANCESTOR) dt_control_set_mouse_over_id(-1);
1162 
1163   if(thumb->disable_actions) return TRUE;
1164   for(int i = 0; i < MAX_STARS; i++)
1165   {
1166     _set_flag(thumb->w_stars[i], GTK_STATE_FLAG_PRELIGHT, FALSE);
1167     gtk_widget_queue_draw(thumb->w_stars[i]);
1168   }
1169   return TRUE;
1170 }
1171 
_event_main_leave(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)1172 static gboolean _event_main_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1173 {
1174   // if we leave for ancestor, that means we leave for blank thumbtable area
1175   if(event->detail == GDK_NOTIFY_ANCESTOR) dt_control_set_mouse_over_id(-1);
1176   return FALSE;
1177 }
1178 
1179 // we only want to specify that the mouse is hovereing the thumbnail
_event_main_drag_motion(GtkWidget * widget,GdkDragContext * dc,gint x,gint y,guint time,gpointer user_data)1180 static gboolean _event_main_drag_motion(GtkWidget *widget, GdkDragContext *dc, gint x, gint y, guint time,
1181                                         gpointer user_data)
1182 {
1183   _event_main_motion(widget, NULL, user_data);
1184   return TRUE;
1185 }
1186 
dt_thumbnail_create_widget(dt_thumbnail_t * thumb,float zoom_ratio)1187 GtkWidget *dt_thumbnail_create_widget(dt_thumbnail_t *thumb, float zoom_ratio)
1188 {
1189   // main widget (overlay)
1190   thumb->w_main = gtk_overlay_new();
1191   gtk_widget_set_name(thumb->w_main, "thumb_main");
1192   _thumb_update_rating_class(thumb);
1193   gtk_widget_set_size_request(thumb->w_main, thumb->width, thumb->height);
1194 
1195   if(thumb->imgid > 0)
1196   {
1197     // this is only here to ensure that mouse-over value is updated correctly
1198     // all dragging actions take place inside thumbatble.c
1199     gtk_drag_dest_set(thumb->w_main, GTK_DEST_DEFAULT_MOTION, target_list_all, n_targets_all, GDK_ACTION_MOVE);
1200     g_signal_connect(G_OBJECT(thumb->w_main), "drag-motion", G_CALLBACK(_event_main_drag_motion), thumb);
1201 
1202     g_signal_connect(G_OBJECT(thumb->w_main), "button-press-event", G_CALLBACK(_event_main_press), thumb);
1203     g_signal_connect(G_OBJECT(thumb->w_main), "button-release-event", G_CALLBACK(_event_main_release), thumb);
1204 
1205     g_object_set_data(G_OBJECT(thumb->w_main), "thumb", thumb);
1206     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE,
1207                               G_CALLBACK(_dt_active_images_callback), thumb);
1208     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_SELECTION_CHANGED,
1209                               G_CALLBACK(_dt_selection_changed_callback), thumb);
1210     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_MIPMAP_UPDATED,
1211                               G_CALLBACK(_dt_mipmaps_updated_callback), thumb);
1212     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED,
1213                               G_CALLBACK(_dt_preview_updated_callback), thumb);
1214     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_IMAGE_INFO_CHANGED,
1215                               G_CALLBACK(_dt_image_info_changed_callback), thumb);
1216     DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_COLLECTION_CHANGED,
1217                               G_CALLBACK(_dt_collection_changed_callback), thumb);
1218 
1219     // the background
1220     thumb->w_back = gtk_event_box_new();
1221     gtk_widget_set_events(thumb->w_back, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK
1222                                              | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1223                                              | GDK_POINTER_MOTION_MASK);
1224     gtk_widget_set_name(thumb->w_back, "thumb_back");
1225     g_signal_connect(G_OBJECT(thumb->w_back), "motion-notify-event", G_CALLBACK(_event_main_motion), thumb);
1226     g_signal_connect(G_OBJECT(thumb->w_back), "leave-notify-event", G_CALLBACK(_event_main_leave), thumb);
1227     gtk_widget_show(thumb->w_back);
1228     gtk_container_add(GTK_CONTAINER(thumb->w_main), thumb->w_back);
1229 
1230     // the file extension label
1231     thumb->w_ext = gtk_label_new("");
1232     gtk_widget_set_name(thumb->w_ext, "thumb_ext");
1233     gtk_widget_set_valign(thumb->w_ext, GTK_ALIGN_START);
1234     gtk_widget_set_halign(thumb->w_ext, GTK_ALIGN_START);
1235     gtk_label_set_justify(GTK_LABEL(thumb->w_ext), GTK_JUSTIFY_CENTER);
1236     gtk_widget_show(thumb->w_ext);
1237     gtk_overlay_add_overlay(GTK_OVERLAY(thumb->w_main), thumb->w_ext);
1238     gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(thumb->w_main), thumb->w_ext, TRUE);
1239 
1240     // the image drawing area
1241     thumb->w_image_box = gtk_overlay_new();
1242     gtk_widget_set_name(thumb->w_image_box, "thumb_image");
1243     gtk_widget_set_size_request(thumb->w_image_box, thumb->width, thumb->height);
1244     gtk_widget_set_valign(thumb->w_image_box, GTK_ALIGN_START);
1245     gtk_widget_set_halign(thumb->w_image_box, GTK_ALIGN_START);
1246     gtk_widget_show(thumb->w_image_box);
1247     // we add a eventbox which cover all the w_image_box otherwise event don't work in areas not covered by w_image
1248     // itself
1249     GtkWidget *evt_image = gtk_event_box_new();
1250     gtk_widget_set_valign(evt_image, GTK_ALIGN_FILL);
1251     gtk_widget_set_halign(evt_image, GTK_ALIGN_FILL);
1252     gtk_widget_set_events(evt_image, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK
1253                                          | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1254                                          | GDK_POINTER_MOTION_MASK);
1255     g_signal_connect(G_OBJECT(evt_image), "motion-notify-event", G_CALLBACK(_event_main_motion), thumb);
1256     g_signal_connect(G_OBJECT(evt_image), "enter-notify-event", G_CALLBACK(_event_image_enter_leave), thumb);
1257     g_signal_connect(G_OBJECT(evt_image), "leave-notify-event", G_CALLBACK(_event_image_enter_leave), thumb);
1258     gtk_widget_show(evt_image);
1259     gtk_overlay_add_overlay(GTK_OVERLAY(thumb->w_image_box), evt_image);
1260     thumb->w_image = gtk_drawing_area_new();
1261     GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_image);
1262     if(thumb->container == DT_THUMBNAIL_CONTAINER_PREVIEW)
1263       gtk_style_context_add_class(context, "dt_preview_thumb_image");
1264     else if(thumb->container == DT_THUMBNAIL_CONTAINER_CULLING)
1265       gtk_style_context_add_class(context, "dt_culling_thumb_image");
1266     gtk_widget_set_name(thumb->w_image, "thumb_image");
1267     gtk_widget_set_valign(thumb->w_image, GTK_ALIGN_CENTER);
1268     gtk_widget_set_halign(thumb->w_image, GTK_ALIGN_CENTER);
1269     // the size will be defined at the end, inside dt_thumbnail_resize
1270     gtk_widget_set_events(thumb->w_image, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK
1271                                               | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1272                                               | GDK_POINTER_MOTION_MASK);
1273     g_signal_connect(G_OBJECT(thumb->w_image), "draw", G_CALLBACK(_event_image_draw), thumb);
1274     g_signal_connect(G_OBJECT(thumb->w_image), "motion-notify-event", G_CALLBACK(_event_main_motion), thumb);
1275     g_signal_connect(G_OBJECT(thumb->w_image), "enter-notify-event", G_CALLBACK(_event_image_enter_leave), thumb);
1276     g_signal_connect(G_OBJECT(thumb->w_image), "leave-notify-event", G_CALLBACK(_event_image_enter_leave), thumb);
1277     gtk_widget_show(thumb->w_image);
1278     gtk_overlay_add_overlay(GTK_OVERLAY(thumb->w_image_box), thumb->w_image);
1279     gtk_overlay_add_overlay(GTK_OVERLAY(thumb->w_main), thumb->w_image_box);
1280 
1281     // triangle to indicate current image(s) in filmstrip
1282     thumb->w_cursor = gtk_drawing_area_new();
1283     gtk_widget_set_name(thumb->w_cursor, "thumb_cursor");
1284     gtk_widget_set_valign(thumb->w_cursor, GTK_ALIGN_START);
1285     gtk_widget_set_halign(thumb->w_cursor, GTK_ALIGN_CENTER);
1286     g_signal_connect(G_OBJECT(thumb->w_cursor), "draw", G_CALLBACK(_event_cursor_draw), thumb);
1287     gtk_overlay_add_overlay(GTK_OVERLAY(thumb->w_main), thumb->w_cursor);
1288 
1289     // determine the overlays parents
1290     GtkWidget *overlays_parent = thumb->w_main;
1291     if(thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK) overlays_parent = thumb->w_image_box;
1292 
1293     // the infos background
1294     thumb->w_bottom_eb = gtk_event_box_new();
1295     gtk_widget_set_name(thumb->w_bottom_eb, "thumb_bottom");
1296     g_signal_connect(G_OBJECT(thumb->w_bottom_eb), "enter-notify-event", G_CALLBACK(_event_box_enter_leave),
1297                      thumb);
1298     g_signal_connect(G_OBJECT(thumb->w_bottom_eb), "leave-notify-event", G_CALLBACK(_event_box_enter_leave),
1299                      thumb);
1300     gtk_widget_set_valign(thumb->w_bottom_eb, GTK_ALIGN_END);
1301     gtk_widget_set_halign(thumb->w_bottom_eb, GTK_ALIGN_CENTER);
1302     gtk_widget_show(thumb->w_bottom_eb);
1303     if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED
1304        || thumb->over == DT_THUMBNAIL_OVERLAYS_MIXED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
1305     {
1306       gchar *lb = g_strdup(thumb->info_line);
1307       thumb->w_bottom = gtk_label_new(NULL);
1308       gtk_label_set_markup(GTK_LABEL(thumb->w_bottom), lb);
1309       g_free(lb);
1310     }
1311     else
1312     {
1313       thumb->w_bottom = gtk_label_new(NULL);
1314       gtk_label_set_markup(GTK_LABEL(thumb->w_bottom), "");
1315     }
1316     gtk_widget_set_name(thumb->w_bottom, "thumb_bottom_label");
1317     gtk_widget_show(thumb->w_bottom);
1318     gtk_label_set_yalign(GTK_LABEL(thumb->w_bottom), 0.05);
1319     gtk_label_set_ellipsize(GTK_LABEL(thumb->w_bottom), PANGO_ELLIPSIZE_MIDDLE);
1320     gtk_container_add(GTK_CONTAINER(thumb->w_bottom_eb), thumb->w_bottom);
1321     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_bottom_eb);
1322 
1323     // the reject icon
1324     thumb->w_reject = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_reject, 0, NULL);
1325     gtk_widget_set_name(thumb->w_reject, "thumb_reject");
1326     dt_action_define(&darktable.control->actions_thumb, NULL, "rating", thumb->w_reject, &dt_action_def_rating);
1327     gtk_widget_set_valign(thumb->w_reject, GTK_ALIGN_END);
1328     gtk_widget_set_halign(thumb->w_reject, GTK_ALIGN_START);
1329     gtk_widget_show(thumb->w_reject);
1330     g_signal_connect(G_OBJECT(thumb->w_reject), "button-press-event", G_CALLBACK(_event_rating_press), thumb);
1331     g_signal_connect(G_OBJECT(thumb->w_reject), "button-release-event", G_CALLBACK(_event_rating_release), thumb);
1332     g_signal_connect(G_OBJECT(thumb->w_reject), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1333     g_signal_connect(G_OBJECT(thumb->w_reject), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1334     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_reject);
1335 
1336     // the stars
1337     for(int i = 0; i < MAX_STARS; i++)
1338     {
1339       thumb->w_stars[i] = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_star, 0, NULL);
1340       g_signal_connect(G_OBJECT(thumb->w_stars[i]), "enter-notify-event", G_CALLBACK(_event_star_enter), thumb);
1341       g_signal_connect(G_OBJECT(thumb->w_stars[i]), "leave-notify-event", G_CALLBACK(_event_star_leave), thumb);
1342       g_signal_connect(G_OBJECT(thumb->w_stars[i]), "button-press-event", G_CALLBACK(_event_rating_press), thumb);
1343       g_signal_connect(G_OBJECT(thumb->w_stars[i]), "button-release-event", G_CALLBACK(_event_rating_release),
1344                        thumb);
1345       gtk_widget_set_name(thumb->w_stars[i], "thumb_star");
1346       dt_action_define(&darktable.control->actions_thumb, NULL, "rating", thumb->w_stars[i], &dt_action_def_rating);
1347       gtk_widget_set_valign(thumb->w_stars[i], GTK_ALIGN_END);
1348       gtk_widget_set_halign(thumb->w_stars[i], GTK_ALIGN_START);
1349       gtk_widget_show(thumb->w_stars[i]);
1350       gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_stars[i]);
1351     }
1352 
1353     // the color labels
1354     thumb->w_color = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_label_flower, thumb->colorlabels, &darktable.bauhaus->colorlabels);
1355     gtk_widget_set_name(thumb->w_color, "thumb_colorlabels");
1356     gtk_widget_set_valign(thumb->w_color, GTK_ALIGN_END);
1357     gtk_widget_set_halign(thumb->w_color, GTK_ALIGN_END);
1358     gtk_widget_set_no_show_all(thumb->w_color, TRUE);
1359     g_signal_connect(G_OBJECT(thumb->w_color), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1360     g_signal_connect(G_OBJECT(thumb->w_color), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1361     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_color);
1362 
1363     // the local copy indicator
1364     thumb->w_local_copy = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_local_copy, CPF_DO_NOT_USE_BORDER, NULL);
1365     gtk_widget_set_name(thumb->w_local_copy, "thumb_localcopy");
1366     gtk_widget_set_valign(thumb->w_local_copy, GTK_ALIGN_START);
1367     gtk_widget_set_halign(thumb->w_local_copy, GTK_ALIGN_END);
1368     gtk_widget_set_no_show_all(thumb->w_local_copy, TRUE);
1369     g_signal_connect(G_OBJECT(thumb->w_local_copy), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave),
1370                      thumb);
1371     g_signal_connect(G_OBJECT(thumb->w_local_copy), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave),
1372                      thumb);
1373     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_local_copy);
1374 
1375     // the altered icon
1376     thumb->w_altered = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_altered, CPF_DO_NOT_USE_BORDER, NULL);
1377     gtk_widget_set_name(thumb->w_altered, "thumb_altered");
1378     gtk_widget_set_valign(thumb->w_altered, GTK_ALIGN_START);
1379     gtk_widget_set_halign(thumb->w_altered, GTK_ALIGN_END);
1380     gtk_widget_set_no_show_all(thumb->w_altered, TRUE);
1381     g_signal_connect(G_OBJECT(thumb->w_altered), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1382     g_signal_connect(G_OBJECT(thumb->w_altered), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1383     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_altered);
1384 
1385     // the group bouton
1386     thumb->w_group = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_grouping, CPF_DO_NOT_USE_BORDER, NULL);
1387     gtk_widget_set_name(thumb->w_group, "thumb_group");
1388     g_signal_connect(G_OBJECT(thumb->w_group), "button-release-event", G_CALLBACK(_event_grouping_release), thumb);
1389     g_signal_connect(G_OBJECT(thumb->w_group), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1390     g_signal_connect(G_OBJECT(thumb->w_group), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1391     gtk_widget_set_valign(thumb->w_group, GTK_ALIGN_START);
1392     gtk_widget_set_halign(thumb->w_group, GTK_ALIGN_END);
1393     gtk_widget_set_no_show_all(thumb->w_group, TRUE);
1394     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_group);
1395 
1396     // the sound icon
1397     thumb->w_audio = dtgtk_thumbnail_btn_new(dtgtk_cairo_paint_audio, CPF_DO_NOT_USE_BORDER, NULL);
1398     gtk_widget_set_name(thumb->w_audio, "thumb_audio");
1399     g_signal_connect(G_OBJECT(thumb->w_audio), "button-release-event", G_CALLBACK(_event_audio_release), thumb);
1400     g_signal_connect(G_OBJECT(thumb->w_audio), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1401     g_signal_connect(G_OBJECT(thumb->w_audio), "leave-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1402     gtk_widget_set_valign(thumb->w_audio, GTK_ALIGN_START);
1403     gtk_widget_set_halign(thumb->w_audio, GTK_ALIGN_END);
1404     gtk_widget_set_no_show_all(thumb->w_audio, TRUE);
1405     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_audio);
1406 
1407     // the zoom indicator
1408     thumb->w_zoom_eb = gtk_event_box_new();
1409     g_signal_connect(G_OBJECT(thumb->w_zoom_eb), "enter-notify-event", G_CALLBACK(_event_btn_enter_leave), thumb);
1410     gtk_widget_set_name(thumb->w_zoom_eb, "thumb_zoom");
1411     gtk_widget_set_valign(thumb->w_zoom_eb, GTK_ALIGN_START);
1412     gtk_widget_set_halign(thumb->w_zoom_eb, GTK_ALIGN_START);
1413     if(zoom_ratio == IMG_TO_FIT)
1414       thumb->w_zoom = gtk_label_new(_("fit"));
1415     else
1416       thumb->w_zoom = gtk_label_new("mini");
1417     gtk_widget_set_name(thumb->w_zoom, "thumb_zoom_label");
1418     gtk_widget_show(thumb->w_zoom);
1419     gtk_container_add(GTK_CONTAINER(thumb->w_zoom_eb), thumb->w_zoom);
1420     gtk_overlay_add_overlay(GTK_OVERLAY(overlays_parent), thumb->w_zoom_eb);
1421 
1422     dt_thumbnail_resize(thumb, thumb->width, thumb->height, TRUE, zoom_ratio);
1423   }
1424   gtk_widget_show(thumb->w_main);
1425   g_object_ref(G_OBJECT(thumb->w_main));
1426   return thumb->w_main;
1427 }
1428 
dt_thumbnail_new(int width,int height,float zoom_ratio,int imgid,int rowid,dt_thumbnail_overlay_t over,dt_thumbnail_container_t container,gboolean tooltip)1429 dt_thumbnail_t *dt_thumbnail_new(int width, int height, float zoom_ratio, int imgid, int rowid,
1430                                  dt_thumbnail_overlay_t over, dt_thumbnail_container_t container, gboolean tooltip)
1431 {
1432   dt_thumbnail_t *thumb = calloc(1, sizeof(dt_thumbnail_t));
1433   thumb->width = width;
1434   thumb->height = height;
1435   thumb->imgid = imgid;
1436   thumb->rowid = rowid;
1437   thumb->over = over;
1438   thumb->container = container;
1439   thumb->zoomable = (container == DT_THUMBNAIL_CONTAINER_CULLING || container == DT_THUMBNAIL_CONTAINER_PREVIEW);
1440   thumb->zoom = 1.0f;
1441   thumb->overlay_timeout_duration = dt_conf_get_int("plugins/lighttable/overlay_timeout");
1442   thumb->tooltip = tooltip;
1443   thumb->expose_again_timeout_id = 0;
1444 
1445   // we read and cache all the infos from dt_image_t that we need
1446   const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->imgid, 'r');
1447   if(img)
1448   {
1449     thumb->filename = g_strdup(img->filename);
1450     if(thumb->over != DT_THUMBNAIL_OVERLAYS_NONE)
1451     {
1452       thumb->has_audio = (img->flags & DT_IMAGE_HAS_WAV);
1453       thumb->has_localcopy = (img->flags & DT_IMAGE_LOCAL_COPY);
1454     }
1455     dt_image_cache_read_release(darktable.image_cache, img);
1456   }
1457   if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED
1458      || over == DT_THUMBNAIL_OVERLAYS_MIXED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
1459     _thumb_update_extended_infos_line(thumb);
1460 
1461   // we read all other infos
1462   _image_get_infos(thumb);
1463 
1464   // we create the widget
1465   dt_thumbnail_create_widget(thumb, zoom_ratio);
1466 
1467   // let's see if the images are selected or active or mouse_overed
1468   _dt_active_images_callback(NULL, thumb);
1469   _dt_selection_changed_callback(NULL, thumb);
1470   if(dt_control_get_mouse_over_id() == thumb->imgid) dt_thumbnail_set_mouseover(thumb, TRUE);
1471 
1472   // set tooltip for altered icon if needed
1473   if(thumb->is_altered)
1474   {
1475     char *tooltip_txt = dt_history_get_items_as_string(thumb->imgid);
1476     if(tooltip_txt)
1477     {
1478       gtk_widget_set_tooltip_text(thumb->w_altered, tooltip_txt);
1479       g_free(tooltip_txt);
1480     }
1481   }
1482 
1483   // grouping tooltip
1484   _image_update_group_tooltip(thumb);
1485 
1486   // get the file extension
1487   _thumb_write_extension(thumb);
1488 
1489   // ensure all icons are up to date
1490   _thumb_update_icons(thumb);
1491 
1492   return thumb;
1493 }
1494 
dt_thumbnail_destroy(dt_thumbnail_t * thumb)1495 void dt_thumbnail_destroy(dt_thumbnail_t *thumb)
1496 {
1497   if(thumb->overlay_timeout_id > 0) g_source_remove(thumb->overlay_timeout_id);
1498   if(thumb->expose_again_timeout_id != 0) g_source_remove(thumb->expose_again_timeout_id);
1499   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_selection_changed_callback), thumb);
1500   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_active_images_callback), thumb);
1501   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_mipmaps_updated_callback), thumb);
1502   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_preview_updated_callback), thumb);
1503   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_image_info_changed_callback), thumb);
1504   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_dt_collection_changed_callback), thumb);
1505   if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) > 0)
1506     cairo_surface_destroy(thumb->img_surf);
1507   thumb->img_surf = NULL;
1508   if(thumb->w_main) gtk_widget_destroy(thumb->w_main);
1509   if(thumb->filename) g_free(thumb->filename);
1510   if(thumb->info_line) g_free(thumb->info_line);
1511   if(thumb->img_margin) gtk_border_free(thumb->img_margin);
1512   free(thumb);
1513 }
1514 
dt_thumbnail_update_infos(dt_thumbnail_t * thumb)1515 void dt_thumbnail_update_infos(dt_thumbnail_t *thumb)
1516 {
1517   if(!thumb) return;
1518   _image_get_infos(thumb);
1519   _thumb_write_extension(thumb);
1520   _thumb_update_icons(thumb);
1521   gtk_widget_queue_draw(thumb->w_main);
1522 }
1523 
_thumb_resize_overlays(dt_thumbnail_t * thumb)1524 static void _thumb_resize_overlays(dt_thumbnail_t *thumb)
1525 {
1526   PangoAttrList *attrlist;
1527   PangoAttribute *attr;
1528   int width = 0;
1529   int height = 0;
1530 
1531   int max_size = darktable.gui->icon_size;
1532   if(max_size < 2) max_size = round(1.2f * darktable.bauhaus->line_height); // fallback if toolbar icons are not realized
1533 
1534   if(thumb->over != DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
1535   {
1536     gtk_widget_get_size_request(thumb->w_main, &width, &height);
1537     // we need to squeeze 5 stars + 1 reject + 1 colorlabels symbols on a thumbnail width
1538     // stars + reject having a width of 2 * r1 and spaced by r1 => 18 * r1
1539     // colorlabels => 3 * r1 + space r1
1540     // inner margins are defined in css (margin_* values)
1541 
1542     // retrieves the size of the main icons in the top panel, thumbtable overlays shall not exceed that
1543     const float r1 = fminf(max_size / 2.0f, (width - thumb->img_margin->left - thumb->img_margin->right) / 22.0f);
1544     const float icon_size = 2.5 * r1;
1545 
1546     // file extension
1547     gtk_widget_set_margin_top(thumb->w_ext, thumb->img_margin->top);
1548     gtk_widget_set_margin_start(thumb->w_ext, thumb->img_margin->left);
1549 
1550     // bottom background
1551     gtk_widget_set_margin_start(thumb->w_bottom, thumb->img_margin->left);
1552     gtk_widget_set_margin_end(thumb->w_bottom, thumb->img_margin->right);
1553     if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED
1554        || thumb->over == DT_THUMBNAIL_OVERLAYS_MIXED)
1555     {
1556       attrlist = pango_attr_list_new();
1557       attr = pango_attr_size_new_absolute(1.5 * r1 * PANGO_SCALE);
1558       pango_attr_list_insert(attrlist, attr);
1559       gtk_label_set_attributes(GTK_LABEL(thumb->w_bottom), attrlist);
1560       pango_attr_list_unref(attrlist);
1561       int w = 0;
1562       int h = 0;
1563       pango_layout_get_pixel_size(gtk_label_get_layout(GTK_LABEL(thumb->w_bottom)), &w, &h);
1564       gtk_widget_set_size_request(thumb->w_bottom_eb, width, icon_size * 0.75 + h + 3 * thumb->img_margin->bottom);
1565     }
1566     else
1567       gtk_widget_set_size_request(thumb->w_bottom_eb, width, icon_size * 0.75 + 2 * thumb->img_margin->bottom);
1568 
1569     gtk_label_set_xalign(GTK_LABEL(thumb->w_bottom), 0.5);
1570     gtk_label_set_yalign(GTK_LABEL(thumb->w_bottom), 0);
1571     gtk_widget_set_margin_top(thumb->w_bottom, thumb->img_margin->bottom);
1572     gtk_widget_set_valign(thumb->w_bottom_eb, GTK_ALIGN_END);
1573     gtk_widget_set_halign(thumb->w_bottom_eb, GTK_ALIGN_CENTER);
1574 
1575     // reject icon
1576     const int margin_b_icons = MAX(0, thumb->img_margin->bottom - icon_size * 0.125 - 1);
1577     gtk_widget_set_size_request(thumb->w_reject, icon_size, icon_size);
1578     gtk_widget_set_valign(thumb->w_reject, GTK_ALIGN_END);
1579     int pos = MAX(0, MAX(thumb->img_margin->left - icon_size * 0.125, (width - 15.0 * r1) * 0.5 - 4 * 3.0 * r1));
1580     gtk_widget_set_margin_start(thumb->w_reject, pos);
1581     gtk_widget_set_margin_bottom(thumb->w_reject, margin_b_icons);
1582 
1583     // stars
1584     for(int i = 0; i < MAX_STARS; i++)
1585     {
1586       gtk_widget_set_size_request(thumb->w_stars[i], icon_size, icon_size);
1587       gtk_widget_set_valign(thumb->w_stars[i], GTK_ALIGN_END);
1588       gtk_widget_set_margin_bottom(thumb->w_stars[i], margin_b_icons);
1589       gtk_widget_set_margin_start(
1590           thumb->w_stars[i], thumb->img_margin->left
1591                                  + (width - thumb->img_margin->left - thumb->img_margin->right - 13.0 * r1) * 0.5
1592                                  + i * 2.5 * r1);
1593     }
1594 
1595     // the color labels
1596     gtk_widget_set_size_request(thumb->w_color, icon_size, icon_size);
1597     gtk_widget_set_valign(thumb->w_color, GTK_ALIGN_END);
1598     gtk_widget_set_halign(thumb->w_color, GTK_ALIGN_START);
1599     gtk_widget_set_margin_bottom(thumb->w_color, margin_b_icons);
1600     pos = MIN(width - (thumb->img_margin->right - icon_size * 0.125 + icon_size),
1601               (width - 15.0 * r1) * 0.5 + 8.25 * 3.0 * r1);
1602     gtk_widget_set_margin_start(thumb->w_color, pos);
1603 
1604     // the local copy indicator
1605     gtk_widget_set_size_request(thumb->w_local_copy, 1.618 * r1, 1.618 * r1);
1606     gtk_widget_set_halign(thumb->w_local_copy, GTK_ALIGN_END);
1607 
1608     // the altered icon
1609     gtk_widget_set_size_request(thumb->w_altered, 2.0 * r1, 2.0 * r1);
1610     gtk_widget_set_halign(thumb->w_altered, GTK_ALIGN_END);
1611     gtk_widget_set_margin_top(thumb->w_altered, thumb->img_margin->top);
1612     gtk_widget_set_margin_end(thumb->w_altered, thumb->img_margin->right);
1613 
1614     // the group bouton
1615     gtk_widget_set_size_request(thumb->w_group, 2.0 * r1, 2.0 * r1);
1616     gtk_widget_set_halign(thumb->w_group, GTK_ALIGN_END);
1617     gtk_widget_set_margin_top(thumb->w_group, thumb->img_margin->top);
1618     gtk_widget_set_margin_end(thumb->w_group, thumb->img_margin->right + 2.5 * r1);
1619 
1620     // the sound icon
1621     gtk_widget_set_size_request(thumb->w_audio, 2.0 * r1, 2.0 * r1);
1622     gtk_widget_set_halign(thumb->w_audio, GTK_ALIGN_END);
1623     gtk_widget_set_margin_top(thumb->w_audio, thumb->img_margin->top);
1624     gtk_widget_set_margin_end(thumb->w_audio, thumb->img_margin->right + 5.0 * r1);
1625 
1626     // the filmstrip cursor
1627     gtk_widget_set_size_request(thumb->w_cursor, 6.0 * r1, 1.5 * r1);
1628   }
1629   else
1630   {
1631     gtk_widget_get_size_request(thumb->w_image, &width, &height);
1632     int w = 0;
1633     int h = 0;
1634     gtk_widget_get_size_request(thumb->w_image_box, &w, &h);
1635     const int px = (w - width) / 2;
1636     const int py = (h - height) / 2;
1637 
1638     // we need to squeeze 5 stars + 1 reject + 1 colorlabels symbols on a thumbnail width
1639     // all icons having a width of 3.0 * r1 => 21 * r1
1640     // we want r1 spaces at extremities, after reject, before colorlables => 4 * r1
1641     const float r1 = fminf(max_size / 2.0f, width / 25.0f);
1642 
1643     // file extension
1644     gtk_widget_set_margin_top(thumb->w_ext, 0.03 * width + py);
1645     gtk_widget_set_margin_start(thumb->w_ext, 0.03 * width + px);
1646 
1647     // bottom background
1648     attrlist = pango_attr_list_new();
1649     attr = pango_attr_size_new_absolute(1.5 * r1 * PANGO_SCALE);
1650     pango_attr_list_insert(attrlist, attr);
1651     gtk_label_set_attributes(GTK_LABEL(thumb->w_bottom), attrlist);
1652     gtk_label_set_attributes(GTK_LABEL(thumb->w_zoom), attrlist);
1653     pango_attr_list_unref(attrlist);
1654     w = 0;
1655     h = 0;
1656     pango_layout_get_pixel_size(gtk_label_get_layout(GTK_LABEL(thumb->w_bottom)), &w, &h);
1657     // for the position, we use css margin and use it as per thousand (and not pixels)
1658     GtkBorder *margins = gtk_border_new();
1659     GtkBorder *borders = gtk_border_new();
1660     GtkStateFlags state = gtk_widget_get_state_flags(thumb->w_bottom_eb);
1661     GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_bottom_eb);
1662     GtkStateFlags statei = gtk_widget_get_state_flags(thumb->w_image);
1663     GtkStyleContext *contexti = gtk_widget_get_style_context(thumb->w_image);
1664     gtk_style_context_get_margin(context, state, margins);
1665     gtk_style_context_get_border(contexti, statei, borders);
1666     const int padding = r1;
1667     const int padding_t = 0.8 * r1; // reduced to compensate label top margin applied by gtk
1668     const int margin_t = height * margins->top / 1000;
1669     const int margin_l = width * margins->left / 1000;
1670     const int border_t = borders->top;
1671     const int border_l = borders->left;
1672     const float icon_size = 3.0 * r1;
1673     const float icon_size2 = 2.0 * r1;
1674     const int line2 = padding_t + h + padding - icon_size / 8.0 + margin_t + border_t;
1675     const int line3 = line2 + icon_size - icon_size / 8.0 + padding - icon_size / 8.0;
1676     gtk_border_free(margins);
1677     gtk_border_free(borders);
1678 
1679     const int min_width = 2.0 * padding - icon_size / 4.0 + 2 * r1 + 7 * icon_size;
1680     gtk_widget_set_size_request(thumb->w_bottom_eb, CLAMP(w + padding_t * 2.0, min_width, width),
1681                                 line3 - margin_t - border_t + icon_size2 + padding);
1682 
1683     gtk_label_set_xalign(GTK_LABEL(thumb->w_bottom), 0);
1684     gtk_label_set_yalign(GTK_LABEL(thumb->w_bottom), 0);
1685     gtk_widget_set_valign(thumb->w_bottom_eb, GTK_ALIGN_START);
1686     gtk_widget_set_halign(thumb->w_bottom_eb, GTK_ALIGN_START);
1687 
1688     gtk_widget_set_margin_top(thumb->w_bottom_eb, margin_t + border_t + py);
1689     gtk_widget_set_margin_start(thumb->w_bottom_eb, margin_l + border_l + px);
1690     gtk_widget_set_margin_top(thumb->w_bottom, padding_t);
1691     gtk_widget_set_margin_start(thumb->w_bottom, padding_t);
1692     gtk_widget_set_margin_end(thumb->w_bottom, padding_t);
1693 
1694     // reject icon
1695     gtk_widget_set_size_request(thumb->w_reject, icon_size, icon_size);
1696     gtk_widget_set_valign(thumb->w_reject, GTK_ALIGN_START);
1697     gtk_widget_set_margin_start(thumb->w_reject, padding - icon_size / 8.0 + border_l + px);
1698     gtk_widget_set_margin_top(thumb->w_reject, line2 + py);
1699     // stars
1700     for(int i = 0; i < MAX_STARS; i++)
1701     {
1702       gtk_widget_set_size_request(thumb->w_stars[i], icon_size, icon_size);
1703       gtk_widget_set_valign(thumb->w_stars[i], GTK_ALIGN_START);
1704       gtk_widget_set_margin_top(thumb->w_stars[i], line2 + py);
1705       gtk_widget_set_margin_start(thumb->w_stars[i],
1706                                   padding - icon_size / 8.0 + border_l + r1 + (i + 1) * 3.0 * r1 + px);
1707     }
1708     // the color labels
1709     gtk_widget_set_size_request(thumb->w_color, icon_size, icon_size);
1710     gtk_widget_set_valign(thumb->w_color, GTK_ALIGN_START);
1711     gtk_widget_set_halign(thumb->w_color, GTK_ALIGN_START);
1712     gtk_widget_set_margin_top(thumb->w_color, line2 + py);
1713     gtk_widget_set_margin_start(thumb->w_color,
1714                                 padding - icon_size / 8.0 + border_l + 2.0 * r1 + (MAX_STARS + 1) * 3.0 * r1 + px);
1715     // the local copy indicator
1716     gtk_widget_set_size_request(thumb->w_local_copy, icon_size2, icon_size2);
1717     gtk_widget_set_halign(thumb->w_local_copy, GTK_ALIGN_START);
1718     gtk_widget_set_margin_top(thumb->w_altered, line3 + py);
1719     gtk_widget_set_margin_start(thumb->w_altered, 10.0 * r1 + px);
1720     // the altered icon
1721     gtk_widget_set_size_request(thumb->w_altered, icon_size2, icon_size2);
1722     gtk_widget_set_halign(thumb->w_altered, GTK_ALIGN_START);
1723     gtk_widget_set_margin_top(thumb->w_altered, line3 + py);
1724     gtk_widget_set_margin_start(thumb->w_altered, 7.0 * r1 + px);
1725     // the group bouton
1726     gtk_widget_set_size_request(thumb->w_group, icon_size2, icon_size2);
1727     gtk_widget_set_halign(thumb->w_group, GTK_ALIGN_START);
1728     gtk_widget_set_margin_top(thumb->w_group, line3 + py);
1729     gtk_widget_set_margin_start(thumb->w_group, 4.0 * r1 + px);
1730     // the sound icon
1731     gtk_widget_set_size_request(thumb->w_audio, icon_size2, icon_size2);
1732     gtk_widget_set_halign(thumb->w_audio, GTK_ALIGN_START);
1733     gtk_widget_set_margin_top(thumb->w_audio, line3 + py);
1734     gtk_widget_set_margin_start(thumb->w_audio, r1 + px);
1735     // the zoomming indicator
1736     gtk_widget_set_margin_top(thumb->w_zoom_eb, line3 + py);
1737     gtk_widget_set_margin_start(thumb->w_zoom_eb, 18.0 * r1 + px);
1738   }
1739 }
1740 
dt_thumbnail_resize(dt_thumbnail_t * thumb,int width,int height,gboolean force,float zoom_ratio)1741 void dt_thumbnail_resize(dt_thumbnail_t *thumb, int width, int height, gboolean force, float zoom_ratio)
1742 {
1743   int w = 0;
1744   int h = 0;
1745   gtk_widget_get_size_request(thumb->w_main, &w, &h);
1746 
1747   // first, we verify that there's something to change
1748   if(!force && w == width && h == height) return;
1749 
1750   // widget resizing
1751   thumb->width = width;
1752   thumb->height = height;
1753   gtk_widget_set_size_request(thumb->w_main, width, height);
1754 
1755   // for thumbtable, we need to set the size class to the image widget
1756   if(thumb->container == DT_THUMBNAIL_CONTAINER_LIGHTTABLE)
1757   {
1758     // we get the corresponding size
1759     const char *txt = dt_conf_get_string_const("plugins/lighttable/thumbnail_sizes");
1760     gchar **ts = g_strsplit(txt, "|", -1);
1761     int i = 0;
1762     while(ts[i])
1763     {
1764       const int s = g_ascii_strtoll(ts[i], NULL, 10);
1765       if(thumb->width < s) break;
1766       i++;
1767     }
1768     g_strfreev(ts);
1769 
1770     gchar *cl = g_strdup_printf("dt_thumbnails_%d", i);
1771     GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_image);
1772     if(!gtk_style_context_has_class(context, cl))
1773     {
1774       // we remove all previous size class if any
1775       GList *l = gtk_style_context_list_classes(context);
1776       for(GList *l_iter = l; l_iter; l_iter = g_list_next(l_iter))
1777       {
1778         gchar *ll = (gchar *)l_iter->data;
1779         if(g_str_has_prefix(ll, "dt_thumbnails_"))
1780         {
1781           gtk_style_context_remove_class(context, ll);
1782         }
1783       }
1784       g_list_free(l);
1785 
1786       // we set the new class
1787       gtk_style_context_add_class(context, cl);
1788     }
1789     g_free(cl);
1790   }
1791 
1792   // file extension
1793   _thumb_retrieve_margins(thumb);
1794   gtk_widget_set_margin_start(thumb->w_ext, thumb->img_margin->left);
1795   gtk_widget_set_margin_top(thumb->w_ext, thumb->img_margin->top);
1796 
1797   // retrieves the size of the main icons in the top panel, thumbtable overlays shall not exceed that
1798   int max_size = darktable.gui->icon_size;
1799   if(max_size < 2) max_size = round(1.2f * darktable.bauhaus->line_height); // fallback if toolbar icons are not realized
1800 
1801   const int fsize = fminf(max_size, (height - thumb->img_margin->top - thumb->img_margin->bottom) / 11.0f);
1802 
1803   PangoAttrList *attrlist = pango_attr_list_new();
1804   PangoAttribute *attr = pango_attr_size_new_absolute(fsize * PANGO_SCALE);
1805   pango_attr_list_insert(attrlist, attr);
1806   // the idea is to reduce line-height, but it doesn't work for whatever reason...
1807   // PangoAttribute *attr2 = pango_attr_rise_new(-fsize * PANGO_SCALE);
1808   // pango_attr_list_insert(attrlist, attr2);
1809   gtk_label_set_attributes(GTK_LABEL(thumb->w_ext), attrlist);
1810   pango_attr_list_unref(attrlist);
1811 
1812   // for overlays different than block, we compute their size here, so we have valid value for th image area compute
1813   if(thumb->over != DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK) _thumb_resize_overlays(thumb);
1814   // we change the size and margins according to the size change. This will be refined after
1815   _thumb_set_image_area(thumb, zoom_ratio);
1816 
1817   // and the overlays for the block only (the others have been done before)
1818   if(thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK) _thumb_resize_overlays(thumb);
1819 
1820   // reset surface
1821   dt_thumbnail_image_refresh(thumb);
1822 }
1823 
dt_thumbnail_set_group_border(dt_thumbnail_t * thumb,dt_thumbnail_border_t border)1824 void dt_thumbnail_set_group_border(dt_thumbnail_t *thumb, dt_thumbnail_border_t border)
1825 {
1826   GtkStyleContext *context = gtk_widget_get_style_context(thumb->w_main);
1827   if(border == DT_THUMBNAIL_BORDER_NONE)
1828   {
1829     gtk_style_context_remove_class(context, "dt_group_left");
1830     gtk_style_context_remove_class(context, "dt_group_top");
1831     gtk_style_context_remove_class(context, "dt_group_right");
1832     gtk_style_context_remove_class(context, "dt_group_bottom");
1833     thumb->group_borders = DT_THUMBNAIL_BORDER_NONE;
1834     return;
1835   }
1836   else if(border & DT_THUMBNAIL_BORDER_LEFT)
1837     gtk_style_context_add_class(context, "dt_group_left");
1838   else if(border & DT_THUMBNAIL_BORDER_TOP)
1839     gtk_style_context_add_class(context, "dt_group_top");
1840   else if(border & DT_THUMBNAIL_BORDER_RIGHT)
1841     gtk_style_context_add_class(context, "dt_group_right");
1842   else if(border & DT_THUMBNAIL_BORDER_BOTTOM)
1843     gtk_style_context_add_class(context, "dt_group_bottom");
1844 
1845   thumb->group_borders |= border;
1846 }
1847 
dt_thumbnail_set_mouseover(dt_thumbnail_t * thumb,gboolean over)1848 void dt_thumbnail_set_mouseover(dt_thumbnail_t *thumb, gboolean over)
1849 {
1850   if(thumb->mouse_over == over) return;
1851   thumb->mouse_over = over;
1852   _thumb_update_icons(thumb);
1853 
1854   if(!thumb->mouse_over)
1855   {
1856     _set_flag(thumb->w_bottom_eb, GTK_STATE_FLAG_PRELIGHT, FALSE);
1857   }
1858   gtk_widget_queue_draw(thumb->w_main);
1859 }
1860 
1861 // set if the thumbnail should react (mouse_over) to drag and drop
1862 // note that it's just cosmetic as dropping occurs in thumbtable in any case
dt_thumbnail_set_drop(dt_thumbnail_t * thumb,gboolean accept_drop)1863 void dt_thumbnail_set_drop(dt_thumbnail_t *thumb, gboolean accept_drop)
1864 {
1865   if(accept_drop)
1866   {
1867     gtk_drag_dest_set(thumb->w_main, GTK_DEST_DEFAULT_MOTION, target_list_all, n_targets_all, GDK_ACTION_MOVE);
1868   }
1869   else
1870   {
1871     gtk_drag_dest_unset(thumb->w_main);
1872   }
1873 }
1874 
1875 // force the image to be reloaded from cache if any
dt_thumbnail_image_refresh(dt_thumbnail_t * thumb)1876 void dt_thumbnail_image_refresh(dt_thumbnail_t *thumb)
1877 {
1878   thumb->img_surf_dirty = TRUE;
1879 
1880   // we ensure that the image is not completely outside the thumbnail, otherwise the image_draw is not triggered
1881   if(gtk_widget_get_margin_start(thumb->w_image_box) >= thumb->width
1882      || gtk_widget_get_margin_top(thumb->w_image_box) >= thumb->height)
1883   {
1884     gtk_widget_set_margin_start(thumb->w_image_box, 0);
1885     gtk_widget_set_margin_top(thumb->w_image_box, 0);
1886   }
1887   gtk_widget_queue_draw(thumb->w_main);
1888 }
1889 
_widget_change_parent_overlay(GtkWidget * w,GtkOverlay * new_parent)1890 static void _widget_change_parent_overlay(GtkWidget *w, GtkOverlay *new_parent)
1891 {
1892   g_object_ref(w);
1893   gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(w)), w);
1894   gtk_overlay_add_overlay(new_parent, w);
1895   gtk_widget_show(w);
1896   g_object_unref(w);
1897 }
dt_thumbnail_set_overlay(dt_thumbnail_t * thumb,dt_thumbnail_overlay_t over,int timeout)1898 void dt_thumbnail_set_overlay(dt_thumbnail_t *thumb, dt_thumbnail_overlay_t over, int timeout)
1899 {
1900   thumb->overlay_timeout_duration = timeout;
1901   // if no change, do nothing...
1902   if(thumb->over == over) return;
1903   dt_thumbnail_overlay_t old_over = thumb->over;
1904   thumb->over = over;
1905 
1906   // first, if we change from/to hover/block, we need to change some parent widgets
1907   if(old_over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK || over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
1908   {
1909     GtkOverlay *overlays_parent = GTK_OVERLAY(thumb->w_main);
1910     if(thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK) overlays_parent = GTK_OVERLAY(thumb->w_image_box);
1911 
1912     _widget_change_parent_overlay(thumb->w_bottom_eb, overlays_parent);
1913     _widget_change_parent_overlay(thumb->w_reject, overlays_parent);
1914     for(int i = 0; i < MAX_STARS; i++)
1915     {
1916       _widget_change_parent_overlay(thumb->w_stars[i], overlays_parent);
1917     }
1918     _widget_change_parent_overlay(thumb->w_color, overlays_parent);
1919     _widget_change_parent_overlay(thumb->w_local_copy, overlays_parent);
1920     _widget_change_parent_overlay(thumb->w_altered, overlays_parent);
1921     _widget_change_parent_overlay(thumb->w_group, overlays_parent);
1922     _widget_change_parent_overlay(thumb->w_audio, overlays_parent);
1923     _widget_change_parent_overlay(thumb->w_zoom_eb, overlays_parent);
1924   }
1925 
1926   // we read and cache all the infos from dt_image_t that we need, depending on the overlay level
1927   // note that when "downgrading" overlay level, we don't bother to remove the infos
1928   dt_thumbnail_reload_infos(thumb);
1929 
1930   // and we resize the overlays
1931   _thumb_resize_overlays(thumb);
1932 }
1933 
1934 // force the image to be redraw at the right position
dt_thumbnail_image_refresh_position(dt_thumbnail_t * thumb)1935 void dt_thumbnail_image_refresh_position(dt_thumbnail_t *thumb)
1936 {
1937   // let's sanitize and apply panning values
1938   // here we have to make sure to properly align according to ppd
1939   int iw = 0;
1940   int ih = 0;
1941   gtk_widget_get_size_request(thumb->w_image, &iw, &ih);
1942   thumb->zoomx = CLAMP(thumb->zoomx, (iw * darktable.gui->ppd_thb - thumb->img_width) / darktable.gui->ppd_thb, 0);
1943   thumb->zoomy = CLAMP(thumb->zoomy, (ih * darktable.gui->ppd_thb - thumb->img_height) / darktable.gui->ppd_thb, 0);
1944   gtk_widget_queue_draw(thumb->w_main);
1945 }
1946 
1947 // get the max zoom value of the thumb
dt_thumbnail_get_zoom100(dt_thumbnail_t * thumb)1948 float dt_thumbnail_get_zoom100(dt_thumbnail_t *thumb)
1949 {
1950   if(thumb->zoom_100 < 1.0f) // we only compute the sizes if needed
1951   {
1952     int w = 0;
1953     int h = 0;
1954     dt_image_get_final_size(thumb->imgid, &w, &h);
1955     if(!thumb->img_margin) _thumb_retrieve_margins(thumb);
1956 
1957     const float used_h = (float)(thumb->height - thumb->img_margin->top - thumb->img_margin->bottom);
1958     const float used_w = (float)(thumb->width - thumb->img_margin->left - thumb->img_margin->right);
1959     thumb->zoom_100 = fmaxf((float)w / used_w, (float)h / used_h);
1960     if(thumb->zoom_100 < 1.0f) thumb->zoom_100 = 1.0f;
1961   }
1962 
1963   return thumb->zoom_100;
1964 }
1965 
dt_thumbnail_get_zoom_ratio(dt_thumbnail_t * thumb)1966 float dt_thumbnail_get_zoom_ratio(dt_thumbnail_t *thumb)
1967 {
1968   if(thumb->zoom_100 < 1.0f) // we only compute the sizes if needed
1969     dt_thumbnail_get_zoom100(thumb);
1970 
1971   return _thumb_zoom_to_zoom_ratio(thumb->zoom, thumb->zoom_100);
1972 }
1973 
1974 // force the reload of image infos
dt_thumbnail_reload_infos(dt_thumbnail_t * thumb)1975 void dt_thumbnail_reload_infos(dt_thumbnail_t *thumb)
1976 {
1977   const dt_image_t *img = dt_image_cache_get(darktable.image_cache, thumb->imgid, 'r');
1978   if(img)
1979   {
1980     if(thumb->over != DT_THUMBNAIL_OVERLAYS_NONE)
1981     {
1982       thumb->filename = g_strdup(img->filename);
1983       thumb->has_audio = (img->flags & DT_IMAGE_HAS_WAV);
1984       thumb->has_localcopy = (img->flags & DT_IMAGE_LOCAL_COPY);
1985     }
1986 
1987     dt_image_cache_read_release(darktable.image_cache, img);
1988   }
1989   if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED
1990      || thumb->over == DT_THUMBNAIL_OVERLAYS_MIXED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
1991     _thumb_update_extended_infos_line(thumb);
1992 
1993   // we read all other infos
1994   if(thumb->over != DT_THUMBNAIL_OVERLAYS_NONE)
1995   {
1996     _image_get_infos(thumb);
1997     _thumb_update_icons(thumb);
1998   }
1999 
2000   _thumb_write_extension(thumb);
2001 
2002   // extended overlay text
2003   gchar *lb = NULL;
2004   if(thumb->over == DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED
2005      || thumb->over == DT_THUMBNAIL_OVERLAYS_MIXED || thumb->over == DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK)
2006     lb = g_strdup(thumb->info_line);
2007 
2008   // we set the text
2009   gtk_label_set_markup(GTK_LABEL(thumb->w_bottom), lb);
2010   g_free(lb);
2011 }
2012 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
2013 // vim: shiftwidth=2 expandtab tabstop=2 cindent
2014 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
2015