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