/* This file is part of darktable, Copyright (C) 2011-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/darktable.h" #include "common/debug.h" #include "common/math.h" #include "common/styles.h" #include "common/undo.h" #include "control/conf.h" #include "control/control.h" #include "develop/develop.h" #include "develop/masks.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/styles.h" #include "libs/lib.h" #include "libs/lib_api.h" #include "common/history.h" #include "bauhaus/bauhaus.h" #include #include #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif DT_MODULE(1) typedef struct dt_undo_history_t { GList *history; int history_end; GList *iop_order_list; dt_masks_edit_mode_t mask_edit_mode; dt_dev_pixelpipe_display_mask_t request_mask_display; } dt_undo_history_t; typedef struct dt_lib_history_t { /* vbox with managed history items */ GtkWidget *history_box; GtkWidget *create_button; GtkWidget *compress_button; gboolean record_undo; int record_history_level; // set to +1 in signal DT_SIGNAL_DEVELOP_HISTORY_WILL_CHANGE // and back to -1 in DT_SIGNAL_DEVELOP_HISTORY_CHANGE. We want // to avoid multiple will-change before a change cb. } dt_lib_history_t; /* 3 widgets in each history line */ #define HIST_WIDGET_NUMBER 0 #define HIST_WIDGET_MODULE 1 #define HIST_WIDGET_STATUS 2 /* compress history stack */ static void _lib_history_compress_clicked_callback(GtkButton *widget, gpointer user_data); static void _lib_history_compress_pressed_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self); static void _lib_history_button_clicked_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self); static void _lib_history_create_style_button_clicked_callback(GtkWidget *widget, gpointer user_data); /* signal callback for history change */ static void _lib_history_will_change_callback(gpointer instance, dt_lib_module_t *self); static void _lib_history_change_callback(gpointer instance, dt_lib_module_t *self); static void _lib_history_module_remove_callback(gpointer instance, dt_iop_module_t *module, gpointer user_data); const char *name(dt_lib_module_t *self) { return _("history"); } const char *description(dt_lib_module_t *self) { return _("display the sequence of edit actions\n" "- click on an entry to temporarily return to\n" " that earlier state of the edit\n" " (warning: any edits will be made to that\n" " earlier state, losing later changes)\n" "- shift-click to focus that module without\n" " changing the edit state"); } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_DARKROOM; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_LEFT_CENTER; } int position(const dt_lib_module_t *self) { return 900; } void gui_init(dt_lib_module_t *self) { /* initialize ui widgets */ dt_lib_history_t *d = g_malloc0(sizeof(dt_lib_history_t)); self->data = (void *)d; d->record_undo = TRUE; d->record_history_level = 0; d->history_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_has_tooltip(d->history_box, FALSE); d->compress_button = dt_action_button_new (self, N_("compress history stack"), _lib_history_compress_clicked_callback, self, _("create a minimal history stack which produces the same image\n" "ctrl+click to truncate history to the selected item"), 0, 0); // capture phase, so that the claim in the callback also keeps the button's // own gesture from emitting "clicked" (see the callback) gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER(dt_gui_connect_click(d->compress_button, _lib_history_compress_pressed_callback, NULL, self)), GTK_PHASE_CAPTURE); /* add toolbar button for creating style */ d->create_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, CPF_NONE, NULL, &(dtgtk_button_config_t){ .tooltip = _("create a style from the current history stack"), .action = DT_ACTION(self), .action_label = N_("create style from history"), .action_def = &dt_action_def_button, .clicked_cb = G_CALLBACK(_lib_history_create_style_button_clicked_callback), }); gtk_widget_set_name(d->create_button, "non-flat"); self->widget = dt_gui_vbox (dt_ui_resize_wrap(d->history_box, 1, "plugins/darkroom/history/windowheight"), dt_gui_hbox(dt_gui_expand(d->compress_button), d->create_button)); gtk_widget_set_name(self->widget, "history-ui"); gtk_widget_show_all(self->widget); /* connect to history change signal for updating the history view */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_HISTORY_WILL_CHANGE, _lib_history_will_change_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_HISTORY_CHANGE, _lib_history_change_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MODULE_REMOVE, _lib_history_module_remove_callback); } void gui_cleanup(dt_lib_module_t *self) { g_free(self->data); self->data = NULL; } static GtkWidget *_lib_history_create_button(dt_lib_module_t *self, const int num, const char *label, const gboolean enabled, const gboolean default_enabled, const gboolean always_on, const gboolean selected, const gboolean deprecated) { /* create label */ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gchar numlab[10]; g_snprintf(numlab, sizeof(numlab), "%2d", num + 1); GtkWidget *numwidget = gtk_label_new(numlab); gtk_widget_set_name(numwidget, "history-number"); dt_gui_add_class(numwidget, "dt_history_items"); dt_gui_add_class(numwidget, "dt_monospace"); GtkWidget *onoff = NULL; /* create toggle button */ GtkWidget *widget = gtk_toggle_button_new_with_label(""); dt_gui_add_class(widget, "dt_transparent_background"); GtkWidget *lab = gtk_bin_get_child(GTK_BIN(widget)); gtk_widget_set_halign(lab, GTK_ALIGN_START); gtk_label_set_xalign(GTK_LABEL(lab), 0); gtk_label_set_ellipsize(GTK_LABEL(lab), PANGO_ELLIPSIZE_END); gtk_label_set_markup (GTK_LABEL (lab), label); if(always_on) { onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch_on, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("always-on module"), }); dtgtk_button_set_active(DTGTK_BUTTON(onoff), TRUE); } else if(default_enabled) { onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("default enabled module"), }); dtgtk_button_set_active(DTGTK_BUTTON(onoff), enabled); } else { if(deprecated) { onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch_deprecated, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("deprecated module"), }); } else { onoff = dtgtk_button_new(dtgtk_cairo_paint_switch, 0, NULL); dt_gui_add_class(onoff, enabled ? "" : "dt_history_switch_off"); } dt_gui_add_class(lab, enabled ? "" : "dt_history_switch_off"); dtgtk_button_set_active(DTGTK_BUTTON(onoff), enabled); } dt_gui_add_class(widget, "dt_history_items"); dt_gui_add_class(onoff, "dt_history_switch"); gtk_widget_set_sensitive(onoff, FALSE); if(selected) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), TRUE); /* Handle the press before GtkToggleButton's TARGET-phase default gesture. * This lets an active-row re-click claim the sequence before the widget can * toggle itself off, while unclaimed presses still reach normal handling. */ GtkGestureSingle *history_gesture = dt_gui_connect_click(widget, _lib_history_button_clicked_callback, NULL, self); gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(history_gesture), GTK_PHASE_CAPTURE); /* associate the history number */ g_object_set_data(G_OBJECT(widget), "history-number", GINT_TO_POINTER(num + 1)); g_object_set_data(G_OBJECT(widget), "label", (gpointer)label); gtk_box_pack_start(GTK_BOX(hbox), numwidget, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0); gtk_box_pack_end(GTK_BOX(hbox), onoff, FALSE, FALSE, 0); return hbox; } static void _reset_module_instance(GList *hist, dt_iop_module_t *module, const int multi_priority) { for(; hist; hist = g_list_next(hist)) { dt_dev_history_item_t *hit = hist->data; if(!hit->module && strcmp(hit->op_name, module->op) == 0 && hit->multi_priority == multi_priority) { hit->module = module; } } } struct _cb_data { dt_iop_module_t *module; int multi_priority; }; static void _undo_items_cb(gpointer user_data, const dt_undo_type_t type, dt_undo_data_t data) { struct _cb_data *udata = (struct _cb_data *)user_data; dt_undo_history_t *hdata = (dt_undo_history_t *)data; _reset_module_instance(hdata->history, udata->module, udata->multi_priority); } static void _history_invalidate_cb(gpointer user_data, const dt_undo_type_t type, dt_undo_data_t data) { dt_iop_module_t *module = (dt_iop_module_t *)user_data; dt_undo_history_t *hist = (dt_undo_history_t *)data; dt_dev_invalidate_history_module(hist->history, module); } static void _add_module_expander(GList *iop_list, dt_iop_module_t *module) { // dt_dev_reload_history_items won't do this for base instances // and it will call gui_init() for the rest // so we do it here if(!dt_iop_is_hidden(module) && !module->expander) { /* add module to right panel */ dt_iop_gui_set_expander(module); dt_iop_gui_set_expanded(module, TRUE, FALSE); dt_iop_gui_update_blending(module); } } // return the 1st history entry that matches module static dt_dev_history_item_t *_search_history_by_module(GList *history_list, dt_iop_module_t *module) { dt_dev_history_item_t *hist_ret = NULL; for(GList *history = history_list; history; history = g_list_next(history)) { dt_dev_history_item_t *hist_item = history->data; if(hist_item->module == module) { hist_ret = hist_item; break; } } return hist_ret; } static gboolean _check_deleted_instances(dt_develop_t *dev, GList **_iop_list, GList *history_list) { GList *iop_list = *_iop_list; gboolean deleted_module_found = FALSE; // we will check on dev->iop if there's a module that is not in history GList *modules = iop_list; while(modules) { dt_iop_module_t *mod = modules->data; gboolean delete_module = FALSE; // base modules are a special case // most base modules won't be in history and must not be deleted // but the user may have deleted a base instance of a multi-instance module // and then undo and redo, so we will end up with two entries in dev->iop // with multi_priority == 0, this can't happen and the extra one must be deleted // dev->iop is sorted by (priority, multi_priority DESC), so if the next one is // a base instance too, one must be deleted if(mod->multi_priority == 0) { GList *modules_next = g_list_next(modules); if(modules_next) { dt_iop_module_t *mod_next = modules_next->data; if(strcmp(mod_next->op, mod->op) == 0 && mod_next->multi_priority == 0) { // is the same one, check which one must be deleted const gboolean mod_in_history = _search_history_by_module(history_list, mod) != NULL; const gboolean mod_next_in_history = _search_history_by_module(history_list, mod_next) != NULL; // current is in history and next is not, delete next if(mod_in_history && !mod_next_in_history) { mod = mod_next; modules = modules_next; delete_module = TRUE; } // current is not in history and next is, delete current else if(!mod_in_history && mod_next_in_history) { delete_module = TRUE; } else { if(mod_in_history && mod_next_in_history) dt_print(DT_DEBUG_ALWAYS, "[_check_deleted_instances] found duplicate module" " %s %s (%i) and %s %s (%i) both in history", mod->op, mod->multi_name, mod->multi_priority, mod_next->op, mod_next->multi_name, mod_next->multi_priority); else dt_print(DT_DEBUG_ALWAYS, "[_check_deleted_instances] found duplicate module" " %s %s (%i) and %s %s (%i) none in history", mod->op, mod->multi_name, mod->multi_priority, mod_next->op, mod_next->multi_name, mod_next->multi_priority); } } } } // this is a regular multi-instance and must be in history else { delete_module = (_search_history_by_module(history_list, mod) == NULL); } // if module is not in history we delete it if(delete_module) { deleted_module_found = TRUE; if(darktable.develop->gui_module == mod) dt_iop_request_focus(NULL); DT_ENTER_GUI_UPDATE(); // we remove the plugin effectively if(!dt_iop_is_hidden(mod)) { // we just hide the module to avoid lots of gtk critical warnings gtk_widget_hide(mod->expander); // this is copied from dt_iop_gui_delete_callback(), not sure // why the above sentence... dt_iop_gui_cleanup_module(mod); } iop_list = g_list_remove_link(iop_list, modules); // remove it from all snapshots dt_undo_iterate(darktable.undo, DT_UNDO_HISTORY, mod, &_history_invalidate_cb); // we cleanup the module dt_action_cleanup_instance_iop(mod); // don't delete the module, a pipe may still need it dev->alliop = g_list_append(dev->alliop, mod); DT_LEAVE_GUI_UPDATE(); // and reset the list modules = iop_list; continue; } modules = g_list_next(modules); } if(deleted_module_found) iop_list = g_list_sort(iop_list, dt_sort_iop_by_order); *_iop_list = iop_list; return deleted_module_found; } static gboolean _rebuild_multi_priority(GList *history_list) { gboolean changed = FALSE; for(const GList *history = history_list; history; history = g_list_next(history)) { dt_dev_history_item_t *hitem = history->data; // if multi_priority is different in history and dev->iop // we keep the history version if(hitem->module && hitem->module->multi_priority != hitem->multi_priority) { dt_iop_update_multi_priority(hitem->module, hitem->multi_priority); changed = TRUE; } } return changed; } static gboolean _create_deleted_modules(GList **_iop_list, GList *history_list) { GList *iop_list = *_iop_list; gboolean changed = FALSE; gboolean done = FALSE; GList *l = history_list; while(l) { GList *next = g_list_next(l); dt_dev_history_item_t *hitem = l->data; // this fixes the duplicate module when undo: hitem->multi_priority = 0; if(hitem->module == NULL) { changed = TRUE; const dt_iop_module_t *base_module = dt_iop_get_module_from_list(iop_list, hitem->op_name); if(base_module == NULL) { dt_print(DT_DEBUG_ALWAYS, "[_create_deleted_modules] can't find base module for %s", hitem->op_name); return changed; } // from there we create a new module for this base instance. The // goal is to do a very minimal setup of the new module to be // able to write the history items. From there we reload the // whole history back and this will recreate the proper module // instances. dt_iop_module_t *module = calloc(1, sizeof(dt_iop_module_t)); if(dt_iop_load_module(module, base_module->so, base_module->dev)) { return changed; } module->instance = base_module->instance; if(!dt_iop_is_hidden(module)) { /** this works inside correct DT_ENTER/LEAVE_GUI_UPDATE() and ensures the module->gui_lock nutex is correctly intialized. */ dt_iop_gui_init(module); } // adjust the multi_name of the new module g_strlcpy(module->multi_name, hitem->multi_name, sizeof(module->multi_name)); dt_iop_update_multi_priority(module, hitem->multi_priority); module->iop_order = hitem->iop_order; // we insert this module into dev->iop iop_list = g_list_insert_sorted(iop_list, module, dt_sort_iop_by_order); // add the expander, dt_dev_reload_history_items() don't work well without one _add_module_expander(iop_list, module); // if not already done, set the module to all others same instance if(!done) { _reset_module_instance(history_list, module, hitem->multi_priority); // and do that also in the undo/redo lists struct _cb_data udata = { module, hitem->multi_priority }; dt_undo_iterate(darktable.undo, DT_UNDO_HISTORY, &udata, &_undo_items_cb); done = TRUE; } hitem->module = module; } l = next; } *_iop_list = iop_list; return changed; } static void _pop_undo(gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, dt_undo_action_t action, GList **imgs) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; if(type == DT_UNDO_HISTORY) { dt_lib_history_t *d = self->data; dt_undo_history_t *hist = (dt_undo_history_t *)data; dt_develop_t *dev = darktable.develop; // we swap current and undo history; // it will move between undo and redo queue GList *history_temp = hist->history; int history_end_temp = hist->history_end; GList *iop_order_list_temp = hist->iop_order_list; GList *iop_temp = g_list_copy(dev->iop); // Undo/redo can destroy module GUI data and alter the module topology. // Keep all screen pipes quiescent until the replacement develop state is // complete, so no old node can resume with a cleaned-up module GUI. dt_dev_pixelpipe_stop_and_lock_all(dev); // topology has changed? gboolean pipe_remove = FALSE; // we have to check if multi_priority has changed since history was saved // we will adjust it here if(_rebuild_multi_priority(history_temp)) { pipe_remove = TRUE; iop_temp = g_list_sort(iop_temp, dt_sort_iop_by_order); } // check if this undo a delete module and re-create it if(_create_deleted_modules(&iop_temp, history_temp)) { pipe_remove = TRUE; } // check if this is a redo of a delete module or an undo of an add module if(_check_deleted_instances(dev, &iop_temp, history_temp)) { pipe_remove = TRUE; } // disable recording undo as the _lib_history_change_callback will // be triggered by the calls below d->record_undo = FALSE; dt_pthread_mutex_lock(&dev->history_mutex); hist->history = dev->history; hist->history_end = dev->history_end; hist->iop_order_list = dev->iop_order_list; dev->history = history_temp; dev->history_end = history_end_temp; dev->iop_order_list = iop_order_list_temp; g_list_free(dev->iop); dev->iop = iop_temp; // topology has changed dt_dev_pixelpipe_rebuild(dev); // if dev->iop has changed reflect that on module list if(pipe_remove) dt_dev_reorder_gui_module_list(dev); // write new history and reload dt_dev_write_history(dev); dt_dev_reload_history_items(dev); dt_pthread_mutex_unlock(&dev->history_mutex); dt_ioppr_resync_modules_order(dev); dt_dev_pixelpipe_unlock_all(dev); dt_dev_modulegroups_set(darktable.develop, dt_dev_modulegroups_get(darktable.develop)); if(dev->gui_module) { dt_masks_set_edit_mode(dev->gui_module, hist->mask_edit_mode); darktable.develop->gui_module->request_mask_display = hist->request_mask_display; dt_iop_gui_blend_data_t *bd = dev->gui_module->blend_data; if(bd) gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(bd->showmask), hist->request_mask_display == DT_DEV_PIXELPIPE_DISPLAY_MASK); } } } static void _history_undo_data_free(gpointer data) { dt_undo_history_t *hist = (dt_undo_history_t *)data; g_list_free_full(hist->history, dt_dev_free_history_item); g_list_free_full(hist->iop_order_list, free); free(data); } static void _lib_history_module_remove_callback(gpointer instance, dt_iop_module_t *module, gpointer user_data) { dt_undo_iterate(darktable.undo, DT_UNDO_HISTORY, module, &_history_invalidate_cb); } static void _lib_history_will_change_callback(gpointer instance, dt_lib_module_t *self) { dt_lib_history_t *lib = self->data; gtk_container_foreach(GTK_CONTAINER(lib->history_box), (GtkCallback)gtk_widget_set_has_tooltip, NULL); if(lib->record_history_level++ == 0 && lib->record_undo) { /* record undo/redo history snapshot */ dt_undo_history_t *hist = malloc(sizeof(dt_undo_history_t)); hist->history = dt_history_duplicate(darktable.develop->history); hist->history_end = darktable.develop->history_end; hist->iop_order_list = dt_ioppr_iop_order_copy_deep(darktable.develop->iop_order_list); if(darktable.develop->gui_module) { hist->mask_edit_mode = dt_masks_get_edit_mode(); hist->request_mask_display = darktable.develop->gui_module->request_mask_display; } else { hist->mask_edit_mode = DT_MASKS_EDIT_OFF; hist->request_mask_display = DT_DEV_PIXELPIPE_DISPLAY_NONE; } dt_undo_record(darktable.undo, self, DT_UNDO_HISTORY, (dt_undo_data_t)hist, _pop_undo, _history_undo_data_free); } } #define CHG_STR(fmt, ...) g_strdup_printf("%s\t\t" #fmt "\t\u2192\t\t" #fmt " ", __VA_ARGS__) static gchar *_lib_history_bauhaus_text(gpointer bhfield, const char *d, dt_iop_module_t *module, float oval, float pval) { for(GSList *w = module->widget_list_bh; w; w = g_slist_next(w)) { dt_action_target_t *at = w->data; GtkWidget *widget = at->target; if(dt_bauhaus_widget_get_field(widget) == bhfield) { dt_action_t *as = at->action->owner; gchar *s = as && as->type == DT_ACTION_TYPE_SECTION ? g_strdup_printf("%s/%s", as->label, d) : NULL; gchar *old_text = dt_bauhaus_slider_get_text(widget, oval); gchar *new_text = dt_bauhaus_slider_get_text(widget, pval); gchar *ret_text = CHG_STR(%s, s ? s : d, old_text, new_text); g_free(old_text); g_free(new_text); g_free(s); return ret_text; } } return NULL; } static gchar *_lib_history_change_text(dt_introspection_field_t *field, const char *d, dt_iop_module_t *module, gpointer params, gpointer oldpar) { void *p = (void*)params + field->header.offset; void *o = (void*)oldpar + field->header.offset; #define CHANGE_TEXT_BAUHAUS(format, fieldtype) \ _lib_history_bauhaus_text((void*)module->params + field->header.offset, \ d, module, *(fieldtype*)o, *(fieldtype*)p) \ ?: CHG_STR(format, d, *(fieldtype*)o, *(fieldtype*)p) switch(field->header.type) { case DT_INTROSPECTION_TYPE_STRUCT: case DT_INTROSPECTION_TYPE_UNION: { gchar **change_parts = g_malloc0_n(field->Struct.entries + 1, sizeof(char*)); int num_parts = 0; for(int i = 0; i < field->Struct.entries; i++) { dt_introspection_field_t *entry = field->Struct.fields[i]; gchar *description = _(*entry->header.description ? entry->header.description : entry->header.field_name); if(d) description = g_strdup_printf("%s.%s", d, description); gchar *part = _lib_history_change_text(entry, description, module, params, oldpar); if(part) change_parts[num_parts++] = part; if(d) g_free(description); } gchar *struct_text = num_parts ? g_strjoinv("\n", change_parts) : NULL; g_strfreev(change_parts); return struct_text; } break; case DT_INTROSPECTION_TYPE_ARRAY: if(field->Array.type == DT_INTROSPECTION_TYPE_CHAR) { // stored params need not terminate the array; the checks below stop only at NUL const gboolean is_valid = memchr(o, '\0', field->Array.count) && memchr(p, '\0', field->Array.count) && g_utf8_validate((char *)o, -1, NULL) && g_utf8_validate((char *)p, -1, NULL); if(is_valid && strncmp((char*)o, (char*)p, field->Array.count)) return CHG_STR("%s", d, (char*)o, (char*)p); } else { const int max_elements = 4; gchar **change_parts = g_malloc0_n(max_elements + 1, sizeof(char*)); int num_parts = 0; for(int i = 0, item_offset = 0; i < field->Array.count; i++, item_offset += field->Array.field->header.size) { char *description = g_strdup_printf("%s[%d]", d, i); char *element_text = _lib_history_change_text(field->Array.field, description, module, (uint8_t *)params + item_offset, (uint8_t *)oldpar + item_offset); g_free(description); if(element_text && ++num_parts <= max_elements) change_parts[num_parts - 1] = element_text; else g_free(element_text); } gchar *array_text = NULL; if(num_parts > max_elements) array_text = g_strdup_printf("%s\t%d changes", d, num_parts); else if(num_parts > 0) array_text = g_strjoinv("\n", change_parts); g_strfreev(change_parts); return array_text; } break; case DT_INTROSPECTION_TYPE_FLOAT: if(!feqf(*(float*)o, *(float*)p, 1e-6) && (dt_isfinite(*(float*)o) || dt_isfinite(*(float*)p))) return CHANGE_TEXT_BAUHAUS(%.4f, float); break; case DT_INTROSPECTION_TYPE_INT: if(*(int*)o != *(int*)p) return CHANGE_TEXT_BAUHAUS(%d, int); break; case DT_INTROSPECTION_TYPE_UINT: if(*(unsigned int*)o != *(unsigned int*)p) return CHANGE_TEXT_BAUHAUS(%u, unsigned int); break; case DT_INTROSPECTION_TYPE_USHORT: if(*(unsigned short int*)o != *(unsigned short int*)p) return CHANGE_TEXT_BAUHAUS(%hu, unsigned short int); break; case DT_INTROSPECTION_TYPE_INT8: if(*(uint8_t*)o != *(uint8_t*)p) return CHANGE_TEXT_BAUHAUS(%d, uint8_t); break; case DT_INTROSPECTION_TYPE_CHAR: if(*(char*)o != *(char*)p) return CHANGE_TEXT_BAUHAUS('%c', char); break; case DT_INTROSPECTION_TYPE_FLOATCOMPLEX: if(*(float complex*)o != *(float complex*)p) return CHG_STR(%.4f + %.4fi, d, creal(*(float complex*)o), cimag(*(float complex*)o), creal(*(float complex*)p), cimag(*(float complex*)p)); break; case DT_INTROSPECTION_TYPE_ENUM: if(*(int*)o != *(int*)p) { const char *old_str = N_("unknown"), *new_str = N_("unknown"); for(dt_introspection_type_enum_tuple_t *i = field->Enum.values; i && i->name; i++) { if(i->value == *(int*)o) { old_str = i->description; if(!*old_str) old_str = i->name; } if(i->value == *(int*)p) { new_str = i->description; if(!*new_str) new_str = i->name; } } return CHG_STR(%s, d, _(old_str), _(new_str)); } break; case DT_INTROSPECTION_TYPE_BOOL: if(*(gboolean*)o != *(gboolean*)p) { char *old_str = *(gboolean*)o ? "on" : "off"; char *new_str = *(gboolean*)p ? "on" : "off"; return CHG_STR(%s, d, _(old_str), _(new_str)); } break; case DT_INTROSPECTION_TYPE_OPAQUE: { // TODO: special case float2 } break; default: dt_print(DT_DEBUG_ALWAYS, "unsupported introspection type \"%s\" encountered" " in _lib_history_change_text (field %s)\n", field->header.type_name, field->header.field_name); break; } return NULL; } static gboolean _changes_tooltip_callback(GtkWidget *widget, const gint x, const gint y, const gboolean keyboard_mode, GtkTooltip *tooltip, const dt_dev_history_item_t *hitem) { dt_iop_params_t *old_params = hitem->module->default_params; dt_develop_blend_params_t *old_blend = hitem->module->default_blendop_params; for(const GList *find_old = darktable.develop->history; find_old && find_old->data != hitem; find_old = g_list_next(find_old)) { const dt_dev_history_item_t *hiprev = find_old->data; if(hiprev->module == hitem->module) { old_params = hiprev->params; old_blend = hiprev->blend_params; } } gchar **change_parts = g_malloc0_n(sizeof(dt_develop_blend_params_t) / (sizeof(float)) + 10, sizeof(char*)); if(hitem->module->have_introspection) change_parts[0] = _lib_history_change_text(hitem->module->get_introspection()->field, NULL, hitem->module, hitem->params, old_params); int num_parts = change_parts[0] ? 1 : 0; if(hitem->module->flags() & IOP_FLAGS_SUPPORTS_BLENDING) { #define add_blend_history_change(field, fmt, label) \ if((hitem->blend_params->field) != (old_blend->field)) \ { \ gchar *chg_fmt = g_strconcat("%s\t\t", fmt, "\t\u2192\t\t", fmt, NULL); \ change_parts[num_parts++] = \ _lib_history_bauhaus_text(&hitem->module->blend_params->field, \ label, hitem->module, old_blend->field, \ hitem->blend_params->field) \ ?: g_strdup_printf(chg_fmt, label, \ old_blend->field, hitem->blend_params->field); \ g_free(chg_fmt); \ } #define add_blend_history_change_enum(field, label, list) \ if((hitem->blend_params->field) != (old_blend->field)) \ { \ const char *old_str = NULL, *new_str = NULL; \ for(const dt_introspection_type_enum_tuple_t *i = list; i->name; i++) \ { \ if(i->value == (old_blend->field)) old_str = i->name; \ if(i->value == (hitem->blend_params->field)) new_str = i->name; \ } \ \ change_parts[num_parts++] = (!old_str || !new_str) \ ? CHG_STR(%d, label, old_blend->field, \ hitem->blend_params->field)\ : CHG_STR(%s, label, Q_(old_str), Q_(new_str)); \ } add_blend_history_change_enum(blend_cst, _("colorspace"), dt_develop_blend_colorspace_names); add_blend_history_change_enum(mask_mode, _("mask mode"), dt_develop_mask_mode_names); add_blend_history_change_enum(blend_mode & DEVELOP_BLEND_MODE_MASK, _("blend mode"), dt_develop_blend_mode_names); add_blend_history_change_enum(blend_mode & DEVELOP_BLEND_REVERSE, _("blend operation"), dt_develop_blend_mode_flag_names); add_blend_history_change(blend_parameter, _("%.2f EV"), _("blend fulcrum")); add_blend_history_change(opacity, "%.4f", _("mask opacity")); add_blend_history_change_enum (mask_combine & (DEVELOP_COMBINE_INV | DEVELOP_COMBINE_INCL), _("combine masks"), dt_develop_combine_masks_names); add_blend_history_change(feathering_radius, "%.4f", _("feathering radius")); add_blend_history_change_enum(feathering_guide, _("feathering guide"), dt_develop_feathering_guide_names); add_blend_history_change(blur_radius, "%.4f", _("mask blur")); add_blend_history_change(contrast, "%.4f", _("mask contrast")); add_blend_history_change(brightness, "%.4f", _("brightness")); add_blend_history_change(raster_mask_instance, "%d", _("raster mask instance")); add_blend_history_change(raster_mask_id, "%d", _("raster mask id")); add_blend_history_change_enum(raster_mask_invert, _("invert mask"), dt_develop_invert_mask_names); const char old_combine = old_blend->mask_combine & DEVELOP_COMBINE_MASKS_POS ? '+' : '-'; const char new_combine = hitem->blend_params->mask_combine & DEVELOP_COMBINE_MASKS_POS ? '+' : '-'; if(new_combine != old_combine) change_parts[num_parts++] = CHG_STR(%c, _("drawn mask polarity"), old_combine, new_combine); const dt_mask_id_t old_grid = dt_develop_blend_mask_group_id(hitem->module, old_blend); const dt_mask_id_t new_grid = dt_develop_blend_mask_group_id(hitem->module, hitem->blend_params); if(new_grid != old_grid) change_parts[num_parts++] = old_grid == 0 ? g_strdup_printf(_("a drawn mask was added")) : new_grid == 0 ? g_strdup_printf(_("the drawn mask was removed")) : g_strdup_printf(_("the drawn mask was changed")); dt_iop_gui_blend_data_t *bd = hitem->module->blend_data; for(int in_out = 1; in_out >= 0; in_out--) { gboolean first = TRUE; for(const dt_iop_gui_blendif_channel_t *b = bd ? dt_develop_blendif_channels_for_csp(bd->csp) : NULL; b && b->label != NULL; b++) { const dt_develop_blendif_channels_t ch = b->param_channels[in_out]; const int oactive = old_blend->blendif & (1 << ch); const int nactive = hitem->blend_params->blendif & (1 << ch); const int opolarity = old_blend->blendif & (1 << (ch + 16)); const int npolarity = hitem->blend_params->blendif & (1 << (ch + 16)); float *of = &old_blend->blendif_parameters[4 * ch]; float *nf = &hitem->blend_params->blendif_parameters[4 * ch]; const float oboost = exp2f(old_blend->blendif_boost_factors[ch]); const float nboost = exp2f(hitem->blend_params->blendif_boost_factors[ch]); if((oactive || nactive) && (memcmp(of, nf, sizeof(float) * 4) || opolarity != npolarity)) { if(first) { change_parts[num_parts++] = g_strdup(in_out ? _("parametric output mask:") : _("parametric input mask:")); first = FALSE; } char s[4][2][25]; for(int k = 0; k < 4; k++) { b->scale_print(of[k], oboost, s[k][0], sizeof(s[k][0])); b->scale_print(nf[k], nboost, s[k][1], sizeof(s[k][1])); } char *opol = !oactive ? "" : (opolarity ? "(-)" : "(+)"); char *npol = !nactive ? "" : (npolarity ? "(-)" : "(+)"); change_parts[num_parts++] = CHG_STR(%s| %s- %s| %s%s, _(b->name), s[0][0], s[1][0], s[2][0], s[3][0], opol, s[0][1], s[1][1], s[2][1], s[3][1], npol); } } } } gchar *tooltip_text = g_strjoinv("\n", change_parts); g_strfreev(change_parts); gboolean show_tooltip = *tooltip_text; if(show_tooltip) { static GtkWidget *view = NULL; if(!view) { view = gtk_text_view_new(); dt_gui_add_class(view, "dt_transparent_background"); dt_gui_add_class(view, "dt_monospace"); g_signal_connect(G_OBJECT(view), "destroy", G_CALLBACK(gtk_widget_destroyed), &view); } // find tabs to align columns and decimals int t[5] = { 0 }; struct lconv *locale = localeconv(); for(char *line = tooltip_text; *line; ) { gchar *endline = g_strstr_len(line, -1, "\n"); if(!endline) endline = line + strlen(line); gchar *tab1 = g_strstr_len(line, endline - line, "\t"); if(tab1) { if(t[0] <= tab1 - line) t[0] = tab1 - line + 1; tab1 += 2; gchar *tab2 = g_strstr_len(tab1, endline - tab1, "\t"); if(tab2) { if(t[3] < tab2 - tab1) t[3] = tab2 - tab1; gchar *decimal1 = g_strstr_len(tab1, tab2 - tab1, locale->decimal_point); if(decimal1 && t[1] < decimal1 - tab1) t[1] = decimal1 - tab1; if(decimal1 && t[2] < tab2 - decimal1) t[2] = tab2 - decimal1; gchar *tab3 = g_strrstr_len(tab2, endline - tab2, "\t") + 1; gchar *decimal2 = g_strstr_len(tab3, endline - tab3, locale->decimal_point); if(decimal2 && t[4] < decimal2 - tab3) t[4] = decimal2 - tab3; } } line = endline; if(*line) line++; } if(t[3] < t[1] + t[2]) t[3] = t[1] + t[2]; PangoLayout *layout = gtk_widget_create_pango_layout(view, " "); int char_width; pango_layout_get_size(layout, &char_width, NULL); g_object_unref(layout); PangoTabArray *tabs = pango_tab_array_new_with_positions (5, FALSE, PANGO_TAB_LEFT , char_width * (t[0]), PANGO_TAB_DECIMAL, char_width * (t[0] + t[1] + 1), PANGO_TAB_LEFT , char_width * (t[0] + t[3] + 1), PANGO_TAB_LEFT , char_width * (t[0] + t[3] + 3), PANGO_TAB_DECIMAL, char_width * (t[0] + t[3] + 4 + t[4])); gtk_text_view_set_tabs(GTK_TEXT_VIEW(view), tabs); pango_tab_array_free(tabs); GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)); gtk_text_buffer_set_text(buffer, tooltip_text, -1); gtk_tooltip_set_custom(tooltip, view); gtk_widget_map(view); // FIXME: workaround added in order to fix // #9908, probably a Gtk issue, remove when // fixed upstream } g_free(tooltip_text); return show_tooltip; } static gchar *_lib_history_button_label(const dt_dev_history_item_t *item) { gchar *label = NULL; if(!item) label = g_strdup(""); else label = dt_history_get_name_label(item->module->name(), item->multi_name, TRUE, item->multi_name_hand_edited); return label; } static void _lib_history_change_callback(gpointer instance, dt_lib_module_t *self) { dt_lib_history_t *d = self->data; d->record_history_level--; d->record_undo = TRUE; dt_lib_gui_queue_update(self); } void gui_update(dt_lib_module_t *self) { dt_lib_history_t *d = self->data; /* lock history mutex */ dt_pthread_mutex_lock(&darktable.develop->history_mutex); /* first destroy all buttons in list */ dt_gui_container_destroy_children(GTK_CONTAINER(d->history_box)); /* add default which always should be */ GtkWidget *widget = _lib_history_create_button(self, -1, _("original"), FALSE, FALSE, TRUE, darktable.develop->history_end == 0, FALSE); gtk_box_pack_end(GTK_BOX(d->history_box), widget, FALSE, FALSE, 0); int num = 0; /* iterate over history items and add them to list*/ for(const GList *history = darktable.develop->history; history; history = g_list_next(history)) { const dt_dev_history_item_t *hitem = history->data; gchar *label = _lib_history_button_label(hitem); const gboolean selected = (num == darktable.develop->history_end - 1); widget = _lib_history_create_button (self, num, label, (hitem->enabled || (strcmp(hitem->op_name, "mask_manager") == 0)), hitem->module->default_enabled, hitem->module->hide_enable_button, selected, hitem->module->flags() & IOP_FLAGS_DEPRECATED); g_free(label); gtk_widget_set_has_tooltip(widget, TRUE); g_signal_connect(G_OBJECT(widget), "query-tooltip", G_CALLBACK(_changes_tooltip_callback), (void *)hitem); gtk_box_pack_end(GTK_BOX(d->history_box), widget, FALSE, FALSE, 0); num++; } /* show all widgets */ gtk_widget_show_all(d->history_box); dt_gui_widget_reallocate_now(d->history_box); if(num == darktable.develop->history_end) { // history has changed, scroll to the top GtkWidget *sw = gtk_widget_get_ancestor(d->history_box, GTK_TYPE_SCROLLED_WINDOW); GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)); gtk_adjustment_set_value(adj, 0); } dt_pthread_mutex_unlock(&darktable.develop->history_mutex); } static void _lib_history_truncate(const gboolean compress) { dt_dev_history_truncate(darktable.develop, compress); } static void _lib_history_compress_clicked_callback(GtkButton *widget, gpointer user_data) { _lib_history_truncate(TRUE); } static void _lib_history_compress_pressed_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self) { /* Claim the sequence, replacing the `return TRUE` of the button-press-event * handler this gesture was converted from. Without it GtkButton's own * gesture still completes the click and emits "clicked", so * _lib_history_compress_clicked_callback would run _lib_history_truncate() * a second time -- on release, while the pixelpipe job started by this call * is still running -- and ctrl+click would truncate and then compress. */ dt_gui_claim(gesture); const gboolean compress = !(dt_key_modifier_state() & GDK_CONTROL_MASK); _lib_history_truncate(compress); } static void _lib_history_button_clicked_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self) { GtkWidget *widget = dt_gui_get_widget(gesture); const dt_imgid_t imgid = darktable.develop->image_storage.id; if(!dt_is_valid_imgid(imgid)) return; static gboolean reset = FALSE; if(reset) return; // shift-click just show the corresponding module in modulegroups. // tested before the active-row guard below: revealing a module never changes // the selection, so it must work on the selected row too if(dt_key_modifier_state() & GDK_SHIFT_MASK) { /* Claim the sequence, replacing the `return TRUE` of the button-press-event * handler this gesture was converted from: the history entry must only be * revealed in modulegroups, not selected, so GtkToggleButton must not get * to complete the click and flip its state. */ dt_gui_claim(gesture); const int num = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "history-number")); dt_dev_history_item_t *hist = g_list_nth_data(darktable.develop->history, num - 1); if(hist) { dt_dev_modulegroups_switch(darktable.develop, hist->module); dt_iop_gui_set_expanded(hist->module, TRUE, TRUE); } return; } if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) { // Exactly one history row must stay selected. Claiming the press prevents // GtkToggleButton's default gesture from toggling the active row off. dt_gui_claim(gesture); return; } dt_lib_history_t *d = self->data; reset = TRUE; /* deactivate all toggle buttons */ GList *children = gtk_container_get_children(GTK_CONTAINER(d->history_box)); for(GList *l = children; l != NULL; l = g_list_next(l)) { GtkToggleButton *b = GTK_TOGGLE_BUTTON (dt_gui_container_nth_child(GTK_CONTAINER(l->data), HIST_WIDGET_MODULE)); if(b != GTK_TOGGLE_BUTTON(widget)) g_object_set(G_OBJECT(b), "active", FALSE, (gchar *)0); } g_list_free(children); reset = FALSE; if(dt_atomic_get_int(&darktable.gui->reset) != 0) return; dt_dev_undo_start_record(darktable.develop); /* revert to given history item. */ const int num = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "history-number")); dt_dev_pop_history_items(darktable.develop, num); // set the module list order dt_dev_reorder_gui_module_list(darktable.develop); dt_image_update_final_size(imgid); dt_dev_pixelpipe_cache_invalidate_later(darktable.develop->preview_pipe, 0, "history button: "); /* signal history changed */ dt_dev_undo_end_record(darktable.develop); dt_iop_connect_accels_all(); dt_dev_modulegroups_set(darktable.develop, dt_dev_modulegroups_get(darktable.develop)); } static void _lib_history_create_style_button_clicked_callback(GtkWidget *widget, gpointer user_data) { if(darktable.develop->image_storage.id) { dt_dev_write_history(darktable.develop); dt_gui_styles_dialog_new(darktable.develop->image_storage.id); } } void gui_reset(dt_lib_module_t *self) { const dt_imgid_t imgid = darktable.develop->image_storage.id; if(!dt_is_valid_imgid(imgid)) return; if(!dt_conf_get_bool("ask_before_discard") || dt_gui_show_yes_no_dialog (_("delete image's history?"), "", _("do you really want to clear history of current image?"))) { dt_dev_undo_start_record(darktable.develop); dt_history_delete_on_image_ext(imgid, FALSE, TRUE); dt_dev_undo_end_record(darktable.develop); dt_dev_modulegroups_set(darktable.develop, dt_dev_modulegroups_get(darktable.develop)); // the reset removes the focused module's mask; left open, the separate masks // panel would go on offering to edit it. A module still on screen keeps the // focus, so that an undo brings its panel back with the mask. One the group // just hid (active modules) loses it: the group's own focus drop is ignored // inside its gui update (modulegroups.c:1153, imageop.c:2854) dt_iop_module_t *focused = dt_dev_gui_module(); if(focused && !(focused->expander && gtk_widget_get_visible(focused->expander))) dt_iop_request_focus(NULL); else dt_iop_gui_blend_masks_panel_fold_unmasked(); dt_control_queue_redraw_center(); } } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on