/* 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