Skip to content

Latest commit

 

History

History
219 lines (158 loc) · 6.89 KB

File metadata and controls

219 lines (158 loc) · 6.89 KB

GUI Recipes - Common Patterns

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.


Recipe 1: Two-Tab Notebook UI

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(&notebook_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), &notebook_def);
}

Recipe 2: Visual Section Labels

// 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");

Recipe 3: Icon Toggle Button for Mask Display

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
}

Recipe 4: Shortcut Sections for Organized Controls

// 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");

Recipe 5: Array Parameter Sliders (RGB)

// 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"));

Recipe 6: Conditional Widget Visibility

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.


Recipe 7: Creating Labels

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")));

Recipe 8: Collapsible Section

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.