/*
This file is part of darktable,
Copyright (C) 2011-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 "bauhaus/bauhaus.h"
#include "common/darktable.h"
#include "common/debug.h"
#include "common/image_cache.h"
#include "common/iop_group.h"
#include "common/presets.h"
#include "control/conf.h"
#include "control/control.h"
#include "develop/develop.h"
#include "dtgtk/button.h"
#include "dtgtk/icon.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "libs/lib.h"
#include "libs/lib_api.h"
#include
#include
#include
#include
#include
#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif
DT_MODULE(1)
// the T_ macros are for the translation engine to take them into account
#define FALLBACK_PRESET_NAME "workflow: scene-referred"
#define T_FALLBACK_PRESET_NAME _("workflow: scene-referred")
#define DEPRECATED_PRESET_NAME "modules: deprecated"
#define T_DEPRECATED_PRESET_NAME _("modules: deprecated")
#define CURRENT_PRESET_NAME "last modified layout"
#define T_CURRENT_PRESET_NAME _("last modified layout")
// list of recommended basics widgets
#define RECOMMENDED_BASICS \
"|exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast|colorbalancergb/global " \
"vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation|ashift/rotation|denoiseprofile|lens|bilat|"
// if a preset cannot be loaded or the current preset deleted, this is the fallback preset
#define PADDING 2
#include "modulegroups.h"
typedef enum dt_lib_modulegroups_basic_item_position_t
{
NORMAL,
NEW_MODULE,
FIRST_MODULE
} dt_lib_modulegroups_basic_item_position_t;
typedef enum dt_lib_modulegroups_basic_item_type_t
{
WIDGET_TYPE_NONE = 0,
WIDGET_TYPE_BAUHAUS_SLIDER,
WIDGET_TYPE_BAUHAUS_COMBO,
WIDGET_TYPE_ACTIVATE_BTN,
WIDGET_TYPE_MISC
} dt_lib_modulegroups_basic_item_type_t;
typedef struct dt_lib_modulegroups_basic_item_t
{
gchar *id;
gchar *module_op;
gchar *widget_name; // translated
GtkWidget *widget;
GtkWidget *temp_widget;
GtkWidget *old_parent;
dt_lib_modulegroups_basic_item_type_t widget_type;
int old_pos;
gboolean expand;
gboolean fill;
guint padding;
GtkPackType packtype;
gchar *tooltip;
int grid_x, grid_y, grid_w, grid_h;
GtkWidget *box;
dt_iop_module_t *module;
} dt_lib_modulegroups_basic_item_t;
typedef struct dt_lib_modulegroups_group_t
{
gchar *name;
GtkWidget *button;
gchar *icon;
GtkWidget *iop_box;
// default
GList *modules;
} dt_lib_modulegroups_group_t;
typedef struct dt_lib_modulegroups_t
{
int32_t current;
GtkWidget *text_entry;
GtkWidget *hbox_buttons;
GtkWidget *active_btn;
GtkWidget *basic_btn;
GtkWidget *hbox_groups;
GtkWidget *hbox_search_box;
GtkWidget *deprecated;
gboolean force_deprecated_message;
GList *groups;
gboolean show_search;
gboolean full_active;
GList *edit_groups;
gboolean edit_show_search;
gboolean edit_full_active;
gchar *edit_preset;
gboolean edit_ro;
gboolean edit_basics_show;
GList *edit_basics;
// editor dialog
GtkWidget *dialog;
gboolean editor_reset;
GtkWidget *presets_combo, *presets_btn_remove, *presets_btn_dup, *presets_btn_rename, *presets_btn_new;
GtkWidget *preset_groups_box, *preset_btn_add_group, *preset_read_only_label, *preset_reset_btn;
GtkWidget *edit_search_cb, *edit_full_active_cb;
GtkWidget *basics_chkbox, *edit_basics_groupbox, *edit_basics_box;
GtkWidget *edit_autoapply_chkbox, *edit_autoapply_btn;
gboolean basics_show;
GList *basics;
GtkWidget *vbox_basic;
GtkWidget *mod_vbox_basic;
dt_iop_module_t *force_show_module;
} dt_lib_modulegroups_t;
typedef enum dt_lib_modulegroup_iop_visibility_type_t
{
DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE,
DT_MODULEGROUP_SEARCH_IOP_GROUPS_VISIBLE,
DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE
} dt_lib_modulegroup_iop_visibility_type_t;
static GtkWidget *_active_popover_menu = NULL;
/* toggle button callback */
static void _lib_modulegroups_toggle(GtkWidget *button, dt_lib_module_t *self);
/* helper function to update iop module view depending on group */
static void _lib_modulegroups_update_iop_visibility(dt_lib_module_t *self);
/* modulergroups proxy set group function
\see dt_dev_modulegroups_set()
*/
static void _lib_modulegroups_set(dt_lib_module_t *self, uint32_t group);
/* modulegroups proxy update visibility function
*/
static void _lib_modulegroups_update_visibility_proxy(dt_lib_module_t *self);
/* modulegroups proxy get group function
\see dt_dev_modulegroups_get()
*/
static uint32_t _lib_modulegroups_get(dt_lib_module_t *self);
/* modulegroups proxy test function.
tests if iop module group flags matches modulegroup.
*/
static gboolean _lib_modulegroups_test(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module);
/* modulegroups proxy test visibility function.
tests if iop module is preset in one groups for current layout.
*/
static gboolean _lib_modulegroups_test_visible(dt_lib_module_t *self, gchar *module);
/* modulegroups proxy switch group function.
sets the active group which module belongs too.
*/
static void _lib_modulegroups_switch_group(dt_lib_module_t *self, dt_iop_module_t *module);
static void _manage_preset_update_list(dt_lib_module_t *self);
static void _manage_editor_load(const char *preset, dt_lib_module_t *self);
static void _buttons_update(dt_lib_module_t *self);
const char *name(dt_lib_module_t *self)
{
return _("modulegroups");
}
dt_view_type_flags_t views(dt_lib_module_t *self)
{
return DT_VIEW_DARKROOM;
}
uint32_t container(dt_lib_module_t *self)
{
return DT_UI_CONTAINER_PANEL_RIGHT_TOP;
}
/* this module should always be shown without expander */
gboolean expandable(dt_lib_module_t *self)
{
return FALSE;
}
int position(const dt_lib_module_t *self)
{
return 999;
}
static GtkWidget *_buttons_get_from_pos(dt_lib_module_t *self, const int pos)
{
const dt_lib_modulegroups_t *d = self->data;
if(pos == DT_MODULEGROUP_ACTIVE_PIPE) return d->active_btn;
if(pos == DT_MODULEGROUP_BASICS) return d->basic_btn;
dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, pos - 1);
if(gr) return gr->button;
return NULL;
}
// scroll the right panel so the topmost visible module is at its top.
// scheduled with g_idle_add() so it runs after GTK has laid out the filtered modules
static gboolean _scroll_to_first_match(gpointer user_data)
{
GtkWidget *box = GTK_WIDGET(dt_ui_get_container(darktable.gui->ui,
DT_UI_CONTAINER_PANEL_RIGHT_CENTER));
GtkWidget *sw = gtk_widget_get_ancestor(box, GTK_TYPE_SCROLLED_WINDOW);
if(!sw) return G_SOURCE_REMOVE;
GList *children = gtk_container_get_children(GTK_CONTAINER(box));
for(const GList *c = children; c; c = g_list_next(c))
{
GtkWidget *w = c->data;
if(gtk_widget_get_visible(w) && !g_strcmp0(gtk_widget_get_name(w), "iop-expander"))
{
GtkAllocation a;
gtk_widget_get_allocation(w, &a);
gtk_adjustment_set_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)), a.y);
break;
}
}
g_list_free(children);
return G_SOURCE_REMOVE;
}
static void _text_entry_changed_callback(GtkEntry *entry, dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
_lib_modulegroups_update_iop_visibility(self);
const gchar *text = gtk_entry_get_text(entry);
if(text && text[0] != '\0')
g_idle_add(_scroll_to_first_match, NULL);
}
static DTGTKCairoPaintIconFunc _buttons_get_icon_fct(const gchar *icon)
{
if(g_strcmp0(icon, "active") == 0)
return dtgtk_cairo_paint_modulegroup_active;
else if(g_strcmp0(icon, "favorites") == 0)
return dtgtk_cairo_paint_modulegroup_favorites;
else if(g_strcmp0(icon, "tone") == 0)
return dtgtk_cairo_paint_modulegroup_tone;
else if(g_strcmp0(icon, "color") == 0)
return dtgtk_cairo_paint_modulegroup_color;
else if(g_strcmp0(icon, "correct") == 0)
return dtgtk_cairo_paint_modulegroup_correct;
else if(g_strcmp0(icon, "effect") == 0)
return dtgtk_cairo_paint_modulegroup_effect;
else if(g_strcmp0(icon, "grading") == 0)
return dtgtk_cairo_paint_modulegroup_grading;
else if(g_strcmp0(icon, "technical") == 0)
return dtgtk_cairo_paint_modulegroup_technical;
return dtgtk_cairo_paint_modulegroup_basic;
}
static gint _iop_compare(gconstpointer a, gconstpointer b)
{
return g_strcmp0((gchar *)a, (gchar *)b);
}
static gboolean _lib_modulegroups_test_internal(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module)
{
if(group == DT_MODULEGROUP_ACTIVE_PIPE) return module->enabled;
dt_lib_modulegroups_t *d = self->data;
dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, group - 1);
if(gr)
{
return (g_list_find_custom(gr->modules, module->so->op, _iop_compare) != NULL);
}
return FALSE;
}
static gboolean _lib_modulegroups_test(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module)
{
return _lib_modulegroups_test_internal(self, group, module);
}
static gboolean _lib_modulegroups_test_visible(dt_lib_module_t *self, gchar *module)
{
dt_lib_modulegroups_t *d = self->data;
for(const GList *l = d->groups; l; l = g_list_next(l))
{
dt_lib_modulegroups_group_t *gr = l->data;
if(g_list_find_custom(gr->modules, module, _iop_compare) != NULL)
{
return TRUE;
}
}
return FALSE;
}
static void _basics_init_item(dt_lib_modulegroups_basic_item_t *item)
{
if(!item->id) return;
gchar **elems = g_strsplit(item->id, "/", -1);
if(g_strv_length(elems) > 0)
{
item->module_op = g_strdup(elems[0]);
if(item->widget && DT_IS_BAUHAUS_WIDGET(item->widget))
{
if(g_strv_length(elems) > 2)
item->widget_name = g_strdup_printf("%s - %s", _(elems[1]), dt_bauhaus_widget_get_label(item->widget));
else if(g_strv_length(elems) > 1)
item->widget_name = g_strdup(dt_bauhaus_widget_get_label(item->widget));
else
{
item->widget_name = g_strdup(_("on-off"));
item->widget_type = WIDGET_TYPE_ACTIVATE_BTN;
}
}
else
{
if(g_strv_length(elems) > 2)
item->widget_name = g_strdup_printf("%s - %s", _(elems[1]), _(elems[2]));
else if(g_strv_length(elems) > 1)
item->widget_name = g_strdup(_(elems[1]));
else
{
item->widget_name = g_strdup(_("on-off"));
item->widget_type = WIDGET_TYPE_ACTIVATE_BTN;
}
}
}
g_strfreev(elems);
}
static void _basics_free_item(dt_lib_modulegroups_basic_item_t *item)
{
g_free(item->id);
g_free(item->module_op);
if(item->tooltip) g_free(item->tooltip);
g_free(item->widget_name);
}
static void _basics_remove_widget(dt_lib_modulegroups_basic_item_t *item)
{
if(item->widget && item->widget_type != WIDGET_TYPE_ACTIVATE_BTN && item->temp_widget)
{
g_signal_handlers_disconnect_by_data(item->widget, item);
g_signal_handlers_disconnect_by_data(item->old_parent, item);
if(GTK_IS_CONTAINER(item->old_parent) && gtk_widget_get_parent(item->widget) == item->box)
{
g_object_ref(item->widget);
gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(item->widget)), item->widget);
if(GTK_IS_BOX(item->old_parent))
{
if(item->packtype == GTK_PACK_START)
gtk_box_pack_start(GTK_BOX(item->old_parent), item->widget, item->expand, item->fill, item->padding);
else
gtk_box_pack_end(GTK_BOX(item->old_parent), item->widget, item->expand, item->fill, item->padding);
gtk_box_reorder_child(GTK_BOX(item->old_parent), item->widget, item->old_pos);
}
else if(GTK_IS_GRID(item->old_parent))
{
gtk_grid_attach(GTK_GRID(item->old_parent), item->widget, item->grid_x, item->grid_y, item->grid_w,
item->grid_h);
}
g_object_unref(item->widget);
}
// put back tooltip
if(GTK_IS_WIDGET(item->widget))
{
gtk_widget_set_tooltip_text(item->widget, item->tooltip);
gtk_widget_set_has_tooltip(item->widget, TRUE); // even if no tip, to show shortcuts
}
// put back label
if(DT_IS_BAUHAUS_WIDGET(item->widget))
dt_bauhaus_widget_set_show_extended_label(item->widget, FALSE);
}
// cleanup item
item->widget = NULL;
if(item->box) gtk_widget_destroy(item->box);
item->box = NULL;
if(item->temp_widget) gtk_widget_destroy(item->temp_widget);
item->temp_widget = NULL;
item->old_parent = NULL;
item->module = NULL;
if(item->tooltip)
{
g_free(item->tooltip);
item->tooltip = NULL;
}
}
static void _basics_hide(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(!d->vbox_basic) return;
gtk_widget_hide(d->vbox_basic);
for(const GList *l = d->basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
_basics_remove_widget(item);
}
gtk_widget_destroy(d->vbox_basic);
d->vbox_basic = NULL;
}
static void _basics_goto_module(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *module)
{
dt_dev_modulegroups_switch(darktable.develop, module);
const gboolean single = dt_conf_get_bool("darkroom/ui/single_module");
dt_iop_gui_set_expanded(module, TRUE, single);
// when single_module is on, the first call with collapse_others=TRUE
// may toggle the module closed if all others were already closed;
// this second call ensures the module ends up expanded.
if(single)
dt_iop_gui_set_expanded(module, TRUE, FALSE);
}
static void _basics_on_off_label_callback(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
GtkWidget *btn)
{
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(btn)));
}
static void _sync_visibility(GtkWidget *widget,
GParamSpec *pspec,
dt_lib_modulegroups_basic_item_t *item)
{
if(widget == item->temp_widget)
gtk_widget_set_visible(item->widget, gtk_widget_get_visible(item->temp_widget));
if(widget == item->widget)
gtk_widget_set_visible(item->temp_widget, gtk_widget_get_visible(item->widget));
gtk_widget_set_visible(item->box, !dt_action_widget_invisible(item->temp_widget));
}
static void _basics_off_sync(GtkToggleButton *toggle,
GParamSpec *pspec,
GtkWidget *btn)
{
DT_ENTER_GUI_UPDATE();
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn),
gtk_toggle_button_get_active(toggle));
DT_LEAVE_GUI_UPDATE();
}
static void _manage_direct_module_popup(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_module_t *self);
static void _basics_add_widget(dt_lib_module_t *self, dt_lib_modulegroups_basic_item_t *item, GtkWidget *w,
dt_lib_modulegroups_basic_item_position_t item_pos)
{
dt_lib_modulegroups_t *d = self->data;
// if widget already exists, let's remove it and read it correctly
if(item->widget)
{
_basics_remove_widget(item);
if(item->widget) return; // we shouldn't arrive here !
}
// what type of ui we have ?
const gboolean compact_ui = !dt_conf_get_bool("plugins/darkroom/modulegroups_basics_sections_labels");
// we retrieve parents, positions, etc... so we can put the widget back in its module
if(item->widget_type == WIDGET_TYPE_ACTIVATE_BTN)
{
// we only show the on-off widget for compact ui. otherwise the button is included in the header
if(compact_ui)
{
// on-off widgets
item->widget = GTK_WIDGET(item->module->off);
item->tooltip = gtk_widget_get_tooltip_text(item->widget); // no need to copy, returns a newly-alloced string
// create new basic widget
item->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(item->box, "basics-widget");
// we create a new button linked with the real one
// because it create too much pb to remove the button from the expander
GtkWidget *btn = dt_iop_gui_header_button(item->module,
dtgtk_cairo_paint_switch,
DT_ACTION_ELEMENT_ENABLE,
item->box);
g_signal_connect_object(G_OBJECT(item->module->off), "notify::active",
G_CALLBACK(_basics_off_sync), btn, 0);
GtkWidget *evb = gtk_event_box_new();
GtkWidget *lb = gtk_label_new(item->module->name());
gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
gtk_widget_set_name(lb, "basics-iop_name");
gtk_container_add(GTK_CONTAINER(evb), lb);
/* Keep the label proxy in sync with the enable button. In particular,
* an insensitive button can still be toggled by set_active(). */
gtk_widget_set_sensitive(evb, gtk_widget_get_sensitive(btn));
dt_gui_connect_click(evb, _basics_on_off_label_callback, NULL, btn);
gtk_box_pack_start(GTK_BOX(item->box), evb, FALSE, TRUE, 0);
// disable widget if needed (multiinstance)
if(dt_iop_count_instances(item->module->so) > 1)
{
gtk_widget_set_sensitive(evb, FALSE);
gtk_widget_set_sensitive(btn, FALSE);
gtk_widget_set_tooltip_text(
lb, _("this quick access widget is disabled as there are multiple instances "
"of this module present. Please use the full module to access this widget..."));
gtk_widget_set_tooltip_text(
btn, _("this quick access widget is disabled as there are multiple instances "
"of this module present. Please use the full module to access this widget..."));
}
else
{
GtkWidget *orig_label = gtk_widget_get_parent(item->module->label);
gchar *tooltip = gtk_widget_get_tooltip_text(orig_label);
gtk_widget_set_tooltip_text(lb, tooltip);
gtk_widget_set_tooltip_text(btn, tooltip);
g_free(tooltip);
}
gtk_widget_show_all(item->box);
}
}
else
{
// classic widgets (sliders and combobox)
if(!w || !GTK_IS_WIDGET(w)) return;
if(GTK_IS_BOX(gtk_widget_get_parent(w)))
{
item->widget = w;
item->old_parent = gtk_widget_get_parent(item->widget);
// we retrieve current positions, etc...
gtk_box_query_child_packing(GTK_BOX(item->old_parent), item->widget, &item->expand, &item->fill,
&item->padding, &item->packtype);
gtk_container_child_get(GTK_CONTAINER(item->old_parent), item->widget, "position", &item->old_pos, NULL);
}
else if(GTK_IS_GRID(gtk_widget_get_parent(w)))
{
item->widget = w;
item->old_parent = gtk_widget_get_parent(item->widget);
gtk_container_child_get(GTK_CONTAINER(item->old_parent), item->widget,
"left-attach", &item->grid_x, "top-attach", &item->grid_y,
"width", &item->grid_w, "height", &item->grid_h, NULL);
}
else
{
// we don't allow other parents at the moment
return;
}
// save old tooltip
item->tooltip = gtk_widget_get_tooltip_text(item->widget);
// create new quick access widget
item->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(item->box, "basics-widget");
gtk_widget_show(item->box);
// we reparent the iop widget here
g_object_ref(item->widget);
gtk_container_remove(GTK_CONTAINER(item->old_parent), item->widget);
gtk_box_pack_start(GTK_BOX(item->box), item->widget, TRUE, TRUE, 0);
gtk_widget_set_hexpand(item->widget, FALSE);
g_object_unref(item->widget);
// change the widget label to integrate section name
if(DT_IS_BAUHAUS_WIDGET(w))
{
dt_bauhaus_widget_set_show_extended_label(item->widget, TRUE);
item->module = dt_bauhaus_widget_get_module(item->widget);
}
// we put the temporary widget at the place of the real widget in the module
// this avoid order mismatch when putting back the real widget
item->temp_widget = gtk_label_new("temp widget");
if(GTK_IS_CONTAINER(item->old_parent))
{
if(GTK_IS_BOX(item->old_parent))
{
if(item->packtype == GTK_PACK_START)
gtk_box_pack_start(GTK_BOX(item->old_parent), item->temp_widget, item->expand, item->fill, item->padding);
else
gtk_box_pack_end(GTK_BOX(item->old_parent), item->temp_widget, item->expand, item->fill, item->padding);
gtk_box_reorder_child(GTK_BOX(item->old_parent), item->temp_widget, item->old_pos);
}
else if(GTK_IS_GRID(item->old_parent))
{
gtk_grid_attach(GTK_GRID(item->old_parent), item->temp_widget, item->grid_x, item->grid_y, item->grid_w,
item->grid_h);
}
}
gchar *txt = g_strdup_printf("%s (%s)\n\n%s%s%s", item->widget_name, item->module->name(),
item->tooltip ? item->tooltip : "", item->tooltip ? "\n\n" : "",
_("(some features may only be available in the full module interface)"));
gtk_widget_set_tooltip_text(item->widget, txt);
g_free(txt);
g_signal_connect(item->widget , "notify::visible", G_CALLBACK(_sync_visibility), item);
g_signal_connect(item->old_parent , "notify::visible", G_CALLBACK(_sync_visibility), item);
g_signal_connect(item->temp_widget , "notify::visible", G_CALLBACK(_sync_visibility), item);
g_signal_connect(G_OBJECT(item->temp_widget), "destroy", G_CALLBACK(gtk_widget_destroyed), &item->temp_widget);
g_signal_connect_swapped(G_OBJECT(item->temp_widget), "destroy", G_CALLBACK(_basics_remove_widget), item);
_sync_visibility(item->widget, NULL, item);
}
// if it's the first widget of a module, we need to create the module box structure
if(item_pos != NORMAL)
{
// we create the module header box
GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *evb = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(evb), header_box);
gtk_widget_show_all(evb);
g_object_set_data(G_OBJECT(evb), "module", item->module->so);
dt_gui_connect_click_secondary(evb, _manage_direct_module_popup, NULL, self);
gtk_widget_set_name(header_box, "basics-header-box");
dt_gui_add_class(header_box, "dt_big_btn_canvas");
gtk_box_pack_start(GTK_BOX(d->vbox_basic), evb, FALSE, FALSE, 0);
// we create the module box structure
GtkWidget *hbox_basic = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hbox_basic, "basics-module-hbox");
gtk_box_pack_start(GTK_BOX(d->vbox_basic), hbox_basic, TRUE, TRUE, 0);
d->mod_vbox_basic = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(hbox_basic), d->mod_vbox_basic, TRUE, TRUE, 0);
gtk_widget_show_all(hbox_basic);
// we create the link to the full iop
gchar *tt = g_strdup_printf(_("go to the full version of the %s module"), item->module->name());
GtkWidget *wbt = dtgtk_button_new_full(dtgtk_cairo_paint_link, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = tt,
});
gtk_widget_show(wbt);
gtk_widget_set_name(wbt, "basics-link");
gtk_widget_set_valign(wbt, GTK_ALIGN_CENTER);
g_free(tt);
dt_gui_connect_click(wbt, _basics_goto_module, NULL, item->module);
gtk_box_pack_end(GTK_BOX(compact_ui ? hbox_basic : header_box), wbt, FALSE, FALSE, 0);
// we create a button to open the presets menu
GtkWidget *pbt = dt_iop_gui_header_button(item->module,
dtgtk_cairo_paint_presets,
DT_ACTION_ELEMENT_PRESETS,
compact_ui ? hbox_basic : header_box);
gtk_widget_set_name(pbt, "quick-presets");
gtk_widget_set_valign(pbt, GTK_ALIGN_CENTER);
// we create a button to reset the module
GtkWidget *rbt = dt_iop_gui_header_button(item->module,
dtgtk_cairo_paint_reset,
DT_ACTION_ELEMENT_RESET,
compact_ui ? hbox_basic : header_box);
gtk_widget_set_name(rbt, "quick-reset");
gtk_widget_set_valign(rbt, GTK_ALIGN_CENTER);
if(!compact_ui)
{
// we add the on-off button
GtkWidget *btn = dt_iop_gui_header_button(item->module,
dtgtk_cairo_paint_switch,
DT_ACTION_ELEMENT_ENABLE,
header_box);
g_signal_connect_object(G_OBJECT(item->module->off), "notify::active",
G_CALLBACK(_basics_off_sync), btn, 0);
gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
dt_gui_add_class(btn, "dt_transparent_background");
// we add to the module header the section label and the link to the full iop
GtkWidget *sect = dt_ui_section_label_new(item->module->name());
gtk_label_set_xalign(GTK_LABEL(sect), 0.5); // we center the module name
gtk_widget_show(sect);
gtk_box_pack_start(GTK_BOX(header_box), sect, TRUE, TRUE, 0);
}
else if(item_pos == FIRST_MODULE)
// if there is no label, we handle separately in css the first module header
gtk_widget_set_name(header_box, "basics-header-box-first");
}
if(item->box) gtk_box_pack_start(GTK_BOX(d->mod_vbox_basic), item->box, FALSE, FALSE, 0);
}
static gchar *_action_id(dt_action_t *action)
{
if(action->type != DT_ACTION_TYPE_IOP && action->owner)
{
gchar *owner_id = _action_id(action->owner);
gchar *combined_id = g_strdup_printf("%s/%s", owner_id, action->id);
g_free(owner_id);
return combined_id;
}
else
return g_strdup(action->id);
}
static dt_lib_modulegroups_basic_item_position_t
_basics_add_items_from_module_widget(dt_lib_module_t *self, dt_iop_module_t *module, GtkWidget *w,
dt_lib_modulegroups_basic_item_position_t item_pos)
{
if(!w) return item_pos;
dt_lib_modulegroups_t *d = self->data;
// search for a corresponding basic item
dt_action_t *ac = module->so->actions.target;
while(ac)
{
if(ac->type >= DT_ACTION_TYPE_WIDGET && ac->target == w)
{
gchar *action_id = _action_id(ac);
for(const GList *l = d->basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
if(!item->module && g_strcmp0(item->module_op, module->op) == 0
&& item->widget_type != WIDGET_TYPE_ACTIVATE_BTN)
{
if(!strcmp(item->id, action_id))
{
item->module = module;
_basics_add_widget(self, item, ac->target, item_pos);
// we have found and added the widget. no need to go further
g_free(action_id);
return NORMAL;
}
}
}
g_free(action_id);
}
if(ac->type == DT_ACTION_TYPE_SECTION)
ac = ac->target;
else if(!ac->next && ac->owner->type == DT_ACTION_TYPE_SECTION)
ac = ac->owner->next;
else
ac = ac->next;
}
// if w is a container, test all subwidgets
if(GTK_IS_CONTAINER(w))
{
GList *ll = gtk_container_get_children(GTK_CONTAINER(w));
for(const GList *l = ll; l; l = g_list_next(l))
{
item_pos = _basics_add_items_from_module_widget(self, module, l->data, item_pos);
}
g_list_free(ll);
}
return item_pos;
}
static void _basics_show(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->vbox_basic && gtk_widget_get_visible(d->vbox_basic)) return;
/* QAP construction creates activation buttons whose callbacks are already
* connected before their initial active state is synchronized. Every QAP
* rebuild must therefore be a programmatic GUI update, including the path
* reached when the last character is deleted from module search. Without
* this scope, that initial toggled signal can recursively rebuild QAP. */
DT_ENTER_GUI_UPDATE();
if(!d->vbox_basic)
{
d->vbox_basic = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
dt_ui_container_add_widget(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER, d->vbox_basic);
}
if(dt_conf_get_bool("plugins/darkroom/modulegroups_basics_sections_labels"))
gtk_widget_set_name(d->vbox_basic, "basics-box-labels");
else
gtk_widget_set_name(d->vbox_basic, "basics-box");
dt_gui_add_class(d->vbox_basic,"dt_plugin_ui");
dt_lib_modulegroups_basic_item_position_t item_pos = FIRST_MODULE;
for(GList *modules = g_list_last(darktable.develop->iop); modules; modules = g_list_previous(modules))
{
dt_iop_module_t *module = modules->data;
// we record if it's a new module or not to set css class and box structure
if(item_pos != FIRST_MODULE) item_pos = NEW_MODULE;
if(!dt_iop_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED) && module->iop_order != INT_MAX)
{
// first, we add on-off buttons if any
for(const GList *l = d->basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
if(!item->module && g_strcmp0(item->module_op, module->op) == 0)
{
if(item->widget_type == WIDGET_TYPE_ACTIVATE_BTN)
{
item->module = module;
_basics_add_widget(self, item, NULL, item_pos);
item_pos = NORMAL;
}
}
}
// for the other items, we want them in same order as the module gui
_basics_add_items_from_module_widget(self, module, module->widget, item_pos);
}
}
gtk_widget_show(d->vbox_basic);
DT_LEAVE_GUI_UPDATE();
}
static uint32_t _lib_modulegroups_get_activated(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we get the current group and verify that it is effectively activated
// this can not be the case if we are in search mode
GtkWidget *bt = _buttons_get_from_pos(self, d->current);
if(bt && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(bt))) return d->current;
return DT_MODULEGROUP_NONE;
}
static gboolean _is_module_in_history(dt_iop_module_t *module)
{
for(const GList *hists = darktable.develop->history; hists; hists = g_list_next(hists))
{
dt_dev_history_item_t *hist = hists->data;
if(hist->module == module) return TRUE;
}
return FALSE;
}
static void _lib_modulegroups_update_iop_visibility(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we hide eventual basic panel
if(d->current == DT_MODULEGROUP_BASICS && !d->basics_show) d->current = DT_MODULEGROUP_ACTIVE_PIPE;
_basics_hide(self);
// if we have a module to force or still have none selected, set d-current to active pipe
// note: DT_MODULEGROUP_NONE ("all modules", reached by clicking the active group
// button again or restored from conf) is a real group state and must be kept;
// only INVALID (no selection) is upgraded to the active pipe.
if(d->current == DT_MODULEGROUP_INVALID) d->current = DT_MODULEGROUP_ACTIVE_PIPE;
const gchar *text_entered = (gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
? gtk_entry_get_text(GTK_ENTRY(d->text_entry))
: NULL;
dt_print(DT_DEBUG_IOPORDER, "[lib_modulegroups_update_iop_visibility] modulegroups");
// update basic button selection too
DT_ENTER_GUI_UPDATE();
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basic_btn), d->current == DT_MODULEGROUP_BASICS);
/* only show module group as selected if not currently searching */
if((d->show_search || d->force_show_module) && d->current != DT_MODULEGROUP_NONE)
{
GtkWidget *bt = _buttons_get_from_pos(self, d->current);
if(bt)
{
/* toggle button visibility without executing callback */
if((text_entered && text_entered[0] != '\0') || d->force_show_module)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), FALSE);
else
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), TRUE);
}
}
DT_LEAVE_GUI_UPDATE();
// hide deprecated message. it will be shown after if needed
gtk_widget_set_visible(d->deprecated, FALSE);
for(const GList *modules = darktable.develop->iop; modules; modules = g_list_next(modules))
{
/*
* iterate over iop modules and do various test to
* detect if the modules should be shown or not.
*/
dt_iop_module_t *module = modules->data;
GtkWidget *w = module->expander;
if(module->enabled)
dt_print(DT_DEBUG_IOPORDER, "%20s %d%s",
module->op, module->iop_order, (dt_iop_is_hidden(module)) ? ", hidden" : "");
/* skip modules without an gui */
if(dt_iop_is_hidden(module)) continue;
// do not show non-active modules
// we don't want the user to mess with those
if(module->iop_order == INT_MAX)
{
if(dt_dev_gui_module() == module) dt_iop_request_focus(NULL);
if(w) gtk_widget_hide(w);
continue;
}
// if we have a module that should be forced shown
if(d->force_show_module)
{
if(d->force_show_module == module && w)
gtk_widget_show(w);
else
gtk_widget_hide(w);
continue;
}
// if there's some search text show matching modules only
if(text_entered && text_entered[0] != '\0')
{
/* don't show deprecated ones unless they are enabled */
if(module->flags() & IOP_FLAGS_DEPRECATED && !(module->enabled))
{
if(dt_dev_gui_module() == module) dt_iop_request_focus(NULL);
if(w) gtk_widget_hide(w);
}
else
{
const int is_match = (g_strstr_len(g_utf8_casefold(dt_iop_get_localized_name(module->op), -1), -1,
g_utf8_casefold(text_entered, -1))
!= NULL) ||
(g_strstr_len(g_utf8_casefold(dt_iop_get_localized_aliases(module->op), -1), -1,
g_utf8_casefold(text_entered, -1))
!= NULL) ||
(g_strstr_len(g_utf8_casefold(module->multi_name, -1), -1,
g_utf8_casefold(text_entered, -1))
!= NULL);
if(is_match)
gtk_widget_show(w);
else
gtk_widget_hide(w);
}
continue;
}
/* lets show/hide modules dependent on current group*/
const gboolean show_deprecated =
dt_conf_is_equal("plugins/darkroom/modulegroups_preset", _(DEPRECATED_PRESET_NAME));
gboolean show_module = TRUE;
switch(d->current)
{
case DT_MODULEGROUP_BASICS:
{
show_module = FALSE;
}
break;
case DT_MODULEGROUP_ACTIVE_PIPE:
{
if(d->full_active)
show_module = _is_module_in_history(module);
else
show_module = module->enabled;
}
break;
case DT_MODULEGROUP_NONE:
{
/* show all except hidden ones */
show_module = (((!(module->flags() & IOP_FLAGS_DEPRECATED) || show_deprecated)
&& _lib_modulegroups_test_visible(self, module->op))
|| module->enabled);
}
break;
default:
{
// show deprecated module in specific group deprecated
gtk_widget_set_visible(d->deprecated,
show_deprecated || d->force_deprecated_message);
show_module = (_lib_modulegroups_test_internal(self, d->current, module)
&& (!(module->flags() & IOP_FLAGS_DEPRECATED)
|| module->enabled
|| show_deprecated));
}
}
if(show_module)
{
if(dt_dev_gui_module() == module && !module->expanded) dt_iop_request_focus(NULL);
if(w) gtk_widget_show(w);
}
else
{
if(dt_dev_gui_module() == module) dt_iop_request_focus(NULL);
if(w) gtk_widget_hide(w);
}
}
// we show eventual basic panel but only if no text in the search box
if(d->current == DT_MODULEGROUP_BASICS && !(text_entered && text_entered[0] != '\0')) _basics_show(self);
}
/* switch to the given group, or to the all modules view for DT_MODULEGROUP_NONE,
* and refresh the panel; shared by the tab buttons and the active button popup.
* the caller must hold the gui update lock (DT_TRY_GUI_UPDATE) so the nested
* toggled signals from the button state changes below are dropped. */
static void _lib_modulegroups_switch_to(dt_lib_module_t *self, const int group)
{
dt_lib_modulegroups_t *d = self->data;
const int ngroups = g_list_length(d->groups);
const dt_iop_module_t *gui_module = dt_dev_gui_module();
const gboolean enabled = gui_module ? gui_module->enabled : FALSE;
/** In some cases the switch to the quick access panel is not safe for
various reasons if a module holds gui focus.
1) modules enforcing a crop via IOP_TAG_CROPPING or if they have
special drawing code.
If enabled it's good to assume that the user has some work
left to do so we don't switch and don't fiddle with specials.
If not enabled a simple defucus and redraw is good and we can switch.
2) If we are in mask edit mode it's dangerous to switch
as there are no visible controls that masks are editied so we don't
switch
3) In mask visualizing mode or if we have blend pickers we also don't
switch.
*/
if(d->current != DT_MODULEGROUP_BASICS
&& group == DT_MODULEGROUP_BASICS
&& gui_module)
{
if((gui_module->operation_tags() & IOP_TAG_CROPPING)
|| (gui_module->flags() & IOP_FLAGS_GUIDES_SPECIAL_DRAW))
{
if(enabled)
{
dt_control_log(_("close `%s` before switching to quick access panel"), gui_module->name());
d->current = DT_MODULEGROUP_NONE;
goto update_group;
}
else
{
dt_iop_request_focus(NULL);
dt_control_queue_redraw_center();
}
}
if(gui_module->flags() & IOP_FLAGS_SUPPORTS_BLENDING)
{
const dt_iop_gui_blend_data_t *bd = gui_module->blend_data;
const gboolean mask_edit = bd && enabled && bd->masks_shown != DT_MASKS_EDIT_OFF;
const gboolean mask_visual = bd && gui_module->request_mask_display != DT_DEV_PIXELPIPE_DISPLAY_NONE;
const gboolean picking = bd && gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF;
if(mask_edit || mask_visual || picking)
{
dt_control_log(_("finish mask work in `%s` before switching to quick access panel"), gui_module->name());
d->current = DT_MODULEGROUP_NONE;
goto update_group;
}
}
}
/* deactivate all buttons */
for(int k = 0; k <= ngroups; k++)
{
const GtkWidget *bt = _buttons_get_from_pos(self, k);
if(bt) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), FALSE);
}
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basic_btn), FALSE);
if(d->current == DT_MODULEGROUP_BASICS) dt_iop_request_focus(NULL);
/* the all modules view has no button of its own, so keep them all deactivated */
if(group == DT_MODULEGROUP_NONE)
d->current = DT_MODULEGROUP_NONE;
else
{
d->current = group;
const GtkWidget *bt = _buttons_get_from_pos(self, group);
if(bt) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), TRUE);
}
/* clear search text */
if(gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
gtk_entry_set_text(GTK_ENTRY(d->text_entry), "");
update_group:
/* update visibility, this must be reached even if we don't switch */
d->force_show_module = NULL;
_lib_modulegroups_update_iop_visibility(self);
}
static void _lib_modulegroups_toggle(GtkWidget *button, dt_lib_module_t *self)
{
DT_TRY_GUI_UPDATE();
dt_lib_modulegroups_t *d = self->data;
const gchar *text_entered = (gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
? gtk_entry_get_text(GTK_ENTRY(d->text_entry))
: NULL;
/* store toggled modulegroup */
int gid = 0;
const int ngroups = g_list_length(d->groups);
for(int k = 0; k <= ngroups; k++)
{
const GtkWidget *bt = _buttons_get_from_pos(self, k);
if(bt == button)
gid = k;
}
if(button == d->basic_btn) gid = DT_MODULEGROUP_BASICS;
/* only deselect button if not currently searching else re-enable module */
if(d->current == gid && gid != DT_MODULEGROUP_BASICS && !(text_entered && text_entered[0] != '\0'))
_lib_modulegroups_switch_to(self, DT_MODULEGROUP_NONE);
else
_lib_modulegroups_switch_to(self, gid);
DT_LEAVE_GUI_UPDATE();
}
typedef struct _set_gui_thread_t
{
dt_lib_module_t *self;
uint32_t group;
} _set_gui_thread_t;
static gboolean _lib_modulegroups_set_gui_thread(gpointer user_data)
{
_set_gui_thread_t *params = (_set_gui_thread_t *)user_data;
/* set current group and update visibility; the button state changes must
* not run the toggle handler (restoring the active pipe while it is selected
* must not be taken for a click switching to the all modules view) */
DT_ENTER_GUI_UPDATE();
_lib_modulegroups_switch_to(params->self, params->group);
DT_LEAVE_GUI_UPDATE();
free(params);
return G_SOURCE_REMOVE;
}
static gboolean _lib_modulegroups_upd_gui_thread(gpointer user_data)
{
_set_gui_thread_t *params = (_set_gui_thread_t *)user_data;
_lib_modulegroups_update_iop_visibility(params->self);
free(params);
return G_SOURCE_REMOVE;
}
/* this is a proxy function so it might be called from another thread */
static void _lib_modulegroups_set(dt_lib_module_t *self, uint32_t group)
{
_set_gui_thread_t *params = malloc(sizeof(_set_gui_thread_t));
if(!params) return;
params->self = self;
params->group = group;
/* run synchronously on the gui thread: the visibility update must stay in
* the same main loop iteration as the caller (e.g. right after the module
* list was rebuilt), otherwise every module is visible for at least one
* frame before the current group filter applies. only defer when called
* from a worker thread. */
if(g_main_context_is_owner(g_main_context_default()))
_lib_modulegroups_set_gui_thread(params);
else
g_main_context_invoke(NULL, _lib_modulegroups_set_gui_thread, params);
}
/* this is a proxy function so it might be called from another thread */
static void _lib_modulegroups_update_visibility_proxy(dt_lib_module_t *self)
{
_set_gui_thread_t *params = malloc(sizeof(_set_gui_thread_t));
if(!params) return;
params->self = self;
/* same as above: run synchronously on the gui thread */
if(g_main_context_is_owner(g_main_context_default()))
_lib_modulegroups_upd_gui_thread(params);
else
g_main_context_invoke(NULL, _lib_modulegroups_upd_gui_thread, params);
}
static void _lib_modulegroups_switch_group(dt_lib_module_t *self, dt_iop_module_t *module)
{
/* lets find the group which is not active pipe */
dt_lib_modulegroups_t *d = self->data;
const int ngroups = g_list_length(d->groups);
for(int k = 1; k <= ngroups; k++)
{
if(_lib_modulegroups_test(self, k, module))
{
d->force_show_module = NULL;
_lib_modulegroups_set(self, k);
return;
}
}
// if we arrive here, that means the module is not part of any group
// so we force it to be shown outside any group
d->force_show_module = module;
d->current = DT_MODULEGROUP_INVALID;
_lib_modulegroups_set(self, DT_MODULEGROUP_INVALID);
}
static uint32_t _lib_modulegroups_get(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
return d->current;
}
static dt_lib_modulegroup_iop_visibility_type_t _preset_retrieve_old_search_pref(gchar **ret)
{
// show the search box ?
const char *show_text_entry = dt_conf_get_string_const("plugins/darkroom/search_iop_by_text");
dt_lib_modulegroup_iop_visibility_type_t val = DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE;
if(strcmp(show_text_entry, "show search text") == 0)
{
// we only show the search box. no groups
dt_util_str_cat(&*ret, "1ꬹ1");
val = DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE;
}
else if(strcmp(show_text_entry, "show groups") == 0)
{
// we don't show the search box
dt_util_str_cat(&*ret, "0");
val = DT_MODULEGROUP_SEARCH_IOP_GROUPS_VISIBLE;
}
else
{
// we show both
dt_util_str_cat(&*ret, "1");
val = DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE;
}
return val;
}
/* presets syntax :
Layout presets are saved as string consisting of blocs separated by ꬹ
OPTIONSꬹBLOC_0ꬹBLOC_1ꬹBLOC_2....
OPTION : show_search(0-1) | active == history (0-1)
BLOC_0 : reserved for future use. Always 1
BLOC_1.... : blocs describing each group, contains :
name|icon_name||iop_name_0|iop_name_1....
*/
static gchar *_preset_retrieve_old_layout_updated()
{
gchar *ret = NULL;
// show the search box ?
if(_preset_retrieve_old_search_pref(&ret) == DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE) return ret;
// layout with "new" 3 groups
for(int i = 0; i < 4; i++)
{
// group name and icon
if(i == 0)
{
dt_util_str_cat(&ret,
"1|0ꬹ1|||%s",
"exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast"
"|colorbalancergb/global vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation"
"|ashift/rotation|denoiseprofile|lens|bilat");
dt_util_str_cat(&ret, "ꬹfavorites|favorites|");
}
else if(i == 1)
dt_util_str_cat(&ret, "ꬹtechnical|technical|");
else if(i == 2)
dt_util_str_cat(&ret, "ꬹgrading|grading|");
else if(i == 3)
dt_util_str_cat(&ret, "ꬹeffects|effect|");
// list of modules
for(const GList *modules = darktable.iop; modules; modules = g_list_next(modules))
{
dt_iop_module_so_t *module = modules->data;
if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
{
// get previous visibility values
const int group = module->default_group();
gchar *key = g_strdup_printf("plugins/darkroom/%s/visible", module->op);
const gboolean visi = dt_conf_get_bool(key);
g_free(key);
key = g_strdup_printf("plugins/darkroom/%s/favorite", module->op);
const gboolean fav = dt_conf_get_bool(key);
g_free(key);
if((i == 0 && fav && visi) || (i == 1 && (group & IOP_GROUP_TECHNICAL) && visi)
|| (i == 2 && (group & IOP_GROUP_GRADING) && visi) || (i == 3 && (group & IOP_GROUP_EFFECTS) && visi))
{
dt_util_str_cat(&ret, "|%s", module->op);
}
}
}
}
return ret;
}
static gchar *_preset_retrieve_old_layout(const char *list, const char *list_fav)
{
gchar *ret = NULL;
// show the search box ?
if(_preset_retrieve_old_search_pref(&ret) == DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE) return ret;
// layout with "old" 5 groups
for(int i = 0; i < 6; i++)
{
// group name and icon
if(i == 0)
{
// we don't have to care about "modern" workflow for temperature as it's more recent than this layout
dt_util_str_cat(&ret,
"1|0ꬹ1|||%s",
"exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast"
"|colorbalancergb/global vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation"
"|ashift/rotation|denoiseprofile|lens|bilat");
dt_util_str_cat(&ret, "ꬹfavorites|favorites|");
}
else if(i == 1)
dt_util_str_cat(&ret, "ꬹbase|basic|");
else if(i == 2)
dt_util_str_cat(&ret, "ꬹtone|tone|");
else if(i == 3)
dt_util_str_cat(&ret, "ꬹcolor|color|");
else if(i == 4)
dt_util_str_cat(&ret, "ꬹcorrect|correct|");
else if(i == 5)
dt_util_str_cat(&ret, "ꬹeffect|effect|");
// list of modules
for(const GList *modules = darktable.iop; modules; modules = g_list_next(modules))
{
dt_iop_module_so_t *module = modules->data;
if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
{
gchar *search = g_strdup_printf("|%s|", module->op);
gchar *key;
// get previous visibility values
int group = -1;
if(i > 0 && list)
{
// we retrieve the group from hardcoded one
const int gr = module->default_group();
if(gr & IOP_GROUP_BASIC)
group = 1;
else if(gr & IOP_GROUP_TONE)
group = 2;
else if(gr & IOP_GROUP_COLOR)
group = 3;
else if(gr & IOP_GROUP_CORRECT)
group = 4;
else if(gr & IOP_GROUP_EFFECT)
group = 5;
}
else if(i > 0)
{
key = g_strdup_printf("plugins/darkroom/%s/modulegroup", module->op);
group = dt_conf_get_int(key);
g_free(key);
}
gboolean visi = FALSE;
if(list)
visi = (strstr(list, search) != NULL);
else
{
key = g_strdup_printf("plugins/darkroom/%s/visible", module->op);
visi = dt_conf_get_bool(key);
g_free(key);
}
gboolean fav = FALSE;
if(i == 0 && list_fav)
fav = (strstr(list_fav, search) != NULL);
else if(i == 0)
{
key = g_strdup_printf("plugins/darkroom/%s/favorite", module->op);
fav = dt_conf_get_bool(key);
g_free(key);
}
if((i == 0 && fav && visi) || (i == group && visi))
{
dt_util_str_cat(&ret, "|%s", module->op);
}
g_free(search);
}
}
}
return ret;
}
static void _preset_retrieve_old_presets(dt_lib_module_t *self)
{
// we retrieve old modulelist presets
GList *old = dt_presets_list_params("modulelist", 1, TRUE);
for(const GList *l = old; l; l = g_list_next(l))
{
const dt_presets_params_t *row = l->data;
const char *pname = row->name;
const char *p = row->op_params;
const int size = row->op_params_size;
gchar *list = NULL;
gchar *fav = NULL;
int pos = 0;
while(pos < size)
{
const char *op = p + pos;
const int op_len = strlen(op);
dt_iop_module_state_t state = p[pos + op_len + 1];
if(state == IOP_STATE_ACTIVE)
dt_util_str_cat(&list, "|%s", op);
else if(state == IOP_STATE_FAVORITE)
{
dt_util_str_cat(&fav, "|%s", op);
dt_util_str_cat(&list, "|%s", op);
}
pos += op_len + 2;
}
dt_util_str_cat(&list, "|");
dt_util_str_cat(&fav, "|");
gchar *tx = _preset_retrieve_old_layout(list, fav);
dt_lib_presets_add(pname, self->plugin_name, self->version(), tx, strlen(tx), FALSE, 0);
g_free(tx);
g_free(list);
g_free(fav);
}
g_list_free_full(old, dt_presets_params_free);
// and we remove all existing modulelist presets
dt_presets_delete_version("modulelist", 1);
}
static gchar *_preset_to_string(dt_lib_module_t *self, gboolean edition)
{
dt_lib_modulegroups_t *d = self->data;
gchar *res = NULL;
const gboolean show_search = edition ? d->edit_show_search : d->show_search;
const gboolean full_active = edition ? d->edit_full_active : d->full_active;
dt_util_str_cat(&res, "%d|%d", show_search ? 1 : 0, full_active ? 1 : 0);
const gboolean basics_show = edition ? d->edit_basics_show : d->basics_show;
GList *basics = edition ? d->edit_basics : d->basics;
GList *groups = edition ? d->edit_groups : d->groups;
// basics widgets
dt_util_str_cat(&res, "ꬹ%d||", basics_show ? 1 : 0);
for(const GList *l = basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
dt_util_str_cat(&res, "|%s", item->id);
}
for(const GList *l = groups; l; l = g_list_next(l))
{
dt_lib_modulegroups_group_t *g = l->data;
dt_util_str_cat(&res, "ꬹ%s|%s|", g->name, g->icon);
for(const GList *ll = g->modules; ll; ll = g_list_next(ll))
{
gchar *m = (gchar *)ll->data;
dt_util_str_cat(&res, "|%s", m);
}
}
return res;
}
static void _preset_from_string(dt_lib_module_t *self, gchar *txt, gboolean edition)
{
if(!txt) return;
dt_lib_modulegroups_t *d = self->data;
gboolean show_search = TRUE;
gboolean full_active = FALSE;
gchar **gr = g_strsplit(txt, "ꬹ", -1);
// read the general options
if(g_strv_length(gr) > 0)
{
gchar **gr2 = g_strsplit(gr[0], "|", -1);
// do we show the search bar
if(!g_strcmp0(gr2[0], "0")) show_search = FALSE;
// do we show all history module in active group
if(g_strv_length(gr2) > 1 && (g_strcmp0(gr2[1], "1") == 0)) full_active = TRUE;
g_strfreev(gr2);
}
// read the basics widgets
if(g_strv_length(gr) > 1)
{
if(gr[1])
{
gchar **gr2 = g_strsplit(gr[1], "|", -1);
gboolean basics_show = FALSE;
if(g_strv_length(gr2) > 3 && (g_strcmp0(gr2[0], "1") == 0)) basics_show = TRUE;
if(edition)
d->edit_basics_show = basics_show;
else
d->basics_show = basics_show;
for(int j = 3; j < g_strv_length(gr2); j++)
{
dt_lib_modulegroups_basic_item_t *item = g_try_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
if(!item)
continue;
item->id = g_strdup(gr2[j]);
_basics_init_item(item);
if(edition)
d->edit_basics = g_list_append(d->edit_basics, item);
else
d->basics = g_list_append(d->basics, item);
}
g_strfreev(gr2);
}
}
GList *res = NULL;
// read the groups
for(int i = 2; i < g_strv_length(gr); i++)
{
gchar *tx = gr[i];
if(tx)
{
gchar **gr2 = g_strsplit(tx, "|", -1);
const int nb = g_strv_length(gr2);
if(nb > 2)
{
dt_lib_modulegroups_group_t *group = g_try_malloc0(sizeof(dt_lib_modulegroups_group_t));
if(group)
{
group->name = g_strdup(gr2[0]);
group->icon = g_strdup(gr2[1]);
// gr2[2] is reserved for eventual future use
for(int j = 3; j < nb; j++)
{
group->modules = g_list_append(group->modules, g_strdup(gr2[j]));
}
res = g_list_prepend(res, group);
}
}
g_strfreev(gr2);
}
}
g_strfreev(gr);
res = g_list_reverse(res); // list was built in reverse order, so un-reverse it
// and we set the values
if(edition)
{
d->edit_show_search = show_search;
d->edit_full_active = full_active;
d->edit_groups = res;
}
else
{
d->show_search = show_search;
d->full_active = full_active;
d->groups = res;
}
}
// start no quick access
#define SNQA() \
{ \
g_free(tx); \
tx = g_strdup("1|0ꬹ0||"); \
}
// start quick access
#define SQA(is_scene_referred) \
{ \
g_free(tx); \
tx = g_strdup_printf("1|0ꬹ1||"); \
if(is_scene_referred) \
{ \
if(wf_filmic) \
{ \
AM("filmicrgb/white relative exposure"); \
AM("filmicrgb/black relative exposure"); \
AM("filmicrgb/contrast"); \
} \
else if(wf_sigmoid) \
{ \
AM("sigmoid/contrast"); \
AM("sigmoid/skew"); \
} \
else if(wf_agx) \
{ \
/*AM("agx/white relative exposure");*/ \
/*AM("agx/black relative exposure");*/ \
AM("agx/auto tune levels"); \
AM("agx/curve/contrast"); \
AM("agx/curve/shoulder power"); \
AM("agx/curve/toe power"); \
AM("agx/look/saturation"); \
AM("agx/look/preserve hue"); \
} \
AM("channelmixerrgb/temperature"); \
AM("channelmixerrgb/chroma"); \
AM("channelmixerrgb/hue"); \
AM("channelmixerrgb/illuminant"); \
AM("channelmixerrgb/F source"); \
AM("channelmixerrgb/LED source"); \
} \
else \
{ \
AM("temperature/temperature"); \
AM("temperature/tint"); \
} \
AM("colorequal/page"); \
AM("colorequal/graph"); \
AM("colorequal/node placement"); \
AM("exposure/exposure"); \
if(!is_scene_referred) \
{ \
/* contrast is already in filmic/sigmoid */ \
AM("colorbalancergb/contrast"); \
} \
AM("colorbalancergb/global chroma"); \
AM("colorbalancergb/global vibrance"); \
AM("colorbalancergb/global saturation"); \
AM("colorbalancergb/global brilliance"); \
AM("ashift/rotation"); \
AM("denoiseprofile/strength"); \
AM("toneequal/graph"); \
AM("toneequal/mask exposure compensation"); \
AM("toneequal/mask contrast compensation"); \
AM("lens"); \
if(is_scene_referred) \
AM("contrastntexture/fine details"); \
else \
AM("bilat/detail"); \
}
// start module group
#define SMG(g,n) dt_util_str_cat(&tx, "ꬹ%s|%s|", g, n)
// add module
#define AM(n) dt_util_str_cat(&tx, "|%s", n)
void init_presets(dt_lib_module_t *self)
{
self->pref_based_presets = TRUE;
/*
For the record, one can create the preset list by using the following code:
$ cat <( git grep "return.*IOP_GROUP_TONE" -- src/iop/ | cut -d':' -f1 ) \
<( git grep IOP_FLAGS_DEPRECATED -- src/iop/ | cut -d':' -f1 ) | \
grep -E -v "useless|mask_manager|gamma" | sort | uniq --unique | \
while read file; do BN=$(basename $(basename $file .cc) .c); \
echo "AM(\"${BN:0:16}\");" ; done
*/
const gboolean is_scene_referred = dt_is_scene_referred();
const gboolean wf_filmic =
dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (filmic)");
const gboolean wf_sigmoid =
dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (sigmoid)");
const gboolean wf_agx =
dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (AgX)");
const gboolean wf_spektrafilm =
dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (spektrafilm)");
const gboolean wf_none =
dt_conf_is_equal("plugins/darkroom/workflow", "none");
// all modules
gchar *tx = NULL;
SQA(is_scene_referred);
SMG(C_("modulegroup", "base"), "basic");
AM("basecurve");
AM("crop");
AM("ashift");
AM("colisa");
AM("colorreconstruct");
AM("demosaic");
AM("exposure");
AM("finalscale");
AM("demosaicscale");
AM("flip");
AM("highlights");
AM("negadoctor");
AM("overexposed");
AM("rawoverexposed");
AM("rawprepare");
AM("shadhi");
AM("temperature");
AM("toneequal");
SMG(C_("modulegroup", "tone"), "tone");
AM("agx");
AM("bilat");
AM("contrastntexture");
AM("filmicrgb");
AM("levels");
AM("rgbcurve");
AM("rgblevels");
AM("sigmoid");
AM("spektrafilm");
AM("tonecurve");
SMG(C_("modulegroup", "color"), "color");
AM("channelmixerrgb");
AM("colorbalancergb");
AM("colorchecker");
AM("colorcontrast");
AM("colorcorrection");
AM("colorin");
AM("colorout");
AM("colorzones");
AM("colorequal");
AM("colorharmonizer");
AM("lut3d");
AM("monochrome");
AM("profile");
AM("primaries");
AM("satcurvergb");
AM("gamma");
AM("velvia");
SMG(C_("modulegroup", "correct"), "correct");
AM("atrous");
AM("bilateral");
AM("cacorrect");
AM("cacorrectrgb");
AM("denoiseprofile");
AM("dither");
AM("hazeremoval");
AM("hotpixels");
AM("lens");
AM("liquify");
AM("nlmeans");
AM("rasterfile");
AM("rawdenoise");
AM("retouch");
AM("rotatepixels");
AM("scalepixels");
AM("sharpen");
SMG(C_("modulegroup", "effect"), "effect");
AM("bloom");
AM("borders");
AM("colorize");
AM("colormapping");
AM("enlargecanvas");
AM("graduatednd");
AM("grain");
AM("highpass");
AM("lowlight");
AM("lowpass");
AM("overlay");
AM("soften");
AM("splittoning");
AM("vignette");
AM("watermark");
AM("censorize");
AM("blurs");
AM("diffuse");
dt_lib_presets_add(_("modules: all"),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
// minimal / 3 tabs
SQA(is_scene_referred);
SMG(C_("modulegroup", "base"), "basic");
AM("ashift");
if(is_scene_referred)
AM("sigmoid");
else
AM("basecurve");
AM("crop");
AM("denoiseprofile");
AM("exposure");
AM("flip");
AM("lens");
AM("temperature");
SMG(C_("modulegroup", "grading"), "grading");
AM("channelmixerrgb");
AM("colorequal");
AM("graduatednd");
AM("rgbcurve");
AM("rgblevels");
AM("splittoning");
SMG(C_("modulegroup", "effects"), "effect");
AM("borders");
AM("monochrome");
AM("retouch");
AM("sharpen");
AM("vignette");
AM("watermark");
dt_lib_presets_add(_("workflow: beginner"),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
// display referred
SQA(FALSE);
SMG(C_("modulegroup", "base"), "basic");
AM("basecurve");
AM("toneequal");
AM("crop");
AM("ashift");
AM("flip");
AM("exposure");
AM("temperature");
AM("rgbcurve");
AM("rgblevels");
AM("bilat");
AM("shadhi");
AM("highlights");
SMG(C_("modulegroup", "color"), "color");
AM("channelmixerrgb");
AM("colorbalancergb");
AM("colorcorrection");
AM("colorzones");
AM("monochrome");
AM("velvia");
SMG(C_("modulegroup", "correct"), "correct");
AM("cacorrect");
AM("cacorrectrgb");
AM("denoiseprofile");
AM("hazeremoval");
AM("hotpixels");
AM("lens");
AM("retouch");
AM("liquify");
AM("rasterfile");
AM("sharpen");
AM("nlmeans");
SMG(C_("modulegroup", "effect"), "effect");
AM("borders");
AM("enlargecanvas");
AM("colorize");
AM("graduatednd");
AM("grain");
AM("overlay");
AM("splittoning");
AM("vignette");
AM("watermark");
AM("censorize");
dt_lib_presets_add(_("workflow: display-referred"),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
// scene referred
SQA(TRUE);
SMG(C_("modulegroup", "base"), "basic");
if(wf_filmic || wf_none)
AM("filmicrgb");
if(wf_sigmoid || wf_none)
AM("sigmoid");
if(wf_agx || wf_none)
AM("agx");
if(wf_spektrafilm || wf_none)
AM("spektrafilm");
AM("toneequal");
AM("crop");
AM("ashift");
AM("flip");
AM("exposure");
AM("temperature");
AM("contrastntexture");
AM("highlights");
SMG(C_("modulegroup", "color"), "color");
AM("channelmixerrgb");
AM("colorbalancergb");
AM("colorequal");
AM("colorharmonizer");
AM("primaries");
AM("satcurvergb");
SMG(C_("modulegroup", "correct"), "correct");
AM("cacorrect");
AM("cacorrectrgb");
AM("denoiseprofile");
AM("hazeremoval");
AM("hotpixels");
AM("lens");
AM("retouch");
AM("liquify");
AM("rasterfile");
AM("sharpen");
AM("nlmeans");
SMG(C_("modulegroup", "effect"), "effect");
AM("atrous");
AM("borders");
AM("enlargecanvas");
AM("graduatednd");
AM("grain");
AM("overlay");
AM("vignette");
AM("watermark");
AM("censorize");
AM("blurs");
AM("diffuse");
dt_lib_presets_add(_("workflow: scene-referred"),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
// search only (only active modules visible)
SNQA();
dt_lib_presets_add(_("search only"),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
// There is no need for the deprecated modules group now, as there have been
// no new module deprecations for a long time. The group is not for access
// to all once deprecated modules, it should only contain deprecated modules
// temporarily (planned for 1 year) to prepare users of these modules for
// the need to learn the replacement modules.
// We are not removing the following code, just commenting it out for possible
// updating if we decide to deprecate any modules again in the future.
#if 0
// This is a special preset for all newly deprecated modules, so users still
// have a chance to access them until next release (with warning messages)
SNQA();
SMG(C_("modulegroup", "deprecated"), "basic");
// these modules are deprecated in 4.4 and should be removed in 4.8 (1 year later)
AM("levels");
AM("colisa");
dt_lib_presets_add(_(DEPRECATED_PRESET_NAME),
self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
#endif
g_free(tx);
// if needed, we add a new preset, based on last user config
if(!dt_conf_key_exists("plugins/darkroom/modulegroups_preset"))
{
tx = _preset_retrieve_old_layout(NULL, NULL);
dt_lib_presets_add(_("previous config"),
self->plugin_name, self->version(), tx, strlen(tx), FALSE, 0);
dt_conf_set_string("plugins/darkroom/modulegroups_preset", _("previous layout"));
g_free(tx);
tx = _preset_retrieve_old_layout_updated();
dt_lib_presets_add(_("previous config with new layout"),
self->plugin_name, self->version(), tx,
strlen(tx), FALSE, 0);
g_free(tx);
}
// if they exists, we retrieve old user presets from old modulelist lib
_preset_retrieve_old_presets(self);
}
static gchar *_presets_get_minimal(dt_lib_module_t *self)
{
const gboolean is_scene_referred = dt_is_scene_referred();
const gboolean wf_filmic = dt_conf_is_equal("plugins/darkroom/workflow",
"scene-referred (filmic)");
const gboolean wf_sigmoid = dt_conf_is_equal("plugins/darkroom/workflow",
"scene-referred (sigmoid)");
const gboolean wf_spektrafilm = dt_conf_is_equal("plugins/darkroom/workflow",
"scene-referred (spektrafilm)");
const gboolean wf_agx = dt_conf_is_equal("plugins/darkroom/workflow",
"scene-referred (AgX)");
// all modules
gchar *tx = NULL;
SQA(is_scene_referred);
AM("exposure/exposure");
AM("colorbalancergb/contrast");
SMG(C_("modulegroup", "base"), "basic");
if(is_scene_referred)
{
if(wf_filmic)
AM("filmicrgb");
else if(wf_sigmoid)
AM("sigmoid");
else if(wf_agx)
AM("agx");
else if(wf_spektrafilm)
AM("spektrafilm");
}
else
AM("basecurve");
AM("exposure");
AM("colorbalancergb");
return tx;
}
#undef SNQA
#undef SQA
#undef SMG
#undef AM
void *legacy_params(dt_lib_module_t *self,
const void *const old_params,
const size_t old_params_size,
const int old_version,
int *new_version,
size_t *new_size)
{
return NULL;
}
void *get_params(dt_lib_module_t *self, int *size)
{
gchar *tx = _preset_to_string(self, FALSE);
*size = strlen(tx);
return tx;
}
static void _manage_editor_groups_cleanup(dt_lib_module_t *self,
const gboolean edition)
{
dt_lib_modulegroups_t *d = self->data;
GList *l = edition ? d->edit_groups : d->groups;
for(; l; l = g_list_next(l))
{
dt_lib_modulegroups_group_t *gr = l->data;
g_free(gr->name);
g_free(gr->icon);
g_list_free_full(gr->modules, g_free);
}
if(edition)
{
g_list_free_full(d->edit_groups, g_free);
d->edit_groups = NULL;
}
else
{
g_list_free_full(d->groups, g_free);
d->groups = NULL;
_basics_hide(self);
}
l = edition ? d->edit_basics : d->basics;
for(; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
_basics_free_item(item);
}
if(edition)
{
g_list_free_full(d->edit_basics, g_free);
d->edit_basics = NULL;
}
else
{
g_list_free_full(d->basics, g_free);
d->basics = NULL;
}
}
static void _manage_editor_basics_remove(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
const char *id = (char *)g_object_get_data(G_OBJECT(widget), "widget_id");
for(GList *l = d->edit_basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
if(g_strcmp0(item->id, id) == 0)
{
_basics_free_item(item);
d->edit_basics = g_list_delete_link(d->edit_basics, l);
gtk_widget_destroy(gtk_widget_get_parent(widget));
break;
}
}
}
static int _manage_editor_module_find_multi(gconstpointer a, gconstpointer b)
{
// we search for a other instance of module with lower priority
dt_iop_module_t *ma = (dt_iop_module_t *)a;
dt_iop_module_t *mb = (dt_iop_module_t *)b;
if(g_strcmp0(ma->op, mb->op) != 0) return 1;
if(ma->multi_priority >= mb->multi_priority) return 0;
return 1;
}
static void _manage_editor_basics_update_list(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// first, we remove all existing modules
dt_gui_container_destroy_children(GTK_CONTAINER(d->edit_basics_box));
// and we add the ones from the list
for(const GList *modules = g_list_last(darktable.develop->iop);
modules;
modules = g_list_previous(modules))
{
dt_iop_module_t *module = modules->data;
for(const GList *l = d->edit_basics; l; l = g_list_next(l))
{
dt_lib_modulegroups_basic_item_t *item = l->data;
if(g_strcmp0(module->op, item->module_op) == 0 && !dt_iop_is_hidden(module))
{
// we want to avoid showing multiple instances of the same module
if(module->multi_priority <= 0
|| g_list_find_custom(darktable.develop->iop, module,
_manage_editor_module_find_multi) == NULL)
{
GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb, "modulegroups-iop-header");
gchar *lbn = g_strdup_printf("%s\n %s", module->name(), item->widget_name);
GtkWidget *lb = gtk_label_new(lbn);
gtk_label_set_ellipsize(GTK_LABEL(lb), PANGO_ELLIPSIZE_END);
gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
g_free(lbn);
gtk_widget_set_name(lb, "iop-panel-label");
gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0);
if(!d->edit_ro)
{
GtkWidget *btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("remove this widget"),
.clicked_cb = G_CALLBACK(_manage_editor_basics_remove),
.clicked_data = self,
});
g_object_set_data(G_OBJECT(btn), "widget_id", item->id);
gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0);
}
gtk_box_pack_start(GTK_BOX(d->edit_basics_box), hb, FALSE, TRUE, 0);
}
}
}
}
gtk_widget_show_all(d->edit_basics_box);
}
int set_params(dt_lib_module_t *self, const void *params, int size)
{
if(!params) return 1;
// cleanup existing groups
_manage_editor_groups_cleanup(self, FALSE);
_preset_from_string(self, (char *)params, FALSE);
gchar *tx = g_strdup_printf("plugins/darkroom/%s/last_preset", self->plugin_name);
gchar *value = dt_conf_get_string(tx);
dt_conf_set_string("plugins/darkroom/modulegroups_preset", value);
g_free(value);
g_free(tx);
_buttons_update(self);
return 0;
}
static void _manage_editor_save(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(!d->edit_preset) return;
// get all the values
d->edit_show_search =
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->edit_search_cb));
d->edit_full_active =
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->edit_full_active_cb));
gchar *params = _preset_to_string(self, TRUE);
// update the preset in the database
dt_lib_presets_update(d->edit_preset,
self->plugin_name, self->version(), d->edit_preset, "", params,
strlen(params));
g_free(params);
// update groups
const char *preset = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
if(g_strcmp0(preset, d->edit_preset) == 0)
{
const int cur = d->current;
// we update the gui
if(!dt_lib_presets_apply(d->edit_preset, self->plugin_name, self->version()))
dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
self->plugin_name, self->version());
// and we ensure the right group is selected
d->current = cur;
_lib_modulegroups_update_iop_visibility(self);
}
}
static void _manage_editor_module_remove(GtkWidget *widget,
dt_lib_module_t *self)
{
const char *module = (char *)g_object_get_data(G_OBJECT(widget), "module_name");
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
for(GList *l = gr->modules; l; l = g_list_next(l))
{
const char *tx = (char *)l->data;
if(g_strcmp0(tx, module) == 0)
{
g_free(l->data);
gr->modules = g_list_delete_link(gr->modules, l);
gtk_widget_destroy(gtk_widget_get_parent(widget));
break;
}
}
}
static void _manage_editor_module_update_list(dt_lib_module_t *self,
dt_lib_modulegroups_group_t *gr)
{
dt_lib_modulegroups_t *d = self->data;
// first, we remove all existing modules
dt_gui_container_destroy_children(GTK_CONTAINER(gr->iop_box));
// and we add the ones from the list
for(GList *modules2 = g_list_last(darktable.develop->iop);
modules2;
modules2 = g_list_previous(modules2))
{
dt_iop_module_t *module = modules2->data;
if((!(module->flags() & IOP_FLAGS_DEPRECATED)
|| !g_strcmp0(gr->name, C_("modulegroup", "deprecated")))
&& !dt_iop_is_hidden(module)
&& g_list_find_custom(gr->modules, module->op, _iop_compare))
{
// we want to avoid showing multiple instances of the same module
if(module->multi_priority <= 0
|| g_list_find_custom(darktable.develop->iop, module,
_manage_editor_module_find_multi) == NULL)
{
GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb, "modulegroups-iop-header");
GtkWidget *lb = gtk_label_new(module->name());
gtk_label_set_ellipsize(GTK_LABEL(lb), PANGO_ELLIPSIZE_END);
gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
gtk_widget_set_name(lb, "iop-panel-label");
gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0);
if(!d->edit_ro)
{
GtkWidget *btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("remove this module"),
.clicked_cb = G_CALLBACK(_manage_editor_module_remove),
.clicked_data = self,
});
g_object_set_data(G_OBJECT(btn), "module_name", module->op);
g_object_set_data(G_OBJECT(btn), "group", gr);
gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0);
}
gtk_box_pack_start(GTK_BOX(gr->iop_box), hb, FALSE, TRUE, 0);
}
}
}
gtk_widget_show_all(gr->iop_box);
}
static void _manage_editor_group_update_arrows(GtkWidget *box)
{
// we go throw all group columns
GList *lw = gtk_container_get_children(GTK_CONTAINER(box));
int pos = 0;
const int max = g_list_length(lw) - 1;
for(const GList *lw_iter = lw; lw_iter; lw_iter = g_list_next(lw_iter))
{
GtkWidget *w = (GtkWidget *)lw_iter->data;
GtkWidget *hb = dt_gui_container_nth_child(GTK_CONTAINER(w), 1);
if(pos > 0 && hb) // we skip the first item as it's quick access panel
{
GList *lw2 = gtk_container_get_children(GTK_CONTAINER(hb));
if(!g_list_shorter_than(lw2, 3)) //do we have at least three?
{
GtkWidget *left = (GtkWidget *)lw2->data;
GtkWidget *right = (GtkWidget *)g_list_nth_data(lw2, 2);
gtk_widget_set_sensitive(left, pos > 1);
gtk_widget_set_sensitive(right, pos < max);
}
g_list_free(lw2);
}
pos++;
}
g_list_free(lw);
}
static void _manage_direct_save(dt_lib_module_t *self)
{
// get all the values
gchar *params = _preset_to_string(self, FALSE);
// update the preset in the database
dt_lib_presets_add(_(CURRENT_PRESET_NAME),
self->plugin_name, self->version(), params, strlen(params), FALSE, 0);
g_free(params);
// update the preset name
dt_conf_set_string("plugins/darkroom/modulegroups_preset", _(CURRENT_PRESET_NAME));
// and we update the gui
if(!dt_lib_presets_apply(_(CURRENT_PRESET_NAME), self->plugin_name, self->version()))
dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
self->plugin_name, self->version());
}
static void _manage_direct_module_toggle(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
GVariant *v_module = g_variant_get_child_value(parameter, 0);
GVariant *v_group = g_variant_get_child_value(parameter, 1);
const gchar *module = g_variant_get_string(v_module, NULL);
dt_lib_modulegroups_group_t *gr = (dt_lib_modulegroups_group_t *)g_variant_get_uint64(v_group);
g_variant_unref(v_module);
g_variant_unref(v_group);
if(g_strcmp0(module, "") == 0) return;
GList *found_item = g_list_find_custom(gr->modules, module, _iop_compare);
if(!found_item)
{
gr->modules = g_list_append(gr->modules, g_strdup(module));
}
else
{
gr->modules = g_list_delete_link(gr->modules, found_item);
}
_manage_direct_save(self);
}
static gint _basics_item_find(gconstpointer a, gconstpointer b)
{
dt_lib_modulegroups_basic_item_t *ia = (dt_lib_modulegroups_basic_item_t *)a;
return g_strcmp0(ia->id, (char *)b);
}
static int _lib_modulegroups_basics_module_toggle_action(dt_lib_module_t *self,
dt_action_t *action,
const gboolean doit)
{
dt_lib_modulegroups_t *d = self->data;
gchar *action_id = _action_id(action);
GList *found_item = g_list_find_custom(d->basics, action_id, _basics_item_find);
if(!doit)
g_free(action_id);
else
{
_basics_hide(self); // to be sure we put back all widget in their right modules
if(!found_item)
{
dt_lib_modulegroups_basic_item_t *item = g_try_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
if(item)
{
item->id = action_id;
_basics_init_item(item);
d->basics = g_list_append(d->basics, item);
}
}
else
{
_basics_free_item(found_item->data);
d->basics = g_list_delete_link(d->basics, found_item);
g_free(action_id);
}
_manage_direct_save(self);
}
return found_item ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP;
}
static int _lib_modulegroups_basics_module_toggle(dt_lib_module_t *self,
GtkWidget *widget,
const gboolean doit)
{
if(GTK_IS_BUTTON(widget)) return 0;
dt_action_t *action = dt_action_widget(widget);
dt_action_t *owner = action;
while(owner && owner->type >= DT_ACTION_TYPE_SECTION)
owner = owner->owner;
if(!owner || owner->type != DT_ACTION_TYPE_IOP)
return 0;
return _lib_modulegroups_basics_module_toggle_action(self, action, doit);
}
static void _manage_direct_basics_module_toggle(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_action_t *m_action = (dt_action_t *)g_variant_get_uint64(parameter);
if(!m_action) return;
_lib_modulegroups_basics_module_toggle_action(self, m_action, TRUE);
}
static void _manage_editor_basics_add(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_modulegroups_t *d = self->data;
dt_action_t *m_action = (dt_action_t *)g_variant_get_uint64(parameter);
gchar *action_id = _action_id(m_action);
if(g_list_find_custom(d->edit_basics, action_id, _basics_item_find))
g_free(action_id);
else
{
dt_lib_modulegroups_basic_item_t *item = g_try_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
if(item)
{
item->id = action_id;
_basics_init_item(item);
d->edit_basics = g_list_append(d->edit_basics, item);
}
_manage_editor_basics_update_list(self);
}
}
static void _manage_editor_module_add(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
GVariant *v_module = g_variant_get_child_value(parameter, 0);
GVariant *v_group = g_variant_get_child_value(parameter, 1);
const gchar *module = g_variant_get_string(v_module, NULL);
dt_lib_modulegroups_group_t *gr = (dt_lib_modulegroups_group_t *)g_variant_get_uint64(v_group);
g_variant_unref(v_module);
g_variant_unref(v_group);
if(g_strcmp0(module, "") == 0) return;
if(!g_list_find_custom(gr->modules, module, _iop_compare))
{
gr->modules = g_list_append(gr->modules, g_strdup(module));
_manage_editor_module_update_list(self, gr);
}
}
static int _manage_editor_module_so_add_sort(gconstpointer a, gconstpointer b)
{
dt_iop_module_so_t *ma = (dt_iop_module_so_t *)a;
dt_iop_module_so_t *mb = (dt_iop_module_so_t *)b;
gchar *s1 = g_utf8_normalize(ma->name(), -1, G_NORMALIZE_ALL);
gchar *sa = g_utf8_casefold(s1, -1);
g_free(s1);
s1 = g_utf8_normalize(mb->name(), -1, G_NORMALIZE_ALL);
gchar *sb = g_utf8_casefold(s1, -1);
g_free(s1);
const int res = g_strcmp0(sa, sb);
g_free(sa);
g_free(sb);
return -res;
}
static void _manage_module_add_popup(GtkWidget *widget,
dt_lib_modulegroups_group_t *gr,
gpointer data,
const gboolean toggle)
{
GActionGroup *action_group = gtk_widget_get_action_group(widget, "modulegroups");
if(action_group == NULL)
{
action_group = G_ACTION_GROUP(g_simple_action_group_new());
GActionEntry action_entries[] =
{
{ "toggle", _manage_direct_module_toggle, "(st)", NULL, NULL },
{ "add", _manage_editor_module_add, "(st)", NULL, NULL }
};
g_action_map_add_action_entries(G_ACTION_MAP(action_group),
action_entries,
G_N_ELEMENTS(action_entries),
data);
gtk_widget_insert_action_group(widget,
"modulegroups",
G_ACTION_GROUP(action_group));
g_object_unref(action_group);
}
GMenu *menu = g_menu_new();
GMenu *sm_all = g_menu_new();
int nba = 0; // nb of already present items
GList *m2 = g_list_sort(g_list_copy(darktable.iop), _manage_editor_module_so_add_sort);
for(const GList *modules = m2; modules; modules = g_list_next(modules))
{
dt_iop_module_so_t *module = modules->data;
if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
{
if(!g_list_find_custom(gr->modules, module->op, _iop_compare))
{
// does it belong to recommended modules ?
if(((module->default_group() & IOP_GROUP_BASIC)
&& g_strcmp0(gr->name, _("base")) == 0)
|| ((module->default_group() & IOP_GROUP_COLOR)
&& g_strcmp0(gr->name, _("color")) == 0)
|| ((module->default_group() & IOP_GROUP_CORRECT)
&& g_strcmp0(gr->name, _("correct")) == 0)
|| ((module->default_group() & IOP_GROUP_TONE)
&& g_strcmp0(gr->name, _("tone")) == 0)
|| ((module->default_group() & IOP_GROUP_EFFECT)
&& g_strcmp0(gr->name, C_("modulegroup", "effect")) == 0)
|| ((module->default_group() & IOP_GROUP_TECHNICAL)
&& g_strcmp0(gr->name, _("technical")) == 0)
|| ((module->default_group() & IOP_GROUP_GRADING)
&& g_strcmp0(gr->name, _("grading")) == 0)
|| ((module->default_group() & IOP_GROUP_EFFECTS)
&& g_strcmp0(gr->name, C_("modulegroup", "effects")) == 0))
{
GMenuItem *item = g_menu_item_new(module->name(), NULL);
g_menu_item_set_action_and_target_value(item,
toggle? "modulegroups.toggle" : "modulegroups.add",
g_variant_new("(st)", module->op, (guintptr)gr));
g_menu_prepend_item(menu, item);
g_object_unref(item);
}
GMenuItem *item = g_menu_item_new(module->name(), NULL);
g_menu_item_set_action_and_target_value(item,
toggle? "modulegroups.toggle" : "modulegroups.add",
g_variant_new("(st)", module->op, (guintptr)gr));
g_menu_prepend_item(sm_all, item);
g_object_unref(item);
}
else if(toggle)
{
GMenuItem *item = g_menu_item_new(module->name(), NULL);
g_menu_item_set_action_and_target_value(item,
"modulegroups.toggle",
g_variant_new("(st)", module->op, (guintptr)gr));
g_menu_prepend_item(menu, item);
g_object_unref(item);
nba++;
}
}
}
g_list_free(m2);
GMenu *all_modules_section = menu;
if(toggle && nba > 0)
{
GMenu *section = menu;
menu = g_menu_new();
g_menu_append_section(menu, _("remove module"), G_MENU_MODEL(section));
all_modules_section = g_menu_new();
g_menu_append_section(menu, _("add module"), G_MENU_MODEL(all_modules_section));
}
// show the submenu with all the modules
GMenuItem *item = g_menu_item_new_submenu(_("all available modules"), G_MENU_MODEL(sm_all));
g_menu_append_item(all_modules_section, item);
g_object_unref(item);
g_object_unref(sm_all);
// popup the menu
GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
g_object_unref(menu);
gtk_popover_popup(GTK_POPOVER(popover_menu));
}
static gchar *_action_label(dt_action_t *action)
{
if(action->type != DT_ACTION_TYPE_IOP && action->owner)
{
gchar *owner_id = _action_label(action->owner);
gchar *combined_id = g_strdup_printf("%s - %s", owner_id, action->label);
g_free(owner_id);
return combined_id;
}
else
return g_strdup(action->label);
}
static GMenu *_build_menu_from_actions(dt_action_t *actions,
dt_lib_module_t *self,
GMenuItem *on_off,
GMenu *remove_section,
GMenu *add_section,
const gboolean full_menu,
int *num_selected)
{
const gchar *detailed_action = full_menu ? "qap.toggle"
: "qap.add";
GMenu *new_base = NULL;
while(actions)
{
if(actions == &darktable.control->actions_focus ||
actions == &darktable.control->actions_blend)
{
actions = actions->next;
continue;
}
if(actions->type == DT_ACTION_TYPE_IOP)
{
dt_iop_module_so_t *so = (dt_iop_module_so_t *)actions;
if(dt_iop_so_is_hidden(so) || so->flags() & IOP_FLAGS_DEPRECATED)
{
actions = actions->next;
continue;
}
}
gchar *action_label = NULL;
GMenuItem *item = NULL;
GMenu *new_sub = NULL;
dt_action_t *action = NULL;
if(actions->type >= DT_ACTION_TYPE_SECTION && !on_off) // not an iop module or blending (CATEGORY)
{
// FIXME don't check here if on/off is enabled, because it
// depends on image (reload_defaults) respond later to image
// changed signal
action = actions->owner;
action_label = g_strdup_printf("%s - %s", actions->owner->label, _("on-off"));
on_off = item = g_menu_item_new(action_label, NULL);
// in next loop deal with first actual widget or section
}
else
{
if(actions->type <= DT_ACTION_TYPE_SECTION)
new_sub = _build_menu_from_actions(actions->target,
self,
on_off,
remove_section,
add_section,
full_menu,
num_selected);
if(new_sub
|| (actions->type >= DT_ACTION_TYPE_WIDGET
&& actions->target
&& !GTK_IS_BUTTON(actions->target)))
{
action = actions;
action_label = _action_label(actions);
item = g_menu_item_new(actions->label, NULL);
}
actions = actions->next;
}
if(item)
{
if(!new_base)
new_base = g_menu_new();
if(new_sub)
g_menu_item_set_submenu(item, G_MENU_MODEL(new_sub));
else
g_menu_item_set_action_and_target_value(item,
detailed_action,
g_variant_new_uint64((guintptr)action));
g_menu_append_item(new_base, item);
if(new_sub)
g_object_unref(new_sub);
else
{
dt_lib_modulegroups_t *d = self->data;
GMenuItem *item_top = NULL;
gchar *action_id = _action_id(action);
if(g_list_find_custom(full_menu
? d->basics
: d->edit_basics,
action_id, _basics_item_find))
{
if(full_menu)
{
item_top = g_menu_item_new(action_label, NULL);
g_menu_item_set_action_and_target_value(item_top,
detailed_action,
g_variant_new_uint64((guintptr)action));
g_menu_append_item(remove_section, item_top);
g_object_unref(item_top);
++*num_selected;
}
}
else
{
gchar *delimited_id = g_strdup_printf("|%s|", action_id);
if(strstr(RECOMMENDED_BASICS, delimited_id))
{
item_top = g_menu_item_new(action_label, NULL);
g_menu_item_set_action_and_target_value(item_top,
detailed_action,
g_variant_new_uint64((guintptr)action));
g_menu_append_item(add_section, item_top);
g_object_unref(item_top);
}
g_free(delimited_id);
}
g_free(action_id);
}
g_object_unref(item);
g_free(action_label);
}
}
return new_base;
}
static void _set_action_group(GtkWidget *widget,
dt_lib_module_t *self)
{
GActionGroup *action_group = gtk_widget_get_action_group(widget, "qap");
if(action_group == NULL)
{
action_group = G_ACTION_GROUP(g_simple_action_group_new());
GActionEntry action_entries[] =
{
{ "toggle", _manage_direct_basics_module_toggle, "t", NULL, NULL },
{ "add", _manage_editor_basics_add, "t", NULL, NULL }
};
g_action_map_add_action_entries(G_ACTION_MAP(action_group),
action_entries,
G_N_ELEMENTS(action_entries),
self);
gtk_widget_insert_action_group(widget,
"qap",
G_ACTION_GROUP(action_group));
g_object_unref(action_group);
}
}
static void _manage_basics_add_popup(GtkWidget *widget,
dt_lib_module_t *self,
const gboolean full_menu)
{
_set_action_group(widget, self);
int nba = 0; // nb of already present items
GMenu *menu = g_menu_new();
GMenu *remove_section = g_menu_new();
GMenu *add_section = g_menu_new();
GMenu *all_modules =
_build_menu_from_actions(darktable.control->actions_iops.target, self, NULL,
remove_section, add_section, full_menu, &nba);
GMenu *all_modules_section = menu;
g_menu_append_section(menu, _("remove widget"), G_MENU_MODEL(remove_section));
g_object_unref(remove_section);
g_menu_append_section(menu, _("add widget"), G_MENU_MODEL(add_section));
g_object_unref(add_section);
GMenuItem *item = g_menu_item_new_submenu(_("all available modules"), G_MENU_MODEL(all_modules));
g_menu_append_item(all_modules_section, item);
g_object_unref(item);
g_object_unref(all_modules);
// popup the menu
GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
g_object_unref(menu);
gtk_popover_popup(GTK_POPOVER(popover_menu));
}
static void _manage_editor_basics_add_popup(GtkWidget *widget,
dt_lib_module_t *self)
{
_manage_basics_add_popup(widget, self, FALSE);
}
static void _manage_editor_module_add_popup(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
_manage_module_add_popup(widget, gr, self, FALSE);
}
static void _presets_pressed_cb(GtkGestureSingle *gesture,
int n_press,
double x,
double y,
dt_lib_module_t *self)
{
const GdkModifierType state =
dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));
if(state & GDK_CONTROL_MASK)
{
manage_presets(self);
dt_gui_claim(gesture);
}
}
static void _manage_direct_popup(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_module_t *self)
{
// this gesture only serves right clicks; a primary press would be a
// shortcut-activated toggle effect (see dt_gui_current_button in gtk.h)
if(dt_gui_current_button(gesture) != GDK_BUTTON_SECONDARY) return;
GtkWidget *widget = dt_gui_get_widget(gesture);
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
if(!g_strcmp0(gr->name, C_("modulegroup", "deprecated"))) return;
_manage_module_add_popup(widget, gr, self, TRUE);
}
static void _manage_direct_basic_popup(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_module_t *self)
{
/* a shortcut-activated primary press (toggle/on/off effect, see
* dt_gui_current_button() in gtk.h) flips the button; real clicks on
* this secondary-only gesture are always secondary and open the popup */
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
GtkWidget *widget = dt_gui_get_widget(gesture);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),
!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)));
return;
}
_manage_basics_add_popup(dt_gui_get_widget(gesture), self, TRUE);
}
static void _manage_direct_module_popup(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_module_t *self)
{
// this gesture only serves right clicks; a primary press would be a
// shortcut-activated toggle effect (see dt_gui_current_button in gtk.h)
if(dt_gui_current_button(gesture) != GDK_BUTTON_SECONDARY) return;
GtkWidget *widget = dt_gui_get_widget(gesture);
dt_action_t *module = g_object_get_data(G_OBJECT(widget), "module");
_set_action_group(widget, self);
int nba = 0; // nb of already present items
GMenu *remove_section = g_menu_new();
GMenu *add_section = g_menu_new();
GMenu *this_module = _build_menu_from_actions(module->target,
self,
NULL,
remove_section,
add_section,
TRUE,
&nba);
g_object_unref(remove_section);
g_object_unref(add_section);
// popup the menu
GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, this_module);
g_object_unref(this_module);
gtk_popover_popup(GTK_POPOVER(popover_menu));
}
/* display modes offered by the active button popup */
typedef enum dt_lib_modulegroup_popup_mode_t
{
DT_MODULEGROUP_POPUP_ALL = 0, /* show every module */
DT_MODULEGROUP_POPUP_ACTIVE_HISTORY, /* active pipe, incl. history modules */
DT_MODULEGROUP_POPUP_ACTIVE /* active pipe only */
} dt_lib_modulegroup_popup_mode_t;
static void _manage_direct_active_mode_toggled(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_modulegroups_t *d = self->data;
const dt_lib_modulegroup_popup_mode_t mode = g_variant_get_int32(parameter);
switch(mode)
{
case DT_MODULEGROUP_POPUP_ALL:
DT_TRY_GUI_UPDATE();
_lib_modulegroups_switch_to(self, DT_MODULEGROUP_NONE);
DT_LEAVE_GUI_UPDATE();
break;
case DT_MODULEGROUP_POPUP_ACTIVE_HISTORY:
case DT_MODULEGROUP_POPUP_ACTIVE:
{
/* full_active is part of the modulegroups preset, so persist it */
d->full_active = (mode == DT_MODULEGROUP_POPUP_ACTIVE_HISTORY);
const int cur = d->current;
_manage_direct_save(self);
d->current = cur;
DT_TRY_GUI_UPDATE();
_lib_modulegroups_switch_to(self, DT_MODULEGROUP_ACTIVE_PIPE);
DT_LEAVE_GUI_UPDATE();
break;
}
}
// close the menu
gtk_popover_popdown(GTK_POPOVER(_active_popover_menu));
}
static void _manage_direct_active_popup(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_module_t *self)
{
GtkWidget *widget = dt_gui_get_widget(gesture);
/* a shortcut-activated primary press (toggle/on/off effect, see
* dt_gui_current_button() in gtk.h) flips the button; real clicks on
* this secondary-only gesture are always secondary and open the popup */
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),
!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)));
return;
}
GActionGroup *action_group = gtk_widget_get_action_group(widget, "activemodules");
if(action_group == NULL)
{
GActionEntry action_entries[] =
{
{ "toggle", _manage_direct_active_mode_toggled, "i", "0" }
};
action_group = G_ACTION_GROUP(g_simple_action_group_new());
g_action_map_add_action_entries(G_ACTION_MAP(action_group),
action_entries,
G_N_ELEMENTS(action_entries),
self);
gtk_widget_insert_action_group(widget,
"activemodules",
G_ACTION_GROUP(action_group));
g_object_unref(action_group);
}
GMenu *menu = g_menu_new();
GMenuItem *item;
item = g_menu_item_new(_("show all modules"), NULL);
g_menu_item_set_action_and_target_value(item,
"activemodules.toggle",
g_variant_new("i", DT_MODULEGROUP_POPUP_ALL));
g_menu_append_item(menu, item);
g_object_unref(item);
item = g_menu_item_new(_("show active and history modules"), NULL);
g_menu_item_set_action_and_target_value(item,
"activemodules.toggle",
g_variant_new("i", DT_MODULEGROUP_POPUP_ACTIVE_HISTORY));
g_menu_append_item(menu, item);
g_object_unref(item);
item = g_menu_item_new(_("show active modules only"), NULL);
g_menu_item_set_action_and_target_value(item,
"activemodules.toggle",
g_variant_new("i", DT_MODULEGROUP_POPUP_ACTIVE));
g_menu_append_item(menu, item);
g_object_unref(item);
// mark the active item
dt_lib_modulegroups_t *d = self->data;
dt_lib_modulegroup_popup_mode_t current;
if(d->current == DT_MODULEGROUP_ACTIVE_PIPE && d->full_active)
current = DT_MODULEGROUP_POPUP_ACTIVE_HISTORY;
else if(d->current == DT_MODULEGROUP_ACTIVE_PIPE && !d->full_active)
current = DT_MODULEGROUP_POPUP_ACTIVE;
else
current = DT_MODULEGROUP_POPUP_ALL;
GAction *item_action = g_action_map_lookup_action(G_ACTION_MAP(action_group), "toggle");
g_simple_action_set_state(G_SIMPLE_ACTION(item_action), g_variant_new("i", current));
// popup the menu
_active_popover_menu = dt_gui_popover_menu_from_model(widget, menu);
g_object_unref(menu);
gtk_popover_popup(GTK_POPOVER(_active_popover_menu));
}
static void _dt_dev_image_changed_callback(gpointer instance,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
dt_develop_t *dev = darktable.develop;
if(!dev || !dt_is_valid_imgid(dev->image_storage.id)) return;
const dt_image_t *image = dt_image_cache_get(dev->image_storage.id, 'r');
if(!image) return;
gchar *preset = dt_presets_find_lib_autoapply(self->plugin_name, self->version(), image);
dt_image_cache_read_release(image);
if(preset)
{
dt_lib_presets_apply(preset, self->plugin_name, self->version());
g_free(preset);
}
// check for missing camera samples
if(image->camera_missing_sample)
{
gchar *label = dt_image_camera_missing_sample_message(image, FALSE);
d->force_deprecated_message = TRUE;
gtk_label_set_markup(GTK_LABEL(d->deprecated), label);
g_free(label);
gtk_widget_set_visible(d->deprecated, TRUE);
}
else
{
d->force_deprecated_message = FALSE;
gtk_label_set_markup
(GTK_LABEL(d->deprecated),
_("the following modules are deprecated because they have internal design mistakes"
" that can't be corrected and alternative modules that correct them.\n"
"they will be removed for new edits in the next release."));
}
}
static void _scroll_group_buttons(GtkEventControllerScroll *controller,
gdouble dx,
gdouble dy,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
int delta = (dy > 0) ? 1 : (dy < 0) ? -1 : 0;
if(delta == 0)
delta = (dx > 0) ? -1 : (dx < 0) ? 1 : 0;
if(delta != 0)
{
GtkWidget *adjacent = d->current == DT_MODULEGROUP_BASICS && delta < 0
? d->active_btn
: d->current <= DT_MODULEGROUP_ACTIVE_PIPE && delta > 0
? d->basic_btn
: _buttons_get_from_pos(self, d->current - delta);
if(adjacent) gtk_button_clicked(GTK_BUTTON(adjacent));
}
}
// cycle module groups action
static const gchar *_get_current_group_name(dt_lib_module_t *self)
{
const dt_lib_modulegroups_t *d = self->data;
if(d->current == DT_MODULEGROUP_NONE)
return _("all");
if(d->current == DT_MODULEGROUP_BASICS)
return _("quick access");
if(d->current == DT_MODULEGROUP_ACTIVE_PIPE)
return d->full_active ? _("all") : _("active pipeline");
const dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, d->current - 1);
return gr ? gr->name : _("unknown");
}
static void _cycle_module_groups(const gboolean down, dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
const int ngroups = g_list_length(d->groups);
const gboolean has_basics = gtk_widget_is_visible(d->basic_btn);
// Visual order left-to-right: [QAP/basics] [All/active] [group1] ... [groupN]
// cycle forward: active → 1 → ... → N → basics → active → ...
// cycle backward: active → basics → N → ... → 1 → active → ...
GtkWidget *next = NULL;
if(down)
{
if(d->current == DT_MODULEGROUP_BASICS)
next = d->active_btn;
else if(d->current == DT_MODULEGROUP_ACTIVE_PIPE)
next = ngroups > 0 ? _buttons_get_from_pos(self, 1) : (has_basics ? d->basic_btn : NULL);
else
{
next = _buttons_get_from_pos(self, d->current + 1);
if(!next)
next = has_basics ? d->basic_btn : d->active_btn;
}
}
else
{
if(d->current == DT_MODULEGROUP_BASICS)
next = ngroups > 0 ? _buttons_get_from_pos(self, ngroups) : d->active_btn;
else if(d->current == DT_MODULEGROUP_ACTIVE_PIPE)
next =
has_basics ? d->basic_btn : (ngroups > 0 ? _buttons_get_from_pos(self, ngroups) : NULL);
else if(d->current == 1)
next = d->active_btn;
else
next = _buttons_get_from_pos(self, d->current - 1);
}
if(next)
{
gtk_button_clicked(GTK_BUTTON(next));
dt_toast_log(_("module group: '%s'"), _get_current_group_name(self));
}
}
static float _action_callback_cycle_module_groups(gpointer target,
dt_action_element_t element,
const dt_action_effect_t effect,
const float move_size)
{
dt_lib_module_t *self = darktable.develop->proxy.modulegroups.module;
if(!self)
return DT_ACTION_NOT_VALID;
if(DT_PERFORM_ACTION(move_size))
{
if(effect == DT_ACTION_EFFECT_DEFAULT_KEY) // toggle
{
dt_lib_modulegroups_t *d = self->data;
gtk_button_clicked(GTK_BUTTON(d->active_btn));
dt_toast_log(_("module group: '%s'"), _get_current_group_name(self));
}
else
_cycle_module_groups(effect == DT_ACTION_EFFECT_DEFAULT_DOWN, self);
return 0;
}
return DT_ACTION_NOT_VALID;
}
static const gchar *_action_effect_cycle_module_groups[]
= { N_("toggle"), N_("previous"), N_("next"), NULL };
static const dt_action_element_def_t _action_elements_cycle_module_groups[]
= { { NULL, _action_effect_cycle_module_groups } };
static const dt_action_def_t _action_def_cycle_module_groups
= { N_("cycle module groups"),
_action_callback_cycle_module_groups,
_action_elements_cycle_module_groups,
NULL, TRUE };
static void _text_entry_focus_in(GtkEventController *controller,
GtkWidget *search_box)
{
gtk_widget_show(search_box);
}
void gui_init(dt_lib_module_t *self)
{
/* initialize ui widgets */
dt_lib_modulegroups_t *d = g_malloc0(sizeof(dt_lib_modulegroups_t));
self->data = (void *)d;
self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(self->widget, "modules-tabs");
dt_gui_add_class(self->widget, "dt_big_btn_canvas");
d->hbox_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
d->hbox_search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
// groups
d->hbox_groups = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *scrollbox = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(scrollbox), d->hbox_groups);
dt_gui_connect_scroll(scrollbox,
GTK_EVENT_CONTROLLER_SCROLL_VERTICAL
| GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
_scroll_group_buttons, self);
gtk_box_pack_start(GTK_BOX(d->hbox_buttons), scrollbox, TRUE, TRUE, 0);
// basic group button
d->basic_btn = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_modulegroup_basics, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("quick access panel\n"
"right-click tab icon to add/remove widgets"),
.action = DT_ACTION(self),
.action_label = N_("quick access panel"),
.action_def = &dt_action_def_toggle,
.toggled_cb = G_CALLBACK(_lib_modulegroups_toggle),
.toggled_data = self,
});
dt_gui_connect_click_secondary(d->basic_btn, _manage_direct_basic_popup, NULL, self);
gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->basic_btn, TRUE, TRUE, 0);
d->vbox_basic = NULL;
d->basics = NULL;
// active group button
d->active_btn = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_modulegroup_active, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("show only active modules"),
.action = DT_ACTION(self),
.action_label = N_("active modules"),
.action_def = &dt_action_def_toggle,
.toggled_cb = G_CALLBACK(_lib_modulegroups_toggle),
.toggled_data = self,
});
dt_gui_connect_click_secondary(d->active_btn, _manage_direct_active_popup, NULL, self);
gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->active_btn, TRUE, TRUE, 0);
// cycle module groups action
dt_action_define(
DT_ACTION(self), NULL, N_("cycle module groups"), NULL, &_action_def_cycle_module_groups);
// we load now the presets btn
self->presets_button = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("presets\nctrl+click to manage"),
});
gtk_box_pack_start(GTK_BOX(d->hbox_buttons), self->presets_button, FALSE, FALSE, 0);
dt_gui_connect_click(self->presets_button, _presets_pressed_cb, NULL, self);
/* search box */
d->text_entry = gtk_search_entry_new();
dt_action_define(&darktable.view_manager->proxy.darkroom.view->actions, NULL, N_("search modules"), d->text_entry, &dt_action_def_entry);
gtk_entry_set_placeholder_text(GTK_ENTRY(d->text_entry),
_("search modules by name or tag"));
g_signal_connect(G_OBJECT(d->text_entry), "search-changed",
G_CALLBACK(_text_entry_changed_callback), self);
g_signal_connect(G_OBJECT(d->text_entry), "stop-search",
G_CALLBACK(dt_gui_search_stop), dt_ui_center(darktable.gui->ui));
dt_gui_connect_focus(d->text_entry, _text_entry_focus_in, NULL, d->hbox_search_box);
GtkWidget *visibility_wrapper = gtk_event_box_new(); // extra layer prevents disabling shortcuts when hidden
gtk_container_add(GTK_CONTAINER(visibility_wrapper), d->text_entry);
gtk_box_pack_start(GTK_BOX(d->hbox_search_box), visibility_wrapper, TRUE, TRUE, 0);
gtk_entry_set_width_chars(GTK_ENTRY(d->text_entry), 0);
gtk_entry_set_max_width_chars(GTK_ENTRY(d->text_entry), 35);
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(d->text_entry),
GTK_ENTRY_ICON_SECONDARY, _("clear text"));
gtk_box_pack_start(GTK_BOX(self->widget), d->hbox_buttons, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(self->widget), d->hbox_search_box, TRUE, TRUE, 0);
// deprecated message
d->deprecated
= gtk_label_new(_("the following modules are deprecated because they have internal design mistakes"
" that can't be corrected and alternative modules that correct them.\n"
"they will be removed for new edits in the next release."));
dt_gui_add_class(d->deprecated, "dt_warning");
gtk_label_set_line_wrap(GTK_LABEL(d->deprecated), TRUE);
gtk_box_pack_start(GTK_BOX(self->widget), d->deprecated, TRUE, TRUE, 0);
d->current = dt_conf_get_int("plugins/darkroom/groups");
/* press the active button without running the toggle handler: the real
* selection was just restored from conf, and activating the active button
* while it is selected would switch to the all modules view */
DT_ENTER_GUI_UPDATE();
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->active_btn),
d->current != DT_MODULEGROUP_NONE);
DT_LEAVE_GUI_UPDATE();
if(d->current == DT_MODULEGROUP_NONE) _lib_modulegroups_update_iop_visibility(self);
gtk_widget_show_all(self->widget);
gtk_widget_set_no_show_all(d->deprecated, TRUE);
gtk_widget_set_no_show_all(d->hbox_buttons, TRUE);
gtk_widget_set_no_show_all(d->hbox_search_box, TRUE);
/*
* set the proxy functions
*/
darktable.develop->proxy.modulegroups.module = self;
darktable.develop->proxy.modulegroups.set = _lib_modulegroups_set;
darktable.develop->proxy.modulegroups.update_visibility = _lib_modulegroups_update_visibility_proxy;
darktable.develop->proxy.modulegroups.get = _lib_modulegroups_get;
darktable.develop->proxy.modulegroups.get_activated = _lib_modulegroups_get_activated;
darktable.develop->proxy.modulegroups.test = _lib_modulegroups_test;
darktable.develop->proxy.modulegroups.switch_group = _lib_modulegroups_switch_group;
darktable.develop->proxy.modulegroups.test_visible = _lib_modulegroups_test_visible;
darktable.develop->proxy.modulegroups.basics_module_toggle = _lib_modulegroups_basics_module_toggle;
// check for autoapplypresets on image change
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_IMAGE_CHANGED, _dt_dev_image_changed_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_INITIALIZE, _dt_dev_image_changed_callback);
}
void gui_cleanup(dt_lib_module_t *self)
{
darktable.develop->proxy.modulegroups.module = NULL;
darktable.develop->proxy.modulegroups.set = NULL;
darktable.develop->proxy.modulegroups.get = NULL;
darktable.develop->proxy.modulegroups.get_activated = NULL;
darktable.develop->proxy.modulegroups.test = NULL;
darktable.develop->proxy.modulegroups.switch_group = NULL;
g_free(self->data);
self->data = NULL;
}
static void _buttons_update(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// ensure we have no remaining force shown modules
d->force_show_module = NULL;
// first, we destroy all existing buttons except active one an preset one
GList *children = gtk_container_get_children(GTK_CONTAINER(d->hbox_groups));
GList *l = children;
if(!g_list_is_empty(l))
l = g_list_next(l); // skip basics group
if(!g_list_is_empty(l))
l = g_list_next(l); // skip active group
for(; l; l = g_list_next(l))
{
GtkWidget *bt = (GtkWidget *)l->data;
gtk_widget_destroy(bt);
}
g_list_free(children);
gtk_widget_set_visible(d->basic_btn, d->basics_show);
// if there's no groups, we ensure that the preset button is on the
// search line and we hide the active button
gtk_widget_set_visible(d->hbox_search_box, d->show_search);
if(!d->groups && d->show_search)
{
if(gtk_widget_get_parent(self->presets_button) != d->hbox_search_box)
{
g_object_ref(self->presets_button);
gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(self->presets_button)),
self->presets_button);
gtk_box_pack_start(GTK_BOX(d->hbox_search_box),
self->presets_button, FALSE, FALSE, 0);
g_object_unref(self->presets_button);
}
gtk_widget_hide(d->hbox_buttons);
d->current = DT_MODULEGROUP_ACTIVE_PIPE;
_lib_modulegroups_update_iop_visibility(self);
return;
}
else
{
if(gtk_widget_get_parent(self->presets_button) != d->hbox_buttons)
{
g_object_ref(self->presets_button);
gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(self->presets_button)),
self->presets_button);
gtk_box_pack_start(GTK_BOX(d->hbox_buttons), self->presets_button, FALSE, FALSE, 0);
g_object_unref(self->presets_button);
}
gtk_widget_show(d->hbox_buttons);
gtk_widget_show(d->hbox_groups);
}
// then we repopulate the box with new buttons
for(l = d->groups; l; l = g_list_next(l))
{
dt_lib_modulegroups_group_t *gr = l->data;
char *tooltip = g_strdup_printf(_("%s\nright-click tab icon to add/remove modules"), gr->name);
GtkWidget *bt = dtgtk_togglebutton_new_full(_buttons_get_icon_fct(gr->icon), 0, NULL,
&(dtgtk_button_config_t){
.tooltip = tooltip,
.toggled_cb = G_CALLBACK(_lib_modulegroups_toggle),
.toggled_data = self,
});
g_free(tooltip);
g_object_set_data(G_OBJECT(bt), "group", gr);
dt_gui_connect_click_secondary(bt, _manage_direct_popup, NULL, self);
gr->button = bt;
gtk_box_pack_start(GTK_BOX(d->hbox_groups), bt, TRUE, TRUE, 0);
gtk_widget_show(bt);
}
// last, if d->current still valid, we select it otherwise the first one
if(d->current == DT_MODULEGROUP_BASICS
? !d->basics_show
: d->current > g_list_length(d->groups))
{
d->current = DT_MODULEGROUP_ACTIVE_PIPE;
}
/* restore the selected group; the button presses must not run the toggle
* handler (restoring the active pipe while it is selected must not be
* taken for a click switching to the all modules view) */
DT_ENTER_GUI_UPDATE();
_lib_modulegroups_switch_to(self, d->current);
DT_LEAVE_GUI_UPDATE();
}
static void _manage_editor_group_move_right(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
GtkWidget *vb = gtk_widget_get_parent(gtk_widget_get_parent(widget));
// we move the group inside the list
const int pos = g_list_index(d->edit_groups, gr);
if(pos < 0 || pos >= g_list_length(d->edit_groups) - 1) return;
d->edit_groups = g_list_remove(d->edit_groups, gr);
d->edit_groups = g_list_insert(d->edit_groups, gr, pos + 1);
// we move the group in the ui (the position need +1 due to the quick access panel)
gtk_box_reorder_child(GTK_BOX(gtk_widget_get_parent(vb)), vb, pos + 2);
// and we update arrows
_manage_editor_group_update_arrows(gtk_widget_get_parent(vb));
}
static void _manage_editor_group_move_left(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
GtkWidget *vb = gtk_widget_get_parent(gtk_widget_get_parent(widget));
// we move the group inside the list
const int pos = g_list_index(d->edit_groups, gr);
if(pos <= 0) return;
d->edit_groups = g_list_remove(d->edit_groups, gr);
d->edit_groups = g_list_insert(d->edit_groups, gr, pos - 1);
// we move the group in the ui (the position need +1 due to the quick access panel)
gtk_box_reorder_child(GTK_BOX(gtk_widget_get_parent(vb)), vb, pos);
// and we update arrows
_manage_editor_group_update_arrows(gtk_widget_get_parent(vb));
}
static void _manage_editor_group_remove(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we don't allow to remove the last group if no quick access or searchbox
if(g_list_is_singleton(d->edit_groups)
&& !d->edit_basics_show
&& !d->edit_show_search)
{
return;
}
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
GtkWidget *vb =
gtk_widget_get_parent(gtk_widget_get_parent(gtk_widget_get_parent(widget)));
GtkWidget *groups_box = gtk_widget_get_parent(vb);
// we remove the group from the list and destroy it
d->edit_groups = g_list_remove(d->edit_groups, gr);
g_free(gr->name);
g_free(gr->icon);
g_list_free_full(gr->modules, g_free);
g_free(gr);
// we remove the group from the ui
gtk_widget_destroy(vb);
// and we update arrows
_manage_editor_group_update_arrows(groups_box);
}
static void _manage_editor_group_name_changed(GtkWidget *tb,
GdkEventButton *event,
dt_lib_module_t *self)
{
dt_lib_modulegroups_group_t *gr = (dt_lib_modulegroups_group_t *)g_object_get_data(G_OBJECT(tb), "group");
const gchar *txt = gtk_entry_get_text(GTK_ENTRY(tb));
g_free(gr->name);
gr->name = g_strdup(txt);
}
static void _manage_editor_group_icon_changed(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_lib_modulegroups_group_t *gr)
{
GtkWidget *widget = dt_gui_get_widget(gesture);
const char *ic = (char *)g_object_get_data(G_OBJECT(widget), "ic_name");
g_free(gr->icon);
gr->icon = g_strdup(ic);
GtkWidget *pop = gtk_widget_get_parent(gtk_widget_get_parent(widget));
GtkWidget *btn = gtk_popover_get_relative_to(GTK_POPOVER(pop));
dtgtk_button_set_paint(DTGTK_BUTTON(btn), _buttons_get_icon_fct(ic), 0, NULL);
gtk_popover_popdown(GTK_POPOVER(pop));
}
static void _manage_editor_group_icon_popup(GtkWidget *btn,
dt_lib_module_t *self)
{
dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(btn), "group");
GtkWidget *pop = gtk_popover_new(btn);
GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(pop, "modulegroups-icons-popup");
GtkWidget *eb, *hb, *ic;
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_basic, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("basic icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "basic");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_active, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("active icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "active");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_color, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("color icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "color");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_correct, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("correct icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "correct");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_effect, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("effect icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "effect");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_favorites, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("favorites icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "favorites");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_tone, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("tone icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "tone");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_grading, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("grading icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "grading");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
eb = gtk_event_box_new();
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_technical, 0, NULL);
gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("technical icon")), TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(eb), "ic_name", "technical");
dt_gui_connect_click_all(eb, _manage_editor_group_icon_changed, NULL, gr);
gtk_container_add(GTK_CONTAINER(eb), hb);
gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(pop), vb);
gtk_widget_show_all(pop);
}
static GtkWidget *_manage_editor_group_init_basics_box(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
GtkWidget *vb2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(vb2, "modulegroups-groupbox");
// line to edit the group
GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb2, "modulegroups-header");
GtkWidget *btn = NULL;
GtkWidget *hb3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb3, "modulegroups-header-center");
gtk_widget_set_hexpand(hb3, TRUE);
btn = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_basics, 0, NULL);
gtk_widget_set_name(btn, "modulegroups-group-icon");
gtk_widget_set_sensitive(btn, FALSE);
gtk_box_pack_start(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
GtkWidget *tb = gtk_entry_new();
gtk_entry_set_width_chars(GTK_ENTRY(tb), 5);
gtk_widget_set_tooltip_text(tb, _("quick access panel widgets"));
gtk_widget_set_sensitive(tb, FALSE);
gtk_entry_set_text(GTK_ENTRY(tb), _("quick access"));
gtk_box_pack_start(GTK_BOX(hb3), tb, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb2), hb3, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vb2), hb2, FALSE, TRUE, 0);
// chosen widgets
GtkWidget *vb3 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *sw = dt_gui_scroll_wrap(vb3);
d->edit_basics_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
_manage_editor_basics_update_list(self);
gtk_box_pack_start(GTK_BOX(vb3), d->edit_basics_box, FALSE, TRUE, 0);
// '+' button to add new widgets
if(!d->edit_ro)
{
GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *bt = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL,
&(dtgtk_button_config_t){
.tooltip = _("add widget to the quick access panel"),
.clicked_cb = G_CALLBACK(_manage_editor_basics_add_popup),
.clicked_data = self,
});
gtk_widget_set_name(bt, "modulegroups-btn");
gtk_widget_set_halign(hb4, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(hb4), bt, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0);
}
gtk_box_pack_start(GTK_BOX(vb2), sw, TRUE, TRUE, 0);
return vb2;
}
static GtkWidget *_manage_editor_group_init_modules_box(dt_lib_module_t *self,
dt_lib_modulegroups_group_t *gr)
{
dt_lib_modulegroups_t *d = self->data;
GtkWidget *vb2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(vb2, "modulegroups-groupbox");
// line to edit the group
GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb2, "modulegroups-header");
GtkWidget *btn = NULL;
GtkWidget *hb3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb3, "modulegroups-header-center");
gtk_widget_set_hexpand(hb3, TRUE);
// icon
btn = dtgtk_button_new_full(_buttons_get_icon_fct(gr->icon), 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("group icon"),
.clicked_cb = G_CALLBACK(_manage_editor_group_icon_popup),
.clicked_data = self,
});
gtk_widget_set_name(btn, "modulegroups-group-icon");
gtk_widget_set_sensitive(btn, !d->edit_ro);
g_object_set_data(G_OBJECT(btn), "group", gr);
gtk_box_pack_start(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
// entry for group name
GtkWidget *tb = gtk_entry_new();
gtk_entry_set_width_chars(GTK_ENTRY(tb), 5);
gtk_widget_set_tooltip_text(tb, _("group name"));
g_object_set_data(G_OBJECT(tb), "group", gr);
gtk_widget_set_sensitive(tb, !d->edit_ro);
g_signal_connect(G_OBJECT(tb), "changed",
G_CALLBACK(_manage_editor_group_name_changed), self);
gtk_entry_set_text(GTK_ENTRY(tb), gr->name);
gtk_box_pack_start(GTK_BOX(hb3), tb, TRUE, TRUE, 0);
// remove button
if(!d->edit_ro)
{
btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("remove group"),
.clicked_cb = G_CALLBACK(_manage_editor_group_remove),
.clicked_data = self,
});
g_object_set_data(G_OBJECT(btn), "group", gr);
gtk_box_pack_end(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
}
gtk_box_pack_start(GTK_BOX(hb2), hb3, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vb2), hb2, FALSE, TRUE, 0);
// chosen modules
GtkWidget *vb3 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *sw = dt_gui_scroll_wrap(vb3);
gr->iop_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
_manage_editor_module_update_list(self, gr);
gtk_box_pack_start(GTK_BOX(vb3), gr->iop_box, FALSE, TRUE, 0);
// '+' button to add new module
if(!d->edit_ro)
{
GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
// left arrow
btn = dtgtk_button_new_full(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_RIGHT, NULL,
&(dtgtk_button_config_t){
.tooltip = _("move group to the left"),
.clicked_cb = G_CALLBACK(_manage_editor_group_move_left),
.clicked_data = self,
});
gtk_widget_set_name(btn, "modulegroups-btn");
g_object_set_data(G_OBJECT(btn), "group", gr);
gtk_box_pack_start(GTK_BOX(hb4), btn, FALSE, FALSE, 2);
// plus button
GtkWidget *plusbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *bt = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL,
&(dtgtk_button_config_t){
.tooltip = _("add module to the group"),
.clicked_cb = G_CALLBACK(_manage_editor_module_add_popup),
.clicked_data = self,
});
gtk_widget_set_name(bt, "modulegroups-btn");
g_object_set_data(G_OBJECT(bt), "group", gr);
gtk_widget_set_halign(plusbox, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(plusbox), bt, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hb4), plusbox, TRUE, TRUE, 0);
//right arrow
btn = dtgtk_button_new_full(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_LEFT, NULL,
&(dtgtk_button_config_t){
.tooltip = _("move group to the right"),
.clicked_cb = G_CALLBACK(_manage_editor_group_move_right),
.clicked_data = self,
});
gtk_widget_set_name(btn, "modulegroups-btn");
g_object_set_data(G_OBJECT(btn), "group", gr);
gtk_box_pack_end(GTK_BOX(hb4), btn, FALSE, FALSE, 2);
gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0);
}
gtk_box_pack_start(GTK_BOX(vb2), sw, TRUE, TRUE, 0);
return vb2;
}
static void _manage_editor_reset(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
gchar *txt = g_strdup(d->edit_preset);
_manage_editor_load(txt, self);
g_free(txt);
}
static void _manage_editor_group_add(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
dt_lib_modulegroups_group_t *gr = g_try_malloc0(sizeof(dt_lib_modulegroups_group_t));
if(gr)
{
gr->name = g_strdup(_("new"));
gr->icon = g_strdup("basic");
d->edit_groups = g_list_append(d->edit_groups, gr);
// we update the group list
GtkWidget *vb2 = _manage_editor_group_init_modules_box(self, gr);
gtk_box_pack_start(GTK_BOX(d->preset_groups_box), vb2, FALSE, TRUE, 0);
gtk_widget_show_all(vb2);
}
// and we update arrows
_manage_editor_group_update_arrows(d->preset_groups_box);
}
static void _manage_editor_basics_toggle(GtkWidget *button,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->editor_reset) return;
const gboolean state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
// we don't allow that to be false if there's no group or search
if(!state
&& g_list_is_empty(d->edit_groups)
&& !d->edit_show_search)
{
d->editor_reset = TRUE;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
d->editor_reset = FALSE;
}
d->edit_basics_show = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
gtk_widget_set_visible(d->edit_basics_groupbox, d->edit_basics_show);
}
static void _manage_editor_search_toggle(GtkWidget *button,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->editor_reset) return;
const gboolean state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
// we don't allow that to be false if there's no group or quick access
if(!state
&& g_list_is_empty(d->edit_groups)
&& !d->edit_basics_show)
{
d->editor_reset = TRUE;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
d->editor_reset = FALSE;
}
d->edit_show_search = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
}
static void _manage_editor_full_active_toggle(GtkWidget *button,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->editor_reset) return;
d->edit_full_active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
}
static void _preset_autoapply_changed(dt_gui_presets_edit_dialog_t *g)
{
dt_lib_module_t *self = g->data;
dt_lib_modulegroups_t *d = self->data;
// we reread the presets autoapply values from the database
dt_presets_params_t p = { 0 };
if(!dt_presets_get_params(self->plugin_name, self->version(), d->edit_preset, &p))
return;
const int autoapply = p.autoapply;
dt_presets_params_clear(&p);
// we refresh the checkbox
d->editor_reset = TRUE;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_autoapply_chkbox), autoapply);
d->editor_reset = FALSE;
}
static void _manage_editor_preset_name_verify(GtkWidget *tb,
gpointer params[])
{
GtkDialog *dialog = params[0];
GList *names = params[1];
GtkWidget *warning_label = params[2];
const gchar *txt = gtk_entry_get_text(GTK_ENTRY(tb));
gboolean good = *txt;
// we don't want empty name
if(good)
{
for(const GList *l = names; l; l = g_list_next(l))
{
if(!g_strcmp0(l->data, txt))
{
good = FALSE;
break;
}
}
}
gtk_widget_set_visible(warning_label, !good);
gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_OK, good);
}
static void _manage_editor_preset_action(GtkWidget *btn,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we get the default name
gchar *new_name = NULL;
if(btn == d->presets_btn_rename)
new_name = g_strdup(d->edit_preset);
else if(btn == d->presets_btn_new)
new_name = g_strdup(_("new"));
else if(btn == d->presets_btn_dup)
new_name = g_strdup_printf("%s_1", d->edit_preset);
else
return;
// we first get the list of all the existing preset names
GList *names = dt_presets_version_names(self->plugin_name, self->version(), TRUE);
gint res = GTK_RESPONSE_OK;
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("rename preset"), GTK_WINDOW(d->dialog),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_CANCEL,
_("_rename"), GTK_RESPONSE_OK, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
GtkWidget *lb = gtk_label_new(_("a preset with this name already exists!"));
GtkWidget *tb = gtk_entry_new();
gtk_entry_set_activates_default(GTK_ENTRY(tb), TRUE);
gtk_entry_set_width_chars(GTK_ENTRY(tb),
10 + g_utf8_strlen(gtk_window_get_title(GTK_WINDOW(dialog)),
-1));
gpointer verify_params[] = {dialog, names, lb};
g_signal_connect(G_OBJECT(tb), "changed",
G_CALLBACK(_manage_editor_preset_name_verify), verify_params);
dt_gui_dialog_add(GTK_DIALOG(dialog), gtk_label_new(_("new preset name:")), tb, lb);
gtk_widget_show_all(dialog);
gtk_entry_set_text(GTK_ENTRY(tb), new_name);
res = gtk_dialog_run(GTK_DIALOG(dialog));
g_free(new_name);
if(res == GTK_RESPONSE_OK)
{
if(btn == d->presets_btn_rename)
{
// we update the database
dt_presets_rename(self->plugin_name, self->version(), d->edit_preset,
gtk_entry_get_text(GTK_ENTRY(tb)));
// we update the presets list
g_free(d->edit_preset);
d->edit_preset = g_strdup(gtk_entry_get_text(GTK_ENTRY(tb)));
d->editor_reset = TRUE;
_manage_preset_update_list(self);
gtk_combo_box_set_active_id(GTK_COMBO_BOX(d->presets_combo), d->edit_preset);
d->editor_reset = FALSE;
}
else if(btn == d->presets_btn_new)
{
// create a new minimal preset
char *tx = _presets_get_minimal(self);
dt_lib_presets_add(gtk_entry_get_text(GTK_ENTRY(tb)),
self->plugin_name, self->version(), tx, strlen(tx),
FALSE, 0);
g_free(tx);
// update the presets list
d->editor_reset = TRUE;
_manage_preset_update_list(self);
d->editor_reset = FALSE;
// select the new preset
_manage_editor_load(gtk_entry_get_text(GTK_ENTRY(tb)), self);
}
else if(btn == d->presets_btn_dup)
{
char *tx = _preset_to_string(self, TRUE);
dt_lib_presets_add(gtk_entry_get_text(GTK_ENTRY(tb)),
self->plugin_name, self->version(), tx, strlen(tx),
FALSE, 0);
g_free(tx);
// update the presets list
d->editor_reset = TRUE;
_manage_preset_update_list(self);
d->editor_reset = FALSE;
// select the new preset
_manage_editor_load(gtk_entry_get_text(GTK_ENTRY(tb)), self);
}
}
gtk_widget_destroy(dialog);
g_list_free_full(names, g_free);
}
static void _preset_autoapply_edit(GtkButton *button,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->editor_reset) return;
dt_presets_details_t details = { 0 };
if(dt_presets_get_details(d->edit_preset, self->plugin_name, self->version(), &details))
{
const int rowid = details.rowid;
dt_presets_details_clear(&details);
dt_gui_presets_show_edit_dialog(d->edit_preset,
rowid, G_CALLBACK(_preset_autoapply_changed),
self, FALSE, FALSE, FALSE, GTK_WINDOW(d->dialog));
}
}
static void _manage_editor_load(const char *preset,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// if we have a currently edited preset, we save it
if(d->edit_preset && g_strcmp0(preset, d->edit_preset) != 0)
{
_manage_editor_save(self);
}
// we avoid widgets events to be executed
d->editor_reset = TRUE;
// we remove all widgets from the groups-box
dt_gui_container_destroy_children(GTK_CONTAINER(d->preset_groups_box));
gtk_box_set_homogeneous(GTK_BOX(d->preset_groups_box), TRUE);
// we select the right preset in the combobox (or we select the first one)
gboolean sel_ok = FALSE;
if(preset) sel_ok = gtk_combo_box_set_active_id(GTK_COMBO_BOX(d->presets_combo), preset);
if(!sel_ok) gtk_combo_box_set_active(GTK_COMBO_BOX(d->presets_combo), 0);
const gchar *sel_preset = gtk_combo_box_get_active_id(GTK_COMBO_BOX(d->presets_combo));
// get all presets groups
if(d->edit_groups) _manage_editor_groups_cleanup(self, TRUE);
if(d->edit_preset) g_free(d->edit_preset);
d->edit_groups = NULL;
d->edit_preset = NULL;
int autoapply = 0;
dt_presets_params_t p = { 0 };
if(dt_presets_get_params(self->plugin_name, self->version(), sel_preset, &p))
{
d->edit_ro = p.writeprotect;
_preset_from_string(self, (char *)p.op_params, TRUE);
d->edit_basics_box = NULL;
d->edit_basics_groupbox = NULL;
d->edit_preset = g_strdup(sel_preset);
autoapply = p.autoapply;
dt_presets_params_clear(&p);
}
else
{
d->editor_reset = FALSE;
return;
}
// presets buttons
gtk_widget_set_sensitive(d->presets_btn_rename, !d->edit_ro);
gtk_widget_set_sensitive(d->presets_btn_remove, !d->edit_ro);
gtk_widget_set_sensitive(d->presets_btn_dup, g_strcmp0(sel_preset,
_(DEPRECATED_PRESET_NAME)));
// search checkbox
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_search_cb), d->edit_show_search);
gtk_widget_set_sensitive(d->edit_search_cb, !d->edit_ro);
// full_active checkbox
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_full_active_cb),
d->edit_full_active);
gtk_widget_set_sensitive(d->edit_full_active_cb, !d->edit_ro);
// basics checkbox
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basics_chkbox), d->edit_basics_show);
gtk_widget_set_sensitive(d->basics_chkbox, !d->edit_ro);
// autoapply
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_autoapply_chkbox), autoapply);
gtk_widget_set_sensitive(d->edit_autoapply_btn, !d->edit_ro);
// new group button
gtk_widget_set_visible(d->preset_btn_add_group, !d->edit_ro);
// set up basics widgets
d->edit_basics_groupbox = _manage_editor_group_init_basics_box(self);
gtk_box_pack_start(GTK_BOX(d->preset_groups_box),
d->edit_basics_groupbox, FALSE, TRUE, 0);
gtk_widget_show_all(d->edit_basics_groupbox);
gtk_widget_set_no_show_all(d->edit_basics_groupbox, TRUE);
gtk_widget_set_visible(d->edit_basics_groupbox, d->edit_basics_show);
// other groups
for(const GList *l = d->edit_groups; l; l = g_list_next(l))
{
dt_lib_modulegroups_group_t *gr = l->data;
GtkWidget *vb2 = _manage_editor_group_init_modules_box(self, gr);
gtk_widget_show_all(vb2);
gtk_box_pack_start(GTK_BOX(d->preset_groups_box), vb2, FALSE, TRUE, 0);
}
// read-only message
gtk_widget_set_visible(d->preset_read_only_label, d->edit_ro);
// reset button
gtk_widget_set_visible(d->preset_reset_btn, !d->edit_ro);
// and we update arrows
if(!d->edit_ro) _manage_editor_group_update_arrows(d->preset_groups_box);
d->editor_reset = FALSE;
// set keyboard focus on the scrollable window (not on a widget)
// gtk_widget_grab_focus(sw);
}
static void _manage_preset_change(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
if(d->editor_reset) return;
const char *preset = gtk_combo_box_get_active_id(GTK_COMBO_BOX(d->presets_combo));
_manage_editor_load(preset, self);
}
static void _manage_preset_delete(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
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'?"),
d->edit_preset))
{
dt_lib_presets_remove(d->edit_preset, self->plugin_name, self->version());
// if the deleted preset was the one currently in use, load default preset
if(dt_conf_key_exists("plugins/darkroom/modulegroups_preset"))
{
const char *cur = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
if(g_strcmp0(cur, d->edit_preset) == 0)
{
dt_conf_set_string("plugins/darkroom/modulegroups_preset",
C_("modulegroup", FALLBACK_PRESET_NAME));
dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
self->plugin_name, self->version());
}
}
// reload presets list
_manage_preset_update_list(self);
// we fallback to the first preset
_manage_editor_load(NULL, self);
}
}
static void _manage_preset_update_list(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we first remove all existing entries from the combobox
gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(d->presets_combo));
// and we repopulate it, shipped defaults first
GList *names = dt_presets_version_names(self->plugin_name, self->version(), TRUE);
for(const GList *l = names; l; l = g_list_next(l))
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->presets_combo), l->data, l->data);
g_list_free_full(names, g_free);
}
static void _manage_editor_destroy(GtkWidget *widget,
dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
// we save the last edited preset
_manage_editor_save(self);
// and we free editing data
if(d->edit_groups) _manage_editor_groups_cleanup(self, TRUE);
if(d->edit_preset) g_free(d->edit_preset);
d->edit_groups = NULL;
d->edit_preset = NULL;
}
static void _manage_show_window(dt_lib_module_t *self)
{
dt_lib_modulegroups_t *d = self->data;
GtkWindow *win = GTK_WINDOW(dt_ui_main_window(darktable.gui->ui));
d->dialog = gtk_dialog_new_with_buttons
(_("manage module layouts"), win,
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, NULL, NULL);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(d->dialog);
#endif
dt_gui_dialog_restore_size(GTK_DIALOG(d->dialog), "modulegroups");
gtk_widget_set_name(d->dialog, "modulegroups-manager");
gtk_window_set_title(GTK_WINDOW(d->dialog), _("manage module layouts"));
// remove the small border
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(d->dialog));
gtk_container_set_border_width(GTK_CONTAINER(content), 0);
GtkWidget *vb_main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // main box
// preset combobox
GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb, "modulegroups-topbox");
GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(vb, "modulegroups-top-boxes");
GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(hb2), gtk_label_new(_("preset: ")), FALSE, TRUE, 2);
d->presets_combo = gtk_combo_box_text_new();
g_signal_connect(G_OBJECT(d->presets_combo), "changed",
G_CALLBACK(_manage_preset_change), self);
gtk_box_pack_start(GTK_BOX(hb2), d->presets_combo, TRUE, TRUE, 2);
gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 2);
// presets buttons
hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
d->presets_btn_remove = dt_action_button_new(NULL, N_("remove"),
_manage_preset_delete,
self,_("remove the preset"), 0, 0);
gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_remove, TRUE, TRUE, 2);
d->presets_btn_dup = dt_action_button_new(NULL, N_("duplicate"),
_manage_editor_preset_action,
self,_("duplicate the preset"), 0, 0);
gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_dup, TRUE, TRUE, 2);
d->presets_btn_rename = dt_action_button_new(NULL, N_("rename"),
_manage_editor_preset_action,
self,_("rename the preset"), 0, 0);
gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_rename, TRUE, TRUE, 2);
d->presets_btn_new = dt_action_button_new(NULL, N_("new"),
_manage_editor_preset_action,
self,_("create a new empty preset"), 0, 0);
gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_new, TRUE, TRUE, 2);
gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 2);
gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 2);
// presets settings (search + quick access + full active)
vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_name(vb, "modulegroups-top-boxes");
d->edit_search_cb = gtk_check_button_new_with_label(_("show search line"));
g_signal_connect(G_OBJECT(d->edit_search_cb), "toggled",
G_CALLBACK(_manage_editor_search_toggle), self);
gtk_box_pack_start(GTK_BOX(vb), d->edit_search_cb, FALSE, TRUE, 0);
d->basics_chkbox = gtk_check_button_new_with_label(_("show quick access panel"));
g_signal_connect(G_OBJECT(d->basics_chkbox), "toggled",
G_CALLBACK(_manage_editor_basics_toggle), self);
gtk_box_pack_start(GTK_BOX(vb), d->basics_chkbox, FALSE, TRUE, 0);
d->edit_full_active_cb =
gtk_check_button_new_with_label(_("show all history modules in active group"));
gtk_widget_set_tooltip_text(
d->edit_full_active_cb,
_("show modules that are present in the history stack,"
" regardless of whether or not they are currently enabled"));
g_signal_connect(G_OBJECT(d->edit_full_active_cb), "toggled",
G_CALLBACK(_manage_editor_full_active_toggle),
self);
gtk_box_pack_start(GTK_BOX(vb), d->edit_full_active_cb, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 0);
// presets settings (autoapply)
vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
d->edit_autoapply_chkbox = gtk_check_button_new_with_label(_("auto-apply this preset"));
gtk_widget_set_sensitive(d->edit_autoapply_chkbox, FALSE); // always readonly. change are done with the button...
gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_chkbox, FALSE, TRUE, 0);
d->edit_autoapply_btn = dtgtk_button_new_full(dtgtk_cairo_paint_preferences, 0, NULL,
&(dtgtk_button_config_t){
.clicked_cb = G_CALLBACK(_preset_autoapply_edit),
.clicked_data = self,
});
gtk_widget_set_name(d->edit_autoapply_btn, "modulegroups-autoapply-btn");
gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_btn, FALSE, FALSE, 2);
gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vb_main), hb, FALSE, TRUE, 0);
// groups title line
hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(hb, "modulegroups-groups-title");
gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("module groups")), FALSE, TRUE, 0);
d->preset_btn_add_group = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL,
&(dtgtk_button_config_t){
.clicked_cb = G_CALLBACK(_manage_editor_group_add),
.clicked_data = self,
});
gtk_box_pack_start(GTK_BOX(hb), d->preset_btn_add_group, FALSE, FALSE, 0);
gtk_widget_set_halign(hb, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(vb_main), hb, FALSE, TRUE, 0);
// groups line
d->preset_groups_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_name(d->preset_groups_box, "modulegroups-groups-box");
gtk_widget_set_vexpand(d->preset_groups_box, TRUE);
gtk_widget_set_halign(d->preset_groups_box, GTK_ALIGN_FILL);
gtk_box_pack_start(GTK_BOX(vb_main), d->preset_groups_box, TRUE, TRUE, 0);
// read only message
d->preset_read_only_label
= gtk_label_new(_("this is a built-in read-only preset."
" duplicate it if you want to make changes"));
gtk_widget_set_name(d->preset_read_only_label, "modulegroups-ro");
gtk_box_pack_start(GTK_BOX(vb_main), d->preset_read_only_label, FALSE, TRUE, 0);
// reset button
hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
d->preset_reset_btn = gtk_button_new_with_label(_("reset"));
g_signal_connect(G_OBJECT(d->preset_reset_btn), "clicked",
G_CALLBACK(_manage_editor_reset), self);
gtk_box_pack_end(GTK_BOX(hb2), d->preset_reset_btn, FALSE, TRUE, 0);
GtkWidget *help = gtk_button_new_with_label(_("?"));
dt_gui_add_help_link(help, "modulegroups");
g_signal_connect(help, "clicked", G_CALLBACK(dt_gui_show_help), NULL);
gtk_box_pack_end(GTK_BOX(hb2), help, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vb_main), hb2, FALSE, TRUE, 0);
// we load the presets list
_manage_preset_update_list(self);
gtk_widget_show_all(vb_main);
// and we select the current one
const char *preset = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
_manage_editor_load(preset, self);
dt_gui_dialog_add(GTK_DIALOG(d->dialog), vb_main);
g_signal_connect(d->dialog, "destroy", G_CALLBACK(_manage_editor_destroy), self);
gtk_window_set_resizable(GTK_WINDOW(d->dialog), TRUE);
gtk_window_set_position(GTK_WINDOW(d->dialog), GTK_WIN_POS_CENTER_ON_PARENT);
gtk_widget_show(d->dialog);
}
void manage_presets(dt_lib_module_t *self)
{
_manage_show_window(self);
}
void view_leave(dt_lib_module_t *self,
dt_view_t *old_view,
dt_view_t *new_view)
{
if(!strcmp(old_view->module_name, "darkroom"))
{
_basics_hide(self);
}
}
void view_enter(dt_lib_module_t *self,
dt_view_t *old_view,
dt_view_t *new_view)
{
if(!strcmp(new_view->module_name, "darkroom"))
{
dt_lib_modulegroups_t *d = self->data;
// and we initialize the buttons too
char *preset = dt_conf_get_string("plugins/darkroom/modulegroups_preset");
if(!dt_lib_presets_apply(preset, self->plugin_name, self->version()))
dt_lib_presets_apply(_(FALLBACK_PRESET_NAME), self->plugin_name, self->version());
g_free(preset);
// and set the current group
d->current = dt_conf_get_int("plugins/darkroom/groups");
}
}
gboolean preset_autoapply(dt_lib_module_t *self)
{
return TRUE;
}
#undef PADDING
// 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