/*
    This file is part of darktable,
    Copyright (C) 2009-2025 darktable developers.

    darktable is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    darktable is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/extra_optimizations.h"

#include "views/view.h"
#include "bauhaus/bauhaus.h"
#include "common/collection.h"
#include "common/darktable.h"
#include "common/debug.h"
#include "common/focus_peaking.h"
#include "common/image_cache.h"
#include "common/mipmap_cache.h"
#include "common/module.h"
#include "common/selection.h"
#include "common/undo.h"
#include "common/usermanual_url.h"
#include "control/conf.h"
#include "control/control.h"
#include "develop/develop.h"
#include "dtgtk/button.h"
#include "dtgtk/expander.h"
#include "dtgtk/thumbtable.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "libs/lib.h"
#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif

#include <glib.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

static void dt_view_manager_load_modules(dt_view_manager_t *vm);
static int dt_view_load_module(void *v,
                               const char *libname,
                               const char *module_name);
static void dt_view_unload_module(dt_view_t *view);

void dt_view_manager_init(dt_view_manager_t *vm)
{
  /* prepare statements */
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "SELECT imgid FROM main.selected_images "
                              "WHERE imgid = ?1", -1, &vm->statements.is_selected, NULL);
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "DELETE FROM main.selected_images WHERE imgid = ?1",
                              -1, &vm->statements.delete_from_selected, NULL);
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "INSERT OR IGNORE INTO main.selected_images (imgid)"
                              " VALUES (?1)", -1,
                              &vm->statements.make_selected, NULL);
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "SELECT num FROM main.history WHERE imgid = ?1", -1,
                              &vm->statements.have_history, NULL);
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "SELECT color FROM main.color_labels WHERE imgid=?1",
                              -1, &vm->statements.get_color, NULL);
  DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
                              "SELECT id"
                              " FROM main.images"
                              " WHERE group_id = (SELECT group_id"
                              "                   FROM main.images"
                              "                   WHERE id=?1)"
                              "   AND id != ?2",
                              -1, &vm->statements.get_grouped, NULL);

  vm->module_toolbox = dt_gui_hbox();
  vm->view_toolbox = dt_gui_hbox();

  dt_view_manager_load_modules(vm);

  vm->current_view = NULL;
  vm->audio.audio_player_id = -1;
}

void dt_view_manager_cleanup(dt_view_manager_t *vm)
{
  for(GList *iter = vm->views;
      iter;
      iter = g_list_next(iter))
    dt_view_unload_module((dt_view_t *)iter->data);

  g_list_free_full(vm->views, free);
  vm->views = NULL;
}

const dt_view_t *dt_view_manager_get_current_view(const dt_view_manager_t *vm)
{
  return vm->current_view;
}

// we want a stable order of views, for example for viewswitcher.
// anything not hardcoded will be put alphabetically wrt. localised names.
static gint sort_views(gconstpointer a, gconstpointer b)
{
  static const char *view_order[] = {"lighttable", "darkroom"};
  static const int n_view_order = G_N_ELEMENTS(view_order);

  dt_view_t *av = (dt_view_t *)a;
  dt_view_t *bv = (dt_view_t *)b;
  const char *aname = av->name(av);
  const char *bname = bv->name(bv);
  int apos = n_view_order;
  int bpos = n_view_order;

  for(int i = 0; i < n_view_order; i++)
  {
    if(!strcmp(av->module_name, view_order[i]))
      apos = i;
    if(!strcmp(bv->module_name, view_order[i]))
      bpos = i;
  }

  // order will be zero iff apos == bpos which can only happen when
  // both views are not in view_order
  const int order = apos - bpos;
  return order ? order : strcmp(aname, bname);
}

static void dt_view_manager_load_modules(dt_view_manager_t *vm)
{
  vm->views = dt_module_load_modules("/views", sizeof(dt_view_t),
                                     dt_view_load_module, NULL, sort_views);
}

/* default flags for view which does not implement the flags() function */
static uint32_t default_flags()
{
  return 0;
}

/** load a view module */
static int dt_view_load_module(void *v,
                               const char *libname,
                               const char *module_name)
{
  dt_view_t *module = (dt_view_t *)v;
  g_strlcpy(module->module_name, module_name, sizeof(module->module_name));

#define INCLUDE_API_FROM_MODULE_LOAD "view_load_module"
#include "views/view_api.h"

  module->data = NULL;
  module->vscroll_size = module->vscroll_viewport_size = 1.0;
  module->hscroll_size = module->hscroll_viewport_size = 1.0;
  module->vscroll_pos = module->hscroll_pos = 0.0;
  module->height = module->width = 100; // set to non-insane defaults
                                        // before first
                                        // expose/configure.

#ifdef USE_LUA
  dt_lua_register_view(darktable.lua_state.state, module);
#endif

  if(module->init) module->init(module);

  if(darktable.gui)
  {
    module->actions = (dt_action_t){ DT_ACTION_TYPE_VIEW,
                                     module->module_name,
                                     module->name(module) };
    dt_action_insert_sorted(&darktable.control->actions_views, &module->actions);
    if(module->gui_init) module->gui_init(module);
  }

  return 0;
}

/** unload, cleanup */
static void dt_view_unload_module(dt_view_t *view)
{
  if(view->cleanup)
    view->cleanup(view);

  if(view->module)
    g_module_close(view->module);
}

static void _show_hide_toolbox_widget(GtkWidget *widget,
                                      gpointer data)
{
  gtk_widget_set_no_show_all(widget, TRUE);
  gtk_widget_set_visible(widget,
    GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "views")) & GPOINTER_TO_INT(data));
}

gboolean dt_view_manager_switch(dt_view_manager_t *vm,
                                const char *view_name)
{
  gboolean switching_to_none = *view_name == '\0';
  dt_view_t *new_view = NULL;

  if(!switching_to_none)
  {
    for(GList *iter = vm->views; iter; iter = g_list_next(iter))
    {
      dt_view_t *v = iter->data;
      if(!g_ascii_strcasecmp(v->module_name, view_name))
      {
        new_view = v;
        break;
      }
    }
    if(!new_view)
      return TRUE; // the requested view doesn't exist
  }

  return dt_view_manager_switch_by_view(vm, new_view);
}

gboolean dt_view_manager_switch_by_view(dt_view_manager_t *vm,
                                        const dt_view_t *nv)
{
  dt_view_t *old_view = vm->current_view;
  dt_view_t *new_view = (dt_view_t *)nv; // views belong to us, we can de-const them :-)

  // possibly avoid switch for as we are gimping
  if(old_view
      && new_view
      && dt_check_gimpmode("file")
      && dt_view_get_current() == DT_VIEW_DARKROOM)
    return FALSE;

  dt_set_backthumb_time(0.0);

  // destroy old module list

  /* clear the undo list, for now we do this unconditionally. At some
     point we will probably want to clear only part of the undo
     list. This should probably done with a view proxy routine
     returning the type of undo to remove. */
  dt_undo_clear(darktable.undo, DT_UNDO_ALL);

  /* Special case when entering nothing (just before leaving dt) */
  if(!new_view)
  {
    if(old_view)
    {
      /* leave the current view*/
      if(old_view->leave) old_view->leave(old_view);

      /* iterator plugins and cleanup plugins in current view */
      for(GList *iter = darktable.lib->plugins;
          iter;
          iter = g_list_next(iter))
      {
        dt_lib_module_t *plugin = iter->data;

        /* does this module belong to current view ?*/
        if(dt_lib_is_visible_in_view(plugin, old_view))
        {
          if(plugin->view_leave) plugin->view_leave(plugin, old_view, NULL);
          plugin->gui_cleanup(plugin);
          plugin->data = NULL;
          plugin->widget = NULL;
        }
      }
    }

    /* remove all widgets in all containers */
    for(int l = 0; l < DT_UI_CONTAINER_SIZE; l++)
      dt_ui_container_destroy_children(darktable.gui->ui, l);
    vm->current_view = NULL;

    /* remove sticky accels window */
    if(vm->accels_window.window)
      dt_view_accels_hide(vm);
    return FALSE;
  }

  // invariant: new_view != NULL after this point
  assert(new_view != NULL);

  if(new_view->try_enter)
  {
    const gboolean error = new_view->try_enter(new_view);
    if(error)
    {
      DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_VIEW_CANNOT_CHANGE,
                              old_view, new_view);
      return error;
    }
  }

  // show we are busy changing views
  dt_control_change_cursor("wait");
  gdk_display_sync(gdk_display_get_default());

  /* cleanup current view before initialization of new  */
  if(old_view)
  {
    /* leave current view */
    if(new_view != old_view && old_view->leave)
      old_view->leave(old_view);

    /* iterator plugins and cleanup plugins in current view */
    for(GList *iter = darktable.lib->plugins;
        iter;
        iter = g_list_next(iter))
    {
      dt_lib_module_t *plugin = iter->data;

      if(new_view == old_view && !plugin->expandable(plugin)) continue;

      /* does this module belong to current view ?*/
      GtkWidget *ppw = plugin->expander ? plugin->expander : plugin->widget;
      if(ppw && gtk_widget_get_ancestor(ppw, GTK_TYPE_WINDOW))
      {
        if(plugin->view_leave)
          plugin->view_leave(plugin, old_view, new_view);

        /*
          When expanders get destroyed, they destroy the child
          so remove the child before that
          */
        if(plugin->widget)
          gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(plugin->widget)), plugin->widget);
        if(plugin->expander)
          gtk_widget_destroy(plugin->expander);
      }
      plugin->expander = NULL;
    }
  }

  /* change current view to the new view */

  /*
     Race Condition

     the current view is set to the new view prior to
     initializing.  Plugins are initialized according to
     the current view, so it must be set prior.

     If a Lua script tries to register a lib, it checks the
     current view to see if is lighttable.  If the current
     view is lighttable then the lib tries to install.

     If the current view is set to lighttable, but the
     view hasn't completely initialized, and Lua attempts
     to install the lib the result will be a hang.

     There is a work around in the Lua initialization code,
     data/luarc, to check that the initialization is complete
     and set a flag, darktable_gui_safe, when it is safe to
     register libs
  */

  vm->current_view = new_view;

  dt_view_type_flags_t view_type = new_view->view(new_view);

  if(new_view != old_view) // implement preference toggle only on view change
    dt_ui_container_swap_left_right(darktable.gui->ui, view_type == DT_VIEW_DARKROOM
                                    && dt_conf_get_bool("plugins/darkroom/panel_swap"));

  /* restore visible stat of panels for the new view */
  dt_ui_restore_panels(darktable.gui->ui);

  /* lets add plugins related to new view into panels.  this has to be
   * done in reverse order to have the lowest position at the
   * bottom! */

  // adjust order per view in case user made changes
  darktable.lib->plugins = g_list_sort(darktable.lib->plugins, dt_lib_sort_plugins);

  for(GList *iter = g_list_last(darktable.lib->plugins);
      iter;
      iter = g_list_previous(iter))
  {
    dt_lib_module_t *plugin = iter->data;
    GtkWidget *w = plugin->widget;

    if(plugin->expandable(plugin))
    {
      if(!dt_lib_is_visible_in_view(plugin, new_view))
        continue;

      w = dt_lib_gui_get_expander(plugin);

      /* set expanded if last mode was that */
      char var[1024];
      snprintf(var, sizeof(var), "plugins/%s/%s/expanded",
               new_view->module_name, plugin->plugin_name);
      const gboolean expanded = dt_conf_get_bool(var);
      dt_lib_gui_set_expanded(plugin, expanded);
      dt_lib_set_visible(plugin, TRUE);
    }
    else if(new_view != old_view && plugin->views(plugin) & view_type)
    {
      dt_lib_gui_get_expander(plugin); // connect modulegroups presets button

      if(dt_lib_is_visible(plugin))
        gtk_widget_show_all(plugin->widget);
      else
        gtk_widget_hide(plugin->widget);
    }
    else
      continue;

    if(plugin->view_enter)
      plugin->view_enter(plugin, old_view, new_view);

    dt_gui_add_help_link(w, plugin->plugin_name);
    // some plugins help links depend on the view
    if(!strcmp(plugin->plugin_name,"module_toolbox")
      || !strcmp(plugin->plugin_name,"view_toolbox"))
    {
      gtk_container_foreach(GTK_CONTAINER(w), _show_hide_toolbox_widget, GINT_TO_POINTER(view_type));
      if(view_type == DT_VIEW_LIGHTTABLE)
        dt_gui_add_help_link(w, "lighttable_mode");
      if(view_type == DT_VIEW_DARKROOM)
        dt_gui_add_help_link(w, "darkroom_bottom_panel");
    }

    /* add module to its container */
    dt_ui_container_add_widget(darktable.gui->ui, dt_lib_get_container(plugin), w);
  }

  darktable.lib->gui_module = NULL;

  /* enter view. crucially, do this before initing the plugins below,
      as e.g. modulegroups requires the dr stuff to be inited. */
  if(new_view != old_view && new_view->enter)
    new_view->enter(new_view);

  /* update the scrollbars */
  dt_ui_update_scrollbars(darktable.gui->ui);

  dt_shortcuts_select_view(view_type);

  /* update sticky accels window */
  if(vm->accels_window.window && vm->accels_window.sticky) dt_view_accels_refresh(vm);

  /* raise view changed signal */
  DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_VIEWMANAGER_VIEW_CHANGED, old_view, new_view);

  // update log visibility
  DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_CONTROL_LOG_REDRAW);

  // update toast visibility
  DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_CONTROL_TOAST_REDRAW);

  // reset the cursor to the default one
  dt_control_change_cursor("default");

  return FALSE;
}

const char *dt_view_manager_name(const dt_view_manager_t *vm)
{
  if(!vm->current_view)
    return "";
  if(vm->current_view->name)
    return vm->current_view->name(vm->current_view);
  else
    return vm->current_view->module_name;
}

void dt_view_manager_expose(dt_view_manager_t *vm,
                            cairo_t *cr,
                            const int32_t width,
                            const int32_t height,
                            const int32_t pointerx,
                            const int32_t pointery)
{
  if(!vm->current_view)
  {
    dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_BG);
    cairo_paint(cr);
    return;
  }
  vm->current_view->width = width;
  vm->current_view->height = height;

  if(vm->current_view->expose)
  {
    /* expose the view */
    cairo_rectangle(cr, 0, 0, vm->current_view->width, vm->current_view->height);
    cairo_clip(cr);
    cairo_new_path(cr);
    cairo_save(cr);
    float px = pointerx, py = pointery;
    if(pointery > vm->current_view->height)
    {
      px = 10000.0;
      py = -1.0;
    }
    vm->current_view->expose(vm->current_view, cr,
                             vm->current_view->width,
                             vm->current_view->height, px, py);

    cairo_restore(cr);
    /* expose plugins */
    for(const GList *plugins = g_list_last(darktable.lib->plugins);
        plugins;
        plugins = g_list_previous(plugins))
    {
      dt_lib_module_t *plugin = plugins->data;

      /* does this module belong to current view ?*/
      if(plugin->gui_post_expose
         && dt_lib_is_visible_in_view(plugin, vm->current_view))
        plugin->gui_post_expose(plugin, cr,
                                vm->current_view->width,
                                vm->current_view->height, px, py);
    }
  }
}

void dt_view_manager_reset(dt_view_manager_t *vm)
{
  if(!vm->current_view)
    return;
  if(vm->current_view->reset)
    vm->current_view->reset(vm->current_view);
}

void dt_view_manager_mouse_leave(dt_view_manager_t *vm)
{
  if(!vm->current_view) return;
  dt_view_t *v = vm->current_view;

  /* lets check if any plugins want to handle mouse move */
  gboolean handled = FALSE;
  for(const GList *plugins = g_list_last(darktable.lib->plugins);
      plugins;
      plugins = g_list_previous(plugins))
  {
    dt_lib_module_t *plugin = plugins->data;

    /* does this module belong to current view ?*/
    if(plugin->mouse_leave && dt_lib_is_visible_in_view(plugin, v))
      if(plugin->mouse_leave(plugin))
        handled = TRUE;
  }

  /* if not handled by any plugin let pass to view handler*/
  if(!handled && v->mouse_leave)
    v->mouse_leave(v);
}

void dt_view_manager_mouse_enter(dt_view_manager_t *vm)
{
  if(!vm->current_view)
    return;
  if(vm->current_view->mouse_enter)
    vm->current_view->mouse_enter(vm->current_view);
}

void dt_view_manager_mouse_moved(dt_view_manager_t *vm,
                                 const double x,
                                 const double y,
                                 const double pressure,
                                 const int which)
{
  if(!vm->current_view) return;
  dt_view_t *v = vm->current_view;

  /* lets check if any plugins want to handle mouse move */
  gboolean handled = FALSE;
  for(const GList *plugins = g_list_last(darktable.lib->plugins);
      plugins;
      plugins = g_list_previous(plugins))
  {
    dt_lib_module_t *plugin = plugins->data;

    /* does this module belong to current view ?*/
    if(plugin->mouse_moved && dt_lib_is_visible_in_view(plugin, v))
      if(plugin->mouse_moved(plugin, x, y, pressure, which))
        handled = TRUE;
  }

  /* if not handled by any plugin let pass to view handler*/
  if(!handled && v->mouse_moved)
    v->mouse_moved(v, x, y, pressure, which);
}

int dt_view_manager_button_released(dt_view_manager_t *vm,
                                    const double x,
                                    const double y,
                                    const int which,
                                    const uint32_t state)
{
  if(!vm->current_view)
    return 0;
  dt_view_t *v = vm->current_view;

  /* lets check if any plugins want to handle button press */
  gboolean handled = FALSE;
  for(const GList *plugins = g_list_last(darktable.lib->plugins);
      plugins;
      plugins = g_list_previous(plugins))
  {
    dt_lib_module_t *plugin = plugins->data;

    /* does this module belong to current view ?*/
    if(plugin->button_released && dt_lib_is_visible_in_view(plugin, v))
      if(plugin->button_released(plugin, x, y, which, state))
        handled = TRUE;
  }

  if(handled)
    return 1;
  /* if not handled by any plugin let pass to view handler*/
  else if(v->button_released)
    v->button_released(v, x, y, which, state);

  return 0;
}

int dt_view_manager_button_pressed(dt_view_manager_t *vm,
                                   const double x,
                                   const double y,
                                   const double pressure,
                                   const int which,
                                   const int type,
                                   const uint32_t state)
{
  if(!vm->current_view)
    return 0;
  dt_view_t *v = vm->current_view;

  /* lets check if any plugins want to handle button press */
  gboolean handled = FALSE;
  for(const GList *plugins = g_list_last(darktable.lib->plugins);
      plugins && !handled;
      plugins = g_list_previous(plugins))
  {
    dt_lib_module_t *plugin = plugins->data;

    /* does this module belong to current view ?*/
    if(plugin->button_pressed && dt_lib_is_visible_in_view(plugin, v))
      if(plugin->button_pressed(plugin, x, y, pressure, which, type, state))
        handled = TRUE;
  }

  if(handled)
    return 1;
  /* if not handled by any plugin let pass to view handler*/
  else if(v->button_pressed)
    return v->button_pressed(v, x, y, pressure, which, type, state);

  return 0;
}

void dt_view_manager_configure(dt_view_manager_t *vm,
                               const int width,
                               const int height)
{
  for(GList *iter = vm->views; iter; iter = g_list_next(iter))
  {
    // this is necessary for all
    dt_view_t *v = iter->data;
    v->width = width;
    v->height = height;
    if(v->configure)
      v->configure(v, width, height);
  }
}

void dt_view_manager_scrolled(dt_view_manager_t *vm,
                              const double x,
                              const double y,
                              const int up,
                              const int state)
{
  if(!vm->current_view)
    return;
  if(vm->current_view->scrolled)
    vm->current_view->scrolled(vm->current_view, x, y, up, state);
}

gboolean dt_view_manager_gesture_pan(dt_view_manager_t *vm,
                                     const double x,
                                     const double y,
                                     const double dx,
                                     const double dy,
                                     const int state)
{
  if(!vm->current_view)
  {
    return FALSE;
  }
  else if(vm->current_view->gesture_pan)
  {
    return vm->current_view->gesture_pan(vm->current_view, x, y, dx, dy, state);
  }
  else
  {
    return FALSE;
  }
}

gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm,
                                       const double x,
                                       const double y,
                                       const double dx,
                                       const double dy,
                                       const int phase,
                                       const double scale,
                                       const int state)
{
  if(!vm->current_view)
  {
    return FALSE;
  }
  else if(vm->current_view->gesture_pinch)
  {
    return vm->current_view->gesture_pinch(vm->current_view, x, y, dx, dy, phase, scale, state);
  }
  else
  {
    return FALSE;
  }
}

void dt_view_manager_scrollbar_changed(dt_view_manager_t *vm,
                                       const double x,
                                       const double y)
{
  if(!vm->current_view)
    return;
  if(vm->current_view->scrollbar_changed)
    vm->current_view->scrollbar_changed(vm->current_view, x, y);
}

void dt_view_set_scrollbar(dt_view_t *view,
                           const float hpos,
                           const float hlower,
                           const float hsize,
                           const float hwinsize,
                           const float vpos,
                           const float vlower,
                           const float vsize,
                           const float vwinsize)
{
  view->vscroll_pos = vpos;
  view->vscroll_lower = vlower;
  view->vscroll_size = vsize;
  view->vscroll_viewport_size = vwinsize;
  view->hscroll_pos = hpos;
  view->hscroll_lower = hlower;
  view->hscroll_size = hsize;
  view->hscroll_viewport_size = hwinsize;

  dt_gui_widgets_t *widgets = &darktable.gui->widgets;
  gtk_widget_queue_draw(widgets->left_border);
  gtk_widget_queue_draw(widgets->right_border);
  gtk_widget_queue_draw(widgets->bottom_border);
  gtk_widget_queue_draw(widgets->top_border);

  dt_ui_update_scrollbars(darktable.gui->ui);
}

dt_view_surface_value_t dt_view_image_get_surface(const dt_imgid_t imgid,
                                                  const int32_t width,
                                                  const int32_t height,
                                                  cairo_surface_t **surface,
                                                  const gboolean quality)
{
  return dt_view_image_get_surface_cached(imgid, width, height, surface, quality, NULL, NULL);
}

dt_view_surface_value_t dt_view_image_get_surface_cached(const dt_imgid_t imgid,
                                                         const int32_t width,
                                                         const int32_t height,
                                                         cairo_surface_t **surface,
                                                         const gboolean quality,
                                                         cairo_surface_t **mip_cache,
                                                         dt_mipmap_size_t *mip_cache_level)
{
  const double tt = dt_get_debug_wtime();

  dt_view_surface_value_t ret = DT_VIEW_SURFACE_KO;
  // if surface not null, clean it up
  if(*surface
     && cairo_surface_get_reference_count(*surface) > 0)
    cairo_surface_destroy(*surface);

  *surface = NULL;

  // get mipmap cache image
  const int32_t mipwidth = width * darktable.gui->ppd;
  const int32_t mipheight = height * darktable.gui->ppd;
  dt_mipmap_size_t mip = dt_mipmap_cache_get_matching_size(mipwidth, mipheight);

  // Fast path: if the caller provides a cached native-resolution surface for
  // this exact mip level, skip the expensive mipmap fetch + color conversion
  // and just rescale the existing cached surface.
  if(mip_cache && *mip_cache && mip_cache_level && *mip_cache_level == mip
     && cairo_surface_status(*mip_cache) == CAIRO_STATUS_SUCCESS)
  {
    const int32_t cached_wd = cairo_image_surface_get_width(*mip_cache);
    const int32_t cached_ht = cairo_image_surface_get_height(*mip_cache);
    if(cached_wd > 0 && cached_ht > 0)
    {
      float scale = fminf(width / (float)cached_wd,
                          height / (float)cached_ht) * darktable.gui->ppd_thb;
      const int32_t img_width  = roundf(cached_wd * scale);
      const int32_t img_height = roundf(cached_ht * scale);
      scale = fmaxf(img_width / (float)cached_wd, img_height / (float)cached_ht);
      *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, img_width, img_height);
      if(*surface && cairo_surface_status(*surface) == CAIRO_STATUS_SUCCESS)
      {
        cairo_t *cr = cairo_create(*surface);
        cairo_scale(cr, scale, scale);
        cairo_set_source_surface(cr, *mip_cache, 0, 0);
        // Match the filter logic of the full path below: at a 1:1 mapping
        // (scale ~ 1.0, i.e. a true 100% view) use NEAREST so we display the
        // cached native pixels exactly, pixel-for-pixel, rather than softening
        // them with interpolation.  The cached surface always corresponds to
        // the exact mip (we only cache when mip == buf.size && buf_wd > 30), so
        // the skull/fallback branches of the full path can't apply here.
        if(fabsf(scale - 1.0f) < 0.01f)
          cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
        else
          cairo_pattern_set_filter(cairo_get_source(cr),
                                   ((darktable.gui->filter_image == CAIRO_FILTER_FAST)
                                    && quality)
                                   ? CAIRO_FILTER_GOOD
                                   : darktable.gui->filter_image);
        cairo_paint(cr);
        cairo_destroy(cr);
        return DT_VIEW_SURFACE_OK;
      }
      // Surface creation failed; fall through to the full path below.
      if(*surface && cairo_surface_get_reference_count(*surface) > 0)
        cairo_surface_destroy(*surface);
      *surface = NULL;
    }
  }

  // if needed, we load the mimap buffer
  dt_mipmap_buffer_t buf;
  dt_mipmap_cache_get(&buf, imgid, mip, DT_MIPMAP_BEST_EFFORT, 'r');

  const int32_t buf_wd = buf.width;
  const int32_t buf_ht = buf.height;

  dt_print(DT_DEBUG_LIGHTTABLE,
      "dt_view_image_get_surface  id %i, dots %ix%i -> mip %ix%i, found %ix%i",
      imgid, mipwidth, mipheight, darktable.mipmap_cache->max_width[mip], darktable.mipmap_cache->max_height[mip], buf_wd, buf_ht);

  // no image is available at the moment as we didn't get buffer data
  if(!buf.buf)
  {
    dt_mipmap_cache_release(&buf);
    return DT_VIEW_SURFACE_KO;
  }

  // so we create a new image surface to return
  float scale = fminf(width / (float)buf_wd,
                      height / (float)buf_ht) * darktable.gui->ppd_thb;
  const int32_t img_width = roundf(buf_wd * scale);
  const int32_t img_height = roundf(buf_ht * scale);
  // due to the forced rounding above, we need to recompute scaling
  scale = fmaxf(img_width / (float)buf_wd, img_height / (float)buf_ht);
  *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, img_width, img_height);

  // we transfer cached image on a cairo_surface (with colorspace transform if needed)
  cairo_surface_t *tmp_surface = NULL;
  uint8_t *rgbbuf = calloc((size_t)buf_wd * buf_ht * 4, sizeof(uint8_t));
  if(rgbbuf)
  {
    gboolean have_lock = FALSE;
    cmsHTRANSFORM transform = NULL;

    if(dt_conf_get_bool("cache_color_managed"))
    {
      pthread_rwlock_rdlock(&darktable.color_profiles->xprofile_lock);
      have_lock = TRUE;

      // we only color manage when a thumbnail is sRGB or
      // AdobeRGB. everything else just gets dumped to the screen
      if(buf.color_space == DT_COLORSPACE_SRGB
         && darktable.color_profiles->transform_srgb_to_display)
      {
        transform = darktable.color_profiles->transform_srgb_to_display;
      }
      else if(buf.color_space == DT_COLORSPACE_ADOBERGB
              && darktable.color_profiles->transform_adobe_rgb_to_display)
      {
        transform = darktable.color_profiles->transform_adobe_rgb_to_display;
      }
      else
      {
        pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock);
        have_lock = FALSE;
        if(buf.color_space == DT_COLORSPACE_NONE)
        {
          dt_print(DT_DEBUG_ALWAYS,
                  "oops, there seems to be a code path not setting the"
                  " color space of thumbnails!");
        }
        else if(buf.color_space != DT_COLORSPACE_DISPLAY
                && buf.color_space != DT_COLORSPACE_DISPLAY2)
        {
          dt_print(DT_DEBUG_ALWAYS,
                  "oops, there seems to be a code path setting an"
                  " unhandled color space of thumbnails (%s)!",
                  dt_colorspaces_get_name(buf.color_space, "from file"));
        }
      }
    }

    DT_OMP_FOR()
    for(int i = 0; i < buf.height; i++)
    {
      const uint8_t *in = buf.buf + i * buf.width * 4;
      uint8_t *out = rgbbuf + i * buf.width * 4;

      if(transform)
      {
        cmsDoTransform(transform, in, out, buf.width);
      }
      else
      {
        for(int j = 0; j < buf.width; j++, in += 4, out += 4)
        {
          out[0] = in[2];
          out[1] = in[1];
          out[2] = in[0];
        }
      }
    }
    if(have_lock)
      pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock);

    const int32_t stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, buf_wd);
    tmp_surface = cairo_image_surface_create_for_data(rgbbuf,
                                                      CAIRO_FORMAT_RGB24,
                                                      buf_wd, buf_ht, stride);
  }

  // draw the image scaled:
  if(tmp_surface)
  {
    cairo_t *cr = cairo_create(*surface);
    cairo_scale(cr, scale, scale);

    cairo_set_source_surface(cr, tmp_surface, 0, 0);
    // set filter no nearest: in skull/error mode, we want to see big
    // pixels.  in 1 iir mode for the right mip, we want to see
    // exactly what the pipe gave us, 1:1 pixel for pixel.  in
    // between, filtering just makes stuff go unsharp.
    if((buf_wd <= 30 && buf_ht <= 30)
       || fabsf(scale - 1.0f) < 0.01f)
      cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
    else if(mip != buf.size)
      cairo_pattern_set_filter(cairo_get_source(cr),
                               CAIRO_FILTER_FAST); // not the right
                                                   // size, so we
                                                   // scale as fast a
                                                   // possible
    else
      cairo_pattern_set_filter(cairo_get_source(cr),
                               ((darktable.gui->filter_image == CAIRO_FILTER_FAST)
                                && quality)
                               ? CAIRO_FILTER_GOOD
                               : darktable.gui->filter_image);

    cairo_paint(cr);
    /* from focus_peaking.h
       static inline void dt_focuspeaking(cairo_t *cr, int width, int height,
                                       uint8_t *const restrict image,
                                       const int buf_width, const int buf_height)

       The current implementation assumes the data at image is
       organized as a rectangle without a stride, So we pass the raw
       data to be processed, this is more data but correct.
    */
    if(darktable.gui->show_focus_peaking && mip == buf.size)
      dt_focuspeaking(cr, buf_wd, buf_ht, rgbbuf);

    cairo_surface_destroy(tmp_surface);
    cairo_destroy(cr);

    // Cache the native-resolution surface so future zoom events at the
    // same mip level can skip the expensive mipmap fetch + color conversion.
    // Only cache when we have the exact mip (not a fallback standin) and
    // the image is not a skull/error placeholder.
    if(mip_cache && mip == buf.size && buf_wd > 30)
    {
      if(*mip_cache && cairo_surface_get_reference_count(*mip_cache) > 0)
        cairo_surface_destroy(*mip_cache);
      *mip_cache = cairo_image_surface_create(CAIRO_FORMAT_RGB24, buf_wd, buf_ht);
      if(*mip_cache && cairo_surface_status(*mip_cache) == CAIRO_STATUS_SUCCESS)
      {
        // Reconstruct the tmp_surface content from rgbbuf into the cached surface.
        // We use create_for_data here (cheap, no extra alloc) since rgbbuf is still
        // live at this point.
        const int32_t cache_stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, buf_wd);
        cairo_surface_t *src = cairo_image_surface_create_for_data(
            rgbbuf, CAIRO_FORMAT_RGB24, buf_wd, buf_ht, cache_stride);
        if(src && cairo_surface_status(src) == CAIRO_STATUS_SUCCESS)
        {
          cairo_t *cr_cache = cairo_create(*mip_cache);
          cairo_set_source_surface(cr_cache, src, 0, 0);
          cairo_pattern_set_filter(cairo_get_source(cr_cache), CAIRO_FILTER_NEAREST);
          cairo_paint(cr_cache);
          cairo_destroy(cr_cache);
          cairo_surface_destroy(src);
        }
        else
        {
          if(src) cairo_surface_destroy(src);
          cairo_surface_destroy(*mip_cache);
          *mip_cache = NULL;
        }
      }
      if(mip_cache_level) *mip_cache_level = mip;
    }
  }

  // we consider skull/error as ok as the image hasn't to be reload
  if(buf_wd <= 30 && buf_ht <= 30)
    ret = DT_VIEW_SURFACE_OK;
  else if(mip != buf.size)
    ret = DT_VIEW_SURFACE_SMALLER;
  else
    ret = DT_VIEW_SURFACE_OK;

  dt_mipmap_cache_release(&buf);
  if(rgbbuf) free(rgbbuf);

  // logs
  if(darktable.unmuted & DT_DEBUG_PERF)
    dt_print(DT_DEBUG_LIGHTTABLE | DT_DEBUG_PERF,
             "got surface  %ix%i created in %0.04f sec",
             img_width, img_height, dt_get_wtime() - tt);

  // we consider skull as ok as the image hasn't to be reload
  if(ret != DT_VIEW_SURFACE_OK)
    dt_print(DT_DEBUG_LIGHTTABLE,
      "dt_view_image_get_surface  ID=%i with surface problem %s%s%s",
      imgid,
      tmp_surface ? "" : "no tmp_surface, ",
      ret == DT_VIEW_SURFACE_SMALLER ? "DT_VIEW_SURFACE_SMALLER" : "",
      ret == DT_VIEW_SURFACE_KO ? "DT_VIEW_SURFACE_KO" : "");
  return ret;
}

char* dt_view_extend_modes_str(const char * name,
                               const gboolean is_hdr,
                               const gboolean is_bw,
                               const gboolean is_bw_flow)
{
  char* upcase = g_ascii_strup(name, -1);  // extension in capital
                                           // letters to avoid
                                           // character descenders
  // convert to canonical format extension
  if(0 == g_ascii_strcasecmp(upcase, "JPG"))
  {
      gchar* canonical = g_strdup("JPEG");
      g_free(upcase);
      upcase = canonical;
  }
  else if(0 == g_ascii_strcasecmp(upcase, "HDR"))
  {
      gchar* canonical = g_strdup("RGBE");
      g_free(upcase);
      upcase = canonical;
  }
  else if(0 == g_ascii_strcasecmp(upcase, "TIF"))
  {
      gchar* canonical = g_strdup("TIFF");
      g_free(upcase);
      upcase = canonical;
  }

  if(is_hdr)
  {
    gchar* fullname = g_strdup_printf("%s HDR", upcase);
    g_free(upcase);
    upcase = fullname;
  }
  if(is_bw)
  {
    gchar* fullname = g_strdup_printf("%s B&W", upcase);
    g_free(upcase);
    upcase = fullname;
    if(!is_bw_flow)
    {
      fullname = g_strdup_printf("%s-", upcase);
      g_free(upcase);
      upcase = fullname;
    }
  }

  return upcase;
}

/**
 * \brief Set the selection bit to a given value for the specified image
 * \param[in] imgid The image id
 * \param[in] value The boolean value for the bit
 */
void dt_view_set_selection(const dt_imgid_t imgid,
                           const int value)
{
  /* clear and reset statement */
  DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
  DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);

  /* setup statement and iterate over rows */
  DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, imgid);

  if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW)
  {
    if(!value)
    {
      /* Value is set and should be unset; get rid of it */

      /* clear and reset statement */
      DT_DEBUG_SQLITE3_CLEAR_BINDINGS
        (darktable.view_manager->statements.delete_from_selected);
      DT_DEBUG_SQLITE3_RESET
        (darktable.view_manager->statements.delete_from_selected);

      /* setup statement and execute */
      DT_DEBUG_SQLITE3_BIND_INT
        (darktable.view_manager->statements.delete_from_selected, 1, imgid);
      sqlite3_step(darktable.view_manager->statements.delete_from_selected);
    }
  }
  else if(value)
  {
    /* Select bit is unset and should be set; add it */

    /* clear and reset statement */
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.make_selected);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.make_selected);

    /* setup statement and execute */
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.make_selected, 1, imgid);
    sqlite3_step(darktable.view_manager->statements.make_selected);
  }
}

/**
 * \brief Toggle the selection bit in the database for the specified image
 * \param[in] imgid The image id
 */
void dt_view_toggle_selection(const dt_imgid_t imgid)
{
  /* clear and reset statement */
  DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.is_selected);
  DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.is_selected);

  /* setup statement and iterate over rows */
  DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.is_selected, 1, imgid);
  if(sqlite3_step(darktable.view_manager->statements.is_selected) == SQLITE_ROW)
  {
    /* clear and reset statement */
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS
      (darktable.view_manager->statements.delete_from_selected);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.delete_from_selected);

    /* setup statement and execute */
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.delete_from_selected,
                              1, imgid);
    sqlite3_step(darktable.view_manager->statements.delete_from_selected);
  }
  else
  {
    /* clear and reset statement */
    DT_DEBUG_SQLITE3_CLEAR_BINDINGS(darktable.view_manager->statements.make_selected);
    DT_DEBUG_SQLITE3_RESET(darktable.view_manager->statements.make_selected);

    /* setup statement and execute */
    DT_DEBUG_SQLITE3_BIND_INT(darktable.view_manager->statements.make_selected, 1, imgid);
    sqlite3_step(darktable.view_manager->statements.make_selected);
  }
}

dt_view_type_flags_t dt_view_get_current(void)
{
  if(!darktable.view_manager) return DT_VIEW_LIGHTTABLE;

  const dt_view_t *cv = dt_view_manager_get_current_view(darktable.view_manager);
  if(!cv || ! cv->view) return DT_VIEW_LIGHTTABLE;

  return cv->view(cv);
}

/**
 * \brief Reset filter
 */
void dt_view_filtering_reset(const dt_view_manager_t *vm,
                             const gboolean smart_filter)
{
  if(vm->proxy.module_filtering.module && vm->proxy.module_filtering.reset_filter)
    vm->proxy.module_filtering.reset_filter
      (vm->proxy.module_filtering.module, smart_filter);
}

void dt_view_filtering_show_pref_menu(const dt_view_manager_t *vm,
                                      GtkWidget *bt)
{
  if(vm->proxy.module_filtering.module && vm->proxy.module_filtering.show_pref_menu)
    vm->proxy.module_filtering.show_pref_menu(vm->proxy.module_filtering.module, bt);
}

GtkWidget *dt_view_filter_get_filters_box(const dt_view_manager_t *vm)
{
  if(vm->proxy.filter.module && vm->proxy.filter.get_filter_box)
    return vm->proxy.filter.get_filter_box(vm->proxy.filter.module);
  return NULL;
}
GtkWidget *dt_view_filter_get_sort_box(const dt_view_manager_t *vm)
{
  if(vm->proxy.filter.module && vm->proxy.filter.get_sort_box)
    return vm->proxy.filter.get_sort_box(vm->proxy.filter.module);
  return NULL;
}

GtkWidget *dt_view_filter_get_count(const dt_view_manager_t *vm)
{
  if(vm && vm->proxy.filter.module && vm->proxy.filter.get_count)
    return vm->proxy.filter.get_count(vm->proxy.filter.module);
  return NULL;
}

void dt_view_active_images_reset(const gboolean raise)
{
  if(!darktable.view_manager->active_images)
    return;
  g_slist_free(darktable.view_manager->active_images);
  darktable.view_manager->active_images = NULL;

  if(raise)
    DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
}
void dt_view_active_images_add(const dt_imgid_t imgid, const gboolean raise)
{
  darktable.view_manager->active_images =
    g_slist_append(darktable.view_manager->active_images, GINT_TO_POINTER(imgid));
  if(raise)
    DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_ACTIVE_IMAGES_CHANGE);
}
GSList *dt_view_active_images_get()
{
  return darktable.view_manager->active_images;
}

void dt_view_manager_view_toolbox_add(dt_view_manager_t *vm,
                                      GtkWidget *tool,
                                      const dt_view_type_flags_t views)
{
  g_object_set_data(G_OBJECT(tool), "views", GINT_TO_POINTER(views));
  dt_gui_box_add(vm->view_toolbox, tool);
}

void dt_view_manager_module_toolbox_add(dt_view_manager_t *vm,
                                        GtkWidget *tool,
                                        const dt_view_type_flags_t views)
{
  g_object_set_data(G_OBJECT(tool), "views", GINT_TO_POINTER(views));
  dt_gui_box_add(vm->module_toolbox, tool);
}

dt_darkroom_layout_t dt_view_darkroom_get_layout(const dt_view_manager_t *vm)
{
  if(vm->proxy.darkroom.view)
    return vm->proxy.darkroom.get_layout(vm->proxy.darkroom.view);
  else
    return DT_DARKROOM_LAYOUT_EDITING;
}

void dt_view_lighttable_set_zoom(dt_view_manager_t *vm,
                                 const gint zoom)
{
  if(vm->proxy.lighttable.module)
    vm->proxy.lighttable.set_zoom(vm->proxy.lighttable.module, zoom);
}

gint dt_view_lighttable_get_zoom(const dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    return vm->proxy.lighttable.get_zoom(vm->proxy.lighttable.module);
  else
    return 10;
}

void dt_view_lighttable_culling_preview_refresh(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module
     && vm->proxy.lighttable.culling_preview_refresh)
    vm->proxy.lighttable.culling_preview_refresh(vm->proxy.lighttable.view);
}

void dt_view_lighttable_culling_preview_reload_overlays(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module
    && vm->proxy.lighttable.culling_preview_reload_overlays)
    vm->proxy.lighttable.culling_preview_reload_overlays(vm->proxy.lighttable.view);
}

dt_lighttable_layout_t dt_view_lighttable_get_layout(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    return vm->proxy.lighttable.get_layout(vm->proxy.lighttable.module);
  else
    return DT_LIGHTTABLE_LAYOUT_FILEMANAGER;
}

void dt_view_lighttable_update_layout_buttons(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    vm->proxy.lighttable.update_layout_btn(vm->proxy.lighttable.module);
}

dt_lighttable_culling_restriction_t dt_view_lighttable_culling_initial_restriction(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    return vm->proxy.lighttable.get_culling_initial_restriction(vm->proxy.lighttable.module);
  else
    return FALSE;
}

dt_lighttable_culling_restriction_t dt_view_lighttable_culling_restricted_state(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.view)
    return vm->proxy.lighttable.get_culling_restricted_state(vm->proxy.lighttable.view);
  else
    return FALSE;
}

void dt_view_lighttable_set_culling_restricted_state(dt_view_manager_t *vm,
                                                     const dt_lighttable_culling_restriction_t state)
{
  if(vm->proxy.lighttable.view)
    vm->proxy.lighttable.set_culling_restricted_state(vm->proxy.lighttable.view, state);
}

dt_imgid_t dt_view_lighttable_get_culling_selection(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    return vm->proxy.lighttable.get_culling_selection(vm->proxy.lighttable.view);
  else
    return NO_IMGID;
}

gboolean dt_view_lighttable_preview_state(dt_view_manager_t *vm)
{
  if(vm->proxy.lighttable.module)
    return vm->proxy.lighttable.get_preview_state(vm->proxy.lighttable.view);
  else
    return FALSE;
}

void dt_view_lighttable_set_preview_state(dt_view_manager_t *vm,
                                          const gboolean state,
                                          const gboolean sticky,
                                          const gboolean focus,
                                          const dt_lighttable_culling_restriction_t restriction)
{
  if(vm->proxy.lighttable.module)
    vm->proxy.lighttable.set_preview_state(vm->proxy.lighttable.view, state, sticky, focus, restriction);
}

void dt_view_lighttable_change_offset(dt_view_manager_t *vm,
                                      const gboolean reset,
                                      const dt_imgid_t imgid)
{
  if(vm && vm->proxy.lighttable.module)
    vm->proxy.lighttable.change_offset(vm->proxy.lighttable.view, reset, imgid);
}

void dt_view_collection_update(const dt_view_manager_t *vm)
{
  if(vm->proxy.module_filtering.module)
    vm->proxy.module_filtering.update(vm->proxy.module_filtering.module);
  if(vm->proxy.module_collect.module)
    vm->proxy.module_collect.update(vm->proxy.module_collect.module);
}

void dt_view_filtering_set_sort(const dt_view_manager_t *vm,
                                const int sort,
                                const gboolean asc)
{
  if(vm->proxy.module_filtering.module)
    vm->proxy.module_filtering.set_sort(vm->proxy.module_filtering.module, sort, asc);
}

int32_t dt_view_tethering_get_selected_imgid(const dt_view_manager_t *vm)
{
  if(vm->proxy.tethering.view)
    return vm->proxy.tethering.get_selected_imgid(vm->proxy.tethering.view);

  return -1;
}

void dt_view_tethering_set_job_code(const dt_view_manager_t *vm,
                                    const char *name)
{
  if(vm->proxy.tethering.view)
    vm->proxy.tethering.set_job_code(vm->proxy.tethering.view, name);
}

const char *dt_view_tethering_get_job_code(const dt_view_manager_t *vm)
{
  if(vm->proxy.tethering.view)
    return vm->proxy.tethering.get_job_code(vm->proxy.tethering.view);
  return NULL;
}

#ifdef HAVE_MAP
void dt_view_map_center_on_location(const dt_view_manager_t *vm,
                                    const gdouble lon,
                                    const gdouble lat,
                                    const gdouble zoom)
{
  if(vm->proxy.map.view)
    vm->proxy.map.center_on_location(vm->proxy.map.view, lon, lat, zoom);
}

void dt_view_map_center_on_bbox(const dt_view_manager_t *vm,
                                const gdouble lon1,
                                const gdouble lat1,
                                const gdouble lon2,
                                const gdouble lat2)
{
  if(vm->proxy.map.view)
    vm->proxy.map.center_on_bbox(vm->proxy.map.view, lon1, lat1, lon2, lat2);
}

void dt_view_map_show_osd(const dt_view_manager_t *vm)
{
  if(vm->proxy.map.view)
    vm->proxy.map.show_osd(vm->proxy.map.view);
}

void dt_view_map_set_map_source(const dt_view_manager_t *vm,
                                const OsmGpsMapSource_t map_source)
{
  if(vm->proxy.map.view)
    vm->proxy.map.set_map_source(vm->proxy.map.view, map_source);
}

GObject *dt_view_map_add_marker(const dt_view_manager_t *vm,
                                const dt_geo_map_display_t type,
                                GList *points)
{
  if(vm->proxy.map.view)
    return vm->proxy.map.add_marker(vm->proxy.map.view, type, points);
  return NULL;
}

gboolean dt_view_map_remove_marker(const dt_view_manager_t *vm,
                                   const dt_geo_map_display_t type,
                                   GObject *marker)
{
  if(vm->proxy.map.view)
    return vm->proxy.map.remove_marker(vm->proxy.map.view, type, marker);
  return FALSE;
}
void dt_view_map_add_location(const dt_view_manager_t *vm,
                              dt_map_location_data_t *p,
                              const guint posid)
{
  if(vm->proxy.map.view)
    vm->proxy.map.add_location(vm->proxy.map.view, p, posid);
}

void dt_view_map_location_action(const dt_view_manager_t *vm,
                                 const int action)
{
  if(vm->proxy.map.view)
    vm->proxy.map.location_action(vm->proxy.map.view, action);
}

void dt_view_map_drag_set_icon(const dt_view_manager_t *vm,
                               GdkDragContext *context,
                               const dt_imgid_t imgid,
                               const int count)
{
  if(vm->proxy.map.view)
    vm->proxy.map.drag_set_icon(vm->proxy.map.view, context, imgid, count);
}

#endif

#ifdef HAVE_PRINT
void dt_view_print_settings(const dt_view_manager_t *vm,
                            dt_print_info_t *pinfo,
                            dt_images_box *imgs)
{
  if(vm->proxy.print.view)
    vm->proxy.print.print_settings(vm->proxy.print.view, pinfo, imgs);
}
#endif

GSList *dt_mouse_action_create_simple(GSList *actions,
                                      const dt_mouse_action_type_t type,
                                      const GdkModifierType accel,
                                      const char *const description)
{
  dt_mouse_action_t *a = calloc(1, sizeof(dt_mouse_action_t));
  if(a)
  {
    a->action = type;
    a->mods = accel;
    g_strlcpy(a->name, description, sizeof(a->name));
    return g_slist_append(actions, a);
  }
  else
    return actions;
}

GSList *dt_mouse_action_create_format(GSList *actions,
                                      const dt_mouse_action_type_t type,
                                      const GdkModifierType accel,
                                      const char *const format_string,
                                      const char *const replacement)
{
  dt_mouse_action_t *a = calloc(1, sizeof(dt_mouse_action_t));
  if(a)
  {
    a->action = type;
    a->mods = accel;
    g_snprintf(a->name, sizeof(a->name), format_string, replacement);
    return g_slist_append(actions, a);
  }
  else
    return actions;
}

static gchar *_mouse_action_get_string(dt_mouse_action_t *ma)
{
  gchar *atxt = NULL;
  if(ma->mods & GDK_SHIFT_MASK  )
    dt_util_str_cat(&atxt, "%s+", _("shift"));
  if(ma->mods & GDK_CONTROL_MASK)
    dt_util_str_cat(&atxt, "%s+", _("ctrl"));
  if(ma->mods & GDK_MOD1_MASK   )
#ifdef __APPLE__
    dt_util_str_cat(&atxt, "%s+", _("option"));
#else
    dt_util_str_cat(&atxt, "%s+", _("alt"));
#endif

  switch(ma->action)
  {
    case DT_MOUSE_ACTION_LEFT:
      dt_util_str_cat(&atxt, _("left-click"));
      break;
    case DT_MOUSE_ACTION_RIGHT:
      dt_util_str_cat(&atxt, _("right-click"));
      break;
    case DT_MOUSE_ACTION_MIDDLE:
      dt_util_str_cat(&atxt, _("middle-click"));
      break;
    case DT_MOUSE_ACTION_SCROLL:
      dt_util_str_cat(&atxt, _("scroll"));
      break;
    case DT_MOUSE_ACTION_DOUBLE_LEFT:
      dt_util_str_cat(&atxt, _("left double-click"));
      break;
    case DT_MOUSE_ACTION_DOUBLE_RIGHT:
      dt_util_str_cat(&atxt, _("right double-click"));
      break;
    case DT_MOUSE_ACTION_DRAG_DROP:
      dt_util_str_cat(&atxt, _("drag and drop"));
      break;
    case DT_MOUSE_ACTION_LEFT_DRAG:
      dt_util_str_cat(&atxt, _("left-click+drag"));
      break;
    case DT_MOUSE_ACTION_RIGHT_DRAG:
      dt_util_str_cat(&atxt, _("right-click+drag"));
      break;
  }

  return atxt;
}

static void _accels_window_destroy(GtkWidget *widget, dt_view_manager_t *vm)
{
  // set to NULL so we can rely on it after
  vm->accels_window.window = NULL;
}

static void _accels_window_sticky(GtkWidget *widget,
                                  dt_view_manager_t *vm)
{
  if(!vm->accels_window.window) return;

  // creating new window
  GtkWindow *win = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
  dt_gui_add_class(GTK_WIDGET(win), "dt_accels_window");
  gtk_window_set_title(win, _("darktable - accels window"));
  GtkAllocation alloc;
  gtk_widget_get_allocation(dt_ui_main_window(darktable.gui->ui), &alloc);

  gtk_window_set_resizable(win, TRUE);
  gtk_window_set_icon_name(win, "darktable");
  gtk_window_set_default_size(win, alloc.width * 0.7, alloc.height * 0.7);
  g_signal_connect(win, "destroy", G_CALLBACK(_accels_window_destroy), vm);

  GtkWidget *sw = dt_gui_container_first_child(GTK_CONTAINER(vm->accels_window.window));
  g_object_ref(sw);
  gtk_container_remove(GTK_CONTAINER(vm->accels_window.window), sw);
  gtk_container_add(GTK_CONTAINER(win), sw);
  g_object_unref(sw);

  gtk_widget_destroy(vm->accels_window.window);
  vm->accels_window.window = GTK_WIDGET(win);
  gtk_widget_show_all(vm->accels_window.window);
  gtk_widget_hide(vm->accels_window.sticky_btn);

  vm->accels_window.sticky = TRUE;
}

void dt_view_accels_show(dt_view_manager_t *vm)
{
  if(vm->accels_window.window) return;

  vm->accels_window.sticky = FALSE;
  vm->accels_window.prevent_refresh = FALSE;
  vm->accels_window.window = gtk_window_new(GTK_WINDOW_POPUP);
#ifdef GDK_WINDOWING_QUARTZ
  dt_osx_disallow_fullscreen(vm->accels_window.window);
#endif
  dt_gui_add_class(vm->accels_window.window, "dt_accels_window");

  GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  vm->accels_window.flow_box = gtk_flow_box_new();
  dt_gui_add_class(vm->accels_window.flow_box, "dt_accels_box");
  gtk_orientable_set_orientation(GTK_ORIENTABLE(vm->accels_window.flow_box),
                                 GTK_ORIENTATION_HORIZONTAL);

  gtk_box_pack_start(GTK_BOX(hb), vm->accels_window.flow_box, TRUE, TRUE, 0);

  GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  vm->accels_window.sticky_btn = dtgtk_button_new(dtgtk_cairo_paint_multiinstance, 0, NULL);
  gtk_widget_set_tooltip_text(vm->accels_window.sticky_btn,
                              _("switch to a classic window which will stay open after key release"));
  g_signal_connect(G_OBJECT(vm->accels_window.sticky_btn), "clicked",
                   G_CALLBACK(_accels_window_sticky),
                   vm);
  dt_gui_add_class(vm->accels_window.sticky_btn, "dt_accels_stick");
  gtk_box_pack_start(GTK_BOX(vb), vm->accels_window.sticky_btn, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, FALSE, 0);

  dt_view_accels_refresh(vm);

  GtkAllocation alloc;
  gtk_widget_get_allocation(dt_ui_main_window(darktable.gui->ui), &alloc);
  GtkWidget *sw = dt_gui_scroll_wrap(hb);
  gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), alloc.height);
  gtk_scrolled_window_set_max_content_width(GTK_SCROLLED_WINDOW(sw), alloc.width);
  gtk_container_add(GTK_CONTAINER(vm->accels_window.window), sw);

  gtk_window_set_resizable(GTK_WINDOW(vm->accels_window.window), FALSE);
  gtk_window_set_default_size(GTK_WINDOW(vm->accels_window.window),
                              alloc.width, alloc.height);
  gtk_window_set_transient_for(GTK_WINDOW(vm->accels_window.window),
                               GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
  gtk_window_set_keep_above(GTK_WINDOW(vm->accels_window.window), TRUE);
  // needed on macOS to avoid fullscreening the popup with newer GTK
  gtk_window_set_type_hint(GTK_WINDOW(vm->accels_window.window),
                           GDK_WINDOW_TYPE_HINT_POPUP_MENU);

  gtk_window_set_gravity(GTK_WINDOW(vm->accels_window.window), GDK_GRAVITY_STATIC);
  gtk_window_set_position(GTK_WINDOW(vm->accels_window.window),
                          GTK_WIN_POS_CENTER_ON_PARENT);
  gtk_widget_show_all(vm->accels_window.window);
}

void dt_view_accels_hide(dt_view_manager_t *vm)
{
  if(vm->accels_window.window && vm->accels_window.sticky)
    return;
  if(vm->accels_window.window)
    gtk_widget_destroy(vm->accels_window.window);
  vm->accels_window.window = NULL;
}

void dt_view_accels_refresh(dt_view_manager_t *vm)
{
  if(!vm->accels_window.window || vm->accels_window.prevent_refresh)
    return;

  // drop all existing tables
  GList *lw = gtk_container_get_children(GTK_CONTAINER(vm->accels_window.flow_box));

  for(const GList *lw_iter = lw; lw_iter; lw_iter = g_list_next(lw_iter))
  {
    GtkWidget *w = (GtkWidget *)lw_iter->data;
    gtk_widget_destroy(w);
  }
  g_list_free(lw);

  // get the list of valid accel for this view
  const dt_view_t *cv = dt_view_manager_get_current_view(vm);
  const dt_view_type_flags_t v = cv->view(cv);

  GHashTable *blocks = dt_shortcut_category_lists(v);

  dt_action_t *first_category = darktable.control->actions;

  // we add the mouse actions too
  dt_action_t mouse_actions = { .label = _("mouse actions"),
                                .next = first_category };
  if(cv->mouse_actions)
  {
    GtkListStore *mouse_list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
    g_hash_table_insert(blocks, &mouse_actions, mouse_list);

    GSList *actions = cv->mouse_actions(cv);
    for(GSList *lm = actions; lm; lm = g_slist_next(lm))
    {
      dt_mouse_action_t *ma = lm->data;
      if(ma)
      {
        gchar *atxt = _mouse_action_get_string(ma);
        gtk_list_store_insert_with_values(mouse_list, NULL, -1, 0, atxt, 1, ma->name, -1);
        g_free(atxt);
      }
    }
    g_slist_free_full(actions, g_free);

    first_category = &mouse_actions;
  }

  // now we create and insert the widget to display all accels by categories
  for(dt_action_t *category = first_category; category; category = category->next)
  {
    GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    // the title
    GtkWidget *lb = gtk_label_new(category->label);
    dt_gui_add_class(lb, "dt_accels_cat_title");
    gtk_box_pack_start(GTK_BOX(box), lb, FALSE, FALSE, 0);

    // the list of accels
    GtkTreeModel *model = GTK_TREE_MODEL(g_hash_table_lookup(blocks, category));
    if(model)
    {
      GtkWidget *list = gtk_tree_view_new_with_model(model);
      g_object_unref(model);
      GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
      GtkTreeViewColumn *column =
        gtk_tree_view_column_new_with_attributes(_("shortcut"), renderer, "text", 0, NULL);
      gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);
      column = gtk_tree_view_column_new_with_attributes(_("action"),
                                                        renderer, "text", 1, NULL);
      gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);

      gtk_box_pack_start(GTK_BOX(box), list, FALSE, FALSE, 0);

      gtk_flow_box_insert(GTK_FLOW_BOX(vm->accels_window.flow_box), box, -1);
    }
  }

  g_hash_table_destroy(blocks);

  gtk_widget_show_all(vm->accels_window.flow_box);
}

static void _audio_child_watch(const GPid pid,
                               const gint status,
                               gpointer data)
{
  dt_view_manager_t *vm = (dt_view_manager_t *)data;
  vm->audio.audio_player_id = -1;
  g_spawn_close_pid(pid);
}

void dt_view_audio_start(dt_view_manager_t *vm,
                         const dt_imgid_t imgid)
{
  char *player = dt_conf_get_string("plugins/lighttable/audio_player");
  if(player && *player)
  {
    char *filename = dt_image_get_audio_path(imgid);
    if(filename)
    {
      char *argv[] = { player, filename, NULL };
      gboolean ret = g_spawn_async(NULL, argv, NULL,
                                   G_SPAWN_DO_NOT_REAP_CHILD
                                   | G_SPAWN_SEARCH_PATH
                                   | G_SPAWN_STDOUT_TO_DEV_NULL
                                   | G_SPAWN_STDERR_TO_DEV_NULL,
                                   NULL, NULL,
                                   &vm->audio.audio_player_pid, NULL);

      if(ret)
      {
        vm->audio.audio_player_id = imgid;
        vm->audio.audio_player_event_source =
          g_child_watch_add(vm->audio.audio_player_pid,
                            (GChildWatchFunc)_audio_child_watch, vm);
      }
      else
        vm->audio.audio_player_id = -1;

      g_free(filename);
    }
  }
  g_free(player);
}

void dt_view_audio_stop(dt_view_manager_t *vm)
{
  // make sure that the process didn't finish yet and that _audio_child_watch() hasn't run
  if(vm->audio.audio_player_id == -1)
    return;

  // we don't want to trigger the callback due to a possible race condition
  g_source_remove(vm->audio.audio_player_event_source);
#ifdef _WIN32
// TODO: add Windows code to actually kill the process
#else  // _WIN32
  if(vm->audio.audio_player_id != -1)
  {
    if(getpgid(0) != getpgid(vm->audio.audio_player_pid))
      kill(-vm->audio.audio_player_pid, SIGKILL);
    else
      kill(vm->audio.audio_player_pid, SIGKILL);
  }
#endif // _WIN32
  g_spawn_close_pid(vm->audio.audio_player_pid);
  vm->audio.audio_player_id = -1;
}

static void _match_rotation(cairo_t *cr,
                            dt_dev_zoom_pos_t d,
                            double dx,
                            double dy,
                            const double cx,
                            const double cy)
{
  const double xx = d[2] - d[0];
  const double yx = d[3] - d[1];
  const double xy = d[4] - d[0];
  const double yy = d[5] - d[1];

  if(fabs(xx * yy - xy * yx) < 1e-10)
    return;

  const double sx = hypot(xx, yx);
  const double sy = hypot(xy, yy);
  cairo_matrix_t matrix;
  cairo_matrix_init(&matrix, xx / sx, yx / sx, xy / sy, yy / sy, 0, 0);
  cairo_transform(cr, &matrix);

  dx += 0.5 * cx;
  dy += 0.5 * cy;

  cairo_matrix_invert(&matrix);
  cairo_matrix_transform_point(&matrix, &dx, &dy);
  cairo_translate(cr, dx - 0.5 * cx, dy - 0.5 * cy);
}

void dt_view_paint_surface(cairo_t *cr,
                           const size_t width,
                           const size_t height,
                           dt_dev_viewport_t *port,
                           const dt_window_t window,
                           uint8_t *buf,
                           float buf_scale,
                           int buf_width,
                           int buf_height,
                           dt_dev_zoom_pos_t buf_zoom_pos)
{
  // Use the viewport's develop if available, otherwise fall back to global
  dt_develop_t *dev = port->dev ? port->dev : darktable.develop;
  // Preview pipe for fallback rendering - only available for main develop
  dt_dev_pixelpipe_t *pp = darktable.develop->preview_pipe;

  int processed_width, processed_height;
  dt_dev_get_processed_size(port, &processed_width, &processed_height);
  if(!processed_width || !processed_height)
    return;

  float pts[14];
  memcpy(pts, buf_zoom_pos, sizeof(dt_dev_zoom_pos_t));
  memcpy(pts + 6, pp->backbuf_zoom_pos, sizeof(dt_dev_zoom_pos_t));
  pts[12] = port->zoom_x; pts[13] = port->zoom_y;
  dt_dev_distort_transform_plus(dev, port->pipe, 0.0f, DT_DEV_TRANSFORM_DIR_ALL_GEOMETRY, pts, 7);

  const float offset_x  = pts[0] / processed_width  - 0.5f;
  const float offset_y  = pts[1] / processed_height - 0.5f;
  const float preview_x = pts[6] / processed_width  - 0.5f;
  const float preview_y = pts[7] / processed_height - 0.5f;
  const float zoom_x    = pts[12]/ processed_width  - 0.5f;
  const float zoom_y    = pts[13]/ processed_height - 0.5f;

  const dt_dev_zoom_t zoom = port->zoom;
  const int closeup = port->closeup;
  const float ppd           = port->ppd;
  const double tb           = port->border_size;
  const float zoom_scale    = dt_dev_get_zoom_scale(port, zoom, 1<<closeup, TRUE);
  const float backbuf_scale = dt_dev_get_zoom_scale(port, zoom, 1.0f, 0) * ppd;

  dt_print_pipe(DT_DEBUG_EXPOSE,
      "dt_view_paint_surface",
        port->pipe, NULL, DT_DEVICE_NONE, NULL, NULL,
        "viewport zoom_scale %6.3f backbuf_scale %6.3f "
        "(x=%6.2f y=%6.2f) -> (x=%+.3f y=%+.3f)",
        zoom_scale, backbuf_scale,
        port->zoom_x, port->zoom_y, zoom_x, zoom_y);

  cairo_save(cr);

  if(port->color_assessment)
  {
    // force middle grey in background
    dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_COLOR_ASSESSMENT_BG);
  }
  else
  {
    if(dev->full_preview)
      dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_PREVIEW_BG);
    else
      dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG);
  }

  cairo_paint(cr);

  cairo_translate(cr, 0.5 * width, 0.5 * height);
  dt_pthread_mutex_lock(&pp->backbuf_mutex);

  const int maxw = MIN(port->width, backbuf_scale * processed_width * (1<<closeup) / ppd);
  const int maxh = MIN(port->height, backbuf_scale * processed_height * (1<<closeup) / ppd);

  if(port->color_assessment
     && window != DT_WINDOW_SLIDESHOW)
  {
    // draw the white frame around picture
    const double ratio = dt_conf_get_float("darkroom/ui/color_assessment_border_white_ratio");
    const double borw = maxw + 2.0 * tb * ratio;
    const double borh = maxh + 2.0 * tb * ratio;
    cairo_rectangle(cr, -0.5 * borw, -0.5 * borh, borw, borh);
    dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_COLOR_ASSESSMENT_FG);
    cairo_fill(cr);
  }

  cairo_rectangle(cr, -0.5 * maxw, -0.5 * maxh, maxw, maxh);
  cairo_clip(cr);
  const double back_scale = (buf_scale == 0 ? 1.0 : backbuf_scale / buf_scale) * (1<<closeup) / ppd;
  const double trans_x = (offset_x - zoom_x) * processed_width * buf_scale - 0.5 * buf_width;
  const double trans_y = (offset_y - zoom_y) * processed_height * buf_scale - 0.5 * buf_height;

  // Check if we should use the preview pipe for fallback rendering
  // This is only valid for the main develop (not for pinned images which have dev != darktable.develop)
  const gboolean use_preview_fallback =
     (dev == darktable.develop)
     && pp->output_imgid == dev->image_storage.id
     && (port->pipe->output_imgid != dev->image_storage.id
         || fabsf(backbuf_scale / buf_scale - 1.0f) > .09f
         || floor(maxw / 2 / back_scale) - 1 > MIN(- trans_x, trans_x + buf_width)
         || floor(maxh / 2 / back_scale) - 1 > MIN(- trans_y, trans_y + buf_height))
     && (port == &dev->full || port == &dev->preview2);

  if(use_preview_fallback)
  {
    dt_print_pipe(DT_DEBUG_PIPE | DT_DEBUG_EXPOSE | DT_DEBUG_VERBOSE,
      "dt_view_paint_surface sets DT_DEV_PIPE_ZOOMED", port->pipe, NULL, DT_DEVICE_NONE, NULL, NULL);
    port->pipe->changed |= DT_DEV_PIPE_ZOOMED;
    if(port->pipe->status == DT_DEV_PIXELPIPE_VALID)
      port->pipe->status = DT_DEV_PIXELPIPE_DIRTY;

    // draw preview
    const int full_pipe_width = dev->full.pipe ? dev->full.pipe->processed_width : 1;
    const float wd = processed_width * pp->processed_width / MAX(1, full_pipe_width);
    const float ht = processed_height * pp->processed_width / MAX(1, full_pipe_width);

    cairo_save(cr);
    cairo_scale(cr, zoom_scale, zoom_scale);
    _match_rotation(cr, pts + 6, (preview_x - zoom_x) * wd, (preview_y - zoom_y) * ht, 0, 0);
    cairo_surface_t *preview = dt_view_create_surface(pp->backbuf, pp->backbuf_width, pp->backbuf_height);
    cairo_set_source_surface(cr, preview, -0.5 * pp->backbuf_width, -0.5 * pp->backbuf_height);
    cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_FAST);
    cairo_paint(cr);
    cairo_surface_destroy(preview);
    cairo_restore(cr);

    dt_print_pipe(DT_DEBUG_EXPOSE,
        "  painting",
         pp, NULL, DT_DEVICE_NONE, NULL, NULL,
         "size %4lux%-4lu processed %4.0fx%-4.0f "
         "buf %4dx%-4d scale=%.3f "
         "zoom (x=%6.2f y=%6.2f) -> offset (x=%+.3f y=%+.3f)",
         width, height, wd, ht,
         pp->backbuf_width, pp->backbuf_height, zoom_scale,
         pp->backbuf_zoom_pos[0], pp->backbuf_zoom_pos[1],
         preview_x, preview_y);
  }

  dt_pthread_mutex_unlock(&pp->backbuf_mutex);

  if(port->pipe->output_imgid == dev->image_storage.id
     || pp->output_imgid != dev->image_storage.id)
  {
    dt_print_pipe(DT_DEBUG_EXPOSE,
        "  painting",
         port->pipe, NULL, DT_DEVICE_NONE, NULL, NULL,
         "size %4lux%-4lu processed %4dx%-4d "
         "buf %4dx%-4d scale=%.3f "
         "zoom (x=%6.2f y=%6.2f) -> offset (x=%+.3f y=%+.3f)",
         width, height, processed_width, processed_height,
         buf_width, buf_height, buf_scale,
         buf_zoom_pos[0], buf_zoom_pos[1],
         offset_x, offset_y);

    cairo_scale(cr, back_scale, back_scale);
    _match_rotation(cr, pts, trans_x, trans_y, buf_width, buf_height);
    cairo_surface_t *surface = dt_view_create_surface(buf, buf_width, buf_height);
    cairo_set_source_surface(cr, surface, 0, 0);
    cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_FAST);
    cairo_paint(cr);

    if(darktable.gui->show_focus_peaking
      && window != DT_WINDOW_SLIDESHOW)
    {
      dt_focuspeaking(cr, buf_width, buf_height,
                      cairo_image_surface_get_data(surface));
    }
    cairo_surface_destroy(surface);
  }

  cairo_restore(cr);
}

cairo_surface_t *dt_view_create_surface(uint8_t *buffer,
                                        const size_t processed_width,
                                        const size_t processed_height)
{
  const int32_t stride =
    cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, processed_width);
  return cairo_image_surface_create_for_data
    (buffer, CAIRO_FORMAT_RGB24, processed_width, processed_height, stride);
}

dt_view_context_t dt_view_get_context_hash(void)
{
  dt_develop_t *dev = darktable.develop;
  dt_dev_zoom_t zoom;
  int closeup;
  float zoom_x, zoom_y;
  dt_dev_get_viewport_params(&dev->full, &zoom, &closeup, &zoom_x, &zoom_y);
  const float zoom_scale = dt_dev_get_zoom_scale(&dev->full, zoom, 1<<closeup, TRUE);

  // calculate a hash on view parameters. Use flt_prec here to avoid different hashes
  // for irrelevant variations for the zooms.
  const float flt_prec = 1.e6;
  const uint32_t test[] = { (uint32_t)dev->full.color_assessment,
                            (uint32_t)darktable.gui->show_focus_peaking,
                            (uint32_t)closeup,
                            (uint32_t)(zoom_scale * flt_prec),
                            (uint32_t)(zoom_x * flt_prec),
                            (uint32_t)(zoom_y * flt_prec),
                            (uint32_t)(dev->late_scaling.enabled) };

  return (dt_view_context_t)dt_hash(DT_INITHASH, &test, sizeof(test));
}

gboolean dt_view_check_context_hash(dt_view_context_t *ctx)
{
  const dt_view_context_t curctx = dt_view_get_context_hash();
  if(curctx == *ctx)
  {
    return TRUE;
  }
  else
  {
    *ctx = curctx;
    return FALSE;
  }
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on
