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*/
44#include "common/paths.h" // DT_PATH_MAX
46#include "common/act_on.h"
47#include "control/settings.h"
48#include "common/conf.h"
50#include "widgets/gdkkeys.h"
53#include "gui/dtgtk/thumbnail.h"
55#include "common/collection.h"
56#include "history/history.h"
57#include "caches/image_cache.h"
59#include "common/hash.h"
60#include "common/selection.h"
61#include "common/times.h"
63#include "common/utility.h"
64#include "control/control.h"
66
69#include "gui/drag_and_drop.h"
70#include "views/view.h"
71#include "widgets/bauhaus.h"
72#include "gui/import.h"
73
74#ifdef GDK_WINDOWING_QUARTZ
75#include "osx/osx.h"
76#endif
77
78#include <glib-object.h>
79#include <math.h>
80#include <glib.h>
81#include "gui/application.h"
83#include "common/glib_utils.h"
84
85
87{
89
90 dt_thumbtable_t *src = NULL;
91 if(dt_gui_get_ui()->thumbtable_lighttable == dst)
93 else if(dt_gui_get_ui()->thumbtable_filmstrip == dst)
95
96 if(IS_NULL_PTR(src) || src == dst) return FALSE;
97
99 const gboolean can_clone = (src->lut
100 && src->collection_inited
101 && src->collection_hash == dst->collection_hash
102 && src->collapse_groups == dst->collapse_groups
103 && src->collection_count > 0);
104 if(!can_clone)
105 {
107 return FALSE;
108 }
109
110 const uint32_t count = src->collection_count;
111 dt_thumbtable_cache_t *cloned_lut = malloc(count * sizeof(dt_thumbtable_cache_t));
112 if(IS_NULL_PTR(cloned_lut))
113 {
115 return FALSE;
116 }
117
118 memcpy(cloned_lut, src->lut, count * sizeof(dt_thumbtable_cache_t));
120
121 for(uint32_t i = 0; i < count; i++)
122 cloned_lut[i].thumb = NULL;
123
124 dt_thumbtable_cache_t *old_lut = NULL;
126 old_lut = dst->lut;
127 dst->lut = cloned_lut;
128 dst->collection_count = count;
129 dst->collection_inited = TRUE;
131
132 dt_free(old_lut);
133 return TRUE;
134}
135
176
177static gboolean _thumbtable_idle_update(gpointer user_data);
179static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data);
180static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data);
181static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data);
182static void _scrollbar_widget_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data);
183
184static gint _thumb_compare_rowid_desc(gconstpointer a, gconstpointer b)
185{
186 const dt_thumbnail_t *thumb_a = (const dt_thumbnail_t *)a;
187 const dt_thumbnail_t *thumb_b = (const dt_thumbnail_t *)b;
188
189 if(thumb_a->rowid < thumb_b->rowid) return 1;
190 if(thumb_a->rowid > thumb_b->rowid) return -1;
191 return 0;
192}
193
194static int _grab_focus(dt_thumbtable_t *table)
195{
196 if(IS_NULL_PTR(table)) return 0;
197 table->focus_idle_id = 0;
198
199 if(table->ops->grab_focus) table->ops->grab_focus(table);
201 return 0;
202}
203
204void dt_thumbtable_schedule_focus_real(dt_thumbtable_t *table, const gint priority)
205{
206 if(IS_NULL_PTR(table) || table->focus_idle_id) return;
207 dt_print(DT_DEBUG_LIGHTTABLE, "[thumbtable] schedule_focus queued (priority=%d)\n", priority);
208 table->focus_idle_id = g_idle_add_full(priority, (GSourceFunc)_grab_focus, table, NULL);
209}
210
211static gboolean _thumbtable_idle_update(gpointer user_data)
212{
213 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
214 if(IS_NULL_PTR(table)) return G_SOURCE_REMOVE;
215
216 table->idle_update_id = 0;
219 if(table->thumb_nb == 0) gtk_widget_queue_draw(table->grid);
220 return G_SOURCE_REMOVE;
221}
222
224{
225 if(IS_NULL_PTR(table)) return;
226 if(table->scroll_window && !gtk_widget_is_visible(table->scroll_window)) return;
227 if(table->idle_update_id) return;
228 table->idle_update_id = g_idle_add_full(G_PRIORITY_LOW, (GSourceFunc)_thumbtable_idle_update, table, NULL);
229}
230
235
236static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data)
237{
238 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
239 if(IS_NULL_PTR(table)) return;
240
241 // Only react to the adjustment that is meaningful in the current mode.
242 if(!table->ops->wants_scroll_value(table, adjustment)) return;
243
245}
246
247static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
248{
249 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
250 if(IS_NULL_PTR(table)) return;
251
252 // Page size is only used to size the filemanager/grid (filmstrip uses its parent allocation), and
253 // only the scroll-axis (vertical) adjustment matters. Reacting to the cross-axis adjustment would
254 // re-enter configure (whose grid-width write drives that page size) and can spin in a resize loop.
255 if(!table->ops->wants_page_size_notify(table, object)) return;
256
258}
259
260static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
261{
262 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
263 if(IS_NULL_PTR(table) || IS_NULL_PTR(allocation)) return;
264
265 if(allocation->width == table->last_parent_width && allocation->height == table->last_parent_height)
266 return;
267
268 table->last_parent_width = allocation->width;
269 table->last_parent_height = allocation->height;
271}
272
273static void _scrollbar_widget_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
274{
275 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
276 if(IS_NULL_PTR(table) || IS_NULL_PTR(allocation) || IS_NULL_PTR(table->scroll_window)) return;
277
278 // The frontend decides which scrollbar's geometry drives its thumbnail sizing (filmstrip: the
279 // horizontal scrollbar height; filemanager: the vertical scrollbar width) and records it.
280 if(table->ops->relevant_scrollbar_changed(table, widget, allocation))
282}
283
284// We can't trust the mouse enter/leave events on thumnbails to properly
285// update active thumbnail styling, so we need to catch the signal here and update the whole list.
286void _mouse_over_image_callback(gpointer instance, gpointer user_data)
287{
288 if(IS_NULL_PTR(user_data)) return;
289 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
290 if(IS_NULL_PTR(table->lut) || table->collection_count == 0) return;
291
292 const int32_t imgid = dt_control_get_mouse_over_id();
293
295
296 int32_t group_id = UNKNOWN_IMAGE;
297
298 // We iterate and update only over the visible range of thumbs + 2 rows as a safety margin
299 int32_t row_start = CLAMP(table->min_row_id - 2 * table->thumbs_per_row, 0, table->collection_count - 1);
300 int32_t row_end = CLAMP(table->max_row_id + 2 * table->thumbs_per_row, 0, table->collection_count - 1);
301 for(int rowid = row_start; rowid <= row_end; rowid++)
302 {
303 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
304 if(IS_NULL_PTR(thumb)) continue; // thumb object not inited
305
306 const gboolean mouse_over = thumb->mouse_over;
307 dt_thumbnail_set_mouseover(thumb, thumb->info.id == imgid);
308
309 if(thumb->info.id == imgid && dt_thumbtable_info_is_grouped(table->lut[rowid].thumb->info))
310 group_id = thumb->info.group_id;
311
312 if(thumb->mouse_over != mouse_over)
313 gtk_widget_queue_draw(thumb->widget);
314 }
315
316 // Now, we update all the thumbs of the same image group
317 if(table->draw_group_borders)
318 {
319 for(int rowid = row_start; rowid <= row_end; rowid++)
320 {
321 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
322 if(IS_NULL_PTR(thumb)) continue; // thumb object not inited
323
324 // In CSS:
325 // images borders from non-grouped images are transparent (default),
326 // images borders from non-hovered groups, when there is none, are dark orange (base border classes)
327 // images borders from the hovered group, when there is one, are bright orange (overwrite base border classes)
328 // images borders from non-hovered groups, when there is one, are transparent (overwrite base border classes width default)
329 // Here we dispatch the additional CSS classes alloying to overwrite
330 // the base border classes.
331 if(thumb->info.group_id == group_id)
332 {
333 dt_gui_add_class(thumb->widget, "hovered-group");
334 dt_gui_remove_class(thumb->widget, "non-hovered-group");
335 }
336 else if(group_id == UNKNOWN_IMAGE)
337 {
338 dt_gui_remove_class(thumb->widget, "hovered-group");
339 dt_gui_remove_class(thumb->widget, "non-hovered-group");
340 }
341 else
342 {
343 dt_gui_remove_class(thumb->widget, "hovered-group");
344 dt_gui_add_class(thumb->widget, "non-hovered-group");
345 }
346 }
347 }
348
350}
351
352
353static void _rowid_to_position(dt_thumbtable_t *table, int rowid, int *x, int *y)
354{
355 if(table->thumbs_per_row < 1) return;
356 table->ops->rowid_to_position(table, rowid, x, y);
357}
358
359// Needs updated table->x_position and table->y_position
360static int _position_to_rowid(dt_thumbtable_t *table, const double x, const double y)
361{
362 return table->ops->position_to_rowid(table, x, y);
363}
364
365// Find the x, y coordinates of any given thumbnail
366// Return TRUE if a position could be computed
367// thumb->rowid and table->thumbs_per_row need to have been inited before calling this
369{
370 if(table->thumbs_per_row < 1) return FALSE;
371 _rowid_to_position(table, thumb->rowid, &thumb->x, &thumb->y);
372 return TRUE;
373}
374
375// Updates table->x_position and table->y_position
377{
378 *y = gtk_adjustment_get_value(table->v_scrollbar);
379 *x = gtk_adjustment_get_value(table->h_scrollbar);
380}
381
383{
384 double x = 0.;
385 double y = 0.;
387 table->rowid = _position_to_rowid(table, x, y);
388}
389
390static int dt_thumbtable_scroll_to_position(dt_thumbtable_t *table, const double x, const double y)
391{
392 gtk_adjustment_set_value(table->v_scrollbar, y);
393 gtk_adjustment_set_value(table->h_scrollbar, x);
394 return 0;
395}
396
397static void dt_thumbtable_scroll_to_rowid(dt_thumbtable_t *table, int rowid)
398{
399 // Find (x, y) of the current thumbnail (north-west corner)
400 int x = 0, y = 0;
401 _rowid_to_position(table, rowid, &x, &y);
402
403 // Put the image always in the center of the view, if possible,
404 // aka move from north-west corner to center of the thumb
405 x += table->thumb_width / 2;
406 y += table->thumb_height / 2;
407
408 // Scroll viewport there
409 const double x_scroll = (double)x - (double)table->view_width / 2.;
410 const double y_scroll = (double)y - (double)table->view_height / 2.;
411 dt_thumbtable_scroll_to_position(table, x_scroll, y_scroll);
412}
413
415{
416 if(IS_NULL_PTR(table) || IS_NULL_PTR(table->lut) || table->collection_count <= 0) return UNKNOWN_IMAGE;
417
418 for(int i = 0; i < table->collection_count; i++)
419 if(table->lut[i].imgid == imgid)
420 return i;
421
422 return UNKNOWN_IMAGE;
423}
424
426{
427 if(IS_NULL_PTR(table) || !table->collection_inited || IS_NULL_PTR(table->lut) || table->collection_count <= 0)
428 return 1;
429
430 int rowid = UNKNOWN_IMAGE;
431 if(imgid > UNKNOWN_IMAGE)
432 {
434 rowid = dt_thumbtable_find_rowid_from_imgid(table, imgid);
436 }
437 else
438 rowid = table->rowid;
439
440 if(rowid == UNKNOWN_IMAGE) return 1;
441
442 dt_thumbtable_scroll_to_rowid(table, rowid);
443
444 return 0;
445}
446
447
449{
450 if(table->rowid > UNKNOWN_IMAGE)
452 else
454 return 0;
455}
456
457
459{
461 if(id < 0) id = dt_control_get_keyboard_over_id();
462 if(id < 0) id = dt_control_get_mouse_over_id();
463 dt_print(DT_DEBUG_LIGHTTABLE, "[thumbtable] scroll_to_selection: id=%d\n", id);
465 return 0;
466}
467
468// Find the row ids (as in SQLite indices) of the images contained within viewport at current scrolling stage
469static gboolean _get_row_ids(dt_thumbtable_t *table, int *rowid_min, int *rowid_max)
470{
471 if(!table->configured || IS_NULL_PTR(table->v_scrollbar) || IS_NULL_PTR(table->h_scrollbar)) return FALSE;
472 table->ops->get_row_ids(table, rowid_min, rowid_max);
473 return TRUE;
474}
475
476// Find out if a given row id is visible at current scroll step
477gboolean _is_rowid_visible(dt_thumbtable_t *table, int rowid)
478{
479 if(!table->configured || IS_NULL_PTR(table->v_scrollbar) || IS_NULL_PTR(table->h_scrollbar)) return FALSE;
480 return table->ops->is_rowid_visible(table, rowid);
481}
482
483// Returns TRUE if visible row ids have changed since last check
485{
486 int rowid_min = 0;
487 int rowid_max = 0;
488 if(!_get_row_ids(table, &rowid_min, &rowid_max)) return FALSE;
489 if(rowid_min != table->min_row_id || rowid_max != table->max_row_id)
490 {
491 table->min_row_id = rowid_min;
492 table->max_row_id = rowid_max;
493 table->thumbs_inited = FALSE;
494 return TRUE;
495 }
496 return FALSE;
497}
498
500{
501 if(!table->configured || !table->collection_inited) return;
502 table->ops->update_content_size(table);
503}
504
505
506// Store the geometry computed by dt_thumbtable_configure. It must NOT re-derive thumb_width/height:
507// configure already applies the cell-decoration budget when computing them, so re-deriving here
508// (e.g. floor(width/cols), ignoring deco) would disagree by a pixel and make thumbs_changed forever
509// true - an infinite configure/redraw loop.
510void _grid_configure(dt_thumbtable_t *table, int width, int height, int per_row, int thumb_width, int thumb_height)
511{
512 if(width < 32 || height < 32) return;
513
514 table->thumbs_per_row = per_row;
515 table->view_width = width;
516 table->view_height = height;
517 table->thumb_width = thumb_width;
518 table->thumb_height = thumb_height;
519
520 table->configured = TRUE;
521
522 dt_print(DT_DEBUG_LIGHTTABLE, "Configuring thumbtable w=%i h=%i thumbs/row=%i thumb_width=%i\n",
523 table->view_width, table->view_height, table->thumbs_per_row, table->thumb_width);
524}
525
526// Extra extent one thumb cell adds beyond the layout stride (thumb_width/height).
527//
528// The cells (.thumb-cell) carry a transparent border plus a negative margin, used to overlap and merge
529// the borders of adjacent cells. As a result a cell's margin box is larger than the stride by
530// (border + margin); inner cells overlap their neighbours so it cancels out, but the last row/column
531// has no neighbour to overlap into, so the GtkFixed grid ends up exactly that much larger than
532// cols*thumb_width (GtkFixed sizes itself as max(child->x + child_preferred), and the child's
533// preferred size includes its margins). Budgeting for it - read from the theme rather than hardcoded -
534// keeps the grid within its viewport under the NEVER scroll policy instead of forcing the scrolled
535// window (and its scrollbar) past the parent overlay. The cell is square, so H and V surplus are equal.
537{
538 GtkWidgetPath *path = gtk_widget_path_new();
539 gtk_widget_path_append_type(path, GTK_TYPE_EVENT_BOX);
540 gtk_widget_path_iter_add_class(path, -1, "thumb-cell");
541
542 GtkStyleContext *ctx = gtk_style_context_new();
543 gtk_style_context_set_path(ctx, path);
544
545 GtkBorder margin, border;
546 gtk_style_context_get_margin(ctx, GTK_STATE_FLAG_NORMAL, &margin);
547 gtk_style_context_get_border(ctx, GTK_STATE_FLAG_NORMAL, &border);
548
549 g_object_unref(ctx);
550 gtk_widget_path_unref(path);
551
552 return MAX(0, (border.left + border.right) + (margin.left + margin.right));
553}
554
555// Track size changes of the container or number of thumbs per row
556// and recomputed the size of individual thumbnails accordingly
558{
559 if(!gtk_widget_is_visible(table->scroll_window)) return;
560
561 int new_width = 0;
562 int new_height = 0;
563 int new_thumbs_per_row = 0;
564 int new_thumb_width = 0;
565 int new_thumb_height = 0;
566
567 // The frontend reads the parent allocation and derives the viewport size, per-row count and
568 // individual thumbnail size (including the scrollbar gutter and per-cell decoration budget).
569 table->ops->configure_dims(table, &new_width, &new_height, &new_thumbs_per_row,
570 &new_thumb_width, &new_thumb_height);
571
572 // Parent is not allocated or something went wrong:
573 // ensure to reset everything so no further code will run.
574 if(new_width < 32 || new_height < 32)
575 {
576 table->thumbs_inited = FALSE;
577 table->configured = FALSE;
578 table->thumbs_per_row = 0;
579 table->thumb_height = 0;
580 table->thumb_width = 0;
581 return;
582 }
583
584 const gboolean thumbs_changed = (!table->configured
585 || new_thumbs_per_row != table->thumbs_per_row
586 || new_thumb_width != table->thumb_width
587 || new_thumb_height != table->thumb_height);
588
589 // Always keep view sizes in sync: they are used for navigation and scroll centering.
590 table->view_width = new_width;
591 table->view_height = new_height;
592
593 if(thumbs_changed)
594 {
595 table->thumbs_inited = FALSE;
596 _grid_configure(table, new_width, new_height, new_thumbs_per_row, new_thumb_width, new_thumb_height);
597 _update_grid_area(table);
598 }
599}
600
602{
603 if(IS_NULL_PTR(table) || imgid <= 0) return NULL;
604 return (dt_thumbnail_t *)g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid));
605}
606
608{
609 if(IS_NULL_PTR(table) || IS_NULL_PTR(out) || imgid <= 0) return FALSE;
610
611 // Prefer LUT-backed metadata to avoid touching the image cache for read-only UI needs.
612 gboolean found = FALSE;
614 if(table->lut)
615 {
616 for(int rowid = 0; rowid < table->collection_count; rowid++)
617 {
618 if(table->lut[rowid].imgid == imgid && table->lut[rowid].thumb)
619 {
620 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
621 if(g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid)) == thumb)
622 {
623 *out = thumb->info;
624 found = TRUE;
625 break;
626 }
627 table->lut[rowid].thumb = NULL;
628 }
629 }
630 }
632
633 return found;
634}
635
636#define CLAMP_ROW(rowid) CLAMP(rowid, 0, table->collection_count - 1)
637#define IS_COLLECTION_EDGE(rowid) (rowid < 0 || rowid >= table->collection_count)
638
640{
641 // Reset all CSS classes
642 dt_thumbnail_border_t borders = 0;
643 dt_thumbnail_set_group_border(thumb, borders);
644
645 const int32_t rowid = thumb->rowid;
646
647 // Ungrouped image: abort
648 if(!dt_thumbtable_info_is_grouped(table->lut[rowid].thumb->info) || !table->draw_group_borders) return;
649
650 // The frontend adds the mode-specific border flags (grid closes on all four neighbours; the
651 // filmstrip always closes top+bottom and checks only its horizontal neighbours).
652 table->ops->group_borders(table, thumb, &borders);
653
654 dt_thumbnail_set_group_border(thumb, borders);
655}
656
657void _add_thumbnail_at_rowid(dt_thumbtable_t *table, const size_t rowid, const int32_t mouse_over)
658{
659 const int32_t imgid = table->lut[rowid].imgid;
660 dt_image_t info;
661 dt_image_t *img = dt_image_cache_get(imgid, 'r');
662 if(IS_NULL_PTR(img)) return;
663
664 // Take a private copy
665 info = *img;
667
668 dt_thumbnail_t *thumb = NULL;
669 gboolean new_item = TRUE;
670 gboolean new_position = TRUE;
671
672 // Do we already have a thumbnail at the correct postion for the correct imgid ?
673 if(table->lut[rowid].thumb && table->lut[rowid].imgid == imgid)
674 {
675 // Reuse the LUT entry only if it still matches the live thumbnail registry.
676 dt_thumbnail_t *mapped_thumb = _find_thumb_by_imgid(table, imgid);
677 if(mapped_thumb == table->lut[rowid].thumb)
678 {
679 thumb = mapped_thumb;
680 new_position = FALSE;
681 }
682 else
683 {
684 table->lut[rowid].thumb = NULL;
685 }
686 }
687 else
688 {
689 // NO : Try to find an existing thumbnail widget by imgid in table->list
690 // That will be faster if we only changed the sorting order but are still in the same collection.
691 // NOTE: the thumb widget position in grid will be wrong
692 thumb = _find_thumb_by_imgid(table, imgid);
693 }
694
695 if(thumb)
696 {
697 // Ensure everything is up-to-date
698 thumb->rowid = rowid;
699 dt_thumbnail_resync_info(thumb, &info);
700 new_item = FALSE;
701 }
702 else
703 {
704 thumb = dt_thumbnail_new(rowid, table->overlays, table, &info);
705 if(IS_NULL_PTR(thumb)) return;
706 g_hash_table_insert(table->list, GINT_TO_POINTER(thumb->info.id), thumb);
707 table->thumb_nb += 1;
708 }
709
710 table->lut[rowid].thumb = thumb;
711
712 // Resize
713 gboolean size_changed = (table->thumb_height != thumb->height || table->thumb_width != thumb->width);
714 if(new_item || size_changed || table->overlays != thumb->over)
715 {
716 dt_thumbnail_set_overlay(thumb, table->overlays);
717 dt_thumbnail_resize(thumb, table->thumb_width, table->thumb_height);
718 }
719
720 // Actually moving the widgets in the grid is more expensive, do it only if necessary
721 if(new_item)
722 {
723 _set_thumb_position(table, thumb);
724 table->ops->place_child(table, thumb);
725 //fprintf(stdout, "adding new thumb at #%lu: %i, %i\n", rowid, thumb->x, thumb->y);
726 }
727 else if(new_position || size_changed)
728 {
729 _set_thumb_position(table, thumb);
730 table->ops->move_child(table, thumb);
731 //fprintf(stdout, "moving new thumb at #%lu: %i, %i\n", rowid, thumb->x, thumb->y);
732 }
733
734 // Update visual states and flags. Mouse over is not connected to a signal and cheap to update
735 dt_thumbnail_set_mouseover(thumb, (mouse_over == thumb->info.id));
737
738 // Per-mode selection/action state (filmstrip tracks active images and disables actions).
739 table->ops->on_thumbnail_added(table, thumb);
740
741 _add_thumbnail_group_borders(table, thumb);
742 gtk_widget_show(thumb->widget);
743}
744
745
746// Add and/or resize thumbnails within visible viewort at current scroll level
748{
749 const int32_t mouse_over = dt_control_get_mouse_over_id();
750
751 // for(size_t rowid = 0; rowid < table->collection_count; rowid++)
752 for(size_t rowid = MAX(table->min_row_id, 0); rowid < MIN(table->max_row_id, table->collection_count); rowid++)
753 _add_thumbnail_at_rowid(table, rowid, mouse_over);
754}
755
756// Resize the thumbnails that are still existing but outside of visible viewport at current scroll level
758{
759 if(!table->configured) return;
760
761 GHashTableIter iter;
762 gpointer value = NULL;
763 g_hash_table_iter_init(&iter, table->list);
764 while(g_hash_table_iter_next(&iter, NULL, &value))
765 {
767 gboolean size_changed = (table->thumb_height != thumb->height || table->thumb_width != thumb->width);
768
769 if(size_changed || table->overlays != thumb->over)
770 {
771 // Overlay modes may change the height of the image
772 // to accommodate buttons. We need to resize on overlay changes.
773 dt_thumbnail_set_overlay(thumb, table->overlays);
774 dt_thumbnail_resize(thumb, table->thumb_width, table->thumb_height);
775 if(size_changed)
776 {
777 _set_thumb_position(table, thumb);
778 table->ops->move_child(table, thumb);
779 }
781 }
782
784 _add_thumbnail_group_borders(table, thumb);
785 gtk_widget_queue_draw(thumb->widget);
786 }
787}
788
789// Destroy the thumbnails whose rowid lies outside [keep_min, keep_max). Each is a child of
790// table->grid, and GTK restyles an appended child by recursing through all its previous siblings:
791// unbounded, paging through a large collection overflows the main-thread stack (1 MiB on Windows).
792static void _evict_thumbnails(dt_thumbtable_t *table, const int keep_min, const int keep_max)
793{
794 if(keep_max <= keep_min) return; // no visible range: nothing tells what to keep
795
796 const double start = dt_get_wtime();
797 GList *evicted = NULL;
798
800 GHashTableIter iter;
801 gpointer value = NULL;
802 g_hash_table_iter_init(&iter, table->list);
803 while(g_hash_table_iter_next(&iter, NULL, &value))
804 {
806 if(thumb->rowid >= keep_min && thumb->rowid < keep_max) continue;
807 g_hash_table_iter_remove(&iter);
808 evicted = g_list_prepend(evicted, thumb);
809 table->thumb_nb--;
810 }
812
813 // dt_thumbnail_destroy() takes table->lock, and clears the LUT slots still pointing to the thumbnail
814 const guint count = g_list_length(evicted);
815 for(GList *l = evicted; l; l = g_list_next(l))
816 {
817 dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
818 gtk_widget_hide(thumb->widget);
820 }
821 g_list_free(evicted);
822
823 dt_print(DT_DEBUG_LIGHTTABLE, "Evicted %u thumbs outside of %i and %i in %0.04f sec\n", count, keep_min,
824 keep_max, dt_get_wtime() - start);
825}
826
827static gboolean _evict_idle(gpointer user_data)
828{
829 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
830 table->evict_idle_id = 0;
831
832 const int span = table->max_row_id - table->min_row_id;
833 _evict_thumbnails(table, table->min_row_id - span, table->max_row_id + span);
834 return G_SOURCE_REMOVE;
835}
836
837
839{
840 _update_row_ids(table);
841
842 if(!gtk_widget_is_visible(table->scroll_window) || IS_NULL_PTR(table->lut) || !table->configured || !table->collection_inited
843 || table->thumbs_inited || table->collection_count == 0)
844 return;
845
846 if(table->reset_collection)
847 {
849 table->reset_collection = FALSE;
850 }
851
852 const double start = dt_get_wtime();
853
855
856 gboolean empty_list = (table->thumb_nb == 0);
857
859
860 if(!empty_list)
861 _resize_thumbnails(table);
862
863 table->thumbs_inited = TRUE;
864
866
867 // Destroying a thumbnail costs half of creating one, so it waits until this page is drawn. The idle
868 // starves while Page Down is held: past 4 visible ranges, evict now to keep the stack bounded.
869 const int span = table->max_row_id - table->min_row_id;
870 if((int)table->thumb_nb > 4 * span)
871 _evict_thumbnails(table, table->min_row_id - span, table->max_row_id + span);
872 else if(!table->evict_idle_id)
873 table->evict_idle_id = g_idle_add_full(G_PRIORITY_LOW + 10, _evict_idle, table, NULL);
874
875 const char *const name = gtk_widget_get_name(table->grid);
876 dt_print(DT_DEBUG_LIGHTTABLE, "[%s] Populated %d thumbs between %i and %i in %0.04f sec \n",
877 name ? name : "thumbtable", table->thumb_nb, table->min_row_id, table->max_row_id,
878 dt_get_wtime() - start);
879}
880
881
882static void _dt_profile_change_callback(gpointer instance, int type, gpointer user_data)
883{
884 if(IS_NULL_PTR(user_data)) return;
885 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
887}
888
889// Repaint every materialised thumbnail's "highlighted" state from the mode's source of truth
890// (grid: the selection; filmstrip: the active/developed image - see is_thumb_highlighted). Called
891// on both selection and active-image changes so the mode tracks whichever one it cares about and
892// is not clobbered by the other. Issue #954: the darkroom clears the selection AND sets a new
893// active image when the developed picture changes; the filmstrip must follow the active image.
895{
896 if(IS_NULL_PTR(table)) return;
897 // Hidden tables re-evaluate highlights on view re-entry (populate calls on_thumbnail_added), so
898 // there is no point (and, for huge collections, real cost) in scanning them here.
899 if(!gtk_widget_is_visible(table->scroll_window)) return;
900
901 gboolean first = TRUE;
902
904 if(IS_NULL_PTR(table->lut) || !table->collection_inited || table->collection_count == 0)
905 {
907 return;
908 }
909
910 for(int rowid = 0; rowid < table->collection_count; rowid++)
911 {
912 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
913 if(IS_NULL_PTR(thumb)) continue;
914
915 if(g_hash_table_lookup(table->list, GINT_TO_POINTER(table->lut[rowid].imgid)) != thumb)
916 {
917 table->lut[rowid].thumb = NULL;
918 continue;
919 }
920
921 const gboolean was_highlighted = thumb->selected;
922 dt_thumbnail_update_selection(thumb, table->ops->is_thumb_highlighted(table, thumb->info.id));
923
924 if(thumb->selected && first)
925 {
927
928 // Sync the table active row id with the first highlighted thumb
929 table->rowid = thumb->rowid;
930 first = FALSE;
931 }
932
933 if(thumb->selected != was_highlighted)
934 gtk_widget_queue_draw(thumb->widget);
935 }
937}
938
939static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
940{
941 if(IS_NULL_PTR(user_data)) return;
943}
944
945// The filmstrip highlights the active/developed image; refresh when it changes (e.g. navigating to
946// another picture in darkroom). The grid's highlight source is the selection, so this is a no-op
947// there.
948static void _dt_active_images_changed_callback(gpointer instance, gpointer user_data)
949{
950 if(IS_NULL_PTR(user_data)) return;
952}
953
959
961{
962 return table->focus_regions;
963}
964
970
972{
973 return table->focus_peaking;
974}
975
983
985{
986 return table->draw_group_borders;
987}
988
989// can be called with imgid = -1, in that case we reload all mipmaps
990// reinit = FALSE should be called when the mipmap is ready to redraw,
991// reinit = TRUE should be called when a refreshed mipmap has been requested but we have nothing yet to draw
992void dt_thumbtable_refresh_thumbnail_real(dt_thumbtable_t *table, int32_t imgid, gboolean reinit)
993{
995 if(imgid != UNKNOWN_IMAGE)
996 {
997 dt_thumbnail_t *thumb = (dt_thumbnail_t *)g_hash_table_lookup(table->list, GINT_TO_POINTER(imgid));
998 if(thumb)
1000 }
1001 else
1002 {
1003 GHashTableIter iter;
1004 gpointer value = NULL;
1005 g_hash_table_iter_init(&iter, table->list);
1006 while(g_hash_table_iter_next(&iter, NULL, &value))
1007 {
1010 }
1011 }
1013}
1014
1015
1016// this is called each time the images info change
1017// NOTE: remember we populate the table->lut with the current collection
1018// but we init actual thumbnail objects and add their widgets to the grid
1019// only when they appear in viewport.
1020// So, we have to account for the fact that we may not have actual
1021// thumbnails to refresh yet, and the table->list doesn't contain
1022// uninited thumbs.
1023// So we always use table->lut to find imgid, update the cached info
1024// and try to update widgets only if we have some.
1025// Otherwise, fresh info will be read when initing new thumbnails objects.
1026static void _dt_image_info_changed_callback(gpointer instance, gpointer imgs, gpointer user_data)
1027{
1028 if(!user_data || IS_NULL_PTR(imgs)) return;
1029 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1030 if(IS_NULL_PTR(table->lut)) return;
1031
1032 dt_pthread_mutex_lock(&table->lock);
1033
1034 for(GList *l = g_list_first(imgs); l; l = g_list_next(l))
1035 {
1036 const int32_t imgid = GPOINTER_TO_INT(l->data);
1037 if(imgid <= 0) continue;
1038
1039 const int rowid = dt_thumbtable_find_rowid_from_imgid(table, imgid);
1040 if(rowid == UNKNOWN_IMAGE) continue;
1041
1042 dt_thumbnail_t *thumb = table->lut[rowid].thumb;
1043 if(thumb)
1044 {
1045 // Refresh the cached LUT info from the image cache for write-driven updates
1046 // (ratings, color labels, etc.) while still keeping read paths cache-free.
1047 const dt_image_t *img = dt_image_cache_testget(imgid, 'r');
1048 if(img)
1049 {
1050 dt_thumbnail_resync_info(thumb, img);
1053 _add_thumbnail_group_borders(table, thumb);
1054 gtk_widget_queue_draw(thumb->widget);
1055 }
1056 }
1057
1058 if(dt_view_manager_get_global()->image_info_id == imgid)
1060 }
1061
1063}
1064
1065/* One collected image, as the repository read it. Context for _dt_collection_lut() below. */
1071
1072static void _collection_lut_row(void *user_data, const dt_image_t *row)
1073{
1074 _collection_lut_ctx_t *ctx = (_collection_lut_ctx_t *)user_data;
1075 const int32_t imgid = row->id;
1076 const int32_t groupid = row->group_id;
1077
1078 if(ctx->table->collapse_groups && imgid != groupid)
1079 {
1080 // if user requested to collapse image groups in GUI,
1081 // only the group leader is shown. But we need to make sure
1082 // there is no dangling selection pointing to hidden group members
1083 // because it's unexpected that unvisible items might be selected,
1084 // and selection sanitization only deals with imgids outside of current collection,
1085 // but group members are always within the collection.
1087 return;
1088 }
1089
1090 dt_thumbtable_cache_t entry = { .thumb = NULL, .imgid = imgid, .groupid = groupid };
1091 g_array_append_val(ctx->collection, entry);
1092
1093 // Populate the image cache. We don't keep a copy here because it wouldn't
1094 // be memory-managed
1095 dt_image_t info = *row;
1097
1098#ifndef NDEBUG
1100#endif
1101
1102 // Seed the cache and be done
1104}
1105
1107{
1108 // NOTE: non-grouped images have group_id equal to their own id
1109 // grouped images have group_id equal to the id of the "group leader".
1110 // In old database versions, it's possible that group_id may have been set to -1 for non-grouped images.
1111 // That would actually make group detection much easier...
1112
1113 // Convert SQL imgids into C objects we can work with. In-memory collected images don't store
1114 // group_id, so the repository reads the whole row again from the images table.
1115 GArray *collection = g_array_new(FALSE, FALSE, sizeof(dt_thumbtable_cache_t));
1116 _collection_lut_ctx_t ctx = { .table = table, .collection = collection };
1118
1119 if(IS_NULL_PTR(collection) || collection->len == 0)
1120 {
1121 dt_thumbtable_cache_t *old_lut = NULL;
1122 dt_pthread_mutex_lock(&table->lock);
1123 old_lut = table->lut;
1124 table->lut = NULL;
1125 table->collection_count = 0;
1126 table->collection_inited = FALSE;
1128
1129 dt_free(old_lut);
1130 if(collection) g_array_free(collection, TRUE);
1131 return;
1132 }
1133
1134 // Build the collection LUT, aka a fixed-sized array of image objects
1135 // where the position of an image in the collection is directly the index in the LUT/array.
1136 // This makes for very efficient position -> imgid/thumbnail accesses directly in C,
1137 // especially from GUI code. The downside is we need to fully clear and recreate the LUT
1138 // everytime a collection changes (meaning filters OR sorting changed).
1139 dt_thumbtable_cache_t *new_lut = malloc(collection->len * sizeof(dt_thumbtable_cache_t));
1140
1141 if(IS_NULL_PTR(new_lut))
1142 {
1143 g_array_free(collection, TRUE);
1144 return;
1145 }
1146
1147 memcpy(new_lut, collection->data, collection->len * sizeof(dt_thumbtable_cache_t));
1148
1149 dt_thumbtable_cache_t *old_lut = NULL;
1150 dt_pthread_mutex_lock(&table->lock);
1151 old_lut = table->lut;
1152 table->lut = new_lut;
1153 table->collection_count = collection->len;
1154 table->collection_inited = TRUE;
1156
1157 dt_free(old_lut);
1158 g_array_free(collection, TRUE);
1159}
1160
1162{
1163 // The collection query's generation stands in for the query text, which no longer leaves the
1164 // database module. It advances on every recomposition, so it changes exactly when the text
1165 // would have.
1166 const uint64_t generation = dt_collection_query_get_generation();
1167 uint64_t hash = dt_hash(5384, (char *)&generation, sizeof(uint64_t));
1168
1169 // Factor in the number of images in the collection result
1171 hash = dt_hash(hash, (char *)&num_pics, sizeof(uint32_t));
1172
1173 if(hash != table->collection_hash || table->reset_collection)
1174 {
1175 // Collection changed: reset everything
1176 table->collection_hash = hash;
1177 table->collection_inited = FALSE;
1178 return TRUE;
1179 }
1180 return FALSE;
1181}
1182
1183
1184// this is called each time collected images change
1185static void _dt_collection_changed_callback(gpointer instance, dt_collection_change_t query_change,
1186 dt_collection_properties_t changed_property, gpointer imgs,
1187 const int next, gpointer user_data)
1188{
1189 if(IS_NULL_PTR(user_data)) return;
1190 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1191
1192 gboolean collapse_groups = dt_conf_get_bool("ui_last/grouping");
1193 gboolean collapsing_changed = (table->collapse_groups != collapse_groups);
1194
1195 // Remember where the scrolling is at to possibly get the same visible images
1196 // despite collection changes (provided they are still there).
1198
1199 // See if the collection changed
1200 gboolean grouping_changed = changed_property == DT_COLLECTION_PROP_GROUPING;
1201 gboolean hash_changed = _dt_collection_get_hash(table);
1202 gboolean changed = hash_changed || collapsing_changed || grouping_changed;
1204 "[thumbtable] collection_changed_callback: query_change=%d changed_property=%d hash_changed=%d "
1205 "collapsing_changed=%d grouping_changed=%d -> changed=%d\n",
1206 query_change, changed_property, hash_changed, collapsing_changed, grouping_changed, changed);
1207 if(changed)
1208 {
1209 // If groups are collapsed, we add only the group leader image to the collection
1210 // It needs to be set before running _dt_collection_lut()
1211 table->collapse_groups = collapse_groups;
1212 if(!_thumbtable_clone_lut(table))
1213 _dt_collection_lut(table);
1214
1215 table->thumbs_inited = FALSE;
1217
1218 if(table->collection_count == 0)
1219 {
1221 "The current filtered collection contains no image. Relax your filters or fetch a non-empty collection"));
1222 }
1223
1224 // Ensure we have something to scroll
1226
1227 // Number of images may have changed, size of grid too:
1228 _update_grid_area(table);
1229
1230 // Coalesce multiple layout/resize signals that can happen during collection loads.
1232
1233 // DT_COLLECTION_CHANGE_BACKGROUND_SYNC marks a passive background sync (see
1234 // dt_collection_notify_imported()): the grid content above must still refresh -- it's driven
1235 // by the hash, which is what actually detected new/removed images -- but a background import
1236 // dropping images into the browsed folder must not also hijack the user's scroll position
1237 // every time it does. A real navigational change (folder switch, filter edit...) always
1238 // carries a different query_change and keeps re-centering on the selection as before.
1239 if(query_change != DT_COLLECTION_CHANGE_BACKGROUND_SYNC)
1240 dt_thumbtable_schedule_focus(table, G_PRIORITY_DEFAULT_IDLE);
1241 }
1242}
1243
1244// get the class name associated with the overlays mode
1246{
1247 switch(over)
1248 {
1250 return g_strdup("dt_overlays_none");
1252 return g_strdup("dt_overlays_always");
1254 default:
1255 return g_strdup("dt_overlays_hover");
1256 }
1257}
1258
1259// update thumbtable class and overlays mode, depending on size category
1261{
1262 // we change the overlay mode
1263 gchar *txt = g_strdup("plugins/lighttable/overlays/global");
1265 dt_free(txt);
1266
1268}
1269
1270// change the type of overlays that should be shown
1272{
1273 if(IS_NULL_PTR(table)) return;
1274 if(over == table->overlays) return;
1275
1276 // Cleanup old Darktable stupid modes
1277 dt_conf_set_int("plugins/lighttable/overlays/global", sanitize_overlays(over));
1278
1279 gchar *cl0 = _thumbs_get_overlays_class(table->overlays);
1280 gchar *cl1 = _thumbs_get_overlays_class(over);
1281 dt_gui_remove_class(table->grid, cl0);
1282 dt_gui_add_class(table->grid, cl1);
1283 dt_free(cl0);
1284 dt_free(cl1);
1285
1286 table->thumbs_inited = FALSE;
1287 table->overlays = over;
1288
1289 dt_pthread_mutex_lock(&table->lock);
1290 _resize_thumbnails(table);
1292}
1293
1294static void _event_dnd_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data,
1295 const guint target_type, const guint time, gpointer user_data)
1296{
1297 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1298 g_assert(!IS_NULL_PTR(selection_data));
1299
1300 switch(target_type)
1301 {
1302 case DND_TARGET_IMGID:
1303 {
1304 const int imgs_nb = g_list_length(table->drag_list);
1305 if(imgs_nb)
1306 {
1307 uint32_t *imgs = malloc(sizeof(uint32_t) * imgs_nb);
1308 GList *l = table->drag_list;
1309 for(int i = 0; i < imgs_nb; i++)
1310 {
1311 imgs[i] = GPOINTER_TO_INT(l->data);
1312 l = g_list_next(l);
1313 }
1314 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
1315 _DWORD, (guchar *)imgs, imgs_nb * sizeof(uint32_t));
1316 dt_free(imgs);
1317 }
1318 break;
1319 }
1320 default: // return the location of the file as a last resort
1321 case DND_TARGET_URI:
1322 {
1323 GList *l = table->drag_list;
1324 if(g_list_is_singleton(l))
1325 {
1326 gchar pathname[DT_PATH_MAX] = { 0 };
1327 gboolean from_cache = TRUE;
1328 const int id = GPOINTER_TO_INT(l->data);
1329 dt_image_full_path(id, pathname, sizeof(pathname), &from_cache, __FUNCTION__);
1330 gchar *uri = g_strdup_printf("file://%s", pathname); // TODO: should we add the host?
1331 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
1332 _BYTE, (guchar *)uri, strlen(uri));
1333 dt_free(uri);
1334 }
1335 else
1336 {
1337 GList *images = NULL;
1338 for(; l; l = g_list_next(l))
1339 {
1340 const int id = GPOINTER_TO_INT(l->data);
1341 gchar pathname[DT_PATH_MAX] = { 0 };
1342 gboolean from_cache = TRUE;
1343 dt_image_full_path(id, pathname, sizeof(pathname), &from_cache, __FUNCTION__);
1344 gchar *uri = g_strdup_printf("file://%s", pathname); // TODO: should we add the host?
1345 images = g_list_prepend(images, uri);
1346 }
1347 images = g_list_reverse(images); // list was built in reverse order, so un-reverse it
1348 gchar *uri_list = dt_util_glist_to_str("\r\n", images);
1349 g_list_free_full(images, dt_free_gpointer);
1350 images = NULL;
1351 gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data), _BYTE,
1352 (guchar *)uri_list, strlen(uri_list));
1353 dt_free(uri_list);
1354 }
1355 break;
1356 }
1357 }
1358}
1359
1360static void _thumbtable_drag_set_icon(dt_thumbtable_t *table, GdkDragContext *context)
1361{
1362 if(IS_NULL_PTR(table) || !table->drag_list) return;
1363
1364 const int32_t imgid = dt_control_get_mouse_over_id();
1365 dt_thumbnail_t *thumb = _find_thumb_by_imgid(table, imgid);
1366 if(IS_NULL_PTR(thumb)) return;
1367
1368 cairo_surface_t *surface = NULL;
1369 int hotspot_x = 0;
1370 int hotspot_y = 0;
1371 int width = 0;
1372 int height = 0;
1373
1374 dt_pthread_mutex_lock(&thumb->lock);
1375 if(thumb->img_surf && cairo_surface_get_reference_count(thumb->img_surf) > 0)
1376 {
1377 surface = cairo_surface_reference(thumb->img_surf);
1378 width = thumb->img_width;
1379 height = thumb->img_height;
1380 hotspot_x = width / 2;
1381 hotspot_y = height / 2;
1382 }
1384
1385 if(IS_NULL_PTR(surface)) return;
1386
1387 GtkWidget *image = gtk_image_new_from_surface(surface);
1388 cairo_surface_destroy(surface);
1389 // thumb->img_width/img_height are already in logical (CSS) pixels, but GtkImage sizes itself
1390 // from the surface's raw physical pixel count, ignoring its device scale: on HiDPI screens
1391 // (PPD > 1) that makes the drag icon balloon to PPD× the intended size. Pin the widget to the
1392 // correct logical size explicitly; the surface still paints crisply since cairo itself honors
1393 // the device scale when compositing.
1394 gtk_widget_set_size_request(image, width, height);
1395
1396 gtk_widget_show(image);
1397 gtk_drag_set_icon_widget(context, image, hotspot_x, hotspot_y);
1398}
1399
1400static void _event_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
1401{
1402 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1403 const int32_t imgid = dt_control_get_mouse_over_id();
1404
1405 // Commit the hovered image before dt_act_on_get_images() snapshots the payload: the filmstrip
1406 // raises a view signal, the grid extends the selection.
1407 table->ops->on_drag_begin(table, imgid);
1408
1410 _thumbtable_drag_set_icon(table, context);
1411}
1412
1413// Regular files are collected into `files` directly (unfiltered by type, same as always).
1414// Folders are collected into `*folders` instead of being rejected: the caller asks the user
1415// which file type(s) to pull out of them, then scans them on a background job (see
1416// _thumbtable_dnd_scan_folders_and_import()).
1417GList *_thumbtable_dnd_import_check(GList *files, const char *pathname, int *elements, GList **folders)
1418{
1419 if (IS_NULL_PTR(pathname) || IS_NULL_PTR(pathname))
1420 {
1421 fprintf(stdout,"DND check: no pathname.\n");
1422 return files;
1423 }
1424 fprintf(stdout,"DND check pathname: %s\n", pathname);
1425
1426 if(g_file_test(pathname, G_FILE_TEST_IS_REGULAR))
1427 {
1428 if (dt_supported_image(pathname))
1429 {
1430 files = g_list_prepend(files, g_strdup(pathname));
1431 (*elements)++;
1432 }
1433 else
1434 fprintf(stderr, "`%s`: Unkonwn format.", pathname);
1435 }
1436 else if(g_file_test(pathname, G_FILE_TEST_IS_DIR))
1437 {
1438 *folders = g_list_prepend(*folders, g_strdup(pathname));
1439 }
1440 else
1441 {
1442 fprintf(stderr, "DND check: `%s` not a file or folder.\n", pathname);
1443 }
1444
1445 return files;
1446}
1447
1448// Response codes for the file-type dialog below, same shape as control_jobs.c's
1449// _dt_delete_dialog_choice (a GtkResponseType-typed gtk_dialog_run() return value doesn't fit a
1450// 3-way custom choice, so this stands in for it exactly like that one does).
1457
1458// State for the background job that recursively scans dropped folders (drag-and-drop) and
1459// merges the result with any directly-dropped files before handing everything to
1460// dt_control_import(). Mirrors the Import dialog's own recursive scan
1461// (_recurse_folder()/_filter_document() in gui/import.c) but without that dialog's
1462// generation/cancellation bookkeeping: a drop happens once, atomically, there is no live
1463// file-chooser selection that could invalidate a scan already in flight.
1465{
1466 GList *folders; // owned: char* paths of the dropped folders to recurse into
1467 GList *files; // owned: char* paths already known (directly-dropped regular files)
1470
1471static void _dnd_recurse_folder(const char *folder_path, dt_import_filter_type_t filter_type, GList **files,
1472 guint *scan_errors)
1473{
1474 GFile *folder = g_file_new_for_path(folder_path);
1475 GError *error = NULL;
1476 GFileEnumerator *children = g_file_enumerate_children(
1477 folder, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NONE, NULL, &error);
1478 if(IS_NULL_PTR(children))
1479 {
1480 // e.g. permission denied, or the folder vanished mid-scan.
1481 if(error)
1482 {
1483 dt_print(DT_DEBUG_IMPORT, "[import] could not fully scan folder `%s': %s\n", folder_path, error->message);
1484 g_error_free(error);
1485 (*scan_errors)++;
1486 }
1487 g_object_unref(folder);
1488 return;
1489 }
1490
1491 GFile *child = NULL;
1492 GError *iter_error = NULL;
1493 while(g_file_enumerator_iterate(children, NULL, &child, NULL, &iter_error))
1494 {
1495 // g_file_enumerator_iterate returns FALSE only on errors, not on end of enumeration; an ugly
1496 // break is needed here, else infinite loop.
1497 if(IS_NULL_PTR(child)) break;
1498
1499 gchar *pathname = g_file_get_path(child);
1500 if(pathname && g_file_test(pathname, G_FILE_TEST_IS_REGULAR) && dt_import_passes_filter(filter_type, pathname))
1501 {
1502 *files = g_list_prepend(*files, pathname);
1503 pathname = NULL;
1504 }
1505 else if(pathname && g_file_test(pathname, G_FILE_TEST_IS_DIR))
1506 {
1507 _dnd_recurse_folder(pathname, filter_type, files, scan_errors);
1508 }
1509 dt_free(pathname);
1510 // g_file_enumerator_iterate() returns transfer-none children owned by the enumerator; unref
1511 // happens when the enumerator advances or is destroyed.
1512 child = NULL;
1513 }
1514
1515 // A FALSE return above without a set error just means "iteration finished normally" -- only
1516 // report when GLib actually set one.
1517 if(iter_error)
1518 {
1519 dt_print(DT_DEBUG_IMPORT, "[import] could not fully scan folder `%s': %s\n", folder_path, iter_error->message);
1520 g_error_free(iter_error);
1521 (*scan_errors)++;
1522 }
1523
1524 g_object_unref(children);
1525 g_object_unref(folder);
1526}
1527
1529{
1531
1532 // Take over the already-collected files; the scan below prepends the matches found in the
1533 // dropped folder(s) onto the same list.
1534 GList *files = data->files;
1535 data->files = NULL;
1536
1537 guint scan_errors = 0;
1538 for(GList *l = data->folders; l; l = g_list_next(l))
1539 _dnd_recurse_folder((const char *)l->data, data->filter_type, &files, &scan_errors);
1540
1541 const int elements = g_list_length(files);
1542 if(elements > 0)
1543 {
1544 dt_control_import_t import_data = {.imgs = files, // ownership transferred to the import job
1545 .datetime = g_date_time_new_now_local(),
1546 .copy = FALSE, // we only import in place, like plain-file DnD.
1547 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
1548 .base_folder = dt_conf_get_string("session/base_directory_pattern"),
1549 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
1550 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
1551 .target_dir = NULL,
1552 .elements = elements,
1553 .discarded = NULL};
1554 if(dt_control_import(import_data))
1555 dt_control_log(_("Could not start the import job."));
1556 }
1557 else
1558 {
1559 g_list_free(files);
1560 dt_control_log(_("No matching file found in the dropped folder(s)."));
1561 }
1562
1563 if(scan_errors > 0)
1564 dt_control_log(ngettext("%u folder could not be scanned (permission denied?)",
1565 "%u folders could not be scanned (permission denied?)", scan_errors),
1566 scan_errors);
1567
1568 return elements > 0 ? 0 : 1;
1569}
1570
1572{
1574 g_list_free_full(data->folders, dt_free_gpointer);
1575 // data->files is only non-NULL here if _dnd_folder_import_job_run() never ran (job disposed
1576 // before execution): the success path hands the list to dt_control_import(), and the
1577 // no-match path frees it itself.
1578 if(data->files) g_list_free_full(data->files, dt_free_gpointer);
1579 dt_free(data);
1580}
1581
1582// Ask which file type(s) to pull out of the dropped folder(s) (mirrors the Import dialog's
1583// All/Raw/Raster filter), then recursively scan them on a background job -- scanning can be slow
1584// for a large/deep folder, and this runs from the GUI thread's drag-and-drop handler. The result
1585// is merged with `files` (already-known, directly-dropped regular files -- always imported
1586// regardless of the chosen filter: the question is about what to pull out of folders, not about
1587// re-filtering files the user explicitly picked) before everything is handed to
1588// dt_control_import(). Takes ownership of both `folders` and `files`.
1589static void _thumbtable_dnd_scan_folders_and_import(GList *folders, GList *files)
1590{
1591 GtkWidget *dialog = gtk_message_dialog_new(
1592 GTK_WINDOW(dt_gui_get_ui()->main_window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION,
1593 GTK_BUTTONS_NONE, "%s",
1594 _("The dropped folder(s) will be scanned recursively.\nWhich files should be imported?"));
1595#ifdef GDK_WINDOWING_QUARTZ
1597#endif
1598
1599 gtk_dialog_add_button(GTK_DIALOG(dialog), _("All image files"), _DT_DND_IMPORT_CHOICE_ALL);
1600 gtk_dialog_add_button(GTK_DIALOG(dialog), _("Raw image files"), _DT_DND_IMPORT_CHOICE_RAW);
1601 gtk_dialog_add_button(GTK_DIALOG(dialog), _("Raster image files"), _DT_DND_IMPORT_CHOICE_RASTER);
1602
1603 gtk_window_set_title(GTK_WINDOW(dialog), _("Import folder"));
1604 const gint choice = gtk_dialog_run(GTK_DIALOG(dialog));
1605 gtk_widget_destroy(dialog);
1606
1608 && choice != _DT_DND_IMPORT_CHOICE_RASTER)
1609 {
1610 // Dialog closed without a choice: treat the whole drop as cancelled, directly-dropped files
1611 // included, rather than second-guessing whether the user still wants those imported alone.
1612 g_list_free_full(folders, dt_free_gpointer);
1613 g_list_free_full(files, dt_free_gpointer);
1614 return;
1615 }
1616
1617 dt_job_t *job = dt_control_job_create(&_dnd_folder_import_job_run, "scan dropped folder(s) for import");
1618 if(IS_NULL_PTR(job))
1619 {
1620 g_list_free_full(folders, dt_free_gpointer);
1621 g_list_free_full(files, dt_free_gpointer);
1622 return;
1623 }
1624
1625 dt_dnd_folder_import_t *data = g_malloc0(sizeof(dt_dnd_folder_import_t));
1626 data->folders = folders;
1627 data->files = files;
1628 data->filter_type = choice == _DT_DND_IMPORT_CHOICE_RAW
1631
1634}
1635
1636static gboolean _thumbtable_dnd_import(GtkSelectionData *selection_data)
1637{
1638 gchar **uris = gtk_selection_data_get_uris(selection_data);
1639 int elements = 0;
1640 GList *files = NULL;
1641 GList *folders = NULL;
1642
1643 if(uris)
1644 {
1645 GVfs *vfs = g_vfs_get_default();
1646 for (int i = 0; !IS_NULL_PTR(uris[i]); i++)
1647 {
1648 GFile *filepath = g_vfs_get_file_for_uri(vfs, uris[i]);
1649 const gchar *pathname = g_strdup(g_file_get_path(filepath));
1650 files = _thumbtable_dnd_import_check(files, pathname, &elements, &folders);
1651 g_object_unref(filepath);
1652 }
1653
1654 if(folders)
1655 {
1656 // At least one dropped item is a folder: ask which file type(s) to pull out of it/them,
1657 // then scan (background job) and import everything -- folders and any directly-dropped
1658 // files together -- once done. Ownership of both lists passes to that flow.
1660 files = NULL;
1661 }
1662 else if(elements > 0)
1663 {
1664 // WARNING: we copy a Glist of pathes as char*
1665 // The references to the char* still belong to the original.
1666 // We will free them in the import job.
1667 dt_control_import_t data = {.imgs = g_list_copy(files),
1668 .datetime = g_date_time_new_now_local(),
1669 .copy = FALSE, // we only import in place.
1670 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
1671 .base_folder = dt_conf_get_string("session/base_directory_pattern"),
1672 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
1673 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
1674 .target_dir = NULL,
1675 .elements = elements,
1676 .discarded = NULL
1677 };
1678
1679 if(dt_control_import(data))
1680 dt_control_log(_("Could not start the import job."));
1681 }
1682 else fprintf(stderr,"No files to import. Check your selection or use 'File > Import'.");
1683 }
1684
1685 g_strfreev(uris);
1686 g_list_free(files);
1687 files = NULL;
1688 return elements >= 0 ? TRUE : FALSE;
1689}
1690
1691void dt_thumbtable_event_dnd_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
1692 GtkSelectionData *selection_data, guint target_type, guint time,
1693 gpointer user_data)
1694{
1695 gboolean success = FALSE;
1696
1697 if((target_type == DND_TARGET_URI) && (!IS_NULL_PTR(selection_data))
1698 && (gtk_selection_data_get_length(selection_data) >= 0))
1699 {
1700 success = _thumbtable_dnd_import(selection_data);
1701 }
1702
1703 gtk_drag_finish(context, success, FALSE, time);
1704}
1705
1706static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
1707{
1708 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1709 if(table->drag_list)
1710 {
1711 g_list_free(table->drag_list);
1712 table->drag_list = NULL;
1713 }
1714 // in any case, with reset the reordering class if any
1715 dt_gui_remove_class(table->grid, "dt_thumbtable_reorder");
1716}
1717
1718int _imgid_to_rowid(dt_thumbtable_t *table, int32_t imgid)
1719{
1720 if(IS_NULL_PTR(table->lut)) return UNKNOWN_IMAGE;
1721
1722 int rowid = UNKNOWN_IMAGE;
1723
1724 dt_pthread_mutex_lock(&table->lock);
1725 for(int i = 0; i < table->collection_count; i++)
1726 {
1727 if(table->lut[i].imgid == imgid)
1728 {
1729 rowid = i;
1730 break;
1731 }
1732 }
1734
1735 return rowid;
1736}
1737
1738void dt_thumbtable_move_in_grid(dt_thumbtable_t *table, GdkEventKey *event, dt_thumbtable_direction_t direction, int origin_imgid)
1739{
1740 if(IS_NULL_PTR(table->lut)) return;
1741 if(!gtk_widget_is_visible(table->scroll_window)) return;
1742
1743 int current_rowid = _imgid_to_rowid(table, origin_imgid);
1744 int offset = 0;
1745
1746 switch(direction)
1747 {
1748 case DT_TT_MOVE_UP:
1749 offset = - table->thumbs_per_row;
1750 break;
1751 case DT_TT_MOVE_DOWN:
1752 offset = + table->thumbs_per_row;
1753 break;
1754 case DT_TT_MOVE_LEFT:
1755 offset = - 1;
1756 break;
1757 case DT_TT_MOVE_RIGHT:
1758 offset = + 1;
1759 break;
1761 offset = - table->view_height / table->thumb_height * table->thumbs_per_row;
1762 break;
1764 offset = + table->view_height / table->thumb_height * table->thumbs_per_row;
1765 break;
1766 case DT_TT_MOVE_START:
1767 offset = - origin_imgid;
1768 break;
1769 case DT_TT_MOVE_END:
1770 offset = +table->collection_count; // will be clamped below, don't care
1771 break;
1772 }
1773
1774 int new_rowid = CLAMP(current_rowid + offset, 0, table->collection_count - 1);
1775
1776 dt_pthread_mutex_lock(&table->lock);
1777 int new_imgid = table->lut[new_rowid].imgid;
1779
1780 dt_thumbtable_dispatch_over(table, event->type, new_imgid);
1781
1782 if(!_is_rowid_visible(table, new_rowid))
1783 {
1784 // GUI update will be handled through the value-changed event of the GtkAdjustment
1785 dt_thumbtable_scroll_to_imgid(table, new_imgid);
1786 }
1787 else
1788 {
1789 // We still need to update all visible thumbs to keep mouse_over states in sync
1790 table->thumbs_inited = FALSE;
1791 dt_thumbtable_update(table);
1792 }
1793}
1794
1796{
1797 if(table->alternate_mode == enable) return;
1798 table->alternate_mode = enable;
1799
1800 dt_pthread_mutex_lock(&table->lock);
1801 GHashTableIter iter;
1802 gpointer value = NULL;
1803 g_hash_table_iter_init(&iter, table->list);
1804 while(g_hash_table_iter_next(&iter, NULL, &value))
1805 {
1808 }
1810}
1811
1812
1813gboolean dt_thumbtable_key_pressed_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
1814{
1815 if(!gtk_window_is_active(GTK_WINDOW(dt_gui_get_ui()->main_window))) return FALSE;
1816 if(IS_NULL_PTR(user_data)) return FALSE;
1817 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1818 if(IS_NULL_PTR(table->lut)) return FALSE;
1819
1820 // Find out the current image
1821 // NOTE: when moving into the grid from key arrow events,
1822 // if the cursor is still overlaying the grid when scrolling, it can collide
1823 // with key event and set the mouse_over focus elsewhere.
1824 // For this reason, we use our own private keyboard_over event,
1825 // and use the mouse_over as a fall-back only.
1826 // Key events are "knobby", therefore more reliale than "hover",
1827 // so they always take precedence.
1828 int32_t imgid = dt_control_get_keyboard_over_id();
1829 if(imgid < 0) imgid = dt_control_get_mouse_over_id();
1830 if(imgid < 0) imgid = dt_selection_get_first_id(dt_selection_get_global());
1831 if(imgid < 0 && table->lut)
1832 {
1833 dt_pthread_mutex_lock(&table->lock);
1834 imgid = table->lut[0].imgid;
1836 }
1837
1838 //fprintf(stdout, "%s\n", gtk_accelerator_name(event->keyval, event->state));
1839
1840 // Exit alternative mode on any keystroke other than alt
1841 if(event->keyval != GDK_KEY_Alt_L && event->keyval != GDK_KEY_Alt_R)
1842 _alternative_mode(table, FALSE);
1843
1844 guint key = dt_keys_mainpad_alternatives(event->keyval);
1845
1846 // Mode-specific navigation/selection keys first: the grid consumes Up/Down (row navigation) and
1847 // space/nobreakspace (selection); the filmstrip has none and lets them fall through.
1848 if(table->ops->handle_key && table->ops->handle_key(table, event, key, imgid))
1849 return TRUE;
1850
1851 // Keys handled identically in both modes.
1852 switch(key)
1853 {
1854 case GDK_KEY_Left:
1855 {
1856 dt_thumbtable_move_in_grid(table, event, DT_TT_MOVE_LEFT, imgid);
1857 return TRUE;
1858 }
1859 case GDK_KEY_Right:
1860 {
1861 dt_thumbtable_move_in_grid(table, event, DT_TT_MOVE_RIGHT, imgid);
1862 return TRUE;
1863 }
1864 case GDK_KEY_Page_Up:
1865 {
1867 return TRUE;
1868 }
1869 case GDK_KEY_Page_Down:
1870 {
1872 return TRUE;
1873 }
1874 case GDK_KEY_Home:
1875 {
1876 dt_thumbtable_move_in_grid(table, event, DT_TT_MOVE_START, imgid);
1877 return TRUE;
1878 }
1879 case GDK_KEY_End:
1880 {
1881 dt_thumbtable_move_in_grid(table, event, DT_TT_MOVE_END, imgid);
1882 return TRUE;
1883 }
1884 case GDK_KEY_Return:
1885 {
1886 // This is only to be consistent with mouse events:
1887 // opening to darkroom happens with double click (aka ACTIVATE event),
1888 // but the first click always select the clicked thumbnail before.
1889 // So we do the same here, even though it's not required and actually slightly annoying.
1890 // (grid selects the thumbnail first; the filmstrip just activates.)
1891 if(table->ops->pre_activate) table->ops->pre_activate(table, imgid);
1892
1894 return TRUE;
1895 }
1896 case GDK_KEY_Alt_L:
1897 case GDK_KEY_Alt_R:
1898 {
1899 _alternative_mode(table, TRUE);
1900 return TRUE;
1901 }
1902 case GDK_KEY_Delete:
1903 {
1904 // This is the "Remove image from library" shortcut, declared on Delete alone. A Delete
1905 // with a modifier that the dispatcher let through (Shift+Delete read as auto-repeat) is
1906 // not it, and must not remove anything.
1907 if(!dt_modifier_is(event->state, 0)) return FALSE;
1909 return TRUE;
1910 }
1911 }
1912 return FALSE;
1913}
1914
1915gboolean dt_thumbtable_key_released_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
1916{
1917 if(!gtk_window_is_active(GTK_WINDOW(dt_gui_get_ui()->main_window))) return FALSE;
1918
1919 if(IS_NULL_PTR(user_data)) return FALSE;
1920 dt_thumbtable_t *table = (dt_thumbtable_t *)user_data;
1921
1922 //fprintf(stdout, "%s\n", gtk_accelerator_name(event->keyval, event->state));
1923 _alternative_mode(table, FALSE);
1924 return FALSE;
1925}
1926
1927
1928static gboolean _draw_callback(GtkWidget *widget, cairo_t *cr, gpointer user_data)
1929{
1930 if(IS_NULL_PTR(user_data)) return TRUE;
1931
1932 // Ensure the background color is painted
1933 GtkStyleContext *context = gtk_widget_get_style_context(widget);
1934 GtkAllocation allocation;
1935 gtk_widget_get_allocation(widget, &allocation);
1936 gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);
1937 gtk_render_frame(context, cr, 0, 0, allocation.width, allocation.height);
1938
1939 return FALSE;
1940}
1941
1946
1947gboolean _event_main_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1948{
1949 if(IS_NULL_PTR(user_data)) return TRUE;
1951 return TRUE;
1952}
1953
1954
1955// Single dispatch point mapping a mode to its layout strategy. Every former if(mode==...) branch
1956// now routes through the returned vtable (see thumbtable_internal.h and the two frontends).
1958{
1959 switch(mode)
1960 {
1964 default:
1965 return dt_thumbtable_grid_ops();
1966 }
1967}
1968
1970{
1971 dt_thumbtable_t *table = (dt_thumbtable_t *)calloc(1, sizeof(dt_thumbtable_t));
1972
1973 // Bind the layout strategy before building the content widget: the frontend decides whether the
1974 // content is a GtkFixed (grid) or a GtkLayout (filmstrip).
1975 table->mode = mode;
1976 table->ops = _ops_for_mode(mode);
1977
1978 table->scroll_window = gtk_scrolled_window_new(NULL, NULL);
1979 // Named so the theme can zero its "scrollbar-spacing" style property (see dt_thumbtable_configure).
1980 gtk_widget_set_name(table->scroll_window, "thumbtable-scroll");
1981 gtk_scrolled_window_set_overlay_scrolling(GTK_SCROLLED_WINDOW(table->scroll_window), FALSE);
1982 // No frame: the image grid is full-bleed content and needs no recessed border.
1983 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(table->scroll_window), GTK_SHADOW_NONE);
1984
1985 table->v_scrollbar = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(table->scroll_window));
1986 table->h_scrollbar = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(table->scroll_window));
1987 g_signal_connect(G_OBJECT(table->v_scrollbar), "value-changed", G_CALLBACK(_scrollbar_value_changed), table);
1988 g_signal_connect(G_OBJECT(table->h_scrollbar), "value-changed", G_CALLBACK(_scrollbar_value_changed), table);
1989 g_signal_connect(G_OBJECT(table->v_scrollbar), "notify::page-size", G_CALLBACK(_scrollbar_page_size_notify), table);
1990 g_signal_connect(G_OBJECT(table->h_scrollbar), "notify::page-size", G_CALLBACK(_scrollbar_page_size_notify), table);
1991
1992 // Track actual scrollbar widget allocations: filmstrip sizing depends on the horizontal scrollbar height,
1993 // and filemanager fallback sizing depends on the vertical scrollbar width.
1994 GtkWidget *h_scroll = gtk_scrolled_window_get_hscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
1995 GtkWidget *v_scroll = gtk_scrolled_window_get_vscrollbar(GTK_SCROLLED_WINDOW(table->scroll_window));
1996 if(h_scroll)
1997 g_signal_connect(G_OBJECT(h_scroll), "size-allocate", G_CALLBACK(_scrollbar_widget_size_allocate), table);
1998 if(v_scroll)
1999 g_signal_connect(G_OBJECT(v_scroll), "size-allocate", G_CALLBACK(_scrollbar_widget_size_allocate), table);
2000
2001 // Content widget: GtkFixed for the grid, GtkLayout for the filmstrip (the latter is a
2002 // GtkScrollable, avoiding the implicit GtkViewport that collapsed the strip - issue #877).
2003 table->grid = table->ops->create_content_widget();
2004 dt_gui_add_class(table->grid, "dt_thumbtable");
2005 gtk_container_add(GTK_CONTAINER(table->scroll_window), table->grid);
2006
2007 // A non-scrollable content widget (GtkFixed) gets wrapped in an implicit GtkViewport whose
2008 // default shadow is GTK_SHADOW_IN (a .frame border). Drop it so it doesn't add to the scrolled
2009 // window's size. A GtkScrollable (GtkLayout) is added directly, with no viewport to adjust.
2010 GtkWidget *viewport = gtk_bin_get_child(GTK_BIN(table->scroll_window));
2011 if(GTK_IS_VIEWPORT(viewport))
2012 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
2013
2014 g_object_set_data(G_OBJECT(table->grid), DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY, GINT_TO_POINTER(1));
2015 gtk_widget_set_has_tooltip(table->grid, FALSE);
2016 // The filmstrip must never hold keyboard focus (see its ops' grab_focus comment): clicking one
2017 // of its thumbnails would otherwise steal focus away from the current view's own center widget,
2018 // and Return then gets swallowed here (raising a plain ACTIVATE/preview) instead of reaching the
2019 // view's own key_pressed (e.g. Studio Capture's "open in darkroom").
2020 const gboolean can_focus = (table->mode != DT_THUMBTABLE_MODE_FILMSTRIP);
2021 gtk_widget_set_can_focus(table->grid, can_focus);
2022 gtk_widget_set_focus_on_click(table->grid, can_focus);
2023 gtk_widget_add_events(table->grid, GDK_LEAVE_NOTIFY_MASK);
2024 gtk_widget_set_app_paintable(table->grid, TRUE);
2025 g_signal_connect(G_OBJECT(table->grid), "leave-notify-event", G_CALLBACK(_event_main_leave), table);
2026
2027 // Disable auto re-scrolling to beginning when a child of scrolled window gets the focus
2028 // Doesn't seem to work...
2029 // https://stackoverflow.com/questions/26693042/gtkscrolledwindow-disable-scroll-to-focused-child
2030 GtkAdjustment *dummy = gtk_adjustment_new(0., 0., 1., 1., 1., 1.);
2031 gtk_container_set_focus_hadjustment(GTK_CONTAINER(table->scroll_window), dummy);
2032 gtk_container_set_focus_vadjustment(GTK_CONTAINER(table->scroll_window), dummy);
2033 gtk_container_set_focus_hadjustment(GTK_CONTAINER(table->grid), dummy);
2034 gtk_container_set_focus_vadjustment(GTK_CONTAINER(table->grid), dummy);
2035 g_object_unref(dummy);
2036
2037 // drag and drop : used for reordering, interactions with maps, exporting uri to external apps, importing images
2038 // in filmroll...
2039 gtk_drag_source_set(table->grid, GDK_BUTTON1_MASK, target_list_all, n_targets_all, GDK_ACTION_MOVE);
2040 gtk_drag_dest_set(table->grid, GTK_DEST_DEFAULT_ALL, target_list_all, n_targets_all, GDK_ACTION_MOVE);
2041 g_signal_connect_after(table->grid, "drag-begin", G_CALLBACK(_event_dnd_begin), table);
2042 g_signal_connect_after(table->grid, "drag-end", G_CALLBACK(_event_dnd_end), table);
2043 g_signal_connect(table->grid, "drag-data-get", G_CALLBACK(_event_dnd_get), table);
2044 g_signal_connect(table->grid, "drag-data-received", G_CALLBACK(dt_thumbtable_event_dnd_received), table);
2045
2046 gtk_widget_add_events(table->grid, GDK_STRUCTURE_MASK | GDK_EXPOSURE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
2047 g_signal_connect(table->grid, "draw", G_CALLBACK(_draw_callback), table);
2048 g_signal_connect(table->grid, "key-press-event", G_CALLBACK(dt_thumbtable_key_pressed_grid), table);
2049 g_signal_connect(table->grid, "key-release-event", G_CALLBACK(dt_thumbtable_key_released_grid), table);
2050 gtk_widget_show(table->grid);
2051
2052 table->thumb_nb = 0;
2053 table->grid_cols = 0;
2054 table->collection_inited = FALSE;
2055 table->configured = FALSE;
2056 table->thumbs_inited = FALSE;
2057 table->collection_hash = -1;
2058 table->list = g_hash_table_new(g_direct_hash, g_direct_equal);
2059 table->collection_count = 0;
2060 table->min_row_id = 0;
2061 table->max_row_id = 0;
2062 table->lut = NULL;
2063 table->reset_collection = FALSE;
2064 table->x_position = 0.;
2065 table->y_position = 0.;
2066 table->alternate_mode = FALSE;
2067 table->rowid = -1;
2068 table->collapse_groups = dt_conf_get_bool("ui_last/grouping");
2069 table->draw_group_borders = dt_conf_get_bool("plugins/lighttable/group_borders");
2070 table->idle_update_id = 0;
2071 table->focus_idle_id = 0;
2072 table->evict_idle_id = 0;
2073 table->last_parent_width = 0;
2074 table->last_parent_height = 0;
2075 table->last_h_scrollbar_height = -1;
2076 table->last_v_scrollbar_width = -1;
2077
2078 dt_pthread_mutex_init(&table->lock, NULL);
2079
2080 dt_gui_add_help_link(table->grid, dt_get_help_url("lighttable_filemanager"));
2081
2082 // set css name and class
2083 gtk_widget_set_name(table->grid, "thumbtable-filemanager");
2084
2085 // overlays mode
2087
2088 // we register globals signals
2090 G_CALLBACK(_dt_collection_changed_callback), table);
2092 G_CALLBACK(_dt_selection_changed_callback), table);
2094 G_CALLBACK(_dt_active_images_changed_callback), table);
2096 G_CALLBACK(_dt_profile_change_callback), table);
2098 G_CALLBACK(_dt_image_info_changed_callback), table);
2100 G_CALLBACK(_mouse_over_image_callback), table);
2101
2102 dt_thumbtable_set_parent(table, mode);
2103
2104 // The file manager belongs only to lighttable, while the filmstrip widget is
2105 // shared by darkroom, print and map. Each owner view needs its own accel
2106 // paths so shortcut search, menus and dispatch stay in sync with the
2107 // currently active accel group.
2108 GtkAccelGroup *accel_groups[] = {
2113 };
2114 const char *path_bases[] = {
2115 _("Lighttable/Thumbtable"),
2116 _("Darkroom/Filmstrip"),
2117 _("Print/Filmstrip"),
2118 _("Map/Filmstrip")
2119 };
2120 // The filemanager owns only the lighttable accel group (index 0); the shared filmstrip owns
2121 // the darkroom/print/map groups (indices 1-3).
2122 int first_group = 1, last_group = 4;
2123 switch(mode)
2124 {
2126 first_group = 0;
2127 last_group = 1;
2128 break;
2129 default:
2130 break;
2131 }
2132
2133 gchar *path = NULL;
2134 for(int group_index = first_group; group_index < last_group; group_index++)
2135 {
2136 GtkAccelGroup *accel_group = accel_groups[group_index];
2137 const char *path_base = path_bases[group_index];
2138
2139 path = dt_accels_build_path(path_base, _("Move up"));
2141 path, table->grid, GDK_KEY_Up, 0);
2142 dt_free(path);
2143 path = dt_accels_build_path(path_base, _("Move down"));
2145 path, table->grid, GDK_KEY_Down, 0);
2146 dt_free(path);
2147 path = dt_accels_build_path(path_base, _("Move left"));
2149 path, table->grid, GDK_KEY_Left, 0);
2150 dt_free(path);
2151 path = dt_accels_build_path(path_base, _("Move right"));
2153 path, table->grid, GDK_KEY_Right, 0);
2154 dt_free(path);
2155 path = dt_accels_build_path(path_base, _("Go to previous page"));
2157 path, table->grid, GDK_KEY_Page_Up, 0);
2158 dt_free(path);
2159 path = dt_accels_build_path(path_base, _("Go to next page"));
2161 path, table->grid, GDK_KEY_Page_Down, 0);
2162 dt_free(path);
2163 path = dt_accels_build_path(path_base, _("Go to the start"));
2165 path, table->grid, GDK_KEY_Home, 0);
2166 dt_free(path);
2167 path = dt_accels_build_path(path_base, _("Go to the end"));
2169 path, table->grid, GDK_KEY_End, 0);
2170 dt_free(path);
2171 path = dt_accels_build_path(path_base, _("Select the current thumbnail"));
2173 path, table->grid, GDK_KEY_space, 0);
2174 dt_free(path);
2175 // Kept as the literal Ctrl key (not DT_PRIMARY_MASK): Cmd+Space is reserved
2176 // system-wide by macOS to open Spotlight.
2177 path = dt_accels_build_path(path_base, _("Toogle the current thumbnail from selection"));
2179 path, table->grid, GDK_KEY_space, GDK_CONTROL_MASK);
2180 dt_free(path);
2181 path = dt_accels_build_path(path_base, _("Expand the current selection up to the hovered thumbnail"));
2183 path, table->grid, GDK_KEY_space, GDK_SHIFT_MASK);
2184 dt_free(path);
2185 path = dt_accels_build_path(path_base, _("Open the current thumbnail in darkroom"));
2187 path, table->grid, GDK_KEY_Return, 0);
2188 dt_free(path);
2189 path = dt_accels_build_path(path_base, _("Enable thumbnail transient alternative view"));
2191 path, table->grid, GDK_KEY_Alt_L, 0);
2192 dt_free(path);
2193 }
2194
2195 path = dt_accels_build_path(_("Global/Menu/File"), _("Remove image from library"));
2197 path, table->grid, GDK_KEY_Delete, 0);
2198 dt_free(path);
2199 return table;
2200}
2201
2202
2204{
2205 // Cleanup existing stuff
2206 const double start = dt_get_wtime();
2207
2208 dt_pthread_mutex_lock(&table->lock);
2209 if(table->lut)
2210 for(int rowid = 0; rowid < table->collection_count; rowid++)
2211 table->lut[rowid].thumb = NULL;
2212
2213 // WARNING: we need to detach children from parent starting from the last
2214 // otherwise, Gtk updates the index of all the next children in sequence
2215 // and that takes forever when thumb_nb > 1000
2216 GList *thumbs = g_hash_table_get_values(table->list);
2217 thumbs = g_list_sort(thumbs, _thumb_compare_rowid_desc);
2218 g_hash_table_remove_all(table->list);
2220
2221 for(GList *l = thumbs; l; l = g_list_next(l))
2222 {
2223 dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
2224 gtk_widget_hide(thumb->widget);
2225 dt_thumbnail_destroy(thumb);
2226 }
2227 g_list_free(thumbs);
2228 thumbs = NULL;
2229
2230 dt_print(DT_DEBUG_LIGHTTABLE, "Cleaning the list of %i elements in %0.04f sec\n", table->thumb_nb,
2231 dt_get_wtime() - start);
2232
2233 table->thumb_nb = 0;
2234 table->thumbs_inited = FALSE;
2235}
2236
2237
2239{
2240 if(table->idle_update_id)
2241 {
2242 g_source_remove(table->idle_update_id);
2243 table->idle_update_id = 0;
2244 }
2245 if(table->focus_idle_id)
2246 {
2247 g_source_remove(table->focus_idle_id);
2248 table->focus_idle_id = 0;
2249 }
2250 if(table->evict_idle_id)
2251 {
2252 g_source_remove(table->evict_idle_id);
2253 table->evict_idle_id = 0;
2254 }
2255
2261
2263
2265
2266 g_hash_table_destroy(table->list);
2267 if(table->lut)
2268 {
2269 dt_free(table->lut);
2270 }
2271
2272 dt_free(table);
2273}
2274
2276{
2277 if(IS_NULL_PTR(table)) return;
2278
2279 if(table->idle_update_id)
2280 {
2281 g_source_remove(table->idle_update_id);
2282 table->idle_update_id = 0;
2283 }
2284 if(table->focus_idle_id)
2285 {
2286 g_source_remove(table->focus_idle_id);
2287 table->focus_idle_id = 0;
2288 }
2289 if(table->evict_idle_id)
2290 {
2291 g_source_remove(table->evict_idle_id);
2292 table->evict_idle_id = 0;
2293 }
2294
2295 table->reset_collection = TRUE;
2296
2297 dt_pthread_mutex_lock(&table->lock);
2298 if(table->lut)
2299 for(int rowid = 0; rowid < table->collection_count; rowid++)
2300 table->lut[rowid].thumb = NULL;
2301
2302 GList *thumbs = g_hash_table_get_values(table->list);
2303 thumbs = g_list_sort(thumbs, _thumb_compare_rowid_desc);
2304 g_hash_table_remove_all(table->list);
2306
2307 for(GList *l = thumbs; l; l = g_list_next(l))
2308 {
2309 dt_thumbnail_t *thumb = (dt_thumbnail_t *)l->data;
2310 gtk_widget_hide(thumb->widget);
2311 dt_thumbnail_destroy(thumb);
2312 }
2313 g_list_free(thumbs);
2314
2315 table->thumb_nb = 0;
2316 table->thumbs_inited = FALSE;
2317}
2318
2320{
2321 dt_thumbtable_schedule_focus(table, G_PRIORITY_DEFAULT_IDLE);
2322}
2323
2325{
2326 table->mode = mode;
2327 table->ops = _ops_for_mode(mode);
2328
2329 table->parent_overlay = gtk_overlay_new();
2330 g_signal_connect(G_OBJECT(table->parent_overlay), "size-allocate", G_CALLBACK(_parent_overlay_size_allocate), table);
2331
2332 // The frontend builds its own scroll stack (overlay child vs. main child), widget name, help link
2333 // and scroll policy.
2334 table->ops->setup_parent(table);
2335}
2336
2338{
2339 if(!table->collection_inited || table->collection_count == 0) return;
2340
2341 if(table->collapse_groups)
2342 dt_control_log(_("Image groups are collapsed in view.\n"
2343 "Selecting all images will only target visible members of image groups.\n"
2344 "Uncollapse groups to select all their members"));
2345
2346 GList *img = NULL;
2347
2348 dt_pthread_mutex_lock(&table->lock);
2349 for(size_t i = 0; i < table->collection_count; i++)
2350 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2352
2353 if(img)
2354 {
2356 g_list_free(img);
2357 img = NULL;
2358 }
2359}
2360
2361void dt_thumbtable_select_range(dt_thumbtable_t *table, const int rowid)
2362{
2363 if(!table->collection_inited || table->collection_count == 0) return;
2364 if(rowid < 0 || rowid > table->collection_count - 1) return;
2365
2366 if(table->collapse_groups)
2367 dt_control_log(_("Image groups are collapsed in view.\n"
2368 "Selecting a range of images will only target visible members of image groups.\n"
2369 "Uncollapse groups to select all their members"));
2370
2371 dt_pthread_mutex_lock(&table->lock);
2372
2373 // Find the bounds of the current selection
2374 size_t rowid_end = 0;
2375 size_t rowid_start = table->collection_count - 1;
2377
2378 if(selected)
2379 {
2380 for(GList *s = g_list_first(selected); s; s = g_list_next(s))
2381 {
2382 int32_t imgid = GPOINTER_TO_INT(s->data);
2383 int row = dt_thumbtable_find_rowid_from_imgid(table, imgid);
2384 if(row < 0) continue; // not found - should not happen
2385 if(row < rowid_start) rowid_start = row;
2386 if(row > rowid_end) rowid_end = row;
2387 }
2388 g_list_free(selected);
2389 selected = NULL;
2390 }
2391 else
2392 {
2393 // range selection always has to start from a previous selection
2395 return;
2396 }
2397
2398 if(rowid_start > rowid_end)
2399 {
2400 // the start is strictly after the end, we have a deep problem
2402 return;
2403 }
2404
2405 // Find the extra imgids to select
2406 GList *img = NULL;
2407 if(rowid > rowid_end && rowid_end < table->collection_count - 1)
2408 {
2409 // select after
2410 for(int i = rowid_end + 1; i <= rowid; i++)
2411 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2412 }
2413 else if(rowid < rowid_start && rowid_start > 0)
2414 {
2415 // select before
2416 for(int i = rowid_start - 1; i >= rowid; i--)
2417 img = g_list_prepend(img, GINT_TO_POINTER(table->lut[i].imgid));
2418 }
2419 // else: select within. What should that yield ??? Deselect ?
2420
2422
2423 if(img)
2424 {
2426 g_list_free(img);
2427 img = NULL;
2428 }
2429}
2430
2432{
2433 if(!table->collection_inited || table->collection_count == 0) return;
2434
2435 // Record initial selection, select all, then deselect initial selection
2436 GList *to_deselect = dt_selection_get_list(dt_selection_get_global());
2437 if(to_deselect)
2438 {
2441 g_list_free(to_deselect);
2442 to_deselect = NULL;
2443 }
2444}
2445
2446static gint64 next_over_time = 0;
2447
2448void dt_thumbtable_dispatch_over(dt_thumbtable_t *table, GdkEventType type, int32_t imgid)
2449{
2450 if(!gtk_widget_is_visible(table->scroll_window)) return;
2451
2452 gint64 current_time = g_get_real_time(); // microseconds
2453 if(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE)
2454 {
2455 // allow the mouse to capture the next hover events
2456 // in more than 100 ms:
2457 next_over_time = current_time + 100000;
2458
2461 }
2462 else if(type == GDK_ENTER_NOTIFY || type == GDK_LEAVE_NOTIFY)
2463 {
2464 // When navigating the grid with arrow keys, the view will get scrolled.
2465 // If the mouse pointer is over the grid, it will enter a new thumbnail
2466 // which will trigger leave/enter events.
2467 // But we don't want that when interacting from the keyboard, so disallow
2468 // recording enter/leave events in the next 100 ms after keyboard interaction.
2469 if(current_time > next_over_time)
2471 else
2472 return;
2473 }
2474 else if(type == GDK_MOTION_NOTIFY || type == GDK_BUTTON_PRESS || type == GDK_2BUTTON_PRESS)
2475 {
2476 // Active mouse pointer interactions: accept unconditionnaly
2478 }
2479 else
2480 {
2481 fprintf(stderr, "[dt_thumbtable_dispatch_over] unsupported event type: %i\n", type);
2482 return;
2483 }
2484
2485 dt_pthread_mutex_lock(&table->lock);
2486 table->rowid = dt_thumbtable_find_rowid_from_imgid(table, imgid);
2488
2489 // Attempt to re-grab focus on every interaction to restore keyboard navigation,
2490 // for example after a combobox grabbed it on click.
2491 if(!gtk_widget_has_focus(table->grid))
2492 {
2493 // But giving focus to the grid scrolls it back to top, so we have to re-scroll it after
2494 double x = 0.;
2495 double y = 0.;
2497 gtk_widget_grab_focus(table->grid);
2499 }
2500}
2501
2502// clang-format off
2503// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
2504// vim: shiftwidth=2 expandtab tabstop=2 cindent
2505// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
2506// 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)
Build the accel path of feature under scope.
Handle default and user-set shortcuts (accelerators)
#define DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY
GList * dt_act_on_get_images()
Definition act_on.c:38
struct dt_accels_t * dt_gui_get_accels(void)
struct dt_ui_t * dt_gui_get_ui(void)
void dt_gui_add_help_link(GtkWidget *widget, char *link)
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
dt_collection_t * dt_collection_get_global(void)
Definition collection.c:138
uint32_t dt_collection_get_count(const dt_collection_t *collection)
Definition collection.c:269
dt_collection_properties_t
Definition collection.h:120
@ DT_COLLECTION_PROP_GROUPING
Definition collection.h:143
dt_collection_change_t
Definition collection.h:160
@ DT_COLLECTION_CHANGE_BACKGROUND_SYNC
Definition collection.h:174
uint64_t dt_collection_query_get_generation(void)
dt_iop_colorout_gui_data_t dummy
Definition colorout.c:644
static const float x
const float *const lut
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
Read the stored string for name as a private copy.
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
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.
int32_t dt_control_get_mouse_over_id()
Definition control.c:1032
void dt_control_set_mouse_over_id(int32_t value)
Definition control.c:1040
void dt_control_log(const char *msg,...)
Definition control.c:839
int32_t dt_control_get_keyboard_over_id()
Definition control.c:1058
void dt_control_set_keyboard_over_id(int32_t value)
Definition control.c:1066
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:654
gboolean dt_control_remove_images()
struct dt_view_manager_t * dt_view_manager_get_global(void)
Definition darktable.c:602
struct dt_gui_gtk_t * dt_gui_get_global(void)
Definition darktable.c:526
GHashTable * selected
set of checked row labels, mirrored to conf on every change
GtkWidget * folder
destination folder chooser
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:127
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Initialise a mutex. With mutexattr NULL – which is how 54 of the 56 call sites in this tree spell it ...
Definition dtpthread.h:104
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:132
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:117
const dt_thumbtable_layout_ops_t * dt_thumbtable_grid_ops(void)
const dt_thumbtable_layout_ops_t * dt_thumbtable_filmstrip_ops(void)
Definition filmstrip.c:250
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:118
#define g_list_is_singleton(list)
Definition glib_utils.h:33
static uint64_t dt_hash(uint64_t hash, const char *str, size_t size)
Definition hash.h:55
static gboolean enable(const dt_image_t *image)
Definition highlights.c:879
#define UNKNOWN_IMAGE
Definition image.h:78
dt_image_t * dt_image_cache_testget(const int32_t imgid, char mode)
void dt_image_derive_fields(dt_image_t *img)
Compute the fields the database does not store: rating, monochrome and HDR predicates,...
dt_image_t * dt_image_cache_get(const int32_t imgid, char mode)
void dt_image_cache_read_release(const dt_image_t *img)
gboolean dt_supported_image(const gchar *filename)
Definition darktable.c:384
void dt_image_repository_foreach_collected(dt_image_repository_collected_cb cb, void *user_data)
Every image of the current collection, in collection order, already mapped.
Reading and writing one dt_image_t to and from the library database. The SQL half of what used to be ...
gboolean dt_import_passes_filter(const dt_import_filter_type_t filter_type, const gchar *pathname)
Definition import.c:203
dt_import_filter_type_t
Definition import.h:46
@ DT_IMPORT_FILTER_ALL
Definition import.h:47
@ DT_IMPORT_FILTER_RASTER
Definition import.h:49
@ DT_IMPORT_FILTER_RAW
Definition import.h:48
int 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.
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:138
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:419
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:132
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:115
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:57
hash
Definition layers.h:90
_lib_location_type_t type
Definition location.c:1
@ DT_DEBUG_LIGHTTABLE
Definition logging.h:60
@ DT_DEBUG_IMPORT
Definition logging.h:77
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#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 macros.h:96
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:105
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
const char * name
Definition pdf.h:90
int32_t dt_selection_get_first_id(struct dt_selection_t *selection)
Definition selection.c:103
void dt_selection_deselect_list(struct dt_selection_t *selection, const GList *const l)
Definition selection.c:354
struct dt_selection_t * dt_selection_get_global(void)
Definition selection.c:80
GList * dt_selection_get_list(struct dt_selection_t *selection)
Definition selection.c:185
void dt_selection_select_list(struct dt_selection_t *selection, const GList *const l)
Definition selection.c:330
void dt_selection_deselect(dt_selection_t *selection, int32_t imgid)
Definition selection.c:288
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:411
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:390
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE
Definition signal.h:99
@ DT_SIGNAL_ACTIVE_IMAGES_CHANGE
This signal is raised when image shown in the main view change no param, no returned value.
Definition signal.h:68
@ 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:246
@ 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:63
@ DT_SIGNAL_IMAGE_INFO_CHANGED
This signal is raised when any of image info has changed
Definition signal.h:148
@ DT_SIGNAL_SELECTION_CHANGED
This signal is raised when the selection is changed no param, no returned value.
Definition signal.h:131
@ DT_SIGNAL_COLLECTION_CHANGED
This signal is raised when collection changed. To avoid leaking the list, dt_collection_t is connecte...
Definition signal.h:126
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
unsigned __int64 uint64_t
Definition strptime.c:75
dt_thumbtable_t * table
GtkAccelGroup * map_accels
GtkAccelGroup * print_accels
GtkAccelGroup * lighttable_accels
GtkAccelGroup * darkroom_accels
dt_import_filter_type_t filter_type
int32_t group_id
Definition image.h:410
int32_t id
Definition image.h:410
gboolean mouse_over
Definition thumbnail.h:74
dt_pthread_mutex_t lock
Definition thumbnail.h:131
dt_image_t info
Definition thumbnail.h:77
gboolean selected
Definition thumbnail.h:75
int32_t rowid
Definition thumbnail.h:69
dt_thumbnail_overlay_t over
Definition thumbnail.h:107
cairo_surface_t * img_surf
Definition thumbnail.h:86
GtkWidget * widget
Definition thumbnail.h:80
Cache entry for a single thumbnail.
Definition thumbtable.h:90
dt_thumbnail_t * thumb
Definition thumbtable.h:91
Per-mode layout strategy. One instance per frontend, shared (const) by all tables of that mode and st...
void(* pre_activate)(dt_thumbtable_t *table, int32_t imgid)
void(* on_drag_begin)(dt_thumbtable_t *table, int32_t imgid)
int(* position_to_rowid)(dt_thumbtable_t *table, const double x, const double y)
void(* configure_dims)(dt_thumbtable_t *table, int *new_width, int *new_height, int *per_row, int *thumb_width, int *thumb_height)
void(* get_row_ids)(dt_thumbtable_t *table, int *rowid_min, int *rowid_max)
gboolean(* wants_page_size_notify)(dt_thumbtable_t *table, GObject *object)
void(* move_child)(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
void(* group_borders)(dt_thumbtable_t *table, dt_thumbnail_t *thumb, dt_thumbnail_border_t *borders)
gboolean(* wants_scroll_value)(dt_thumbtable_t *table, GtkAdjustment *adjustment)
void(* update_content_size)(dt_thumbtable_t *table)
void(* grab_focus)(dt_thumbtable_t *table)
gboolean(* is_rowid_visible)(dt_thumbtable_t *table, int rowid)
void(* rowid_to_position)(dt_thumbtable_t *table, int rowid, int *x, int *y)
void(* on_thumbnail_added)(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
gboolean(* relevant_scrollbar_changed)(dt_thumbtable_t *table, GtkWidget *widget, GtkAllocation *allocation)
gboolean(* is_thumb_highlighted)(dt_thumbtable_t *table, int32_t imgid)
GtkWidget *(* create_content_widget)(void)
gboolean(* handle_key)(dt_thumbtable_t *table, GdkEventKey *event, guint key, int32_t imgid)
void(* place_child)(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
void(* setup_parent)(dt_thumbtable_t *table)
const dt_thumbtable_layout_ops_t * ops
Definition thumbtable.h:107
GHashTable * list
Definition thumbtable.h:119
gboolean focus_peaking
Definition thumbtable.h:190
gboolean collapse_groups
Definition thumbtable.h:183
GtkWidget * grid
Definition thumbtable.h:111
gboolean alternate_mode
Definition thumbtable.h:175
gboolean collection_inited
Definition thumbtable.h:141
dt_thumbtable_mode_t mode
Definition thumbtable.h:104
dt_pthread_mutex_t lock
Definition thumbtable.h:169
int last_h_scrollbar_height
Definition thumbtable.h:205
GtkAdjustment * v_scrollbar
Definition thumbtable.h:158
int last_v_scrollbar_width
Definition thumbtable.h:206
gboolean focus_regions
Definition thumbtable.h:189
GList * drag_list
Definition thumbtable.h:133
gboolean configured
Definition thumbtable.h:143
GtkWidget * scroll_window
Definition thumbtable.h:155
dt_thumbtable_cache_t * lut
Definition thumbtable.h:153
gboolean thumbs_inited
Definition thumbtable.h:142
GtkAdjustment * h_scrollbar
Definition thumbtable.h:159
uint32_t thumb_nb
Definition thumbtable.h:136
dt_thumbnail_overlay_t overlays
Definition thumbtable.h:109
gboolean reset_collection
Definition thumbtable.h:172
GtkWidget * parent_overlay
Definition thumbtable.h:164
uint64_t collection_hash
Definition thumbtable.h:146
gboolean draw_group_borders
Definition thumbtable.h:192
dt_thumbtable_t * thumbtable_lighttable
dt_thumbtable_t * thumbtable_filmstrip
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:1391
void dt_thumbnail_set_mouseover(dt_thumbnail_t *thumb, gboolean over)
Definition thumbnail.c:1676
void dt_thumbnail_resync_info(dt_thumbnail_t *thumb, const dt_image_t *const info)
Definition thumbnail.c:1371
void dt_thumbnail_set_overlay(dt_thumbnail_t *thumb, dt_thumbnail_overlay_t mode)
Definition thumbnail.c:1508
void dt_thumbnail_set_group_border(dt_thumbnail_t *thumb, dt_thumbnail_border_t border)
Definition thumbnail.c:1651
int dt_thumbnail_destroy(dt_thumbnail_t *thumb)
Definition thumbnail.c:1419
void dt_thumbnail_update_gui(dt_thumbnail_t *thumb)
Definition thumbnail.c:1494
void dt_thumbnail_update_selection(dt_thumbnail_t *thumb, gboolean selected)
Definition thumbnail.c:923
void dt_thumbnail_alternative_mode(dt_thumbnail_t *thumb, gboolean enable)
Definition thumbnail.c:964
void dt_thumbnail_resize(dt_thumbnail_t *thumb, int width, int height)
Definition thumbnail.c:1606
#define dt_thumbnail_image_refresh(thumb)
Definition thumbnail.h:166
static dt_thumbnail_overlay_t sanitize_overlays(dt_thumbnail_overlay_t overlays)
Definition thumbnail.h:174
dt_thumbnail_border_t
Definition thumbnail.h:50
dt_thumbnail_overlay_t
Definition thumbnail.h:59
@ DT_THUMBNAIL_OVERLAYS_HOVER_NORMAL
Definition thumbnail.h:61
@ DT_THUMBNAIL_OVERLAYS_ALWAYS_NORMAL
Definition thumbnail.h:62
@ DT_THUMBNAIL_OVERLAYS_NONE
Definition thumbnail.h:60
static int _position_to_rowid(dt_thumbtable_t *table, const double x, const double y)
Definition thumbtable.c:360
void _add_thumbnail_at_rowid(dt_thumbtable_t *table, const size_t rowid, const int32_t mouse_over)
Definition thumbtable.c:657
static void _scrollbar_page_size_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
Definition thumbtable.c:247
int dt_thumbtable_scroll_to_selection(dt_thumbtable_t *table)
Scroll to show selected content.
Definition thumbtable.c:458
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:425
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)
Definition thumbtable.c:971
static gboolean _thumbtable_idle_update(gpointer user_data)
Definition thumbtable.c:211
void dt_thumbtable_update(dt_thumbtable_t *table)
Definition thumbtable.c:838
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:382
void dt_thumbtable_get_scroll_position(dt_thumbtable_t *table, double *x, double *y)
Definition thumbtable.c:376
static gint _thumb_compare_rowid_desc(gconstpointer a, gconstpointer b)
Definition thumbtable.c:184
static int _grab_focus(dt_thumbtable_t *table)
Definition thumbtable.c:194
static int dt_thumbtable_scroll_to_position(dt_thumbtable_t *table, const double x, const double y)
Definition thumbtable.c:390
static void _thumbs_update_overlays_mode(dt_thumbtable_t *table)
void dt_thumbtable_configure(dt_thumbtable_t *table)
Definition thumbtable.c:557
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:510
static void _dt_active_images_changed_callback(gpointer instance, gpointer user_data)
Definition thumbtable.c:948
gboolean _is_rowid_visible(dt_thumbtable_t *table, int rowid)
Definition thumbtable.c:477
int _imgid_to_rowid(dt_thumbtable_t *table, int32_t imgid)
gboolean dt_thumbtable_get_focus_regions(dt_thumbtable_t *table)
Definition thumbtable.c:960
void dt_thumbtable_set_focus_peaking(dt_thumbtable_t *table, gboolean enable)
Definition thumbtable.c:965
static gboolean _evict_idle(gpointer user_data)
Definition thumbtable.c:827
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:273
static void _dt_collection_lut(dt_thumbtable_t *table)
void _resize_thumbnails(dt_thumbtable_t *table)
Definition thumbtable.c:757
void _populate_thumbnails(dt_thumbtable_t *table)
Definition thumbtable.c:747
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:607
_dt_dnd_import_choice
@ _DT_DND_IMPORT_CHOICE_RASTER
@ _DT_DND_IMPORT_CHOICE_RAW
@ _DT_DND_IMPORT_CHOICE_ALL
gboolean _update_row_ids(dt_thumbtable_t *table)
Definition thumbtable.c:484
static void _scrollbar_value_changed(GtkAdjustment *adjustment, gpointer user_data)
Definition thumbtable.c:236
static const dt_thumbtable_layout_ops_t * _ops_for_mode(dt_thumbtable_mode_t mode)
static void _thumbtable_schedule_update(dt_thumbtable_t *table)
Definition thumbtable.c:223
static void _parent_overlay_size_allocate(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
Definition thumbtable.c:260
void _add_thumbnail_group_borders(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
Definition thumbtable.c:639
void dt_thumbtable_stop(dt_thumbtable_t *table)
void dt_thumbtable_set_focus_regions(dt_thumbtable_t *table, gboolean enable)
Definition thumbtable.c:954
static void dt_thumbtable_scroll_to_rowid(dt_thumbtable_t *table, int rowid)
Definition thumbtable.c:397
static void _collection_lut_row(void *user_data, const dt_image_t *row)
void _update_grid_area(dt_thumbtable_t *table)
Definition thumbtable.c:499
int dt_thumbtable_scroll_to_active_rowid(dt_thumbtable_t *table)
Scroll to show the active row.
Definition thumbtable.c:448
gboolean dt_thumbtable_get_draw_group_borders(dt_thumbtable_t *table)
Definition thumbtable.c:984
int dt_thumbtable_thumb_cell_decoration(void)
Definition thumbtable.c:536
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:86
void _alternative_mode(dt_thumbtable_t *table, gboolean enable)
gboolean dt_thumbtable_key_pressed_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
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:353
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)
Definition thumbtable.c:882
static gboolean _get_row_ids(dt_thumbtable_t *table, int *rowid_min, int *rowid_max)
Definition thumbtable.c:469
void dt_thumbtable_refresh_thumbnail_real(dt_thumbtable_t *table, int32_t imgid, gboolean reinit)
Definition thumbtable.c:992
void dt_thumbtable_select_all(dt_thumbtable_t *table)
Select all images in the current grid.
void dt_thumbtable_schedule_focus_real(dt_thumbtable_t *table, const gint priority)
Definition thumbtable.c:204
static void _dt_selection_changed_callback(gpointer instance, gpointer user_data)
Definition thumbtable.c:939
void dt_thumbtable_reset_collection(dt_thumbtable_t *table)
gboolean dt_thumbtable_key_released_grid(GtkWidget *self, GdkEventKey *event, gpointer user_data)
int dt_thumbtable_find_rowid_from_imgid(dt_thumbtable_t *table, const int32_t imgid)
Definition thumbtable.c:414
static void _evict_thumbnails(dt_thumbtable_t *table, const int keep_min, const int keep_max)
Definition thumbtable.c:792
static void _event_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
static gboolean _set_thumb_position(dt_thumbtable_t *table, dt_thumbnail_t *thumb)
Definition thumbtable.c:368
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_move_in_grid(dt_thumbtable_t *table, GdkEventKey *event, dt_thumbtable_direction_t direction, int origin_imgid)
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:231
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)
Definition thumbtable.c:976
static void _dnd_folder_import_job_cleanup(void *p)
void _mouse_over_image_callback(gpointer instance, gpointer user_data)
Definition thumbtable.c:286
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 _dnd_recurse_folder(const char *folder_path, dt_import_filter_type_t filter_type, GList **files, guint *scan_errors)
static void _thumbtable_dnd_scan_folders_and_import(GList *folders, GList *files)
GList * _thumbtable_dnd_import_check(GList *files, const char *pathname, int *elements, GList **folders)
static int32_t _dnd_folder_import_job_run(dt_job_t *job)
static void _refresh_highlights(dt_thumbtable_t *table)
Definition thumbtable.c:894
dt_thumbnail_t * _find_thumb_by_imgid(dt_thumbtable_t *table, const int32_t imgid)
Definition thumbtable.c:601
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:64
@ DT_THUMBTABLE_MODE_FILMSTRIP
Definition thumbtable.h:67
@ DT_THUMBTABLE_MODE_FILEMANAGER
Definition thumbtable.h:66
#define dt_thumbtable_refresh_thumbnail(table, imgid, reinit)
Definition thumbtable.h:301
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)
static gboolean dt_thumbtable_info_is_grouped(const dt_image_t info)
Private interface shared between the thumbtable engine (thumbtable.c) and its two frontends (filemana...
#define dt_thumbtable_schedule_focus(table, priority)
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 double dt_get_wtime(void)
Definition times.h:43
char * dt_get_help_url(char *name)
gchar * dt_util_glist_to_str(const gchar *separator, GList *items)
Definition utility.c:193
void dt_view_image_info_update(int32_t imgid)
Definition view.c:1544
static gboolean dt_modifier_is(GdkModifierType state, const GdkModifierType desired_modifier_mask)
void dt_gui_remove_class(GtkWidget *widget, const gchar *class_name)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)