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