Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
thumbtable.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2020-2022 Aldric Renaudin.
4 Copyright (C) 2020 Bill Ferguson.
5 Copyright (C) 2020 Chris Elston.
6 Copyright (C) 2020-2022 Diederik Ter Rahe.
7 Copyright (C) 2020, 2022 Hanno Schwalm.
8 Copyright (C) 2020 Heiko Bauke.
9 Copyright (C) 2020 Hubert Kowalski.
10 Copyright (C) 2020-2021 Pascal Obry.
11 Copyright (C) 2020, 2022 Philippe Weyland.
12 Copyright (C) 2020-2021 Ralf Brown.
13 Copyright (C) 2021 Dan Torop.
14 Copyright (C) 2021 domosbg.
15 Copyright (C) 2021 Erkan Ozgur Yilmaz.
16 Copyright (C) 2021 Fabio Heer.
17 Copyright (C) 2021 luzpaz.
18 Copyright (C) 2022-2023, 2025-2026 Aurélien PIERRE.
19 Copyright (C) 2022 Martin Bařinka.
20 Copyright (C) 2022 Miloš Komarčević.
21 Copyright (C) 2022 Nicolas Auffray.
22 Copyright (C) 2022 solarer.
23 Copyright (C) 2022 Victor Forsiuk.
24 Copyright (C) 2023 Luca Zulberti.
25 Copyright (C) 2023 Ricky Moon.
26 Copyright (C) 2025-2026 Guillaume Stutin.
27
28 darktable is free software: you can redistribute it and/or modify
29 it under the terms of the GNU General Public License as published by
30 the Free Software Foundation, either version 3 of the License, or
31 (at your option) any later version.
32
33 darktable is distributed in the hope that it will be useful,
34 but WITHOUT ANY WARRANTY; without even the implied warranty of
35 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36 GNU General Public License for more details.
37
38 You should have received a copy of the GNU General Public License
39 along with darktable. If not, see <http://www.gnu.org/licenses/>.
40*/
43#include "common/darktable.h"
44#include "gui/gdkkeys.h"
45#include "dtgtk/thumbtable.h"
46#include "dtgtk/thumbnail.h"
48#include "common/collection.h"
49#include "common/colorlabels.h"
50#include "common/history.h"
51#include "common/image_cache.h"
52#include "common/grouping.h"
53#include "common/ratings.h"
54#include "common/selection.h"
55#include "common/undo.h"
56#include "control/control.h"
58
59#include "gui/accelerators.h"
60#include "gui/drag_and_drop.h"
61#include "views/view.h"
62#include "bauhaus/bauhaus.h"
63
64#ifdef GDK_WINDOWING_QUARTZ
65#include "osx/osx.h"
66#endif
67
68#include <glib-object.h>
69#include <math.h>
70
71
73{
75
76 dt_thumbtable_t *src = NULL;
79 else if(darktable.gui->ui->thumbtable_filmstrip == dst)
81
82 if(IS_NULL_PTR(src) || src == dst) return FALSE;
83
85 const gboolean can_clone = (src->lut
86 && src->collection_inited
87 && src->collection_hash == dst->collection_hash
88 && src->collapse_groups == dst->collapse_groups
89 && src->collection_count > 0);
90 if(!can_clone)
91 {
93 return FALSE;
94 }
95
96 const uint32_t count = src->collection_count;
97 dt_thumbtable_cache_t *cloned_lut = malloc(count * sizeof(dt_thumbtable_cache_t));
98 if(IS_NULL_PTR(cloned_lut))
99 {
101 return FALSE;
102 }
103
104 memcpy(cloned_lut, src->lut, count * sizeof(dt_thumbtable_cache_t));
106
107 for(uint32_t i = 0; i < count; i++)
108 cloned_lut[i].thumb = NULL;
109
110 dt_thumbtable_cache_t *old_lut = NULL;
112 old_lut = dst->lut;
113 dst->lut = cloned_lut;
114 dst->collection_count = count;
115 dst->collection_inited = TRUE;
117
118 dt_free(old_lut);
119 return TRUE;
120}
121
162
163static gboolean _thumbtable_idle_update(gpointer user_data);
165static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data);
166static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data);
167static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data);
168static void _scrollbar_widget_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data);
169
170static gint _thumb_compare_rowid_desc(gconstpointer a, gconstpointer b)
171{
172 const dt_thumbnail_t *thumb_a = (const dt_thumbnail_t *)a;
173 const dt_thumbnail_t *thumb_b = (const dt_thumbnail_t *)b;
174
175 if(thumb_a->rowid < thumb_b->rowid) return 1;
176 if(thumb_a->rowid > thumb_b->rowid) return -1;
177 return 0;
178}
179
180static int _grab_focus(dt_thumbtable_t *table)
181{
182 if(IS_NULL_PTR(table)) return 0;
183 table->focus_idle_id = 0;
184
186 {
187 GtkWidget *focused = NULL;
188 GtkWidget *toplevel = gtk_widget_get_toplevel(table->grid);
189 if(!IS_NULL_PTR(toplevel) && GTK_IS_WINDOW(toplevel))
190 focused = gtk_window_get_focus(GTK_WINDOW(toplevel));
191
192 // Grab focus here otherwise, on first click over the grid,
193 // scrolled window gets scrolled all the way to the top and it's annoying.
194 // This can work only if the grid is mapped and realized, which we ensure
195 // by wrapping that in a g_idle() method.
196 if(IS_NULL_PTR(focused) || (!GTK_IS_EDITABLE(focused) && !GTK_IS_TEXT_VIEW(focused)))
197 gtk_widget_grab_focus(table->grid);
198 }
200 return 0;
201}
202
203static void _thumbtable_schedule_focus(dt_thumbtable_t *table, const gint priority)
204{
205 if(IS_NULL_PTR(table) || table->focus_idle_id) return;
206 table->focus_idle_id = g_idle_add_full(priority, (GSourceFunc)_grab_focus, table, NULL);
207}
208
209static gboolean _thumbtable_idle_update(gpointer user_data)
210{
211 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
212 if(IS_NULL_PTR(table)) return G_SOURCE_REMOVE;
213
214 table->idle_update_id = 0;
217 if(table->thumb_nb == 0) gtk_widget_queue_draw(table->grid);
218 return G_SOURCE_REMOVE;
219}
220
222{
223 if(IS_NULL_PTR(table)) return;
224 if(table->scroll_window && !gtk_widget_is_visible(table->scroll_window)) return;
225 if(table->idle_update_id) return;
226 table->idle_update_id = g_idle_add_full(G_PRIORITY_LOW, (GSourceFunc)_thumbtable_idle_update, table, NULL);
227}
228
233
246static gboolean _thumbtable_idle_apply_grid_configuration(gpointer user_data)
247{
248 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
249 if(IS_NULL_PTR(table)) return G_SOURCE_REMOVE;
250
251 table->idle_update_id = 0;
252
253 // Reconfigure the grid with new column settings from config
255
256 // Update and populate visible thumbnails at new sizes
258
260
261 // Queue redraw for any unpopulated areas
262 if(table->thumb_nb == 0) gtk_widget_queue_draw(table->grid);
263
264 // Schedule scrolling as a follow-up idle callback with lower priority.
265 // This ensures the GTK widget grid is fully mapped and realized before we attempt to scroll.
266 // We use a lower priority (G_PRIORITY_LOW) to let the GTK layout pass complete first.
267 _thumbtable_schedule_focus(table, G_PRIORITY_LOW);
268
269 return G_SOURCE_REMOVE;
270}
271
287{
288 if(IS_NULL_PTR(table)) return;
289 if(table->scroll_window && !gtk_widget_is_visible(table->scroll_window)) return;
290
291 // Cancel any pending standard idle update to coalesce configuration changes
292 if(table->idle_update_id)
293 {
294 g_source_remove(table->idle_update_id);
295 table->idle_update_id = 0;
296 }
297
298 // Ensure we have the current active image so we can scroll back to it after grid size change
300
301 // Schedule the coordinated grid configuration with higher priority to ensure
302 // it runs before other pending updates
303 table->idle_update_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
305 table, NULL);
306}
307
308static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data)
309{
310 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
311 if(IS_NULL_PTR(table)) return;
312
313 // Only react to the adjustment that is meaningful in the current mode.
314 if(table->mode == DT_THUMBTABLE_MODE_FILEMANAGER && adjustment != table->v_scrollbar) return;
315 if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP && adjustment != table->h_scrollbar) return;
316
318}
319
320static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
321{
322 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
323 if(IS_NULL_PTR(table)) return;
324
325 // Page size is only used to size the filemanager/grid. Filmstrip uses its parent allocation.
326 if(table->mode != DT_THUMBTABLE_MODE_FILEMANAGER) return;
327
328 // Only react to the scroll-axis (vertical) adjustment. The cross-axis adjustment's page size is
329 // driven by the grid width we set during configure; reacting to it would re-enter configure and
330 // can spin in a resize/redraw loop. (It is dormant under the NEVER horizontal policy, but guarding
331 // here keeps that assumption from silently breaking.)
332 if(object != (GObject *)table->v_scrollbar) return;
333
335}
336
337static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
338{
339 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
340 if(IS_NULL_PTR(table) || IS_NULL_PTR(allocation)) return;
341
342 if(allocation->width == table->last_parent_width && allocation->height == table->last_parent_height)
343 return;
344
345 table->last_parent_width = allocation->width;
346 table->last_parent_height = allocation->height;
348}
349
350static void _scrollbar_widget_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
351{
352 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
353 if(IS_NULL_PTR(table) || IS_NULL_PTR(allocation) || IS_NULL_PTR(table->scroll_window)) return;
354
355 GtkWidget *h_scroll = gtk_scrolled_window_get_hscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
356 GtkWidget *v_scroll = gtk_scrolled_window_get_vscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
357
358 // Filmstrip height depends on the horizontal scrollbar height. When it gets realized/allocated,
359 // we need to recompute thumbnail sizes even if the parent size didn't change.
360 if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP && widget == h_scroll)
361 {
362 if(allocation->height == table->last_h_scrollbar_height) return;
363 table->last_h_scrollbar_height = allocation->height;
365 }
366
367 // Filemanager fallback sizing subtracts the vertical scrollbar width.
368 if(table->mode == DT_THUMBTABLE_MODE_FILEMANAGER && widget == v_scroll)
369 {
370 if(allocation->width == table->last_v_scrollbar_width) return;
371 table->last_v_scrollbar_width = allocation->width;
373 }
374}
375
376// We can't trust the mouse enter/leave events on thumnbails to properly
377// update active thumbnail styling, so we need to catch the signal here and update the whole list.
378void _mouse_over_image_callback(gpointer instance, gpointer user_data)
379{
380 if(IS_NULL_PTR(user_data)) return;
381 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
382 if(IS_NULL_PTR(table->lut) || table->collection_count == 0) return;
383
384 const int32_t imgid = dt_control_get_mouse_over_id();
385
387
388 int32_t group_id = UNKNOWN_IMAGE;
389
390 // We iterate and update only over the visible range of thumbs + 2 rows as a safety margin
391 int32_t row_start = CLAMP(table->min_row_id - 2 * table->thumbs_per_row, 0, table->collection_count - 1);
392 int32_t row_end = CLAMP(table->max_row_id + 2 * table->thumbs_per_row, 0, table->collection_count - 1);
393 for(int rowid = row_start; rowid <= row_end; rowid++)
394 {
395 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
396 if(IS_NULL_PTR(thumb)) continue; // thumb object not inited
397
398 const gboolean mouse_over = thumb->mouse_over;
399 dt_thumbnail_set_mouseover(thumb, thumb->info.id == imgid);
400
401 if(thumb->info.id == imgid && dt_thumbtable_info_is_grouped(table->lut[rowid].thumb->info))
402 group_id = thumb->info.group_id;
403
404 if(thumb->mouse_over != mouse_over)
405 gtk_widget_queue_draw(thumb->widget);
406 }
407
408 // Now, we update all the thumbs of the same image group
409 if(table->draw_group_borders)
410 {
411 for(int rowid = row_start; rowid <= row_end; rowid++)
412 {
413 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
414 if(IS_NULL_PTR(thumb)) continue; // thumb object not inited
415
416 // In CSS:
417 // images borders from non-grouped images are transparent (default),
418 // images borders from non-hovered groups, when there is none, are dark orange (base border classes)
419 // images borders from the hovered group, when there is one, are bright orange (overwrite base border classes)
420 // images borders from non-hovered groups, when there is one, are transparent (overwrite base border classes width default)
421 // Here we dispatch the additional CSS classes alloying to overwrite
422 // the base border classes.
423 if(thumb->info.group_id == group_id)
424 {
425 dt_gui_add_class(thumb->widget, "hovered-group");
426 dt_gui_remove_class(thumb->widget, "non-hovered-group");
427 }
428 else if(group_id == UNKNOWN_IMAGE)
429 {
430 dt_gui_remove_class(thumb->widget, "hovered-group");
431 dt_gui_remove_class(thumb->widget, "non-hovered-group");
432 }
433 else
434 {
435 dt_gui_remove_class(thumb->widget, "hovered-group");
436 dt_gui_add_class(thumb->widget, "non-hovered-group");
437 }
438 }
439 }
440
442}
443
444
445static void _rowid_to_position(dt_thumbtable_t *table, int rowid, int *x, int *y)
446{
447 if(table->thumbs_per_row < 1) return;
448
450 {
451 int row = rowid / table->thumbs_per_row; // euclidean division
452 int col = rowid % table->thumbs_per_row;
453 *x = col * table->thumb_width;
454 *y = row * table->thumb_height;
455 }
456 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
457 {
458 *x = rowid * table->thumb_width;
459 *y = 0;
460 }
461}
462
463// Needs updated table->x_position and table->y_position
464static int _position_to_rowid(dt_thumbtable_t *table, const double x, const double y)
465{
467 {
468 // Attempt to get the image rowid sitting in the center of the middle row
469 return (y + table->view_height / 2) / table->thumb_height * table->thumbs_per_row
470 + table->thumbs_per_row / 2 - 1;
471 }
472 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
473 {
474 return x + (table->view_width / 2.) / table->thumb_width;
475 }
476 return UNKNOWN_IMAGE;
477}
478
479// Find the x, y coordinates of any given thumbnail
480// Return TRUE if a position could be computed
481// thumb->rowid and table->thumbs_per_row need to have been inited before calling this
483{
484 if(table->thumbs_per_row < 1) return FALSE;
485 _rowid_to_position(table, thumb->rowid, &thumb->x, &thumb->y);
486 return TRUE;
487}
488
489// Updates table->x_position and table->y_position
491{
492 *y = gtk_adjustment_get_value(table->v_scrollbar);
493 *x = gtk_adjustment_get_value(table->h_scrollbar);
494}
495
497{
498 double x = 0.;
499 double y = 0.;
501 table->rowid = _position_to_rowid(table, x, y);
502}
503
504static int dt_thumbtable_scroll_to_position(dt_thumbtable_t *table, const double x, const double y)
505{
506 gtk_adjustment_set_value(table->v_scrollbar, y);
507 gtk_adjustment_set_value(table->h_scrollbar, x);
508 return 0;
509}
510
511static void dt_thumbtable_scroll_to_rowid(dt_thumbtable_t *table, int rowid)
512{
513 // Find (x, y) of the current thumbnail (north-west corner)
514 int x = 0, y = 0;
515 _rowid_to_position(table, rowid, &x, &y);
516
517 // Put the image always in the center of the view, if possible,
518 // aka move from north-west corner to center of the thumb
519 x += table->thumb_width / 2;
520 y += table->thumb_height / 2;
521
522 // Scroll viewport there
523 const double x_scroll = (double)x - (double)table->view_width / 2.;
524 const double y_scroll = (double)y - (double)table->view_height / 2.;
525 dt_thumbtable_scroll_to_position(table, x_scroll, y_scroll);
526}
527
528static int _find_rowid_from_imgid(dt_thumbtable_t *table, const int32_t imgid)
529{
530 if(IS_NULL_PTR(table) || IS_NULL_PTR(table->lut) || table->collection_count <= 0) return UNKNOWN_IMAGE;
531
532 for(int i = 0; i < table->collection_count; i++)
533 if(table->lut[i].imgid == imgid)
534 return i;
535
536 return UNKNOWN_IMAGE;
537}
538
540{
541 if(IS_NULL_PTR(table) || !table->collection_inited || IS_NULL_PTR(table->lut) || table->collection_count <= 0)
542 return 1;
543
544 int rowid = UNKNOWN_IMAGE;
545 if(imgid > UNKNOWN_IMAGE)
546 {
548 rowid = _find_rowid_from_imgid(table, imgid);
550 }
551 else
552 rowid = table->rowid;
553
554 if(rowid == UNKNOWN_IMAGE) return 1;
555
556 dt_thumbtable_scroll_to_rowid(table, rowid);
557
558 return 0;
559}
560
561
563{
564 if(table->rowid > UNKNOWN_IMAGE)
566 else
568 return 0;
569}
570
571
573{
575 if(id < 0) id = dt_control_get_keyboard_over_id();
576 if(id < 0) id = dt_control_get_mouse_over_id();
577 //fprintf(stdout, "scrolling to %i\n", id);
579 return 0;
580}
581
582// Find the row ids (as in SQLite indices) of the images contained within viewport at current scrolling stage
583static gboolean _get_row_ids(dt_thumbtable_t *table, int *rowid_min, int *rowid_max)
584{
585 if(!table->configured || IS_NULL_PTR(table->v_scrollbar) || IS_NULL_PTR(table->h_scrollbar)) return FALSE;
586
588 {
589 // Pixel coordinates of the viewport:
590 float page_size = gtk_adjustment_get_page_size(table->v_scrollbar);
591 float position = gtk_adjustment_get_value(table->v_scrollbar);
592
593 // what is currently visible lies between position and position + page_size.
594 // don't preload next/previous rows because, when in 1 thumb/column,
595 // that can be quite slow
596 int row_min = floorf(position / (float)table->thumb_height);
597 int row_max = ceilf((position + page_size) / (float)table->thumb_height);
598
599 // rowid is the positional ID of the image in the SQLite collection, indexed from 0.
600 // SQLite indexes from 1 but then be use our own array to cache results.
601 *rowid_min = row_min * table->thumbs_per_row;
602 *rowid_max = row_max * table->thumbs_per_row;
603 }
604 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
605 {
606 float page_size = gtk_adjustment_get_page_size(table->h_scrollbar);
607 float position = gtk_adjustment_get_value(table->h_scrollbar);
608
609 // Preload the previous and next pages too because thumbnails are typically small
610 int row_min = (position - page_size) / table->thumb_width;
611 int row_max = (position + 2.f * page_size) / table->thumb_width;
612
613 *rowid_min = row_min * table->thumbs_per_row;
614 *rowid_max = row_max * table->thumbs_per_row;
615 }
616
617 return TRUE;
618}
619
620// Find out if a given row id is visible at current scroll step
621gboolean _is_rowid_visible(dt_thumbtable_t *table, int rowid)
622{
623 if(!table->configured || IS_NULL_PTR(table->v_scrollbar) || IS_NULL_PTR(table->h_scrollbar)) return FALSE;
624
626 { // Pixel coordinates of the viewport:
627 int page_size = gtk_adjustment_get_page_size(table->v_scrollbar);
628 int position = gtk_adjustment_get_value(table->v_scrollbar);
629 int page_bottom = page_size + position;
630
631 int img_top = (rowid / table->thumbs_per_row) * table->thumb_height;
632 int img_bottom = img_top + table->thumb_height;
633 return img_top >= position && img_bottom <= page_bottom;
634 }
635 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
636 {
637 int page_size = gtk_adjustment_get_page_size(table->h_scrollbar);
638 int position = gtk_adjustment_get_value(table->h_scrollbar);
639 int page_right = page_size + position;
640
641 int img_left = rowid * table->thumb_height;
642 int img_right = img_left + table->thumb_width;
643 return img_left >= position && img_right <= page_right;
644 }
645
646 return FALSE;
647}
648
649// Returns TRUE if visible row ids have changed since last check
651{
652 int rowid_min = 0;
653 int rowid_max = 0;
654 if(!_get_row_ids(table, &rowid_min, &rowid_max)) return FALSE;
655 if(rowid_min != table->min_row_id || rowid_max != table->max_row_id)
656 {
657 table->min_row_id = rowid_min;
658 table->max_row_id = rowid_max;
659 table->thumbs_inited = FALSE;
660 return TRUE;
661 }
662 return FALSE;
663}
664
666{
667 if(!table->configured || !table->collection_inited) return;
668
669 double main_dimension = 0.;
670 int current_w = 0, current_h = 0;
671 gtk_widget_get_size_request(table->grid, &current_w, &current_h);
672 gboolean changed = FALSE;
673
675 {
676 const int height = (int)ceilf((float)table->collection_count / (float)table->thumbs_per_row) * table->thumb_height;
677 main_dimension = height;
678 // Pin the cross-axis (width) to the viewport so the grid reaches the scrollbar instead of stopping
679 // at the floor-rounded column total. Safe under NEVER: the grid content already fits view_width
680 // (decoration budgeted in configure), so this lands on a stable fixpoint (grid = view_width,
681 // scrolled window = parent) without perturbing the cross-axis adjustment.
682 if(current_h != height || current_w != table->view_width)
683 {
684 gtk_widget_set_size_request(table->grid, table->view_width, height);
685 changed = TRUE;
686 }
687 }
688 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
689 {
690 const int width = table->collection_count * table->thumb_width;
691 main_dimension = width;
692 // Pin the cross-axis (height) to the viewport for the same reason as above.
693 if(current_w != width || current_h != table->view_height)
694 {
695 gtk_widget_set_size_request(table->grid, width, table->view_height);
696 changed = TRUE;
697 }
698 }
699 else
700 {
701 main_dimension = 0.;
702 if(current_w != -1 || current_h != -1)
703 {
704 gtk_widget_set_size_request(table->grid, -1, -1);
705 changed = TRUE;
706 }
707 }
708
709 if(changed)
710 dt_print(DT_DEBUG_LIGHTTABLE, "Configuring grid size main dimension: %.f\n", main_dimension);
711}
712
713
714// Store the geometry computed by dt_thumbtable_configure. It must NOT re-derive thumb_width/height:
715// configure already applies the cell-decoration budget when computing them, so re-deriving here
716// (e.g. floor(width/cols), ignoring deco) would disagree by a pixel and make thumbs_changed forever
717// true - an infinite configure/redraw loop.
718void _grid_configure(dt_thumbtable_t *table, int width, int height, int per_row, int thumb_width, int thumb_height)
719{
720 if(width < 32 || height < 32) return;
721
722 table->thumbs_per_row = per_row;
723 table->view_width = width;
724 table->view_height = height;
725 table->thumb_width = thumb_width;
726 table->thumb_height = thumb_height;
727
728 table->configured = TRUE;
729
730 dt_print(DT_DEBUG_LIGHTTABLE, "Configuring thumbtable w=%i h=%i thumbs/row=%i thumb_width=%i\n",
731 table->view_width, table->view_height, table->thumbs_per_row, table->thumb_width);
732}
733
734// Extra extent one thumb cell adds beyond the layout stride (thumb_width/height).
735//
736// The cells (.thumb-cell) carry a transparent border plus a negative margin, used to overlap and merge
737// the borders of adjacent cells. As a result a cell's margin box is larger than the stride by
738// (border + margin); inner cells overlap their neighbours so it cancels out, but the last row/column
739// has no neighbour to overlap into, so the GtkFixed grid ends up exactly that much larger than
740// cols*thumb_width (GtkFixed sizes itself as max(child->x + child_preferred), and the child's
741// preferred size includes its margins). Budgeting for it - read from the theme rather than hardcoded -
742// keeps the grid within its viewport under the NEVER scroll policy instead of forcing the scrolled
743// window (and its scrollbar) past the parent overlay. The cell is square, so H and V surplus are equal.
745{
746 GtkWidgetPath *path = gtk_widget_path_new();
747 gtk_widget_path_append_type(path, GTK_TYPE_EVENT_BOX);
748 gtk_widget_path_iter_add_class(path, -1, "thumb-cell");
749
750 GtkStyleContext *ctx = gtk_style_context_new();
751 gtk_style_context_set_path(ctx, path);
752
753 GtkBorder margin, border;
754 gtk_style_context_get_margin(ctx, GTK_STATE_FLAG_NORMAL, &margin);
755 gtk_style_context_get_border(ctx, GTK_STATE_FLAG_NORMAL, &border);
756
757 g_object_unref(ctx);
758 gtk_widget_path_unref(path);
759
760 return MAX(0, (border.left + border.right) + (margin.left + margin.right));
761}
762
763// Track size changes of the container or number of thumbs per row
764// and recomputed the size of individual thumbnails accordingly
766{
767 if(!gtk_widget_is_visible(table->scroll_window)) return;
768
769 int cols = 1;
770 int new_width = 0;
771 int new_height = 0;
772 int new_thumbs_per_row = 0;
773 int new_thumb_width = 0;
774 int new_thumb_height = 0;
775
776 // GtkScrolledWindow reserves a "scrollbar-spacing" gutter between the content and the scrollbar.
777 // It is a legacy GtkWidget *style property* (default 3px), not a CSS box property - invisible in the
778 // GTK Inspector's CSS pane. We zero it via CSS (#thumbtable-scroll, see ansel.css) but still read its
779 // real value here so the maths stays exact whatever the theme/DPI yields.
780 gint sb_spacing = 0;
781 gtk_widget_style_get(table->scroll_window, "scrollbar-spacing", &sb_spacing, NULL);
782
784 {
785 // Use actual widget allocations for sizing: GtkAdjustment page sizes are not reliably updated
786 // during shrinking, which can prevent thumbnails from downscaling until another resize happens.
787 new_width = gtk_widget_get_allocated_width(table->parent_overlay);
788 new_height = gtk_widget_get_allocated_height(table->parent_overlay);
789
790 GtkWidget *v_scroll = gtk_scrolled_window_get_vscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
791 const int v_scroll_w = v_scroll ? gtk_widget_get_allocated_width(v_scroll) : 0;
792 if(v_scroll_w > 0) new_width -= v_scroll_w + sb_spacing;
793
794 cols = dt_conf_get_int("plugins/lighttable/images_in_row");
795 }
796 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
797 {
798 // Don't use GtkAdjustment page sizes here: in filmstrip, the scrolled window height can
799 // be influenced by its (resized) children during initial layout, which may cause a
800 // feedback loop where thumbnails keep growing.
801 new_width = gtk_widget_get_allocated_width(table->parent_overlay);
802 new_height = gtk_widget_get_allocated_height(table->parent_overlay);
803 GtkWidget *h_scroll = gtk_scrolled_window_get_hscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
804 int h_scroll_h = 0;
805 if(h_scroll)
806 {
807 h_scroll_h = gtk_widget_get_allocated_height(h_scroll);
808 if(h_scroll_h > 0) h_scroll_h += sb_spacing;
809 }
810
811 // Clamp to the explicit panel size request when present to enforce "container drives child size".
812 int req_w = -1, req_h = -1;
813 gtk_widget_get_size_request(table->parent_overlay, &req_w, &req_h);
814 if(req_h > 0)
815 new_height = MIN(new_height, req_h);
816
817 new_height -= h_scroll_h;
818 }
819
820 // Parent is not allocated or something went wrong:
821 // ensure to reset everything so no further code will run.
822 if(new_width < 32 || new_height < 32)
823 {
824 table->thumbs_inited = FALSE;
825 table->configured = FALSE;
826 table->thumbs_per_row = 0;
827 table->thumb_height = 0;
828 table->thumb_width = 0;
829 return;
830 }
831
832 // Reserve the per-cell decoration surplus on the cross axis so the whole grid (cols*thumb_width plus
833 // the last cell's protruding border) stays within the viewport. Under the NEVER scroll policy a
834 // GtkFixed wider than the viewport would drag the scrolled window (and scrollbar) past the parent.
835 const int deco = _thumb_cell_decoration();
836
837 // Compute derived thumbnail sizes and only invalidate thumbnails when they actually change.
839 {
840 new_thumbs_per_row = cols;
841 new_thumb_width = (int)floorf((float)(new_width - deco) / (float)MAX(new_thumbs_per_row, 1));
842 new_thumb_height = (new_thumbs_per_row == 1) ? new_height : new_thumb_width;
843 }
844 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
845 {
846 new_thumbs_per_row = 1;
847 new_thumb_height = new_height - deco;
848 new_thumb_width = new_thumb_height;
849 }
850
851 const gboolean thumbs_changed = (!table->configured
852 || new_thumbs_per_row != table->thumbs_per_row
853 || new_thumb_width != table->thumb_width
854 || new_thumb_height != table->thumb_height);
855
856 // Always keep view sizes in sync: they are used for navigation and scroll centering.
857 table->view_width = new_width;
858 table->view_height = new_height;
859
860 if(thumbs_changed)
861 {
862 table->thumbs_inited = FALSE;
863 _grid_configure(table, new_width, new_height, new_thumbs_per_row, new_thumb_width, new_thumb_height);
864 _update_grid_area(table);
865 }
866}
867
869{
870 if(IS_NULL_PTR(table) || imgid <= 0) return NULL;
871 return (dt_thumbnail_t *)g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid));
872}
873
875{
876 if(IS_NULL_PTR(table) || IS_NULL_PTR(out) || imgid <= 0) return FALSE;
877
878 // Prefer LUT-backed metadata to avoid touching the image cache for read-only UI needs.
879 gboolean found = FALSE;
881 if(table->lut)
882 {
883 for(int rowid = 0; rowid < table->collection_count; rowid++)
884 {
885 if(table->lut[rowid].imgid == imgid && table->lut[rowid].thumb)
886 {
887 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
888 if(g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid)) == thumb)
889 {
890 *out = thumb->info;
891 found = TRUE;
892 break;
893 }
894 table->lut[rowid].thumb = NULL;
895 }
896 }
897 }
899
900 return found;
901}
902
903#define CLAMP_ROW(rowid) CLAMP(rowid, 0, table->collection_count - 1)
904#define IS_COLLECTION_EDGE(rowid) (rowid < 0 || rowid >= table->collection_count)
905
907{
908 // Reset all CSS classes
909 dt_thumbnail_border_t borders = 0;
910 dt_thumbnail_set_group_border(thumb, borders);
911
912 const int32_t rowid = thumb->rowid;
913 const int32_t groupid = thumb->info.group_id;
914
915 // Ungrouped image: abort
916 if(!dt_thumbtable_info_is_grouped(table->lut[rowid].thumb->info) || !table->draw_group_borders) return;
917
919 {
920 if(table->lut[CLAMP_ROW(rowid - table->thumbs_per_row)].groupid != groupid
921 || IS_COLLECTION_EDGE(rowid - table->thumbs_per_row))
922 borders |= DT_THUMBNAIL_BORDER_TOP;
923
924 if(table->lut[CLAMP_ROW(rowid + table->thumbs_per_row)].groupid != groupid
925 || IS_COLLECTION_EDGE(rowid + table->thumbs_per_row))
927
928 if(table->lut[CLAMP_ROW(rowid - 1)].groupid != groupid
929 || IS_COLLECTION_EDGE(rowid - 1))
930 borders |= DT_THUMBNAIL_BORDER_LEFT;
931
932 if(table->lut[CLAMP_ROW(rowid + 1)].groupid != groupid
933 || IS_COLLECTION_EDGE(rowid + 1))
934 borders |= DT_THUMBNAIL_BORDER_RIGHT;
935
936 // If the group spans over more than a full row,
937 // close the row ends. Otherwise, we leave orphans opened at the row ends.
938 if(table->lut[rowid].thumb->info.group_members > table->thumbs_per_row)
939 {
940 if(rowid % table->thumbs_per_row == 0)
941 borders |= DT_THUMBNAIL_BORDER_LEFT;
942 if(rowid % table->thumbs_per_row == table->thumbs_per_row - 1)
943 borders |= DT_THUMBNAIL_BORDER_RIGHT;
944 }
945 }
946 else if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP)
947 {
949
950 if(table->lut[CLAMP_ROW(rowid - 1)].groupid != groupid
951 || IS_COLLECTION_EDGE(rowid - 1))
952 borders |= DT_THUMBNAIL_BORDER_LEFT;
953
954 if(table->lut[CLAMP_ROW(rowid + 1)].groupid != groupid
955 || IS_COLLECTION_EDGE(rowid + 1))
956 borders |= DT_THUMBNAIL_BORDER_RIGHT;
957 }
958
959 dt_thumbnail_set_group_border(thumb, borders);
960}
961
962void _add_thumbnail_at_rowid(dt_thumbtable_t *table, const size_t rowid, const int32_t mouse_over)
963{
964 const int32_t imgid = table->lut[rowid].imgid;
965 dt_image_t info;
967 if(IS_NULL_PTR(img)) return;
968
969 // Take a private copy
970 info = *img;
972
973 dt_thumbnail_t *thumb = NULL;
974 gboolean new_item = TRUE;
975 gboolean new_position = TRUE;
976
977 // Do we already have a thumbnail at the correct postion for the correct imgid ?
978 if(table->lut[rowid].thumb && table->lut[rowid].imgid == imgid)
979 {
980 // Reuse the LUT entry only if it still matches the live thumbnail registry.
981 dt_thumbnail_t *mapped_thumb = _find_thumb_by_imgid(table, imgid);
982 if(mapped_thumb == table->lut[rowid].thumb)
983 {
984 thumb = mapped_thumb;
985 new_position = FALSE;
986 }
987 else
988 {
989 table->lut[rowid].thumb = NULL;
990 }
991 }
992 else
993 {
994 // NO : Try to find an existing thumbnail widget by imgid in table->list
995 // That will be faster if we only changed the sorting order but are still in the same collection.
996 // NOTE: the thumb widget position in grid will be wrong
997 thumb = _find_thumb_by_imgid(table, imgid);
998 }
999
1000 if(thumb)
1001 {
1002 // Ensure everything is up-to-date
1003 thumb->rowid = rowid;
1004 dt_thumbnail_resync_info(thumb, &info);
1005 new_item = FALSE;
1006 }
1007 else
1008 {
1009 thumb = dt_thumbnail_new(rowid, table->overlays, table, &info);
1010 if(IS_NULL_PTR(thumb)) return;
1011 g_hash_table_insert(table->list, GINT_TO_POINTER(thumb->info.id), thumb);
1012 table->thumb_nb += 1;
1013 }
1014
1015 table->lut[rowid].thumb = thumb;
1016
1017 // Resize
1018 gboolean size_changed = (table->thumb_height != thumb->height || table->thumb_width != thumb->width);
1019 if(new_item || size_changed || table->overlays != thumb->over)
1020 {
1021 dt_thumbnail_set_overlay(thumb, table->overlays);
1022 dt_thumbnail_resize(thumb, table->thumb_width, table->thumb_height);
1023 }
1024
1025 // Actually moving the widgets in the grid is more expensive, do it only if necessary
1026 if(new_item)
1027 {
1028 _set_thumb_position(table, thumb);
1029 gtk_fixed_put(GTK_FIXED(table->grid), thumb->widget, thumb->x, thumb->y);
1030 //fprintf(stdout, "adding new thumb at #%lu: %i, %i\n", rowid, thumb->x, thumb->y);
1031 }
1032 else if(new_position || size_changed)
1033 {
1034 _set_thumb_position(table, thumb);
1035 gtk_fixed_move(GTK_FIXED(table->grid), thumb->widget, thumb->x, thumb->y);
1036 //fprintf(stdout, "moving new thumb at #%lu: %i, %i\n", rowid, thumb->x, thumb->y);
1037 }
1038
1039 // Update visual states and flags. Mouse over is not connected to a signal and cheap to update
1040 dt_thumbnail_set_mouseover(thumb, (mouse_over == thumb->info.id));
1042
1044 {
1046 thumb->disable_actions = TRUE;
1047 }
1048 else if(table->mode == DT_THUMBTABLE_MODE_FILEMANAGER)
1049 {
1051 thumb->disable_actions = FALSE;
1052 }
1053
1054 _add_thumbnail_group_borders(table, thumb);
1055 gtk_widget_show(thumb->widget);
1056}
1057
1058
1059// Add and/or resize thumbnails within visible viewort at current scroll level
1061{
1062 const int32_t mouse_over = dt_control_get_mouse_over_id();
1063
1064 // for(size_t rowid = 0; rowid < table->collection_count; rowid++)
1065 for(size_t rowid = MAX(table->min_row_id, 0); rowid < MIN(table->max_row_id, table->collection_count); rowid++)
1066 _add_thumbnail_at_rowid(table, rowid, mouse_over);
1067}
1068
1069// Resize the thumbnails that are still existing but outside of visible viewport at current scroll level
1071{
1072 if(!table->configured) return;
1073
1074 GHashTableIter iter;
1075 gpointer value = NULL;
1076 g_hash_table_iter_init(&iter, table->list);
1077 while(g_hash_table_iter_next(&iter, NULL, &value))
1078 {
1080 gboolean size_changed = (table->thumb_height != thumb->height || table->thumb_width != thumb->width);
1081
1082 if(size_changed || table->overlays != thumb->over)
1083 {
1084 // Overlay modes may change the height of the image
1085 // to accommodate buttons. We need to resize on overlay changes.
1086 dt_thumbnail_set_overlay(thumb, table->overlays);
1087 dt_thumbnail_resize(thumb, table->thumb_width, table->thumb_height);
1088 if(size_changed)
1089 {
1090 _set_thumb_position(table, thumb);
1091 gtk_fixed_move(GTK_FIXED(table->grid), thumb->widget, thumb->x, thumb->y);
1092 }
1094 }
1095
1097 _add_thumbnail_group_borders(table, thumb);
1098 gtk_widget_queue_draw(thumb->widget);
1099 }
1100}
1101
1102
1104{
1105 _update_row_ids(table);
1106
1107 if(!gtk_widget_is_visible(table->scroll_window) || IS_NULL_PTR(table->lut) || !table->configured || !table->collection_inited
1108 || table->thumbs_inited || table->collection_count == 0)
1109 return;
1110
1111 if(table->reset_collection)
1112 {
1114 table->reset_collection = FALSE;
1115 }
1116
1117 const double start = dt_get_wtime();
1118
1119 dt_pthread_mutex_lock(&table->lock);
1120
1121 gboolean empty_list = (table->thumb_nb == 0);
1122
1123 _populate_thumbnails(table);
1124
1125 if(!empty_list)
1126 _resize_thumbnails(table);
1127
1128 table->thumbs_inited = TRUE;
1129
1131
1132 const char *const name = gtk_widget_get_name(table->grid);
1133 dt_print(DT_DEBUG_LIGHTTABLE, "[%s] Populated %d thumbs between %i and %i in %0.04f sec \n",
1134 name ? name : "thumbtable", table->thumb_nb, table->min_row_id, table->max_row_id,
1135 dt_get_wtime() - start);
1136}
1137
1138
1139static void _dt_profile_change_callback(gpointer instance, int type, gpointer user_data)
1140{
1141 if(IS_NULL_PTR(user_data)) return;
1142 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1144}
1145
1146static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
1147{
1148 if(IS_NULL_PTR(user_data)) return;
1149 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1150 gboolean first = TRUE;
1151
1152 dt_pthread_mutex_lock(&table->lock);
1153 if(IS_NULL_PTR(table->lut) || !table->collection_inited || table->collection_count == 0)
1154 {
1156 return;
1157 }
1158
1159 for(int rowid = 0; rowid < table->collection_count; rowid++)
1160 {
1161 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
1162 if(IS_NULL_PTR(thumb)) continue;
1163
1164 if(g_hash_table_lookup(table->list, GINT_TO_POINTER(table->lut[rowid].imgid)) != thumb)
1165 {
1166 table->lut[rowid].thumb = NULL;
1167 continue;
1168 }
1169
1170 const gboolean selected = thumb->selected;
1172
1173 if(thumb->selected && first)
1174 {
1176
1177 // Sync the table active row id with the first thumb in selection
1178 table->rowid = thumb->rowid;
1179 first = FALSE;
1180 }
1181
1182 if(thumb->selected != selected)
1183 gtk_widget_queue_draw(thumb->widget);
1184 }
1186}
1187
1189{
1190 table->zoom = level;
1193 _thumbtable_schedule_focus(table, G_PRIORITY_DEFAULT_IDLE);
1194}
1195
1197{
1198 return table->zoom;
1199}
1200
1201void dt_thumbtable_offset_zoom(dt_thumbtable_t *table, const double delta_x, const double delta_y)
1202{
1203 dt_pthread_mutex_lock(&table->lock);
1204 GHashTableIter iter;
1205 gpointer value = NULL;
1206 g_hash_table_iter_init(&iter, table->list);
1207 while(g_hash_table_iter_next(&iter, NULL, &value))
1208 {
1210 thumb->zoomx += delta_x;
1211 thumb->zoomy += delta_y;
1212 gtk_widget_queue_draw(thumb->w_image);
1213 }
1215}
1216
1217
1223
1225{
1226 return table->focus_regions;
1227}
1228
1234
1236{
1237 return table->focus_peaking;
1238}
1239
1247
1249{
1250 return table->draw_group_borders;
1251}
1252
1253// can be called with imgid = -1, in that case we reload all mipmaps
1254// reinit = FALSE should be called when the mipmap is ready to redraw,
1255// reinit = TRUE should be called when a refreshed mipmap has been requested but we have nothing yet to draw
1256void dt_thumbtable_refresh_thumbnail_real(dt_thumbtable_t *table, int32_t imgid, gboolean reinit)
1257{
1258 dt_pthread_mutex_lock(&table->lock);
1259 if(imgid != UNKNOWN_IMAGE)
1260 {
1261 dt_thumbnail_t *thumb = (dt_thumbnail_t *)g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid));
1262 if(thumb)
1264 }
1265 else
1266 {
1267 GHashTableIter iter;
1268 gpointer value = NULL;
1269 g_hash_table_iter_init(&iter, table->list);
1270 while(g_hash_table_iter_next(&iter, NULL, &value))
1271 {
1274 }
1275 }
1277}
1278
1279
1280// this is called each time the images info change
1281// NOTE: remember we populate the table->lut with the current collection
1282// but we init actual thumbnail objects and add their widgets to the grid
1283// only when they appear in viewport.
1284// So, we have to account for the fact that we may not have actual
1285// thumbnails to refresh yet, and the table->list doesn't contain
1286// uninited thumbs.
1287// So we always use table->lut to find imgid, update the cached info
1288// and try to update widgets only if we have some.
1289// Otherwise, fresh info will be read when initing new thumbnails objects.
1290static void _dt_image_info_changed_callback(gpointer instance, gpointer imgs, gpointer user_data)
1291{
1292 if(!user_data || IS_NULL_PTR(imgs)) return;
1293 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1294 if(IS_NULL_PTR(table->lut)) return;
1295
1296 dt_pthread_mutex_lock(&table->lock);
1297
1298 for(GList *l = g_list_first(imgs); l; l = g_list_next(l))
1299 {
1300 const int32_t imgid = GPOINTER_TO_INT(l->data);
1301 if(imgid <= 0) continue;
1302
1303 const int rowid = _find_rowid_from_imgid(table, imgid);
1304 if(rowid == UNKNOWN_IMAGE) continue;
1305
1306 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
1307 if(thumb)
1308 {
1309 // Refresh the cached LUT info from the image cache for write-driven updates
1310 // (ratings, color labels, etc.) while still keeping read paths cache-free.
1311 const dt_image_t *img = dt_image_cache_testget(darktable.image_cache, imgid, 'r');
1312 if(img)
1313 {
1314 dt_thumbnail_resync_info(thumb, img);
1317 _add_thumbnail_group_borders(table, thumb);
1318 gtk_widget_queue_draw(thumb->widget);
1319 }
1320 }
1321
1324 }
1325
1327}
1328
1330{
1331 // In-memory collected images don't store group_id, so we need to fetch it again from DB
1332 sqlite3_stmt *stmt = dt_thumbtable_info_get_collection_stmt();
1333
1334 // NOTE: non-grouped images have group_id equal to their own id
1335 // grouped images have group_id equal to the id of the "group leader".
1336 // In old database versions, it's possible that group_id may have been set to -1 for non-grouped images.
1337 // That would actually make group detection much easier...
1338
1339 // Convert SQL imgids into C objects we can work with
1340 GArray *collection = g_array_new(FALSE, FALSE, sizeof(dt_thumbtable_cache_t));
1341 while(sqlite3_step(stmt) == SQLITE_ROW)
1342 {
1343 const int32_t imgid = sqlite3_column_int(stmt, 0);
1344 const int32_t groupid = sqlite3_column_int(stmt, 1);
1345
1346 if(table->collapse_groups && imgid != groupid)
1347 {
1348 // if user requested to collapse image groups in GUI,
1349 // only the group leader is shown. But we need to make sure
1350 // there is no dangling selection pointing to hidden group members
1351 // because it's unexpected that unvisible items might be selected,
1352 // and selection sanitization only deals with imgids outside of current collection,
1353 // but group members are always within the collection.
1355 continue;
1356 }
1357
1358 dt_thumbtable_cache_t entry = { .thumb = NULL, .imgid = imgid, .groupid = groupid };
1359 g_array_append_val(collection, entry);
1360
1361 // Populate the image cache. We don't keep a copy here because it wouldn't
1362 // be memory-managed
1363 dt_image_t info;
1364 dt_image_init(&info);
1365 dt_image_from_stmt(&info, stmt);
1366
1367#ifndef NDEBUG
1369#endif
1370
1371 // Seed the cache and be done
1373 }
1374 sqlite3_reset(stmt);
1375
1376 if(IS_NULL_PTR(collection) || collection->len == 0)
1377 {
1378 dt_thumbtable_cache_t *old_lut = NULL;
1379 dt_pthread_mutex_lock(&table->lock);
1380 old_lut = table->lut;
1381 table->lut = NULL;
1382 table->collection_count = 0;
1383 table->collection_inited = FALSE;
1385
1386 dt_free(old_lut);
1387 if(collection) g_array_free(collection, TRUE);
1388 return;
1389 }
1390
1391 // Build the collection LUT, aka a fixed-sized array of image objects
1392 // where the position of an image in the collection is directly the index in the LUT/array.
1393 // This makes for very efficient position -> imgid/thumbnail accesses directly in C,
1394 // especially from GUI code. The downside is we need to fully clear and recreate the LUT
1395 // everytime a collection changes (meaning filters OR sorting changed).
1396 dt_thumbtable_cache_t *new_lut = malloc(collection->len * sizeof(dt_thumbtable_cache_t));
1397
1398 if(IS_NULL_PTR(new_lut))
1399 {
1400 g_array_free(collection, TRUE);
1401 return;
1402 }
1403
1404 memcpy(new_lut, collection->data, collection->len * sizeof(dt_thumbtable_cache_t));
1405
1406 dt_thumbtable_cache_t *old_lut = NULL;
1407 dt_pthread_mutex_lock(&table->lock);
1408 old_lut = table->lut;
1409 table->lut = new_lut;
1410 table->collection_count = collection->len;
1411 table->collection_inited = TRUE;
1413
1414 dt_free(old_lut);
1415 g_array_free(collection, TRUE);
1416}
1417
1419{
1420 // Hash the collection query string
1421 const char *const query = dt_collection_get_query(darktable.collection);
1422 size_t len = strlen(query);
1423 uint64_t hash = dt_hash(5384, query, len);
1424
1425 // Factor in the number of images in the collection result
1426 uint32_t num_pics = dt_collection_get_count(darktable.collection);
1427 hash = dt_hash(hash, (char *)&num_pics, sizeof(uint32_t));
1428
1429 if(hash != table->collection_hash || table->reset_collection)
1430 {
1431 // Collection changed: reset everything
1432 table->collection_hash = hash;
1433 table->collection_inited = FALSE;
1434 return TRUE;
1435 }
1436 return FALSE;
1437}
1438
1439
1440// this is called each time collected images change
1441static void _dt_collection_changed_callback(gpointer instance, dt_collection_change_t query_change,
1442 dt_collection_properties_t changed_property, gpointer imgs,
1443 const int next, gpointer user_data)
1444{
1445 if(IS_NULL_PTR(user_data)) return;
1446 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1447
1448 gboolean collapse_groups = dt_conf_get_bool("ui_last/grouping");
1449 gboolean collapsing_changed = (table->collapse_groups != collapse_groups);
1450
1451 // Remember where the scrolling is at to possibly get the same visible images
1452 // despite collection changes (provided they are still there).
1454
1455 // See if the collection changed
1456 gboolean grouping_changed = changed_property == DT_COLLECTION_PROP_GROUPING;
1457 gboolean changed = _dt_collection_get_hash(table) || collapsing_changed || grouping_changed;
1458 if(changed)
1459 {
1460 // If groups are collapsed, we add only the group leader image to the collection
1461 // It needs to be set before running _dt_collection_lut()
1462 table->collapse_groups = collapse_groups;
1463 if(!_thumbtable_clone_lut(table))
1464 _dt_collection_lut(table);
1465
1466 table->thumbs_inited = FALSE;
1468
1469 if(table->collection_count == 0)
1470 {
1472 "The current filtered collection contains no image. Relax your filters or fetch a non-empty collection"));
1473 }
1474
1475 // Ensure we have something to scroll
1477
1478 // Number of images may have changed, size of grid too:
1479 _update_grid_area(table);
1480
1481 // Coalesce multiple layout/resize signals that can happen during collection loads.
1483
1484 _thumbtable_schedule_focus(table, G_PRIORITY_DEFAULT_IDLE);
1485 }
1486}
1487
1488// get the class name associated with the overlays mode
1490{
1491 switch(over)
1492 {
1494 return g_strdup("dt_overlays_none");
1496 return g_strdup("dt_overlays_always");
1498 default:
1499 return g_strdup("dt_overlays_hover");
1500 }
1501}
1502
1503// update thumbtable class and overlays mode, depending on size category
1505{
1506 // we change the overlay mode
1507 gchar *txt = g_strdup("plugins/lighttable/overlays/global");
1509 dt_free(txt);
1510
1512}
1513
1514// change the type of overlays that should be shown
1516{
1517 if(IS_NULL_PTR(table)) return;
1518 if(over == table->overlays) return;
1519
1520 // Cleanup old Darktable stupid modes
1521 dt_conf_set_int("plugins/lighttable/overlays/global", sanitize_overlays(over));
1522
1523 gchar *cl0 = _thumbs_get_overlays_class(table->overlays);
1524 gchar *cl1 = _thumbs_get_overlays_class(over);
1525 dt_gui_remove_class(table->grid, cl0);
1526 dt_gui_add_class(table->grid, cl1);
1527 dt_free(cl0);
1528 dt_free(cl1);
1529
1530 table->thumbs_inited = FALSE;
1531 table->overlays = over;
1532
1533 dt_pthread_mutex_lock(&table->lock);
1534 _resize_thumbnails(table);
1536}
1537
1538static void _event_dnd_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data,
1539 const guint target_type, const guint time, gpointer user_data)
1540{
1541 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1542 g_assert(!IS_NULL_PTR(selection_data));
1543
1544 switch(target_type)
1545 {
1546 case DND_TARGET_IMGID:
1547 {
1548 const int imgs_nb = g_list_length(table->drag_list);
1549 if(imgs_nb)
1550 {
1551 uint32_t *imgs = malloc(sizeof(uint32_t) * imgs_nb);
1552 GList *l = table->drag_list;
1553 for(int i = 0; i < imgs_nb; i++)
1554 {
1555 imgs[i] = GPOINTER_TO_INT(l->data);
1556 l = g_list_next(l);
1557 }
1558 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
1559 _DWORD, (guchar *)imgs, imgs_nb * sizeof(uint32_t));
1560 dt_free(imgs);
1561 }
1562 break;
1563 }
1564 default: // return the location of the file as a last resort
1565 case DND_TARGET_URI:
1566 {
1567 GList *l = table->drag_list;
1568 if(g_list_is_singleton(l))
1569 {
1570 gchar pathname[PATH_MAX] = { 0 };
1571 gboolean from_cache = TRUE;
1572 const int id = GPOINTER_TO_INT(l->data);
1573 dt_image_full_path(id, pathname, sizeof(pathname), &from_cache, __FUNCTION__);
1574 gchar *uri = g_strdup_printf("file://%s", pathname); // TODO: should we add the host?
1575 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
1576 _BYTE, (guchar *)uri, strlen(uri));
1577 dt_free(uri);
1578 }
1579 else
1580 {
1581 GList *images = NULL;
1582 for(; l; l = g_list_next(l))
1583 {
1584 const int id = GPOINTER_TO_INT(l->data);
1585 gchar pathname[PATH_MAX] = { 0 };
1586 gboolean from_cache = TRUE;
1587 dt_image_full_path(id, pathname, sizeof(pathname), &from_cache, __FUNCTION__);
1588 gchar *uri = g_strdup_printf("file://%s", pathname); // TODO: should we add the host?
1589 images = g_list_prepend(images, uri);
1590 }
1591 images = g_list_reverse(images); // list was built in reverse order, so un-reverse it
1592 gchar *uri_list = dt_util_glist_to_str("\r\n", images);
1593 g_list_free_full(images, dt_free_gpointer);
1594 images = NULL;
1595 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data), _BYTE,
1596 (guchar *)uri_list, strlen(uri_list));
1597 dt_free(uri_list);
1598 }
1599 break;
1600 }
1601 }
1602}
1603
1604static void _thumbtable_drag_set_icon(dt_thumbtable_t *table, GdkDragContext *context)
1605{
1606 if(IS_NULL_PTR(table) || !table->drag_list) return;
1607
1608 const int32_t imgid = GPOINTER_TO_INT(table->drag_list->data);
1609 dt_thumbnail_t *thumb = _find_thumb_by_imgid(table, imgid);
1610 if(IS_NULL_PTR(thumb)) return;
1611
1612 cairo_surface_t *surface = NULL;
1613 int hotspot_x = 0;
1614 int hotspot_y = 0;
1615
1616 dt_pthread_mutex_lock(&thumb->lock);
1617 if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) > 0)
1618 {
1619 surface = cairo_surface_reference(thumb->img_surf);
1620 hotspot_x = thumb->img_width / 2;
1621 hotspot_y = thumb->img_height / 2;
1622 }
1624
1625 if(IS_NULL_PTR(surface)) return;
1626
1627 GtkWidget *image = gtk_image_new_from_surface(surface);
1628 cairo_surface_destroy(surface);
1629
1630 gtk_widget_show(image);
1631 gtk_drag_set_icon_widget(context, image, hotspot_x, hotspot_y);
1632}
1633
1634static void _event_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
1635{
1636 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1637 const int32_t imgid = dt_control_get_mouse_over_id();
1638
1639 if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP && imgid > 0)
1640 {
1641 /* Views that need drags to commit the hovered image must do it before
1642 * dt_act_on_get_images() snapshots the payload. */
1644 }
1645
1647 _thumbtable_drag_set_icon(table, context);
1648}
1649
1650GList *_thumbtable_dnd_import_check(GList *files, const char *pathname, int *elements)
1651{
1652 if (IS_NULL_PTR(pathname) || IS_NULL_PTR(pathname))
1653 {
1654 fprintf(stdout,"DND check: no pathname.\n");
1655 return files;
1656 }
1657 fprintf(stdout,"DND check pathname: %s\n", pathname);
1658
1659 if(g_file_test(pathname, G_FILE_TEST_IS_REGULAR))
1660 {
1661 if (dt_supported_image(pathname))
1662 {
1663 files = g_list_prepend(files, g_strdup(pathname));
1664 (*elements)++;
1665 }
1666 else
1667 fprintf(stderr, "`%s`: Unkonwn format.", pathname);
1668 }
1669 else if(g_file_test(pathname, G_FILE_TEST_IS_DIR))
1670 {
1671 fprintf(stderr, "DND check: Folders are not allowed");
1672 dt_control_log(_("'%s': Please use 'File > Import' to import a folder."), pathname);
1673 }
1674 else
1675 {
1676 fprintf(stderr, "DND check: `%s` not a file or folder.\n", pathname);
1677 }
1678
1679 return files;
1680}
1681
1682static gboolean _thumbtable_dnd_import(GtkSelectionData *selection_data)
1683{
1684 gchar **uris = gtk_selection_data_get_uris(selection_data);
1685 int elements = 0;
1686 GList *files = NULL;
1687
1688 if(uris)
1689 {
1690 GVfs *vfs = g_vfs_get_default();
1691 for (int i = 0; !IS_NULL_PTR(uris[i]); i++)
1692 {
1693 GFile *filepath = g_vfs_get_file_for_uri(vfs, uris[i]);
1694 const gchar *pathname = g_strdup(g_file_get_path(filepath));
1695 files = _thumbtable_dnd_import_check(files, pathname, &elements);
1696 g_object_unref(filepath);
1697 }
1698
1699 if(elements > 0)
1700 {
1701 // WARNING: we copy a Glist of pathes as char*
1702 // The references to the char* still belong to the original.
1703 // We will free them in the import job.
1704 dt_control_import_t data = {.imgs = g_list_copy(files),
1705 .datetime = g_date_time_new_now_local(),
1706 .copy = FALSE, // we only import in place.
1707 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
1708 .base_folder = dt_conf_get_string("session/base_directory_pattern"),
1709 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
1710 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
1711 .target_dir = NULL,
1712 .elements = elements,
1713 .discarded = NULL
1714 };
1715
1716 dt_control_import(data);
1717 }
1718 else fprintf(stderr,"No files to import. Check your selection or use 'File > Import'.");
1719 }
1720
1721 g_strfreev(uris);
1722 g_list_free(files);
1723 files = NULL;
1724 return elements >= 0 ? TRUE : FALSE;
1725}
1726
1727void dt_thumbtable_event_dnd_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
1728 GtkSelectionData *selection_data, guint target_type, guint time,
1729 gpointer user_data)
1730{
1731 gboolean success = FALSE;
1732
1733 if((target_type == DND_TARGET_URI) && (!IS_NULL_PTR(selection_data))
1734 && (gtk_selection_data_get_length(selection_data) >= 0))
1735 {
1736 success = _thumbtable_dnd_import(selection_data);
1737 }
1738
1739 gtk_drag_finish(context, success, FALSE, time);
1740}
1741
1742static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
1743{
1744 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1745 if(table->drag_list)
1746 {
1747 g_list_free(table->drag_list);
1748 table->drag_list = NULL;
1749 }
1750 // in any case, with reset the reordering class if any
1751 dt_gui_remove_class(table->grid, "dt_thumbtable_reorder");
1752}
1753
1754int _imgid_to_rowid(dt_thumbtable_t *table, int32_t imgid)
1755{
1756 if(IS_NULL_PTR(table->lut)) return UNKNOWN_IMAGE;
1757
1758 int rowid = UNKNOWN_IMAGE;
1759
1760 dt_pthread_mutex_lock(&table->lock);
1761 for(int i = 0; i < table->collection_count; i++)
1762 {
1763 if(table->lut[i].imgid == imgid)
1764 {
1765 rowid = i;
1766 break;
1767 }
1768 }
1770
1771 return rowid;
1772}
1773
1785
1786
1787void _move_in_grid(dt_thumbtable_t *table, GdkEventKey *event, dt_thumbtable_direction_t direction, int origin_imgid)
1788{
1789 if(IS_NULL_PTR(table->lut)) return;
1790 if(!gtk_widget_is_visible(table->scroll_window)) return;
1791
1792 int current_rowid = _imgid_to_rowid(table, origin_imgid);
1793 int offset = 0;
1794
1795 switch(direction)
1796 {
1797 case DT_TT_MOVE_UP:
1798 offset = - table->thumbs_per_row;
1799 break;
1800 case DT_TT_MOVE_DOWN:
1801 offset = + table->thumbs_per_row;
1802 break;
1803 case DT_TT_MOVE_LEFT:
1804 offset = - 1;
1805 break;
1806 case DT_TT_MOVE_RIGHT:
1807 offset = + 1;
1808 break;
1810 offset = - table->view_height / table->thumb_height * table->thumbs_per_row;
1811 break;
1813 offset = + table->view_height / table->thumb_height * table->thumbs_per_row;
1814 break;
1815 case DT_TT_MOVE_START:
1816 offset = - origin_imgid;
1817 break;
1818 case DT_TT_MOVE_END:
1819 offset = +table->collection_count; // will be clamped below, don't care
1820 break;
1821 }
1822
1823 int new_rowid = CLAMP(current_rowid + offset, 0, table->collection_count - 1);
1824
1825 dt_pthread_mutex_lock(&table->lock);
1826 int new_imgid = table->lut[new_rowid].imgid;
1828
1829 dt_thumbtable_dispatch_over(table, event->type, new_imgid);
1830
1831 if(!_is_rowid_visible(table, new_rowid))
1832 {
1833 // GUI update will be handled through the value-changed event of the GtkAdjustment
1834 dt_thumbtable_scroll_to_imgid(table, new_imgid);
1835 }
1836 else
1837 {
1838 // We still need to update all visible thumbs to keep mouse_over states in sync
1839 table->thumbs_inited = FALSE;
1840 dt_thumbtable_update(table);
1841 }
1842}
1843
1845{
1846 if(table->alternate_mode == enable) return;
1847 table->alternate_mode = enable;
1848
1849 dt_pthread_mutex_lock(&table->lock);
1850 GHashTableIter iter;
1851 gpointer value = NULL;
1852 g_hash_table_iter_init(&iter, table->list);
1853 while(g_hash_table_iter_next(&iter, NULL, &value))
1854 {
1857 }
1859}
1860
1861
1862gboolean dt_thumbtable_key_pressed_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
1863{
1864 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
1865 if(IS_NULL_PTR(user_data)) return FALSE;
1866 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1867 if(IS_NULL_PTR(table->lut)) return FALSE;
1868
1869 // Find out the current image
1870 // NOTE: when moving into the grid from key arrow events,
1871 // if the cursor is still overlaying the grid when scrolling, it can collide
1872 // with key event and set the mouse_over focus elsewhere.
1873 // For this reason, we use our own private keyboard_over event,
1874 // and use the mouse_over as a fall-back only.
1875 // Key events are "knobby", therefore more reliale than "hover",
1876 // so they always take precedence.
1877 int32_t imgid = dt_control_get_keyboard_over_id();
1878 if(imgid < 0) imgid = dt_control_get_mouse_over_id();
1879 if(imgid < 0) imgid = dt_selection_get_first_id(darktable.selection);
1880 if(imgid < 0 && table->lut)
1881 {
1882 dt_pthread_mutex_lock(&table->lock);
1883 imgid = table->lut[0].imgid;
1885 }
1886
1887 //fprintf(stdout, "%s\n", gtk_accelerator_name(event->keyval, event->state));
1888
1889 // Exit alternative mode on any keystroke other than alt
1890 if(event->keyval != GDK_KEY_Alt_L && event->keyval != GDK_KEY_Alt_R)
1891 _alternative_mode(table, FALSE);
1892
1893 guint key = dt_keys_mainpad_alternatives(event->keyval);
1894 switch(key)
1895 {
1896 case GDK_KEY_Up:
1897 {
1899 {
1900 _move_in_grid(table, event, DT_TT_MOVE_UP, imgid);
1901 return TRUE;
1902 }
1903 break;
1904 }
1905 case GDK_KEY_Down:
1906 {
1908 {
1909 _move_in_grid(table, event, DT_TT_MOVE_DOWN, imgid);
1910 return TRUE;
1911 }
1912 break;
1913 }
1914 case GDK_KEY_Left:
1915 {
1916 _move_in_grid(table, event, DT_TT_MOVE_LEFT, imgid);
1917 return TRUE;
1918 }
1919 case GDK_KEY_Right:
1920 {
1921 _move_in_grid(table, event, DT_TT_MOVE_RIGHT, imgid);
1922 return TRUE;
1923 }
1924 case GDK_KEY_Page_Up:
1925 {
1926 _move_in_grid(table, event, DT_TT_MOVE_PREVIOUS_PAGE, imgid);
1927 return TRUE;
1928 }
1929 case GDK_KEY_Page_Down:
1930 {
1931 _move_in_grid(table, event, DT_TT_MOVE_NEXT_PAGE, imgid);
1932 return TRUE;
1933 }
1934 case GDK_KEY_Home:
1935 {
1936 _move_in_grid(table, event, DT_TT_MOVE_START, imgid);
1937 return TRUE;
1938 }
1939 case GDK_KEY_End:
1940 {
1941 _move_in_grid(table, event, DT_TT_MOVE_END, imgid);
1942 return TRUE;
1943 }
1944 case GDK_KEY_space:
1945 {
1947 {
1948 if(dt_modifier_is(event->state, GDK_SHIFT_MASK))
1949 {
1950 dt_pthread_mutex_lock(&table->lock);
1951 int rowid = _find_rowid_from_imgid(table, imgid);
1953 dt_thumbtable_select_range(table, rowid);
1954 }
1955 else if(dt_modifier_is(event->state, GDK_CONTROL_MASK))
1957 else
1959 return TRUE;
1960 }
1961 break;
1962 }
1963 case GDK_KEY_nobreakspace:
1964 {
1965 // Shift + space is decoded as nobreakspace on BÉPO keyboards
1967 {
1968 dt_pthread_mutex_lock(&table->lock);
1969 int rowid = _find_rowid_from_imgid(table, imgid);
1971 dt_thumbtable_select_range(table, rowid);
1972 return TRUE;
1973 }
1974 break;
1975 }
1976 case GDK_KEY_Return:
1977 {
1978 // This is only to be consistent with mouse events:
1979 // opening to darkroom happens with double click (aka ACTIVATE event),
1980 // but the first click always select the clicked thumbnail before.
1981 // So we do the same here, even though it's not required and actually slightly annoying.
1984
1986 return TRUE;
1987 }
1988 case GDK_KEY_Alt_L:
1989 case GDK_KEY_Alt_R:
1990 {
1991 _alternative_mode(table, TRUE);
1992 return TRUE;
1993 }
1994 case GDK_KEY_Delete:
1995 {
1997 return TRUE;
1998 }
1999 }
2000 return FALSE;
2001}
2002
2003gboolean dt_thumbtable_key_released_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
2004{
2005 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
2006
2007 if(IS_NULL_PTR(user_data)) return FALSE;
2008 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
2009
2010 //fprintf(stdout, "%s\n", gtk_accelerator_name(event->keyval, event->state));
2011 _alternative_mode(table, FALSE);
2012 return FALSE;
2013}
2014
2015
2016static gboolean _draw_callback(GtkWidget *widget, cairo_t *cr, gpointer user_data)
2017{
2018 if(IS_NULL_PTR(user_data)) return TRUE;
2019
2020 // Ensure the background color is painted
2021 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2022 GtkAllocation allocation;
2023 gtk_widget_get_allocation(widget, &allocation);
2024 gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);
2025 gtk_render_frame(context, cr, 0, 0, allocation.width, allocation.height);
2026
2027 return FALSE;
2028}
2029
2034
2035gboolean _event_main_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2036{
2037 if(IS_NULL_PTR(user_data)) return TRUE;
2039 return TRUE;
2040}
2041
2042
2044{
2045 dt_thumbtable_t *table = (dt_thumbtable_t *)calloc(1, sizeof(dt_thumbtable_t));
2046
2047 table->scroll_window = gtk_scrolled_window_new(NULL, NULL);
2048 // Named so the theme can zero its "scrollbar-spacing" style property (see dt_thumbtable_configure).
2049 gtk_widget_set_name(table->scroll_window, "thumbtable-scroll");
2050 gtk_scrolled_window_set_overlay_scrolling(GTK_SCROLLED_WINDOW(table->scroll_window), FALSE);
2051 // No frame: the image grid is full-bleed content and needs no recessed border.
2052 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(table->scroll_window), GTK_SHADOW_NONE);
2053
2054 table->v_scrollbar = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(table->scroll_window));
2055 table->h_scrollbar = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(table->scroll_window));
2056 g_signal_connect(G_OBJECT(table->v_scrollbar), "value-changed", G_CALLBACK(_scrollbar_value_changed), table);
2057 g_signal_connect(G_OBJECT(table->h_scrollbar), "value-changed", G_CALLBACK(_scrollbar_value_changed), table);
2058 g_signal_connect(G_OBJECT(table->v_scrollbar), "notify::page-size", G_CALLBACK(_scrollbar_page_size_notify), table);
2059 g_signal_connect(G_OBJECT(table->h_scrollbar), "notify::page-size", G_CALLBACK(_scrollbar_page_size_notify), table);
2060
2061 // Track actual scrollbar widget allocations: filmstrip sizing depends on the horizontal scrollbar height,
2062 // and filemanager fallback sizing depends on the vertical scrollbar width.
2063 GtkWidget *h_scroll = gtk_scrolled_window_get_hscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
2064 GtkWidget *v_scroll = gtk_scrolled_window_get_vscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
2065 if(h_scroll)
2066 g_signal_connect(G_OBJECT(h_scroll), "size-allocate", G_CALLBACK(_scrollbar_widget_size_allocate), table);
2067 if(v_scroll)
2068 g_signal_connect(G_OBJECT(v_scroll), "size-allocate", G_CALLBACK(_scrollbar_widget_size_allocate), table);
2069
2070 table->grid = gtk_fixed_new();
2071 dt_gui_add_class(table->grid, "dt_thumbtable");
2072 gtk_container_add(GTK_CONTAINER(table->scroll_window), table->grid);
2073
2074 // gtk_container_add() wraps the grid in an implicit GtkViewport whose default shadow is
2075 // GTK_SHADOW_IN (a .frame border). Drop it so it doesn't add to the scrolled window's size.
2076 GtkWidget *viewport = gtk_bin_get_child(GTK_BIN(table->scroll_window));
2077 if(GTK_IS_VIEWPORT(viewport))
2078 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
2079
2080 g_object_set_data(G_OBJECT(table->grid), DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY, GINT_TO_POINTER(1));
2081 gtk_widget_set_has_tooltip(table->grid, FALSE);
2082 gtk_widget_set_can_focus(table->grid, TRUE);
2083 gtk_widget_set_focus_on_click(table->grid, TRUE);
2084 gtk_widget_add_events(table->grid, GDK_LEAVE_NOTIFY_MASK);
2085 gtk_widget_set_app_paintable(table->grid, TRUE);
2086 g_signal_connect(G_OBJECT(table->grid), "leave-notify-event", G_CALLBACK(_event_main_leave), table);
2087
2088 // Disable auto re-scrolling to beginning when a child of scrolled window gets the focus
2089 // Doesn't seem to work...
2090 // https://stackoverflow.com/questions/26693042/gtkscrolledwindow-disable-scroll-to-focused-child
2091 GtkAdjustment *dummy = gtk_adjustment_new(0., 0., 1., 1., 1., 1.);
2092 gtk_container_set_focus_hadjustment(GTK_CONTAINER(table->scroll_window), dummy);
2093 gtk_container_set_focus_vadjustment(GTK_CONTAINER(table->scroll_window), dummy);
2094 gtk_container_set_focus_hadjustment(GTK_CONTAINER(table->grid), dummy);
2095 gtk_container_set_focus_vadjustment(GTK_CONTAINER(table->grid), dummy);
2096 g_object_unref(dummy);
2097
2098 // drag and drop : used for reordering, interactions with maps, exporting uri to external apps, importing images
2099 // in filmroll...
2100 gtk_drag_source_set(table->grid, GDK_BUTTON1_MASK, target_list_all, n_targets_all, GDK_ACTION_MOVE);
2101 gtk_drag_dest_set(table->grid, GTK_DEST_DEFAULT_ALL, target_list_all, n_targets_all, GDK_ACTION_MOVE);
2102 g_signal_connect_after(table->grid, "drag-begin", G_CALLBACK(_event_dnd_begin), table);
2103 g_signal_connect_after(table->grid, "drag-end", G_CALLBACK(_event_dnd_end), table);
2104 g_signal_connect(table->grid, "drag-data-get", G_CALLBACK(_event_dnd_get), table);
2105 g_signal_connect(table->grid, "drag-data-received", G_CALLBACK(dt_thumbtable_event_dnd_received), table);
2106
2107 gtk_widget_add_events(table->grid, GDK_STRUCTURE_MASK | GDK_EXPOSURE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
2108 g_signal_connect(table->grid, "draw", G_CALLBACK(_draw_callback), table);
2109 g_signal_connect(table->grid, "key-press-event", G_CALLBACK(dt_thumbtable_key_pressed_grid), table);
2110 g_signal_connect(table->grid, "key-release-event", G_CALLBACK(dt_thumbtable_key_released_grid), table);
2111 gtk_widget_show(table->grid);
2112
2113 table->thumb_nb = 0;
2114 table->grid_cols = 0;
2115 table->collection_inited = FALSE;
2116 table->configured = FALSE;
2117 table->thumbs_inited = FALSE;
2118 table->collection_hash = -1;
2119 table->list = g_hash_table_new(g_direct_hash, g_direct_equal);
2120 table->collection_count = 0;
2121 table->min_row_id = 0;
2122 table->max_row_id = 0;
2123 table->lut = NULL;
2124 table->reset_collection = FALSE;
2125 table->x_position = 0.;
2126 table->y_position = 0.;
2127 table->alternate_mode = FALSE;
2128 table->rowid = -1;
2129 table->collapse_groups = dt_conf_get_bool("ui_last/grouping");
2130 table->draw_group_borders = dt_conf_get_bool("plugins/lighttable/group_borders");
2131 table->idle_update_id = 0;
2132 table->focus_idle_id = 0;
2133 table->last_parent_width = 0;
2134 table->last_parent_height = 0;
2135 table->last_h_scrollbar_height = -1;
2136 table->last_v_scrollbar_width = -1;
2137
2138 dt_pthread_mutex_init(&table->lock, NULL);
2139
2140 dt_gui_add_help_link(table->grid, dt_get_help_url("lighttable_filemanager"));
2141
2142 // set css name and class
2143 gtk_widget_set_name(table->grid, "thumbtable-filemanager");
2144
2145 // overlays mode
2147
2148 // we register globals signals
2150 G_CALLBACK(_dt_collection_changed_callback), table);
2152 G_CALLBACK(_dt_selection_changed_callback), table);
2154 G_CALLBACK(_dt_profile_change_callback), table);
2156 G_CALLBACK(_dt_image_info_changed_callback), table);
2158 G_CALLBACK(_mouse_over_image_callback), table);
2159
2160 dt_thumbtable_set_parent(table, mode);
2161
2162 // The file manager belongs only to lighttable, while the filmstrip widget is
2163 // shared by darkroom, print and map. Each owner view needs its own accel
2164 // paths so shortcut search, menus and dispatch stay in sync with the
2165 // currently active accel group.
2166 GtkAccelGroup *accel_groups[] = {
2171 };
2172 const char *path_bases[] = {
2173 _("Lighttable/Thumbtable"),
2174 _("Darkroom/Filmstrip"),
2175 _("Print/Filmstrip"),
2176 _("Map/Filmstrip")
2177 };
2178 const int first_group = (mode == DT_THUMBTABLE_MODE_FILEMANAGER) ? 0 : 1;
2179 const int last_group = (mode == DT_THUMBTABLE_MODE_FILEMANAGER) ? 1 : 4;
2180
2181 gchar *path = NULL;
2182 for(int group_index = first_group; group_index < last_group; group_index++)
2183 {
2184 GtkAccelGroup *accel_group = accel_groups[group_index];
2185 const char *path_base = path_bases[group_index];
2186
2187 path = dt_accels_build_path(path_base, _("Move up"));
2189 path, table->grid, GDK_KEY_Up, 0);
2190 dt_free(path);
2191 path = dt_accels_build_path(path_base, _("Move down"));
2193 path, table->grid, GDK_KEY_Down, 0);
2194 dt_free(path);
2195 path = dt_accels_build_path(path_base, _("Move left"));
2197 path, table->grid, GDK_KEY_Left, 0);
2198 dt_free(path);
2199 path = dt_accels_build_path(path_base, _("Move right"));
2201 path, table->grid, GDK_KEY_Right, 0);
2202 dt_free(path);
2203 path = dt_accels_build_path(path_base, _("Go to previous page"));
2205 path, table->grid, GDK_KEY_Page_Up, 0);
2206 dt_free(path);
2207 path = dt_accels_build_path(path_base, _("Go to next page"));
2209 path, table->grid, GDK_KEY_Page_Down, 0);
2210 dt_free(path);
2211 path = dt_accels_build_path(path_base, _("Go to the start"));
2213 path, table->grid, GDK_KEY_Home, 0);
2214 dt_free(path);
2215 path = dt_accels_build_path(path_base, _("Go to the end"));
2217 path, table->grid, GDK_KEY_End, 0);
2218 dt_free(path);
2219 path = dt_accels_build_path(path_base, _("Select the current thumbnail"));
2221 path, table->grid, GDK_KEY_space, 0);
2222 dt_free(path);
2223 path = dt_accels_build_path(path_base, _("Toogle the current thumbnail from selection"));
2225 path, table->grid, GDK_KEY_space, GDK_CONTROL_MASK);
2226 dt_free(path);
2227 path = dt_accels_build_path(path_base, _("Expand the current selection up to the hovered thumbnail"));
2229 path, table->grid, GDK_KEY_space, GDK_SHIFT_MASK);
2230 dt_free(path);
2231 path = dt_accels_build_path(path_base, _("Open the current thumbnail in darkroom"));
2233 path, table->grid, GDK_KEY_Return, 0);
2234 dt_free(path);
2235 path = dt_accels_build_path(path_base, _("Enable thumbnail transient alternative view"));
2237 path, table->grid, GDK_KEY_Alt_L, 0);
2238 dt_free(path);
2239 }
2240
2241 path = dt_accels_build_path(_("Global/Menu/File"), _("Remove image from library"));
2243 path, table->grid, GDK_KEY_Delete, 0);
2244 dt_free(path);
2245 return table;
2246}
2247
2248
2250{
2251 // Cleanup existing stuff
2252 const double start = dt_get_wtime();
2253
2254 dt_pthread_mutex_lock(&table->lock);
2255 if(table->lut)
2256 for(int rowid = 0; rowid < table->collection_count; rowid++)
2257 table->lut[rowid].thumb = NULL;
2258
2259 // WARNING: we need to detach children from parent starting from the last
2260 // otherwise, Gtk updates the index of all the next children in sequence
2261 // and that takes forever when thumb_nb > 1000
2262 GList *thumbs = g_hash_table_get_values(table->list);
2263 thumbs = g_list_sort(thumbs, _thumb_compare_rowid_desc);
2264 g_hash_table_remove_all(table->list);
2266
2267 for(GList *l = thumbs; l; l = g_list_next(l))
2268 {
2269 dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
2270 gtk_widget_hide(thumb->widget);
2271 dt_thumbnail_destroy(thumb);
2272 }
2273 g_list_free(thumbs);
2274 thumbs = NULL;
2275
2276 dt_print(DT_DEBUG_LIGHTTABLE, "Cleaning the list of %i elements in %0.04f sec\n", table->thumb_nb,
2277 dt_get_wtime() - start);
2278
2279 table->thumb_nb = 0;
2280 table->thumbs_inited = FALSE;
2281}
2282
2283
2285{
2286 if(table->idle_update_id)
2287 {
2288 g_source_remove(table->idle_update_id);
2289 table->idle_update_id = 0;
2290 }
2291 if(table->focus_idle_id)
2292 {
2293 g_source_remove(table->focus_idle_id);
2294 table->focus_idle_id = 0;
2295 }
2296
2301
2303
2305
2307
2308 g_hash_table_destroy(table->list);
2309 if(table->lut)
2310 {
2311 dt_free(table->lut);
2312 }
2313
2314 dt_free(table);
2315}
2316
2318{
2319 if(IS_NULL_PTR(table)) return;
2320
2321 if(table->idle_update_id)
2322 {
2323 g_source_remove(table->idle_update_id);
2324 table->idle_update_id = 0;
2325 }
2326 if(table->focus_idle_id)
2327 {
2328 g_source_remove(table->focus_idle_id);
2329 table->focus_idle_id = 0;
2330 }
2331
2332 table->reset_collection = TRUE;
2333
2334 dt_pthread_mutex_lock(&table->lock);
2335 if(table->lut)
2336 for(int rowid = 0; rowid < table->collection_count; rowid++)
2337 table->lut[rowid].thumb = NULL;
2338
2339 GList *thumbs = g_hash_table_get_values(table->list);
2340 thumbs = g_list_sort(thumbs, _thumb_compare_rowid_desc);
2341 g_hash_table_remove_all(table->list);
2343
2344 for(GList *l = thumbs; l; l = g_list_next(l))
2345 {
2346 dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
2347 gtk_widget_hide(thumb->widget);
2348 dt_thumbnail_destroy(thumb);
2349 }
2350 g_list_free(thumbs);
2351
2352 table->thumb_nb = 0;
2353 table->thumbs_inited = FALSE;
2354}
2355
2357{
2358 _thumbtable_schedule_focus(table, G_PRIORITY_DEFAULT_IDLE);
2359}
2360
2362{
2363 table->mode = mode;
2364 table->parent_overlay = gtk_overlay_new();
2365 gtk_overlay_add_overlay(GTK_OVERLAY(table->parent_overlay), table->scroll_window);
2366 g_signal_connect(G_OBJECT(table->parent_overlay), "size-allocate", G_CALLBACK(_parent_overlay_size_allocate), table);
2367
2369 {
2370 gtk_widget_set_name(table->grid, "thumbtable-filemanager");
2371 dt_gui_add_help_link(table->grid, dt_get_help_url("lighttable_filemanager"));
2372 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(table->scroll_window), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
2373 }
2374 else if(mode == DT_THUMBTABLE_MODE_FILMSTRIP)
2375 {
2376 gtk_widget_set_name(table->grid, "thumbtable-filmstrip");
2377 dt_gui_add_help_link(table->grid, dt_get_help_url("filmstrip"));
2378 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(table->scroll_window), GTK_POLICY_ALWAYS, GTK_POLICY_NEVER);
2379 }
2380}
2381
2383{
2384 if(!table->collection_inited || table->collection_count == 0) return;
2385
2386 if(table->collapse_groups)
2387 dt_control_log(_("Image groups are collapsed in view.\n"
2388 "Selecting all images will only target visible members of image groups.\n"
2389 "Uncollapse groups to select all their members"));
2390
2391 GList *img = NULL;
2392
2393 dt_pthread_mutex_lock(&table->lock);
2394 for(size_t i = 0; i < table->collection_count; i++)
2395 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2397
2398 if(img)
2399 {
2401 g_list_free(img);
2402 img = NULL;
2403 }
2404}
2405
2406void dt_thumbtable_select_range(dt_thumbtable_t *table, const int rowid)
2407{
2408 if(!table->collection_inited || table->collection_count == 0) return;
2409 if(rowid < 0 || rowid > table->collection_count - 1) return;
2410
2411 if(table->collapse_groups)
2412 dt_control_log(_("Image groups are collapsed in view.\n"
2413 "Selecting a range of images will only target visible members of image groups.\n"
2414 "Uncollapse groups to select all their members"));
2415
2416 dt_pthread_mutex_lock(&table->lock);
2417
2418 // Find the bounds of the current selection
2419 size_t rowid_end = 0;
2420 size_t rowid_start = table->collection_count - 1;
2421 GList *selected = dt_selection_get_list(darktable.selection);
2422
2423 if(selected)
2424 {
2425 for(GList *s = g_list_first(selected); s; s = g_list_next(s))
2426 {
2427 int32_t imgid = GPOINTER_TO_INT(s->data);
2428 int row = _find_rowid_from_imgid(table, imgid);
2429 if(row < 0) continue; // not found - should not happen
2430 if(row < rowid_start) rowid_start = row;
2431 if(row > rowid_end) rowid_end = row;
2432 }
2433 g_list_free(selected);
2434 selected = NULL;
2435 }
2436 else
2437 {
2438 // range selection always has to start from a previous selection
2440 return;
2441 }
2442
2443 if(rowid_start > rowid_end)
2444 {
2445 // the start is strictly after the end, we have a deep problem
2447 return;
2448 }
2449
2450 // Find the extra imgids to select
2451 GList *img = NULL;
2452 if(rowid > rowid_end && rowid_end < table->collection_count - 1)
2453 {
2454 // select after
2455 for(int i = rowid_end + 1; i <= rowid; i++)
2456 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2457 }
2458 else if(rowid < rowid_start && rowid_start > 0)
2459 {
2460 // select before
2461 for(int i = rowid_start - 1; i >= rowid; i--)
2462 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2463 }
2464 // else: select within. What should that yield ??? Deselect ?
2465
2467
2468 if(img)
2469 {
2471 g_list_free(img);
2472 img = NULL;
2473 }
2474}
2475
2477{
2478 if(!table->collection_inited || table->collection_count == 0) return;
2479
2480 // Record initial selection, select all, then deselect initial selection
2481 GList *to_deselect = dt_selection_get_list(darktable.selection);
2482 if(to_deselect)
2483 {
2486 g_list_free(to_deselect);
2487 to_deselect = NULL;
2488 }
2489}
2490
2491static gint64 next_over_time = 0;
2492
2493void dt_thumbtable_dispatch_over(dt_thumbtable_t *table, GdkEventType type, int32_t imgid)
2494{
2495 if(!gtk_widget_is_visible(table->scroll_window)) return;
2496
2497 gint64 current_time = g_get_real_time(); // microseconds
2498 if(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE)
2499 {
2500 // allow the mouse to capture the next hover events
2501 // in more than 100 ms:
2502 next_over_time = current_time + 100000;
2503
2506 }
2507 else if(type == GDK_ENTER_NOTIFY || type == GDK_LEAVE_NOTIFY)
2508 {
2509 // When navigating the grid with arrow keys, the view will get scrolled.
2510 // If the mouse pointer is over the grid, it will enter a new thumbnail
2511 // which will trigger leave/enter events.
2512 // But we don't want that when interacting from the keyboard, so disallow
2513 // recording enter/leave events in the next 100 ms after keyboard interaction.
2514 if(current_time > next_over_time)
2516 else
2517 return;
2518 }
2519 else if(type == GDK_MOTION_NOTIFY || type == GDK_BUTTON_PRESS || type == GDK_2BUTTON_PRESS)
2520 {
2521 // Active mouse pointer interactions: accept unconditionnaly
2523 }
2524 else
2525 {
2526 fprintf(stderr, "[dt_thumbtable_dispatch_over] unsupported event type: %i\n", type);
2527 return;
2528 }
2529
2530 dt_pthread_mutex_lock(&table->lock);
2531 table->rowid = _find_rowid_from_imgid(table, imgid);
2533
2534 // Attempt to re-grab focus on every interaction to restore keyboard navigation,
2535 // for example after a combobox grabbed it on click.
2536 if(!gtk_widget_has_focus(table->grid))
2537 {
2538 // But giving focus to the grid scrolls it back to top, so we have to re-scroll it after
2539 double x = 0.;
2540 double y = 0.;
2542 gtk_widget_grab_focus(table->grid);
2544 }
2545}
2546
2547// clang-format off
2548// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
2549// vim: shiftwidth=2 expandtab tabstop=2 cindent
2550// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
2551// clang-format on
void dt_accels_new_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *accel_group, const gchar *accel_path, GtkWidget *widget, guint key_val, GdkModifierType accel_mods)
Add a new virtual shortcut. Virtual shortcuts are immutable, read-only and don't trigger any action....
gchar * dt_accels_build_path(const gchar *scope, const gchar *feature)
Handle default and user-set shortcuts (accelerators)
#define DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY
GList * dt_act_on_get_images()
Definition act_on.c:39
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int position()
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
const gchar * dt_collection_get_query(const dt_collection_t *collection)
Definition collection.c:552
uint32_t dt_collection_get_count(const dt_collection_t *collection)
Definition collection.c:836
dt_collection_properties_t
Definition collection.h:107
@ DT_COLLECTION_PROP_GROUPING
Definition collection.h:130
dt_collection_change_t
Definition collection.h:147
dt_iop_colorout_gui_data_t dummy
Definition colorout.c:828
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
void dt_image_init(dt_image_t *img)
void dt_image_full_path(const int32_t imgid, char *pathname, size_t pathname_len, gboolean *from_cache, const char *calling_func)
Get the full path of an image out of the database.
char * key
int type
char * name
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
int32_t dt_control_get_mouse_over_id()
Definition control.c:923
void dt_control_set_mouse_over_id(int32_t value)
Definition control.c:931
void dt_control_log(const char *msg,...)
Definition control.c:761
int32_t dt_control_get_keyboard_over_id()
Definition control.c:949
void dt_control_set_keyboard_over_id(int32_t value)
Definition control.c:957
gboolean dt_control_remove_images()
darktable_t darktable
Definition darktable.c:181
gboolean dt_supported_image(const gchar *filename)
check if file is a supported image
Definition darktable.c:312
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1542
#define UNKNOWN_IMAGE
Definition darktable.h:182
@ DT_DEBUG_LIGHTTABLE
Definition darktable.h:725
#define g_list_is_singleton(list)
Definition darktable.h:938
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:463
#define dt_free(ptr)
Definition darktable.h:456
static uint64_t dt_hash(uint64_t hash, const char *str, size_t size)
Definition darktable.h:1043
static const dt_aligned_pixel_simd_t value
Definition darktable.h:577
static double dt_get_wtime(void)
Definition darktable.h:914
static gboolean dt_modifier_is(const GdkModifierType state, const GdkModifierType desired_modifier_mask)
Definition darktable.h:893
#define PATH_MAX
Definition darktable.h:1062
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:281
static const GtkTargetEntry target_list_all[]
#define _DWORD
@ DND_TARGET_URI
@ DND_TARGET_IMGID
#define _BYTE
static const guint n_targets_all
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:374
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:359
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:379
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:364
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:113
void dt_gui_remove_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:143
void dt_gui_add_help_link(GtkWidget *widget, char *link)
Definition gtk.c:2022
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
static gboolean enable(dt_image_t *image)
void dt_image_cache_read_release(dt_image_cache_t *cache, const dt_image_t *img)
dt_image_t * dt_image_cache_get(dt_image_cache_t *cache, const int32_t imgid, char mode)
void dt_image_from_stmt(dt_image_t *img, sqlite3_stmt *stmt)
dt_image_t * dt_image_cache_testget(dt_image_cache_t *cache, const int32_t imgid, char mode)
void dt_control_import(dt_control_import_t data)
Process a list of images to import with or without copying the files on an arbitrary hard-drive.
static const float x
const float *const lut
int32_t dt_selection_get_first_id(struct dt_selection_t *selection)
Definition selection.c:69
void dt_selection_deselect_list(struct dt_selection_t *selection, const GList *const l)
Definition selection.c:343
GList * dt_selection_get_list(struct dt_selection_t *selection)
Definition selection.c:172
gboolean dt_selection_is_id_selected(struct dt_selection_t *selection, int32_t imgid)
Definition selection.c:393
void dt_selection_select_list(struct dt_selection_t *selection, const GList *const l)
Definition selection.c:320
void dt_selection_deselect(dt_selection_t *selection, int32_t imgid)
Definition selection.c:281
void dt_selection_select_single(dt_selection_t *selection, int32_t imgid)
Definition selection.c:289
void dt_selection_toggle(dt_selection_t *selection, int32_t imgid)
Definition selection.c:296
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:368
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:347
@ DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE
Definition signal.h:95
@ DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED
This signal is raised when a profile is changed by the user 1 uint32_t : the profile type that has ch...
Definition signal.h:242
@ DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE
This signal is raised when mouse hovers over image thumbs both on lighttable and in the filmstrip....
Definition signal.h:59
@ DT_SIGNAL_IMAGE_INFO_CHANGED
This signal is raised when any of image info has changed
Definition signal.h:144
@ DT_SIGNAL_SELECTION_CHANGED
This signal is raised when the selection is changed no param, no returned value.
Definition signal.h:127
@ DT_SIGNAL_VIEWMANAGER_FILMSTRIP_DRAG_BEGIN
This signal is raised when a drag starts from the filmstrip. Views that need filmstrip drags to commi...
Definition signal.h:111
@ DT_SIGNAL_COLLECTION_CHANGED
This signal is raised when collection changed. To avoid leaking the list, dt_collection_t is connecte...
Definition signal.h:122
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:357
struct _GtkWidget GtkWidget
Definition splash.h:29
unsigned __int64 uint64_t
Definition strptime.c:75
struct dt_gui_gtk_t * gui
Definition darktable.h:775
struct dt_collection_t * collection
Definition darktable.h:781
struct dt_selection_t * selection
Definition darktable.h:782
struct dt_control_signal_t * signals
Definition darktable.h:774
struct dt_image_cache_t * image_cache
Definition darktable.h:777
struct dt_view_manager_t * view_manager
Definition darktable.h:772
GtkAccelGroup * map_accels
GtkAccelGroup * global_accels
GtkAccelGroup * print_accels
GtkAccelGroup * lighttable_accels
GtkAccelGroup * darkroom_accels
dt_accels_t * accels
Definition gtk.h:194
dt_ui_t * ui
Definition gtk.h:164
int32_t group_id
Definition image.h:319
uint32_t group_members
Definition image.h:320
int32_t id
Definition image.h:319
gboolean mouse_over
Definition thumbnail.h:73
dt_pthread_mutex_t lock
Definition thumbnail.h:130
gboolean disable_actions
Definition thumbnail.h:104
dt_image_t info
Definition thumbnail.h:76
gboolean selected
Definition thumbnail.h:74
int32_t rowid
Definition thumbnail.h:68
GtkWidget * w_image
Definition thumbnail.h:84
dt_thumbnail_overlay_t over
Definition thumbnail.h:106
cairo_surface_t * img_surf
Definition thumbnail.h:85
GtkWidget * widget
Definition thumbnail.h:79
Cache entry for a single thumbnail.
Definition thumbtable.h:88
dt_thumbnail_t * thumb
Definition thumbtable.h:89
GHashTable * list
Definition thumbtable.h:108
gboolean focus_peaking
Definition thumbtable.h:179
gboolean collapse_groups
Definition thumbtable.h:172
GtkWidget * grid
Definition thumbtable.h:100
gboolean alternate_mode
Definition thumbtable.h:164
gboolean collection_inited
Definition thumbtable.h:130
dt_thumbtable_mode_t mode
Definition thumbtable.h:97
dt_pthread_mutex_t lock
Definition thumbtable.h:158
int last_h_scrollbar_height
Definition thumbtable.h:192
GtkAdjustment * v_scrollbar
Definition thumbtable.h:147
dt_thumbtable_zoom_t zoom
Definition thumbtable.h:175
int last_v_scrollbar_width
Definition thumbtable.h:193
gboolean focus_regions
Definition thumbtable.h:178
GList * drag_list
Definition thumbtable.h:122
gboolean configured
Definition thumbtable.h:132
GtkWidget * scroll_window
Definition thumbtable.h:144
dt_thumbtable_cache_t * lut
Definition thumbtable.h:142
gboolean thumbs_inited
Definition thumbtable.h:131
GtkAdjustment * h_scrollbar
Definition thumbtable.h:148
uint32_t thumb_nb
Definition thumbtable.h:125
dt_thumbnail_overlay_t overlays
Definition thumbtable.h:98
gboolean reset_collection
Definition thumbtable.h:161
GtkWidget * parent_overlay
Definition thumbtable.h:153
uint64_t collection_hash
Definition thumbtable.h:135
gboolean draw_group_borders
Definition thumbtable.h:181
dt_thumbtable_t * thumbtable_lighttable
GtkWidget * main_window
dt_thumbtable_t * thumbtable_filmstrip
int32_t image_info_id
Definition view.h:210
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
dt_thumbnail_t * dt_thumbnail_new(int rowid, dt_thumbnail_overlay_t over, dt_thumbtable_t *table, dt_image_t *info)
Definition thumbnail.c:1338
void dt_thumbnail_set_mouseover(dt_thumbnail_t *thumb, gboolean over)
Definition thumbnail.c:1611
void dt_thumbnail_resync_info(dt_thumbnail_t *thumb, const dt_image_t *const info)
Definition thumbnail.c:1318
void dt_thumbnail_set_overlay(dt_thumbnail_t *thumb, dt_thumbnail_overlay_t mode)
Definition thumbnail.c:1455
void dt_thumbnail_set_group_border(dt_thumbnail_t *thumb, dt_thumbnail_border_t border)
Definition thumbnail.c:1586
int dt_thumbnail_destroy(dt_thumbnail_t *thumb)
Definition thumbnail.c:1366
void dt_thumbnail_update_gui(dt_thumbnail_t *thumb)
Definition thumbnail.c:1441
void dt_thumbnail_update_selection(dt_thumbnail_t *thumb, gboolean selected)
Definition thumbnail.c:870
void dt_thumbnail_alternative_mode(dt_thumbnail_t *thumb, gboolean enable)
Definition thumbnail.c:911
void dt_thumbnail_resize(dt_thumbnail_t *thumb, int width, int height)
Definition thumbnail.c:1547
#define dt_thumbnail_image_refresh(thumb)
Definition thumbnail.h:165
static dt_thumbnail_overlay_t sanitize_overlays(dt_thumbnail_overlay_t overlays)
Definition thumbnail.h:173
dt_thumbnail_border_t
Definition thumbnail.h:49
@ DT_THUMBNAIL_BORDER_BOTTOM
Definition thumbnail.h:54
@ DT_THUMBNAIL_BORDER_TOP
Definition thumbnail.h:52
@ DT_THUMBNAIL_BORDER_RIGHT
Definition thumbnail.h:53
@ DT_THUMBNAIL_BORDER_LEFT
Definition thumbnail.h:51
dt_thumbnail_overlay_t
Definition thumbnail.h:58
@ DT_THUMBNAIL_OVERLAYS_HOVER_NORMAL
Definition thumbnail.h:60
@ DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL
Definition thumbnail.h:61
@ DT_THUMBNAIL_OVERLAYS_NONE
Definition thumbnail.h:59
void dt_thumbtable_set_zoom(dt_thumbtable_t *table, dt_thumbtable_zoom_t level)
static int _position_to_rowid(dt_thumbtable_t *table, const double x, const double y)
Definition thumbtable.c:464
void _add_thumbnail_at_rowid(dt_thumbtable_t *table, const size_t rowid, const int32_t mouse_over)
Definition thumbtable.c:962
static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
Definition thumbtable.c:320
int dt_thumbtable_scroll_to_selection(dt_thumbtable_t *table)
Scroll to show selected content.
Definition thumbtable.c:572
gboolean _event_main_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
int dt_thumbtable_scroll_to_imgid(dt_thumbtable_t *table, int32_t imgid)
Scroll the view to show a specific image.
Definition thumbtable.c:539
static void _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)
gboolean dt_thumbtable_get_focus_peaking(dt_thumbtable_t *table)
static gboolean _thumbtable_idle_update(gpointer user_data)
Definition thumbtable.c:209
void dt_thumbtable_update(dt_thumbtable_t *table)
void dt_thumbtable_offset_zoom(dt_thumbtable_t *table, const double delta_x, const double delta_y)
static void _thumbtable_drag_set_icon(dt_thumbtable_t *table, GdkDragContext *context)
void dt_thumbtable_set_active_rowid(dt_thumbtable_t *table)
Update internal active row tracking.
Definition thumbtable.c:496
void dt_thumbtable_get_scroll_position(dt_thumbtable_t *table, double *x, double *y)
Definition thumbtable.c:490
static gint _thumb_compare_rowid_desc(gconstpointer a, gconstpointer b)
Definition thumbtable.c:170
void _move_in_grid(dt_thumbtable_t *table, GdkEventKey *event, dt_thumbtable_direction_t direction, int origin_imgid)
static int _grab_focus(dt_thumbtable_t *table)
Definition thumbtable.c:180
static int dt_thumbtable_scroll_to_position(dt_thumbtable_t *table, const double x, const double y)
Definition thumbtable.c:504
static void _thumbs_update_overlays_mode(dt_thumbtable_t *table)
void dt_thumbtable_apply_grid_configuration(dt_thumbtable_t *table)
Apply grid configuration changes with proper event synchronization.
Definition thumbtable.c:286
void dt_thumbtable_configure(dt_thumbtable_t *table)
Definition thumbtable.c:765
void dt_thumbtable_cleanup(dt_thumbtable_t *table)
static gint64 next_over_time
void _grid_configure(dt_thumbtable_t *table, int width, int height, int per_row, int thumb_width, int thumb_height)
Definition thumbtable.c:718
gboolean _is_rowid_visible(dt_thumbtable_t *table, int rowid)
Definition thumbtable.c:621
int _imgid_to_rowid(dt_thumbtable_t *table, int32_t imgid)
gboolean dt_thumbtable_get_focus_regions(dt_thumbtable_t *table)
void dt_thumbtable_set_focus_peaking(dt_thumbtable_t *table, gboolean enable)
static int _find_rowid_from_imgid(dt_thumbtable_t *table, const int32_t imgid)
Definition thumbtable.c:528
GList * _thumbtable_dnd_import_check(GList *files, const char *pathname, int *elements)
void dt_thumbtable_event_dnd_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, gpointer user_data)
Handle drag-and-drop data received.
static gboolean _thumbtable_dnd_import(GtkSelectionData *selection_data)
static void _scrollbar_widget_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
Definition thumbtable.c:350
static void _dt_collection_lut(dt_thumbtable_t *table)
void _resize_thumbnails(dt_thumbtable_t *table)
void _populate_thumbnails(dt_thumbtable_t *table)
void dt_thumbtable_invert_selection(dt_thumbtable_t *table)
Invert the current selection.
void _dt_thumbtable_empty_list(dt_thumbtable_t *table)
static gboolean _dt_collection_get_hash(dt_thumbtable_t *table)
static void _event_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
gboolean dt_thumbtable_get_thumbnail_info(dt_thumbtable_t *table, int32_t imgid, dt_image_t *out)
Definition thumbtable.c:874
gboolean _update_row_ids(dt_thumbtable_t *table)
Definition thumbtable.c:650
static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data)
Definition thumbtable.c:308
static void _thumbtable_schedule_update(dt_thumbtable_t *table)
Definition thumbtable.c:221
static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
Definition thumbtable.c:337
void _add_thumbnail_group_borders(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
Definition thumbtable.c:906
void dt_thumbtable_stop(dt_thumbtable_t *table)
dt_thumbtable_direction_t
@ DT_TT_MOVE_LEFT
@ DT_TT_MOVE_RIGHT
@ DT_TT_MOVE_DOWN
@ DT_TT_MOVE_NEXT_PAGE
@ DT_TT_MOVE_UP
@ DT_TT_MOVE_START
@ DT_TT_MOVE_PREVIOUS_PAGE
@ DT_TT_MOVE_END
static gboolean _thumbtable_idle_apply_grid_configuration(gpointer user_data)
Idle callback for applying grid configuration.
Definition thumbtable.c:246
void dt_thumbtable_set_focus_regions(dt_thumbtable_t *table, gboolean enable)
static void dt_thumbtable_scroll_to_rowid(dt_thumbtable_t *table, int rowid)
Definition thumbtable.c:511
void _update_grid_area(dt_thumbtable_t *table)
Definition thumbtable.c:665
int dt_thumbtable_scroll_to_active_rowid(dt_thumbtable_t *table)
Scroll to show the active row.
Definition thumbtable.c:562
gboolean dt_thumbtable_get_draw_group_borders(dt_thumbtable_t *table)
void dt_thumbtable_dispatch_over(dt_thumbtable_t *table, GdkEventType type, int32_t imgid)
Update the mouse-over image ID with conflict resolution.
static gboolean _thumbtable_clone_lut(dt_thumbtable_t *dst)
Definition thumbtable.c:72
void _alternative_mode(dt_thumbtable_t *table, gboolean enable)
gboolean dt_thumbtable_key_pressed_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
#define CLAMP_ROW(rowid)
Definition thumbtable.c:903
void dt_thumbtable_set_overlays_mode(dt_thumbtable_t *table, dt_thumbnail_overlay_t over)
Set the overlay display mode for thumbnails.
static void _rowid_to_position(dt_thumbtable_t *table, int rowid, int *x, int *y)
Definition thumbtable.c:445
static gboolean _draw_callback(GtkWidget *widget, cairo_t *cr, gpointer user_data)
static void _dt_profile_change_callback(gpointer instance, int type, gpointer user_data)
static gboolean _get_row_ids(dt_thumbtable_t *table, int *rowid_min, int *rowid_max)
Definition thumbtable.c:583
void dt_thumbtable_refresh_thumbnail_real(dt_thumbtable_t *table, int32_t imgid, gboolean reinit)
void dt_thumbtable_select_all(dt_thumbtable_t *table)
Select all images in the current grid.
static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
void dt_thumbtable_reset_collection(dt_thumbtable_t *table)
gboolean dt_thumbtable_key_released_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
dt_thumbtable_zoom_t dt_thumbtable_get_zoom(dt_thumbtable_t *table)
static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
#define IS_COLLECTION_EDGE(rowid)
Definition thumbtable.c:904
static gboolean _set_thumb_position(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
Definition thumbtable.c:482
static void _event_dnd_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, const guint target_type, const guint time, gpointer user_data)
void dt_thumbtable_set_parent(dt_thumbtable_t *table, dt_thumbtable_mode_t mode)
static gchar * _thumbs_get_overlays_class(dt_thumbnail_overlay_t over)
void dt_thumbtable_queue_update(dt_thumbtable_t *table)
Definition thumbtable.c:229
dt_thumbtable_t * dt_thumbtable_new(dt_thumbtable_mode_t mode)
Create a new thumbnail table widget.
void dt_thumbtable_set_draw_group_borders(dt_thumbtable_t *table, gboolean enable)
void _mouse_over_image_callback(gpointer instance, gpointer user_data)
Definition thumbtable.c:378
void dt_thumbtable_select_range(dt_thumbtable_t *table, const int rowid)
Select a range of images in the collection.
static void _dt_image_info_changed_callback(gpointer instance, gpointer imgs, gpointer user_data)
static void _thumbtable_schedule_focus(dt_thumbtable_t *table, const gint priority)
Definition thumbtable.c:203
dt_thumbnail_t * _find_thumb_by_imgid(dt_thumbtable_t *table, const int32_t imgid)
Definition thumbtable.c:868
static int _thumb_cell_decoration(void)
Definition thumbtable.c:744
void dt_thumbtable_update_parent(dt_thumbtable_t *table)
A widget to manage and display image thumbnails in Ansel's lighttable and filmstrip views.
dt_thumbtable_mode_t
Display modes for the thumbnail table.
Definition thumbtable.h:62
@ DT_THUMBTABLE_MODE_FILMSTRIP
Definition thumbtable.h:65
@ DT_THUMBTABLE_MODE_FILEMANAGER
Definition thumbtable.h:64
dt_thumbtable_zoom_t
Zoom levels for thumbnail display.
Definition thumbtable.h:75
#define dt_thumbtable_refresh_thumbnail(table, imgid, reinit)
Definition thumbtable.h:283
void dt_thumbtable_info_cleanup(void)
void dt_thumbtable_info_debug_assert_matches_cache(const dt_image_t *sql_info)
void dt_thumbtable_info_seed_image_cache(const dt_image_t *info)
sqlite3_stmt * dt_thumbtable_info_get_collection_stmt(void)
static gboolean dt_thumbtable_info_is_grouped(const dt_image_t info)
char * dt_get_help_url(char *name)
gchar * dt_util_glist_to_str(const gchar *separator, GList *items)
Definition utility.c:166
gboolean dt_view_active_images_has_imgid(int32_t imgid)
Definition view.c:1296
void dt_view_image_info_update(int32_t imgid)
Definition view.c:1470