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