Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
libs/tools/lighttable.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2011, 2013 Henrik Andersson.
4 Copyright (C) 2012 Petr Styblo.
5 Copyright (C) 2012 Richard Wonka.
6 Copyright (C) 2012-2014, 2016-2017 Tobias Ellinghaus.
7 Copyright (C) 2013 johannes hanika.
8 Copyright (C) 2013 Pascal de Bruijn.
9 Copyright (C) 2013-2014, 2016 Roman Lebedev.
10 Copyright (C) 2014, 2019-2022 Aldric Renaudin.
11 Copyright (C) 2018 Mario Lueder.
12 Copyright (C) 2018 Rikard Öxler.
13 Copyright (C) 2019, 2022-2023, 2025 Aurélien PIERRE.
14 Copyright (C) 2019, 2021 Bill Ferguson.
15 Copyright (C) 2019 Edgardo Hoszowski.
16 Copyright (C) 2019-2021 Pascal Obry.
17 Copyright (C) 2020-2021 Chris Elston.
18 Copyright (C) 2021-2022 Diederik Ter Rahe.
19 Copyright (C) 2021 Ralf Brown.
20 Copyright (C) 2022 Martin Bařinka.
21 Copyright (C) 2022 Nicolas Auffray.
22
23 darktable is free software: you can redistribute it and/or modify
24 it under the terms of the GNU General Public License as published by
25 the Free Software Foundation, either version 3 of the License, or
26 (at your option) any later version.
27
28 darktable is distributed in the hope that it will be useful,
29 but WITHOUT ANY WARRANTY; without even the implied warranty of
30 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 GNU General Public License for more details.
32
33 You should have received a copy of the GNU General Public License
34 along with darktable. If not, see <http://www.gnu.org/licenses/>.
35*/
36
37#include "system/macros.h"
38#include "system/mem_alloc.h"
40#include "control/signal.h"
41#include <gdk/gdkkeysyms.h>
42
43#include "common/collection.h"
44#include "common/conf.h"
46#include "gui/actions/menu.h"
47
48#include "gui/application.h"
49#include "libs/lib.h"
50#include "libs/lib_api.h"
54
55DT_MODULE(1)
56
63
64/* set columns proxy function */
65static void _lib_lighttable_set_columns(dt_lib_module_t *self, gint columns);
66
67/* columns slider change callback */
68static void _lib_lighttable_columns_slider_changed(GtkWidget *widget, gpointer user_data);
69
70static void _set_columns(dt_lib_module_t *self, int columns);
71
72const char *name(struct dt_lib_module_t *self)
73{
74 return _("lighttable");
75}
76
77const char **views(dt_lib_module_t *self)
78{
79 static const char *v[] = {"lighttable", NULL};
80 return v;
81}
82
87
89{
90 return 0;
91}
92
94{
95 return 1001;
96}
97
98gboolean _columns_in_action(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
99 GdkModifierType modifier, gpointer data)
100{
101 dt_lib_module_t *self = (dt_lib_module_t *)data;
102 int current_level = dt_conf_get_int("plugins/lighttable/images_in_row");
103 int new_level = CLAMP(current_level - 1, 1, 12);
104 _lib_lighttable_set_columns(self, new_level);
105 dt_conf_set_int("plugins/lighttable/images_in_row_backup", new_level);
106 return TRUE;
107}
108
109gboolean _columns_out_action(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
110 GdkModifierType modifier, gpointer data)
111{
112 dt_lib_module_t *self = (dt_lib_module_t *)data;
113 int current_level = dt_conf_get_int("plugins/lighttable/images_in_row");
114 int new_level = CLAMP(current_level + 1, 1, 12);
115 _lib_lighttable_set_columns(self, new_level);
116 dt_conf_set_int("plugins/lighttable/images_in_row_backup", new_level);
117 return TRUE;
118}
119
120static void _dt_collection_changed_callback(gpointer instance, dt_collection_change_t query_change,
121 dt_collection_properties_t changed_property, gpointer imgs,
122 const int next, gpointer user_data)
123{
124 if(IS_NULL_PTR(user_data)) return;
125 // DT_COLLECTION_CHANGE_BACKGROUND_SYNC means the signal comes from a passive background sync
126 // (e.g. dt_collection_notify_imported(), called once per imported image for the whole duration
127 // of an import job) rather than the user navigating: nothing below -- column/zoom adjustments
128 // driven by the CURRENT collection's content -- is meaningful to redo on every one of those.
129 // Reacting anyway unconditionally reset thumbnail zoom (full thumbnail reload +
130 // scroll-to-selection) on every such notification, fighting whatever the user was doing in the
131 // grid for the entire duration of the import, whether or not it even touched what was on
132 // screen.
133 if(query_change == DT_COLLECTION_CHANGE_BACKGROUND_SYNC) return;
134 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
135
137 {
138 int current_level = dt_conf_get_int("plugins/lighttable/images_in_row");
140
141 switch(num_images)
142 {
143 case 1:
144 case 2:
145 case 3:
146 case 4:
147 case 5:
148 _lib_lighttable_set_columns(self, num_images);
149 dt_conf_set_int("plugins/lighttable/images_in_row_backup", current_level);
150 break;
151 case 6:
153 dt_conf_set_int("plugins/lighttable/images_in_row_backup", current_level);
154 break;
155 case 7:
156 case 8:
158 dt_conf_set_int("plugins/lighttable/images_in_row_backup", current_level);
159 break;
160 case 9:
161 case 10:
162 case 11:
163 case 12:
164 case 13:
165 case 14:
166 case 15:
168 dt_conf_set_int("plugins/lighttable/images_in_row_backup", current_level);
169 break;
170 default:
171 if(dt_conf_key_exists("plugins/lighttable/images_in_row_backup"))
172 _lib_lighttable_set_columns(self, dt_conf_get_int("plugins/lighttable/images_in_row_backup"));
173 }
174 }
175 else if(dt_conf_key_exists("plugins/lighttable/images_in_row_backup"))
176 {
177 _lib_lighttable_set_columns(self, dt_conf_get_int("plugins/lighttable/images_in_row_backup"));
178 }
179
180 // Reset zoom
181 dt_thumbtable_set_zoom(dt_gui_get_ui()->thumbtable_lighttable, 0);
182}
183
184static gboolean _zoom_combobox_changed(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
185{
186 const int level = GPOINTER_TO_INT(get_custom_data(GTK_WIDGET(user_data)));
187 dt_thumbtable_set_zoom(dt_gui_get_ui()->thumbtable_lighttable, level);
188 return TRUE;
189}
190
191static gboolean _zoom_checked(GtkWidget *widget)
192{
193 const int level = GPOINTER_TO_INT(get_custom_data(widget));
194 return dt_thumbtable_get_zoom(dt_gui_get_ui()->thumbtable_lighttable) == level;
195}
196
197
198// Ctrl + Scroll changes the number of columns
199static gboolean _thumbtable_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer data)
200{
201 dt_lib_module_t *self = (dt_lib_module_t *)data;
202
203 if(dt_modifier_is(event->state, DT_PRIMARY_MASK))
204 {
205 int scroll_y;
206 dt_gui_get_scroll_unit_deltas(event, NULL, &scroll_y);
207
208 int current_level = dt_conf_get_int("plugins/lighttable/images_in_row");
209 int new_level = CLAMP(current_level + CLAMP(scroll_y, -1, 1), 1, 12);
210
211 _lib_lighttable_set_columns(self, new_level);
212 dt_conf_set_int("plugins/lighttable/images_in_row_backup", new_level);
213 return TRUE;
214 }
215 return FALSE;
216}
217
218static gboolean _focus_toggle_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
219{
221 gboolean state = dt_thumbtable_get_focus_regions(table);
223 return TRUE;
224}
225
231
232static gboolean focus_peaking_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
233{
235 gboolean focus_peaking = dt_thumbtable_get_focus_peaking(table);
236 dt_thumbtable_set_focus_peaking(table, !focus_peaking);
237 return TRUE;
238}
239
245
246void append_thumbnails(GtkWidget **menus, GList **lists, const dt_menus_t index, GtkAccelGroup *accel_group)
247{
248 // Focusing options
249 add_generic_sub_menu_entry(menus, lists, _("Overlay focus zones"), index, NULL, _focus_toggle_callback, _focus_checked, NULL,
250 NULL, 0, 0, accel_group);
251
252 add_generic_sub_menu_entry(menus, lists, _("Overlay focus peaking"), index, NULL, focus_peaking_callback,
253 focus_peaking_checked_callback, NULL, NULL, GDK_KEY_p,
254 DT_PRIMARY_MASK | GDK_SHIFT_MASK, accel_group);
255
256 // Zoom
257 add_generic_top_submenu_entry(menus, lists, _("Zoom"), index, accel_group);
258 GtkWidget *parent = get_last_widget(lists);
259 add_generic_sub_sub_menu_entry(menus, parent, lists, _("Fit"), index, GINT_TO_POINTER(0), _zoom_combobox_changed,
260 _zoom_checked, NULL, NULL, 0, 0, accel_group);
261 add_generic_sub_sub_menu_entry(menus, parent, lists, _("50 %"), index, GINT_TO_POINTER(1), _zoom_combobox_changed,
262 _zoom_checked, NULL, NULL, 0, 0, accel_group);
263 add_generic_sub_sub_menu_entry(menus, parent, lists, _("100 %"), index, GINT_TO_POINTER(2), _zoom_combobox_changed,
264 _zoom_checked, NULL, NULL, 0, 0, accel_group);
265 add_generic_sub_sub_menu_entry(menus, parent, lists, _("200 %"), index, GINT_TO_POINTER(3), _zoom_combobox_changed,
266 _zoom_checked, NULL, NULL, 0, 0, accel_group);
267}
268
270{
271 /* initialize ui widgets */
273 self->data = (void *)d;
274
275 self->widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
276 dt_gui_add_class(self->widget, "lighttable_box");
277 gtk_widget_set_halign(self->widget, GTK_ALIGN_END);
278 gtk_widget_set_hexpand(self->widget, FALSE);
279
280 // Thumbnail menu
281 GtkAccelGroup *accel_group = dt_gui_get_accels()->lighttable_accels;
282 GtkWidget *menu_bar = gtk_menu_bar_new();
283 GtkWidget *menus[1];
284 const int index = 0;
285 d->menu_items = NULL;
286 add_generic_top_menu_entry(menu_bar, menus, &d->menu_items, index, _("_Thumbnails"), accel_group, "Lighttable");
287 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(menu_bar), FALSE, FALSE, 0);
288 append_thumbnails(menus, &d->menu_items, index, accel_group);
289
290 // dumb empty flexible spacer at the end
291 GtkWidget *spacer = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
292 gtk_box_pack_start(GTK_BOX(self->widget), spacer, TRUE, TRUE, 0);
293
294 GtkWidget *label = gtk_label_new(C_("quickfilter", "Columns"));
295 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(label), FALSE, FALSE, 0);
296
297 d->columns = gtk_spin_button_new_with_range(1., 12., 1.);
298 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->columns), FALSE, FALSE, 0);
299 gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->columns), dt_conf_get_int("plugins/lighttable/images_in_row"));
301
302 g_signal_connect(G_OBJECT(d->columns), "value-changed", G_CALLBACK(_lib_lighttable_columns_slider_changed), self);
303
304 dt_accels_new_lighttable_action(_columns_in_action, self, NULL, N_("Lighttable/Actions"), N_("Zoom in the thumbtable grid"),
305 GDK_KEY_plus, DT_PRIMARY_MASK, _("Triggers the action"));
306 dt_accels_new_lighttable_action(_columns_out_action, self, NULL, N_("Lighttable/Actions"), N_("Zoom out the thumbtable grid"),
307 GDK_KEY_minus, DT_PRIMARY_MASK, _("Triggers the action"));
308
310 G_CALLBACK(_dt_collection_changed_callback), self);
311
312 _lib_lighttable_columns_slider_changed(d->columns, self); // the slider defaults to 1 and GTK doesn't
313 // fire a value-changed signal when setting
314 // it to 1 => empty text box
315
316 // Wire a scroll event handler on thumbtable here. This avoids us a proxy
318 d->scroll_handler_id
319 = g_signal_connect(G_OBJECT(table->scroll_window), "scroll-event", G_CALLBACK(_thumbtable_scroll), self);
320}
321
323{
324 if(IS_NULL_PTR(self->data)) return;
326
328
329 if(d->scroll_handler_id > 0)
330 {
332 if(!IS_NULL_PTR(table) && !IS_NULL_PTR(table->scroll_window))
333 g_signal_handler_disconnect(G_OBJECT(table->scroll_window), d->scroll_handler_id);
334 d->scroll_handler_id = 0;
335 }
336
337 GList *menu_widgets = NULL;
338 for(GList *iter = d->menu_items; iter; iter = g_list_next(iter))
339 {
340 dt_menu_entry_t *entry = (dt_menu_entry_t *)iter->data;
341 if(IS_NULL_PTR(entry) || IS_NULL_PTR(entry->widget) || !GTK_IS_WIDGET(entry->widget)) continue;
342 menu_widgets = g_list_prepend(menu_widgets, g_object_ref(entry->widget));
343 }
344
345 for(GList *iter = menu_widgets; iter; iter = g_list_next(iter))
346 {
347 GtkWidget *widget = GTK_WIDGET(iter->data);
348 if(GTK_IS_WIDGET(widget)) gtk_widget_destroy(widget);
349 g_object_unref(widget);
350 }
351 g_list_free(menu_widgets);
352
353 g_list_free(d->menu_items);
354 d->menu_items = NULL;
355
356 dt_free(self->data);
357}
358
359static void _set_columns(dt_lib_module_t *self, int columns)
360{
361 dt_conf_set_int("plugins/lighttable/images_in_row", columns);
362
363 // Use the coordinated grid configuration function that properly orders:
364 // 1. Grid reconfiguration with new column count
365 // 2. Thumbnail updates and resizing
366 // 3. Scrolling to active selection
367 // This prevents partial updates and ensures synchronization.
369}
370
371static void _lib_lighttable_columns_slider_changed(GtkWidget *widget, gpointer user_data)
372{
373 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
375 const int cols = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(d->columns));
376 _set_columns(self, cols);
377 dt_conf_set_int("plugins/lighttable/images_in_row_backup", cols);
378}
379
380static void _lib_lighttable_set_columns(dt_lib_module_t *self, gint columns)
381{
383 gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->columns), columns);
384 _set_columns(self, columns);
385}
386
387// clang-format off
388// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
389// vim: shiftwidth=2 expandtab tabstop=2 cindent
390// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
391// clang-format on
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnect accels while a text or search entry has the focus, and reconnect them when it loses it....
Handle default and user-set shortcuts (accelerators)
#define DT_PRIMARY_MASK
struct dt_accels_t * dt_gui_get_accels(void)
struct dt_ui_t * dt_gui_get_ui(void)
#define dt_accels_new_lighttable_action(a, b, w, c, d, e, f, g)
#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_change_t
Definition collection.h:160
@ DT_COLLECTION_CHANGE_BACKGROUND_SYNC
Definition collection.h:174
const float v
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
int dt_conf_key_exists(const char *key)
Does key have a value?
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.
struct dt_gui_gtk_t * dt_gui_get_global(void)
Definition darktable.c:523
void dt_thumbtable_set_zoom(dt_thumbtable_t *table, dt_thumbtable_zoom_t level)
void dt_thumbtable_apply_grid_configuration(dt_thumbtable_t *table)
Apply grid configuration changes with proper event synchronization.
dt_thumbtable_zoom_t dt_thumbtable_get_zoom(dt_thumbtable_t *table)
static void _lib_lighttable_columns_slider_changed(GtkWidget *widget, gpointer user_data)
static gboolean focus_peaking_checked_callback()
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)
void append_thumbnails(GtkWidget **menus, GList **lists, const dt_menus_t index, GtkAccelGroup *accel_group)
gboolean _columns_out_action(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval, GdkModifierType modifier, gpointer data)
static void _set_columns(dt_lib_module_t *self, int columns)
void gui_cleanup(dt_lib_module_t *self)
static gboolean focus_peaking_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
static gboolean _focus_toggle_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
gboolean _columns_in_action(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval, GdkModifierType modifier, gpointer data)
static gboolean _zoom_checked(GtkWidget *widget)
gboolean _focus_checked(GtkWidget *widget)
uint32_t container(dt_lib_module_t *self)
void gui_init(dt_lib_module_t *self)
int position()
static void _lib_lighttable_set_columns(dt_lib_module_t *self, gint columns)
const char ** views(dt_lib_module_t *self)
int expandable(dt_lib_module_t *self)
static gboolean _thumbtable_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer data)
static gboolean _zoom_combobox_changed(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
#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
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
void add_generic_sub_menu_entry(GtkWidget **menus, GList **lists, const gchar *label, const dt_menus_t index, void *data, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gboolean(*checked_callback)(GtkWidget *widget), gboolean(*active_callback)(GtkWidget *widget), gboolean(*sensitive_callback)(GtkWidget *widget), guint key_val, GdkModifierType mods, GtkAccelGroup *accel_group)
Definition menu.c:544
void add_generic_sub_sub_menu_entry(GtkWidget **menus, GtkWidget *parent, GList **lists, const gchar *label, const dt_menus_t index, void *data, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gboolean(*checked_callback)(GtkWidget *widget), gboolean(*active_callback)(GtkWidget *widget), gboolean(*sensitive_callback)(GtkWidget *widget), guint key_val, GdkModifierType mods, GtkAccelGroup *accel_group)
Definition menu.c:589
GtkWidget * get_last_widget(GList **list)
Definition menu.c:641
void * get_custom_data(GtkWidget *widget)
Definition menu.c:635
void add_generic_top_menu_entry(GtkWidget *menu_bar, GtkWidget **menus, GList **lists, const dt_menus_t index, gchar *label, GtkAccelGroup *accel_group, const char *accel_path_prefix)
Definition menu.c:483
void add_generic_top_submenu_entry(GtkWidget **menus, GList **lists, const gchar *label, const dt_menus_t index, GtkAccelGroup *accel_group)
Definition menu.c:516
dt_menus_t
Definition menu.h:42
#define DT_MODULE(MODVER)
Instantiate the version handshake every module must export. Use once, at file scope,...
const char * name
Definition pdf.h:90
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:407
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:616
@ 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
const float uint32_t state[4]
GtkAccelGroup * lighttable_accels
gboolean culling_mode
GModule *void * data
Definition lib.h:79
GtkWidget * widget
Definition lib.h:83
Definition menu.h:65
GtkWidget * widget
Definition menu.h:66
GtkWidget * scroll_window
Definition thumbtable.h:154
dt_thumbtable_t * thumbtable_lighttable
gboolean dt_thumbtable_get_focus_peaking(dt_thumbtable_t *table)
Definition thumbtable.c:914
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_set_focus_regions(dt_thumbtable_t *table, gboolean enable)
Definition thumbtable.c:897
A widget to manage and display image thumbnails in Ansel's lighttable and filmstrip views.
gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y)
static gboolean dt_modifier_is(GdkModifierType state, const GdkModifierType desired_modifier_mask)
#define DT_GUI_BOX_SPACING
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
@ DT_UI_CONTAINER_PANEL_TOP_SECOND_ROW