dev-doc/Notebook_UI.md
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)
GtkNotebook widget, 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 a dt_action_def_t struct. As you add pages using dt_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 static dt_action_def_t struct within your module's C file for this purpose. It gets filled dynamically by the helper functions.GtkNotebook.GtkWidget *dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip) (gui/gtk.h, gui/gtk.c)
GtkNotebook.GtkNotebook *notebook: The notebook created by dt_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.GtkWidget* which is the content container for the newly added page (typically a GtkBox). You pack all the controls (sliders, comboboxes, etc.) for that specific tab into this returned widget.dt_action_def_t struct originally passed to dt_ui_notebook_new, adding a new element definition corresponding to this tab, using the provided text as the element name.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 in filmicrgb or toneequal), self->widget should point to the container holding both the notebook and those other elements (often a GtkVBox). If the notebook is the entire UI, you can point self->widget to 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_t for 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:
static keyword are automatic variables. They are typically allocated on the function's stack frame. When the function (gui_init in 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.dt_ui_notebook_new and dt_action_define_iop take the address of the notebook_def struct (¬ebook_def). They don't make a full copy of the struct itself.dt_action_define_iop function registers the action (and its associated definition structure, including the .elements array which gets populated by dt_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 the gui_init function has returned (e.g., when a shortcut is pressed, or when displaying the shortcut mapping preferences).notebook_def were not static, it would be an automatic variable. When gui_init returned, the memory where notebook_def resided 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.static Solution: By declaring notebook_def as static, 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.static is required here to guarantee that the dt_action_def_t struct, which the action system needs to reference later, remains alive and valid in memory even after the gui_init function has completed.Create Notebook: Call dt_ui_notebook_new with 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_vbox before 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:
dt_ui_notebook_page to get the content container for that page.dt_ui_notebook_page. Use dt_bauhaus_*_from_params helpers.dt_ui_section_label_new or dt_gui_new_collapsible_section for organization within the page.DT_IOP_SECTION_FOR_PARAMS macro 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...
Set self->widget: Point self->widget to 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 GtkStack with GtkNotebook (colorequal.c):
dt_ui_notebook_page, you create a GtkStack.GtkBox) and add it as a named child to the GtkStack."switch-page" signal. In the callback, get the current page number and set the GtkStack's visible child using gtk_stack_set_visible_child_name.Collapsible Sections (dt_gui_collapsible_section_t):
colorequal.c within the "options" tab.dt_gui_new_collapsible_section to create a header with a toggle and a container.cs->container GtkBox.Important Points & Caveats
self->widget Assignment: 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.dt_ui_notebook_page for that tab (or into a GtkStack child if using that pattern).dt_action_def_t struct passed to dt_ui_notebook_new. The dt_ui_notebook_page function automatically adds elements to this definition. You then register the notebook itself using dt_action_define_iop. Shortcuts for controls within a tab are defined normally using dt_bauhaus_*_from_params or manually with dt_action_define_iop on 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". Use dt_ui_section_label_new for visual separation within a tab.dt_conf_set_int/dt_conf_get_int in gui_cleanup/gui_update if you want the module to remember the last used tab. (See colorequal.c and toneequal.c).N_("...") for static translatable strings (like tab labels passed to dt_ui_notebook_page) and _("...") for runtime translatable strings.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.