Core Concept: GtkNotebook
The foundation for tabbed interfaces in GTK (and therefore darktable's usage) is the GtkNotebook widget. It's a container that manages multiple "pages" (each being another container like a GtkBox) and displays tabs to switch between them.
Darktable Helper Functions
Darktable provides convenient wrapper functions to simplify the creation and management of notebooks within the IOP GUI structure:
-
GtkNotebook *dt_ui_notebook_new(dt_action_def_t *def)(gui/gtk.h,gui/gtk.c)- Purpose: Creates a new
GtkNotebookwidget, ready to have pages added. dt_action_def_t *def: This is crucial for integrating with darktable's shortcut system. You pass a pointer to adt_action_def_tstruct. As you add pages usingdt_ui_notebook_page, this struct will be populated with elements corresponding to each tab, allowing you to define shortcuts for switching directly to specific tabs (e.g., "activate tab 'look'"). You typically define a staticdt_action_def_tstruct within your module's C file for this purpose. It gets filled dynamically by the helper functions.- Returns: A pointer to the newly created
GtkNotebook.
- Purpose: Creates a new
-
GtkWidget *dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip)(gui/gtk.h,gui/gtk.c)- Purpose: Adds a new page (tab) to an existing
GtkNotebook. GtkNotebook *notebook: The notebook created bydt_ui_notebook_new.const char *text: The translatable label for the tab (e.g.,N_("look")).const char *tooltip: An optional translatable tooltip for the tab.- Returns: A
GtkWidget*which is the content container for the newly added page (typically aGtkBox). You pack all the controls (sliders, comboboxes, etc.) for that specific tab into this returned widget. - Side Effect: This function also updates the
dt_action_def_tstruct originally passed todt_ui_notebook_new, adding a new element definition corresponding to this tab, using the providedtextas the element name.
- Purpose: Adds a new page (tab) to an existing
Key dt_iop_module_t Member
self->widget: This pointer must be set to the top-level container widget for your module's entire UI. If your UI includes elements outside the notebook (like the graphs infilmicrgbortoneequal),self->widgetshould point to the container holding both the notebook and those other elements (often aGtkVBox). If the notebook is the entire UI, you can pointself->widgetto it, but it's generally safer to wrap it in a box.
Standard Widget Creation Helpers
You'll populate the pages returned by dt_ui_notebook_page using standard darktable GUI helper functions, often linked to module parameters defined via introspection:
dt_bauhaus_slider_from_params(dt_iop_module_t *self, const char *param)(develop/imageop_gui.h,develop/imageop_gui.c)dt_bauhaus_combobox_from_params(dt_iop_module_t *self, const char *param)(develop/imageop_gui.h,develop/imageop_gui.c)dt_bauhaus_toggle_from_params(dt_iop_module_t *self, const char *param)(develop/imageop_gui.h,develop/imageop_gui.c)dt_ui_section_label_new(const gchar *str)(gui/gtk.h,gui/gtk.c): For adding visual separators/headers within a tab page.
Steps to Create a Tabbed Module UI (Inside gui_init)
-
Create Main Container: Create the top-level container for your module's UI, usually a vertical
GtkBox.// In gui_init(dt_iop_module_t *self) dt_iop_yourmodule_gui_data_t *g = IOP_GUI_ALLOC(yourmodule); GtkWidget *main_vbox = dt_gui_vbox();
-
Define Action Struct: Define a static
dt_action_def_tfor the notebook shortcuts. It will be populated later.static dt_action_def_t notebook_def = { }; // Name and process filled later
notebook_def needs to be static in this context:
- Lifetime of Automatic Variables: Variables declared inside a function without the
statickeyword are automatic variables. They are typically allocated on the function's stack frame. When the function (gui_initin this case) finishes executing and returns, its stack frame is destroyed, and all automatic variables within it cease to exist. Their memory locations are reclaimed and can be overwritten by subsequent function calls. - Passing by Address: Both
dt_ui_notebook_newanddt_action_define_ioptake the address of thenotebook_defstruct (¬ebook_def). They don't make a full copy of the struct itself. - Action System Registration: The
dt_action_define_iopfunction registers the action (and its associated definition structure, including the.elementsarray which gets populated bydt_ui_notebook_page) with darktable's central action/shortcut system. This system needs to be able to refer back to this definition later, potentially long after thegui_initfunction has returned (e.g., when a shortcut is pressed, or when displaying the shortcut mapping preferences). - The Dangling Pointer Problem: If
notebook_defwere not static, it would be an automatic variable. Whengui_initreturned, the memory wherenotebook_defresided would be invalid. However, the action system would still hold a pointer (¬ebook_def) to that now-invalid memory location. This is called a dangling pointer. Attempting to access the data through this dangling pointer later (e.g., to process a shortcut for switching tabs) would lead to undefined behavior – most likely a crash, but potentially silent data corruption. staticSolution: By declaringnotebook_defasstatic, you change its storage duration. It is no longer allocated on the stack. Instead, it's allocated in a static memory segment and persists for the entire lifetime of the program. This ensures that the address (¬ebook_def) passed to the action system remains valid throughout the program's execution, and the action system can safely access the definition struct whenever needed.- In short:
staticis required here to guarantee that thedt_action_def_tstruct, which the action system needs to reference later, remains alive and valid in memory even after thegui_initfunction has completed.
-
Create Notebook: Call
dt_ui_notebook_newwith the address of your action definition struct.g->notebook = dt_ui_notebook_new(¬ebook_def);
-
Add Graph/Other Elements (Optional): If you have UI elements outside the notebook (like a graph area), pack them into the
main_vboxbefore or after the notebook.g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(...)); // Configure expansion on the widget directly (replaces expand=TRUE, fill=TRUE) gtk_widget_set_vexpand(GTK_WIDGET(g->area), TRUE); gtk_widget_set_valign(GTK_WIDGET(g->area), GTK_ALIGN_FILL); // add to the box dt_gui_box_add(main_vbox, GTK_WIDGET(g->area)); // Pack graph first dt_gui_box_add(main_vbox, GTK_WIDGET(g->notebook)); // Then the notebook
-
Add Pages and Controls: For each desired tab:
- Call
dt_ui_notebook_pageto get the content container for that page. - Create and pack the necessary sliders, comboboxes, labels, etc., into the widget returned by
dt_ui_notebook_page. Usedt_bauhaus_*_from_paramshelpers. - Optionally use
dt_ui_section_label_newordt_gui_new_collapsible_sectionfor organization within the page. - Optionally use the
DT_IOP_SECTION_FOR_PARAMSmacro to associate specific controls with a named section within the tab for shortcut mapping.
// Page 1: "Look" GtkWidget *page_look = dt_ui_notebook_page(g->notebook, N_("look"), _("Look adjustments")); self->widget = page_look; // Temporarily set self->widget for dt_bauhaus_* helpers dt_bauhaus_slider_from_params(self, "look_offset"); dt_bauhaus_slider_from_params(self, "look_slope"); // ... add other widgets for the "Look" tab ... // Page 2: "Curve" GtkWidget *page_curve = dt_ui_notebook_page(g->notebook, N_("curve"), _("Curve adjustments")); self->widget = page_curve; // Temporarily set self->widget // Use DT_IOP_SECTION_FOR_PARAMS to group shortcuts under "curve/range" dt_iop_module_t *sect_range = DT_IOP_SECTION_FOR_PARAMS(self, N_("range")); dt_bauhaus_slider_from_params(sect_range, "range_black_relative_exposure"); dt_bauhaus_slider_from_params(sect_range, "range_white_relative_exposure"); // ... add other widgets for the "Curve" tab ... // Add more pages as needed...
- Call
-
Set
self->widget: Pointself->widgetto the main container created in step 1.self->widget = main_vbox; // Restore the main container as self->widget
-
Register Notebook Action: Register the notebook itself with the action system using the struct populated by
dt_ui_notebook_page.// Define the action name and process function for the notebook struct notebook_def.name = "page"; // Or any suitable name notebook_def.process = _action_process_tabs; // (If using the standard tab switching logic) dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
Advanced Patterns
-
Using
GtkStackwithGtkNotebook(colorequal.c):- Instead of packing controls directly into the page returned by
dt_ui_notebook_page, you create aGtkStack. - For each set of controls that should appear for a specific tab, create a container (e.g.,
GtkBox) and add it as a named child to theGtkStack. - Connect to the notebook's
"switch-page"signal. In the callback, get the current page number and set theGtkStack's visible child usinggtk_stack_set_visible_child_name. - This is useful when you have similar controls (like banks of sliders) but they need to operate on different parameters based on the active tab (hue vs. saturation vs. brightness).
- Instead of packing controls directly into the page returned by
-
Collapsible Sections (
dt_gui_collapsible_section_t):- Used in
colorequal.cwithin the "options" tab. - Call
dt_gui_new_collapsible_sectionto create a header with a toggle and a container. - Pack widgets into the
cs->containerGtkBox. - This provides further organization within a single notebook page.
- Used in
Important Points & Caveats
self->widgetAssignment: This is critical. It must point to the widget that contains all UI elements for the module that should be packed into the main GUI panel (often the right-hand panel). Failure to do this correctly can lead to UI elements not appearing or malfunctioning.- Widget Packing: Widgets intended for a specific tab must be packed into the container widget returned by
dt_ui_notebook_pagefor that tab (or into aGtkStackchild if using that pattern). - Shortcut Definition: Tab-switching shortcuts are defined via the
dt_action_def_tstruct passed todt_ui_notebook_new. Thedt_ui_notebook_pagefunction automatically adds elements to this definition. You then register the notebook itself usingdt_action_define_iop. Shortcuts for controls within a tab are defined normally usingdt_bauhaus_*_from_paramsor manually withdt_action_define_iopon the specific control. DT_IOP_SECTION_FOR_PARAMS: This macro is specifically for organizing shortcuts hierarchically. It doesn't create visual sections but allows shortcuts like "module/Tab Name/Section Name/Parameter Name". Usedt_ui_section_label_newfor visual separation within a tab.- State Management: Remember to save/restore the active tab index using
dt_conf_set_int/dt_conf_get_intingui_cleanup/gui_updateif you want the module to remember the last used tab. (Seecolorequal.candtoneequal.c). - Translations: Use
N_("...")for static translatable strings (like tab labels passed todt_ui_notebook_page) and_("...")for runtime translatable strings. - Complexity: While notebooks offer good organization, overly complex nesting (notebooks containing stacks containing collapsible sections) can become harder to manage. Choose the simplest structure that meets the organizational need.
By following these steps and patterns, referencing the example modules provided, you can effectively create multi-tabbed user interfaces for your darktable modules. Remember that the gui_init function is the central place where this UI structure is built.