Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
history_merge_gui.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
21#include "common/conf.h"
22
23#include "system/macros.h"
24#include "system/mem_alloc.h"
25#include "common/logging.h"
26#include "develop/iop_order.h"
28#include "develop/blend.h"
29#include "develop/dev_history.h"
30#include "develop/develop.h"
31#include "develop/imageop.h"
32#include "develop/masks.h"
33#include "gui/application.h"
34
35#include <glib.h>
36#include <string.h>
37#include "widgets/dialog.h"
38#include "widgets/label.h"
40
41typedef struct
42{
43 // Bitmask of `_hm_id_origin_t` describing where the id was seen.
44 guint flags;
45 // Non-owning pointer to the module instance from the pasted set (if any).
46 const dt_iop_module_t *mod_list;
47 // Non-owning pointer to the module instance in the source pipeline (if any).
48 const dt_iop_module_t *src_iop;
49 // Non-owning pointer to the module instance in the destination pipeline (if any).
50 dt_iop_module_t *dst_iop;
52
53static gchar *_hm_clean_module_name(const dt_iop_module_t *mod)
54{
55 const char *raw = (mod && mod->name) ? mod->name() : (mod ? mod->op : "");
56 gchar *clean = delete_underscore(raw ? raw : "");
58 return clean;
59}
60
61static gchar *_hm_module_label_short(const dt_iop_module_t *mod)
62{
63 gchar *name = _hm_clean_module_name(mod);
64 if(IS_NULL_PTR(name)) return g_strdup("");
65 if(mod && mod->multi_name[0] != '\0')
66 {
67 gchar *out = g_strdup_printf("%s (%s)", name, mod->multi_name);
69 return out;
70 }
71 return name;
72}
73
74static gchar *_hm_pretty_id(const char *id)
75{
76 /* Convert a raw node id ("op|multi_name") to a human-friendly string. */
77 if(IS_NULL_PTR(id)) return g_strdup("");
78
79 char op[sizeof(((dt_dev_history_item_t *)0)->op_name)] = { 0 };
80 char name[sizeof(((dt_dev_history_item_t *)0)->multi_name)] = { 0 };
81 _hm_id_to_op_name(id, op, name);
82 if(name[0] == '\0') return g_strdup(op);
83 return g_strdup_printf("%s (%s)", op, name);
84}
85
86static gchar *_hm_pretty_id_from_id_ht(const char *id, GHashTable *id_ht, const gboolean prefer_dest)
87{
88 /* Turn a node id into a label suitable for GTK dialogs. */
89 if(IS_NULL_PTR(id)) return g_strdup("");
90
91 const _hm_id_info_t *info = id_ht ? (const _hm_id_info_t *)g_hash_table_lookup(id_ht, id) : NULL;
92 const dt_iop_module_t *mod = NULL;
93
94 if(info)
95 {
96 if(prefer_dest && info->dst_iop)
97 mod = info->dst_iop;
98 else if(!prefer_dest && info->src_iop)
99 mod = info->src_iop;
100
101 if(IS_NULL_PTR(mod)) mod = info->dst_iop ? info->dst_iop : (info->src_iop ? info->src_iop : info->mod_list);
102 }
103
104 if(mod)
105 {
106 gchar *name = _hm_clean_module_name(mod);
107 if(mod->multi_name[0] == '\0') return name;
108 gchar *out = g_strdup_printf("%s (%s)", name ? name : "", mod->multi_name);
109 dt_free(name);
110 return out;
111 }
112
113 return _hm_pretty_id(id);
114}
115
116static gchar *_hm_cycle_node_label(const dt_digraph_node_t *n, GHashTable *id_ht)
117{
118 /* Wrapper around `_hm_pretty_id_from_id_ht()` for cycle nodes. */
119 return _hm_pretty_id_from_id_ht(n ? n->id : NULL, id_ht, TRUE);
120}
121
122static void _hm_append_cycle_label(GString *out, const char *s, const gboolean bold)
123{
124 gchar *esc = g_markup_escape_text(s ? s : "", -1);
125 if(bold) g_string_append_printf(out, "<b>%s</b>", esc);
126 else g_string_append(out, esc);
127 dt_free(esc);
128}
129
130dt_hm_constraint_choice_t _hm_ask_user_constraints_choice(GHashTable *id_ht, const char *faulty_id,
131 const char *src_prev, const char *src_next,
132 const char *dst_prev, const char *dst_next)
133{
134 /* Ask the user how to resolve incompatible adjacency constraints between source and destination. */
136 if(!g_main_context_is_owner(g_main_context_default())) return DT_HM_CONSTRAINTS_PREFER_DEST;
137
140
141 gchar *faulty = _hm_pretty_id_from_id_ht(faulty_id, id_ht, TRUE);
142 gchar *sp = _hm_pretty_id_from_id_ht(src_prev, id_ht, FALSE);
143 gchar *sn = _hm_pretty_id_from_id_ht(src_next, id_ht, FALSE);
144 gchar *dp = _hm_pretty_id_from_id_ht(dst_prev, id_ht, TRUE);
145 gchar *dn = _hm_pretty_id_from_id_ht(dst_next, id_ht, TRUE);
146
147 GtkDialog *dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
148 _("Incompatible module ordering constraints"), GTK_WINDOW(window),
149 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("Preserve _destination ordering"), GTK_RESPONSE_REJECT,
150 _("Preserve _source ordering"), GTK_RESPONSE_ACCEPT, _("_Cancel"), GTK_RESPONSE_CANCEL, NULL));
151
152 gtk_dialog_set_default_response(dialog, GTK_RESPONSE_REJECT);
153
154 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
155
156 GtkWidget *label = gtk_label_new(NULL);
157 gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
158 gtk_label_set_selectable(GTK_LABEL(label), TRUE);
159 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
160 gtk_label_set_max_width_chars(GTK_LABEL(label), 80);
161
162 gchar *text = g_strdup_printf(_("Two modules require each other as predecessor, creating a 2-cycle.\n\n"
163 "Faulty module: %s\n\n"
164 "Destination wants: %s → %s → %s\n"
165 "Source wants: %s → %s → %s\n\n"
166 "Which ordering constraints should be preserved?"),
167 faulty, dp, faulty, dn, sp, faulty, sn);
168
169 gtk_label_set_text(GTK_LABEL(label), text);
170 gtk_box_pack_start(GTK_BOX(content_area), label, TRUE, TRUE, 0);
171
172 gtk_widget_show_all(GTK_WIDGET(dialog));
173 const int res = gtk_dialog_run(dialog);
174 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
175 gtk_widget_destroy(GTK_WIDGET(dialog));
176 dt_gui_refocus_parent(dialog_parent);
177
178 dt_free(text);
179 dt_free(faulty);
180 dt_free(sp);
181 dt_free(sn);
182 dt_free(dp);
183 dt_free(dn);
184
185 if(res == GTK_RESPONSE_ACCEPT) return DT_HM_CONSTRAINTS_PREFER_SRC;
187}
188
189gboolean _hm_warn_missing_raster_producers(const GList *mod_list)
190{
191 /* Warn the user when pasted modules rely on raster masks that will be missing. */
192 if(IS_NULL_PTR(dt_gui_get_global())) return TRUE;
193 if(!g_main_context_is_owner(g_main_context_default())) return TRUE;
194
196 if(IS_NULL_PTR(window)) return TRUE;
197
198 GHashTable *mods = g_hash_table_new(g_direct_hash, g_direct_equal);
199 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
200 {
201 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
202 if(mod) g_hash_table_add(mods, (gpointer)mod);
203 }
204
205 GString *lines = g_string_new("");
206 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
207 {
208 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
209 if(IS_NULL_PTR(mod)) continue;
210 const dt_iop_module_t *producer = mod->raster_mask.sink.source;
211 if(IS_NULL_PTR(producer)) continue;
212
213 const gboolean missing = !producer || !g_hash_table_contains(mods, producer);
214 if(missing)
215 {
216 gchar *user = _hm_module_label_short(mod);
217 gchar *prod = _hm_module_label_short(producer);
218 g_string_append_printf(lines, "• %s → %s\n", user, prod);
219 dt_free(user);
220 dt_free(prod);
221 }
222 }
223
224 g_hash_table_destroy(mods);
225
226 if(lines->len == 0)
227 {
228 g_string_free(lines, TRUE);
229 return TRUE;
230 }
231
232 GtkDialog *dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
233 _("Missing raster mask producers"), GTK_WINDOW(window),
234 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("_Cancel merge"), GTK_RESPONSE_CANCEL, _("_Continue"),
235 GTK_RESPONSE_ACCEPT, NULL));
236 gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
237 gtk_dialog_set_default_response(dialog, GTK_RESPONSE_ACCEPT);
238
239 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
240
241 GtkWidget *label = gtk_label_new(NULL);
242 gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
243 gtk_widget_set_halign(label, GTK_ALIGN_START);
244 gtk_widget_set_valign(label, GTK_ALIGN_START);
245 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
246 gtk_label_set_max_width_chars(GTK_LABEL(label), 90);
247
248 gchar *text = g_strdup_printf(
249 _("Some pasted modules use raster masks produced by modules that were not included.\n"
250 "Those masks will not be available after the merge.\n\n"
251 "Missing producers:\n\n%s"),
252 lines->str);
253 gtk_label_set_text(GTK_LABEL(label), text);
254 gtk_box_pack_start(GTK_BOX(content_area), label, FALSE, FALSE, 6);
255
256 gtk_widget_show_all(GTK_WIDGET(dialog));
257 const int res = gtk_dialog_run(dialog);
258 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
259 gtk_widget_destroy(GTK_WIDGET(dialog));
260 dt_gui_refocus_parent(dialog_parent);
261
262 dt_free(text);
263 g_string_free(lines, TRUE);
264
265 return res == GTK_RESPONSE_ACCEPT;
266}
267
268void _hm_show_toposort_cycle_popup(GList *cycle_nodes, GHashTable *id_ht)
269{
270 /* Present a detected ordering cycle as a GTK modal popup. */
271 if(IS_NULL_PTR(cycle_nodes)) return;
272 if(IS_NULL_PTR(dt_gui_get_global())) return;
273 if(!g_main_context_is_owner(g_main_context_default())) return;
274
276 if(IS_NULL_PTR(window)) return;
277
278 GPtrArray *labels = g_ptr_array_new_with_free_func(g_free);
279 for(GList *it = g_list_first(cycle_nodes); it; it = g_list_next(it))
280 {
282 g_ptr_array_add(labels, _hm_cycle_node_label(n, id_ht));
283 }
284
285 GString *cycle = g_string_new("");
286 for(guint i = 0; labels && i < labels->len; i++)
287 {
288 const char *s = (const char *)g_ptr_array_index(labels, i);
289 if(i > 0) g_string_append(cycle, " → ");
290 _hm_append_cycle_label(cycle, s, i == 0);
291 }
292 if(labels && labels->len > 0)
293 {
294 const char *first = (const char *)g_ptr_array_index(labels, 0);
295 g_string_append(cycle, " → ");
296 _hm_append_cycle_label(cycle, first, TRUE);
297 }
298
299 GtkDialog *dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
300 _("Incompatible module ordering constraints"), GTK_WINDOW(window),
301 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("_Close"), GTK_RESPONSE_CLOSE, NULL));
302 gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
303
304 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
305
306 GtkWidget *label = gtk_label_new(NULL);
307 gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
308 gtk_widget_set_halign(label, GTK_ALIGN_START);
309 gtk_widget_set_valign(label, GTK_ALIGN_START);
310 gtk_label_set_selectable(GTK_LABEL(label), TRUE);
311 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
312 gtk_label_set_max_width_chars(GTK_LABEL(label), 80);
313
314 GString *text = g_string_new(NULL);
315 gchar *prefix = g_markup_escape_text(
316 _("Module ordering constraints contain a cycle and cannot be satisfied."
317 "Merging pipeline is aborted, we default to source image pipeline order.\n\n"
318 "The next step will allow you to review and edit modules ordering manually."
319 "\n\nCycle:\n\n"), -1);
320 g_string_append(text, prefix);
321 dt_free(prefix);
322 g_string_append(text, cycle->str);
323
324 gtk_label_set_markup(GTK_LABEL(label), text->str);
325 gtk_box_pack_start(GTK_BOX(content_area), label, FALSE, FALSE, 6);
326
327 gtk_widget_show_all(GTK_WIDGET(dialog));
328 gtk_dialog_run(dialog);
329 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
330 gtk_widget_destroy(GTK_WIDGET(dialog));
331 dt_gui_refocus_parent(dialog_parent);
332
333 if(text) g_string_free(text, TRUE);
334 if(cycle) g_string_free(cycle, TRUE);
335 if(labels) g_ptr_array_free(labels, TRUE);
336}
337
338static gchar *_hm_module_row_label(const dt_iop_module_t *mod)
339{
340 /* Format a module instance for the report rows: "<order> <name> (multi_name)". */
341 gchar *name = _hm_clean_module_name(mod);
342 if(mod->multi_name[0] == '\0')
343 {
344 gchar *out = g_strdup_printf("%4d %s", mod->iop_order, name ? name : "");
345 dt_free(name);
346 return out;
347 }
348 gchar *out = g_strdup_printf("%4d %s (%s)", mod->iop_order, name ? name : "", mod->multi_name);
349 dt_free(name);
350 return out;
351}
352
354{
355 if(IS_NULL_PTR(a) || IS_NULL_PTR(b)) return FALSE;
356
357 const gboolean a_has_forms = (!IS_NULL_PTR(a->forms));
358 const gboolean b_has_forms = (!IS_NULL_PTR(b->forms));
359 if(a_has_forms != b_has_forms) return FALSE;
360
361 const int a_mask_id = a->blend_params ? a->blend_params->mask_id : 0;
362 const int b_mask_id = b->blend_params ? b->blend_params->mask_id : 0;
363 if(a_mask_id != b_mask_id) return FALSE;
364
365 if(a_has_forms && a_mask_id > 0)
366 {
367 dt_masks_form_t *a_form = dt_masks_get_from_id_ext(a->forms, a_mask_id);
368 dt_masks_form_t *b_form = dt_masks_get_from_id_ext(b->forms, b_mask_id);
369 // Each snapshot is hashed against its OWN forms list: resolving children from the live
370 // dev->forms (what the old dt_masks_group_get_hash() wrapper did) compared two hybrid
371 // states and could declare different snapshots equal.
372 const uint64_t a_hash = dt_masks_group_get_hash_ext(0, a->forms, a_form);
373 const uint64_t b_hash = dt_masks_group_get_hash_ext(0, b->forms, b_form);
374 if(a_hash != b_hash) return FALSE;
375 }
376
377 return TRUE;
378}
379
381{
382 if(IS_NULL_PTR(a) || IS_NULL_PTR(b)) return FALSE;
383
384 if(strcmp(a->op_name, b->op_name) != 0) return FALSE;
385 if(strcmp(a->multi_name, b->multi_name) != 0) return FALSE;
386 if(a->multi_priority != b->multi_priority) return FALSE;
387 if(a->enabled != b->enabled) return FALSE;
388 if(a->iop_order != b->iop_order) return FALSE;
389
390 const int size_a = a->module ? a->module->params_size : 0;
391 const int size_b = b->module ? b->module->params_size : 0;
392 if(size_a != size_b) return FALSE;
393 if(size_a > 0)
394 {
395 if(IS_NULL_PTR(a->params) || IS_NULL_PTR(b->params)) return FALSE;
396 if(memcmp(a->params, b->params, size_a) != 0) return FALSE;
397 }
398
399 if((IS_NULL_PTR(a->blend_params)) != (IS_NULL_PTR(b->blend_params))) return FALSE;
400 if(a->blend_params && b->blend_params
401 && memcmp(a->blend_params, b->blend_params, sizeof(dt_develop_blend_params_t)) != 0)
402 return FALSE;
403
404 if(!_hm_history_masks_match(a, b)) return FALSE;
405
406 return TRUE;
407}
408
431
432static const char *const HM_REPORT_DISABLED_FG = "#282828";
433static const char *const HM_REPORT_EXISTING_DST_FG = "#bbb";
434
435typedef struct
436{
437 dt_develop_t *dev_dest; // destination develop context to reorder
438 dt_develop_t *dev_src; // source develop context for moved detection
439 GtkListStore *store; // report model to read/update after DnD
440 GHashTable *dst_last_by_id; // last history items by id (mask markers)
441 GHashTable *dst_last_before_by_id; // last history items before merge
442 GHashTable *override; // override markers
443 const GHashTable *orig_ids; // original module ids (inserted markers)
444 const GHashTable *mod_list_ids; // pasted module ids (show disabled)
445 GtkTreePath *drag_path; // path being dragged
446 gboolean in_reorder; // guard against recursive row-reordered signals
448
449typedef struct
450{
451 GtkWidget *legend; // legend table placed before the hint in the footer
452 GtkWidget *hint; // drag-and-drop explanation to align with Destination
453 GtkTreeViewColumn *orig_column;
454 GtkTreeViewColumn *filet_column;
455 GtkTreeViewColumn *src_column;
456 GtkTreeViewColumn *arrow_column;
458
459typedef struct
460{
462 gchar *label;
463 int style;
465
466static gint _hm_label_cmp(gconstpointer a, gconstpointer b)
467{
468 const _hm_label_t *la = (const _hm_label_t *)a;
469 const _hm_label_t *lb = (const _hm_label_t *)b;
470 return (la->iop_order > lb->iop_order) - (la->iop_order < lb->iop_order);
471}
472
473GPtrArray *_hm_collect_labels_from_history_map(GHashTable *last_by_id, const GHashTable *mod_list_ids,
474 GPtrArray **out_styles)
475{
476 GList *labels = NULL;
477 GHashTableIter it;
478 gpointer key = NULL, value = NULL;
479 g_hash_table_iter_init(&it, last_by_id);
480 while(g_hash_table_iter_next(&it, &key, &value))
481 {
483 if(IS_NULL_PTR(hist) || IS_NULL_PTR(hist->module)) continue;
484 if(hist->module->flags() & IOP_FLAGS_NO_HISTORY_STACK) continue;
485 if(!hist->enabled && (IS_NULL_PTR(mod_list_ids) || !g_hash_table_contains((GHashTable *)mod_list_ids, key))) continue;
486
487 gchar *label = _hm_module_row_label(hist->module);
488 if(dt_iop_module_needs_mask_history(hist->module))
489 {
490 gchar *tmp = g_strdup_printf("%s *", label);
491 dt_free(label);
492 label = tmp;
493 }
494
495 _hm_label_t *item = g_malloc0(sizeof(_hm_label_t));
496 item->iop_order = hist->iop_order;
497 item->label = label;
498 item->style = hist->enabled ? PANGO_STYLE_NORMAL : PANGO_STYLE_ITALIC;
499 labels = g_list_insert_sorted(labels, item, (GCompareFunc)_hm_label_cmp);
500 }
501
502 GPtrArray *result = g_ptr_array_new_with_free_func(g_free);
503 GPtrArray *styles = g_ptr_array_new();
504 for(GList *l = g_list_last(labels); l; l = g_list_previous(l))
505 {
506 _hm_label_t *item = (_hm_label_t *)l->data;
507 g_ptr_array_add(result, item->label);
508 g_ptr_array_add(styles, GINT_TO_POINTER(item->style));
509 dt_free(item);
510 }
511 g_list_free(labels);
512 labels = NULL;
513
514 if(out_styles)
515 *out_styles = styles;
516 else
517 g_ptr_array_free(styles, TRUE);
518
519 return result;
520}
521
523{
524 /* Resolve a node id ("op|multi_name") to a module instance in `dev`. */
525 char op[sizeof(((dt_dev_history_item_t *)0)->op_name)];
526 char name[sizeof(((dt_dev_history_item_t *)0)->multi_name)];
527 _hm_id_to_op_name(id, op, name);
528
530 if(IS_NULL_PTR(mod) && name[0] == '\0') mod = dt_iop_get_module_by_op_priority(dev->iop, op, 0);
531 if(IS_NULL_PTR(mod) && name[0] == '\0') mod = dt_iop_get_module_by_op_priority(dev->iop, op, -1);
532 return mod;
533}
534
535static gboolean _hm_module_visible_in_report(const dt_iop_module_t *mod, const GHashTable *mod_list_ids)
536{
537 /* Check whether a module appears in the report list and can be reordered. */
538 if(IS_NULL_PTR(mod)) return FALSE;
539 if(mod->flags() & IOP_FLAGS_NO_HISTORY_STACK) return FALSE;
540 if(mod->enabled) return TRUE;
541 if(IS_NULL_PTR(mod_list_ids)) return FALSE;
542 gchar *id = _hm_make_node_id(mod->op, mod->multi_name);
543 const gboolean keep = g_hash_table_contains((GHashTable *)mod_list_ids, id);
544 dt_free(id);
545 return keep;
546}
547
548static gchar *_hm_report_dest_label(const dt_iop_module_t *mod, GHashTable *dst_last_by_id, const GHashTable *orig_ids)
549{
550 /* Build destination column label with mask/inserted markers and numeric alignment. */
551 gchar *dst_txt = _hm_module_row_label(mod);
552
553 gchar *id = _hm_make_node_id(mod->op, mod->multi_name);
554 const dt_dev_history_item_t *hist_dst
555 = dst_last_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_by_id, id) : NULL;
556 if(!IS_NULL_PTR(hist_dst) && dt_iop_module_needs_mask_history(hist_dst->module))
557 {
558 gchar *tmp = g_strdup_printf("%s *", dst_txt);
559 dt_free(dst_txt);
560 dst_txt = tmp;
561 }
562
563 const gboolean inserted = orig_ids && !g_hash_table_contains((GHashTable *)orig_ids, id);
564 if(inserted)
565 {
566 gchar *tmp = g_strdup_printf("[%s ]", dst_txt);
567 dt_free(dst_txt);
568 dst_txt = tmp;
569 }
570 else if(dst_txt[0] != '\0')
571 {
572 gchar *tmp = g_strdup_printf(" %s", dst_txt);
573 dt_free(dst_txt);
574 dst_txt = tmp;
575 }
576
577 dt_free(id);
578 return dst_txt;
579}
580
581static GPtrArray *_hm_collect_enabled_modules_gui_order(const dt_develop_t *dev, const GHashTable *mod_list_ids)
582{
583 /* Collect report modules in GUI order (reverse pipeline order). */
584 GPtrArray *mods = g_ptr_array_new();
585 for(GList *modules = g_list_last(dev->iop); modules; modules = g_list_previous(modules))
586 {
587 dt_iop_module_t *mod = (dt_iop_module_t *)modules->data;
588 if(IS_NULL_PTR(mod)) continue;
589 if(!_hm_module_visible_in_report(mod, mod_list_ids)) continue;
590 g_ptr_array_add(mods, mod);
591 }
592 return mods;
593}
594
596{
597 /* Update history item ordering to match current module iop_order values. */
598 for(GList *l = g_list_first(dev->history); l; l = g_list_next(l))
599 {
601 if(!hist || !hist->module) continue;
602 hist->iop_order = hist->module->iop_order;
603 }
604}
605
606static GPtrArray *_hm_report_collect_dest_ids(GtkTreeModel *model)
607{
608 /* Collect destination module ids from the report rows, in GUI order (top to bottom). */
609 GPtrArray *ids = g_ptr_array_new_with_free_func(g_free);
610
611 GtkTreeIter iter;
612 gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
613 while(valid)
614 {
615 gboolean is_input = FALSE;
616 gchar *id = NULL;
617 gtk_tree_model_get(model, &iter, HM_REPORT_COL_DST_ID, &id, HM_REPORT_COL_IS_INPUT, &is_input, -1);
618
619 if(!is_input && id && id[0] != '\0')
620 g_ptr_array_add(ids, id);
621 else
622 dt_free(id);
623
624 valid = gtk_tree_model_iter_next(model, &iter);
625 }
626
627 return ids;
628}
629
630static GPtrArray *_hm_report_build_desired_visible_order(dt_develop_t *dev_dest, GtkTreeModel *model)
631{
632 /* Convert GUI-order rows to pipeline-order module pointers for the destination. */
633 GPtrArray *gui_ids = _hm_report_collect_dest_ids(model);
634 GPtrArray *mods = g_ptr_array_new();
635 int missing = 0;
636
637 for(gint i = (gint)gui_ids->len - 1; i >= 0; i--)
638 {
639 const char *id = (const char *)g_ptr_array_index(gui_ids, i);
640 dt_iop_module_t *mod = _hm_module_from_id(dev_dest, id);
641 if(mod)
642 g_ptr_array_add(mods, mod);
643 else
644 missing++;
645 }
646
647 g_ptr_array_free(gui_ids, TRUE);
648
649 if(missing > 0)
650 {
651 dt_print(DT_DEBUG_HISTORY, "[dt_history_merge] report reorder: %d destination modules not found\n", missing);
652 g_ptr_array_free(mods, TRUE);
653 return NULL;
654 }
655
656 return mods;
657}
658
659static GList *_hm_report_build_ordered_modules(dt_develop_t *dev_dest, const GPtrArray *visible_order,
660 const GHashTable *mod_list_ids)
661{
662 /* Build a full ordered module list by reordering only visible modules. */
663 if(IS_NULL_PTR(dev_dest) || !visible_order) return NULL;
664
665 int visible_count = 0;
666 for(const GList *l = g_list_first(dev_dest->iop); l; l = g_list_next(l))
667 {
668 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
669 if(mod && _hm_module_visible_in_report(mod, mod_list_ids)) visible_count++;
670 }
671
672 if(visible_count != (int)visible_order->len)
673 {
675 "[dt_history_merge] report reorder: visible modules mismatch (pipe=%d, gui=%d)\n",
676 visible_count, visible_order->len);
677 }
678
679 GList *ordered = NULL;
680 int visible_idx = 0;
681 const int visible_len = (int)visible_order->len;
682
683 for(const GList *l = g_list_first(dev_dest->iop); l; l = g_list_next(l))
684 {
685 dt_iop_module_t *mod = (dt_iop_module_t *)l->data;
686 if(mod && _hm_module_visible_in_report(mod, mod_list_ids) && visible_idx < visible_len)
687 mod = (dt_iop_module_t *)g_ptr_array_index((GPtrArray *)visible_order, visible_idx++);
688
689 if(mod) ordered = g_list_append(ordered, mod);
690 }
691
692 while(visible_idx < visible_len)
693 {
694 dt_iop_module_t *mod = (dt_iop_module_t *)g_ptr_array_index((GPtrArray *)visible_order, visible_idx++);
695 if(mod) ordered = g_list_append(ordered, mod);
696 }
697
698 return ordered;
699}
700
701static gboolean _hm_report_apply_visible_order(dt_develop_t *dev_dest, const GPtrArray *visible_order,
702 const GHashTable *mod_list_ids)
703{
704 /* Rebuild iop_order_list by reordering only visible modules, keeping others fixed. */
705 GList *ordered = _hm_report_build_ordered_modules(dev_dest, visible_order, mod_list_ids);
706 if(IS_NULL_PTR(ordered)) return FALSE;
707
709 g_list_free(ordered);
710 ordered = NULL;
711 return TRUE;
712}
713
714static GHashTable *_hm_report_build_moved_set(dt_develop_t *dev_src, GtkTreeModel *model,
715 const GHashTable *mod_list_ids)
716{
717 /* Build a set of module ids that changed relative order between source and destination. */
718 GHashTable *moved = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
719 if(IS_NULL_PTR(dev_src)) return moved;
720
721 GPtrArray *dest_ids = _hm_report_collect_dest_ids(model);
722 if(!dest_ids || dest_ids->len == 0)
723 {
724 if(dest_ids) g_ptr_array_free(dest_ids, TRUE);
725 return moved;
726 }
727
728 GHashTable *dest_id_set = g_hash_table_new(g_str_hash, g_str_equal);
729 for(guint i = 0; i < dest_ids->len; i++)
730 g_hash_table_add(dest_id_set, g_ptr_array_index(dest_ids, i));
731
732 GPtrArray *src_common = g_ptr_array_new_with_free_func(g_free);
733 for(const GList *l = g_list_first(dev_src->iop); l; l = g_list_next(l))
734 {
735 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
736 if(IS_NULL_PTR(mod) || !_hm_module_visible_in_report(mod, mod_list_ids)) continue;
737 gchar *id = _hm_make_node_id(mod->op, mod->multi_name);
738 if(g_hash_table_contains(dest_id_set, id))
739 g_ptr_array_add(src_common, id);
740 else
741 dt_free(id);
742 }
743
744 GHashTable *src_id_set = g_hash_table_new(g_str_hash, g_str_equal);
745 for(guint i = 0; i < src_common->len; i++)
746 g_hash_table_add(src_id_set, g_ptr_array_index(src_common, i));
747
748 GPtrArray *dst_common = g_ptr_array_new();
749 for(gint i = (gint)dest_ids->len - 1; i >= 0; i--)
750 {
751 char *id = (char *)g_ptr_array_index(dest_ids, i);
752 if(g_hash_table_contains(src_id_set, id))
753 g_ptr_array_add(dst_common, id);
754 }
755
756 const gboolean same_len = (src_common->len == dst_common->len);
757 gboolean same_order = same_len;
758 if(same_order)
759 {
760 for(guint i = 0; i < src_common->len; i++)
761 {
762 const char *a = (const char *)g_ptr_array_index(src_common, i);
763 const char *b = (const char *)g_ptr_array_index(dst_common, i);
764 if(strcmp(a, b))
765 {
766 same_order = FALSE;
767 break;
768 }
769 }
770 }
771
772 if(!same_order)
773 {
774 GHashTable *src_pos = g_hash_table_new(g_str_hash, g_str_equal);
775 GHashTable *dst_pos = g_hash_table_new(g_str_hash, g_str_equal);
776 for(guint i = 0; i < src_common->len; i++)
777 g_hash_table_insert(src_pos, g_ptr_array_index(src_common, i), GINT_TO_POINTER((int)i));
778 for(guint i = 0; i < dst_common->len; i++)
779 g_hash_table_insert(dst_pos, g_ptr_array_index(dst_common, i), GINT_TO_POINTER((int)i));
780
781 for(guint i = 0; i < src_common->len; i++)
782 {
783 const char *id = (const char *)g_ptr_array_index(src_common, i);
784 const gpointer sp = g_hash_table_lookup(src_pos, id);
785 const gpointer dp = g_hash_table_lookup(dst_pos, id);
786 if(sp && dp && GPOINTER_TO_INT(sp) != GPOINTER_TO_INT(dp))
787 g_hash_table_replace(moved, g_strdup(id), GINT_TO_POINTER(1));
788 }
789
790 g_hash_table_destroy(src_pos);
791 g_hash_table_destroy(dst_pos);
792 }
793
794 g_hash_table_destroy(src_id_set);
795 g_hash_table_destroy(dest_id_set);
796 g_ptr_array_free(src_common, TRUE);
797 g_ptr_array_free(dst_common, TRUE);
798 g_ptr_array_free(dest_ids, TRUE);
799
800 return moved;
801}
802
803static void _hm_report_update_move_styles(GtkListStore *store, dt_develop_t *dev_src,
804 const GHashTable *mod_list_ids)
805{
806 /* Update bold styles for modules moved between source and destination order. */
807 GHashTable *moved = _hm_report_build_moved_set(dev_src, GTK_TREE_MODEL(store), mod_list_ids);
808
809 GtkTreeIter iter;
810 gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
811 while(valid)
812 {
813 gboolean is_input = FALSE;
814 gchar *src_id = NULL;
815 gchar *dst_id = NULL;
816 gboolean dst_disabled = FALSE;
817 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, HM_REPORT_COL_SRC_ID, &src_id, HM_REPORT_COL_DST_ID, &dst_id,
818 HM_REPORT_COL_DST_DISABLED, &dst_disabled, HM_REPORT_COL_IS_INPUT, &is_input, -1);
819
820 const gboolean src_moved = (!is_input && src_id && g_hash_table_contains(moved, src_id));
821 const gboolean dst_moved = (!is_input && dst_id && g_hash_table_contains(moved, dst_id));
822
823 gtk_list_store_set(store, &iter, HM_REPORT_COL_SRC_WEIGHT,
824 src_moved ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, HM_REPORT_COL_DST_WEIGHT,
825 dst_moved ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
826 if(dst_moved && !dst_disabled)
827 gtk_list_store_set(store, &iter, HM_REPORT_COL_DST_STYLE, PANGO_STYLE_NORMAL,
829
830 dt_free(src_id);
831 dt_free(dst_id);
832 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
833 }
834
835 g_hash_table_destroy(moved);
836}
837
838static void _hm_report_update_arrows(GtkListStore *store, GHashTable *override, GHashTable *dst_last_by_id,
839 GHashTable *dst_last_before_by_id)
840{
841 /* Refresh override arrows after destination order changes. */
842 GHashTable *dst_row = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
843
844 GtkTreeIter iter;
845 gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
846 int row = 0;
847 while(valid)
848 {
849 gboolean is_input = FALSE;
850 gchar *src_id = NULL;
851 gchar *dst_id = NULL;
852 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, HM_REPORT_COL_SRC_ID, &src_id, HM_REPORT_COL_DST_ID, &dst_id,
853 HM_REPORT_COL_IS_INPUT, &is_input, -1);
854
855 if(!is_input)
856 {
857 dt_free(src_id);
858
859 if(dst_id && dst_id[0] != '\0')
860 g_hash_table_replace(dst_row, dst_id, GINT_TO_POINTER(row));
861 else
862 dt_free(dst_id);
863 }
864 else
865 {
866 dt_free(src_id);
867 dt_free(dst_id);
868 }
869
870 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
871 row++;
872 }
873
874 valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
875 row = 0;
876 while(valid)
877 {
878 gboolean is_input = FALSE;
879 gchar *src_id = NULL;
880 gchar *dst_id = NULL;
881 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, HM_REPORT_COL_SRC_ID, &src_id, HM_REPORT_COL_DST_ID, &dst_id,
882 HM_REPORT_COL_IS_INPUT, &is_input, -1);
883
884 const char *arrow = "";
885 if(!is_input && src_id && g_hash_table_contains(override, src_id))
886 {
887 const dt_dev_history_item_t *hist_after
888 = dst_last_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_by_id, src_id) : NULL;
889 const dt_dev_history_item_t *hist_before
890 = dst_last_before_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_before_by_id, src_id)
891 : NULL;
892
893 gboolean mask_override = FALSE;
894 if(hist_before)
895 mask_override = !_hm_history_masks_match(hist_after, hist_before);
896 else
897 mask_override = !IS_NULL_PTR(hist_after) && dt_iop_module_needs_mask_history(hist_after->module);
898
899 const gpointer dst_row_ptr = g_hash_table_lookup(dst_row, src_id);
900 if(dst_row_ptr)
901 {
902 const int dst_r = GPOINTER_TO_INT(dst_row_ptr);
903 const int delta = dst_r - row;
904 if(delta == 0)
905 arrow = mask_override ? "→*" : "→";
906 else if(delta == 1)
907 arrow = mask_override ? "↘*" : "↘";
908 else if(delta == -1)
909 arrow = mask_override ? "↗*" : "↗";
910 else if(delta > 1)
911 arrow = mask_override ? "↴*" : "↴";
912 else
913 arrow = mask_override ? "↴*" : "↴";
914 }
915 }
916
917 gtk_list_store_set(store, &iter, HM_REPORT_COL_ARROW, arrow, -1);
918
919 dt_free(src_id);
920 dt_free(dst_id);
921 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
922 row++;
923 }
924
925 g_hash_table_destroy(dst_row);
926}
927
929{
930 /* Ensure the "Input image" row stays anchored at the bottom after DnD. */
931 GtkTreeIter iter;
932 gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
933 GtkTreeIter input_iter;
934 int input_index = -1;
935 int last_index = -1;
936 int idx = 0;
937
938 while(valid)
939 {
940 gboolean is_input = FALSE;
941 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, HM_REPORT_COL_IS_INPUT, &is_input, -1);
942 if(is_input)
943 {
944 input_iter = iter;
945 input_index = idx;
946 }
947 last_index = idx;
948 idx++;
949 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
950 }
951
952 if(input_index >= 0 && input_index != last_index)
953 gtk_list_store_move_after(store, &input_iter, NULL);
954}
955
956static void _hm_report_update_dest_labels(GtkListStore *store, dt_develop_t *dev_dest, GHashTable *dst_last_by_id,
957 const GHashTable *orig_ids, const GHashTable *override)
958{
959 /* Refresh destination column labels after iop_order changes. */
960 GtkTreeIter iter;
961 gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
962 while(valid)
963 {
964 gboolean is_input = FALSE;
965 gchar *id = NULL;
966 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, HM_REPORT_COL_DST_ID, &id, HM_REPORT_COL_IS_INPUT, &is_input, -1);
967
968 if(!is_input && id && id[0] != '\0')
969 {
970 dt_iop_module_t *mod = _hm_module_from_id(dev_dest, id);
971 gchar *dst_txt = mod ? _hm_report_dest_label(mod, dst_last_by_id, orig_ids) : g_strdup("");
972 const gboolean dst_disabled = !IS_NULL_PTR(mod) && !mod->enabled;
973 const gboolean dst_inserted = !IS_NULL_PTR(mod) && !IS_NULL_PTR(orig_ids)
974 && !g_hash_table_contains((GHashTable *)orig_ids, id);
975 const gboolean dst_existing = !IS_NULL_PTR(mod) && !IS_NULL_PTR(orig_ids) && !dst_inserted;
976 const dt_dev_history_item_t *hist_dst
977 = dst_last_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_by_id, id) : NULL;
978 const gboolean dst_masked = !IS_NULL_PTR(hist_dst) && dt_iop_module_needs_mask_history(hist_dst->module);
979 const gboolean dst_overridden = !IS_NULL_PTR(override) && g_hash_table_contains((GHashTable *)override, id);
980 const gboolean dst_plain_existing = dst_existing && !dst_disabled && !dst_masked && !dst_overridden;
981 const gboolean dst_dimmed_existing = dst_existing && !dst_overridden;
982 const gchar *dst_fg = dst_disabled ? HM_REPORT_DISABLED_FG
983 : (dst_dimmed_existing ? HM_REPORT_EXISTING_DST_FG : NULL);
984 const gboolean dst_fg_set = dst_disabled || dst_dimmed_existing;
985 gtk_list_store_set(store, &iter, HM_REPORT_COL_DST, dst_txt,
986 HM_REPORT_COL_DST_STYLE, dst_plain_existing ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL,
987 HM_REPORT_COL_DST_FG, dst_fg, HM_REPORT_COL_DST_FG_SET, dst_fg_set, -1);
988 dt_free(dst_txt);
989 }
990
991 dt_free(id);
992 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
993 }
994}
995
997{
998 /* Apply destination column order to the pipeline and refresh labels/styles. */
999 if(ctx->in_reorder) return;
1000 ctx->in_reorder = TRUE;
1001
1003
1004 GPtrArray *desired = _hm_report_build_desired_visible_order(ctx->dev_dest, GTK_TREE_MODEL(ctx->store));
1005 if(desired && _hm_report_apply_visible_order(ctx->dev_dest, desired, ctx->mod_list_ids))
1006 {
1011 }
1012 if(desired) g_ptr_array_free(desired, TRUE);
1013
1014 ctx->in_reorder = FALSE;
1015}
1016
1017static void _hm_report_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
1018{
1020 if(ctx->drag_path)
1021 {
1022 gtk_tree_path_free(ctx->drag_path);
1023 ctx->drag_path = NULL;
1024 }
1025
1026 GtkTreePath *path = NULL;
1027 GtkTreeViewColumn *column = NULL;
1028 gtk_tree_view_get_cursor(GTK_TREE_VIEW(widget), &path, &column);
1029 if(path) ctx->drag_path = path;
1030}
1031
1032static void _hm_report_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data,
1033 guint info, guint time, gpointer user_data)
1034{
1036 GtkTreePath *path = ctx->drag_path;
1037
1038 if(IS_NULL_PTR(path))
1039 gtk_tree_view_get_cursor(GTK_TREE_VIEW(widget), &path, NULL);
1040
1041 if(IS_NULL_PTR(path)) return;
1042
1043 GtkTreeIter iter;
1044 if(!gtk_tree_model_get_iter(GTK_TREE_MODEL(ctx->store), &iter, path))
1045 {
1046 if(path != ctx->drag_path) gtk_tree_path_free(path);
1047 return;
1048 }
1049
1050 gboolean is_input = FALSE;
1051 gchar *dst_id = NULL;
1052 gtk_tree_model_get(GTK_TREE_MODEL(ctx->store), &iter, HM_REPORT_COL_DST_ID, &dst_id, HM_REPORT_COL_IS_INPUT,
1053 &is_input, -1);
1054 if(is_input || !dst_id || dst_id[0] == '\0')
1055 {
1056 dt_free(dst_id);
1057 if(path != ctx->drag_path) gtk_tree_path_free(path);
1058 return;
1059 }
1060 dt_free(dst_id);
1061
1062 gchar *path_str = gtk_tree_path_to_string(path);
1063 gtk_selection_data_set(selection_data, gdk_atom_intern_static_string("DT_HISTORY_MERGE_DST_ROW"), 8,
1064 (const guchar *)path_str, strlen(path_str));
1065 dt_free(path_str);
1066
1067 if(path != ctx->drag_path) gtk_tree_path_free(path);
1068}
1069
1070static void _hm_report_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
1071 GtkSelectionData *selection_data, guint info, guint time,
1072 gpointer user_data)
1073{
1075
1076 if(IS_NULL_PTR(selection_data)) return;
1077 const guchar *data = gtk_selection_data_get_data(selection_data);
1078 if(IS_NULL_PTR(data)) return;
1079 gchar *src_path_str = g_strdup((const gchar *)data);
1080 if(IS_NULL_PTR(src_path_str)) return;
1081
1082 GtkTreePath *src_path = gtk_tree_path_new_from_string(src_path_str);
1083 dt_free(src_path_str);
1084 if(IS_NULL_PTR(src_path)) return;
1085
1086 GtkTreePath *dst_path = NULL;
1087 GtkTreeViewDropPosition pos;
1088 if(!gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(widget), x, y, &dst_path, &pos))
1089 {
1090 gtk_tree_path_free(src_path);
1091 return;
1092 }
1093
1094 if(gtk_tree_path_compare(src_path, dst_path) == 0)
1095 {
1096 gtk_tree_path_free(src_path);
1097 gtk_tree_path_free(dst_path);
1098 return;
1099 }
1100
1101 GtkTreeIter src_iter;
1102 GtkTreeIter dst_iter;
1103 if(!gtk_tree_model_get_iter(GTK_TREE_MODEL(ctx->store), &src_iter, src_path)
1104 || !gtk_tree_model_get_iter(GTK_TREE_MODEL(ctx->store), &dst_iter, dst_path))
1105 {
1106 gtk_tree_path_free(src_path);
1107 gtk_tree_path_free(dst_path);
1108 return;
1109 }
1110
1111 gboolean src_input = FALSE;
1112 gboolean dst_input = FALSE;
1113 gchar *src_dst_id = NULL;
1114 gtk_tree_model_get(GTK_TREE_MODEL(ctx->store), &src_iter, HM_REPORT_COL_IS_INPUT, &src_input, HM_REPORT_COL_DST_ID,
1115 &src_dst_id, -1);
1116 gtk_tree_model_get(GTK_TREE_MODEL(ctx->store), &dst_iter, HM_REPORT_COL_IS_INPUT, &dst_input, -1);
1117
1118 if(src_input || !src_dst_id || src_dst_id[0] == '\0')
1119 {
1120 dt_free(src_dst_id);
1121 gtk_tree_path_free(src_path);
1122 gtk_tree_path_free(dst_path);
1123 return;
1124 }
1125
1126 GPtrArray *dest_rows = g_ptr_array_new();
1127 GPtrArray *dest_ids = g_ptr_array_new();
1128 GtkTreeIter iter;
1129 gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ctx->store), &iter);
1130 int row_index = 0;
1131 while(valid)
1132 {
1133 gboolean is_input = FALSE;
1134 gchar *id = NULL;
1135 gtk_tree_model_get(GTK_TREE_MODEL(ctx->store), &iter, HM_REPORT_COL_DST_ID, &id, HM_REPORT_COL_IS_INPUT,
1136 &is_input, -1);
1137 if(!is_input && id && id[0] != '\0')
1138 {
1139 g_ptr_array_add(dest_rows, GINT_TO_POINTER(row_index));
1140 g_ptr_array_add(dest_ids, id);
1141 }
1142 else
1143 {
1144 dt_free(id);
1145 }
1146 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(ctx->store), &iter);
1147 row_index++;
1148 }
1149
1150 int src_row_index = gtk_tree_path_get_indices(src_path)[0];
1151 int dst_row_index = gtk_tree_path_get_indices(dst_path)[0];
1152
1153 int src_pos = -1;
1154 for(guint i = 0; i < dest_rows->len; i++)
1155 {
1156 if(GPOINTER_TO_INT(g_ptr_array_index(dest_rows, i)) == src_row_index)
1157 {
1158 src_pos = (int)i;
1159 break;
1160 }
1161 }
1162
1163 if(src_pos < 0)
1164 {
1165 dt_free(src_dst_id);
1166 for(guint i = 0; i < dest_ids->len; i++) dt_free(g_ptr_array_index(dest_ids, i));
1167 g_ptr_array_free(dest_ids, TRUE);
1168 g_ptr_array_free(dest_rows, TRUE);
1169 gtk_tree_path_free(src_path);
1170 gtk_tree_path_free(dst_path);
1171 return;
1172 }
1173
1174 int target_pos = 0;
1175 if(dst_input)
1176 {
1177 target_pos = (int)dest_ids->len;
1178 }
1179 else
1180 {
1181 for(guint i = 0; i < dest_rows->len; i++)
1182 {
1183 const int row = GPOINTER_TO_INT(g_ptr_array_index(dest_rows, i));
1184 if(row < dst_row_index) target_pos++;
1185 }
1186 if(pos == GTK_TREE_VIEW_DROP_AFTER || pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER)
1187 target_pos++;
1188 }
1189
1190 if(target_pos > (int)dest_ids->len) target_pos = (int)dest_ids->len;
1191
1192 if(target_pos == src_pos || target_pos == src_pos + 1)
1193 {
1194 dt_free(src_dst_id);
1195 for(guint i = 0; i < dest_ids->len; i++) dt_free(g_ptr_array_index(dest_ids, i));
1196 g_ptr_array_free(dest_ids, TRUE);
1197 g_ptr_array_free(dest_rows, TRUE);
1198 gtk_tree_path_free(src_path);
1199 gtk_tree_path_free(dst_path);
1200 return;
1201 }
1202
1203 gchar *moved_id = (gchar *)g_ptr_array_index(dest_ids, src_pos);
1204 g_ptr_array_remove_index(dest_ids, src_pos);
1205 if(target_pos > src_pos) target_pos--;
1206 g_ptr_array_insert(dest_ids, target_pos, moved_id);
1207
1208 for(guint i = 0; i < dest_rows->len && i < dest_ids->len; i++)
1209 {
1210 const int row = GPOINTER_TO_INT(g_ptr_array_index(dest_rows, i));
1211 GtkTreeIter row_iter;
1212 if(gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(ctx->store), &row_iter, NULL, row))
1213 gtk_list_store_set(ctx->store, &row_iter, HM_REPORT_COL_DST_ID,
1214 (const char *)g_ptr_array_index(dest_ids, i), -1);
1215 }
1216
1218
1219 for(guint i = 0; i < dest_ids->len; i++) dt_free(g_ptr_array_index(dest_ids, i));
1220 g_ptr_array_free(dest_ids, TRUE);
1221 g_ptr_array_free(dest_rows, TRUE);
1222 dt_free(src_dst_id);
1223
1224 gtk_tree_path_free(src_path);
1225 gtk_tree_path_free(dst_path);
1226}
1227
1228static GHashTable *_hm_build_override_map(const dt_develop_t *dev_dest, GHashTable *src_last_by_id,
1229 GHashTable *dst_last_before_by_id)
1230{
1231 /* Build a set of module ids whose final history item matches the source but not the destination.
1232 *
1233 * We only report overrides when source and destination history items differ.
1234 */
1235 GHashTable *override = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
1236 const int history_end = dt_dev_get_history_end_ext((dt_develop_t *)dev_dest);
1237
1238 for(GList *modules = g_list_first(dev_dest->iop); modules; modules = g_list_next(modules))
1239 {
1240 dt_iop_module_t *mod = (dt_iop_module_t *)modules->data;
1241 dt_dev_history_item_t *hist_after
1242 = dt_dev_history_get_last_item_by_module(dev_dest->history, mod, history_end);
1243
1244 gchar *id = _hm_make_node_id(mod->op, mod->multi_name);
1245 const dt_dev_history_item_t *hist_src
1246 = src_last_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(src_last_by_id, id) : NULL;
1247 const dt_dev_history_item_t *hist_dst
1248 = dst_last_before_by_id ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_before_by_id, id) : NULL;
1249
1250 const gboolean match_src = hist_src && _hm_history_items_match(hist_after, hist_src);
1251 const gboolean match_dst = hist_dst && _hm_history_items_match(hist_after, hist_dst);
1252 if(match_src && !match_dst)
1253 g_hash_table_replace(override, id, GINT_TO_POINTER(1));
1254 else
1255 dt_free(id);
1256 }
1257
1258 return override;
1259}
1260
1266static void _hm_report_align_drag_hint(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
1267{
1269 if(!gtk_widget_get_visible(widget) || allocation->width <= 0) return;
1270
1271 GtkAllocation legend_allocation = { 0 };
1272 gtk_widget_get_allocation(align->legend, &legend_allocation);
1273
1274 const int dst_column_start = gtk_tree_view_column_get_width(align->orig_column)
1275 + gtk_tree_view_column_get_width(align->filet_column)
1276 + gtk_tree_view_column_get_width(align->src_column)
1277 + gtk_tree_view_column_get_width(align->arrow_column);
1278 gtk_widget_set_margin_start(align->hint, MAX(dst_column_start - legend_allocation.width, 0));
1279}
1280
1282 const gboolean merge_iop_order, const gboolean used_source_order,
1283 const dt_history_merge_strategy_t strategy, GHashTable *src_last_by_id,
1284 GHashTable *dst_last_before_by_id, const GHashTable *orig_ids,
1285 const GHashTable *mod_list_ids, const char *source_label,
1286 dt_hm_batch_state_t *batch)
1287{
1288 /* Present a merge report with source/destination pipelines and override markers. */
1289 if(IS_NULL_PTR(dt_gui_get_global())) return FALSE;
1290 if(!g_main_context_is_owner(g_main_context_default())) return FALSE;
1291
1293 if(IS_NULL_PTR(window)) return FALSE;
1294
1295 /* The "before" rows. Derived here, from the pre-merge module map the backend kept, rather
1296 * than handed over ready-made: they are display strings, built with dt_capitalize_label()
1297 * and friends, and the backend has no business holding those. */
1298 GPtrArray *orig_styles = NULL;
1299 GPtrArray *orig_labels
1300 = _hm_collect_labels_from_history_map((GHashTable *)orig_ids, mod_list_ids, &orig_styles);
1301 if(IS_NULL_PTR(orig_labels) || IS_NULL_PTR(orig_styles))
1302 {
1303 if(orig_labels) g_ptr_array_free(orig_labels, TRUE);
1304 if(orig_styles) g_ptr_array_free(orig_styles, TRUE);
1305 return FALSE;
1306 }
1307
1308 const char *merge_mode = merge_iop_order ? _("merge") : _("destination");
1309 const char *strategy_name
1310 = (strategy == DT_HISTORY_MERGE_APPEND) ? _("append")
1311 : (strategy == DT_HISTORY_MERGE_PREPEND) ? _("prepend")
1312 : _("replace");
1313
1314 gchar *title_text
1315 = g_strdup_printf(_("Copy, merging pipeline in %s and history in <b>%s</b> mode."), merge_mode, strategy_name);
1316
1317 GtkDialog *dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
1318 _("History merge report"), GTK_WINDOW(window),
1319 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("_Revert"), GTK_RESPONSE_ACCEPT, _("_Accept"),
1320 GTK_RESPONSE_CLOSE, NULL));
1321
1322 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1323
1324 GtkWidget *label = gtk_label_new(NULL);
1325 gtk_label_set_markup(GTK_LABEL(label), title_text);
1326 gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
1327 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
1328 gtk_label_set_max_width_chars(GTK_LABEL(label), 100);
1329 gtk_box_pack_start(GTK_BOX(content_area), label, FALSE, FALSE, 6);
1330
1331 const char *order_text = used_source_order ? _("Source pipeline order was used")
1332 : _("Destination pipeline order was used");
1333 const char *fallback_text = (used_source_order != merge_iop_order)
1334 ? _(" as a fallback because we could not resolve positionning constraints with source order.")
1335 : ".";
1336 gchar *order_label_text = g_strdup_printf("%s%s", order_text, fallback_text);
1337 GtkWidget *order_label = gtk_label_new(order_label_text);
1338 gtk_label_set_xalign(GTK_LABEL(order_label), 0.0f);
1339 gtk_label_set_line_wrap(GTK_LABEL(order_label), TRUE);
1340 gtk_label_set_max_width_chars(GTK_LABEL(order_label), 100);
1341 gtk_box_pack_start(GTK_BOX(content_area), order_label, FALSE, FALSE, 6);
1342 dt_free(order_label_text);
1343
1344 GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL);
1345 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1346 gtk_widget_set_size_request(scrolled, 740, 420);
1347 dt_gui_add_class(scrolled, "dt_recessed_scroll");
1348 gtk_box_pack_start(GTK_BOX(content_area), scrolled, TRUE, TRUE, 0);
1349
1350 GtkListStore *store = gtk_list_store_new(HM_REPORT_COL_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
1351 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
1352 G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING,
1353 G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN,
1354 G_TYPE_BOOLEAN);
1355 GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1356 g_object_unref(store);
1357 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree), TRUE);
1358 gtk_tree_selection_set_mode(gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)), GTK_SELECTION_NONE);
1359
1360 gchar *src_base = dev_src ? g_path_get_basename(dev_src->image_storage.filename) : g_strdup("");
1361 gchar *dst_base = g_path_get_basename(dev_dest->image_storage.filename);
1362
1363 gchar *orig_title = g_strdup_printf(_("Original: %d %s"), dev_dest->image_storage.id, dst_base);
1364 gchar *src_title = !IS_NULL_PTR(source_label) && source_label[0] != '\0'
1365 ? g_strdup_printf(_("Source: %s"), source_label)
1366 : (dev_src ? g_strdup_printf(_("Source: %d %s"), dev_src->image_storage.id, src_base)
1367 : g_strdup(_("Source")));
1368 gchar *dst_title = g_strdup_printf(_("Destination: %d %s"), dev_dest->image_storage.id, dst_base);
1369
1370 GtkCellRenderer *r_orig = gtk_cell_renderer_text_new();
1371 g_object_set(r_orig, "fixed-height-from-font", 1, "ypad", 0, NULL);
1372 GtkTreeViewColumn *c_orig = gtk_tree_view_column_new_with_attributes(orig_title, r_orig, "text",
1373 HM_REPORT_COL_ORIG, "foreground",
1374 HM_REPORT_COL_ORIG_FG, "foreground-set",
1376 gtk_tree_view_column_set_expand(c_orig, TRUE);
1377 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c_orig);
1378
1379 GtkCellRenderer *r_filet = gtk_cell_renderer_text_new();
1380 g_object_set(r_filet, "xalign", 0.5f, "fixed-height-from-font", 1, "ypad", 0, NULL);
1381 GtkTreeViewColumn *c_filet = gtk_tree_view_column_new_with_attributes("", r_filet, "text",
1382 HM_REPORT_COL_FILET, NULL);
1383 gtk_tree_view_column_set_alignment(c_filet, 0.5f);
1384 gtk_tree_view_column_set_sizing(c_filet, GTK_TREE_VIEW_COLUMN_FIXED);
1385 gtk_tree_view_column_set_fixed_width(c_filet, 16);
1386 gtk_tree_view_column_set_expand(c_filet, FALSE);
1387 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c_filet);
1388
1389 GtkCellRenderer *r_src = gtk_cell_renderer_text_new();
1390 g_object_set(r_src, "fixed-height-from-font", 1, "ypad", 0, NULL);
1391 GtkTreeViewColumn *c_src = gtk_tree_view_column_new_with_attributes(src_title, r_src, "text",
1392 HM_REPORT_COL_SRC, "weight",
1393 HM_REPORT_COL_SRC_WEIGHT, "foreground",
1394 HM_REPORT_COL_SRC_FG, "foreground-set",
1396 gtk_tree_view_column_set_expand(c_src, TRUE);
1397 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c_src);
1398
1399 GtkCellRenderer *r_arrow = gtk_cell_renderer_text_new();
1400 g_object_set(r_arrow, "xalign", 0.5f, "fixed-height-from-font", 1, "ypad", 0, NULL);
1401 GtkTreeViewColumn *c_arrow = gtk_tree_view_column_new_with_attributes(_("Override"), r_arrow, "markup",
1402 HM_REPORT_COL_ARROW, NULL);
1403 gtk_tree_view_column_set_alignment(c_arrow, 0.5f);
1404 gtk_tree_view_column_set_expand(c_arrow, FALSE);
1405 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c_arrow);
1406
1407 GtkCellRenderer *r_dst = gtk_cell_renderer_text_new();
1408 g_object_set(r_dst, "fixed-height-from-font", 1, "ypad", 0, NULL);
1409 GtkTreeViewColumn *c_dst = gtk_tree_view_column_new_with_attributes(dst_title, r_dst, "text",
1410 HM_REPORT_COL_DST, "weight",
1411 HM_REPORT_COL_DST_WEIGHT, "style",
1412 HM_REPORT_COL_DST_STYLE, "foreground",
1413 HM_REPORT_COL_DST_FG, "foreground-set",
1415 gtk_tree_view_column_set_expand(c_dst, TRUE);
1416 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c_dst);
1417
1418 gtk_container_add(GTK_CONTAINER(scrolled), tree);
1419
1420 GtkWidget *legend_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1421 gtk_widget_set_halign(legend_content, GTK_ALIGN_START);
1422
1423 GtkWidget *legend = gtk_grid_new();
1424 gtk_grid_set_column_spacing(GTK_GRID(legend), DT_PIXEL_APPLY_DPI(10));
1425 gtk_widget_set_halign(legend, GTK_ALIGN_START);
1426
1427 gchar *txt_color = g_strdup_printf("<span foreground='%s'> %s </span> ", HM_REPORT_DISABLED_FG, _("Module"));
1428
1429 const struct
1430 {
1431 const gchar *symbol;
1432 const gchar *definition;
1433 gboolean symbol_markup;
1434 } legend_rows[] = {
1435 { _("[ Module ]"), _("inserted module"), FALSE },
1436 { " Module *", _("module uses masks"), FALSE },
1437 { _("<b> Module </b>"), _("moved module"), TRUE },
1438 { txt_color, _("disabled module (shown only if copied)"), TRUE },
1439 { "→", _("parameters overridden on the same row"), FALSE },
1440 { "↗ / ↘", _("parameters overridden on an adjacent row"), FALSE },
1441 { "↴", _("parameters overridden on a farther row"), FALSE },
1442 { "→*", _("masks overridden"), FALSE },
1443 };
1444
1445 /* Fill one two-column table so every symbol stays aligned with its explanation. */
1446 for(int row = 0; row < (int)G_N_ELEMENTS(legend_rows); row++)
1447 {
1448 GtkWidget *legend_symbol = gtk_label_new(NULL);
1449 gtk_label_set_xalign(GTK_LABEL(legend_symbol), 0.5f);
1450 gtk_widget_set_size_request(legend_symbol, DT_PIXEL_APPLY_DPI(72), -1);
1451 if(legend_rows[row].symbol_markup)
1452 gtk_label_set_markup(GTK_LABEL(legend_symbol), legend_rows[row].symbol);
1453 else
1454 gtk_label_set_text(GTK_LABEL(legend_symbol), legend_rows[row].symbol);
1455
1456 GtkWidget *legend_definition = gtk_label_new(legend_rows[row].definition);
1457 gtk_label_set_xalign(GTK_LABEL(legend_definition), 0.0f);
1458 gtk_widget_set_size_request(legend_definition, DT_PIXEL_APPLY_DPI(360), -1);
1459
1460 gtk_grid_attach(GTK_GRID(legend), legend_symbol, 0, row, 1, 1);
1461 gtk_grid_attach(GTK_GRID(legend), legend_definition, 1, row, 1, 1);
1462 }
1463 dt_free(txt_color);
1464
1465 GtkWidget *drag_label = gtk_label_new(_("Drag and drop modules in the “Destination” column to reorder the pipeline."));
1466 gtk_label_set_xalign(GTK_LABEL(drag_label), 0.0f);
1467 gtk_label_set_yalign(GTK_LABEL(drag_label), 0.0f);
1468 gtk_widget_set_valign(drag_label, GTK_ALIGN_START);
1469 gtk_label_set_line_wrap(GTK_LABEL(drag_label), TRUE);
1470 gtk_label_set_max_width_chars(GTK_LABEL(drag_label), 36);
1471
1472 gtk_box_pack_start(GTK_BOX(legend_content), legend, FALSE, FALSE, 0);
1473 gtk_box_pack_start(GTK_BOX(legend_content), drag_label, FALSE, FALSE, 0);
1474 gtk_box_pack_start(GTK_BOX(content_area), legend_content, FALSE, FALSE, 6);
1475
1476 const int orig_len = orig_labels ? orig_labels->len : 0;
1477 GPtrArray *src_mods = dev_src ? _hm_collect_enabled_modules_gui_order(dev_src, mod_list_ids) : g_ptr_array_new();
1478 GPtrArray *dst_mods = _hm_collect_enabled_modules_gui_order(dev_dest, mod_list_ids);
1479 GHashTable *dst_last_by_id = NULL;
1480 if(_hm_build_last_history_by_id(dev_dest, &dst_last_by_id)) return FALSE;
1481
1482 dt_hm_drag_hint_align_t drag_hint_align = { 0 };
1483 drag_hint_align.legend = legend;
1484 drag_hint_align.hint = drag_label;
1485 drag_hint_align.orig_column = c_orig;
1486 drag_hint_align.filet_column = c_filet;
1487 drag_hint_align.src_column = c_src;
1488 drag_hint_align.arrow_column = c_arrow;
1489 g_signal_connect(G_OBJECT(tree), "size-allocate", G_CALLBACK(_hm_report_align_drag_hint), &drag_hint_align);
1490 g_signal_connect(G_OBJECT(legend), "size-allocate", G_CALLBACK(_hm_report_align_drag_hint), &drag_hint_align);
1491
1492 const int src_len = src_mods->len;
1493 const int dst_len = dst_mods->len;
1494 const int rows = MAX(orig_len, MAX(src_len, dst_len));
1495 const int orig_offset = rows - orig_len;
1496 const int src_offset = rows - src_len;
1497 const int dst_offset = rows - dst_len;
1498
1499 GHashTable *override = _hm_build_override_map(dev_dest, src_last_by_id, dst_last_before_by_id);
1500 _hm_report_reorder_ctx_t *reorder_ctx = g_new0(_hm_report_reorder_ctx_t, 1);
1501 reorder_ctx->dev_dest = dev_dest;
1502 reorder_ctx->dev_src = dev_src;
1503 reorder_ctx->store = store;
1504 reorder_ctx->dst_last_by_id = dst_last_by_id;
1505 reorder_ctx->dst_last_before_by_id = dst_last_before_by_id;
1506 reorder_ctx->override = override;
1507 reorder_ctx->orig_ids = orig_ids;
1508 reorder_ctx->mod_list_ids = mod_list_ids;
1509
1510 for(int r = 0; r < rows; r++)
1511 {
1512 const int orig_idx = r - orig_offset;
1513 const int src_idx = r - src_offset;
1514 const int dst_idx = r - dst_offset;
1515
1516 const char *orig_txt = (orig_idx >= 0 && !IS_NULL_PTR(orig_labels))
1517 ? (const char *)g_ptr_array_index((GPtrArray *)orig_labels, orig_idx)
1518 : "";
1519 const gboolean orig_disabled = (orig_idx >= 0 && !IS_NULL_PTR(orig_styles))
1520 && (GPOINTER_TO_INT(g_ptr_array_index((GPtrArray *)orig_styles, orig_idx))
1521 == PANGO_STYLE_ITALIC);
1522 const dt_iop_module_t *src_mod = (src_idx >= 0) ? (const dt_iop_module_t *)g_ptr_array_index(src_mods, src_idx) : NULL;
1523 const dt_iop_module_t *dst_mod = (dst_idx >= 0) ? (const dt_iop_module_t *)g_ptr_array_index(dst_mods, dst_idx) : NULL;
1524
1525 gchar *src_txt = !IS_NULL_PTR(src_mod) ? _hm_module_row_label(src_mod) : g_strdup("");
1526 gchar *dst_txt = !IS_NULL_PTR(dst_mod) ? _hm_report_dest_label(dst_mod, dst_last_by_id, orig_ids) : g_strdup("");
1527 gchar *src_id = !IS_NULL_PTR(src_mod) ? _hm_make_node_id(src_mod->op, src_mod->multi_name) : NULL;
1528 const gboolean src_disabled = !IS_NULL_PTR(src_mod) && !src_mod->enabled;
1529 const gboolean dst_disabled = !IS_NULL_PTR(dst_mod) && !dst_mod->enabled;
1530
1531 if(!IS_NULL_PTR(src_mod) && !IS_NULL_PTR(src_last_by_id))
1532 {
1533 const dt_dev_history_item_t *hist_src = (const dt_dev_history_item_t *)g_hash_table_lookup(src_last_by_id, src_id);
1534 if(!IS_NULL_PTR(hist_src) && dt_iop_module_needs_mask_history(hist_src->module))
1535 {
1536 gchar *tmp = g_strdup_printf("%s *", src_txt);
1537 dt_free(src_txt);
1538 src_txt = tmp;
1539 }
1540 }
1541
1542 const char *arrow = "";
1543
1544 GtkTreeIter iter;
1545 gtk_list_store_append(store, &iter);
1546 gchar *dst_id = !IS_NULL_PTR(dst_mod) ? _hm_make_node_id(dst_mod->op, dst_mod->multi_name) : NULL;
1547 const gboolean dst_inserted = !IS_NULL_PTR(dst_id) && !IS_NULL_PTR(orig_ids)
1548 && !g_hash_table_contains((GHashTable *)orig_ids, dst_id);
1549 const gboolean dst_existing = !IS_NULL_PTR(dst_id) && !IS_NULL_PTR(orig_ids) && !dst_inserted;
1550 const dt_dev_history_item_t *hist_dst
1551 = !IS_NULL_PTR(dst_id) && !IS_NULL_PTR(dst_last_by_id)
1552 ? (const dt_dev_history_item_t *)g_hash_table_lookup(dst_last_by_id, dst_id)
1553 : NULL;
1554 const gboolean dst_masked = !IS_NULL_PTR(hist_dst) && dt_iop_module_needs_mask_history(hist_dst->module);
1555 const gboolean dst_overridden = !IS_NULL_PTR(dst_id) && !IS_NULL_PTR(override)
1556 && g_hash_table_contains((GHashTable *)override, dst_id);
1557 const gboolean dst_plain_existing = dst_existing && !dst_disabled && !dst_masked && !dst_overridden;
1558 const gboolean dst_dimmed_existing = dst_existing && !dst_overridden;
1559 const gchar *dst_fg = dst_disabled ? HM_REPORT_DISABLED_FG : (dst_dimmed_existing ? HM_REPORT_EXISTING_DST_FG : NULL);
1560 const gboolean dst_fg_set = dst_disabled || dst_dimmed_existing;
1561 gtk_list_store_set(store, &iter, HM_REPORT_COL_ORIG, orig_txt, HM_REPORT_COL_FILET, "│", HM_REPORT_COL_SRC,
1563 src_id, HM_REPORT_COL_DST_ID, dst_id, HM_REPORT_COL_SRC_WEIGHT, PANGO_WEIGHT_NORMAL,
1564 HM_REPORT_COL_DST_WEIGHT, PANGO_WEIGHT_NORMAL, HM_REPORT_COL_DST_DISABLED, dst_disabled,
1565 HM_REPORT_COL_DST_STYLE, dst_plain_existing ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL,
1566 HM_REPORT_COL_ORIG_FG, orig_disabled ? HM_REPORT_DISABLED_FG : NULL,
1567 HM_REPORT_COL_SRC_FG, src_disabled ? HM_REPORT_DISABLED_FG : NULL, HM_REPORT_COL_DST_FG, dst_fg,
1568 HM_REPORT_COL_ORIG_FG_SET, orig_disabled,
1569 HM_REPORT_COL_SRC_FG_SET, src_disabled, HM_REPORT_COL_DST_FG_SET, dst_fg_set,
1571 dt_free(dst_id);
1572 dt_free(src_id);
1573
1574 dt_free(src_txt);
1575 dt_free(dst_txt);
1576 }
1577
1578 {
1579 gchar *input_label = g_strdup_printf("%4s %s", "0", _("Input image"));
1580 GtkTreeIter iter;
1581 gtk_list_store_append(store, &iter);
1582 gtk_list_store_set(store, &iter, HM_REPORT_COL_ORIG, input_label, HM_REPORT_COL_FILET, "│", HM_REPORT_COL_SRC,
1583 input_label, HM_REPORT_COL_ARROW, "", HM_REPORT_COL_DST, input_label, HM_REPORT_COL_SRC_ID,
1584 NULL, HM_REPORT_COL_DST_ID, NULL, HM_REPORT_COL_SRC_WEIGHT, PANGO_WEIGHT_NORMAL,
1586 HM_REPORT_COL_DST_STYLE, PANGO_STYLE_ITALIC,
1591 dt_free(input_label);
1592 }
1593
1594 _hm_report_update_move_styles(store, dev_src, mod_list_ids);
1595 _hm_report_update_arrows(store, override, dst_last_by_id, dst_last_before_by_id);
1596
1597 GtkTargetEntry targets[] = { { "DT_HISTORY_MERGE_DST_ROW", GTK_TARGET_SAME_WIDGET, 0 } };
1598 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(tree), GDK_BUTTON1_MASK, targets, 1, GDK_ACTION_MOVE);
1599 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(tree), targets, 1, GDK_ACTION_MOVE);
1600
1601 gulong drag_begin_handler =
1602 g_signal_connect(G_OBJECT(tree), "drag-begin", G_CALLBACK(_hm_report_drag_begin), reorder_ctx);
1603 gulong drag_get_handler =
1604 g_signal_connect(G_OBJECT(tree), "drag-data-get", G_CALLBACK(_hm_report_drag_data_get), reorder_ctx);
1605 gulong drag_recv_handler =
1606 g_signal_connect(G_OBJECT(tree), "drag-data-received", G_CALLBACK(_hm_report_drag_data_received), reorder_ctx);
1607
1608 GtkWidget *batch_check = NULL;
1609 if(batch != NULL)
1610 {
1611 batch_check = gtk_check_button_new_with_label(_("Don't ask again for this batch"));
1612 gtk_widget_set_tooltip_text(batch_check,
1613 _("When checked, the chosen action (Accept or Revert) is applied silently\n"
1614 "to all remaining images in this batch without showing this report again."));
1615 gtk_box_pack_start(GTK_BOX(content_area), batch_check, FALSE, FALSE, 6);
1616 }
1617
1618 gtk_widget_show_all(GTK_WIDGET(dialog));
1619 const int res = gtk_dialog_run(dialog);
1620
1621 if(batch && batch_check && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(batch_check)))
1622 {
1623 if(res == GTK_RESPONSE_ACCEPT) // Revert button
1625 else if(res == GTK_RESPONSE_CLOSE) // Accept button
1627 // GTK_RESPONSE_DELETE_EVENT: leave UNDECIDED — closing the window is ambiguous
1628 }
1629
1630 g_signal_handler_disconnect(tree, drag_begin_handler);
1631 g_signal_handler_disconnect(tree, drag_get_handler);
1632 g_signal_handler_disconnect(tree, drag_recv_handler);
1633 if(reorder_ctx->drag_path) gtk_tree_path_free(reorder_ctx->drag_path);
1634 dt_free(reorder_ctx);
1635
1636 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1637 gtk_widget_destroy(GTK_WIDGET(dialog));
1638 dt_gui_refocus_parent(dialog_parent);
1639
1640 g_hash_table_destroy(override);
1641 g_ptr_array_free(src_mods, TRUE);
1642 g_ptr_array_free(dst_mods, TRUE);
1643 if(dst_last_by_id) g_hash_table_destroy(dst_last_by_id);
1644
1645 dt_free(src_base);
1646 dt_free(dst_base);
1647 dt_free(orig_title);
1648 dt_free(src_title);
1649 dt_free(dst_title);
1650 dt_free(title_text);
1651
1652 // Built at the top of this function, so freed here rather than by the caller.
1653 g_ptr_array_free(orig_labels, TRUE);
1654 g_ptr_array_free(orig_styles, TRUE);
1655
1656 return (res == GTK_RESPONSE_ACCEPT || res == GTK_RESPONSE_DELETE_EVENT);
1657}
1658
1659gboolean dt_gui_merge_options_dialog(const char *title, const char *mode_key,
1660 const char *iop_order_key, const char *ask_key,
1661 const gboolean iop_order_available)
1662{
1663 if(IS_NULL_PTR(dt_gui_get_global())) return TRUE;
1664 if(!g_main_context_is_owner(g_main_context_default())) return TRUE;
1665
1667 if(IS_NULL_PTR(window)) return TRUE;
1668
1669 const dt_history_merge_strategy_t cur_mode = dt_conf_get_int(mode_key);
1670 const gboolean cur_iop_order = dt_conf_get_bool(iop_order_key);
1671
1672 GtkDialog *dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
1673 title, GTK_WINDOW(window),
1674 GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
1675 _("_Cancel"), GTK_RESPONSE_CANCEL,
1676 _("_Apply"), GTK_RESPONSE_OK,
1677 NULL));
1678 gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK);
1679 gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
1680
1681 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1682 gtk_container_set_border_width(GTK_CONTAINER(content), 12);
1683 gtk_box_set_spacing(GTK_BOX(content), 6);
1684
1685 // --- History merge mode ---
1686 GtkWidget *mode_label = gtk_label_new(_("Where should source edits be placed relative to destination ones?"));
1687 gtk_label_set_xalign(GTK_LABEL(mode_label), 0.0f);
1688 gtk_label_set_line_wrap(GTK_LABEL(mode_label), TRUE);
1689 gtk_box_pack_start(GTK_BOX(content), mode_label, FALSE, FALSE, 4);
1690
1691 GtkWidget *radio_prepend = gtk_radio_button_new_with_label(NULL,
1692 _("Below (prepend) — incoming (source) is the base ; on conflicts, the original wins."));
1693 gtk_widget_set_tooltip_text(radio_prepend,
1694 _("Incoming edits are applied BEFORE yours in the processing stack.\n"
1695 "When the same module exists in both, your version wins."));
1696 gtk_box_pack_start(GTK_BOX(content), radio_prepend, FALSE, FALSE, 0);
1697
1698 GtkWidget *radio_append = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(radio_prepend),
1699 _("Above (append) — the original is the base ; on conflicts, incoming (source) wins."));
1700 gtk_widget_set_tooltip_text(radio_append,
1701 _("Incoming edits are applied AFTER yours in the processing stack.\n"
1702 "When the same module exists in both, the incoming version wins."));
1703 gtk_box_pack_start(GTK_BOX(content), radio_append, FALSE, FALSE, 0);
1704
1705 GtkWidget *radio_replace = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(radio_prepend),
1706 _("Replace — discard original edits entirely."));
1707 gtk_widget_set_tooltip_text(radio_replace,
1708 _("Your current edits are discarded and replaced entirely by the incoming history."));
1709 gtk_box_pack_start(GTK_BOX(content), radio_replace, FALSE, FALSE, 0);
1710
1711 switch(cur_mode)
1712 {
1713 case DT_HISTORY_MERGE_APPEND: gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_append), TRUE); break;
1714 case DT_HISTORY_MERGE_REPLACE: gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_replace), TRUE); break;
1715 default: gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_prepend), TRUE); break;
1716 }
1717
1718 gtk_box_pack_start(GTK_BOX(content), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 6);
1719
1720 // --- Pipeline (node) order ---
1721 GtkWidget *iop_check = gtk_check_button_new_with_label(_("Use incoming (source) pipeline order"));
1722 if(iop_order_available)
1723 {
1724 gtk_widget_set_tooltip_text(iop_check,
1725 _("When checked, the module processing order from the incoming source replaces yours.\n"
1726 "When unchecked, your current pipeline order is preserved."));
1727 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(iop_check), cur_iop_order);
1728 }
1729 else
1730 {
1731 gtk_widget_set_tooltip_text(iop_check,
1732 _("This source has no saved pipeline order — your current pipeline order will be kept."));
1733 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(iop_check), FALSE);
1734 gtk_widget_set_sensitive(iop_check, FALSE);
1735 }
1736 gtk_box_pack_start(GTK_BOX(content), iop_check, FALSE, FALSE, 0);
1737
1738 gtk_box_pack_start(GTK_BOX(content), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 6);
1739
1740 // --- Remember choice ---
1741 GtkWidget *ask_check = gtk_check_button_new_with_label(_("Ask every time"));
1742 gtk_widget_set_tooltip_text(ask_check,
1743 _("When unchecked, the current settings are used silently without showing this dialog.\n"
1744 "You can still change the defaults from the menu."));
1745 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ask_check), TRUE);
1746 gtk_box_pack_start(GTK_BOX(content), ask_check, FALSE, FALSE, 0);
1747
1748 gtk_widget_show_all(GTK_WIDGET(dialog));
1749 const int res = gtk_dialog_run(dialog);
1750
1751 if(res == GTK_RESPONSE_OK)
1752 {
1754 if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(radio_append)))
1755 new_mode = DT_HISTORY_MERGE_APPEND;
1756 else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(radio_replace)))
1757 new_mode = DT_HISTORY_MERGE_REPLACE;
1758
1759 dt_conf_set_int(mode_key, new_mode);
1760 if(iop_order_available)
1761 dt_conf_set_bool(iop_order_key, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(iop_check)));
1762 dt_conf_set_bool(ask_key, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ask_check)));
1763 }
1764
1765 GtkWindow *dialog_parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
1766 gtk_widget_destroy(GTK_WIDGET(dialog));
1767 dt_gui_refocus_parent(dialog_parent);
1768 return res == GTK_RESPONSE_OK;
1769}
1770
1771
GtkWidget * dt_gui_main_window(void)
int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
Definition ashift.c:4949
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
static const float x
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
const float delta
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
struct dt_gui_gtk_t * dt_gui_get_global(void)
Definition darktable.c:523
dt_dev_history_item_t * dt_dev_history_get_last_item_by_module(GList *history_list, dt_iop_module_t *module, int history_end)
Find the last history item referencing a module up to history_end.
int32_t dt_dev_get_history_end_ext(struct dt_develop_t *dev)
Get the current history end index (GUI perspective).
Definition develop.c:1899
void dt_gui_refocus_parent(GtkWindow *parent)
Definition dialog.c:64
GtkWidget * window
GtkTreeStore * store
its model, owned by the view
const int res
Definition dtpthread.h:351
void dt_hm_set_merge_report_handler(dt_hm_merge_report_handler_t handler)
void dt_hm_set_constraints_choice_handler(dt_hm_constraints_choice_handler_t handler)
void _hm_id_to_op_name(const char *id, char *op, char *name)
int _hm_build_last_history_by_id(const dt_develop_t *dev, GHashTable **out_map)
char * _hm_make_node_id(const char *op, const char *multi_name)
void dt_hm_set_missing_raster_handler(dt_hm_missing_raster_handler_t handler)
void dt_hm_set_toposort_cycle_handler(dt_hm_toposort_cycle_handler_t handler)
dt_hm_constraint_choice_t
@ DT_HM_CONSTRAINTS_PREFER_DEST
@ DT_HM_CONSTRAINTS_PREFER_SRC
@ DT_HM_BATCH_ACCEPT
@ DT_HM_BATCH_REVERT
dt_history_merge_strategy_t
@ DT_HISTORY_MERGE_REPLACE
@ DT_HISTORY_MERGE_PREPEND
@ DT_HISTORY_MERGE_APPEND
static GList * _hm_report_build_ordered_modules(dt_develop_t *dev_dest, const GPtrArray *visible_order, const GHashTable *mod_list_ids)
static GPtrArray * _hm_collect_enabled_modules_gui_order(const dt_develop_t *dev, const GHashTable *mod_list_ids)
static gchar * _hm_cycle_node_label(const dt_digraph_node_t *n, GHashTable *id_ht)
static void _hm_report_keep_input_row_at_bottom(GtkListStore *store)
static void _hm_report_resync_history_iop_order(dt_develop_t *dev)
static GPtrArray * _hm_report_collect_dest_ids(GtkTreeModel *model)
static void _hm_report_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint info, guint time, gpointer user_data)
static GPtrArray * _hm_report_build_desired_visible_order(dt_develop_t *dev_dest, GtkTreeModel *model)
static void _hm_report_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
gboolean _hm_warn_missing_raster_producers(const GList *mod_list)
void _hm_show_toposort_cycle_popup(GList *cycle_nodes, GHashTable *id_ht)
static void _hm_report_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, guint info, guint time, gpointer user_data)
static void _hm_report_align_drag_hint(GtkWidget *widget, GtkAllocation *allocation, gpointer user_data)
static gchar * _hm_pretty_id(const char *id)
static void _hm_report_update_dest_labels(GtkListStore *store, dt_develop_t *dev_dest, GHashTable *dst_last_by_id, const GHashTable *orig_ids, const GHashTable *override)
gboolean _hm_show_merge_report_popup(dt_develop_t *dev_dest, dt_develop_t *dev_src, const gboolean merge_iop_order, const gboolean used_source_order, const dt_history_merge_strategy_t strategy, GHashTable *src_last_by_id, GHashTable *dst_last_before_by_id, const GHashTable *orig_ids, const GHashTable *mod_list_ids, const char *source_label, dt_hm_batch_state_t *batch)
static void _hm_report_update_move_styles(GtkListStore *store, dt_develop_t *dev_src, const GHashTable *mod_list_ids)
static gboolean _hm_history_items_match(const dt_dev_history_item_t *a, const dt_dev_history_item_t *b)
static GHashTable * _hm_build_override_map(const dt_develop_t *dev_dest, GHashTable *src_last_by_id, GHashTable *dst_last_before_by_id)
void dt_history_merge_gui_register_handlers(void)
dt_hm_constraint_choice_t _hm_ask_user_constraints_choice(GHashTable *id_ht, const char *faulty_id, const char *src_prev, const char *src_next, const char *dst_prev, const char *dst_next)
static void _hm_report_update_arrows(GtkListStore *store, GHashTable *override, GHashTable *dst_last_by_id, GHashTable *dst_last_before_by_id)
static gboolean _hm_history_masks_match(const dt_dev_history_item_t *a, const dt_dev_history_item_t *b)
static gboolean _hm_report_apply_visible_order(dt_develop_t *dev_dest, const GPtrArray *visible_order, const GHashTable *mod_list_ids)
GPtrArray * _hm_collect_labels_from_history_map(GHashTable *last_by_id, const GHashTable *mod_list_ids, GPtrArray **out_styles)
static gboolean _hm_module_visible_in_report(const dt_iop_module_t *mod, const GHashTable *mod_list_ids)
static const char *const HM_REPORT_EXISTING_DST_FG
dt_hm_report_col_t
@ HM_REPORT_COL_DST_ID
@ HM_REPORT_COL_DST
@ HM_REPORT_COL_DST_FG
@ HM_REPORT_COL_SRC_FG
@ HM_REPORT_COL_SRC_ID
@ HM_REPORT_COL_FILET
@ HM_REPORT_COL_SRC_FG_SET
@ HM_REPORT_COL_ORIG_FG_SET
@ HM_REPORT_COL_SRC_WEIGHT
@ HM_REPORT_COL_IS_INPUT
@ HM_REPORT_COL_DST_DISABLED
@ HM_REPORT_COL_DST_STYLE
@ HM_REPORT_COL_ARROW
@ HM_REPORT_COL_DST_FG_SET
@ HM_REPORT_COL_ORIG
@ HM_REPORT_COL_SRC
@ HM_REPORT_COL_DST_WEIGHT
@ HM_REPORT_COL_ORIG_FG
@ HM_REPORT_COL_COUNT
static const char *const HM_REPORT_DISABLED_FG
static gchar * _hm_report_dest_label(const dt_iop_module_t *mod, GHashTable *dst_last_by_id, const GHashTable *orig_ids)
static void _hm_report_apply_store_order(_hm_report_reorder_ctx_t *ctx)
static gchar * _hm_module_label_short(const dt_iop_module_t *mod)
gboolean dt_gui_merge_options_dialog(const char *title, const char *mode_key, const char *iop_order_key, const char *ask_key, const gboolean iop_order_available)
Show a modal dialog to pick merge mode and pipeline order before a paste or style apply.
static gchar * _hm_pretty_id_from_id_ht(const char *id, GHashTable *id_ht, const gboolean prefer_dest)
static gint _hm_label_cmp(gconstpointer a, gconstpointer b)
static void _hm_append_cycle_label(GString *out, const char *s, const gboolean bold)
static dt_iop_module_t * _hm_module_from_id(dt_develop_t *dev, const char *id)
static gchar * _hm_clean_module_name(const dt_iop_module_t *mod)
static GHashTable * _hm_report_build_moved_set(dt_develop_t *dev_src, GtkTreeModel *model, const GHashTable *mod_list_ids)
static gchar * _hm_module_row_label(const dt_iop_module_t *mod)
dt_iop_module_t * dt_iop_get_module_by_instance_name(GList *modules, const char *operation, const char *multi_name)
Definition imageop.c:1857
gboolean dt_iop_module_needs_mask_history(const dt_iop_module_t *module)
Definition imageop.c:1228
dt_iop_module_t * dt_iop_get_module_by_op_priority(GList *modules, const char *operation, const int multi_priority)
Definition imageop.c:1839
@ IOP_FLAGS_NO_HISTORY_STACK
Definition imageop.h:193
const char * model
void dt_ioppr_rebuild_iop_order_from_modules(struct dt_develop_t *dev, GList *ordered_modules)
Rebuild dev->iop_order_list from a list of ordered modules.
Definition iop_order.c:1278
gchar * delete_underscore(const char *s)
Definition label.c:75
@ DT_DEBUG_HISTORY
Definition logging.h:75
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
uint64_t dt_masks_group_get_hash_ext(uint64_t hash, GList *masks, dt_masks_form_t *form)
Definition masks.c:1401
dt_masks_form_t * dt_masks_get_from_id_ext(GList *forms, int id)
Definition masks.c:899
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
const char * name
Definition pdf.h:90
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const float r
unsigned __int64 uint64_t
Definition strptime.c:75
const dt_iop_module_t * src_iop
dt_iop_module_t * dst_iop
const dt_iop_module_t * mod_list
const GHashTable * orig_ids
const GHashTable * mod_list_ids
struct dt_iop_module_t *gboolean enabled
Definition dev_history.h:52
dt_image_t image_storage
Definition develop.h:225
GList * iop
Definition develop.h:269
GList * history
Definition develop.h:259
Directed graph node.
dt_hm_batch_decision_t decision
GtkTreeViewColumn * orig_column
GtkTreeViewColumn * arrow_column
GtkTreeViewColumn * filet_column
GtkTreeViewColumn * src_column
char filename[DT_MAX_FILENAME_LEN]
Definition image.h:386
int32_t id
Definition image.h:401
char multi_name[128]
Definition imageop.h:387
gboolean enabled
Definition imageop.h:313
struct dt_iop_module_t::@23::@24 source
struct dt_iop_module_t::@23::@25 sink
struct dt_iop_module_t::@23 raster_mask
dt_dev_operation_t op
Definition imageop.h:259
#define MAX(a, b)
Definition thinplate.c:29
Small directed-graph helper for constraint aggregation and topological sorting.
#define DT_PIXEL_APPLY_DPI(value)
void dt_capitalize_label(gchar *text)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)