Ctrl K
ComunidadGuías…

src/libs/lib.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2009-2026 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/gdk_event_utils.h"

#include "libs/lib.h"
#include "common/debug.h"
#include "common/module.h"
#include "common/presets.h"
#include "common/color_picker.h"
#include "control/conf.h"
#include "control/control.h"
#include "dtgtk/button.h"
#include "dtgtk/expander.h"
#include "gui/accelerators.h"
#include "gui/drag_and_drop.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "gui/splash.h"

#include <gtk/gtk.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>

typedef struct dt_lib_presets_edit_dialog_t
{
  GtkEntry *name, *description;
  char plugin_name[128];
  int32_t version;
  void *params;
  int32_t params_size;
  gchar *original_name;
  dt_lib_module_t *module;
  gint old_id;
} dt_lib_presets_edit_dialog_t;

static gchar *_get_lib_view_path(const dt_lib_module_t *module,
                                 const dt_view_t *cv,
                                 char *suffix);

gboolean dt_lib_is_visible_in_view(dt_lib_module_t *module,
                                   const dt_view_t *view)
{
  if(!module->views)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "module %s doesn't have views flags",
             module->name(module));
    return FALSE;
  }

  gboolean ret = module->views(module) & view->view(view);
  gchar *key = _get_lib_view_path(module, view, "_visible");
  if(key && dt_conf_key_exists(key))
    ret = dt_conf_get_bool(key);
  g_free(key);

  return ret;
}

/** calls module->cleanup and closes the dl connection. */
static void _lib_unload_module(dt_lib_module_t *module);

gchar *dt_lib_get_active_preset_name(dt_lib_module_info_t *minfo)
{
  GList *presets = dt_presets_list_params(minfo->plugin_name, minfo->version, FALSE);
  gchar *name = NULL;
  // collect all presets for op from db
  for(const GList *l = presets; l; l = g_list_next(l))
  {
    const dt_presets_params_t *p = l->data;
    if(p->op_params_size == minfo->params_size
       && !memcmp(minfo->params, p->op_params, p->op_params_size))
    {
      name = g_strdup(p->name);
      break;
    }
  }
  g_list_free_full(presets, dt_presets_params_free);
  return name;
}

// get dt_lib_module_info_t for a module. Must be freed by _free_module_info()
static dt_lib_module_info_t *_get_module_info_for_module(dt_lib_module_t *module)
{
  dt_lib_module_info_t *mi = calloc(1, sizeof(dt_lib_module_info_t));

  mi->plugin_name = g_strdup(module->plugin_name);
  mi->version = module->version();
  mi->module = module;
  mi->params = module->get_params ? module->get_params(module, &mi->params_size) : NULL;
  if(!mi->params)
  {
    // this is a valid case, for example in location.c when nothing got selected
    // fprintf(stderr, "something went wrong: &params=%p, size=%i\n",
    //         mi->params, mi->params_size);
    mi->params_size = 0;
  }
  return mi;
}

static void _set_module_preset_label(dt_lib_module_t *module,
                                     const gchar *preset_name)
{
  if(!module->expander || !module->has_preset_label(module))
    return;

  const gchar *current_preset_label_text = gtk_label_get_text(GTK_LABEL(module->preset_label));
  gchar *preset_label_text =
    (*preset_name == '\0' || (!dt_conf_get_bool("darkroom/ui/auto_module_name_update")))?
    g_strdup("") : g_strdup_printf("• %s", preset_name);

  if(g_strcmp0(current_preset_label_text, preset_label_text))
    // update the preset label only if changed to avoid infinite loop
    gtk_label_set_text(GTK_LABEL(module->preset_label), preset_label_text);

  g_free(preset_label_text);
}

static void _free_module_info(gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  g_free(minfo->plugin_name);
  free(minfo->params);
  free(minfo);
}

static void _edit_preset(const char *name_in,
                         dt_lib_module_info_t *minfo)
{
  dt_lib_module_info_t *minfo2 = _get_module_info_for_module(minfo->module);

  // get the original name of the preset
  gchar *name = NULL;
  if(name_in == NULL)
  {
    name = dt_lib_get_active_preset_name(minfo2);
    if(name == NULL) return;
  }
  else
    name = g_strdup(name_in);

  // find the rowid of the preset
  int rowid = -1;
  dt_presets_details_t details = { 0 };
  if(dt_presets_get_details(name, minfo2->plugin_name, minfo2->version, &details))
    rowid = details.rowid;
  dt_presets_details_clear(&details);

  // if we don't have a valid rowid, just exit, there's a problem !
  if(rowid < 0) return;

  GtkWidget *window = dt_ui_main_window(darktable.gui->ui);
  dt_gui_presets_show_edit_dialog
    (name, rowid, NULL, NULL, TRUE, TRUE, FALSE, GTK_WINDOW(window));

  _free_module_info(minfo2);
}

static void _menuitem_update_preset(GSimpleAction *action,
                                    GVariant *parameter,
                                    gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  dt_lib_module_info_t *minfo2 = _get_module_info_for_module(minfo->module);

  const gchar *name = g_variant_get_string(parameter,  NULL);

  if(!dt_conf_get_bool("plugins/lighttable/preset/ask_before_delete_preset")
     || dt_gui_show_yes_no_dialog(_("update preset?"), "",
                                  _("do you really want to update the preset `%s'?"),
                                  name))
  {
    // commit all the module fields
    dt_presets_set_op_params(minfo2->plugin_name, name, minfo2->version,
                             minfo2->params, minfo2->params_size);
    DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_PRESETS_CHANGED,
                            g_strdup(minfo->plugin_name));
  }

  _free_module_info(minfo2);
}

static void _menuitem_new_preset(GSimpleAction *action,
                                 GVariant *parameter,
                                 gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  dt_lib_module_info_t *minfo2 = _get_module_info_for_module(minfo->module);

  dt_lib_presets_remove(_("new preset"), minfo2->plugin_name, minfo2->version);

  // add new preset
  dt_presets_insert_lib_preset(_("new preset"), minfo2->plugin_name, minfo2->version,
                               minfo2->params, minfo2->params_size);
  // create a shortcut for the new entry

  dt_action_define_preset(&minfo2->module->actions, _("new preset"));

  // then show edit dialog
  _edit_preset(_("new preset"), minfo2);

  _free_module_info(minfo2);
}

static void _menuitem_edit_preset(GSimpleAction *action,
                                  GVariant *parameter,
                                  gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  dt_lib_module_info_t *minfo2 = _get_module_info_for_module(minfo->module);

  _edit_preset(NULL, minfo2);
  _free_module_info(minfo2);
}

static void _menuitem_manage_presets(GSimpleAction *action,
                                    GVariant *parameter,
                                    gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;

  if(minfo->module->manage_presets)
    minfo->module->manage_presets(minfo->module);
}

static void _menuitem_delete_preset(GSimpleAction *action,
                                    GVariant *parameter,
                                    gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  dt_lib_module_info_t *minfo2 = _get_module_info_for_module(minfo->module);

  gchar *name = dt_lib_get_active_preset_name(minfo2);
  if(name == NULL) return;

  if(!dt_conf_get_bool("plugins/lighttable/preset/ask_before_delete_preset")
     || dt_gui_show_yes_no_dialog(_("delete preset?"), "",
                                  _("do you really want to delete the preset `%s'?"),
                                  name))
  {
    dt_action_rename_preset(&minfo2->module->actions, name, NULL);
    dt_lib_presets_remove(name, minfo2->plugin_name, minfo2->version);
    _set_module_preset_label(minfo2->module, "");

    DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_PRESETS_CHANGED,
                            g_strdup(minfo2->plugin_name));
  }
  g_free(name);
  _free_module_info(minfo2);
}

gchar *dt_lib_presets_duplicate(const gchar *preset,
                                const gchar *module_name,
                                const int module_version)
{
  return dt_presets_duplicate(preset, module_name, module_version);
}

void dt_lib_presets_remove(const gchar *preset,
                           const gchar *module_name,
                           const int module_version)
{
  dt_presets_remove(preset, module_name, module_version);
}

gboolean dt_lib_presets_apply(const gchar *preset,
                              const gchar *module_name,
                              const int module_version)
{
  gboolean ret = TRUE;
  dt_presets_params_t p = { 0 };

  int res = 0;
  if(dt_presets_get_params(module_name, module_version, preset, &p))
  {
    const void *blob = p.op_params;
    const int length = p.op_params_size;
    const int writeprotect = p.writeprotect;
    if(blob)
    {
      for(const GList *it = darktable.lib->plugins; it; it = g_list_next(it))
      {
        dt_lib_module_t *module = it->data;
        if(!strncmp(module->plugin_name, module_name, 128))
        {
          gchar *tx = g_strdup_printf("plugins/darkroom/%s/last_preset", module_name);
          dt_conf_set_string(tx, preset);
          g_free(tx);
          res = module->set_params(module, blob, length);
          if(!res)
            _set_module_preset_label(module, preset);
          break;
        }
      }
    }

    if(!writeprotect) dt_gui_store_last_preset(preset);
  }
  else
    ret = FALSE;
  dt_presets_params_clear(&p);
  if(res)
  {
    dt_control_log(_("deleting preset for obsolete module"));
    dt_lib_presets_remove(preset, module_name, module_version);
  }
  return ret;
}

void dt_lib_presets_update(const gchar *preset,
                           const gchar *module_name,
                           const int module_version,
                           const gchar *newname,
                           const gchar *desc,
                           const void *params,
                           const int32_t params_size)
{
  dt_presets_update_named(preset, module_name, module_version, newname, desc,
                          params, params_size);
}

static void _menuitem_activate_preset(GSimpleAction *action,
                                      GVariant *parameter,
                                      gpointer user_data)
{
  dt_lib_module_info_t *minfo = (dt_lib_module_info_t *)user_data;
  
  g_simple_action_set_state(action, parameter);

  const gchar *preset_name = g_variant_get_string(parameter,  NULL);
  dt_lib_presets_apply(preset_name,
                       minfo->module->plugin_name, minfo->module->version());

  // close the menu
  gtk_popover_popdown(GTK_POPOVER(darktable.gui->active_popover_menu));
}

/* secondary click copies the preset as lua and keeps the menu open: the
 * gesture is in CAPTURE phase and claims the sequence, so the menu shell
 * never sees the press or release and never activates the item.  primary
 * clicks flow normally and apply via the "activate" signal. */
// static void _menuitem_button_preset_released(GtkGestureSingle *gesture,
//                                              gint n_press,
//                                              gdouble x,
//                                              gdouble y,
//                                              dt_lib_module_info_t *minfo)
// {
//   GtkMenuItem *menuitem = GTK_MENU_ITEM(dt_gui_get_widget(gesture));
//   dt_shortcut_copy_lua((dt_action_t*)minfo->module,
//                        g_object_get_data(G_OBJECT(menuitem), "dt-preset-name"));
// }

/* ---------- lib preset menu (dt_gui_presets_popup_menu_show() ops) ---------- */

static GList *_lib_presets_rows(gpointer data)
{
  const dt_lib_module_info_t *minfo = data;
  const gboolean default_first = dt_conf_get_bool("modules/default_presets_first");
  return dt_presets_menu_rows_lib(minfo->plugin_name, minfo->version, default_first);
}

static gboolean _lib_presets_is_active(const dt_presets_params_t *row, gpointer data,
                                      gboolean *writeprotect)
{
  const dt_lib_module_info_t *minfo = data;
  const void *op_params = row->op_params;
  const int32_t op_params_size = row->op_params_size;

  if(op_params_size == minfo->params_size
     && !memcmp(minfo->params, op_params, op_params_size))
  {
    *writeprotect = row->writeprotect;
    return TRUE;
  }
  return FALSE;
}

static void _lib_presets_prefs(GMenu *menu, gpointer data)
{
  const dt_lib_module_info_t *minfo = data;

  GActionGroup *action_group = gtk_widget_get_action_group(minfo->module->presets_button, "presets");
  minfo->module->set_preferences(menu, action_group, minfo->module);
}

static int _lib_position(const dt_lib_module_t *module)
{
  int pos = module->position ? module->position(module) + 1 : 0;

  gchar *key = _get_lib_view_path(module, NULL, "_position");
  if(key && dt_conf_key_exists(key))
    pos = dt_conf_get_int(key);
  g_free(key);

  return pos;
}

gint dt_lib_sort_plugins(gconstpointer a, gconstpointer b)
{
  return ABS(_lib_position(a)) - ABS(_lib_position(b));
}

uint32_t dt_lib_get_container(dt_lib_module_t *module)
{
  uint32_t container = module->container(module);

  if(_lib_position(module) < 0)
    container = container == DT_UI_CONTAINER_PANEL_LEFT_CENTER
              ? DT_UI_CONTAINER_PANEL_RIGHT_CENTER
              : DT_UI_CONTAINER_PANEL_LEFT_CENTER;

  if(container == DT_UI_CONTAINER_PANEL_RIGHT_CENTER
     && dt_view_get_current() == DT_VIEW_DARKROOM)
    container = DT_UI_CONTAINER_PANEL_LEFT_CENTER;

  return container;
}

/* default has_preset_label  implementation */
static gboolean default_has_preset_label(dt_lib_module_t *self)
{
  return FALSE;
}

/* default expandable implementation */
static gboolean default_expandable(dt_lib_module_t *self)
{
  return TRUE;
}

/* default autoapply implementation */
static gboolean default_preset_autoapply(dt_lib_module_t *self)
{
  return FALSE;
}

/* default gui_update implementation */
static void default_gui_update(dt_lib_module_t *self)
{
}

static int dt_lib_load_module(void *m,
                              const char *libname,
                              const char *module_name)
{
  dt_lib_module_t *module = (dt_lib_module_t *)m;
  g_strlcpy(module->plugin_name, module_name, sizeof(module->plugin_name));

#define INCLUDE_API_FROM_MODULE_LOAD "lib_load_module"
#include "libs/lib_api.h"

  if(((!module->get_params || !module->set_params)
      && (module->legacy_params || module->set_params || module->get_params))
     || (!module->init_presets && module->manage_presets))
  {
    dt_print(DT_DEBUG_ALWAYS, "[dt_lib_load_module] illegal method combination in '%s'",
             module->plugin_name);
  }

  if(!module->get_params || !module->set_params)
  {
    // need all at the same time, or none, note that in this case
    // all the presets for the corresponding module will be deleted.
    // see: dt_lib_init_presets.
    module->legacy_params = NULL;
    module->set_params = NULL;
    module->get_params = NULL;
    module->manage_presets = NULL;
  }

  module->widget = NULL;
  module->expander = NULL;
  module->arrow = NULL;
  module->preset_label = NULL;
  module->reset_button = NULL;
  module->presets_button = NULL;

  module->actions = (dt_action_t){ DT_ACTION_TYPE_LIB,
                                   module->plugin_name,
                                   module->name(module) };

  dt_action_insert_sorted(&darktable.control->actions_libs, &module->actions);
#ifdef USE_LUA
  dt_lua_lib_register(darktable.lua_state.state, module);
#endif

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

  return 0;
}

static void *_update_params(dt_lib_module_t *module,
                            const void *const old_params,
                            size_t old_params_size,
                            int old_version,
                            const int target_version,
                            size_t *new_size)
{
  // make a copy of the old params so we can free it in the loop
  void *params = malloc(old_params_size);
  if(params == NULL) return NULL;
  memcpy(params, old_params, old_params_size);
  while(old_version < target_version)
  {
    size_t size;
    int version;
    void *new_params = module->legacy_params(module, params, old_params_size,
                                             old_version, &version, &size);
    free(params);
    if(new_params == NULL) return NULL;
    params = new_params;
    old_version = version;
    old_params_size = size;
  }
  *new_size = old_params_size;
  return params;
}

void dt_lib_init_presets(dt_lib_module_t *module)
{
  // since lighttable presets can't end up in styles or any other
  // place outside of the presets table it is sufficient to update
  // that very table here and assume that everything is up to date
  // elsewhere.  the intended logic is as follows:
  // - no set_params -> delete all presets
  // - op_version >= module_version -> done
  // - op_version < module_version ->
  //   - module has legacy_params -> try to update
  //   - module doesn't have legacy_params -> delete it

  if(module->set_params == NULL)
  {
    dt_presets_delete_module(module->plugin_name);
  }
  else
  {
    // read before updating them: the loop rewrites the rows it reads
    GList *presets = dt_presets_list_stored(module->plugin_name);
    for(const GList *l = presets; l; l = g_list_next(l))
    {
      const dt_presets_params_t *preset = l->data;
      int rowid = preset->rowid;
      int op_version = preset->op_version;
      void *op_params = preset->op_params;
      size_t op_params_size = preset->op_params_size;
      const char *name = preset->name;

      int version = module->version();

      if(op_version < version)
      {
        size_t new_params_size = 0;
        void *new_params = NULL;

        if(module->legacy_params
          && (new_params = _update_params(module, op_params, op_params_size,
                                          op_version, version, &new_params_size)))
        {
          // write the updated preset back to db
          dt_print(DT_DEBUG_ALWAYS,
                   "[lighttable_init_presets] updating '%s' preset '%s'"
                   " from version %d to version %d",
                   module->plugin_name, name, op_version, version);
          dt_presets_upgrade_at_rowid(rowid, version, new_params, new_params_size);
        }
        else
        {
          // delete the preset
          dt_print(DT_DEBUG_ALWAYS,
                   "[lighttable_init_presets] Can't upgrade '%s' preset '%s'"
                   " from version %d to %d, "
                   "no legacy_params() implemented or unable to update",
                   module->plugin_name, name, op_version, version);
          dt_presets_delete_at_rowid(rowid);
        }
        free(new_params);
      }
    }
    g_list_free_full(presets, dt_presets_params_free);
  }

  if(module->init_presets)
    module->init_presets(module);

  DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_PRESETS_CHANGED,
                          g_strdup(module->plugin_name));

  GList *names = dt_presets_version_names(module->plugin_name, module->version(), TRUE);
  for(const GList *l = names; l; l = g_list_next(l))
    dt_action_define_preset(&module->actions, l->data);
  g_list_free_full(names, g_free);
}

static gboolean _lib_draw_callback(GtkWidget *widget,
                                   gpointer cr,
                                   dt_lib_module_t *self)
{
  dt_lib_gui_update(self);

  return FALSE;
}

void dt_lib_gui_queue_update(dt_lib_module_t *module)
{
  module->gui_uptodate = FALSE;
  gtk_widget_queue_draw(module->widget);
}

void dt_lib_gui_update(dt_lib_module_t *module)
{
  if(module && !module->gui_uptodate)
  {
    module->gui_update(module);
    module->gui_uptodate = TRUE;
  }

  if(module && module->has_preset_label(module))
  {
    dt_lib_module_info_t *mi = _get_module_info_for_module(module);
    gchar *active_preset_name = dt_lib_get_active_preset_name(mi);
    _free_module_info(mi);
    _set_module_preset_label(module, active_preset_name? active_preset_name : "");
    g_free(active_preset_name);
  }
}

static void dt_lib_init_module(void *m)
{
  dt_lib_module_t *module = (dt_lib_module_t *)m;
  dt_lib_init_presets(module);

  // Calling the keyboard shortcut initialization callback if present
  // do not init accelerators if there is no gui
  if(darktable.gui)
  {
    dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE, "loading utility module : %s",
             module->plugin_name);
    char *msg = g_strdup_printf(_("%s: %s"),
                                _("loading utility modules"),
                                module->name(module));
    dt_splash_screen_set_progress(msg);
    g_free(msg);

    module->gui_init(module);

    if(module->widget)
    {
      g_object_ref_sink(module->widget);
      g_signal_connect(G_OBJECT(module->widget), "draw",
                       G_CALLBACK(_lib_draw_callback), module);
    }
  }
}

static void _lib_unload_module(dt_lib_module_t *module)
{
  if(module->module)
    g_module_close(module->module);
}

static void _lib_gui_reset_callback(GtkGestureSingle *gesture,
                                      gint n_press,
                                      gdouble x,
                                      gdouble y,
                                      dt_lib_module_t *module)
{
  /* the reset action, shared by the module header reset button (wired through
   * its "clicked" signal in _lib_init_header) and the action/shortcut
   * fallback (_action_process, which passes a NULL gesture) */
  module->gui_reset(module);
  if(module->has_preset_label(module))
    gtk_label_set_text(GTK_LABEL(module->preset_label), "");
  if(gesture)
    dt_gui_claim(gesture);
}

static void _lib_gui_reset_button_clicked_callback(GtkWidget *widget,
                                                   gpointer user_data)
{
  /* the reset runs on the button's own "clicked" signal, not on a custom
   * gesture's press/release: gui_reset() may open a modal dialog, and a
   * dialog opened from inside a press handler would interrupt the press
   * dispatch -- the button's gesture only processes the press after the
   * dialog closes and its release was consumed by the dialog's grab, leaving
   * the button stuck in its pressed state.  "clicked" fires once the press
   * and release have both completed, so the dialog cannot split them.
   * (A custom gesture's "released" is equally unusable: the dialog's grab
   * cancels the gesture and dt_gui_connect_click() forwards that
   * cancellation to the released handler, firing the reset twice.) */
  dt_lib_module_t *module = user_data;
  _lib_gui_reset_callback(NULL, 1, 0.0, 0.0, module);
}

static void _presets_popup_callback(GtkWidget *button,
                                    dt_lib_module_t *module)
{
  dt_lib_module_info_t *minfo = _get_module_info_for_module(module);

  const dt_gui_presets_menu_ops_t ops = {
    .data = minfo,
    .hide_defaults_pref = "plugins/lighttable/hide_default_presets",
    .rows = _lib_presets_rows,
    .is_active = _lib_presets_is_active,
    .params_size = minfo->params_size,
    .activate_cb = _menuitem_activate_preset,
    .manage_cb = minfo->module->manage_presets ? _menuitem_manage_presets : NULL,
    .edit_cb = _menuitem_edit_preset,
    .del_cb = _menuitem_delete_preset,
    .store_cb = _menuitem_new_preset,
    .update_cb = _menuitem_update_preset,
    .prefs = minfo->module->set_preferences ? _lib_presets_prefs : NULL,
  };

  darktable.gui->active_popover_menu = dt_gui_presets_popup_menu_show(button, &ops);

  if(button)
    dtgtk_button_set_active(DTGTK_BUTTON(button), FALSE);
}

void dt_lib_gui_set_expanded(dt_lib_module_t *module, const gboolean expanded)
{
  if(!module->expander || !module->arrow) return;

  dtgtk_expander_set_expanded(DTGTK_EXPANDER(module->expander), expanded);

  /* update expander arrow state */
  gint flags = (expanded ? CPF_DIRECTION_DOWN : CPF_DIRECTION_RIGHT);
  dtgtk_button_set_paint(DTGTK_BUTTON(module->arrow), dtgtk_cairo_paint_solid_arrow,
                         flags, NULL);

  darktable.lib->gui_module = expanded ? module : NULL;

  /* if expanded, make sure the module is visible */
  if(module->expanded_state)
    module->expanded_state(module, expanded);

  /* store expanded state of module */
  char var[1024];
  const dt_view_t *current_view = dt_view_manager_get_current_view(darktable.view_manager);
  snprintf(var, sizeof(var), "plugins/%s/%s/expanded",
           current_view->module_name, module->plugin_name);
  dt_conf_set_bool(var, expanded);
}

gboolean dt_lib_gui_get_expanded(dt_lib_module_t *module)
{
  if(!module->expandable(module)) return true;
  if(!module->expander) return true;
  if(!module->widget)
  {
    char var[1024];
    const dt_view_t *current_view =
      dt_view_manager_get_current_view(darktable.view_manager);
    snprintf(var, sizeof(var), "plugins/%s/%s/expanded",
             current_view->module_name, module->plugin_name);
    return dt_conf_get_bool(var);
  }
  return dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander));
}

static void _lib_plugin_arrow_button_press_cb(GtkGestureSingle *gesture,
                                                gint n_press,
                                                gdouble x,
                                                gdouble y,
                                                dt_lib_module_t *module)
{
  // every press toggles, n_press > 1 included: the gesture reports a quick
  // second click on the same spot as press 2 of a sequence, and skipping it
  // swallowed a real click. GTK3's button-press-event sent each click as a
  // plain press, plus a separate GDK_2BUTTON_PRESS, the only one skipped
  const guint button = gtk_gesture_single_get_current_button(gesture);
  const GdkModifierType state =
    dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));

  if(button == GDK_BUTTON_PRIMARY)
  {
    /* bail out if module is static or disabled */
    if(!module->expandable(module) || (module->arrow && !gtk_widget_is_sensitive(module->arrow))) return;

    if(dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK))
      ; // do nothing (for easier dragging)
    /* handle shiftclick on expander, hide all except this */
    else if(!dt_conf_get_bool("lighttable/ui/single_module") !=
            !dt_modifier_is(state, GDK_SHIFT_MASK))
    {
      const dt_view_t *v = dt_view_manager_get_current_view(darktable.view_manager);
      const uint32_t side = dt_lib_get_container(module);
      gboolean all_other_closed = TRUE;
      for(const GList *it = darktable.lib->plugins; it; it = g_list_next(it))
      {
        dt_lib_module_t *m = it->data;

        if(m != module
           && dt_lib_get_container(m) == side
           && m->expandable(m) && dt_lib_is_visible_in_view(m, v))
        {
          all_other_closed = all_other_closed
            && !dtgtk_expander_get_expanded(DTGTK_EXPANDER(m->expander));
          dt_lib_gui_set_expanded(m, FALSE);
        }
      }
      if(all_other_closed)
        dt_lib_gui_set_expanded
          (module,
           !dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander)));
      else
        dt_lib_gui_set_expanded(module, TRUE);
    }
    else
    {
      /* else just toggle */
      dt_lib_gui_set_expanded
        (module,
         !dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander)));
    }

    //ensure that any gtkentry fields lose focus
    gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));

    return;
  }
  else if(button == GDK_BUTTON_SECONDARY)
  {
    if(gtk_widget_get_sensitive(module->presets_button))
      _presets_popup_callback(module->presets_button, module);

    return;
  }
}

static void _lib_plugin_header_button_release_cb(GtkGestureSingle *gesture,
                                                    gint n_press,
                                                    gdouble x,
                                                    gdouble y,
                                                    dt_lib_module_t *module)
{
  // ignore clicks on buttons inside the header (presets, reset, enable)
#if GTK_CHECK_VERSION(4, 0, 0)
  /* GTK4 has no gtk_get_event_widget(): find the widget under the release
   * point inside the header instead, and ignore the release if it lands on
   * (or inside) a button -- the gesture receives releases over the whole
   * header, including its child buttons.  The surface-compare alternative
   * is wrong: gdk_event_get_surface() always returns this widget's own
   * surface. */
  GtkWidget *const header = dt_gui_get_widget(gesture);
  GtkWidget *target = gtk_widget_pick(header, x, y, GTK_PICK_DEFAULT);
  for(; target && target != header; target = gtk_widget_get_parent(target))
    if(GTK_IS_BUTTON(target)) return;
#else
  const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL);
  if(event && GTK_IS_BUTTON(gtk_get_event_widget((GdkEvent *)event)))
    return;
#endif
  _lib_plugin_arrow_button_press_cb(gesture, n_press, x, y, module);
}

static void show_module_callback(dt_lib_module_t *module)
{
  /* bail out if module is static or disabled */
  if(!module->expandable(module) || (module->arrow && !gtk_widget_is_sensitive(module->arrow))) return;

  if(dt_conf_get_bool("lighttable/ui/single_module"))
  {
    const dt_view_t *v = dt_view_manager_get_current_view(darktable.view_manager);
    const uint32_t side = dt_lib_get_container(module);
    gboolean all_other_closed = TRUE;
    for(const GList *it = darktable.lib->plugins; it; it = g_list_next(it))
    {
      dt_lib_module_t *m = it->data;

      if(m != module
         && dt_lib_get_container(m) == side
         && m->expandable(m)
         && dt_lib_is_visible_in_view(m, v))
      {
        all_other_closed = all_other_closed
          && !dtgtk_expander_get_expanded(DTGTK_EXPANDER(m->expander));
        dt_lib_gui_set_expanded(m, FALSE);
      }
    }
    if(all_other_closed)
      dt_lib_gui_set_expanded
        (module, !dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander)));
    else
      dt_lib_gui_set_expanded(module, TRUE);
  }
  else
  {
    /* else just toggle */
    dt_lib_gui_set_expanded
      (module, !dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander)));
  }
}

static void _header_enter_notify_callback(GtkEventControllerMotion *controller,
                                            gdouble x,
                                            gdouble y,
                                            gpointer user_data)
{
  darktable.control->element = GPOINTER_TO_INT(user_data);
}

static void _body_enter_callback(GtkEventControllerMotion *controller,
                                   gdouble x,
                                   gdouble y,
                                   dt_lib_module_t *module)
{
  // set focused module when entering (not when opening popup)
  // only act on non-inferior (not from child) normal crossings
  GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller));
  if(event)
  {
#if GTK_CHECK_VERSION(4, 0, 0)
    if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR
       && gdk_crossing_event_get_mode(event) == GDK_CROSSING_NORMAL)
#else
    if(event->crossing.detail != GDK_NOTIFY_INFERIOR
       && event->crossing.mode == GDK_CROSSING_NORMAL)
#endif
      darktable.lib->gui_module = module;
#if !GTK_CHECK_VERSION(4, 0, 0)
    gdk_event_free(event);
#endif
  }
}

static void _body_leave_callback(GtkEventControllerMotion *controller,
                                  dt_lib_module_t *module)
{
  // clear focused module when leaving
  // only act on non-inferior (not to child) normal crossings
  GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller));
  if(event)
  {
#if GTK_CHECK_VERSION(4, 0, 0)
    if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR
       && gdk_crossing_event_get_mode(event) == GDK_CROSSING_NORMAL)
#else
    if(event->crossing.detail != GDK_NOTIFY_INFERIOR
       && event->crossing.mode == GDK_CROSSING_NORMAL)
#endif
      darktable.lib->gui_module = NULL;
#if !GTK_CHECK_VERSION(4, 0, 0)
    gdk_event_free(event);
#endif
  }
}

static gboolean _on_drag_motion(GtkWidget *widget,
                                GdkDragContext *dc,
                                gint x, gint y, guint time,
                                dt_lib_module_t *dest)
{
  dt_lib_module_t *src = NULL;
  GtkContainer *src_panel = NULL, *dest_panel = NULL;
  gboolean below = TRUE;
  int dest_pos = 0;

  if(GTK_IS_BOX(dc))
  {
    // propagated from empty panel handler in gtk.c
    src = dest;
    src_panel = GTK_CONTAINER(gtk_widget_get_parent(src->expander));
    dest_panel = GTK_CONTAINER(dc);
  }
  else
  {
    dtgtk_expander_set_drag_hover(DTGTK_EXPANDER(widget), FALSE, TRUE, time);
    gdk_drag_status(dc, 0, time);

    GtkWidget *src_header = gtk_drag_get_source_widget(dc);
    if(!src_header) return TRUE;
    GtkWidget *src_expander = gtk_widget_get_ancestor(src_header, DTGTK_TYPE_EXPANDER);

    for(GList *lib = darktable.lib->plugins; lib; lib = lib->next)
      if(((dt_lib_module_t *)lib->data)->expander == src_expander)
        src = lib->data;

    if(!src_expander || !src || dest == src ) return TRUE;

    src_panel = GTK_CONTAINER(gtk_widget_get_parent(src->expander));
    dest_panel = GTK_CONTAINER(gtk_widget_get_parent(dest->expander));

    int src_pos = G_MAXINT;
    if(dest_panel == src_panel)
      gtk_container_child_get(src_panel, src->expander, "position", &src_pos, NULL);
    gtk_container_child_get(dest_panel, dest->expander, "position", &dest_pos, NULL);

    GtkDarktableExpander *exp = DTGTK_EXPANDER(dest->expander);
    int header_height = gtk_widget_get_allocated_height(dtgtk_expander_get_header(exp));
    below = y > (ABS(dest_pos - src_pos) == 1 && !dtgtk_expander_get_expanded(exp)
                 ? header_height / 2
                 : dest_pos < src_pos
                 ? gtk_widget_get_allocated_height(widget) - header_height
                 : header_height);

    if(below) dest_pos++;
    if(dest_pos > src_pos) dest_pos--;
    if(dest_pos == src_pos) return TRUE;

    if(x != DND_DROP)
    {
      dtgtk_expander_set_drag_hover(DTGTK_EXPANDER(widget), TRUE, below, time);
      gdk_drag_status(dc, GDK_ACTION_COPY, time);

      return TRUE;
    }
  }

  GtkBox *dest_box = GTK_BOX(dest_panel);
  if(dest_panel != src_panel)
  {
    g_object_ref(src->expander);
    gtk_container_remove(src_panel, src->expander);
    gtk_box_pack_start(dest_box, src->expander, FALSE, FALSE, 0);
  }
  gtk_box_reorder_child(dest_box, src->expander, dest_pos);

  GList **list = &darktable.lib->plugins;
  *list = g_list_remove(*list, src);

  GList *dest_list = g_list_find(*list, dest);
  if(below)
    dest = dest_list && dest_list->prev ? dest_list->prev->data : NULL;
  else
    dest_list = dest_list->next;

  int new_pos = dest ? ABS(_lib_position(dest)) + 1 : 1;
  int old_pos = dest_box != dt_ui_get_container(darktable.gui->ui, src->container(src)) ? -1 : +1;
  dt_lib_module_t *cur = src;
  while(new_pos >= ABS(old_pos))
  {
    gchar *key = _get_lib_view_path(cur, NULL, "_position");
    dt_conf_set_int(key, old_pos < 0 ? -new_pos : new_pos);
    g_free(key);

    if(!dest_list) break;

    new_pos++;
    cur = dest_list->data;
    old_pos = _lib_position(cur);
    dest_list = dest_list->next;
  }

  *list = g_list_insert_sorted(*list, src, dt_lib_sort_plugins);

  return TRUE;
}

static gboolean _on_drag_drop(GtkWidget *widget,
                              GdkDragContext *dc,
                              gint x, gint y, guint time,
                              dt_lib_module_t *dest)
{
  return _on_drag_motion(widget, dc, DND_DROP, y, time, dest);
}

GtkWidget *dt_lib_gui_get_expander(dt_lib_module_t *module)
{
  /* check if module is expandable */
  if(!module->expandable(module))
  {
    /* Non-expandable libs (modulegroups, ...) keep their whole widget -- and
     * with it the presets button -- outside an expander, and that widget
     * survives view switches: dt_lib_gui_get_expander() runs once per switch,
     * so the "clicked" wiring and the action registration below must happen
     * only on the first call, or every lighttable<->darkroom round trip would
     * stack one more popup and one more action on the button.  Registering the
     * button with the lib actions lets shortcuts and the action fallback
     * (DT_ACTION_ELEMENT_PRESETS in _action_process) reach it, like the
     * expandable header path does.  GtkButton::clicked is unchanged on GTK4
     * and dt_action_define() is qdata-guarded, so this needs no
     * #if GTK_CHECK_VERSION split. */
    if(module->presets_button
       && !g_object_get_data(G_OBJECT(module->presets_button),
                             "dt-lib-presets-wired"))
    {
      g_signal_connect(G_OBJECT(module->presets_button), "clicked",
                       G_CALLBACK(_presets_popup_callback), module);
      dt_action_define(&module->actions, NULL, NULL, module->presets_button, NULL);
      g_object_set_data(G_OBJECT(module->presets_button),
                        "dt-lib-presets-wired", GINT_TO_POINTER(TRUE));
    }
    module->expander = NULL;
    return NULL;
  }

  GtkWidget *header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_widget_set_name(GTK_WIDGET(header), "module-header");

  GtkWidget *expander = dtgtk_expander_new(header, module->widget);
  GtkWidget *header_evb = dtgtk_expander_get_header_event_box(DTGTK_EXPANDER(expander));
  GtkWidget *body_evb = dtgtk_expander_get_body_event_box(DTGTK_EXPANDER(expander));
  GtkWidget *pluginui_frame = dtgtk_expander_get_frame(DTGTK_EXPANDER(expander));

  dt_ui_container_t container = module->container(module);
  if(container == DT_UI_CONTAINER_PANEL_LEFT_CENTER || container == DT_UI_CONTAINER_PANEL_RIGHT_CENTER)
  {
    static const GtkTargetEntry target_list[] = { { "lib", GTK_TARGET_SAME_APP, DND_TARGET_LIB } };

    gtk_drag_source_set(header_evb, GDK_BUTTON1_MASK, target_list, 1, GDK_ACTION_COPY);
    gtk_drag_dest_set(expander, GTK_DEST_DEFAULT_DROP | GTK_DEST_DEFAULT_HIGHLIGHT, target_list, 1, GDK_ACTION_COPY);
    g_signal_connect(expander, "drag-motion", G_CALLBACK(_on_drag_motion), module);
    g_signal_connect(expander, "drag-drop", G_CALLBACK(_on_drag_drop), module);
  }

  /* setup the header box */
  dt_gui_connect_click(header_evb, NULL, _lib_plugin_header_button_release_cb, module);
  dt_gui_connect_motion(header_evb, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(DT_ACTION_ELEMENT_SHOW));

  /* (un)focus module when entering/leaving body */
  dt_gui_connect_motion(body_evb, NULL, _body_enter_callback, _body_leave_callback, module);

  /*
   * initialize the header widgets
   */
  /* add the expand indicator icon */
  module->arrow = dtgtk_button_new_full(dtgtk_cairo_paint_solid_arrow, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("show module"),
        .action = &module->actions,
      });

  dt_gui_connect_click_all(module->arrow, _lib_plugin_arrow_button_press_cb, NULL, module);
  gtk_box_pack_start(GTK_BOX(header), module->arrow, FALSE, FALSE, 0);

  /* add module label */
  GtkWidget *label = gtk_label_new("");
  GtkWidget *label_evb = gtk_event_box_new();
  gtk_container_add(GTK_CONTAINER(label_evb), label);
  gchar *mname = g_markup_escape_text(module->name(module), -1);
  gtk_label_set_markup(GTK_LABEL(label), mname);
  if(module->description)
    gtk_widget_set_tooltip_text(header, module->description(module));
  else
    gtk_widget_set_tooltip_text(header, mname);
  g_free(mname);
  gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
  g_object_set(G_OBJECT(label), "halign", GTK_ALIGN_START, "xalign", 0.0, (gchar *)0);
  gtk_widget_set_name(label, "lib-panel-label");
  dt_action_define(&module->actions, NULL, NULL, label_evb, NULL);
  gtk_box_pack_start(GTK_BOX(header), label_evb, FALSE, FALSE, 0);

  /* add preset label */
  module->preset_label = gtk_label_new(NULL);
  gtk_widget_set_name(module->preset_label, "lib-module-name");
  gtk_label_set_ellipsize(GTK_LABEL(module->preset_label), PANGO_ELLIPSIZE_MIDDLE);
  gtk_widget_set_valign(module->preset_label, GTK_ALIGN_BASELINE);
  g_object_set(G_OBJECT(module->preset_label), "xalign", 0.0, (gchar *)0);
  gtk_box_pack_start(GTK_BOX(header), module->preset_label, FALSE, FALSE, 0);

  /* add preset button if module has implementation */
  module->presets_button = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("presets and preferences"),
        .action = &module->actions,
        .clicked_cb = G_CALLBACK(_presets_popup_callback),
        .clicked_data = module,
      });
  dt_gui_connect_motion(module->presets_button, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(DT_ACTION_ELEMENT_PRESETS));
  if(!module->get_params
     && !module->set_preferences)
    gtk_widget_set_sensitive(GTK_WIDGET(module->presets_button), FALSE);
  gtk_box_pack_end(GTK_BOX(header), module->presets_button, FALSE, FALSE, 0);

  /* add reset button if module has implementation */
  module->reset_button = dtgtk_button_new_full(dtgtk_cairo_paint_reset, 0, NULL,
      &(dtgtk_button_config_t){
        .action = &module->actions,
        .clicked_cb = G_CALLBACK(_lib_gui_reset_button_clicked_callback),
        .clicked_data = module,
      });
  /* "clicked" rather than a custom gesture: see _lib_gui_reset_button_clicked_callback */
  dt_gui_connect_motion(module->reset_button, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(DT_ACTION_ELEMENT_RESET));
  if(!module->gui_reset) gtk_widget_set_sensitive(module->reset_button, FALSE);
  gtk_box_pack_end(GTK_BOX(header), module->reset_button, FALSE, FALSE, 0);

  /* add button box - for module's specific action button */
  if(module->gui_tool_box)
    gtk_box_pack_end(GTK_BOX(header), module->gui_tool_box(module), FALSE, FALSE, 0);

  gtk_widget_show_all(expander);

  if(module->widget)
  {
    dt_gui_add_class(module->widget, "dt_plugin_ui_main");
    gtk_widget_set_hexpand(module->widget, FALSE);
    gtk_widget_set_vexpand(module->widget, FALSE);
  }
  dt_gui_add_class(pluginui_frame, "dt_plugin_ui");
  module->expander = expander;

  // get active preset and set preset_label
  if(module->has_preset_label && module->has_preset_label(module))
  {
    dt_lib_module_info_t *mi = _get_module_info_for_module(module);
    gchar *preset_name = dt_lib_get_active_preset_name(mi);
    if(preset_name)
    {
      _set_module_preset_label(mi->module, preset_name);
      g_free(preset_name);
    }
    _free_module_info(mi);
  }

  return module->expander;
}

void dt_lib_gui_set_label(dt_lib_module_t *module,
                          const char *label)
{
  if(!module->expander) return;
  GtkWidget *header = DTGTK_EXPANDER(module->expander)->header;
  gtk_box_set_center_widget(GTK_BOX(header), gtk_label_new(label));
  gtk_widget_show_all(header);
}

static void _preferences_changed(gpointer instance, gpointer self)
{
  // reload presets if they are based on the actual workflow which
  // could have been changed after editing the preferences.

  dt_lib_t *lib = (dt_lib_t *)self;

  GList *p = lib->plugins;

  while(p)
  {
    dt_lib_module_t *lmod = p->data;

    if(lmod->pref_based_presets)
      dt_lib_init_presets(lmod);

    p = g_list_next(p);
  }
}

void dt_lib_init(dt_lib_t *lib)
{
  // Setting everything to null initially
  memset(lib, 0, sizeof(dt_lib_t));
  darktable.lib->plugins = dt_module_load_modules("/plugins/lighttable",
                                                  sizeof(dt_lib_module_t),
                                                  dt_lib_load_module,
                                                  dt_lib_init_module,
                                                  dt_lib_sort_plugins);
  DT_CONTROL_SIGNAL_CONNECT(DT_SIGNAL_PREFERENCES_CHANGE, _preferences_changed, lib);
}

void dt_lib_cleanup(dt_lib_t *lib)
{
  DT_CONTROL_SIGNAL_DISCONNECT(_preferences_changed, lib);
  while(lib->plugins)
  {
    dt_lib_module_t *module = lib->plugins->data;
    if(module)
    {
      if(module->data != NULL)
      {
        DT_CONTROL_SIGNAL_DISCONNECT_ALL(module, module->plugin_name);
        module->gui_cleanup(module);
        module->data = NULL;
      }
      _lib_unload_module(module);
      free(module);
    }
    lib->plugins = g_list_delete_link(lib->plugins, lib->plugins);
  }
}

void dt_lib_presets_add(const char *name,
                        const char *plugin_name,
                        const int32_t version,
                        const void *params,
                        const int32_t params_size,
                        const gboolean readonly,
                        const dt_gui_presets_format_flag_t format)
{
  dt_lib_presets_remove(name, plugin_name, version);
  dt_presets_add_lib(name, plugin_name, version, params, params_size, readonly, format);
}

static gchar *_get_lib_view_path(const dt_lib_module_t *module,
                                 const dt_view_t *cv,
                                 char *suffix)
{
  if(!cv && darktable.view_manager)
    cv = dt_view_manager_get_current_view(darktable.view_manager);
  if(!cv) return NULL;
  // in lighttable, we store panels states per layout
  char lay[32] = "";
  if(g_strcmp0(cv->module_name, "lighttable") == 0 && !module->expandable((gpointer)module))
  {
    if(dt_view_lighttable_preview_state(darktable.view_manager))
      g_snprintf(lay, sizeof(lay), "preview/");
    else
      g_snprintf(lay, sizeof(lay), "%d/",
                 dt_view_lighttable_get_layout(darktable.view_manager));
  }
  else if(g_strcmp0(cv->module_name, "darkroom") == 0)
  {
    g_snprintf(lay, sizeof(lay), "%d/",
               dt_view_darkroom_get_layout(darktable.view_manager));
  }

  return g_strdup_printf("plugins/%s/%s%s%s", cv->module_name,
                         lay, module->plugin_name, suffix);
}

gboolean dt_lib_is_visible(dt_lib_module_t *module)
{
  return dt_lib_is_visible_in_view(module, dt_view_manager_get_current_view(darktable.view_manager));
}

void dt_lib_set_visible(dt_lib_module_t *module,
                        const gboolean visible)
{
  gchar *key = _get_lib_view_path(module, NULL, "_visible");
  GtkWidget *widget;
  if(key)
    dt_conf_set_bool(key, visible);
  g_free(key);

  if(module->widget)
  {
    if(module->expander)
      widget = module->expander;
    else
      widget = module->widget;

    if(visible)
      gtk_widget_show(GTK_WIDGET(widget));
    else
      gtk_widget_hide(GTK_WIDGET(widget));
  }
}

gchar *dt_lib_get_localized_name(const gchar *plugin_name)
{
  // Prepare mapping op -> localized name
  static GHashTable *module_names = NULL;
  if(module_names == NULL)
  {
    module_names = g_hash_table_new(g_str_hash, g_str_equal);
    for(const GList *lib = darktable.lib->plugins; lib; lib = g_list_next(lib))
    {
      dt_lib_module_t *module = lib->data;
      g_hash_table_insert(module_names, module->plugin_name,
                          g_strdup(module->name(module)));
    }
  }

  return (gchar *)g_hash_table_lookup(module_names, plugin_name);
}

void dt_lib_colorpicker_set_box_area(dt_lib_t *lib,
                                     const dt_pickerbox_t box)
{
  if(!lib || !lib->proxy.colorpicker.module || !lib->proxy.colorpicker.set_sample_box_area) return;
  lib->proxy.colorpicker.set_sample_box_area(lib->proxy.colorpicker.module, box);
  gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));
}

void dt_lib_colorpicker_set_point(dt_lib_t *lib,
                                  const float pos[2])
{
  if(!lib || !lib->proxy.colorpicker.module || !lib->proxy.colorpicker.set_sample_point) return;
  lib->proxy.colorpicker.set_sample_point(lib->proxy.colorpicker.module, pos);
  gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));
}

/* clear color picker pos */
void dt_lib_colorpicker_reset_box_area(dt_pickerbox_t box)
{
  dt_boundingbox_t reset = { 0.02f, 0.02f, 0.98f, 0.98f };
  dt_color_picker_backtransform_box(darktable.develop, 2, reset, box);
}

void dt_lib_colorpicker_reset_point(dt_pickerpoint_t pos)
{
  dt_boundingbox_t reset = { 0.5f, 0.5f };
  dt_color_picker_backtransform_box(darktable.develop, 1, reset, pos);
}

void dt_lib_colorpicker_setup(dt_lib_t *lib,
                              const gboolean denoise,
                              const gboolean pick_output)

{
  if(!lib || !lib->proxy.colorpicker.module || !lib->proxy.colorpicker.setup_sample) return;
  lib->proxy.colorpicker.setup_sample(lib->proxy.colorpicker.module, denoise, pick_output);
}

void dt_lib_histogram_get_harmony(dt_lib_t *lib, dt_color_harmony_guide_t *guide)
{
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.get_harmony) return;
  lib->proxy.histogram.get_harmony(lib->proxy.histogram.module, guide);
}

void dt_lib_histogram_set_harmony(dt_lib_t *lib, const dt_color_harmony_guide_t *guide)
{
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.set_harmony) return;
  lib->proxy.histogram.set_harmony(lib->proxy.histogram.module, guide);
}

void dt_lib_histogram_set_harmony_callback(dt_lib_t *lib,
    void (*cb)(const dt_color_harmony_guide_t *, void *), void *user_data)
{
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.set_harmony_callback) return;
  lib->proxy.histogram.set_harmony_callback(lib->proxy.histogram.module, cb, user_data);
}

void dt_lib_histogram_set_scope(dt_lib_t *lib, int scope)
{
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.set_scope) return;
  lib->proxy.histogram.set_scope(lib->proxy.histogram.module, scope);
}

void dt_lib_histogram_set_type(dt_lib_t *lib, int type)
{
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.set_type) return;
  lib->proxy.histogram.set_type(lib->proxy.histogram.module, type);
}

void dt_lib_histogram_get_sector_angles(dt_lib_t *lib,
                                        const dt_color_harmony_type_t type,
                                        const int rotation,
                                        float *angles,
                                        int *n)
{
  if(n) *n = 0;
  if(!lib || !lib->proxy.histogram.module || !lib->proxy.histogram.get_sector_angles) return;
  lib->proxy.histogram.get_sector_angles(lib->proxy.histogram.module, type, rotation, angles, n);
}

dt_lib_module_t *dt_lib_get_module(const char *name)
{
  if(!darktable.lib) return NULL;
  /* hide/show modules as last config */
  for(GList *iter = darktable.lib->plugins; iter; iter = g_list_next(iter))
  {
    dt_lib_module_t *plugin = iter->data;
    if(strcmp(plugin->plugin_name, name) == 0)
      return plugin;
  }

  return NULL;
}

gboolean dt_lib_presets_can_autoapply(dt_lib_module_t *mod)
{
  return mod->preset_autoapply(mod);
}

void dt_lib_reset_preset_label(dt_lib_module_t *mod)
{
  if(!mod || !mod->has_preset_label(mod))
    return;

  gtk_label_set_text(GTK_LABEL(mod->preset_label), "");
}

static float _action_process(gpointer target,
                             const dt_action_element_t element,
                             const dt_action_effect_t effect,
                             const float move_size)
{
  dt_lib_module_t *module = target;

  if(DT_PERFORM_ACTION(move_size))
  {
    switch(element)
    {
    case DT_ACTION_ELEMENT_SHOW:
      if(DT_ACTION_TOGGLE_NEEDED(effect, move_size,
           dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander))))
        show_module_callback(module);
      break;
    case DT_ACTION_ELEMENT_RESET:
      if(module->gui_reset)
        _lib_gui_reset_callback(NULL, 1, 0.0, 0.0, module);
      break;
    case DT_ACTION_ELEMENT_PRESETS:
      if(module->get_params || module->set_preferences)
        _presets_popup_callback(module->presets_button, module);
      break;
    }
  }

  return element == DT_ACTION_ELEMENT_SHOW
    && dtgtk_expander_get_expanded(DTGTK_EXPANDER(module->expander));
}

static const dt_action_element_def_t _action_elements[]
  = { { N_("show"), dt_action_effect_toggle },
      { N_("reset"), dt_action_effect_activate },
      { N_("presets"), dt_action_effect_presets },
      { NULL } };

static const dt_shortcut_fallback_t _action_fallbacks[]
  = { { .element = DT_ACTION_ELEMENT_SHOW,
        .button = DT_SHORTCUT_LEFT },
      { .element = DT_ACTION_ELEMENT_RESET,
        .button = DT_SHORTCUT_LEFT,
        .click = DT_SHORTCUT_DOUBLE },
      { .element = DT_ACTION_ELEMENT_PRESETS,
        .button = DT_SHORTCUT_RIGHT },
      { } };

const dt_action_def_t dt_action_def_lib
  = { N_("utility module"),
      _action_process,
      _action_elements,
      _action_fallbacks };

gboolean dt_handle_dialog_enter(GtkEventControllerKey *key,
                                guint keyval,
                                guint keycode,
                                GdkModifierType state,
                                gpointer data)
{
  if(keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter)
  {
    gtk_dialog_response(GTK_DIALOG(gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(key))),
                        GTK_RESPONSE_ACCEPT);
    return TRUE;
  }
  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