diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index adb276f874a6..53c841a19fb2 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -23,6 +23,7 @@ #include "control/conf.h" #include "develop/develop.h" #include "develop/imageop.h" +#include "develop/imageop_gui.h" #include "gui/accelerators.h" #include "gui/color_picker_proxy.h" #include "gui/gtk.h" @@ -1213,7 +1214,9 @@ static void _highlight_changed_notebook_tab(GtkWidget *w, gpointer user_data) c; c = g_list_delete_link(c, c)) { - if(!is_changed && DT_IS_BAUHAUS_WIDGET(c->data) && gtk_widget_get_visible(c->data)) + if(is_changed) continue; + + if(DT_IS_BAUHAUS_WIDGET(c->data) && gtk_widget_get_visible(c->data)) { dt_bauhaus_widget_t *b = DT_BAUHAUS_WIDGET(c->data); if(!b->field) continue; @@ -1227,6 +1230,15 @@ static void _highlight_changed_notebook_tab(GtkWidget *w, gpointer user_data) is_changed = b->combobox.entries->len && b->combobox.active != b->combobox.defpos; } + // dt_bauhaus_toggle_from_params() checkbuttons are plain GtkCheckButtons, + // not bauhaus widgets, so they'd otherwise never be considered here. + // dt_bauhaus_toggle_widget_is_changed() (develop/imageop_gui.c) reads + // the module's current default_params live rather than a value cached + // at some earlier point, so this stays correct even for modules that + // recompute their real defaults per-image (e.g. in reload_defaults()), + // after the checkbox already exists. + else if(GTK_IS_TOGGLE_BUTTON(c->data) && gtk_widget_get_visible(c->data)) + is_changed = dt_bauhaus_toggle_widget_is_changed(GTK_WIDGET(c->data)); } GtkWidget *label = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), w); @@ -1237,6 +1249,11 @@ static void _highlight_changed_notebook_tab(GtkWidget *w, gpointer user_data) dt_gui_remove_class(label, "changed"); } +void dt_bauhaus_highlight_changed_notebook_tab(GtkWidget *w, gpointer user_data) +{ + _highlight_changed_notebook_tab(w, user_data); +} + void dt_bauhaus_update_from_field(dt_iop_module_t *module, GtkWidget *widget, gpointer params, diff --git a/src/bauhaus/bauhaus.h b/src/bauhaus/bauhaus.h index e9ddd0943839..1650746b4b62 100644 --- a/src/bauhaus/bauhaus.h +++ b/src/bauhaus/bauhaus.h @@ -218,6 +218,13 @@ void dt_bauhaus_widget_set_field(GtkWidget *widget, gpointer field, dt_introspection_type_t field_type); gpointer dt_bauhaus_widget_get_field(GtkWidget *widget); +// mark (or re-evaluate, if user_data is NULL/FALSE) the enclosing notebook +// tab as "changed" -- the same mechanism dt_bauhaus_slider/combobox value +// changes already drive internally (_highlight_changed_notebook_tab); this +// is a public wrapper so non-bauhaus widgets bound via the same params +// (e.g. dt_bauhaus_toggle_from_params()'s plain GtkCheckButton) can drive +// the same tab-header indicator. +void dt_bauhaus_highlight_changed_notebook_tab(GtkWidget *w, gpointer user_data); // update one bauhaus widget or all widgets in a module from the provided (blend)params void dt_bauhaus_update_from_field(dt_iop_module_t *module, GtkWidget *widget, diff --git a/src/develop/imageop_gui.c b/src/develop/imageop_gui.c index cbe2e981382c..b062d0306b55 100644 --- a/src/develop/imageop_gui.c +++ b/src/develop/imageop_gui.c @@ -53,6 +53,8 @@ static void _iop_toggle_callback(GtkWidget *togglebutton, dt_module_param_t *dat if(*field != previous) { + dt_bauhaus_highlight_changed_notebook_tab( + togglebutton, GINT_TO_POINTER(dt_bauhaus_toggle_widget_is_changed(togglebutton))); dt_iop_gui_changed(DT_ACTION(self), togglebutton, &previous); } } @@ -246,6 +248,13 @@ GtkWidget *dt_bauhaus_toggle_from_params(dt_iop_module_t *self, const char *para DT_IOP_SECTION_FOR_PARAMS_UNWIND(module_param->module); module_param->param = (uint8_t *)p + f->header.offset; g_signal_connect_data(G_OBJECT(button), "toggled", G_CALLBACK(_iop_toggle_callback), module_param, (GClosureNotify)g_free, 0); + // Stash a second, non-owning reference to the same module_param so + // dt_bauhaus_toggle_widget_reset()/dt_bauhaus_toggle_widget_is_changed() + // can find it from just the widget (e.g. from generic group/page reset + // or tab-changed-tracking code with no other context available). Freed + // together with module_param by the "toggled" signal's GClosureNotify + // above when the button is destroyed; nothing here takes ownership. + g_object_set_data(G_OBJECT(button), "dt-toggle-module-param", module_param); dt_action_define_iop(module_param->module, section, str, button, &dt_action_def_toggle); } @@ -263,6 +272,52 @@ GtkWidget *dt_bauhaus_toggle_from_params(dt_iop_module_t *self, const char *para return button; } +// Both functions below read the module's current default_params live, at +// call time, rather than any value captured when the checkbox was first +// created -- some modules recompute their real defaults per-image (in +// reload_defaults(), which can run again long after the checkbox already +// exists), and a stale snapshot would silently go wrong for exactly those +// modules: resetting, or reporting "changed", against whatever the default +// used to be instead of what it actually is for the image currently loaded. +static gboolean _toggle_widget_default(GtkWidget *togglebutton, gboolean *out_default) +{ + dt_module_param_t *data = g_object_get_data(G_OBJECT(togglebutton), "dt-toggle-module-param"); + if(!data || !data->module || !data->module->default_params) return FALSE; + + const size_t offset = (uint8_t *)data->param - (uint8_t *)data->module->params; + *out_default = *(gboolean *)((uint8_t *)data->module->default_params + offset); + return TRUE; +} + +void dt_bauhaus_toggle_widget_reset(GtkWidget *togglebutton) +{ + // An insensitive checkbox isn't in effect right now -- some modules also + // blank a checkbox's displayed tick while insensitive without touching + // the actual stored value, so there's nothing meaningful to reset, and + // forcing the display back to "checked" would only undo that module's + // own display logic for no reason. + if(!gtk_widget_get_sensitive(togglebutton)) return; + + gboolean default_value; + if(!_toggle_widget_default(togglebutton, &default_value)) return; // not one of ours + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton), default_value); + // if the value was already at default, the "toggled" signal (and with it + // _iop_toggle_callback's own highlight call) never fired -- make sure the + // tab indicator still gets re-evaluated regardless, matching how + // dt_bauhaus_widget_reset()'s callers already expect group/page resets to + // always leave the "changed" state correct. + dt_bauhaus_highlight_changed_notebook_tab(togglebutton, GINT_TO_POINTER(FALSE)); +} + +gboolean dt_bauhaus_toggle_widget_is_changed(GtkWidget *togglebutton) +{ + gboolean default_value; + if(!_toggle_widget_default(togglebutton, &default_value)) return FALSE; + + return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(togglebutton)) != default_value; +} + GtkWidget *dt_iop_togglebutton_new(dt_iop_module_t *self, const char *section, const gchar *label, const gchar *ctrl_label, GCallback callback, gboolean local, guint accel_key, GdkModifierType mods, DTGTKCairoPaintIconFunc paint, GtkWidget *box) diff --git a/src/develop/imageop_gui.h b/src/develop/imageop_gui.h index dea68cccef07..39f68cd5e330 100644 --- a/src/develop/imageop_gui.h +++ b/src/develop/imageop_gui.h @@ -28,6 +28,21 @@ GtkWidget *dt_bauhaus_combobox_from_params(dt_iop_module_t *self, const char *pa GtkWidget *dt_bauhaus_toggle_from_params(dt_iop_module_t *self, const char *param); +// reset a dt_bauhaus_toggle_from_params() checkbutton to its param's current +// default value (read live from the module's default_params at the moment +// of the call, so this stays correct even if the module recomputes its +// defaults per-image after the checkbox was created). No-op if +// `togglebutton` wasn't created by dt_bauhaus_toggle_from_params(), or if +// it's currently insensitive (an insensitive control isn't in effect right +// now, so there's nothing meaningful to reset). +void dt_bauhaus_toggle_widget_reset(GtkWidget *togglebutton); + +// true if a dt_bauhaus_toggle_from_params() checkbutton's current value +// differs from its param's current default (read live, same as above). +// False (not changed) if `togglebutton` wasn't created by +// dt_bauhaus_toggle_from_params(). +gboolean dt_bauhaus_toggle_widget_is_changed(GtkWidget *togglebutton); + // package dt_iop_module_t pointer and section name to pass to a _from_params function // it will then create a widget action in a section, rather than top level in the module // optionally pass a box to add the widgets to diff --git a/src/gui/gtk.c b/src/gui/gtk.c index 32fc10563c8b..d942154062a3 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -31,6 +31,7 @@ #include "bauhaus/bauhaus.h" #include "develop/develop.h" #include "develop/imageop.h" +#include "develop/imageop_gui.h" #include "dtgtk/drawingarea.h" #include "dtgtk/expander.h" #include "dtgtk/sidepanel.h" @@ -3837,13 +3838,32 @@ GdkModifierType dt_key_modifier_state() static void _reset_all_bauhaus(GtkNotebook *notebook, GtkWidget *box) { + // Two passes rather than one: some modules react to a slider/combobox + // changing by programmatically changing a checkbox too (e.g. turning a + // feature back on when one of its own settings is touched). If a + // checkbox were reset in the same pass as sliders/comboboxes, a slider + // reset that runs afterwards could still trigger that kind of side + // effect and silently undo the checkbox reset that already ran earlier + // in the same pass. Resetting every checkbox only after every slider/ + // combobox on the page has already settled avoids that regardless of + // what order the widgets happen to appear in. + GList *toggles = NULL; for(GList *c = gtk_container_get_children(GTK_CONTAINER(box)); c; c = g_list_delete_link(c, c)) { if(DT_IS_BAUHAUS_WIDGET(c->data)) dt_bauhaus_widget_reset(GTK_WIDGET(c->data)); + // dt_bauhaus_toggle_from_params() checkbuttons are plain GtkCheckButtons, + // not bauhaus widgets, so they're otherwise silently skipped by the + // check above; dt_bauhaus_toggle_widget_reset() no-ops for any other + // GtkToggleButton (e.g. one not created by that function). + else if(GTK_IS_TOGGLE_BUTTON(c->data)) + toggles = g_list_prepend(toggles, c->data); } + for(GList *c = toggles; c; c = c->next) + dt_bauhaus_toggle_widget_reset(GTK_WIDGET(c->data)); + g_list_free(toggles); dt_gui_remove_class(gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), box), "changed"); }