/*
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 .
*/
#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
#include
#include
#include
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: ¶ms=%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