1 /*
2     This file is part of darktable,
3     Copyright (C) 2009-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "views/view.h"
20 #include "bauhaus/bauhaus.h"
21 #include "common/collection.h"
22 #include "common/darktable.h"
23 #include "common/debug.h"
24 #include "common/focus_peaking.h"
25 #include "common/image_cache.h"
26 #include "common/mipmap_cache.h"
27 #include "common/module.h"
28 #include "common/selection.h"
29 #include "common/undo.h"
30 #include "common/usermanual_url.h"
31 #include "control/conf.h"
32 #include "control/control.h"
33 #include "develop/develop.h"
34 #include "dtgtk/button.h"
35 #include "dtgtk/expander.h"
36 #include "dtgtk/thumbtable.h"
37 #include "gui/accelerators.h"
38 #include "gui/draw.h"
39 #include "gui/gtk.h"
40 #include "libs/lib.h"
41 #ifdef GDK_WINDOWING_QUARTZ
42 #include "osx/osx.h"
43 #endif
44 
45 #include <glib.h>
46 #include <math.h>
47 #include <stdio.h>
48 #include <stdlib.h>
49 #include <string.h>
50 #include <strings.h>
51 
52 #define DECORATION_SIZE_LIMIT 40
53 
54 static void dt_view_manager_load_modules(dt_view_manager_t *vm);
55 static int dt_view_load_module(void *v, const char *libname, const char *module_name);
56 static void dt_view_unload_module(dt_view_t *view);
57 
dt_view_manager_init(dt_view_manager_t * vm)58 void dt_view_manager_init(dt_view_manager_t *vm)
59 {
60   /* prepare statements */
61   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT imgid FROM main.selected_images "
62                               "WHERE imgid = ?1", -1, &vm->statements.is_selected, NULL);
63   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "DELETE FROM main.selected_images WHERE imgid = ?1",
64                               -1, &vm->statements.delete_from_selected, NULL);
65   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
66                               "INSERT OR IGNORE INTO main.selected_images VALUES (?1)", -1,
67                               &vm->statements.make_selected, NULL);
68   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT num FROM main.history WHERE imgid = ?1", -1,
69                               &vm->statements.have_history, NULL);
70   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT color FROM main.color_labels WHERE imgid=?1",
71                               -1, &vm->statements.get_color, NULL);
72   DT_DEBUG_SQLITE3_PREPARE_V2(
73       dt_database_get(darktable.db),
74       "SELECT id FROM main.images WHERE group_id = (SELECT group_id FROM main.images WHERE id=?1) AND id != ?2",
75       -1, &vm->statements.get_grouped, NULL);
76 
77   dt_view_manager_load_modules(vm);
78 
79   // Modules loaded, let's handle specific cases
80   for(GList *iter = vm->views; iter; iter = g_list_next(iter))
81   {
82     dt_view_t *view = (dt_view_t *)iter->data;
83     if(!strcmp(view->module_name, "darkroom"))
84     {
85       darktable.develop = (dt_develop_t *)view->data;
86       break;
87     }
88   }
89 
90   vm->current_view = NULL;
91   vm->audio.audio_player_id = -1;
92 }
93 
dt_view_manager_gui_init(dt_view_manager_t * vm)94 void dt_view_manager_gui_init(dt_view_manager_t *vm)
95 {
96   for(GList *iter = vm->views; iter; iter = g_list_next(iter))
97   {
98     dt_view_t *view = (dt_view_t *)iter->data;
99     if(view->gui_init) view->gui_init(view);
100   }
101 }
102 
dt_view_manager_cleanup(dt_view_manager_t * vm)103 void dt_view_manager_cleanup(dt_view_manager_t *vm)
104 {
105   for(GList *iter = vm->views; iter; iter = g_list_next(iter)) dt_view_unload_module((dt_view_t *)iter->data);
106   g_list_free_full(vm->views, free);
107   vm->views = NULL;
108 }
109 
dt_view_manager_get_current_view(dt_view_manager_t * vm)110 const dt_view_t *dt_view_manager_get_current_view(dt_view_manager_t *vm)
111 {
112   return vm->current_view;
113 }
114 
115 // we want a stable order of views, for example for viewswitcher.
116 // anything not hardcoded will be put alphabetically wrt. localised names.
sort_views(gconstpointer a,gconstpointer b)117 static gint sort_views(gconstpointer a, gconstpointer b)
118 {
119   static const char *view_order[] = {"lighttable", "darkroom"};
120   static const int n_view_order = G_N_ELEMENTS(view_order);
121 
122   dt_view_t *av = (dt_view_t *)a;
123   dt_view_t *bv = (dt_view_t *)b;
124   const char *aname = av->name(av);
125   const char *bname = bv->name(bv);
126   int apos = n_view_order;
127   int bpos = n_view_order;
128 
129   for(int i = 0; i < n_view_order; i++)
130   {
131     if(!strcmp(av->module_name, view_order[i])) apos = i;
132     if(!strcmp(bv->module_name, view_order[i])) bpos = i;
133   }
134 
135   // order will be zero iff apos == bpos which can only happen when both views are not in view_order
136   const int order = apos - bpos;
137   return order ? order : strcmp(aname, bname);
138 }
139 
dt_view_manager_load_modules(dt_view_manager_t * vm)140 static void dt_view_manager_load_modules(dt_view_manager_t *vm)
141 {
142   vm->views = dt_module_load_modules("/views", sizeof(dt_view_t), dt_view_load_module, NULL, sort_views);
143 }
144 
145 /* default flags for view which does not implement the flags() function */
default_flags()146 static uint32_t default_flags()
147 {
148   return 0;
149 }
150 
151 /** load a view module */
dt_view_load_module(void * v,const char * libname,const char * module_name)152 static int dt_view_load_module(void *v, const char *libname, const char *module_name)
153 {
154   dt_view_t *module = (dt_view_t *)v;
155   g_strlcpy(module->module_name, module_name, sizeof(module->module_name));
156 
157 #define INCLUDE_API_FROM_MODULE_LOAD "view_load_module"
158 #include "views/view_api.h"
159 
160   module->data = NULL;
161   module->vscroll_size = module->vscroll_viewport_size = 1.0;
162   module->hscroll_size = module->hscroll_viewport_size = 1.0;
163   module->vscroll_pos = module->hscroll_pos = 0.0;
164   module->height = module->width = 100; // set to non-insane defaults before first expose/configure.
165   module->accel_closures = NULL;
166 
167   if(!strcmp(module->module_name, "darkroom")) darktable.develop = (dt_develop_t *)module->data;
168 
169 #ifdef USE_LUA
170   dt_lua_register_view(darktable.lua_state.state, module);
171 #endif
172 
173   if(module->init) module->init(module);
174   if(darktable.gui && module->init_key_accels) module->init_key_accels(module);
175 
176   return 0;
177 }
178 
179 /** unload, cleanup */
dt_view_unload_module(dt_view_t * view)180 static void dt_view_unload_module(dt_view_t *view)
181 {
182   if(view->cleanup) view->cleanup(view);
183 
184   g_slist_free(view->accel_closures);
185 
186   if(view->module) g_module_close(view->module);
187 }
188 
dt_vm_remove_child(GtkWidget * widget,gpointer data)189 void dt_vm_remove_child(GtkWidget *widget, gpointer data)
190 {
191   gtk_container_remove(GTK_CONTAINER(data), widget);
192 }
193 
194 /*
195    When expanders get destroyed, they destroy the child
196    so remove the child before that
197    */
_remove_child(GtkWidget * child,GtkContainer * container)198 static void _remove_child(GtkWidget *child,GtkContainer *container)
199 {
200     if(DTGTK_IS_EXPANDER(child))
201     {
202       GtkWidget * evb = dtgtk_expander_get_body_event_box(DTGTK_EXPANDER(child));
203       gtk_container_remove(GTK_CONTAINER(evb),dtgtk_expander_get_body(DTGTK_EXPANDER(child)));
204       gtk_widget_destroy(child);
205     }
206     else
207     {
208       gtk_container_remove(container,child);
209     }
210 }
211 
dt_view_manager_switch(dt_view_manager_t * vm,const char * view_name)212 int dt_view_manager_switch(dt_view_manager_t *vm, const char *view_name)
213 {
214   gboolean switching_to_none = *view_name == '\0';
215   dt_view_t *new_view = NULL;
216 
217   if(!switching_to_none)
218   {
219     for(GList *iter = vm->views; iter; iter = g_list_next(iter))
220     {
221       dt_view_t *v = (dt_view_t *)iter->data;
222       if(!strcmp(v->module_name, view_name))
223       {
224         new_view = v;
225         break;
226       }
227     }
228     if(!new_view) return 1; // the requested view doesn't exist
229   }
230 
231   return dt_view_manager_switch_by_view(vm, new_view);
232 }
233 
dt_view_manager_switch_by_view(dt_view_manager_t * vm,const dt_view_t * nv)234 int dt_view_manager_switch_by_view(dt_view_manager_t *vm, const dt_view_t *nv)
235 {
236   dt_view_t *old_view = vm->current_view;
237   dt_view_t *new_view = (dt_view_t *)nv; // views belong to us, we can de-const them :-)
238 
239   // Before switching views, restore accelerators if disabled
240   if(!darktable.control->key_accelerators_on) dt_control_key_accelerators_on(darktable.control);
241 
242   // reset the cursor to the default one
243   dt_control_change_cursor(GDK_LEFT_PTR);
244 
245   // also ignore what scrolling there was previously happening
246   memset(darktable.gui->scroll_to, 0, sizeof(darktable.gui->scroll_to));
247 
248   // destroy old module list
249 
250   /*  clear the undo list, for now we do this unconditionally. At some point we will probably want to clear
251      only part
252       of the undo list. This should probably done with a view proxy routine returning the type of undo to
253      remove. */
254   dt_undo_clear(darktable.undo, DT_UNDO_ALL);
255 
256   /* Special case when entering nothing (just before leaving dt) */
257   if(!new_view)
258   {
259     if(old_view)
260     {
261       /* leave the current view*/
262       if(old_view->leave) old_view->leave(old_view);
263 
264       /* iterator plugins and cleanup plugins in current view */
265       for(GList *iter = darktable.lib->plugins; iter; iter = g_list_next(iter))
266       {
267         dt_lib_module_t *plugin = (dt_lib_module_t *)(iter->data);
268 
269         /* does this module belong to current view ?*/
270         if(dt_lib_is_visible_in_view(plugin, old_view))
271         {
272           if(plugin->view_leave) plugin->view_leave(plugin, old_view, NULL);
273           plugin->gui_cleanup(plugin);
274           plugin->data = NULL;
275           dt_accel_disconnect_list(&plugin->accel_closures);
276           plugin->widget = NULL;
277         }
278       }
279     }
280 
281     /* remove all widgets in all containers */
282     for(int l = 0; l < DT_UI_CONTAINER_SIZE; l++)
283       dt_ui_container_destroy_children(darktable.gui->ui, l);
284     vm->current_view = NULL;
285 
286     /* remove sticky accels window */
287     if(vm->accels_window.window) dt_view_accels_hide(vm);
288     return 0;
289   }
290 
291   // invariant: new_view != NULL after this point
292   assert(new_view != NULL);
293 
294   if(new_view->try_enter)
295   {
296     int error = new_view->try_enter(new_view);
297     if(error) return error;
298   }
299 
300   /* cleanup current view before initialization of new  */
301   if(old_view)
302   {
303     /* leave current view */
304     if(old_view->leave) old_view->leave(old_view);
305     dt_accel_disconnect_list(&old_view->accel_closures);
306 
307     /* iterator plugins and cleanup plugins in current view */
308     for(GList *iter = darktable.lib->plugins; iter; iter = g_list_next(iter))
309     {
310       dt_lib_module_t *plugin = (dt_lib_module_t *)(iter->data);
311 
312       /* does this module belong to current view ?*/
313       if(dt_lib_is_visible_in_view(plugin, old_view))
314       {
315         if(plugin->view_leave) plugin->view_leave(plugin, old_view, new_view);
316         dt_accel_disconnect_list(&plugin->accel_closures);
317       }
318     }
319 
320     /* remove all widets in all containers */
321     for(int l = 0; l < DT_UI_CONTAINER_SIZE; l++)
322       dt_ui_container_foreach(darktable.gui->ui, l,(GtkCallback)_remove_child);
323   }
324 
325   /* change current view to the new view */
326   vm->current_view = new_view;
327 
328   /* update thumbtable accels */
329   dt_thumbtable_update_accels_connection(dt_ui_thumbtable(darktable.gui->ui), new_view->view(new_view));
330 
331   /* restore visible stat of panels for the new view */
332   dt_ui_restore_panels(darktable.gui->ui);
333 
334   /* lets add plugins related to new view into panels.
335    * this has to be done in reverse order to have the lowest position at the bottom! */
336   for(GList *iter = g_list_last(darktable.lib->plugins); iter; iter = g_list_previous(iter))
337   {
338     dt_lib_module_t *plugin = (dt_lib_module_t *)(iter->data);
339     if(dt_lib_is_visible_in_view(plugin, new_view))
340     {
341 
342       /* try get the module expander  */
343       GtkWidget *w = dt_lib_gui_get_expander(plugin);
344 
345       if(plugin->connect_key_accels) plugin->connect_key_accels(plugin);
346       dt_lib_connect_common_accels(plugin);
347 
348       /* if we didn't get an expander let's add the widget */
349       if(!w) w = plugin->widget;
350 
351       dt_gui_add_help_link(w, dt_get_help_url(plugin->plugin_name));
352       // some plugins help links depend on the view
353       if(!strcmp(plugin->plugin_name,"module_toolbox")
354         || !strcmp(plugin->plugin_name,"view_toolbox"))
355       {
356         dt_view_type_flags_t view_type = new_view->view(new_view);
357         if(view_type == DT_VIEW_LIGHTTABLE)
358           dt_gui_add_help_link(w, dt_get_help_url("lighttable_mode"));
359         if(view_type == DT_VIEW_DARKROOM)
360           dt_gui_add_help_link(w, dt_get_help_url("darkroom_bottom_panel"));
361       }
362 
363 
364       /* add module to its container */
365       dt_ui_container_add_widget(darktable.gui->ui, plugin->container(plugin), w);
366     }
367   }
368 
369   /* hide/show modules as last config */
370   for(GList *iter = darktable.lib->plugins; iter; iter = g_list_next(iter))
371   {
372     dt_lib_module_t *plugin = (dt_lib_module_t *)(iter->data);
373     if(dt_lib_is_visible_in_view(plugin, new_view))
374     {
375       /* set expanded if last mode was that */
376       char var[1024];
377       gboolean expanded = FALSE;
378       gboolean visible = dt_lib_is_visible(plugin);
379       if(plugin->expandable(plugin))
380       {
381         snprintf(var, sizeof(var), "plugins/%s/%s/expanded", new_view->module_name, plugin->plugin_name);
382         expanded = dt_conf_get_bool(var);
383         dt_lib_gui_set_expanded(plugin, expanded);
384         dt_lib_set_visible(plugin, visible);
385       }
386       else
387       {
388         /* show/hide plugin widget depending on expanded flag or if plugin
389             not is expandeable() */
390         if(visible)
391           gtk_widget_show_all(plugin->widget);
392         else
393           gtk_widget_hide(plugin->widget);
394       }
395       if(plugin->view_enter) plugin->view_enter(plugin, old_view, new_view);
396     }
397   }
398 
399   /* enter view. crucially, do this before initing the plugins below,
400       as e.g. modulegroups requires the dr stuff to be inited. */
401   if(new_view->enter) new_view->enter(new_view);
402   if(new_view->connect_key_accels) new_view->connect_key_accels(new_view);
403 
404   /* update the scrollbars */
405   dt_ui_update_scrollbars(darktable.gui->ui);
406 
407   /* update sticky accels window */
408   if(vm->accels_window.window && vm->accels_window.sticky) dt_view_accels_refresh(vm);
409 
410   /* raise view changed signal */
411   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_VIEWMANAGER_VIEW_CHANGED, old_view, new_view);
412 
413   // update log visibility
414   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_LOG_REDRAW);
415 
416   // update toast visibility
417   DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_TOAST_REDRAW);
418   return 0;
419 }
420 
dt_view_manager_name(dt_view_manager_t * vm)421 const char *dt_view_manager_name(dt_view_manager_t *vm)
422 {
423   if(!vm->current_view) return "";
424   if(vm->current_view->name)
425     return vm->current_view->name(vm->current_view);
426   else
427     return vm->current_view->module_name;
428 }
429 
dt_view_manager_expose(dt_view_manager_t * vm,cairo_t * cr,int32_t width,int32_t height,int32_t pointerx,int32_t pointery)430 void dt_view_manager_expose(dt_view_manager_t *vm, cairo_t *cr, int32_t width, int32_t height,
431                             int32_t pointerx, int32_t pointery)
432 {
433   if(!vm->current_view)
434   {
435     dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_BG);
436     cairo_paint(cr);
437     return;
438   }
439   vm->current_view->width = width;
440   vm->current_view->height = height;
441 
442   if(vm->current_view->expose)
443   {
444     /* expose the view */
445     cairo_rectangle(cr, 0, 0, vm->current_view->width, vm->current_view->height);
446     cairo_clip(cr);
447     cairo_new_path(cr);
448     cairo_save(cr);
449     float px = pointerx, py = pointery;
450     if(pointery > vm->current_view->height)
451     {
452       px = 10000.0;
453       py = -1.0;
454     }
455     vm->current_view->expose(vm->current_view, cr, vm->current_view->width, vm->current_view->height, px, py);
456 
457     cairo_restore(cr);
458     /* expose plugins */
459     for(const GList *plugins = g_list_last(darktable.lib->plugins); plugins; plugins = g_list_previous(plugins))
460     {
461       dt_lib_module_t *plugin = (dt_lib_module_t *)(plugins->data);
462 
463       /* does this module belong to current view ?*/
464       if(plugin->gui_post_expose
465          && dt_lib_is_visible_in_view(plugin, vm->current_view))
466         plugin->gui_post_expose(plugin, cr, vm->current_view->width, vm->current_view->height, px, py);
467     }
468   }
469 }
470 
dt_view_manager_reset(dt_view_manager_t * vm)471 void dt_view_manager_reset(dt_view_manager_t *vm)
472 {
473   if(!vm->current_view) return;
474   if(vm->current_view->reset) vm->current_view->reset(vm->current_view);
475 }
476 
dt_view_manager_mouse_leave(dt_view_manager_t * vm)477 void dt_view_manager_mouse_leave(dt_view_manager_t *vm)
478 {
479   if(!vm->current_view) return;
480   dt_view_t *v = vm->current_view;
481 
482   /* lets check if any plugins want to handle mouse move */
483   gboolean handled = FALSE;
484   for(const GList *plugins = g_list_last(darktable.lib->plugins);
485       plugins;
486       plugins = g_list_previous(plugins))
487   {
488     dt_lib_module_t *plugin = (dt_lib_module_t *)(plugins->data);
489 
490     /* does this module belong to current view ?*/
491     if(plugin->mouse_leave && dt_lib_is_visible_in_view(plugin, v))
492       if(plugin->mouse_leave(plugin)) handled = TRUE;
493   }
494 
495   /* if not handled by any plugin let pass to view handler*/
496   if(!handled && v->mouse_leave) v->mouse_leave(v);
497 }
498 
dt_view_manager_mouse_enter(dt_view_manager_t * vm)499 void dt_view_manager_mouse_enter(dt_view_manager_t *vm)
500 {
501   if(!vm->current_view) return;
502   if(vm->current_view->mouse_enter) vm->current_view->mouse_enter(vm->current_view);
503 }
504 
dt_view_manager_mouse_moved(dt_view_manager_t * vm,double x,double y,double pressure,int which)505 void dt_view_manager_mouse_moved(dt_view_manager_t *vm, double x, double y, double pressure, int which)
506 {
507   if(!vm->current_view) return;
508   dt_view_t *v = vm->current_view;
509 
510   /* lets check if any plugins want to handle mouse move */
511   gboolean handled = FALSE;
512   for(const GList *plugins = g_list_last(darktable.lib->plugins);
513       plugins;
514       plugins = g_list_previous(plugins))
515   {
516     dt_lib_module_t *plugin = (dt_lib_module_t *)(plugins->data);
517 
518     /* does this module belong to current view ?*/
519     if(plugin->mouse_moved && dt_lib_is_visible_in_view(plugin, v))
520       if(plugin->mouse_moved(plugin, x, y, pressure, which)) handled = TRUE;
521   }
522 
523   /* if not handled by any plugin let pass to view handler*/
524   if(!handled && v->mouse_moved) v->mouse_moved(v, x, y, pressure, which);
525 }
526 
dt_view_manager_button_released(dt_view_manager_t * vm,double x,double y,int which,uint32_t state)527 int dt_view_manager_button_released(dt_view_manager_t *vm, double x, double y, int which, uint32_t state)
528 {
529   if(!vm->current_view) return 0;
530   dt_view_t *v = vm->current_view;
531 
532   /* lets check if any plugins want to handle button press */
533   gboolean handled = FALSE;
534   for(const GList *plugins = g_list_last(darktable.lib->plugins);
535       plugins;
536       plugins = g_list_previous(plugins))
537   {
538     dt_lib_module_t *plugin = (dt_lib_module_t *)(plugins->data);
539 
540     /* does this module belong to current view ?*/
541     if(plugin->button_released && dt_lib_is_visible_in_view(plugin, v))
542       if(plugin->button_released(plugin, x, y, which, state)) handled = TRUE;
543   }
544 
545   if(handled)
546     return 1;
547   /* if not handled by any plugin let pass to view handler*/
548   else if(v->button_released)
549     v->button_released(v, x, y, which, state);
550 
551   return 0;
552 }
553 
dt_view_manager_button_pressed(dt_view_manager_t * vm,double x,double y,double pressure,int which,int type,uint32_t state)554 int dt_view_manager_button_pressed(dt_view_manager_t *vm, double x, double y, double pressure, int which,
555                                    int type, uint32_t state)
556 {
557   if(!vm->current_view) return 0;
558   dt_view_t *v = vm->current_view;
559 
560   /* lets check if any plugins want to handle button press */
561   gboolean handled = FALSE;
562   for(const GList *plugins = g_list_last(darktable.lib->plugins);
563       plugins && !handled;
564       plugins = g_list_previous(plugins))
565   {
566     dt_lib_module_t *plugin = (dt_lib_module_t *)(plugins->data);
567 
568     /* does this module belong to current view ?*/
569     if(plugin->button_pressed && dt_lib_is_visible_in_view(plugin, v))
570       if(plugin->button_pressed(plugin, x, y, pressure, which, type, state)) handled = TRUE;
571   }
572 
573   if(handled) return 1;
574   /* if not handled by any plugin let pass to view handler*/
575   else if(v->button_pressed)
576     return v->button_pressed(v, x, y, pressure, which, type, state);
577 
578   return 0;
579 }
580 
dt_view_manager_key_pressed(dt_view_manager_t * vm,guint key,guint state)581 int dt_view_manager_key_pressed(dt_view_manager_t *vm, guint key, guint state)
582 {
583   if(!vm->current_view) return 0;
584   if(vm->current_view->key_pressed) return vm->current_view->key_pressed(vm->current_view, key, state);
585   return 0;
586 }
587 
dt_view_manager_key_released(dt_view_manager_t * vm,guint key,guint state)588 int dt_view_manager_key_released(dt_view_manager_t *vm, guint key, guint state)
589 {
590   if(!vm->current_view) return 0;
591   if(vm->current_view->key_released) return vm->current_view->key_released(vm->current_view, key, state);
592   return 0;
593 }
594 
dt_view_manager_configure(dt_view_manager_t * vm,int width,int height)595 void dt_view_manager_configure(dt_view_manager_t *vm, int width, int height)
596 {
597   for(GList *iter = vm->views; iter; iter = g_list_next(iter))
598   {
599     // this is necessary for all
600     dt_view_t *v = (dt_view_t *)iter->data;
601     v->width = width;
602     v->height = height;
603     if(v->configure) v->configure(v, width, height);
604   }
605 }
606 
dt_view_manager_scrolled(dt_view_manager_t * vm,double x,double y,int up,int state)607 void dt_view_manager_scrolled(dt_view_manager_t *vm, double x, double y, int up, int state)
608 {
609   if(!vm->current_view) return;
610   if(vm->current_view->scrolled) vm->current_view->scrolled(vm->current_view, x, y, up, state);
611 }
612 
dt_view_manager_scrollbar_changed(dt_view_manager_t * vm,double x,double y)613 void dt_view_manager_scrollbar_changed(dt_view_manager_t *vm, double x, double y)
614 {
615   if(!vm->current_view) return;
616   if(vm->current_view->scrollbar_changed)
617     vm->current_view->scrollbar_changed(vm->current_view, x, y);
618 }
619 
dt_view_set_scrollbar(dt_view_t * view,float hpos,float hlower,float hsize,float hwinsize,float vpos,float vlower,float vsize,float vwinsize)620 void dt_view_set_scrollbar(dt_view_t *view, float hpos, float hlower, float hsize, float hwinsize, float vpos,
621                            float vlower, float vsize, float vwinsize)
622 {
623   if (view->vscroll_pos == vpos
624       && view->vscroll_lower == vlower
625       && view->vscroll_size == vsize
626       && view->vscroll_viewport_size == vwinsize
627       && view->hscroll_pos == hpos
628       && view->hscroll_lower == hlower
629       && view->hscroll_size == hsize
630       && view->hscroll_viewport_size == hwinsize)
631     return;
632 
633   view->vscroll_pos = vpos;
634   view->vscroll_lower = vlower;
635   view->vscroll_size = vsize;
636   view->vscroll_viewport_size = vwinsize;
637   view->hscroll_pos = hpos;
638   view->hscroll_lower = hlower;
639   view->hscroll_size = hsize;
640   view->hscroll_viewport_size = hwinsize;
641 
642   GtkWidget *widget;
643   widget = darktable.gui->widgets.left_border;
644   gtk_widget_queue_draw(widget);
645   widget = darktable.gui->widgets.right_border;
646   gtk_widget_queue_draw(widget);
647   widget = darktable.gui->widgets.bottom_border;
648   gtk_widget_queue_draw(widget);
649   widget = darktable.gui->widgets.top_border;
650   gtk_widget_queue_draw(widget);
651 
652   if (!darktable.gui->scrollbars.dragging)
653     dt_ui_update_scrollbars(darktable.gui->ui);
654 }
655 
_images_to_act_on_find_custom(gconstpointer a,gconstpointer b)656 static int _images_to_act_on_find_custom(gconstpointer a, gconstpointer b)
657 {
658   return (GPOINTER_TO_INT(a) != GPOINTER_TO_INT(b));
659 }
_images_to_act_on_insert_in_list(GList ** list,const int imgid,gboolean only_visible)660 static void _images_to_act_on_insert_in_list(GList **list, const int imgid, gboolean only_visible)
661 {
662   if(only_visible)
663   {
664     if(!g_list_find_custom(*list, GINT_TO_POINTER(imgid), _images_to_act_on_find_custom))
665       *list = g_list_append(*list, GINT_TO_POINTER(imgid));
666     return;
667   }
668 
669   const dt_image_t *image = dt_image_cache_get(darktable.image_cache, imgid, 'r');
670   if(image)
671   {
672     const int img_group_id = image->group_id;
673     dt_image_cache_read_release(darktable.image_cache, image);
674 
675     if(!darktable.gui
676        || !darktable.gui->grouping
677        || darktable.gui->expanded_group_id == img_group_id
678        || !dt_selection_get_collection(darktable.selection))
679     {
680       if(!g_list_find_custom(*list, GINT_TO_POINTER(imgid), _images_to_act_on_find_custom))
681         *list = g_list_append(*list, GINT_TO_POINTER(imgid));
682     }
683     else
684     {
685       sqlite3_stmt *stmt;
686       gchar *query = dt_util_dstrcat(
687           NULL,
688           "SELECT id"
689           "  FROM main.images"
690           "  WHERE group_id = %d AND id IN (%s)",
691           img_group_id, dt_collection_get_query_no_group(dt_selection_get_collection(darktable.selection)));
692       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
693       while(sqlite3_step(stmt) == SQLITE_ROW)
694       {
695         const int imgidg = sqlite3_column_int(stmt, 0);
696         if(!g_list_find_custom(*list, GINT_TO_POINTER(imgidg), _images_to_act_on_find_custom))
697           *list = g_list_append(*list, GINT_TO_POINTER(imgidg));
698       }
699       sqlite3_finalize(stmt);
700       g_free(query);
701     }
702   }
703 }
704 
705 // get the list of images to act on during global changes (libs, accels)
dt_view_get_images_to_act_on(const gboolean only_visible,const gboolean force,const gboolean ordered)706 const GList *dt_view_get_images_to_act_on(const gboolean only_visible, const gboolean force,
707                                           const gboolean ordered)
708 {
709   /** Here's how it works
710    *
711    *             mouse over| x | x | x |   |   |
712    *     mouse inside table| x | x |   |   |   |
713    * mouse inside selection| x |   |   |   |   |
714    *          active images| ? | ? | x |   | x |
715    *                       |   |   |   |   |   |
716    *                       | S | O | O | S | A |
717    *  S = selection ; O = mouseover ; A = active images
718    *  the mouse can be outside thumbtable in case of filmstrip + mouse in center widget
719    *
720    *  if only_visible is FALSE, then it will add also not visible images because of grouping
721    *  force define if we try to use cache or not
722    *  if ordered is TRUE, we return the list in the gui order. Otherwise the order is undefined (but quicker)
723    **/
724 
725   const int mouseover = dt_control_get_mouse_over_id();
726 
727   // if possible, we return the cached list
728   if(!force
729      && darktable.view_manager->act_on.ok
730      && darktable.view_manager->act_on.image_over == mouseover
731      && darktable.view_manager->act_on.ordered == ordered
732      && darktable.view_manager->act_on.inside_table
733             == dt_ui_thumbtable(darktable.gui->ui)->mouse_inside
734      && g_slist_length(darktable.view_manager->act_on.active_imgs)
735             == g_slist_length(darktable.view_manager->active_images))
736   {
737     // we test active images if mouse outside table
738     gboolean ok = TRUE;
739     if(!dt_ui_thumbtable(darktable.gui->ui)->mouse_inside
740        && darktable.view_manager->act_on.active_imgs)
741     {
742       GSList *l1 = darktable.view_manager->act_on.active_imgs;
743       GSList *l2 = darktable.view_manager->active_images;
744       while(l1 && l2)
745       {
746         if(GPOINTER_TO_INT(l1->data) != GPOINTER_TO_INT(l2->data))
747         {
748           ok = FALSE;
749           break;
750         }
751         l2 = g_slist_next(l2);
752         l1 = g_slist_next(l1);
753       }
754     }
755     if(ok) return darktable.view_manager->act_on.images;
756   }
757 
758   GList *l = NULL;
759   gboolean inside_sel = FALSE;
760   if(mouseover > 0)
761   {
762     // column 1,2,3
763     if(dt_ui_thumbtable(darktable.gui->ui)->mouse_inside)
764     {
765       // column 1,2
766       sqlite3_stmt *stmt;
767       gchar *query = dt_util_dstrcat(NULL, "SELECT imgid FROM main.selected_images WHERE imgid=%d", mouseover);
768       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
769       if(stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW)
770       {
771         inside_sel = TRUE;
772         sqlite3_finalize(stmt);
773       }
774       g_free(query);
775 
776       if(inside_sel)
777       {
778         // column 1
779 
780         // first, we try to return cached list if we were already
781         // inside sel and the selection has not changed
782         if(!force
783            && darktable.view_manager->act_on.ok
784            && darktable.view_manager->act_on.image_over_inside_sel
785            && darktable.view_manager->act_on.inside_table
786            && darktable.view_manager->act_on.ordered == ordered)
787         {
788           return darktable.view_manager->act_on.images;
789         }
790 
791         // we return the list of the selection
792         l = dt_selection_get_list(darktable.selection, only_visible, ordered);
793       }
794       else
795       {
796         // column 2
797         _images_to_act_on_insert_in_list(&l, mouseover, only_visible);
798       }
799     }
800     else
801     {
802       // column 3
803       _images_to_act_on_insert_in_list(&l, mouseover, only_visible);
804       // be absolutely sure we have the id in the list (in darkroom,
805       // the active image can be out of collection)
806       if(!only_visible) _images_to_act_on_insert_in_list(&l, mouseover, TRUE);
807     }
808   }
809   else
810   {
811     // column 4,5
812     if(darktable.view_manager->active_images)
813     {
814       // column 5
815       for(GSList *ll = darktable.view_manager->active_images; ll; ll = g_slist_next(ll))
816       {
817         const int id = GPOINTER_TO_INT(ll->data);
818         _images_to_act_on_insert_in_list(&l, id, only_visible);
819         // be absolutely sure we have the id in the list (in darkroom,
820         // the active image can be out of collection)
821         if(!only_visible) _images_to_act_on_insert_in_list(&l, id, TRUE);
822       }
823     }
824     else
825     {
826       // column 4
827       // we return the list of the selection
828       l = dt_selection_get_list(darktable.selection, only_visible, ordered);
829     }
830   }
831 
832   // let's register the new list as cached
833   darktable.view_manager->act_on.image_over_inside_sel = inside_sel;
834   darktable.view_manager->act_on.ordered = ordered;
835   darktable.view_manager->act_on.image_over = mouseover;
836   g_list_free(darktable.view_manager->act_on.images);
837   darktable.view_manager->act_on.images = l;
838   g_slist_free(darktable.view_manager->act_on.active_imgs);
839   darktable.view_manager->act_on.active_imgs = g_slist_copy(darktable.view_manager->active_images);
840   darktable.view_manager->act_on.inside_table = dt_ui_thumbtable(darktable.gui->ui)->mouse_inside;
841   darktable.view_manager->act_on.ok = TRUE;
842 
843   return darktable.view_manager->act_on.images;
844 }
845 
846 // get the query to retrieve images to act on. this is useful to speedup actions if they already use sqlite queries
dt_view_get_images_to_act_on_query(const gboolean only_visible)847 gchar *dt_view_get_images_to_act_on_query(const gboolean only_visible)
848 {
849   /** Here's how it works
850    *
851    *             mouse over| x | x | x |   |   |
852    *     mouse inside table| x | x |   |   |   |
853    * mouse inside selection| x |   |   |   |   |
854    *          active images| ? | ? | x |   | x |
855    *                       |   |   |   |   |   |
856    *                       | S | O | O | S | A |
857    *  S = selection ; O = mouseover ; A = active images
858    *  the mouse can be outside thumbtable in case of filmstrip + mouse in center widget
859    *
860    *  if only_visible is FALSE, then it will add also not visible images because of grouping
861    *  due to dt_selection_get_list_query limitation, order is always considered as undefined
862    **/
863 
864   const int mouseover = dt_control_get_mouse_over_id();
865 
866   GList *l = NULL;
867   gboolean inside_sel = FALSE;
868   if(mouseover > 0)
869   {
870     // column 1,2,3
871     if(dt_ui_thumbtable(darktable.gui->ui)->mouse_inside)
872     {
873       // column 1,2
874       sqlite3_stmt *stmt;
875       gchar *query = dt_util_dstrcat(NULL, "SELECT imgid FROM main.selected_images WHERE imgid =%d", mouseover);
876       DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
877       if(stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW)
878       {
879         inside_sel = TRUE;
880         sqlite3_finalize(stmt);
881       }
882       g_free(query);
883 
884       if(inside_sel)
885       {
886         // column 1
887         return dt_selection_get_list_query(darktable.selection, only_visible, FALSE);
888       }
889       else
890       {
891         // column 2
892         _images_to_act_on_insert_in_list(&l, mouseover, only_visible);
893       }
894     }
895     else
896     {
897       // column 3
898       _images_to_act_on_insert_in_list(&l, mouseover, only_visible);
899       // be absolutely sure we have the id in the list (in darkroom,
900       // the active image can be out of collection)
901       if(!only_visible) _images_to_act_on_insert_in_list(&l, mouseover, TRUE);
902     }
903   }
904   else
905   {
906     // column 4,5
907     if(darktable.view_manager->active_images)
908     {
909       // column 5
910       for(GSList *ll = darktable.view_manager->active_images;
911           ll;
912           ll = g_slist_next(ll))
913       {
914         const int id = GPOINTER_TO_INT(ll->data);
915         _images_to_act_on_insert_in_list(&l, id, only_visible);
916         // be absolutely sure we have the id in the list (in darkroom,
917         // the active image can be out of collection)
918         if(!only_visible) _images_to_act_on_insert_in_list(&l, id, TRUE);
919       }
920     }
921     else
922     {
923       // column 4
924       return dt_selection_get_list_query(darktable.selection, only_visible, FALSE);
925     }
926   }
927 
928   // if we don't return the selection, we return the list of imgid separated by comma
929   // in the form it can be used inside queries
930   gchar *images = NULL;
931   for(; l; l = g_list_next(l))
932   {
933     images = dt_util_dstrcat(images, "%d,", GPOINTER_TO_INT(l->data));
934   }
935   if(images)
936   {
937     images[strlen(images) - 1] = '\0';
938   }
939   else
940     images = dt_util_dstrcat(NULL, " ");
941   return images;
942 }
943 
944 // get the main image to act on during global changes (libs, accels)
dt_view_get_image_to_act_on()945 int dt_view_get_image_to_act_on()
946 {
947   /** Here's how it works -- same as for list, except we don't care about mouse inside selection or table
948    *
949    *             mouse over| x |   |   |
950    *          active images| ? |   | x |
951    *                       |   |   |   |
952    *                       | O | S | A |
953    *  First image of ...
954    *  S = selection ; O = mouseover ; A = active images
955    **/
956 
957   int ret = -1;
958   const int mouseover = dt_control_get_mouse_over_id();
959 
960   if(mouseover > 0)
961   {
962     ret = mouseover;
963   }
964   else
965   {
966     if(darktable.view_manager->active_images)
967     {
968       ret = GPOINTER_TO_INT(darktable.view_manager->active_images->data);
969     }
970     else
971     {
972       sqlite3_stmt *stmt;
973       DT_DEBUG_SQLITE3_PREPARE_V2
974         (dt_database_get(darktable.db),
975          "SELECT s.imgid"
976          " FROM main.selected_images as s, memory.collected_images as c"
977          " WHERE s.imgid=c.imgid"
978          " ORDER BY c.rowid LIMIT 1",
979                                   -1, &stmt, NULL);
980       if(stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW)
981       {
982         ret = sqlite3_column_int(stmt, 0);
983       }
984       if(stmt) sqlite3_finalize(stmt);
985     }
986   }
987 
988   return ret;
989 }
990 
dt_view_image_get_surface(int imgid,int width,int height,cairo_surface_t ** surface,const gboolean quality)991 dt_view_surface_value_t dt_view_image_get_surface(int imgid, int width, int height, cairo_surface_t **surface,
992                                                   const gboolean quality)
993 {
994   double tt = 0;
995   if((darktable.unmuted & (DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF)) == (DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF))
996     tt = dt_get_wtime();
997 
998   dt_view_surface_value_t ret = DT_VIEW_SURFACE_KO;
999   // if surface not null, clean it up
1000   if(*surface
1001      && cairo_surface_get_reference_count(*surface) > 0) cairo_surface_destroy(*surface);
1002   *surface = NULL;
1003 
1004   // get mipmap cache image
1005   dt_mipmap_cache_t *cache = darktable.mipmap_cache;
1006   dt_mipmap_size_t mip = dt_mipmap_cache_get_matching_size(cache, width * darktable.gui->ppd, height * darktable.gui->ppd);
1007 
1008   // if needed, we load the mimap buffer
1009   dt_mipmap_buffer_t buf;
1010   dt_mipmap_cache_get(cache, &buf, imgid, mip, DT_MIPMAP_BEST_EFFORT, 'r');
1011   const int buf_wd = buf.width;
1012   const int buf_ht = buf.height;
1013 
1014   // if we don't get buffer, no image is awailable at the moment
1015   if(!buf.buf)
1016   {
1017     dt_mipmap_cache_release(darktable.mipmap_cache, &buf);
1018     return DT_VIEW_SURFACE_KO;
1019   }
1020 
1021   // so we create a new image surface to return
1022   float scale = fminf(width / (float)buf_wd, height / (float)buf_ht) * darktable.gui->ppd_thb;
1023   const int img_width = roundf(buf_wd * scale);
1024   const int img_height = roundf(buf_ht * scale);
1025   // due to the forced rounding above, we need to recompute scaling
1026   scale = fmaxf(img_width / (float)buf_wd, img_height / (float)buf_ht);
1027   *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, img_width, img_height);
1028 
1029   // we transfer cached image on a cairo_surface (with colorspace transform if needed)
1030   cairo_surface_t *tmp_surface = NULL;
1031   uint8_t *rgbbuf = (uint8_t *)calloc((size_t)buf_wd * buf_ht * 4, sizeof(uint8_t));
1032   if(rgbbuf)
1033   {
1034     gboolean have_lock = FALSE;
1035     cmsHTRANSFORM transform = NULL;
1036 
1037     if(dt_conf_get_bool("cache_color_managed"))
1038     {
1039       pthread_rwlock_rdlock(&darktable.color_profiles->xprofile_lock);
1040       have_lock = TRUE;
1041 
1042       // we only color manage when a thumbnail is sRGB or AdobeRGB. everything else just gets dumped to the
1043       // screen
1044       if(buf.color_space == DT_COLORSPACE_SRGB
1045          && darktable.color_profiles->transform_srgb_to_display)
1046       {
1047         transform = darktable.color_profiles->transform_srgb_to_display;
1048       }
1049       else if(buf.color_space == DT_COLORSPACE_ADOBERGB
1050               && darktable.color_profiles->transform_adobe_rgb_to_display)
1051       {
1052         transform = darktable.color_profiles->transform_adobe_rgb_to_display;
1053       }
1054       else
1055       {
1056         pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock);
1057         have_lock = FALSE;
1058         if(buf.color_space == DT_COLORSPACE_NONE)
1059         {
1060           fprintf(stderr, "oops, there seems to be a code path not setting the color space of thumbnails!\n");
1061         }
1062         else if(buf.color_space != DT_COLORSPACE_DISPLAY && buf.color_space != DT_COLORSPACE_DISPLAY2)
1063         {
1064           fprintf(stderr,
1065                   "oops, there seems to be a code path setting an unhandled color space of thumbnails (%s)!\n",
1066                   dt_colorspaces_get_name(buf.color_space, "from file"));
1067         }
1068       }
1069     }
1070 
1071 #ifdef _OPENMP
1072 #pragma omp parallel for schedule(static) default(none) shared(buf, rgbbuf, transform)
1073 #endif
1074     for(int i = 0; i < buf.height; i++)
1075     {
1076       const uint8_t *in = buf.buf + i * buf.width * 4;
1077       uint8_t *out = rgbbuf + i * buf.width * 4;
1078 
1079       if(transform)
1080       {
1081         cmsDoTransform(transform, in, out, buf.width);
1082       }
1083       else
1084       {
1085         for(int j = 0; j < buf.width; j++, in += 4, out += 4)
1086         {
1087           out[0] = in[2];
1088           out[1] = in[1];
1089           out[2] = in[0];
1090         }
1091       }
1092     }
1093     if(have_lock) pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock);
1094 
1095     const int32_t stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, buf_wd);
1096     tmp_surface = cairo_image_surface_create_for_data(rgbbuf, CAIRO_FORMAT_RGB24, buf_wd, buf_ht, stride);
1097   }
1098 
1099   // draw the image scaled:
1100   if(tmp_surface)
1101   {
1102     cairo_t *cr = cairo_create(*surface);
1103     cairo_scale(cr, scale, scale);
1104 
1105     cairo_set_source_surface(cr, tmp_surface, 0, 0);
1106     // set filter no nearest:
1107     // in skull mode, we want to see big pixels.
1108     // in 1 iir mode for the right mip, we want to see exactly what the pipe gave us, 1:1 pixel for pixel.
1109     // in between, filtering just makes stuff go unsharp.
1110     if((buf_wd <= 8 && buf_ht <= 8)
1111        || fabsf(scale - 1.0f) < 0.01f)
1112       cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
1113     else if(mip != buf.size)
1114       cairo_pattern_set_filter(cairo_get_source(cr),
1115                                CAIRO_FILTER_FAST); // not the right size, so we scale as fast a possible
1116     else
1117       cairo_pattern_set_filter(cairo_get_source(cr), ((darktable.gui->filter_image == CAIRO_FILTER_FAST) && quality)
1118                                                          ? CAIRO_FILTER_GOOD
1119                                                          : darktable.gui->filter_image);
1120 
1121     cairo_paint(cr);
1122     /* from focus_peaking.h
1123        static inline void dt_focuspeaking(cairo_t *cr, int width, int height,
1124                                        uint8_t *const restrict image,
1125                                        const int buf_width, const int buf_height)
1126        The current implementation assumes the data at image is organized as a rectangle without a stride,
1127        So we pass the raw data to be processed, this is more data but correct.
1128     */
1129     if(darktable.gui->show_focus_peaking && mip == buf.size)
1130       dt_focuspeaking(cr, img_width, img_height, rgbbuf, buf_wd, buf_ht);
1131 
1132     cairo_surface_destroy(tmp_surface);
1133     cairo_destroy(cr);
1134   }
1135 
1136   // we consider skull as ok as the image hasn't to be reload
1137   if(buf_wd <= 8 && buf_ht <= 8)
1138     ret = DT_VIEW_SURFACE_OK;
1139   else if(mip != buf.size)
1140     ret = DT_VIEW_SURFACE_SMALLER;
1141   else
1142     ret = DT_VIEW_SURFACE_OK;
1143 
1144   dt_mipmap_cache_release(darktable.mipmap_cache, &buf);
1145   if(rgbbuf) free(rgbbuf);
1146 
1147   // logs
1148   if((darktable.unmuted & (DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF)) == (DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF))
1149   {
1150     dt_print(DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF,
1151              "[dt_view_image_get_surface]  id %i, dots %ix%i, mip %ix%i, surf %ix%i created in %0.04f sec\n",
1152              imgid, width, height, buf_wd, buf_ht, img_width, img_height, dt_get_wtime() - tt);
1153   }
1154   else if(darktable.unmuted & DT_DEBUG_LIGHTTABLE)
1155   {
1156     dt_print(DT_DEBUG_LIGHTTABLE, "[dt_view_image_get_surface]  id %i, dots %ix%i, mip %ix%i, surf %ix%i\n", imgid,
1157              width, height, buf_wd, buf_ht, img_width, img_height);
1158   }
1159 
1160   // we consider skull as ok as the image hasn't to be reload
1161   return ret;
1162 }
1163 
dt_view_extend_modes_str(const char * name,const gboolean is_hdr,const gboolean is_bw,const gboolean is_bw_flow)1164 char* dt_view_extend_modes_str(const char * name, const gboolean is_hdr, const gboolean is_bw, const gboolean is_bw_flow)
1165 {
1166   char* upcase = g_ascii_strup(name, -1);  // extension in capital letters to avoid character descenders
1167   // convert to canonical format extension
1168   if(0 == g_ascii_strcasecmp(upcase, "JPG"))
1169   {
1170       gchar* canonical = g_strdup("JPEG");
1171       g_free(upcase);
1172       upcase = canonical;
1173   }
1174   else if(0 == g_ascii_strcasecmp(upcase, "HDR"))
1175   {
1176       gchar* canonical = g_strdup("RGBE");
1177       g_free(upcase);
1178       upcase = canonical;
1179   }
1180   else if(0 == g_ascii_strcasecmp(upcase, "TIF"))
1181   {
1182       gchar* canonical = g_strdup("TIFF");
1183       g_free(upcase);
1184       upcase = canonical;
1185   }
1186 
1187   if(is_hdr)
1188   {
1189     gchar* fullname = g_strdup_printf("%s HDR", upcase);
1190     g_free(upcase);
1191     upcase = fullname;
1192   }
1193   if(is_bw)
1194   {
1195     gchar* fullname = g_strdup_printf("%s B&W", upcase);
1196     g_free(upcase);
1197     upcase = fullname;
1198     if(!is_bw_flow)
1199     {
1200       fullname = g_strdup_printf("%s-", upcase);
1201       g_free(upcase);
1202       upcase = fullname;
1203     }
1204   }
1205 
1206   return upcase;
1207 }
1208 
1209 /**
1210  * \brief Set the selection bit to a given value for the specified image
1211  * \param[in] imgid The image id
1212  * \param[in] value The boolean value for the bit
1213  */
dt_view_set_selection(int imgid,int value)1214 void dt_view_set_selection(int imgid, int value)
1215 {
1216   /* clear and reset statement */
1217   DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
1218   DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);
1219 
1220   /* setup statement and iterate over rows */
1221   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, imgid);
1222 
1223   if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW)
1224   {
1225     if(!value)
1226     {
1227       /* Value is set and should be unset; get rid of it */
1228 
1229       /* clear and reset statement */
1230       DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.delete_from_selected);
1231       DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.delete_from_selected);
1232 
1233       /* setup statement and execute */
1234       DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.delete_from_selected, 1, imgid);
1235       sqlite3_step(darktable.view_manager->statements.delete_from_selected);
1236     }
1237   }
1238   else if(value)
1239   {
1240     /* Select bit is unset and should be set; add it */
1241 
1242     /* clear and reset statement */
1243     DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.make_selected);
1244     DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.make_selected);
1245 
1246     /* setup statement and execute */
1247     DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.make_selected, 1, imgid);
1248     sqlite3_step(darktable.view_manager->statements.make_selected);
1249   }
1250 }
1251 
1252 /**
1253  * \brief Toggle the selection bit in the database for the specified image
1254  * \param[in] imgid The image id
1255  */
dt_view_toggle_selection(int imgid)1256 void dt_view_toggle_selection(int imgid)
1257 {
1258   /* clear and reset statement */
1259   DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
1260   DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);
1261 
1262   /* setup statement and iterate over rows */
1263   DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, imgid);
1264   if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW)
1265   {
1266     /* clear and reset statement */
1267     DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.delete_from_selected);
1268     DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.delete_from_selected);
1269 
1270     /* setup statement and execute */
1271     DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.delete_from_selected, 1, imgid);
1272     sqlite3_step(darktable.view_manager->statements.delete_from_selected);
1273   }
1274   else
1275   {
1276     /* clear and reset statement */
1277     DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.make_selected);
1278     DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.make_selected);
1279 
1280     /* setup statement and execute */
1281     DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.make_selected, 1, imgid);
1282     sqlite3_step(darktable.view_manager->statements.make_selected);
1283   }
1284 }
1285 
1286 /**
1287  * \brief Reset filter
1288  */
dt_view_filter_reset(const dt_view_manager_t * vm,gboolean smart_filter)1289 void dt_view_filter_reset(const dt_view_manager_t *vm, gboolean smart_filter)
1290 {
1291   if(vm->proxy.filter.module && vm->proxy.filter.reset_filter)
1292     vm->proxy.filter.reset_filter(vm->proxy.filter.module, smart_filter);
1293 }
1294 
dt_view_active_images_reset(gboolean raise)1295 void dt_view_active_images_reset(gboolean raise)
1296 {
1297   if(!darktable.view_manager->active_images) return;
1298   g_slist_free(darktable.view_manager->active_images);
1299   darktable.view_manager->active_images = NULL;
1300 
1301   if(raise) DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
1302 }
dt_view_active_images_add(int imgid,gboolean raise)1303 void dt_view_active_images_add(int imgid, gboolean raise)
1304 {
1305   darktable.view_manager->active_images
1306       = g_slist_append(darktable.view_manager->active_images, GINT_TO_POINTER(imgid));
1307   if(raise)
1308     DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
1309 }
dt_view_active_images_get()1310 GSList *dt_view_active_images_get()
1311 {
1312   return darktable.view_manager->active_images;
1313 }
1314 
dt_view_manager_view_toolbox_add(dt_view_manager_t * vm,GtkWidget * tool,dt_view_type_flags_t views)1315 void dt_view_manager_view_toolbox_add(dt_view_manager_t *vm, GtkWidget *tool, dt_view_type_flags_t views)
1316 {
1317   if(vm->proxy.view_toolbox.module)
1318     vm->proxy.view_toolbox.add(vm->proxy.view_toolbox.module, tool, views);
1319 }
1320 
dt_view_manager_module_toolbox_add(dt_view_manager_t * vm,GtkWidget * tool,dt_view_type_flags_t views)1321 void dt_view_manager_module_toolbox_add(dt_view_manager_t *vm, GtkWidget *tool, dt_view_type_flags_t views)
1322 {
1323   if(vm->proxy.module_toolbox.module)
1324     vm->proxy.module_toolbox.add(vm->proxy.module_toolbox.module, tool, views);
1325 }
1326 
dt_view_darkroom_get_layout(dt_view_manager_t * vm)1327 dt_darkroom_layout_t dt_view_darkroom_get_layout(dt_view_manager_t *vm)
1328 {
1329   if(vm->proxy.darkroom.view)
1330     return vm->proxy.darkroom.get_layout(vm->proxy.darkroom.view);
1331   else
1332     return DT_DARKROOM_LAYOUT_EDITING;
1333 }
1334 
dt_view_lighttable_set_zoom(dt_view_manager_t * vm,gint zoom)1335 void dt_view_lighttable_set_zoom(dt_view_manager_t *vm, gint zoom)
1336 {
1337   if(vm->proxy.lighttable.module)
1338     vm->proxy.lighttable.set_zoom(vm->proxy.lighttable.module, zoom);
1339 }
1340 
dt_view_lighttable_get_zoom(dt_view_manager_t * vm)1341 gint dt_view_lighttable_get_zoom(dt_view_manager_t *vm)
1342 {
1343   if(vm->proxy.lighttable.module)
1344     return vm->proxy.lighttable.get_zoom(vm->proxy.lighttable.module);
1345   else
1346     return 10;
1347 }
1348 
dt_view_lighttable_culling_init_mode(dt_view_manager_t * vm)1349 void dt_view_lighttable_culling_init_mode(dt_view_manager_t *vm)
1350 {
1351   if(vm->proxy.lighttable.module) vm->proxy.lighttable.culling_init_mode(vm->proxy.lighttable.view);
1352 }
1353 
dt_view_lighttable_culling_preview_refresh(dt_view_manager_t * vm)1354 void dt_view_lighttable_culling_preview_refresh(dt_view_manager_t *vm)
1355 {
1356   if(vm->proxy.lighttable.module)
1357     vm->proxy.lighttable.culling_preview_refresh(vm->proxy.lighttable.view);
1358 }
1359 
dt_view_lighttable_culling_preview_reload_overlays(dt_view_manager_t * vm)1360 void dt_view_lighttable_culling_preview_reload_overlays(dt_view_manager_t *vm)
1361 {
1362   if(vm->proxy.lighttable.module)
1363     vm->proxy.lighttable.culling_preview_reload_overlays(vm->proxy.lighttable.view);
1364 }
1365 
dt_view_lighttable_get_layout(dt_view_manager_t * vm)1366 dt_lighttable_layout_t dt_view_lighttable_get_layout(dt_view_manager_t *vm)
1367 {
1368   if(vm->proxy.lighttable.module)
1369     return vm->proxy.lighttable.get_layout(vm->proxy.lighttable.module);
1370   else
1371     return DT_LIGHTTABLE_LAYOUT_FILEMANAGER;
1372 }
1373 
dt_view_lighttable_preview_state(dt_view_manager_t * vm)1374 gboolean dt_view_lighttable_preview_state(dt_view_manager_t *vm)
1375 {
1376   if(vm->proxy.lighttable.module)
1377     return vm->proxy.lighttable.get_preview_state(vm->proxy.lighttable.view);
1378   else
1379     return FALSE;
1380 }
1381 
dt_view_lighttable_set_preview_state(dt_view_manager_t * vm,gboolean state,gboolean focus)1382 void dt_view_lighttable_set_preview_state(dt_view_manager_t *vm, gboolean state, gboolean focus)
1383 {
1384   if(vm->proxy.lighttable.module)
1385     vm->proxy.lighttable.set_preview_state(vm->proxy.lighttable.view, state, focus);
1386 }
1387 
dt_view_lighttable_change_offset(dt_view_manager_t * vm,gboolean reset,gint imgid)1388 void dt_view_lighttable_change_offset(dt_view_manager_t *vm, gboolean reset, gint imgid)
1389 {
1390   if(vm->proxy.lighttable.module)
1391     vm->proxy.lighttable.change_offset(vm->proxy.lighttable.view, reset, imgid);
1392 }
1393 
dt_view_collection_update(const dt_view_manager_t * vm)1394 void dt_view_collection_update(const dt_view_manager_t *vm)
1395 {
1396   if(vm->proxy.module_collect.module)
1397     vm->proxy.module_collect.update(vm->proxy.module_collect.module);
1398 }
1399 
1400 
dt_view_tethering_get_selected_imgid(const dt_view_manager_t * vm)1401 int32_t dt_view_tethering_get_selected_imgid(const dt_view_manager_t *vm)
1402 {
1403   if(vm->proxy.tethering.view)
1404     return vm->proxy.tethering.get_selected_imgid(vm->proxy.tethering.view);
1405 
1406   return -1;
1407 }
1408 
dt_view_tethering_set_job_code(const dt_view_manager_t * vm,const char * name)1409 void dt_view_tethering_set_job_code(const dt_view_manager_t *vm, const char *name)
1410 {
1411   if(vm->proxy.tethering.view)
1412     vm->proxy.tethering.set_job_code(vm->proxy.tethering.view, name);
1413 }
1414 
dt_view_tethering_get_job_code(const dt_view_manager_t * vm)1415 const char *dt_view_tethering_get_job_code(const dt_view_manager_t *vm)
1416 {
1417   if(vm->proxy.tethering.view)
1418     return vm->proxy.tethering.get_job_code(vm->proxy.tethering.view);
1419   return NULL;
1420 }
1421 
1422 #ifdef HAVE_MAP
dt_view_map_center_on_location(const dt_view_manager_t * vm,gdouble lon,gdouble lat,gdouble zoom)1423 void dt_view_map_center_on_location(const dt_view_manager_t *vm, gdouble lon, gdouble lat, gdouble zoom)
1424 {
1425   if(vm->proxy.map.view)
1426     vm->proxy.map.center_on_location(vm->proxy.map.view, lon, lat, zoom);
1427 }
1428 
dt_view_map_center_on_bbox(const dt_view_manager_t * vm,gdouble lon1,gdouble lat1,gdouble lon2,gdouble lat2)1429 void dt_view_map_center_on_bbox(const dt_view_manager_t *vm, gdouble lon1, gdouble lat1, gdouble lon2, gdouble lat2)
1430 {
1431   if(vm->proxy.map.view)
1432     vm->proxy.map.center_on_bbox(vm->proxy.map.view, lon1, lat1, lon2, lat2);
1433 }
1434 
dt_view_map_show_osd(const dt_view_manager_t * vm)1435 void dt_view_map_show_osd(const dt_view_manager_t *vm)
1436 {
1437   if(vm->proxy.map.view)
1438     vm->proxy.map.show_osd(vm->proxy.map.view);
1439 }
1440 
dt_view_map_set_map_source(const dt_view_manager_t * vm,OsmGpsMapSource_t map_source)1441 void dt_view_map_set_map_source(const dt_view_manager_t *vm, OsmGpsMapSource_t map_source)
1442 {
1443   if(vm->proxy.map.view)
1444     vm->proxy.map.set_map_source(vm->proxy.map.view, map_source);
1445 }
1446 
dt_view_map_add_marker(const dt_view_manager_t * vm,dt_geo_map_display_t type,GList * points)1447 GObject *dt_view_map_add_marker(const dt_view_manager_t *vm, dt_geo_map_display_t type, GList *points)
1448 {
1449   if(vm->proxy.map.view)
1450     return vm->proxy.map.add_marker(vm->proxy.map.view, type, points);
1451   return NULL;
1452 }
1453 
dt_view_map_remove_marker(const dt_view_manager_t * vm,dt_geo_map_display_t type,GObject * marker)1454 gboolean dt_view_map_remove_marker(const dt_view_manager_t *vm, dt_geo_map_display_t type, GObject *marker)
1455 {
1456   if(vm->proxy.map.view)
1457     return vm->proxy.map.remove_marker(vm->proxy.map.view, type, marker);
1458   return FALSE;
1459 }
dt_view_map_add_location(const dt_view_manager_t * vm,dt_map_location_data_t * p,const guint posid)1460 void dt_view_map_add_location(const dt_view_manager_t *vm, dt_map_location_data_t *p, const guint posid)
1461 {
1462   if(vm->proxy.map.view)
1463     vm->proxy.map.add_location(vm->proxy.map.view, p, posid);
1464 }
1465 
dt_view_map_location_action(const dt_view_manager_t * vm,const int action)1466 void dt_view_map_location_action(const dt_view_manager_t *vm, const int action)
1467 {
1468   if(vm->proxy.map.view)
1469     vm->proxy.map.location_action(vm->proxy.map.view, action);
1470 }
1471 
dt_view_map_drag_set_icon(const dt_view_manager_t * vm,GdkDragContext * context,const int imgid,const int count)1472 void dt_view_map_drag_set_icon(const dt_view_manager_t *vm, GdkDragContext *context, const int imgid, const int count)
1473 {
1474   if(vm->proxy.map.view)
1475     vm->proxy.map.drag_set_icon(vm->proxy.map.view, context, imgid, count);
1476 }
1477 
1478 #endif
1479 
1480 #ifdef HAVE_PRINT
dt_view_print_settings(const dt_view_manager_t * vm,dt_print_info_t * pinfo)1481 void dt_view_print_settings(const dt_view_manager_t *vm, dt_print_info_t *pinfo)
1482 {
1483   if (vm->proxy.print.view)
1484     vm->proxy.print.print_settings(vm->proxy.print.view, pinfo);
1485 }
1486 #endif
1487 
dt_mouse_action_create_simple(GSList * actions,dt_mouse_action_type_t type,GdkModifierType accel,const char * const description)1488 GSList *dt_mouse_action_create_simple(GSList *actions, dt_mouse_action_type_t type, GdkModifierType accel,
1489                                       const char *const description)
1490 {
1491   dt_mouse_action_t *a = (dt_mouse_action_t *)calloc(1, sizeof(dt_mouse_action_t));
1492   a->action = type;
1493   a->key.accel_mods = accel;
1494   g_strlcpy(a->name, description, sizeof(a->name));
1495   return g_slist_append(actions, a);
1496 }
1497 
dt_mouse_action_create_format(GSList * actions,dt_mouse_action_type_t type,GdkModifierType accel,const char * const format_string,const char * const replacement)1498 GSList *dt_mouse_action_create_format(GSList *actions, dt_mouse_action_type_t type, GdkModifierType accel,
1499                                       const char *const format_string, const char *const replacement)
1500 {
1501   dt_mouse_action_t *a = (dt_mouse_action_t *)calloc(1, sizeof(dt_mouse_action_t));
1502   a->action = type;
1503   a->key.accel_mods = accel;
1504   g_snprintf(a->name, sizeof(a->name), format_string, replacement);
1505   return g_slist_append(actions, a);
1506 }
1507 
_mouse_action_get_string(dt_mouse_action_t * ma)1508 static gchar *_mouse_action_get_string(dt_mouse_action_t *ma)
1509 {
1510   gchar *accel_label = gtk_accelerator_get_label(ma->key.accel_key, ma->key.accel_mods);
1511   gchar *atxt = dt_util_dstrcat(NULL, "%s", accel_label);
1512   g_free(accel_label);
1513 
1514   if(strcmp(atxt, ""))
1515     atxt = dt_util_dstrcat(atxt, "+");
1516 
1517   switch(ma->action)
1518   {
1519     case DT_MOUSE_ACTION_LEFT:
1520       atxt = dt_util_dstrcat(atxt, _("Left click"));
1521       break;
1522     case DT_MOUSE_ACTION_RIGHT:
1523       atxt = dt_util_dstrcat(atxt, _("Right click"));
1524       break;
1525     case DT_MOUSE_ACTION_MIDDLE:
1526       atxt = dt_util_dstrcat(atxt, _("Middle click"));
1527       break;
1528     case DT_MOUSE_ACTION_SCROLL:
1529       atxt = dt_util_dstrcat(atxt, _("Scroll"));
1530       break;
1531     case DT_MOUSE_ACTION_DOUBLE_LEFT:
1532       atxt = dt_util_dstrcat(atxt, _("Left double-click"));
1533       break;
1534     case DT_MOUSE_ACTION_DOUBLE_RIGHT:
1535       atxt = dt_util_dstrcat(atxt, _("Right double-click"));
1536       break;
1537     case DT_MOUSE_ACTION_DRAG_DROP:
1538       atxt = dt_util_dstrcat(atxt, _("Drag and drop"));
1539       break;
1540     case DT_MOUSE_ACTION_LEFT_DRAG:
1541       atxt = dt_util_dstrcat(atxt, _("Left click+Drag"));
1542       break;
1543     case DT_MOUSE_ACTION_RIGHT_DRAG:
1544       atxt = dt_util_dstrcat(atxt, _("Right click+Drag"));
1545       break;
1546   }
1547 
1548   return atxt;
1549 }
1550 
_accels_window_destroy(GtkWidget * widget,dt_view_manager_t * vm)1551 static void _accels_window_destroy(GtkWidget *widget, dt_view_manager_t *vm)
1552 {
1553   // set to NULL so we can rely on it after
1554   vm->accels_window.window = NULL;
1555 }
1556 
_accels_window_sticky(GtkWidget * widget,GdkEventButton * event,dt_view_manager_t * vm)1557 static void _accels_window_sticky(GtkWidget *widget, GdkEventButton *event, dt_view_manager_t *vm)
1558 {
1559   if(!vm->accels_window.window) return;
1560 
1561   // creating new window
1562   GtkWindow *win = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
1563   GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(win));
1564   gtk_style_context_add_class(context, "accels_window");
1565   gtk_window_set_title(win, _("darktable - accels window"));
1566   GtkAllocation alloc;
1567   gtk_widget_get_allocation(dt_ui_main_window(darktable.gui->ui), &alloc);
1568 
1569   gtk_window_set_resizable(win, TRUE);
1570   gtk_window_set_icon_name(win, "darktable");
1571   gtk_window_set_default_size(win, alloc.width * 0.7, alloc.height * 0.7);
1572   g_signal_connect(win, "destroy", G_CALLBACK(_accels_window_destroy), vm);
1573 
1574   GtkWidget *sw = dt_gui_container_first_child(GTK_CONTAINER(vm->accels_window.window));
1575   g_object_ref(sw);
1576   gtk_container_remove(GTK_CONTAINER(vm->accels_window.window), sw);
1577   gtk_container_add(GTK_CONTAINER(win), sw);
1578   g_object_unref(sw);
1579 
1580   gtk_widget_destroy(vm->accels_window.window);
1581   vm->accels_window.window = GTK_WIDGET(win);
1582   gtk_widget_show_all(vm->accels_window.window);
1583   gtk_widget_hide(vm->accels_window.sticky_btn);
1584 
1585   vm->accels_window.sticky = TRUE;
1586 }
1587 
dt_view_accels_show(dt_view_manager_t * vm)1588 void dt_view_accels_show(dt_view_manager_t *vm)
1589 {
1590   if(vm->accels_window.window) return;
1591 
1592   vm->accels_window.sticky = FALSE;
1593   vm->accels_window.prevent_refresh = FALSE;
1594 
1595   GtkStyleContext *context;
1596   vm->accels_window.window = gtk_window_new(GTK_WINDOW_POPUP);
1597 #ifdef GDK_WINDOWING_QUARTZ
1598   dt_osx_disallow_fullscreen(vm->accels_window.window);
1599 #endif
1600   context = gtk_widget_get_style_context(vm->accels_window.window);
1601   gtk_style_context_add_class(context, "accels_window");
1602 
1603   GtkWidget *sw = gtk_scrolled_window_new(NULL, NULL);
1604   context = gtk_widget_get_style_context(sw);
1605   gtk_style_context_add_class(context, "accels_window_scroll");
1606 
1607   GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
1608 
1609   vm->accels_window.flow_box = gtk_flow_box_new();
1610   context = gtk_widget_get_style_context(vm->accels_window.flow_box);
1611   gtk_style_context_add_class(context, "accels_window_box");
1612   gtk_orientable_set_orientation(GTK_ORIENTABLE(vm->accels_window.flow_box), GTK_ORIENTATION_HORIZONTAL);
1613 
1614   gtk_box_pack_start(GTK_BOX(hb), vm->accels_window.flow_box, TRUE, TRUE, 0);
1615 
1616   GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1617   vm->accels_window.sticky_btn
1618       = dtgtk_button_new(dtgtk_cairo_paint_multiinstance, CPF_STYLE_FLAT, NULL);
1619   g_object_set(G_OBJECT(vm->accels_window.sticky_btn), "tooltip-text",
1620                _("switch to a classic window which will stay open after key release."), (char *)NULL);
1621   g_signal_connect(G_OBJECT(vm->accels_window.sticky_btn), "button-press-event", G_CALLBACK(_accels_window_sticky),
1622                    vm);
1623   context = gtk_widget_get_style_context(vm->accels_window.sticky_btn);
1624   gtk_style_context_add_class(context, "accels_window_stick");
1625   gtk_box_pack_start(GTK_BOX(vb), vm->accels_window.sticky_btn, FALSE, FALSE, 0);
1626   gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, FALSE, 0);
1627 
1628   dt_view_accels_refresh(vm);
1629 
1630   GtkAllocation alloc;
1631   gtk_widget_get_allocation(dt_ui_main_window(darktable.gui->ui), &alloc);
1632   // gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(sw), alloc.height);
1633   gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), alloc.height);
1634   gtk_scrolled_window_set_max_content_width(GTK_SCROLLED_WINDOW(sw), alloc.width);
1635   gtk_container_add(GTK_CONTAINER(sw), hb);
1636   gtk_container_add(GTK_CONTAINER(vm->accels_window.window), sw);
1637 
1638   gtk_window_set_resizable(GTK_WINDOW(vm->accels_window.window), FALSE);
1639   gtk_window_set_default_size(GTK_WINDOW(vm->accels_window.window), alloc.width, alloc.height);
1640   gtk_window_set_transient_for(GTK_WINDOW(vm->accels_window.window),
1641                                GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
1642   gtk_window_set_keep_above(GTK_WINDOW(vm->accels_window.window), TRUE);
1643   // needed on macOS to avoid fullscreening the popup with newer GTK
1644   gtk_window_set_type_hint(GTK_WINDOW(vm->accels_window.window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
1645 
1646   gtk_window_set_gravity(GTK_WINDOW(vm->accels_window.window), GDK_GRAVITY_STATIC);
1647   gtk_window_set_position(GTK_WINDOW(vm->accels_window.window), GTK_WIN_POS_CENTER_ON_PARENT);
1648   gtk_widget_show_all(vm->accels_window.window);
1649 }
1650 
dt_view_accels_hide(dt_view_manager_t * vm)1651 void dt_view_accels_hide(dt_view_manager_t *vm)
1652 {
1653   if(vm->accels_window.window && vm->accels_window.sticky) return;
1654   if(vm->accels_window.window) gtk_widget_destroy(vm->accels_window.window);
1655   vm->accels_window.window = NULL;
1656 }
1657 
dt_view_accels_refresh(dt_view_manager_t * vm)1658 void dt_view_accels_refresh(dt_view_manager_t *vm)
1659 {
1660   if(!vm->accels_window.window || vm->accels_window.prevent_refresh) return;
1661 
1662   // drop all existing tables
1663   GList *lw = gtk_container_get_children(GTK_CONTAINER(vm->accels_window.flow_box));
1664   for(const GList *lw_iter = lw; lw_iter; lw_iter = g_list_next(lw_iter))
1665   {
1666     GtkWidget *w = (GtkWidget *)lw_iter->data;
1667     gtk_widget_destroy(w);
1668   }
1669   g_list_free(lw);
1670 
1671   // get the list of valid accel for this view
1672   const dt_view_t *cv = dt_view_manager_get_current_view(vm);
1673   const dt_view_type_flags_t v = cv->view(cv);
1674   GtkStyleContext *context;
1675 
1676   typedef struct _bloc_t
1677   {
1678     gchar *base;
1679     gchar *title;
1680     GtkListStore *list_store;
1681   } _bloc_t;
1682 
1683   // go through all accels to populate categories with valid ones
1684   GList *blocs = NULL;
1685   GList *bl = NULL;
1686   for(const GList *l = darktable.control->accelerator_list;
1687       l;
1688       l = g_list_next(l))
1689   {
1690     dt_accel_t *da = (dt_accel_t *)l->data;
1691     if(da && (da->views & v) == v)
1692     {
1693       GtkAccelKey ak;
1694       if(gtk_accel_map_lookup_entry(da->path, &ak) && ak.accel_key > 0)
1695       {
1696         // we want the base path
1697         gchar **elems = g_strsplit(da->translated_path, "/", -1);
1698         if(elems[0] && elems[1] && elems[2])
1699         {
1700           // do we already have a category ?
1701           _bloc_t *b = NULL;
1702           for(bl = blocs; bl; bl = g_list_next(bl))
1703           {
1704             _bloc_t *bb = (_bloc_t *)bl->data;
1705             if(strcmp(elems[1], bb->base) == 0)
1706             {
1707               b = bb;
1708               break;
1709             }
1710           }
1711 
1712           // if not found, we create it
1713           if(!b)
1714           {
1715             b = (_bloc_t *)calloc(1, sizeof(_bloc_t));
1716             b->base = dt_util_dstrcat(NULL, "%s", elems[1]);
1717 
1718             if(g_str_has_prefix(da->path, "<Darktable>/views/"))
1719               b->title = dt_util_dstrcat(NULL, "%s", cv->name(cv));
1720             else
1721               b->title = dt_util_dstrcat(NULL, "%s", elems[1]);
1722 
1723             b->list_store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
1724             blocs = g_list_prepend(blocs, b);
1725           }
1726           // we add the new line
1727           GtkTreeIter iter;
1728           gtk_list_store_prepend(b->list_store, &iter);
1729           gchar *txt;
1730           // for views accels, no need to specify the view name, it's in the category title
1731           if(g_str_has_prefix(da->path, "<Darktable>/views/"))
1732             txt = da->translated_path + strlen(elems[0]) + strlen(elems[1]) + strlen(elems[2]) + 3;
1733           else
1734             txt = da->translated_path + strlen(elems[0]) + strlen(elems[1]) + 2;
1735           // for dynamic accel, we need to add the "+scroll"
1736 
1737           gchar *accel_label = gtk_accelerator_get_label(ak.accel_key, ak.accel_mods);
1738           gchar *atxt = dt_util_dstrcat(NULL, "%s", accel_label);
1739           g_free(accel_label);
1740           if(g_str_has_prefix(da->path, "<Darktable>/image operations/") && g_str_has_suffix(da->path, "/dynamic"))
1741             atxt = dt_util_dstrcat(atxt, _("+Scroll"));
1742           gtk_list_store_set(b->list_store, &iter, 0, atxt, 1, txt, -1);
1743           g_free(atxt);
1744           g_strfreev(elems);
1745         }
1746       }
1747     }
1748   }
1749 
1750   // we add the mouse actions too
1751   if(cv->mouse_actions)
1752   {
1753     _bloc_t *bm = (_bloc_t *)calloc(1, sizeof(_bloc_t));
1754     bm->base = NULL;
1755     bm->title = dt_util_dstrcat(NULL, _("mouse actions"));
1756     bm->list_store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
1757     blocs = g_list_prepend(blocs, bm);
1758 
1759     GSList *actions = cv->mouse_actions(cv);
1760     for(GSList *lm = actions; lm; lm = g_slist_next(lm))
1761     {
1762       dt_mouse_action_t *ma = (dt_mouse_action_t *)lm->data;
1763       if(ma)
1764       {
1765         GtkTreeIter iter;
1766         gtk_list_store_append(bm->list_store, &iter);
1767         gchar *atxt = _mouse_action_get_string(ma);
1768         gtk_list_store_set(bm->list_store, &iter, 0, atxt, 1, ma->name, -1);
1769         g_free(atxt);
1770       }
1771     }
1772     g_slist_free(actions); // we've already freed the action records, but still need to free the list itself
1773   }
1774 
1775   // now we create and insert the widget to display all accels by categories
1776   for(bl = blocs; bl; bl = g_list_next(bl))
1777   {
1778     const _bloc_t *bb = (_bloc_t *)bl->data;
1779     GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1780     // the title
1781     GtkWidget *lb = gtk_label_new(bb->title);
1782     context = gtk_widget_get_style_context(lb);
1783     gtk_style_context_add_class(context, "accels_window_cat_title");
1784     gtk_box_pack_start(GTK_BOX(box), lb, FALSE, FALSE, 0);
1785 
1786     // the list of accels
1787     GtkWidget *list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(bb->list_store));
1788     context = gtk_widget_get_style_context(list);
1789     gtk_style_context_add_class(context, "accels_window_list");
1790     GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
1791     GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("Accel"), renderer, "text", 0, NULL);
1792     gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);
1793     column = gtk_tree_view_column_new_with_attributes(_("Action"), renderer, "text", 1, NULL);
1794     gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);
1795 
1796     gtk_box_pack_start(GTK_BOX(box), list, FALSE, FALSE, 0);
1797 
1798     gtk_flow_box_insert(GTK_FLOW_BOX(vm->accels_window.flow_box), box, -1);
1799     g_free(bb->base);
1800     g_free(bb->title);
1801   }
1802   g_list_free_full(blocs, free);
1803 
1804   gtk_widget_show_all(vm->accels_window.flow_box);
1805 }
1806 
_audio_child_watch(GPid pid,gint status,gpointer data)1807 static void _audio_child_watch(GPid pid, gint status, gpointer data)
1808 {
1809   dt_view_manager_t *vm = (dt_view_manager_t *)data;
1810   vm->audio.audio_player_id = -1;
1811   g_spawn_close_pid(pid);
1812 }
1813 
dt_view_audio_start(dt_view_manager_t * vm,int imgid)1814 void dt_view_audio_start(dt_view_manager_t *vm, int imgid)
1815 {
1816   char *player = dt_conf_get_string("plugins/lighttable/audio_player");
1817   if(player && *player)
1818   {
1819     char *filename = dt_image_get_audio_path(imgid);
1820     if(filename)
1821     {
1822       char *argv[] = { player, filename, NULL };
1823       gboolean ret = g_spawn_async(NULL, argv, NULL,
1824                                    G_SPAWN_DO_NOT_REAP_CHILD
1825                                    | G_SPAWN_SEARCH_PATH
1826                                    | G_SPAWN_STDOUT_TO_DEV_NULL
1827                                    | G_SPAWN_STDERR_TO_DEV_NULL,
1828                                    NULL, NULL,
1829                                    &vm->audio.audio_player_pid, NULL);
1830 
1831       if(ret)
1832       {
1833         vm->audio.audio_player_id = imgid;
1834         vm->audio.audio_player_event_source
1835             = g_child_watch_add(vm->audio.audio_player_pid, (GChildWatchFunc)_audio_child_watch, vm);
1836       }
1837       else
1838         vm->audio.audio_player_id = -1;
1839 
1840       g_free(filename);
1841     }
1842   }
1843   g_free(player);
1844 }
1845 
dt_view_audio_stop(dt_view_manager_t * vm)1846 void dt_view_audio_stop(dt_view_manager_t *vm)
1847 {
1848   // make sure that the process didn't finish yet and that _audio_child_watch() hasn't run
1849   if(vm->audio.audio_player_id == -1)
1850     return;
1851 
1852   // we don't want to trigger the callback due to a possible race condition
1853   g_source_remove(vm->audio.audio_player_event_source);
1854 #ifdef _WIN32
1855 // TODO: add Windows code to actually kill the process
1856 #else  // _WIN32
1857   if(vm->audio.audio_player_id != -1)
1858   {
1859     if(getpgid(0) != getpgid(vm->audio.audio_player_pid))
1860       kill(-vm->audio.audio_player_pid, SIGKILL);
1861     else
1862       kill(vm->audio.audio_player_pid, SIGKILL);
1863   }
1864 #endif // _WIN32
1865   g_spawn_close_pid(vm->audio.audio_player_pid);
1866   vm->audio.audio_player_id = -1;
1867 }
1868 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
1869 // vim: shiftwidth=2 expandtab tabstop=2 cindent
1870 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1871