Practical code patterns for common darktable IOP GUI tasks.
For slider configuration recipes (percentage, EV, degrees, radians, hue gradients, color pickers, manual sliders), see sliders.md.
void gui_init(dt_iop_module_t *self)
{
dt_iop_mymodule_gui_data_t *g = IOP_GUI_ALLOC(mymodule);
// Main container
GtkWidget *main_box = dt_gui_vbox();
// Create notebook
static dt_action_def_t notebook_def = { };
g->notebook = dt_ui_notebook_new(¬ebook_def);
dt_gui_box_add(main_box, GTK_WIDGET(g->notebook));
// --- Page 1: Basic ---
GtkWidget *page_basic = dt_ui_notebook_page(g->notebook, N_("basic"), _("Basic controls"));
self->widget = page_basic; // Redirect packing
g->brightness = dt_bauhaus_slider_from_params(self, "brightness");
g->contrast = dt_bauhaus_slider_from_params(self, "contrast");
// --- Page 2: Advanced ---
GtkWidget *page_adv = dt_ui_notebook_page(g->notebook, N_("advanced"), _("Advanced controls"));
self->widget = page_adv;
g->gamma = dt_bauhaus_slider_from_params(self, "gamma");
g->method = dt_bauhaus_combobox_from_params(self, "method");
// --- Final ---
self->widget = main_box; // Set final top-level widget
// Register notebook for shortcuts
notebook_def.name = N_("page");
dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
}// Add a visual divider/header within a page
dt_gui_box_add(self->widget, dt_ui_section_label_new(_("shadows")));
g->shadow_amount = dt_bauhaus_slider_from_params(self, "shadow_amount");
g->shadow_tonality = dt_bauhaus_slider_from_params(self, "shadow_tonality");
dt_gui_box_add(self->widget, dt_ui_section_label_new(_("highlights")));
g->highlight_amount = dt_bauhaus_slider_from_params(self, "highlight_amount");g->show_mask = dt_iop_togglebutton_new(
self,
N_("display"), // Shortcut section
N_("show processing mask"), // Action label
NULL, // No Ctrl+click action
G_CALLBACK(show_mask_callback), // Toggle callback
FALSE, // Not a local shortcut
0, 0, // No accelerator
dtgtk_cairo_paint_showmask, // Mask icon
self->widget // Pack into module
);Callback:
static void show_mask_callback(GtkWidget *widget, dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE()
dt_iop_mymodule_gui_data_t *g = self->gui_data;
g->mask_display = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
dt_iop_refresh_center(self); // Redraw center view
}// Controls will be registered as "module/chroma/global", "module/chroma/shadows", etc.
dt_iop_module_t *sect_chroma = DT_IOP_SECTION_FOR_PARAMS(self, N_("chroma"));
g->chroma_global = dt_bauhaus_slider_from_params(sect_chroma, "chroma_global");
g->chroma_shadows = dt_bauhaus_slider_from_params(sect_chroma, "chroma_shadows");
g->chroma_highlights = dt_bauhaus_slider_from_params(sect_chroma, "chroma_highlights");
// Different section
dt_iop_module_t *sect_sat = DT_IOP_SECTION_FOR_PARAMS(self, N_("saturation"));
g->saturation_global = dt_bauhaus_slider_from_params(sect_sat, "saturation_global");// In params_t:
// float color[3]; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.5
g->color_r = dt_bauhaus_slider_from_params(self, "color[0]");
dt_bauhaus_widget_set_label(g->color_r, NULL, N_("red"));
g->color_g = dt_bauhaus_slider_from_params(self, "color[1]");
dt_bauhaus_widget_set_label(g->color_g, NULL, N_("green"));
g->color_b = dt_bauhaus_slider_from_params(self, "color[2]");
dt_bauhaus_widget_set_label(g->color_b, NULL, N_("blue"));Put all UI state logic (visibility, sensitivity, dynamic labels) in gui_changed():
void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous)
{
dt_iop_mymodule_gui_data_t *g = self->gui_data;
dt_iop_mymodule_params_t *p = self->params;
// Show/hide advanced slider based on mode selection
gtk_widget_set_visible(g->advanced_slider, p->mode == MODE_ADVANCED);
// Disable saturation when in monochrome mode
gtk_widget_set_sensitive(g->saturation, p->mode != MODE_MONOCHROME);
}If your module implements gui_changed(), end gui_update() with gui_changed(self, NULL, NULL) so the dependent UI state it maintains is recomputed — the framework does not call it for you here:
void gui_update(dt_iop_module_t *self)
{
dt_iop_mymodule_gui_data_t *g = self->gui_data;
dt_iop_mymodule_params_t *p = self->params;
// Only unbound widgets need this. The GTK setter is right here because
// dt_iop_togglebutton_new() gives a real GtkToggleButton; a _from_params toggle is a
// Bauhaus widget and is synced for you. See GUI.md, "gui_update()".
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->my_plain_button), p->my_bool);
// Apply all UI state adjustments
gui_changed(self, NULL, NULL);
}That keeps the two automatic routes into gui_changed() in step, so an externally loaded set of params ends up with the same visibility and sensitivity as a user edit would produce. A manual widget callback is the third route, and has to call it itself — see GUI.md.
Always use dt_ui_label_new() instead of gtk_label_new(): the wrapper ellipsizes long text instead of letting it stretch the panel width.
// WRONG
GtkWidget *label = gtk_label_new(_("Description that might be very long"));
// CORRECT
GtkWidget *label = dt_ui_label_new(_("Description that might be very long"));
dt_gui_box_add(self->widget, label);For section headers/dividers, use dt_ui_section_label_new() instead:
dt_gui_box_add(self->widget, dt_ui_section_label_new(_("Advanced Options")));void gui_init(dt_iop_module_t *self)
{
dt_iop_mymodule_gui_data_t *g = IOP_GUI_ALLOC(mymodule);
GtkWidget *main_box = self->widget = dt_gui_vbox();
// Main controls (always visible)
g->amount = dt_bauhaus_slider_from_params(self, "amount");
// Collapsible "advanced" section
dt_gui_new_collapsible_section(&g->advanced_section,
"plugins/darkroom/mymodule/expand_advanced",
_("advanced"),
GTK_BOX(main_box),
DT_ACTION(self));
// Pack widgets into the collapsible container
self->widget = GTK_WIDGET(g->advanced_section.container);
g->detail = dt_bauhaus_slider_from_params(self, "detail");
g->quality = dt_bauhaus_combobox_from_params(self, "quality");
// Restore main container
self->widget = main_box;
}The section remembers its expand/collapse state across sessions via the config key.