1 /*
2     This file is part of darktable,
3     copyright (c) 2020 Aldric Renaudin.
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 /** a class to manage a collection of zoomable thumbnails for culling or full preview.  */
19 #include "dtgtk/culling.h"
20 #include "common/collection.h"
21 #include "common/debug.h"
22 #include "common/selection.h"
23 #include "control/control.h"
24 #include "gui/gtk.h"
25 #include "views/view.h"
26 
27 #define FULL_PREVIEW_IN_MEMORY_LIMIT 9
28 #define ZOOM_MAX 100000.0f
29 
_absmul(float a,float b)30 static inline float _absmul(float a, float b)
31 {
32   return a > b ? a / b : b / a;
33 }
_get_max_in_memory_images()34 static inline int _get_max_in_memory_images()
35 {
36   const int max_in_memory_images = dt_conf_get_int("plugins/lighttable/preview/max_in_memory_images");
37   return MIN(max_in_memory_images, FULL_PREVIEW_IN_MEMORY_LIMIT);
38 }
39 // specials functions for GList globals actions
_list_compare_by_imgid(gconstpointer a,gconstpointer b)40 static gint _list_compare_by_imgid(gconstpointer a, gconstpointer b)
41 {
42   dt_thumbnail_t *th = (dt_thumbnail_t *)a;
43   const int imgid = GPOINTER_TO_INT(b);
44   if(th->imgid < 0 || b < 0) return 1;
45   return (th->imgid != imgid);
46 }
_list_remove_thumb(gpointer user_data)47 static void _list_remove_thumb(gpointer user_data)
48 {
49   dt_thumbnail_t *thumb = (dt_thumbnail_t *)user_data;
50   gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(thumb->w_main)), thumb->w_main);
51   dt_thumbnail_destroy(thumb);
52 }
53 
_get_selection_count()54 static int _get_selection_count()
55 {
56   int nb = 0;
57   gchar *query = dt_util_dstrcat(
58       NULL,
59       "SELECT count(*) FROM main.selected_images AS s, memory.collected_images as m WHERE s.imgid = m.imgid");
60   sqlite3_stmt *stmt;
61   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
62   if(stmt != NULL)
63   {
64     if(sqlite3_step(stmt) == SQLITE_ROW)
65     {
66       nb = sqlite3_column_int(stmt, 0);
67     }
68     sqlite3_finalize(stmt);
69   }
70   g_free(query);
71 
72   return nb;
73 }
74 
75 // get imgid from rowid
_thumb_get_imgid(int rowid)76 static int _thumb_get_imgid(int rowid)
77 {
78   int id = -1;
79   sqlite3_stmt *stmt;
80   gchar *query = dt_util_dstrcat(NULL, "SELECT imgid FROM memory.collected_images WHERE rowid=%d", rowid);
81   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
82   if(sqlite3_step(stmt) == SQLITE_ROW)
83   {
84     id = sqlite3_column_int(stmt, 0);
85   }
86   g_free(query);
87   sqlite3_finalize(stmt);
88   return id;
89 }
90 // get rowid from imgid
_thumb_get_rowid(int imgid)91 static int _thumb_get_rowid(int imgid)
92 {
93   int id = -1;
94   sqlite3_stmt *stmt;
95   gchar *query = dt_util_dstrcat(NULL, "SELECT rowid FROM memory.collected_images WHERE imgid=%d", imgid);
96   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
97   if(sqlite3_step(stmt) == SQLITE_ROW)
98   {
99     id = sqlite3_column_int(stmt, 0);
100   }
101   g_free(query);
102   sqlite3_finalize(stmt);
103   return id;
104 }
105 
106 // compute thumb_size, thumbs_per_row and rows for the current widget size
107 // return TRUE if something as changed (or forced) FALSE otherwise
_compute_sizes(dt_culling_t * table,gboolean force)108 static gboolean _compute_sizes(dt_culling_t *table, gboolean force)
109 {
110   gboolean ret = FALSE; // return value to show if something as changed
111   GtkAllocation allocation;
112   gtk_widget_get_allocation(table->widget, &allocation);
113 
114   if(allocation.width <= 20 || allocation.height <= 20)
115   {
116     table->view_width = allocation.width;
117     table->view_height = allocation.height;
118     return FALSE;
119   }
120 
121   // check the offset
122   if(table->list)
123   {
124     dt_thumbnail_t *th = (dt_thumbnail_t *)table->list->data;
125     if(th->imgid != table->offset_imgid || th->display_focus != table->focus) ret = TRUE;
126   }
127   else if(table->offset_imgid > 0)
128     ret = TRUE;
129 
130   if(table->mode == DT_CULLING_MODE_CULLING)
131   {
132     const int npr = dt_view_lighttable_get_zoom(darktable.view_manager);
133 
134     if(force || allocation.width != table->view_width || allocation.height != table->view_height
135        || npr != table->thumbs_count)
136     {
137       table->thumbs_count = npr;
138       table->view_width = allocation.width;
139       table->view_height = allocation.height;
140       ret = TRUE;
141     }
142   }
143   else if(table->mode == DT_CULLING_MODE_PREVIEW)
144   {
145     if(force || allocation.width != table->view_width || allocation.height != table->view_height)
146     {
147       table->thumbs_count = 1;
148       table->view_width = allocation.width;
149       table->view_height = allocation.height;
150       ret = TRUE;
151     }
152   }
153   return ret;
154 }
155 
156 // set mouse_over_id to thumb under mouse or to first thumb
_thumbs_refocus(dt_culling_t * table)157 static void _thumbs_refocus(dt_culling_t *table)
158 {
159   int overid = -1;
160 
161   if(table->mouse_inside)
162   {
163     // the exact position of the mouse
164     int x = -1;
165     int y = -1;
166     gdk_window_get_origin(gtk_widget_get_window(table->widget), &x, &y);
167     x = table->pan_x - x;
168     y = table->pan_y - y;
169 
170     // which thumb is under the mouse ?
171     for(GList *l = table->list; l; l = g_list_next(l))
172     {
173       dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
174       if(th->x <= x && th->x + th->width > x && th->y <= y && th->y + th->height > y)
175       {
176         overid = th->imgid;
177         break;
178       }
179     }
180   }
181 
182   // if overid not valid, we use the offset image
183   if(overid <= 0)
184   {
185     overid = table->offset_imgid;
186   }
187 
188   // and we set the overid
189   dt_control_set_mouse_over_id(overid);
190 }
191 
_thumbs_move(dt_culling_t * table,int move)192 static void _thumbs_move(dt_culling_t *table, int move)
193 {
194   if(move == 0) return;
195   int new_offset = table->offset;
196   // we sanintize the values to be sure to stay in the allowed collection
197   if(move < 0)
198   {
199     if(table->navigate_inside_selection)
200     {
201       sqlite3_stmt *stmt;
202       gchar *query = dt_util_dstrcat(NULL,
203                                      "SELECT m.rowid FROM memory.collected_images as m, main.selected_images as s "
204                                      "WHERE m.imgid=s.imgid AND m.rowid<=%d "
205                                      "ORDER BY m.rowid DESC LIMIT 1 OFFSET %d",
206                                      table->offset, -1 * move);
207       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
208       if(sqlite3_step(stmt) == SQLITE_ROW)
209       {
210         new_offset = sqlite3_column_int(stmt, 0);
211       }
212       else
213       {
214         // if we are here, that means we don't have enough space to move as wanted. So we move to first position
215         g_free(query);
216         sqlite3_finalize(stmt);
217         query
218             = dt_util_dstrcat(NULL, "SELECT m.rowid FROM memory.collected_images as m, main.selected_images as s "
219                                     "WHERE m.imgid=s.imgid "
220                                     "ORDER BY m.rowid LIMIT 1");
221         DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
222         if(sqlite3_step(stmt) == SQLITE_ROW)
223         {
224           new_offset = sqlite3_column_int(stmt, 0);
225         }
226       }
227       g_free(query);
228       sqlite3_finalize(stmt);
229       if(new_offset == table->offset)
230       {
231         dt_control_log(_("you have reached the start of your selection"));
232         return;
233       }
234     }
235     else
236     {
237       new_offset = MAX(1, table->offset + move);
238       if(new_offset == table->offset)
239       {
240         dt_control_log(_("you have reached the start of your collection"));
241         return;
242       }
243     }
244   }
245   else
246   {
247     if(table->navigate_inside_selection)
248     {
249       sqlite3_stmt *stmt;
250       gchar *query
251           = dt_util_dstrcat(NULL,
252                             "SELECT COUNT(m.rowid) FROM memory.collected_images as m, main.selected_images as s "
253                             "WHERE m.imgid=s.imgid AND m.rowid>%d",
254                             table->offset);
255       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
256       int nb_after = 0;
257       if(sqlite3_step(stmt) == SQLITE_ROW)
258       {
259         nb_after = sqlite3_column_int(stmt, 0);
260       }
261       g_free(query);
262       sqlite3_finalize(stmt);
263 
264       if(nb_after >= table->thumbs_count)
265       {
266         const int delta = MIN(nb_after + 1 - table->thumbs_count, move);
267         query = dt_util_dstrcat(NULL,
268                                 "SELECT m.rowid FROM memory.collected_images as m, main.selected_images as s "
269                                 "WHERE m.imgid=s.imgid AND m.rowid>=%d "
270                                 "ORDER BY m.rowid LIMIT 1 OFFSET %d",
271                                 table->offset, delta);
272         DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
273         if(sqlite3_step(stmt) == SQLITE_ROW)
274         {
275           new_offset = sqlite3_column_int(stmt, 0);
276         }
277         g_free(query);
278         sqlite3_finalize(stmt);
279       }
280 
281       if(new_offset == table->offset)
282       {
283         dt_control_log(_("you have reached the end of your selection"));
284         return;
285       }
286     }
287     else
288     {
289       sqlite3_stmt *stmt;
290       gchar *query = dt_util_dstrcat(NULL,
291                                      "SELECT COUNT(m.rowid) FROM memory.collected_images as m "
292                                      "WHERE m.rowid>%d",
293                                      table->offset);
294       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
295       if(sqlite3_step(stmt) == SQLITE_ROW)
296       {
297         const int nb = sqlite3_column_int(stmt, 0);
298         if(nb >= table->thumbs_count)
299         {
300           new_offset = table->offset + MIN(nb + 1 - table->thumbs_count, move);
301         }
302       }
303       g_free(query);
304       sqlite3_finalize(stmt);
305       if(new_offset == table->offset)
306       {
307         dt_control_log(_("you have reached the end of your collection"));
308         return;
309       }
310     }
311   }
312 
313   if(new_offset != table->offset)
314   {
315     table->offset = new_offset;
316     dt_culling_full_redraw(table, TRUE);
317     _thumbs_refocus(table);
318   }
319 }
320 
_set_table_zoom_ratio(dt_culling_t * table,dt_thumbnail_t * th)321 static void _set_table_zoom_ratio(dt_culling_t *table, dt_thumbnail_t *th)
322 {
323   table->zoom_ratio = dt_thumbnail_get_zoom_ratio(th);
324 }
325 
_get_root_offset(GtkWidget * w_image_box,float x_root,float y_root,int * x_offset,int * y_offset)326 static void _get_root_offset(GtkWidget *w_image_box, float x_root, float y_root, int *x_offset, int *y_offset)
327 {
328   gdk_window_get_origin(gtk_widget_get_window(w_image_box), x_offset, y_offset);
329   *x_offset = x_root - *x_offset;
330   *y_offset = y_root - *y_offset;
331 }
332 
_zoom_and_shift(dt_thumbnail_t * th,const int x_offset,const int y_offset,const float zoom_delta)333 static gboolean _zoom_and_shift(dt_thumbnail_t *th, const int x_offset, const int y_offset, const float zoom_delta)
334 {
335   float zd = CLAMP(th->zoom + zoom_delta, 1.0f, th->zoom_100);
336   if(zd == th->zoom)
337     return FALSE; // delta_zoom did not change this thumbnail's zoom factor
338 
339   const float z_ratio = zd / th->zoom;
340   th->zoom = zd;
341 
342   int posx = x_offset;
343   int posy = y_offset;
344 
345   const int iw = gtk_widget_get_allocated_width(th->w_image);
346   const int ih = gtk_widget_get_allocated_height(th->w_image);
347 
348   // we center the zoom around cursor position
349   if(posx >= 0 && posy >= 0)
350   {
351     // we take in account that the image may be smaller that the imagebox
352     posx -= (gtk_widget_get_allocated_width(th->w_image_box) - iw) / 2;
353     posy -= (gtk_widget_get_allocated_height(th->w_image_box) - ih) / 2;
354   }
355 
356   // we change the value. Values will be sanitized in the drawing event
357   th->zoomx = posx - (posx - th->zoomx) * z_ratio;
358   th->zoomy = posy - (posy - th->zoomy) * z_ratio;
359 
360   dt_thumbnail_image_refresh(th);
361 
362   return TRUE;
363 }
364 
_zoom_to_x_root(dt_thumbnail_t * th,const float x_root,const float y_root,const float zoom_delta)365 static gboolean _zoom_to_x_root(dt_thumbnail_t *th, const float x_root, const float y_root, const float zoom_delta)
366 {
367   int x_offset = 0;
368   int y_offset = 0;
369 
370   _get_root_offset(th->w_image_box, x_root, y_root, &x_offset, &y_offset);
371 
372   return _zoom_and_shift(th, x_offset, y_offset, zoom_delta);
373 }
374 
_zoom_to_center(dt_thumbnail_t * th,const float zoom_delta)375 static gboolean _zoom_to_center(dt_thumbnail_t *th, const float zoom_delta)
376 {
377   float zd = CLAMP(th->zoom + zoom_delta, 1.0f, th->zoom_100);
378   if(zd == th->zoom)
379     return FALSE; // delta_zoom did not change this thumbnail's zoom factor
380 
381   const float z_ratio = zd / th->zoom;
382   th->zoom = zd;
383   // we center the zoom around center of the shown image
384   int iw = 0;
385   int ih = 0;
386   gtk_widget_get_size_request(th->w_image_box, &iw, &ih);
387   th->zoomx = fmaxf(iw - th->img_width * z_ratio, fminf(0.0f, iw / 2.0 - (iw / 2.0 - th->zoomx) * z_ratio));
388   th->zoomy = fmaxf(ih - th->img_height * z_ratio, fminf(0.0f, ih / 2.0 - (ih / 2.0 - th->zoomy) * z_ratio));
389 
390   dt_thumbnail_image_refresh(th);
391 
392   return TRUE;
393 }
394 
_thumbs_zoom_add(dt_culling_t * table,const float zoom_delta,const float x_root,const float y_root,int state)395 static gboolean _thumbs_zoom_add(dt_culling_t *table, const float zoom_delta, const float x_root,
396                                  const float y_root, int state)
397 {
398   const int max_in_memory_images = _get_max_in_memory_images();
399   if(table->mode == DT_CULLING_MODE_CULLING && table->thumbs_count > max_in_memory_images)
400   {
401     dt_control_log(_("zooming is limited to %d images"), max_in_memory_images);
402     return TRUE;
403   }
404 
405   // we ensure the zoom 100 is computed for all images
406   for(GList *l = table->list; l; l = g_list_next(l))
407   {
408     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
409     dt_thumbnail_get_zoom100(th);
410   }
411 
412   if(!g_list_shorter_than(table->list, 2))  // at least two images?
413   {
414     // CULLING with multiple images
415     // if shift+ctrl, we only change the current image
416     if(dt_modifier_is(state, GDK_SHIFT_MASK))
417     {
418       const int mouseid = dt_control_get_mouse_over_id();
419       for(GList *l = table->list; l; l = g_list_next(l))
420       {
421         dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
422         if(th->imgid == mouseid)
423         {
424           if(_zoom_to_x_root(th, x_root, y_root, zoom_delta))
425             _set_table_zoom_ratio(table, th);
426           break;
427         }
428       }
429     }
430     else
431     {
432       const int mouseid = dt_control_get_mouse_over_id();
433       int x_offset = 0;
434       int y_offset = 0;
435       gboolean to_pointer = FALSE;
436 
437       // get the offset for the image under the cursor
438       for(GList *l = table->list; l; l = g_list_next(l))
439       {
440         dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
441         if(th->imgid == mouseid)
442         {
443           _get_root_offset(th->w_image_box, x_root, y_root, &x_offset, &y_offset);
444           to_pointer = TRUE;
445           break;
446         }
447       }
448 
449       // apply the offset to all images
450       for(GList *l = table->list; l; l = g_list_next(l))
451       {
452         dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
453         if(to_pointer == TRUE ? _zoom_and_shift(th, x_offset, y_offset, zoom_delta)
454                               : _zoom_to_center(th, zoom_delta))
455           _set_table_zoom_ratio(table, th);
456       }
457     }
458   }
459   else if(table->list)
460   {
461     // FULL PREVIEW or CULLING with 1 image
462     dt_thumbnail_t *th = (dt_thumbnail_t *)table->list->data;
463     if(_zoom_to_x_root(th, x_root, y_root, zoom_delta))
464       _set_table_zoom_ratio(table, th);
465   }
466 
467   return TRUE;
468 }
469 
_zoom_thumb_fit(dt_thumbnail_t * th)470 static void _zoom_thumb_fit(dt_thumbnail_t *th)
471 {
472   th->zoom = 1.0;
473   th->zoomx = 0;
474   th->zoomy = 0;
475   dt_thumbnail_image_refresh(th);
476 }
477 
_zoom_thumb_max(dt_thumbnail_t * th,float x_root,float y_root)478 static gboolean _zoom_thumb_max(dt_thumbnail_t *th, float x_root, float y_root)
479 {
480   dt_thumbnail_get_zoom100(th);
481   return _zoom_to_x_root(th, x_root, y_root, ZOOM_MAX);
482 }
483 
484 // toggle zoom max / zoom fit of image currently having mouse over id
_toggle_zoom_current(dt_culling_t * table,float x_root,float y_root)485 static void _toggle_zoom_current(dt_culling_t *table, float x_root, float y_root)
486 {
487   const int id = dt_control_get_mouse_over_id();
488   for(GList *l = table->list; l; l = g_list_next(l))
489   {
490     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
491     if(th->imgid == id)
492     {
493       if(th->zoom_100 < 1.0 || th->zoom < th->zoom_100)
494         _zoom_thumb_max(th, x_root, y_root);
495       else
496         _zoom_thumb_fit(th);
497       break;
498     }
499   }
500 }
501 
502 // toggle zoom max / zoom fit of all images in culling table
_toggle_zoom_all(dt_culling_t * table,float x_root,float y_root)503 static void _toggle_zoom_all(dt_culling_t *table, float x_root, float y_root)
504 {
505   gboolean zmax = TRUE;
506   for(GList *l = table->list; l; l = g_list_next(l))
507   {
508     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
509     if(th->zoom_100 < 1.0 || th->zoom < th->zoom_100)
510     {
511       zmax = FALSE;
512       break;
513     }
514   }
515 
516   if(zmax)
517     dt_culling_zoom_fit(table);
518   else
519     _thumbs_zoom_add(table, ZOOM_MAX, x_root, y_root, 0);
520 }
521 
_event_scroll(GtkWidget * widget,GdkEvent * event,gpointer user_data)522 static gboolean _event_scroll(GtkWidget *widget, GdkEvent *event, gpointer user_data)
523 {
524   GdkEventScroll *e = (GdkEventScroll *)event;
525   dt_culling_t *table = (dt_culling_t *)user_data;
526   int delta;
527 
528   if(dt_gui_get_scroll_unit_delta(e, &delta))
529   {
530     if(dt_modifier_is(e->state, GDK_CONTROL_MASK))
531     {
532       // zooming
533       const float zoom_delta = delta < 0 ? 0.5f : -0.5f;
534       _thumbs_zoom_add(table, zoom_delta, e->x_root, e->y_root, e->state);
535     }
536     else
537     {
538       const int move = delta < 0 ? -1 : 1;
539       _thumbs_move(table, move);
540     }
541   }
542   return TRUE;
543 }
544 
_event_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)545 static gboolean _event_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
546 {
547   if(!GTK_IS_CONTAINER(gtk_widget_get_parent(widget))) return TRUE;
548 
549   // we render the background (can be visible if before first image / after last image)
550   GtkStyleContext *context = gtk_widget_get_style_context(widget);
551   gtk_render_background(context, cr, 0, 0, gtk_widget_get_allocated_width(widget),
552                         gtk_widget_get_allocated_height(widget));
553 
554   // but we don't really want to draw something, this is just to know when the widget is really ready
555   dt_culling_t *table = (dt_culling_t *)user_data;
556   dt_culling_full_redraw(table, FALSE);
557   return FALSE; // let's propagate this event
558 }
559 
_event_leave_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)560 static gboolean _event_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
561 {
562   dt_culling_t *table = (dt_culling_t *)user_data;
563   // if the leaving cause is the hide of the widget, no mouseover change
564   if(!gtk_widget_is_visible(widget))
565   {
566     table->mouse_inside = FALSE;
567     return FALSE;
568   }
569 
570   // if we leave thumbtable in favour of an inferior (a thumbnail) it's not a real leave !
571   if(event->detail == GDK_NOTIFY_INFERIOR) return FALSE;
572 
573   table->mouse_inside = FALSE;
574   dt_control_set_mouse_over_id(-1);
575   return TRUE;
576 }
577 
_event_enter_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)578 static gboolean _event_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
579 {
580   // we only handle the case where we enter thumbtable from an inferior (a thumbnail)
581   // this is when the mouse enter an "empty" area of thumbtable
582   if(event->detail != GDK_NOTIFY_INFERIOR) return FALSE;
583 
584   dt_control_set_mouse_over_id(-1);
585   return TRUE;
586 }
587 
_event_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)588 static gboolean _event_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
589 {
590   dt_culling_t *table = (dt_culling_t *)user_data;
591 
592   if(event->button == 2)
593   {
594     // if shift is pressed, we work only with image hovered
595     if(dt_modifier_is(event->state, GDK_SHIFT_MASK))
596       _toggle_zoom_current(table, event->x_root, event->y_root);
597     else
598       _toggle_zoom_all(table, event->x_root, event->y_root);
599     return TRUE;
600   }
601 
602   const int id = dt_control_get_mouse_over_id();
603 
604   if(id > 0 && event->button == 1 && event->type == GDK_2BUTTON_PRESS)
605   {
606     dt_view_manager_switch(darktable.view_manager, "darkroom");
607     return TRUE;
608   }
609 
610   table->pan_x = event->x_root;
611   table->pan_y = event->y_root;
612   table->panning = TRUE;
613   return TRUE;
614 }
615 
_event_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)616 static gboolean _event_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
617 {
618   dt_culling_t *table = (dt_culling_t *)user_data;
619   table->mouse_inside = TRUE;
620   if(!table->panning)
621   {
622     table->pan_x = event->x_root;
623     table->pan_y = event->y_root;
624     return FALSE;
625   }
626 
627   // get the max zoom of all images
628   const int max_in_memory_images = _get_max_in_memory_images();
629   if(table->mode == DT_CULLING_MODE_CULLING && table->thumbs_count > max_in_memory_images) return FALSE;
630 
631   float fz = 1.0f;
632   for (GList *l = table->list; l; l = g_list_next(l))
633   {
634     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
635     fz = fmaxf(fz, th->zoom);
636   }
637 
638   if(table->panning && fz > 1.0f)
639   {
640     const double x = event->x_root;
641     const double y = event->y_root;
642     // we want the images to stay in the screen
643     const float scale = darktable.gui->ppd_thb / darktable.gui->ppd;
644     const float valx = (x - table->pan_x) * scale;
645     const float valy = (y - table->pan_y) * scale;
646 
647     if(dt_modifier_is(event->state, GDK_SHIFT_MASK))
648     {
649       int mouseid = dt_control_get_mouse_over_id();
650       for(GList *l = table->list; l; l = g_list_next(l))
651       {
652         dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
653         if(th->imgid == mouseid)
654         {
655           th->zoomx += valx;
656           th->zoomy += valy;
657           break;
658         }
659       }
660     }
661     else
662     {
663       for(GList *l = table->list; l; l = g_list_next(l))
664       {
665         dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
666         th->zoomx += valx;
667         th->zoomy += valy;
668       }
669     }
670     // sanitize specific positions of individual images
671     for(GList *l = table->list; l; l = g_list_next(l))
672     {
673       dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
674       int iw = 0;
675       int ih = 0;
676       gtk_widget_get_size_request(th->w_image, &iw, &ih);
677       const int mindx = iw * darktable.gui->ppd_thb - th->img_width;
678       const int mindy = ih * darktable.gui->ppd_thb - th->img_height;
679       if(th->zoomx > 0) th->zoomx = 0;
680       if(th->zoomx < mindx) th->zoomx = mindx;
681       if(th->zoomy > 0) th->zoomy = 0;
682       if(th->zoomy < mindy) th->zoomy = mindy;
683     }
684 
685     table->pan_x = x;
686     table->pan_y = y;
687   }
688 
689   for(GList *l = table->list; l; l = g_list_next(l))
690   {
691     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
692     dt_thumbnail_image_refresh_position(th);
693   }
694   return TRUE;
695 }
696 
_event_button_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)697 static gboolean _event_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
698 {
699   dt_culling_t *table = (dt_culling_t *)user_data;
700   table->panning = FALSE;
701   return TRUE;
702 }
703 
704 // called each time the preference change, to update specific parts
_dt_pref_change_callback(gpointer instance,gpointer user_data)705 static void _dt_pref_change_callback(gpointer instance, gpointer user_data)
706 {
707   if(!user_data) return;
708   dt_culling_t *table = (dt_culling_t *)user_data;
709 
710   dt_culling_full_redraw(table, TRUE);
711 
712   for(GList *l = table->list; l; l = g_list_next(l))
713   {
714     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
715     th->overlay_timeout_duration = dt_conf_get_int("plugins/lighttable/overlay_timeout");
716     dt_thumbnail_reload_infos(th);
717     const float zoom_ratio = th->zoom_100 > 1 ? th->zoom / th->zoom_100 : table->zoom_ratio;
718     dt_thumbnail_resize(th, th->width, th->height, TRUE, zoom_ratio);
719   }
720 }
721 
_dt_selection_changed_callback(gpointer instance,gpointer user_data)722 static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
723 {
724   if(!user_data) return;
725   dt_culling_t *table = (dt_culling_t *)user_data;
726   if(!gtk_widget_get_visible(table->widget)) return;
727 
728   // if we are in selection synchronisation mode, we exit this mode
729   if(table->selection_sync) table->selection_sync = FALSE;
730 
731   // if we are in dynamic mode, zoom = selection count
732   if(table->mode == DT_CULLING_MODE_CULLING
733      && dt_view_lighttable_get_layout(darktable.view_manager) == DT_LIGHTTABLE_LAYOUT_CULLING_DYNAMIC)
734   {
735     sqlite3_stmt *stmt;
736     int sel_count = 0;
737     DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
738                                 "SELECT count(*) "
739                                 "FROM memory.collected_images AS col, main.selected_images as sel "
740                                 "WHERE col.imgid=sel.imgid",
741                                 -1, &stmt, NULL);
742     if(sqlite3_step(stmt) == SQLITE_ROW) sel_count = sqlite3_column_int(stmt, 0);
743     sqlite3_finalize(stmt);
744     const int nz = (sel_count <= 1) ? dt_conf_get_int("plugins/lighttable/culling_num_images") : sel_count;
745     dt_view_lighttable_set_zoom(darktable.view_manager, nz);
746   }
747   // if we navigate only in the selection we just redraw to ensure no unselected image is present
748   if(table->navigate_inside_selection)
749   {
750     dt_culling_full_redraw(table, TRUE);
751     _thumbs_refocus(table);
752   }
753 }
754 
_dt_profile_change_callback(gpointer instance,int type,gpointer user_data)755 static void _dt_profile_change_callback(gpointer instance, int type, gpointer user_data)
756 {
757   if(!user_data) return;
758   dt_culling_t *table = (dt_culling_t *)user_data;
759   if(!gtk_widget_get_visible(table->widget)) return;
760 
761   for(GList *l = table->list; l; l = g_list_next(l))
762   {
763     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
764     dt_thumbnail_image_refresh(th);
765   }
766 }
767 
768 // this is called each time mouse_over id change
_dt_mouse_over_image_callback(gpointer instance,gpointer user_data)769 static void _dt_mouse_over_image_callback(gpointer instance, gpointer user_data)
770 {
771   if(!user_data) return;
772   dt_culling_t *table = (dt_culling_t *)user_data;
773   if(!gtk_widget_get_visible(table->widget)) return;
774 
775   const int imgid = dt_control_get_mouse_over_id();
776 
777   if(imgid > 0)
778   {
779     // let's be absolutely sure that the right widget has the focus
780     // otherwise accels don't work...
781     gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));
782   }
783 
784   // we crawl over all images to find the right one
785   for(GList *l = table->list; l; l = g_list_next(l))
786   {
787     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
788     // if needed, the change mouseover value of the thumb
789     if(th->mouse_over != (th->imgid == imgid)) dt_thumbnail_set_mouseover(th, (th->imgid == imgid));
790   }
791 }
792 
_dt_filmstrip_change(gpointer instance,int imgid,gpointer user_data)793 static void _dt_filmstrip_change(gpointer instance, int imgid, gpointer user_data)
794 {
795   if(!user_data || imgid <= 0) return;
796   dt_culling_t *table = (dt_culling_t *)user_data;
797   if(!gtk_widget_get_visible(table->widget)) return;
798 
799   table->offset = _thumb_get_rowid(imgid);
800   dt_culling_full_redraw(table, TRUE);
801   _thumbs_refocus(table);
802 }
803 
804 // get the class name associated with the overlays mode
_thumbs_get_overlays_class(dt_thumbnail_overlay_t over)805 static gchar *_thumbs_get_overlays_class(dt_thumbnail_overlay_t over)
806 {
807   switch(over)
808   {
809     case DT_THUMBNAIL_OVERLAYS_NONE:
810       return dt_util_dstrcat(NULL, "dt_overlays_none");
811     case DT_THUMBNAIL_OVERLAYS_HOVER_EXTENDED:
812       return dt_util_dstrcat(NULL, "dt_overlays_hover_extended");
813     case DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL:
814       return dt_util_dstrcat(NULL, "dt_overlays_always");
815     case DT_THUMBNAIL_OVERLAYS_ALWAYS_EXTENDED:
816       return dt_util_dstrcat(NULL, "dt_overlays_always_extended");
817     case DT_THUMBNAIL_OVERLAYS_MIXED:
818       return dt_util_dstrcat(NULL, "dt_overlays_mixed");
819     case DT_THUMBNAIL_OVERLAYS_HOVER_BLOCK:
820       return dt_util_dstrcat(NULL, "dt_overlays_hover_block");
821     default:
822       return dt_util_dstrcat(NULL, "dt_overlays_hover");
823   }
824 }
825 
dt_culling_new(dt_culling_mode_t mode)826 dt_culling_t *dt_culling_new(dt_culling_mode_t mode)
827 {
828   dt_culling_t *table = (dt_culling_t *)calloc(1, sizeof(dt_culling_t));
829   table->mode = mode;
830   table->zoom_ratio = IMG_TO_FIT;
831   table->widget = gtk_layout_new(NULL, NULL);
832   // TODO dt_gui_add_help_link(table->widget, dt_get_help_url("lighttable_filemanager"));
833 
834   // set css name and class
835   if(mode == DT_CULLING_MODE_PREVIEW)
836     gtk_widget_set_name(table->widget, "preview");
837   else
838     gtk_widget_set_name(table->widget, "culling");
839   GtkStyleContext *context = gtk_widget_get_style_context(table->widget);
840   if(mode == DT_CULLING_MODE_PREVIEW)
841     gtk_style_context_add_class(context, "dt_preview");
842   else
843     gtk_style_context_add_class(context, "dt_culling");
844 
845   // overlays
846   gchar *otxt = dt_util_dstrcat(NULL, "plugins/lighttable/overlays/culling/%d", table->mode);
847   table->overlays = dt_conf_get_int(otxt);
848   g_free(otxt);
849 
850   gchar *cl0 = _thumbs_get_overlays_class(table->overlays);
851   gtk_style_context_add_class(context, cl0);
852   free(cl0);
853 
854   otxt = dt_util_dstrcat(NULL, "plugins/lighttable/overlays/culling_block_timeout/%d", table->mode);
855   table->overlays_block_timeout = 2;
856   if(!dt_conf_key_exists(otxt))
857     table->overlays_block_timeout = dt_conf_get_int("plugins/lighttable/overlay_timeout");
858   else
859     table->overlays_block_timeout = dt_conf_get_int(otxt);
860   g_free(otxt);
861 
862   otxt = dt_util_dstrcat(NULL, "plugins/lighttable/tooltips/culling/%d", table->mode);
863   table->show_tooltips = dt_conf_get_bool(otxt);
864   g_free(otxt);
865 
866   // set widget signals
867   gtk_widget_set_events(table->widget, GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK
868                                            | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK
869                                            | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
870   gtk_widget_set_app_paintable(table->widget, TRUE);
871   gtk_widget_set_can_focus(table->widget, TRUE);
872 
873   g_signal_connect(G_OBJECT(table->widget), "scroll-event", G_CALLBACK(_event_scroll), table);
874   g_signal_connect(G_OBJECT(table->widget), "draw", G_CALLBACK(_event_draw), table);
875   g_signal_connect(G_OBJECT(table->widget), "leave-notify-event", G_CALLBACK(_event_leave_notify), table);
876   g_signal_connect(G_OBJECT(table->widget), "enter-notify-event", G_CALLBACK(_event_enter_notify), table);
877   g_signal_connect(G_OBJECT(table->widget), "button-press-event", G_CALLBACK(_event_button_press), table);
878   g_signal_connect(G_OBJECT(table->widget), "motion-notify-event", G_CALLBACK(_event_motion_notify), table);
879   g_signal_connect(G_OBJECT(table->widget), "button-release-event", G_CALLBACK(_event_button_release), table);
880 
881   // we register globals signals
882   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE,
883                             G_CALLBACK(_dt_mouse_over_image_callback), table);
884   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED,
885                             G_CALLBACK(_dt_profile_change_callback), table);
886   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_PREFERENCES_CHANGE, G_CALLBACK(_dt_pref_change_callback),
887                             table);
888   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE,
889                             G_CALLBACK(_dt_filmstrip_change), table);
890   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_SELECTION_CHANGED,
891                             G_CALLBACK(_dt_selection_changed_callback), table);
892 
893   g_object_ref(table->widget);
894 
895   return table;
896 }
897 
898 // initialize offset, ... values
899 // to be used when reentering culling
dt_culling_init(dt_culling_t * table,int offset)900 void dt_culling_init(dt_culling_t *table, int offset)
901 {
902   /** HOW it works :
903    *
904    * For the first image :
905    *  image_over OR first selected OR first OR -1
906    *
907    * For the navigation in selection :
908    *  culling dynamic mode                       => OFF
909    *  first image in selection AND selection > 1 => ON
910    *  otherwise                                  => OFF
911    *
912    * For the selection following :
913    *  culling dynamic mode                                    => OFF
914    *  first image(s) == selection && selection is continuous  => ON
915    */
916 
917   // init values
918   table->navigate_inside_selection = FALSE;
919   table->selection_sync = FALSE;
920   table->zoom_ratio = IMG_TO_FIT;
921 
922   // reset remaining zooming values if any
923   for(GList *l = table->list; l; l = g_list_next(l))
924   {
925     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
926     thumb->zoom = 1.0f;
927     thumb->zoomx = 0.0;
928     thumb->zoomy = 0.0;
929     thumb->img_surf_dirty = TRUE;
930   }
931 
932   const gboolean culling_dynamic
933       = (table->mode == DT_CULLING_MODE_CULLING
934          && dt_view_lighttable_get_layout(darktable.view_manager) == DT_LIGHTTABLE_LAYOUT_CULLING_DYNAMIC);
935 
936   // get first id
937   sqlite3_stmt *stmt;
938   gchar *query = NULL;
939   int first_id = -1;
940 
941   if(offset > 0)
942     first_id = _thumb_get_imgid(offset);
943   else
944     first_id = dt_control_get_mouse_over_id();
945 
946   if(first_id < 1 || culling_dynamic)
947   {
948     // search the first selected image
949     DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
950                                 "SELECT col.imgid "
951                                 "FROM memory.collected_images AS col, main.selected_images as sel "
952                                 "WHERE col.imgid=sel.imgid "
953                                 "ORDER BY col.rowid "
954                                 "LIMIT 1",
955                                 -1, &stmt, NULL);
956     if(sqlite3_step(stmt) == SQLITE_ROW) first_id = sqlite3_column_int(stmt, 0);
957     sqlite3_finalize(stmt);
958   }
959   if(first_id < 1)
960   {
961     // search the first image shown in view (this is the offset of thumbtable)
962     first_id = _thumb_get_imgid(1);
963   }
964   if(first_id < 1)
965   {
966     // Arrrghh
967     return;
968   }
969 
970   // selection count
971   int sel_count = 0;
972   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
973                               "SELECT count(*) "
974                               "FROM memory.collected_images AS col, main.selected_images as sel "
975                               "WHERE col.imgid=sel.imgid",
976                               -1, &stmt, NULL);
977   if(sqlite3_step(stmt) == SQLITE_ROW) sel_count = sqlite3_column_int(stmt, 0);
978   sqlite3_finalize(stmt);
979 
980   // special culling dynamic mode
981   if(culling_dynamic)
982   {
983     if(sel_count == 0)
984     {
985       dt_control_log(_("no image selected !"));
986       first_id = -1;
987     }
988     table->navigate_inside_selection = TRUE;
989     table->offset = _thumb_get_rowid(first_id);
990     table->offset_imgid = first_id;
991     return;
992   }
993 
994   // is first_id inside selection ?
995   gboolean inside = FALSE;
996   query = dt_util_dstrcat(NULL,
997                           "SELECT col.imgid "
998                           "FROM memory.collected_images AS col, main.selected_images AS sel "
999                           "WHERE col.imgid=sel.imgid AND col.imgid=%d",
1000                           first_id);
1001   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
1002   if(sqlite3_step(stmt) == SQLITE_ROW) inside = TRUE;
1003   sqlite3_finalize(stmt);
1004   g_free(query);
1005 
1006   if(table->mode == DT_CULLING_MODE_PREVIEW)
1007   {
1008     table->navigate_inside_selection = (sel_count > 1 && inside);
1009     table->selection_sync = (sel_count == 1 && inside);
1010   }
1011   else if(table->mode == DT_CULLING_MODE_CULLING)
1012   {
1013     const int zoom = dt_view_lighttable_get_zoom(darktable.view_manager);
1014     // we first determine if we synchronize the selection with culling images
1015     table->selection_sync = FALSE;
1016     if(sel_count == 1 && inside)
1017       table->selection_sync = TRUE;
1018     else if(sel_count == zoom && inside)
1019     {
1020       // we ensure that the selection is continuous
1021       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
1022                                   "SELECT MIN(rowid), MAX(rowid) "
1023                                   "FROM memory.collected_images AS col, main.selected_images as sel "
1024                                   "WHERE col.imgid=sel.imgid ",
1025                                   -1, &stmt, NULL);
1026       if(sqlite3_step(stmt) == SQLITE_ROW)
1027       {
1028         if(sqlite3_column_int(stmt, 0) + sel_count - 1 == sqlite3_column_int(stmt, 1))
1029         {
1030           table->selection_sync = TRUE;
1031         }
1032       }
1033       sqlite3_finalize(stmt);
1034     }
1035 
1036     // we now determine if we limit culling images to the selection
1037     table->navigate_inside_selection = (!table->selection_sync && inside);
1038   }
1039 
1040   table->offset = _thumb_get_rowid(first_id);
1041   table->offset_imgid = first_id;
1042 }
1043 
_thumbs_prefetch(dt_culling_t * table)1044 static void _thumbs_prefetch(dt_culling_t *table)
1045 {
1046   if(!table->list) return;
1047 
1048   // get the mip level by using the max image size actually shown
1049   int maxw = 0;
1050   int maxh = 0;
1051   for(GList *l = table->list; l; l = g_list_next(l))
1052   {
1053     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
1054     maxw = MAX(maxw, th->width);
1055     maxh = MAX(maxh, th->height);
1056   }
1057   dt_mipmap_size_t mip = dt_mipmap_cache_get_matching_size(darktable.mipmap_cache, maxw, maxh);
1058 
1059   // prefetch next image
1060   gchar *query;
1061   sqlite3_stmt *stmt;
1062   dt_thumbnail_t *last = (dt_thumbnail_t *)g_list_last(table->list)->data;
1063   if(table->navigate_inside_selection)
1064   {
1065     query
1066         = dt_util_dstrcat(NULL,
1067                           "SELECT m.imgid "
1068                           "FROM memory.collected_images AS m, main.selected_images AS s "
1069                           "WHERE m.imgid = s.imgid"
1070                           " AND m.rowid > (SELECT mm.rowid FROM memory.collected_images AS mm WHERE mm.imgid=%d) "
1071                           "ORDER BY m.rowid "
1072                           "LIMIT 1",
1073                           last->imgid);
1074   }
1075   else
1076   {
1077     query
1078         = dt_util_dstrcat(NULL,
1079                           "SELECT m.imgid "
1080                           "FROM memory.collected_images AS m "
1081                           "WHERE m.rowid > (SELECT mm.rowid FROM memory.collected_images AS mm WHERE mm.imgid=%d) "
1082                           "ORDER BY m.rowid "
1083                           "LIMIT 1",
1084                           last->imgid);
1085   }
1086   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
1087   if(sqlite3_step(stmt) == SQLITE_ROW)
1088   {
1089     const int id = sqlite3_column_int(stmt, 0);
1090     if(id > 0) dt_mipmap_cache_get(darktable.mipmap_cache, NULL, id, mip, DT_MIPMAP_PREFETCH, 'r');
1091   }
1092   sqlite3_finalize(stmt);
1093   g_free(query);
1094 
1095   // prefetch previous image
1096   dt_thumbnail_t *prev = (dt_thumbnail_t *)(table->list)->data;
1097   if(table->navigate_inside_selection)
1098   {
1099     query
1100         = dt_util_dstrcat(NULL,
1101                           "SELECT m.imgid "
1102                           "FROM memory.collected_images AS m, main.selected_images AS s "
1103                           "WHERE m.imgid = s.imgid"
1104                           " AND m.rowid < (SELECT mm.rowid FROM memory.collected_images AS mm WHERE mm.imgid=%d) "
1105                           "ORDER BY m.rowid DESC "
1106                           "LIMIT 1",
1107                           prev->imgid);
1108   }
1109   else
1110   {
1111     query
1112         = dt_util_dstrcat(NULL,
1113                           "SELECT m.imgid "
1114                           "FROM memory.collected_images AS m "
1115                           "WHERE m.rowid < (SELECT mm.rowid FROM memory.collected_images AS mm WHERE mm.imgid=%d) "
1116                           "ORDER BY m.rowid DESC "
1117                           "LIMIT 1",
1118                           prev->imgid);
1119   }
1120   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
1121   if(sqlite3_step(stmt) == SQLITE_ROW)
1122   {
1123     const int id = sqlite3_column_int(stmt, 0);
1124     if(id > 0) dt_mipmap_cache_get(darktable.mipmap_cache, NULL, id, mip, DT_MIPMAP_PREFETCH, 'r');
1125   }
1126   sqlite3_finalize(stmt);
1127 }
1128 
_thumbs_recreate_list_at(dt_culling_t * table,const int offset)1129 static gboolean _thumbs_recreate_list_at(dt_culling_t *table, const int offset)
1130 {
1131   gchar *query = NULL;
1132   sqlite3_stmt *stmt;
1133 
1134   if(table->navigate_inside_selection)
1135   {
1136     query = dt_util_dstrcat(NULL,
1137                             "SELECT m.rowid, m.imgid, b.aspect_ratio "
1138                             "FROM memory.collected_images AS m, main.selected_images AS s, images AS b "
1139                             "WHERE m.imgid = b.id AND m.imgid = s.imgid AND m.rowid >= %d "
1140                             "ORDER BY m.rowid "
1141                             "LIMIT %d",
1142                             offset, table->thumbs_count);
1143   }
1144   else
1145   {
1146     query = dt_util_dstrcat(NULL,
1147                             "SELECT m.rowid, m.imgid, b.aspect_ratio "
1148                             "FROM (SELECT rowid, imgid "
1149                             "FROM memory.collected_images "
1150                             "WHERE rowid < %d + %d "
1151                             "ORDER BY rowid DESC "
1152                             "LIMIT %d) AS m, "
1153                             "images AS b "
1154                             "WHERE m.imgid = b.id "
1155                             "ORDER BY m.rowid",
1156                             offset, table->thumbs_count, table->thumbs_count);
1157   }
1158 
1159   GList *newlist = NULL;
1160   int nbnew = 0;
1161   int pos = 0;
1162   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
1163   while(sqlite3_step(stmt) == SQLITE_ROW && g_list_shorter_than(newlist, table->thumbs_count+1))
1164   {
1165     const int nrow = sqlite3_column_int(stmt, 0);
1166     const int nid = sqlite3_column_int(stmt, 1);
1167     // first, we search if the thumb is already here
1168     GList *tl = g_list_find_custom(table->list, GINT_TO_POINTER(nid), _list_compare_by_imgid);
1169     if(tl)
1170     {
1171       dt_thumbnail_t *thumb = (dt_thumbnail_t *)tl->data;
1172       thumb->rowid = nrow; // this may have changed
1173       thumb->display_focus = table->focus;
1174       newlist = g_list_prepend(newlist, thumb);
1175       // and we remove the thumb from the old list
1176       table->list = g_list_remove(table->list, thumb);
1177     }
1178     else
1179     {
1180       // we create a completely new thumb
1181       // we set its size to the thumb it replace in the list if any otherwise we set it to something > 0 to trigger
1182       // draw events
1183       int nw = 40;
1184       int nh = 40;
1185       if(table->mode == DT_CULLING_MODE_PREVIEW)
1186       {
1187         nw = table->view_width;
1188         nh = table->view_height;
1189       }
1190       else if(table->list)
1191       {
1192         dt_thumbnail_t *th_model
1193             = (dt_thumbnail_t *)g_list_nth_data(table->list, MIN(pos, g_list_length(table->list) - 1));
1194         nw = th_model->width;
1195         nh = th_model->height;
1196       }
1197       else if(newlist)
1198       {
1199         dt_thumbnail_t *th_model = (dt_thumbnail_t *)newlist->data;  // get most recently added
1200         nw = th_model->width;
1201         nh = th_model->height;
1202       }
1203       dt_thumbnail_t *thumb;
1204       if(table->mode == DT_CULLING_MODE_PREVIEW)
1205         thumb = dt_thumbnail_new(nw, nh, table->zoom_ratio, nid, nrow, table->overlays,
1206                                  DT_THUMBNAIL_CONTAINER_PREVIEW, table->show_tooltips);
1207       else
1208         thumb = dt_thumbnail_new(nw, nh, table->zoom_ratio, nid, nrow, table->overlays,
1209                                  DT_THUMBNAIL_CONTAINER_CULLING, table->show_tooltips);
1210 
1211       thumb->display_focus = table->focus;
1212       thumb->sel_mode = DT_THUMBNAIL_SEL_MODE_DISABLED;
1213       float aspect_ratio = sqlite3_column_double(stmt, 2);
1214       if(!aspect_ratio || aspect_ratio < 0.0001f)
1215       {
1216         aspect_ratio = dt_image_set_aspect_ratio(nid, FALSE);
1217         // if an error occurs, let's use 1:1 value
1218         if(aspect_ratio < 0.0001f) aspect_ratio = 1.0f;
1219       }
1220       thumb->aspect_ratio = aspect_ratio;
1221       newlist = g_list_prepend(newlist, thumb);
1222       nbnew++;
1223     }
1224     // if it's the offset, we record the imgid
1225     if(nrow == table->offset) table->offset_imgid = nid;
1226     pos++;
1227   }
1228   newlist = g_list_reverse(newlist); // list was built in reverse order, so un-reverse it
1229 
1230   // in rare cases, we can have less images than wanted
1231   // although there's images before (this shouldn't happen in preview)
1232   if(table->navigate_inside_selection && g_list_shorter_than(newlist, table->thumbs_count)
1233      && g_list_shorter_than(newlist, _get_selection_count()))
1234   {
1235     const int nb = table->thumbs_count - g_list_length(newlist);
1236     query = dt_util_dstrcat(NULL,
1237                             "SELECT m.rowid, m.imgid, b.aspect_ratio "
1238                             "FROM memory.collected_images AS m, main.selected_images AS s, images AS b "
1239                             "WHERE m.imgid = b.id AND m.imgid = s.imgid AND m.rowid < %d "
1240                             "ORDER BY m.rowid DESC "
1241                             "LIMIT %d",
1242                             offset, nb);
1243     DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
1244     if(stmt != NULL)
1245     {
1246       pos = 0;
1247       while(sqlite3_step(stmt) == SQLITE_ROW && g_list_shorter_than(newlist, table->thumbs_count+1))
1248       {
1249         const int nrow = sqlite3_column_int(stmt, 0);
1250         const int nid = sqlite3_column_int(stmt, 1);
1251         // first, we search if the thumb is already here
1252         GList *tl = g_list_find_custom(table->list, GINT_TO_POINTER(nid), _list_compare_by_imgid);
1253         if(tl)
1254         {
1255           dt_thumbnail_t *thumb = (dt_thumbnail_t *)tl->data;
1256           thumb->rowid = nrow; // this may have changed
1257           newlist = g_list_prepend(newlist, thumb);
1258           // and we remove the thumb from the old list
1259           table->list = g_list_remove(table->list, thumb);
1260         }
1261         else
1262         {
1263           // we create a completely new thumb
1264           // we set its size to the thumb it replace in the list if any otherwise we set it to something > 0 to
1265           // trigger draw events
1266           int nw = 40;
1267           int nh = 40;
1268           if(table->list)
1269           {
1270             dt_thumbnail_t *th_model = (dt_thumbnail_t *)(table->list)->data;
1271             nw = th_model->width;
1272             nh = th_model->height;
1273           }
1274           else if(newlist)
1275           {
1276             dt_thumbnail_t *th_model = (dt_thumbnail_t *)newlist->data;
1277             nw = th_model->width;
1278             nh = th_model->height;
1279           }
1280           dt_thumbnail_t *thumb;
1281           if(table->mode == DT_CULLING_MODE_PREVIEW)
1282             thumb = dt_thumbnail_new(nw, nh, table->zoom_ratio, nid, nrow, table->overlays,
1283                                      DT_THUMBNAIL_CONTAINER_PREVIEW, table->show_tooltips);
1284           else
1285             thumb = dt_thumbnail_new(nw, nh, table->zoom_ratio, nid, nrow, table->overlays,
1286                                      DT_THUMBNAIL_CONTAINER_CULLING, table->show_tooltips);
1287 
1288           thumb->display_focus = table->focus;
1289           thumb->sel_mode = DT_THUMBNAIL_SEL_MODE_DISABLED;
1290           float aspect_ratio = sqlite3_column_double(stmt, 2);
1291           if(!aspect_ratio || aspect_ratio < 0.0001f)
1292           {
1293             aspect_ratio = dt_image_set_aspect_ratio(nid, FALSE);
1294             // if an error occurs, let's use 1:1 value
1295             if(aspect_ratio < 0.0001f) aspect_ratio = 1.0f;
1296           }
1297           thumb->aspect_ratio = aspect_ratio;
1298           newlist = g_list_prepend(newlist, thumb);
1299           nbnew++;
1300         }
1301         // if it's the offset, we record the imgid
1302         if(nrow == table->offset) table->offset_imgid = nid;
1303         pos++;
1304       }
1305       sqlite3_finalize(stmt);
1306     }
1307     g_free(query);
1308   }
1309 
1310   // now we cleanup all remaining thumbs from old table->list and set it again
1311   g_list_free_full(table->list, _list_remove_thumb);
1312   table->list = newlist;
1313 
1314   // and we ensure that we have the right offset
1315   if(table->list)
1316   {
1317     dt_thumbnail_t *thumb = (dt_thumbnail_t *)table->list->data;
1318     table->offset_imgid = thumb->imgid;
1319     table->offset = _thumb_get_rowid(thumb->imgid);
1320   }
1321   return TRUE;
1322 }
1323 
_thumbs_compute_positions(dt_culling_t * table)1324 static gboolean _thumbs_compute_positions(dt_culling_t *table)
1325 {
1326   if(!gtk_widget_get_visible(table->widget)) return FALSE;
1327   if(!table->list) return FALSE;
1328 
1329   // if we have only 1 image, it should take the entire screen
1330   if(g_list_is_singleton(table->list))
1331   {
1332     dt_thumbnail_t *thumb = (dt_thumbnail_t *)table->list->data;
1333     thumb->width = table->view_width;
1334     thumb->height = table->view_height;
1335     thumb->x = 0;
1336     thumb->y = 0;
1337     return TRUE;
1338   }
1339 
1340   int sum_w = 0, max_h = 0, max_w = 0;
1341 
1342   unsigned int total_width = 0, total_height = 0;
1343   int distance = 1;
1344   float avg_ratio = 0;
1345 
1346   // reinit size and positions and get max values
1347   int count = 0;
1348   for(GList *l = table->list; l; l = g_list_next(l))
1349   {
1350     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1351     const float aspect_ratio = thumb->aspect_ratio;
1352     thumb->width = (gint)(sqrt(aspect_ratio) * 100);
1353     thumb->height = (gint)(1 / sqrt(aspect_ratio) * 100);
1354     thumb->x = thumb->y = 0;
1355 
1356     sum_w += thumb->width;
1357     max_w = MAX(max_w, thumb->width);
1358     max_h = MAX(max_h, thumb->height);
1359     avg_ratio += thumb->width / (float)thumb->height;
1360     count++;
1361   }
1362 
1363   avg_ratio /= count;
1364 
1365   int per_row, tmp_per_row, per_col, tmp_per_col;
1366   per_row = tmp_per_row = ceil(sqrt(count));
1367   per_col = tmp_per_col = (count + per_row - 1) / per_row;
1368 
1369   float tmp_slot_ratio, slot_ratio;
1370   tmp_slot_ratio = slot_ratio = (table->view_width / (float)per_row) / (table->view_height / (float)per_col);
1371 
1372   do
1373   {
1374     per_row = tmp_per_row;
1375     per_col = tmp_per_col;
1376     slot_ratio = tmp_slot_ratio;
1377 
1378     if(avg_ratio > slot_ratio)
1379     {
1380       tmp_per_row = per_row - 1;
1381     }
1382     else
1383     {
1384       tmp_per_row = per_row + 1;
1385     }
1386 
1387     if(tmp_per_row == 0) break;
1388 
1389     tmp_per_col = (count + tmp_per_row - 1) / tmp_per_row;
1390 
1391     tmp_slot_ratio = (table->view_width / (float)tmp_per_row) / (table->view_height / (float)tmp_per_col);
1392 
1393   } while(per_row > 0 && per_row <= count && _absmul(tmp_slot_ratio, avg_ratio) < _absmul(slot_ratio, avg_ratio));
1394 
1395   GList *slots = NULL;
1396 
1397   // Vertical layout
1398   for(GList *l = table->list; l; l = g_list_next(l))
1399   {
1400     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1401     GList *slot_iter = slots;
1402     for(; slot_iter; slot_iter = slot_iter->next)
1403     {
1404       GList *slot = (GList *)slot_iter->data;
1405       // Calculate current total height of slot
1406       int slot_h = distance;
1407       for(GList *slot_cw_iter = slot; slot_cw_iter; slot_cw_iter = g_list_next(slot_cw_iter))
1408       {
1409         dt_thumbnail_t *slot_cw = (dt_thumbnail_t *)slot_cw_iter->data;
1410         slot_h = slot_h + slot_cw->height + distance;
1411       }
1412       // Add window to slot if the slot height after adding the window
1413       // doesn't exceed max window height
1414       if(slot_h + distance + thumb->height < max_h)
1415       {
1416         slot_iter->data = g_list_append(slot, thumb);
1417         break;
1418       }
1419     }
1420     // Otherwise, create a new slot with only this window
1421     if(!slot_iter) slots = g_list_prepend(slots, g_list_prepend(NULL, thumb));
1422   }
1423   slots = g_list_reverse(slots);  // list was built in reverse order, so un-reverse it
1424 
1425   GList *rows = g_list_append(NULL, NULL);
1426   {
1427     int row_y = 0, x = 0, row_h = 0;
1428     int max_row_w = sum_w / per_col;
1429     for(GList *slot_iter = slots; slot_iter; slot_iter = g_list_next(slot_iter))
1430     {
1431       GList *slot = (GList *)slot_iter->data;
1432 
1433       // Max width of windows in the slot
1434       int slot_max_w = 0;
1435       for(GList *slot_cw_iter = slot; slot_cw_iter; slot_cw_iter = g_list_next(slot_cw_iter))
1436       {
1437         dt_thumbnail_t *cw = (dt_thumbnail_t *)slot_cw_iter->data;
1438         slot_max_w = MAX(slot_max_w, cw->width);
1439       }
1440 
1441       int y = row_y;
1442       for(GList *slot_cw_iter = slot; slot_cw_iter; slot_cw_iter = g_list_next(slot_cw_iter))
1443       {
1444         dt_thumbnail_t *cw = (dt_thumbnail_t *)slot_cw_iter->data;
1445         cw->x = x + (slot_max_w - cw->width) / 2;
1446         cw->y = y;
1447         y += cw->height + distance;
1448         rows->data = g_list_append(rows->data, cw);
1449       }
1450 
1451       row_h = MAX(row_h, y - row_y);
1452       total_height = MAX(total_height, y);
1453       x += slot_max_w + distance;
1454       total_width = MAX(total_width, x);
1455 
1456       if(x > max_row_w)
1457       {
1458         x = 0;
1459         row_y += row_h;
1460         row_h = 0;
1461         rows = g_list_append(rows, 0);
1462         rows = rows->next;              // keep rows pointing at last element to avoid quadratic runtime
1463       }
1464       g_list_free(slot);
1465     }
1466     g_list_free(slots);
1467     slots = NULL;
1468   }
1469 
1470   rows = g_list_first(rows); // rows points at the last element of the constructed list, so move it back to the start
1471 
1472   total_width -= distance;
1473   total_height -= distance;
1474 
1475   for(const GList *iter = rows; iter; iter = g_list_next(iter))
1476   {
1477     GList *row = (GList *)iter->data;
1478     int row_w = 0, xoff;
1479     int max_rh = 0;
1480 
1481     for(GList *slot_cw_iter = row; slot_cw_iter; slot_cw_iter = g_list_next(slot_cw_iter))
1482     {
1483       dt_thumbnail_t *cw = (dt_thumbnail_t *)slot_cw_iter->data;
1484       row_w = MAX(row_w, cw->x + cw->width);
1485       max_rh = MAX(max_rh, cw->height);
1486     }
1487 
1488     xoff = (total_width - row_w) / 2;
1489 
1490     for(GList *cw_iter = row; cw_iter; cw_iter = g_list_next(cw_iter))
1491     {
1492       dt_thumbnail_t *cw = (dt_thumbnail_t *)cw_iter->data;
1493       cw->x += xoff;
1494       cw->height = max_rh;
1495     }
1496     g_list_free(row);
1497   }
1498 
1499   g_list_free(rows);
1500 
1501   float factor;
1502   factor = (float)(table->view_width - 1) / total_width;
1503   if(factor * total_height > table->view_height - 1) factor = (float)(table->view_height - 1) / total_height;
1504 
1505   int xoff = (table->view_width - (float)total_width * factor) / 2;
1506   int yoff = (table->view_height - (float)total_height * factor) / 2;
1507 
1508   for(GList *l = table->list; l; l = g_list_next(l))
1509   {
1510     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1511     thumb->width = thumb->width * factor;
1512     thumb->height = thumb->height * factor;
1513     thumb->x = thumb->x * factor + xoff;
1514     thumb->y = thumb->y * factor + yoff;
1515   }
1516 
1517   // we save the current first id
1518   dt_conf_set_int("plugins/lighttable/culling_last_id", table->offset_imgid);
1519 
1520   return TRUE;
1521 }
1522 
dt_culling_update_active_images_list(dt_culling_t * table)1523 void dt_culling_update_active_images_list(dt_culling_t *table)
1524 {
1525   // we erase the list of active images
1526   g_slist_free(darktable.view_manager->active_images);
1527   darktable.view_manager->active_images = NULL;
1528 
1529   // and we effectively move and resize thumbs
1530   for(GList *l = table->list; l; l = g_list_next(l))
1531   {
1532     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1533     // we update the active images list
1534     darktable.view_manager->active_images
1535         = g_slist_append(darktable.view_manager->active_images, GINT_TO_POINTER(thumb->imgid));
1536   }
1537 
1538   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
1539 }
1540 
1541 // recreate the list of thumb if needed and recomputes sizes and positions if needed
dt_culling_full_redraw(dt_culling_t * table,gboolean force)1542 void dt_culling_full_redraw(dt_culling_t *table, gboolean force)
1543 {
1544   if(!gtk_widget_get_visible(table->widget)) return;
1545   const double start = dt_get_wtime();
1546   // first, we see if we need to do something
1547   if(!_compute_sizes(table, force)) return;
1548 
1549   // we store first image zoom and pos for new ones
1550   float old_zx = 0.0;
1551   float old_zy = 0.0;
1552   int old_margin_x = 0;
1553   int old_margin_y = 0;
1554   if(table->list)
1555   {
1556     dt_thumbnail_t *thumb = (dt_thumbnail_t *)table->list->data;
1557     old_zx = thumb->zoomx;
1558     old_zy = thumb->zoomy;
1559     old_margin_x = gtk_widget_get_margin_start(thumb->w_image_box);
1560     old_margin_y = gtk_widget_get_margin_top(thumb->w_image_box);
1561   }
1562   // we recreate the list of images
1563   _thumbs_recreate_list_at(table, table->offset);
1564 
1565   // we compute the sizes and positions of thumbs
1566   _thumbs_compute_positions(table);
1567 
1568   // we erase the list of active images
1569   g_slist_free(darktable.view_manager->active_images);
1570   darktable.view_manager->active_images = NULL;
1571 
1572   // and we effectively move and resize thumbs
1573   for(GList *l = table->list; l; l = g_list_next(l))
1574   {
1575     dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1576     // we set the overlays timeout
1577     thumb->overlay_timeout_duration = table->overlays_block_timeout;
1578     // we add or move the thumb at the right position
1579     if(!gtk_widget_get_parent(thumb->w_main))
1580     {
1581       gtk_widget_set_margin_start(thumb->w_image_box, old_margin_x);
1582       gtk_widget_set_margin_top(thumb->w_image_box, old_margin_y);
1583       // and we resize the thumb
1584       dt_thumbnail_resize(thumb, thumb->width, thumb->height, FALSE, table->zoom_ratio);
1585       gtk_layout_put(GTK_LAYOUT(table->widget), thumb->w_main, thumb->x, thumb->y);
1586       thumb->zoomx = old_zx;
1587       thumb->zoomy = old_zy;
1588     }
1589     else
1590     {
1591       gtk_layout_move(GTK_LAYOUT(table->widget), thumb->w_main, thumb->x, thumb->y);
1592       // and we resize the thumb
1593       const float zoom_ratio = thumb->zoom_100 > 1 ? thumb->zoom / thumb->zoom_100 : IMG_TO_FIT;
1594       dt_thumbnail_resize(thumb, thumb->width, thumb->height, FALSE, zoom_ratio);
1595     }
1596 
1597     // we update the active images list
1598     darktable.view_manager->active_images
1599         = g_slist_append(darktable.view_manager->active_images, GINT_TO_POINTER(thumb->imgid));
1600   }
1601 
1602   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
1603 
1604   // if the selection should follow active images
1605   if(table->selection_sync)
1606   {
1607     // deactivate selection_change event
1608     table->select_desactivate = TRUE;
1609     // deselect all
1610     DT_DEBUG_SQLITE3_EXEC(dt_database_get(darktable.db), "DELETE FROM main.selected_images", NULL, NULL, NULL);
1611     // select all active images
1612     GList *ls = NULL;
1613     for (GList *l = table->list; l; l = g_list_next(l))
1614     {
1615       dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1616       ls = g_list_prepend(ls, GINT_TO_POINTER(thumb->imgid));
1617     }
1618     ls = g_list_reverse(ls);  // list was built in reverse order, so un-reverse it
1619     dt_selection_select_list(darktable.selection, ls);
1620     g_list_free(ls);
1621     // reactivate selection_change event
1622     table->select_desactivate = FALSE;
1623   }
1624 
1625   // we prefetch next/previous images
1626   _thumbs_prefetch(table);
1627 
1628   // ensure one of the shown image as the focus (to avoid to keep focus to hidden image)
1629   const int selid = dt_control_get_mouse_over_id();
1630   if(selid >= 0)
1631   {
1632     gboolean in_list = FALSE;
1633     for(GList *l = table->list; l; l = g_list_next(l))
1634     {
1635       dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
1636       if(thumb->imgid == selid)
1637       {
1638         in_list = TRUE;
1639         break;
1640       }
1641     }
1642     if(!in_list && table->list)
1643     {
1644       dt_thumbnail_t *thumb = (dt_thumbnail_t *)table->list->data;
1645       dt_control_set_mouse_over_id(thumb->imgid);
1646     }
1647   }
1648 
1649   // be sure the focus is in the right widget (needed for accels)
1650   gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));
1651 
1652   dt_print(DT_DEBUG_LIGHTTABLE, "done in %0.04f sec\n", dt_get_wtime() - start);
1653 
1654   if(darktable.unmuted & DT_DEBUG_CACHE) dt_mipmap_cache_print(darktable.mipmap_cache);
1655 }
1656 
dt_culling_key_move(dt_culling_t * table,dt_culling_move_t move)1657 gboolean dt_culling_key_move(dt_culling_t *table, dt_culling_move_t move)
1658 {
1659   int val = 0;
1660   switch(move)
1661   {
1662     case DT_CULLING_MOVE_LEFT:
1663     case DT_CULLING_MOVE_UP:
1664       val = -1;
1665       break;
1666     case DT_CULLING_MOVE_RIGHT:
1667     case DT_CULLING_MOVE_DOWN:
1668       val = 1;
1669       break;
1670     case DT_CULLING_MOVE_PAGEUP:
1671       val = -1 * table->thumbs_count;
1672       break;
1673     case DT_CULLING_MOVE_PAGEDOWN:
1674       val = table->thumbs_count;
1675       break;
1676     case DT_CULLING_MOVE_START:
1677       val = -1 * INT_MAX;
1678       break;
1679     case DT_CULLING_MOVE_END:
1680       val = INT_MAX;
1681       break;
1682     default:
1683       val = 0;
1684       break;
1685   }
1686   _thumbs_move(table, val);
1687   return TRUE;
1688 }
1689 
dt_culling_change_offset_image(dt_culling_t * table,int imgid)1690 void dt_culling_change_offset_image(dt_culling_t *table, int imgid)
1691 {
1692   table->offset = _thumb_get_rowid(imgid);
1693   dt_culling_full_redraw(table, TRUE);
1694   _thumbs_refocus(table);
1695 }
1696 
dt_culling_zoom_max(dt_culling_t * table)1697 void dt_culling_zoom_max(dt_culling_t *table)
1698 {
1699   float x = 0;
1700   float y = 0;
1701   if(table->mode == DT_CULLING_MODE_PREVIEW && table->list)
1702   {
1703     dt_thumbnail_t *th = (dt_thumbnail_t *)table->list->data;
1704     x = gtk_widget_get_allocated_width(th->w_image_box) / 2.0;
1705     y = gtk_widget_get_allocated_height(th->w_image_box) / 2.0;
1706   }
1707   _thumbs_zoom_add(table, ZOOM_MAX, x, y, 0);
1708 }
1709 
dt_culling_zoom_fit(dt_culling_t * table)1710 void dt_culling_zoom_fit(dt_culling_t *table)
1711 {
1712   table->zoom_ratio = IMG_TO_FIT;
1713   for(GList *l = table->list; l; l = g_list_next(l))
1714   {
1715     _zoom_thumb_fit((dt_thumbnail_t *)l->data);
1716   }
1717 }
1718 
1719 // change the type of overlays that should be shown
dt_culling_set_overlays_mode(dt_culling_t * table,dt_thumbnail_overlay_t over)1720 void dt_culling_set_overlays_mode(dt_culling_t *table, dt_thumbnail_overlay_t over)
1721 {
1722   if(!table) return;
1723   gchar *txt = dt_util_dstrcat(NULL, "plugins/lighttable/overlays/culling/%d", table->mode);
1724   dt_conf_set_int(txt, over);
1725   g_free(txt);
1726   gchar *cl0 = _thumbs_get_overlays_class(table->overlays);
1727   gchar *cl1 = _thumbs_get_overlays_class(over);
1728 
1729   GtkStyleContext *context = gtk_widget_get_style_context(table->widget);
1730   gtk_style_context_remove_class(context, cl0);
1731   gtk_style_context_add_class(context, cl1);
1732 
1733   txt = dt_util_dstrcat(NULL, "plugins/lighttable/overlays/culling_block_timeout/%d", table->mode);
1734   int timeout = 2;
1735   if(!dt_conf_key_exists(txt))
1736     timeout = dt_conf_get_int("plugins/lighttable/overlay_timeout");
1737   else
1738     timeout = dt_conf_get_int(txt);
1739   g_free(txt);
1740 
1741   txt = dt_util_dstrcat(NULL, "plugins/lighttable/tooltips/culling/%d", table->mode);
1742   table->show_tooltips = dt_conf_get_bool(txt);
1743   g_free(txt);
1744 
1745   // we need to change the overlay content if we pass from normal to extended overlays
1746   // this is not done on the fly with css to avoid computing extended msg for nothing and to reserve space if needed
1747   for(GList *l = table->list; l; l = g_list_next(l))
1748   {
1749     dt_thumbnail_t *th = (dt_thumbnail_t *)l->data;
1750     dt_thumbnail_set_overlay(th, over, timeout);
1751     th->tooltip = table->show_tooltips;
1752     // and we resize the bottom area
1753     const float zoom_ratio = th->zoom_100 > 1 ? th->zoom / th->zoom_100 : table->zoom_ratio;
1754     dt_thumbnail_resize(th, th->width, th->height, TRUE, zoom_ratio);
1755   }
1756 
1757   table->overlays = over;
1758   g_free(cl0);
1759   g_free(cl1);
1760 }
1761 
1762 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
1763 // vim: shiftwidth=2 expandtab tabstop=2 cindent
1764 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1765