diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
index 9baaf1743a53..e41150d5ae31 100644
--- a/data/darktableconfig.xml.in
+++ b/data/darktableconfig.xml.in
@@ -3636,6 +3636,7 @@
+
diff --git a/data/kernels/basecurve.cl b/data/kernels/basecurve.cl
old mode 100644
new mode 100755
index 24a675fe73ab..f0937ea857b0
--- a/data/kernels/basecurve.cl
+++ b/data/kernels/basecurve.cl
@@ -1,6 +1,6 @@
/*
This file is part of darktable,
- copyright (c) 2016-2025 darktable developers.
+ copyright (c) 2016-2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -19,6 +19,40 @@
#include "color_conversion.h"
#include "rgb_norms.h"
+/*
+ Narkowicz (2016) rational approximation of the ACES RRT+ODT curve for sRGB output.
+ Widely used in real-time rendering for its simplicity and visual quality.
+ Does NOT implement the full ACES pipeline (no color space transform, no D60 whitepoint).
+ Reference: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+*/
+inline float _aces_tone_map(const float x)
+{
+ const float a = 2.51f;
+ const float b = 0.03f;
+ const float c = 2.43f;
+ const float d = 0.59f;
+ const float e = 0.14f;
+
+ return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0f, 1.0f);
+}
+/*
+ Narkowicz & Filiberto (2021) rational approximation of the ACES 2.0 RRT curve.
+ More precise than the basic Narkowicz 2016 fit, with a softer shoulder.
+ The pre-scale factor (x * 1.680) in the caller adjusts the exposure point.
+ Does NOT implement the full ACES pipeline (no color space transform, no D60 whitepoint).
+ Reference: https://github.com/h3r2tic/tony-mc-mapface (Narkowicz/Filiberto fit)
+*/
+inline float _aces_20_tonemap(const float x)
+{
+ const float a = 0.0245786f;
+ const float b = 0.000090537f;
+ const float c = 0.983729f;
+ const float d = 0.4329510f;
+ const float e = 0.238081f;
+
+ return clamp((x * (x + a) - b) / (x * (c * x + d) + e), 0.0f, 1.0f);
+}
+
/*
Primary LUT lookup. Measures the luminance of a given pixel using a selectable function, looks up that
luminance in the configured basecurve, and then scales each channel by the result.
@@ -30,9 +64,15 @@
for exposure bracketing, and which may have had a camera-specific base curve applied.
*/
kernel void
-basecurve_lut(read_only image2d_t in, write_only image2d_t out, const int width, const int height,
- const float mul, read_only image2d_t table, constant float *a, const int preserve_colors,
- constant dt_colorspaces_iccprofile_info_cl_t *profile_info, read_only image2d_t lut,
+basecurve_lut(read_only image2d_t in,
+ write_only image2d_t out,
+ const int width, const int height,
+ const float mul,
+ read_only image2d_t table,
+ constant float *a,
+ const int preserve_colors,
+ constant dt_colorspaces_iccprofile_info_cl_t *profile_info,
+ read_only image2d_t lut,
const int use_work_profile)
{
const int x = get_global_id(0);
@@ -75,8 +115,11 @@ basecurve_zero(write_only image2d_t out, const int width, const int height)
to take the risks of "artistic" impacts on their image.
*/
kernel void
-basecurve_legacy_lut(read_only image2d_t in, write_only image2d_t out, const int width, const int height,
- const float mul, read_only image2d_t table, constant float *a)
+basecurve_legacy_lut(read_only image2d_t in,
+ write_only image2d_t out,
+ const int width, const int height,
+ const float mul, read_only image2d_t table,
+ constant float *a)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
@@ -86,9 +129,11 @@ basecurve_legacy_lut(read_only image2d_t in, write_only image2d_t out, const int
float4 pixel = read_imagef(in, sampleri, (int2)(x, y));
// apply ev multiplier and use lut or extrapolation:
- pixel.x = lookup_unbounded(table, mul * pixel.x, a);
- pixel.y = lookup_unbounded(table, mul * pixel.y, a);
- pixel.z = lookup_unbounded(table, mul * pixel.z, a);
+ float3 f = pixel.xyz * mul;
+
+ pixel.x = lookup_unbounded(table, f.x, a);
+ pixel.y = lookup_unbounded(table, f.y, a);
+ pixel.z = lookup_unbounded(table, f.z, a);
pixel = fmax(pixel, 0.f);
write_imagef (out, (int2)(x, y), pixel);
}
@@ -298,14 +343,247 @@ basecurve_reconstruct(read_only image2d_t in, read_only image2d_t tmp, write_onl
}
kernel void
-basecurve_finalize(read_only image2d_t in, read_only image2d_t comb, write_only image2d_t out, const int width, const int height)
+basecurve_finalize(read_only image2d_t in,
+ read_only image2d_t comb,
+ write_only image2d_t out,
+ const int width,
+ const int height, const int workflow_mode,
+ const float shadow_lift,
+ const float highlight_gain,
+ const float ucs_saturation_balance,
+ const float gamut_strength, const float highlight_corr,
+ const int target_gamut,
+ const float look_opacity,
+ const float16 look_mat,
+ const float alpha)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
if(x >= width || y >= height) return;
- float4 pixel = fmax(read_imagef(comb, sampleri, (int2)(x, y)), 0.f);
+ float4 pixel = read_imagef(comb, sampleri, (int2)(x, y));
+
+ // Sanitize to avoid Inf/NaN propagation
+ pixel.xyz = fmax(pixel.xyz, 0.0f);
+ pixel.xyz = fmin(pixel.xyz, (float3)(1e6f));
+
+ if(workflow_mode > 0)
+ {
+ float3 pixel_in = pixel.xyz;
+ float3 look_transformed;
+ look_transformed.x = dot(pixel_in, (float3)(look_mat.s0, look_mat.s1, look_mat.s2));
+ look_transformed.y = dot(pixel_in, (float3)(look_mat.s3, look_mat.s4, look_mat.s5));
+ look_transformed.z = dot(pixel_in, (float3)(look_mat.s6, look_mat.s7, look_mat.s8));
+
+ // Mix between original and transformed
+ pixel.xyz = mix(pixel_in, look_transformed, look_opacity);
+ pixel.xyz = fmax(pixel.xyz, 0.0f); // Anti-black artifacts
+
+ if(highlight_gain != 1.0f)
+ pixel.xyz *= highlight_gain;
+
+ if(shadow_lift != 1.0f)
+ {
+ pixel.x = (pixel.x > 0.0f) ? native_powr(pixel.x, shadow_lift) : pixel.x;
+ pixel.y = (pixel.y > 0.0f) ? native_powr(pixel.y, shadow_lift) : pixel.y;
+ pixel.z = (pixel.z > 0.0f) ? native_powr(pixel.z, shadow_lift) : pixel.z;
+ }
+
+ const float r_coeff = 0.2627f;
+ const float g_coeff = 0.6780f;
+ const float b_coeff = 0.0593f;
+
+ float y_in = pixel.x * r_coeff + pixel.y * g_coeff + pixel.z * b_coeff;
+ float y_out = y_in;
+
+ /* Scene-referred: luminance-adaptive shoulder extension for ACES-like
+ tonemapping using perceptual luminance Jz. */
+ if(workflow_mode == 1 || workflow_mode == 2)
+ {
+ float3 xyz;
+ xyz.x = 0.636958f * pixel.x + 0.144617f * pixel.y + 0.168881f * pixel.z;
+ xyz.y = 0.262700f * pixel.x + 0.677998f * pixel.y + 0.059302f * pixel.z;
+ xyz.z = 0.000000f * pixel.x + 0.028073f * pixel.y + 1.060985f * pixel.z;
+
+ xyz = fmax(xyz, (float3)(0.0f));
+
+ float4 xyz_scaled = (float4)(xyz.x * 400.0f, xyz.y * 400.0f, xyz.z * 400.0f, 0.0f);
+ float4 jab = XYZ_to_JzAzBz(xyz_scaled);
+
+ const float L = clamp(jab.x, 0.0f, 1.0f);
+ const float k = 1.0f + alpha * L * L;
+
+ const float x_scaled = y_in / k;
+ if(workflow_mode == 1)
+ y_out = _aces_tone_map(x_scaled) * k;
+ else
+ y_out = _aces_20_tonemap(x_scaled * 1.680f) * k;
+ }
+
+ float gain = y_out / fmax(y_in, 1e-6f);
+ pixel.xyz *= gain;
+
+ const float threshold = 0.80f;
+ if(y_out > threshold)
+ {
+ float factor = (y_out - threshold) / (1.0f - threshold);
+ factor = clamp(factor, 0.0f, 1.0f);
+ pixel.xyz = mix(pixel.xyz, (float3)y_out, factor);
+ }
+
+ float4 jab = (float4)(0.0f);
+ if(ucs_saturation_balance != 0.0f || gamut_strength > 0.0f || highlight_corr != 0.0f)
+ {
+ // RGB Rec2020 to XYZ D65
+ float3 xyz;
+ xyz.x = 0.636958f * pixel.x + 0.144617f * pixel.y + 0.168881f * pixel.z;
+ xyz.y = 0.262700f * pixel.x + 0.677998f * pixel.y + 0.059302f * pixel.z;
+ xyz.z = 0.000000f * pixel.x + 0.028073f * pixel.y + 1.060985f * pixel.z;
+
+ xyz = fmax(xyz, 0.0f);
+
+ // XYZ to JzAzBz
+ float4 xyz_scaled = (float4)(xyz.x * 400.0f, xyz.y * 400.0f, xyz.z * 400.0f, 0.0f);
+ jab = XYZ_to_JzAzBz(xyz_scaled);
+
+ int modified = 0;
+
+ if(ucs_saturation_balance != 0.0f)
+ {
+ // Chroma-based modulation for saturation balance
+ const float chroma = fmax(fmax(pixel.x, pixel.y), pixel.z) - fmin(fmin(pixel.x, pixel.y), pixel.z);
+ const float effective_saturation = ucs_saturation_balance * fmin(chroma * 2.0f, 1.0f);
+
+ // Apply saturation balance
+ const float Y = xyz.y;
+ const float L = native_sqrt(fmax(Y, 0.0f));
+ const float fulcrum = 0.5f;
+ const float n = (L - fulcrum) / fulcrum;
+ const float mask_shadow = 1.0f / (1.0f + dtcl_exp(n * 4.0f));
+
+ float sat_adjust = effective_saturation * (2.0f * mask_shadow - 1.0f);
+ sat_adjust *= fmin(L * 4.0f, 1.0f);
+ const float sat_factor = 1.0f + sat_adjust;
+ jab.y *= sat_factor;
+ jab.z *= sat_factor;
+ modified = 1;
+ }
+
+ if(gamut_strength > 0.0f)
+ {
+ const float Y = xyz.y;
+ const float L = native_sqrt(fmax(Y, 0.0f));
+ const float chroma_factor = 1.0f - gamut_strength * (0.2f + 0.2f * L);
+ jab.y *= chroma_factor;
+ jab.z *= chroma_factor;
+ modified = 1;
+ }
+
+ // HIGH SENSITIVITY CORRECTION
+ // Start effect at 0.20 up to 0.90. Linear transition.
+ float hl_mask = clamp((jab.x - 0.20f) / 0.70f, 0.0f, 1.0f);
+
+ if(hl_mask > 0.0f && highlight_corr != 0.0f)
+ {
+ // 1. Soft symmetric desaturation (0.75 factor)
+ const float desat = 1.0f - (fabs(highlight_corr) * hl_mask * 0.75f);
+ jab.y *= desat;
+ jab.z *= desat;
+
+ // 2. Controlled Hue Rotation (2.0 factor)
+ const float angle = highlight_corr * hl_mask * 2.0f;
+ const float ca = native_cos(angle);
+ const float sa = native_sin(angle);
+ const float az = jab.y;
+ const float bz = jab.z;
+
+ jab.y = az * ca - bz * sa;
+ jab.z = az * sa + bz * ca;
+ modified = 1;
+ }
+
+ if(jab.x > 0.95f)
+ {
+ const float desat = clamp((1.0f - jab.x) * 20.0f, 0.0f, 1.0f);
+ jab.y *= desat;
+ jab.z *= desat;
+ modified = 1;
+ }
+
+ if(modified)
+ {
+ // JzAzBz to XYZ
+ xyz = JzAzBz_2_XYZ(jab).xyz / 400.0f;
+
+ // XYZ D65 to RGB Rec2020
+ pixel.xyz = XYZ_to_Rec2020(xyz);
+
+ const float min_val = fmin(pixel.x, fmin(pixel.y, pixel.z));
+ if(min_val < 0.0f)
+ {
+ const float lum = 0.2627f * pixel.x + 0.6780f * pixel.y + 0.0593f * pixel.z;
+ if(lum > 0.0f)
+ {
+ const float factor = lum / (lum - min_val);
+ pixel.xyz = lum + factor * (pixel.xyz - lum);
+ }
+ }
+ pixel.xyz = clamp(pixel.xyz, 0.0f, 1.0f);
+ }
+ }
+
+ if(gamut_strength > 0.0f)
+ {
+ float4 orig = pixel;
+
+ float Y = 0.2126f * pixel.x + 0.7152f * pixel.y + 0.0722f * pixel.z;
+ float lum_weight = clamp((Y - 0.3f) / (0.8f - 0.3f), 0.0f, 1.0f);
+ lum_weight = lum_weight * lum_weight * (3.0f - 2.0f * lum_weight);
+ float effective_strength = gamut_strength * lum_weight;
+
+ float limit = 0.90f;
+ if(target_gamut == 1) limit = 0.95f;
+ else if(target_gamut == 2) limit = 1.00f;
+
+ float threshold = limit * (1.0f - (effective_strength * 0.25f));
+ float max_val = fmax(pixel.x, fmax(pixel.y, pixel.z));
+
+ if(max_val > threshold)
+ {
+ const float range = limit - threshold;
+ const float delta = max_val - threshold;
+ const float compressed = threshold + range * delta / (delta + range);
+ const float factor = compressed / max_val;
+
+ const float range_blue = 1.1f * range;
+ const float compressed_blue = threshold + range * delta / (delta + range_blue);
+ const float factor_blue = compressed_blue / max_val;
+
+ pixel.x *= factor;
+ pixel.y *= factor;
+ pixel.z *= factor_blue;
+ }
+ pixel = mix(orig, pixel, effective_strength);
+ }
+
+ // Final gamut check to preserve hue
+ if(pixel.x < 0.0f || pixel.x > 1.0f || pixel.y < 0.0f || pixel.y > 1.0f || pixel.z < 0.0f || pixel.z > 1.0f)
+ {
+ const float luma = 0.2627f * pixel.x + 0.6780f * pixel.y + 0.0593f * pixel.z;
+ const float target_luma = clamp(luma, 0.0f, 1.0f);
+ float t = 1.0f;
+ if(pixel.x < 0.0f) t = fmin(t, target_luma / (target_luma - pixel.x));
+ if(pixel.y < 0.0f) t = fmin(t, target_luma / (target_luma - pixel.y));
+ if(pixel.z < 0.0f) t = fmin(t, target_luma / (target_luma - pixel.z));
+ if(pixel.x > 1.0f) t = fmin(t, (1.0f - target_luma) / (pixel.x - target_luma));
+ if(pixel.y > 1.0f) t = fmin(t, (1.0f - target_luma) / (pixel.y - target_luma));
+ if(pixel.z > 1.0f) t = fmin(t, (1.0f - target_luma) / (pixel.z - target_luma));
+ t = fmax(0.0f, t);
+ pixel.xyz = target_luma + t * (pixel.xyz - target_luma);
+ }
+ }
+
pixel.w = read_imagef(in, sampleri, (int2)(x, y)).w;
write_imagef (out, (int2)(x, y), pixel);
diff --git a/data/kernels/color_conversion.h b/data/kernels/color_conversion.h
index b1dad97cfaa3..af0fdebf6d50 100644
--- a/data/kernels/color_conversion.h
+++ b/data/kernels/color_conversion.h
@@ -154,3 +154,14 @@ static inline float dt_camera_rgb_luminance(const float4 rgb)
const float4 coeffs = { 0.2225045f, 0.7168786f, 0.0606169f, 0.0f };
return dot(rgb, coeffs);
}
+
+/* XYZ D65 to RGB Rec2020 (linear) */
+static inline float3 XYZ_to_Rec2020(const float3 xyz)
+{
+ float3 rgb;
+ // XYZ to Rec.2020 conversion matrix coefficients
+ rgb.x = 1.716651f * xyz.x - 0.355671f * xyz.y - 0.253366f * xyz.z;
+ rgb.y = -0.666684f * xyz.x + 1.616481f * xyz.y + 0.015768f * xyz.z;
+ rgb.z = 0.017640f * xyz.x - 0.042770f * xyz.y + 0.942103f * xyz.z;
+ return rgb;
+}
diff --git a/h origin basecurve_PR --force b/h origin basecurve_PR --force
new file mode 100644
index 000000000000..c7814b052c3f
--- /dev/null
+++ b/h origin basecurve_PR --force
@@ -0,0 +1,29 @@
+[33mcommit bed486a3bfd2384faba1c831ca9859a2bdb65efa[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mbasecurve_PR[m[33m)[m
+Author: Christian Bouhon
+Date: Fri Mar 6 16:54:47 2026 +0100
+
+ 20260306 use of _args(), and use of CLARGFLOAT
+
+[33mcommit 2055e5f8e2fcbd5ee868774628c28fd59027a370[m
+Author: Christian Bouhon
+Date: Mon Mar 2 23:26:38 2026 +0100
+
+ 20260301 no capital letters on default ui & Rec2020
+
+[33mcommit 42521946bad4e2e458a8e058b806aa123d5b6c69[m
+Author: Christian Bouhon
+Date: Sun Mar 1 13:48:15 2026 +0100
+
+ 20260301 XYZ D65 to RGB Rec2020 is now sharing in color_convertion.h
+
+[33mcommit 7bb608c14cee36d42c6564523b98bc41faf52311[m
+Author: Christian Bouhon
+Date: Sat Feb 28 01:42:27 2026 +0100
+
+ 20260228 Requested changes, const and final style adjustments
+
+[33mcommit 41423773369a6580d9feb243aca8529f97c1dd26[m
+Author: Christian Bouhon
+Date: Wed Feb 18 18:20:50 2026 +0100
+
+ 20260218 implement adaptive JzAzBz shoulder extension
diff --git a/src/common/utility.c b/src/common/utility.c
index 77c94912bf0f..a22b947bb8b8 100644
--- a/src/common/utility.c
+++ b/src/common/utility.c
@@ -1217,7 +1217,8 @@ gboolean dt_is_scene_referred(void)
{
return dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (filmic)")
|| dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (sigmoid)")
- || dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (AgX)");
+ || dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (AgX)")
+ || dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (basecurve)");
}
gboolean dt_is_display_referred(void)
diff --git a/src/develop/develop.c b/src/develop/develop.c
index 2a8aeaeec477..842a5f700a5b 100644
--- a/src/develop/develop.c
+++ b/src/develop/develop.c
@@ -1978,7 +1978,7 @@ static gboolean _dev_auto_apply_presets(dt_develop_t *dev)
" THEN multi_name"
" ELSE (ROW_NUMBER() OVER (PARTITION BY operation ORDER BY operation) - 1)"
" END",
- is_display_referred ? "" : "basecurve");
+ ""); // The basecurve is no longer excluded in non-display mode.
// clang-format on
// query for all modules at once:
@@ -3941,4 +3941,4 @@ void dt_dev_init_chroma(dt_develop_t *dev)
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
-// clang-format on
+// clang-format on
\ No newline at end of file
diff --git a/src/develop/modulegroups.c b/src/develop/modulegroups.c
new file mode 100644
index 000000000000..d63d525cb11d
--- /dev/null
+++ b/src/develop/modulegroups.c
@@ -0,0 +1,4158 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2011-2025 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+
+#include "bauhaus/bauhaus.h"
+#include "common/darktable.h"
+#include "common/debug.h"
+#include "common/image_cache.h"
+#include "common/iop_group.h"
+#include "common/presets.h"
+#include "control/conf.h"
+#include "control/control.h"
+#include "develop/develop.h"
+#include "dtgtk/button.h"
+#include "dtgtk/icon.h"
+#include "gui/accelerators.h"
+#include "gui/gtk.h"
+#include "gui/presets.h"
+#include "libs/lib.h"
+#include "libs/lib_api.h"
+#ifdef GDK_WINDOWING_QUARTZ
+#include "osx/osx.h"
+#endif
+
+DT_MODULE(1)
+
+// the T_ macros are for the translation engine to take them into account
+#define FALLBACK_PRESET_NAME "workflow: scene-referred"
+#define T_FALLBACK_PRESET_NAME _("workflow: scene-referred")
+
+#define DEPRECATED_PRESET_NAME "modules: deprecated"
+#define T_DEPRECATED_PRESET_NAME _("modules: deprecated")
+
+#define CURRENT_PRESET_NAME "last modified layout"
+#define T_CURRENT_PRESET_NAME _("last modified layout")
+
+// list of recommended basics widgets
+#define RECOMMENDED_BASICS \
+ "|exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast|colorbalancergb/global " \
+ "vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation|ashift/roration|denoiseprofile|lens|bilat|"
+
+// if a preset cannot be loaded or the current preset deleted, this is the fallback preset
+
+#define PADDING 2
+
+#include "modulegroups.h"
+
+typedef enum dt_lib_modulegroups_basic_item_position_t
+{
+ NORMAL,
+ NEW_MODULE,
+ FIRST_MODULE
+} dt_lib_modulegroups_basic_item_position_t;
+
+typedef enum dt_lib_modulegroups_basic_item_type_t
+{
+ WIDGET_TYPE_NONE = 0,
+ WIDGET_TYPE_BAUHAUS_SLIDER,
+ WIDGET_TYPE_BAUHAUS_COMBO,
+ WIDGET_TYPE_ACTIVATE_BTN,
+ WIDGET_TYPE_MISC
+} dt_lib_modulegroups_basic_item_type_t;
+
+typedef struct dt_lib_modulegroups_basic_item_t
+{
+ gchar *id;
+ gchar *module_op;
+ gchar *widget_name; // translated
+ GtkWidget *widget;
+ GtkWidget *temp_widget;
+ GtkWidget *old_parent;
+ dt_lib_modulegroups_basic_item_type_t widget_type;
+
+ int old_pos;
+ gboolean expand;
+ gboolean fill;
+ guint padding;
+ GtkPackType packtype;
+ gchar *tooltip;
+ int grid_x, grid_y, grid_w, grid_h;
+
+ GtkWidget *box;
+ dt_iop_module_t *module;
+} dt_lib_modulegroups_basic_item_t;
+
+typedef struct dt_lib_modulegroups_group_t
+{
+ gchar *name;
+ GtkWidget *button;
+ gchar *icon;
+ GtkWidget *iop_box;
+ // default
+ GList *modules;
+} dt_lib_modulegroups_group_t;
+
+typedef struct dt_lib_modulegroups_t
+{
+ int32_t current;
+ GtkWidget *text_entry;
+ GtkWidget *hbox_buttons;
+ GtkWidget *active_btn;
+ GtkWidget *basic_btn;
+ GtkWidget *hbox_groups;
+ GtkWidget *hbox_search_box;
+ GtkWidget *deprecated;
+ gboolean force_deprecated_message;
+ GList *groups;
+ gboolean show_search;
+ gboolean full_active;
+
+ GList *edit_groups;
+ gboolean edit_show_search;
+ gboolean edit_full_active;
+ gchar *edit_preset;
+ gboolean edit_ro;
+ gboolean edit_basics_show;
+ GList *edit_basics;
+
+ // editor dialog
+ GtkWidget *dialog;
+ gboolean editor_reset;
+ GtkWidget *presets_combo, *presets_btn_remove, *presets_btn_dup, *presets_btn_rename, *presets_btn_new;
+ GtkWidget *preset_groups_box, *preset_btn_add_group, *preset_read_only_label, *preset_reset_btn;
+ GtkWidget *edit_search_cb, *edit_full_active_cb;
+ GtkWidget *basics_chkbox, *edit_basics_groupbox, *edit_basics_box;
+ GtkWidget *edit_autoapply_chkbox, *edit_autoapply_btn;
+
+ gboolean basics_show;
+ GList *basics;
+ GtkWidget *vbox_basic;
+ GtkWidget *mod_vbox_basic;
+
+ dt_iop_module_t *force_show_module;
+} dt_lib_modulegroups_t;
+
+typedef enum dt_lib_modulegroup_iop_visibility_type_t
+{
+ DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE,
+ DT_MODULEGROUP_SEARCH_IOP_GROUPS_VISIBLE,
+ DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE
+} dt_lib_modulegroup_iop_visibility_type_t;
+
+/* toggle button callback */
+static void _lib_modulegroups_toggle(GtkWidget *button, dt_lib_module_t *self);
+/* helper function to update iop module view depending on group */
+static void _lib_modulegroups_update_iop_visibility(dt_lib_module_t *self);
+
+/* modulergroups proxy set group function
+ \see dt_dev_modulegroups_set()
+*/
+static void _lib_modulegroups_set(dt_lib_module_t *self, uint32_t group);
+/* modulegroups proxy update visibility function
+*/
+static void _lib_modulegroups_update_visibility_proxy(dt_lib_module_t *self);
+/* modulegroups proxy get group function
+ \see dt_dev_modulegroups_get()
+*/
+static uint32_t _lib_modulegroups_get(dt_lib_module_t *self);
+/* modulegroups proxy test function.
+ tests if iop module group flags matches modulegroup.
+*/
+static gboolean _lib_modulegroups_test(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module);
+/* modulegroups proxy test visibility function.
+ tests if iop module is preset in one groups for current layout.
+*/
+static gboolean _lib_modulegroups_test_visible(dt_lib_module_t *self, gchar *module);
+/* modulegroups proxy switch group function.
+ sets the active group which module belongs too.
+*/
+static void _lib_modulegroups_switch_group(dt_lib_module_t *self, dt_iop_module_t *module);
+
+static void _manage_preset_update_list(dt_lib_module_t *self);
+static void _manage_editor_load(const char *preset, dt_lib_module_t *self);
+
+static void _buttons_update(dt_lib_module_t *self);
+
+const char *name(dt_lib_module_t *self)
+{
+ return _("modulegroups");
+}
+
+dt_view_type_flags_t views(dt_lib_module_t *self)
+{
+ return DT_VIEW_DARKROOM;
+}
+
+uint32_t container(dt_lib_module_t *self)
+{
+ return DT_UI_CONTAINER_PANEL_RIGHT_TOP;
+}
+
+
+/* this module should always be shown without expander */
+int expandable(dt_lib_module_t *self)
+{
+ return 0;
+}
+
+int position(const dt_lib_module_t *self)
+{
+ return 999;
+}
+
+static GtkWidget *_buttons_get_from_pos(dt_lib_module_t *self, const int pos)
+{
+ const dt_lib_modulegroups_t *d = self->data;
+ if(pos == DT_MODULEGROUP_ACTIVE_PIPE) return d->active_btn;
+ if(pos == DT_MODULEGROUP_BASICS) return d->basic_btn;
+ dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, pos - 1);
+ if(gr) return gr->button;
+ return NULL;
+}
+
+static void _text_entry_changed_callback(GtkEntry *entry, dt_lib_module_t *self)
+{
+ if(darktable.gui->reset) return;
+ _lib_modulegroups_update_iop_visibility(self);
+}
+
+static DTGTKCairoPaintIconFunc _buttons_get_icon_fct(const gchar *icon)
+{
+ if(g_strcmp0(icon, "active") == 0)
+ return dtgtk_cairo_paint_modulegroup_active;
+ else if(g_strcmp0(icon, "favorites") == 0)
+ return dtgtk_cairo_paint_modulegroup_favorites;
+ else if(g_strcmp0(icon, "tone") == 0)
+ return dtgtk_cairo_paint_modulegroup_tone;
+ else if(g_strcmp0(icon, "color") == 0)
+ return dtgtk_cairo_paint_modulegroup_color;
+ else if(g_strcmp0(icon, "correct") == 0)
+ return dtgtk_cairo_paint_modulegroup_correct;
+ else if(g_strcmp0(icon, "effect") == 0)
+ return dtgtk_cairo_paint_modulegroup_effect;
+ else if(g_strcmp0(icon, "grading") == 0)
+ return dtgtk_cairo_paint_modulegroup_grading;
+ else if(g_strcmp0(icon, "technical") == 0)
+ return dtgtk_cairo_paint_modulegroup_technical;
+
+ return dtgtk_cairo_paint_modulegroup_basic;
+}
+
+static gint _iop_compare(gconstpointer a, gconstpointer b)
+{
+ return g_strcmp0((gchar *)a, (gchar *)b);
+}
+static gboolean _lib_modulegroups_test_internal(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module)
+{
+ if(group == DT_MODULEGROUP_ACTIVE_PIPE) return module->enabled;
+ dt_lib_modulegroups_t *d = self->data;
+ dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, group - 1);
+ if(gr)
+ {
+ return (g_list_find_custom(gr->modules, module->so->op, _iop_compare) != NULL);
+ }
+ return FALSE;
+}
+
+static gboolean _lib_modulegroups_test(dt_lib_module_t *self, uint32_t group, dt_iop_module_t *module)
+{
+ return _lib_modulegroups_test_internal(self, group, module);
+}
+
+static gboolean _lib_modulegroups_test_visible(dt_lib_module_t *self, gchar *module)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ for(const GList *l = d->groups; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_group_t *gr = l->data;
+ if(g_list_find_custom(gr->modules, module, _iop_compare) != NULL)
+ {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void _basics_init_item(dt_lib_modulegroups_basic_item_t *item)
+{
+ if(!item->id) return;
+
+ gchar **elems = g_strsplit(item->id, "/", -1);
+ if(g_strv_length(elems) > 0)
+ {
+ item->module_op = g_strdup(elems[0]);
+ if(item->widget && DT_IS_BAUHAUS_WIDGET(item->widget))
+ {
+ if(g_strv_length(elems) > 2)
+ item->widget_name = g_strdup_printf("%s - %s", _(elems[1]), dt_bauhaus_widget_get_label(item->widget));
+ else if(g_strv_length(elems) > 1)
+ item->widget_name = g_strdup(dt_bauhaus_widget_get_label(item->widget));
+ else
+ {
+ item->widget_name = g_strdup(_("on-off"));
+ item->widget_type = WIDGET_TYPE_ACTIVATE_BTN;
+ }
+ }
+ else
+ {
+ if(g_strv_length(elems) > 2)
+ item->widget_name = g_strdup_printf("%s - %s", _(elems[1]), _(elems[2]));
+ else if(g_strv_length(elems) > 1)
+ item->widget_name = g_strdup(_(elems[1]));
+ else
+ {
+ item->widget_name = g_strdup(_("on-off"));
+ item->widget_type = WIDGET_TYPE_ACTIVATE_BTN;
+ }
+ }
+ }
+ g_strfreev(elems);
+}
+
+static void _basics_free_item(dt_lib_modulegroups_basic_item_t *item)
+{
+ g_free(item->id);
+ g_free(item->module_op);
+ if(item->tooltip) g_free(item->tooltip);
+ g_free(item->widget_name);
+}
+
+static void _basics_remove_widget(dt_lib_modulegroups_basic_item_t *item)
+{
+ if(item->widget && item->widget_type != WIDGET_TYPE_ACTIVATE_BTN && item->temp_widget)
+ {
+ g_signal_handlers_disconnect_by_data(item->widget, item);
+ g_signal_handlers_disconnect_by_data(item->old_parent, item);
+
+ if(GTK_IS_CONTAINER(item->old_parent) && gtk_widget_get_parent(item->widget) == item->box)
+ {
+ g_object_ref(item->widget);
+ gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(item->widget)), item->widget);
+
+ if(GTK_IS_BOX(item->old_parent))
+ {
+ if(item->packtype == GTK_PACK_START)
+ gtk_box_pack_start(GTK_BOX(item->old_parent), item->widget, item->expand, item->fill, item->padding);
+ else
+ gtk_box_pack_end(GTK_BOX(item->old_parent), item->widget, item->expand, item->fill, item->padding);
+
+ gtk_box_reorder_child(GTK_BOX(item->old_parent), item->widget, item->old_pos);
+ }
+ else if(GTK_IS_GRID(item->old_parent))
+ {
+ gtk_grid_attach(GTK_GRID(item->old_parent), item->widget, item->grid_x, item->grid_y, item->grid_w,
+ item->grid_h);
+ }
+
+ g_object_unref(item->widget);
+ }
+ // put back tooltip
+ if(GTK_IS_WIDGET(item->widget))
+ {
+ gtk_widget_set_tooltip_text(item->widget, item->tooltip);
+ gtk_widget_set_has_tooltip(item->widget, TRUE); // even if no tip, to show shortcuts
+ }
+ // put back label
+ if(DT_IS_BAUHAUS_WIDGET(item->widget))
+ dt_bauhaus_widget_set_show_extended_label(item->widget, FALSE);
+ }
+ // cleanup item
+ item->widget = NULL;
+ if(item->box) gtk_widget_destroy(item->box);
+ item->box = NULL;
+ if(item->temp_widget) gtk_widget_destroy(item->temp_widget);
+ item->temp_widget = NULL;
+ item->old_parent = NULL;
+ item->module = NULL;
+ if(item->tooltip)
+ {
+ g_free(item->tooltip);
+ item->tooltip = NULL;
+ }
+}
+
+static void _basics_hide(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(!d->vbox_basic) return;
+ gtk_widget_hide(d->vbox_basic);
+
+ for(const GList *l = d->basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ _basics_remove_widget(item);
+ }
+ gtk_widget_destroy(d->vbox_basic);
+ d->vbox_basic = NULL;
+}
+
+static gboolean _basics_goto_module(GtkWidget *w, GdkEventButton *e, gpointer user_data)
+{
+ dt_iop_module_t *module = (dt_iop_module_t *)(user_data);
+ dt_dev_modulegroups_switch(darktable.develop, module);
+ const gboolean single = dt_conf_get_bool("darkroom/ui/single_module");
+ dt_iop_gui_set_expanded(module, TRUE, single);
+ // when single_module is on, the first call with collapse_others=TRUE
+ // may toggle the module closed if all others were already closed;
+ // this second call ensures the module ends up expanded.
+ if(single)
+ dt_iop_gui_set_expanded(module, TRUE, FALSE);
+ return TRUE;
+}
+
+static gboolean _basics_on_off_label_callback(GtkWidget *widget, GdkEventButton *e, GtkToggleButton *btn)
+{
+ gtk_toggle_button_set_active(btn, !gtk_toggle_button_get_active(btn));
+ return TRUE;
+}
+
+static void _sync_visibility(GtkWidget *widget,
+ GParamSpec *pspec,
+ dt_lib_modulegroups_basic_item_t *item)
+{
+ if(widget == item->temp_widget)
+ gtk_widget_set_visible(item->widget, gtk_widget_get_visible(item->temp_widget));
+ if(widget == item->widget)
+ gtk_widget_set_visible(item->temp_widget, gtk_widget_get_visible(item->widget));
+
+ gtk_widget_set_visible(item->box, !dt_action_widget_invisible(item->temp_widget));
+}
+
+static gboolean _manage_direct_module_popup(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self);
+
+static void _basics_add_widget(dt_lib_module_t *self, dt_lib_modulegroups_basic_item_t *item, GtkWidget *w,
+ dt_lib_modulegroups_basic_item_position_t item_pos)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // if widget already exists, let's remove it and read it correctly
+ if(item->widget)
+ {
+ _basics_remove_widget(item);
+ if(item->widget) return; // we shouldn't arrive here !
+ }
+
+ // what type of ui we have ?
+ const gboolean compact_ui = !dt_conf_get_bool("plugins/darkroom/modulegroups_basics_sections_labels");
+
+ // we retrieve parents, positions, etc... so we can put the widget back in its module
+ if(item->widget_type == WIDGET_TYPE_ACTIVATE_BTN)
+ {
+ // we only show the on-off widget for compact ui. otherwise the button is included in the header
+ if(compact_ui)
+ {
+ // on-off widgets
+ item->widget = GTK_WIDGET(item->module->off);
+ item->tooltip = gtk_widget_get_tooltip_text(item->widget); // no need to copy, returns a newly-alloced string
+
+ // create new basic widget
+ item->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(item->box, "basics-widget");
+
+ // we create a new button linked with the real one
+ // because it create too much pb to remove the button from the expander
+ GtkWidget *btn = dt_iop_gui_header_button(item->module,
+ dtgtk_cairo_paint_switch,
+ DT_ACTION_ELEMENT_ENABLE,
+ item->box);
+ GtkWidget *evb = gtk_event_box_new();
+ GtkWidget *lb = gtk_label_new(item->module->name());
+ gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
+ gtk_widget_set_name(lb, "basics-iop_name");
+ gtk_container_add(GTK_CONTAINER(evb), lb);
+ g_signal_connect(G_OBJECT(evb), "button-press-event", G_CALLBACK(_basics_on_off_label_callback), btn);
+ gtk_box_pack_start(GTK_BOX(item->box), evb, FALSE, TRUE, 0);
+
+ // disable widget if needed (multiinstance)
+ if(dt_iop_count_instances(item->module->so) > 1)
+ {
+ gtk_widget_set_sensitive(evb, FALSE);
+ gtk_widget_set_sensitive(btn, FALSE);
+ gtk_widget_set_tooltip_text(
+ lb, _("this quick access widget is disabled as there are multiple instances "
+ "of this module present. Please use the full module to access this widget..."));
+ gtk_widget_set_tooltip_text(
+ btn, _("this quick access widget is disabled as there are multiple instances "
+ "of this module present. Please use the full module to access this widget..."));
+ }
+ else
+ {
+ GtkWidget *orig_label = gtk_widget_get_parent(item->module->label);
+ gchar *tooltip = gtk_widget_get_tooltip_text(orig_label);
+ gtk_widget_set_tooltip_text(lb, tooltip);
+ gtk_widget_set_tooltip_text(btn, tooltip);
+ g_free(tooltip);
+ }
+
+ gtk_widget_show_all(item->box);
+ }
+ }
+ else
+ {
+ // classic widgets (sliders and combobox)
+ if(!w || !GTK_IS_WIDGET(w)) return;
+
+ if(GTK_IS_BOX(gtk_widget_get_parent(w)))
+ {
+ item->widget = w;
+ item->old_parent = gtk_widget_get_parent(item->widget);
+ // we retrieve current positions, etc...
+ gtk_box_query_child_packing(GTK_BOX(item->old_parent), item->widget, &item->expand, &item->fill,
+ &item->padding, &item->packtype);
+ gtk_container_child_get(GTK_CONTAINER(item->old_parent), item->widget, "position", &item->old_pos, NULL);
+ }
+ else if(GTK_IS_GRID(gtk_widget_get_parent(w)))
+ {
+ item->widget = w;
+ item->old_parent = gtk_widget_get_parent(item->widget);
+
+ gtk_container_child_get(GTK_CONTAINER(item->old_parent), item->widget,
+ "left-attach", &item->grid_x, "top-attach", &item->grid_y,
+ "width", &item->grid_w, "height", &item->grid_h, NULL);
+ }
+ else
+ {
+ // we don't allow other parents at the moment
+ return;
+ }
+
+ // save old tooltip
+ item->tooltip = gtk_widget_get_tooltip_text(item->widget);
+
+ // create new quick access widget
+ item->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(item->box, "basics-widget");
+ gtk_widget_show(item->box);
+
+ // we reparent the iop widget here
+ g_object_ref(item->widget);
+ gtk_container_remove(GTK_CONTAINER(item->old_parent), item->widget);
+ gtk_box_pack_start(GTK_BOX(item->box), item->widget, TRUE, TRUE, 0);
+ gtk_widget_set_hexpand(item->widget, FALSE);
+ g_object_unref(item->widget);
+
+ // change the widget label to integrate section name
+ if(DT_IS_BAUHAUS_WIDGET(w))
+ {
+ dt_bauhaus_widget_set_show_extended_label(item->widget, TRUE);
+ item->module = dt_bauhaus_widget_get_module(item->widget);
+ }
+
+ // we put the temporary widget at the place of the real widget in the module
+ // this avoid order mismatch when putting back the real widget
+ item->temp_widget = gtk_label_new("temp widget");
+ if(GTK_IS_CONTAINER(item->old_parent))
+ {
+ if(GTK_IS_BOX(item->old_parent))
+ {
+ if(item->packtype == GTK_PACK_START)
+ gtk_box_pack_start(GTK_BOX(item->old_parent), item->temp_widget, item->expand, item->fill, item->padding);
+ else
+ gtk_box_pack_end(GTK_BOX(item->old_parent), item->temp_widget, item->expand, item->fill, item->padding);
+
+ gtk_box_reorder_child(GTK_BOX(item->old_parent), item->temp_widget, item->old_pos);
+ }
+ else if(GTK_IS_GRID(item->old_parent))
+ {
+ gtk_grid_attach(GTK_GRID(item->old_parent), item->temp_widget, item->grid_x, item->grid_y, item->grid_w,
+ item->grid_h);
+ }
+ }
+
+ gchar *txt = g_strdup_printf("%s (%s)\n\n%s%s%s", item->widget_name, item->module->name(),
+ item->tooltip ? item->tooltip : "", item->tooltip ? "\n\n" : "",
+ _("(some features may only be available in the full module interface)"));
+ gtk_widget_set_tooltip_text(item->widget, txt);
+ g_free(txt);
+
+ g_signal_connect(item->widget , "notify::visible", G_CALLBACK(_sync_visibility), item);
+ g_signal_connect(item->old_parent , "notify::visible", G_CALLBACK(_sync_visibility), item);
+ g_signal_connect(item->temp_widget , "notify::visible", G_CALLBACK(_sync_visibility), item);
+ g_signal_connect(G_OBJECT(item->temp_widget), "destroy", G_CALLBACK(gtk_widget_destroyed), &item->temp_widget);
+ g_signal_connect_swapped(G_OBJECT(item->temp_widget), "destroy", G_CALLBACK(_basics_remove_widget), item);
+
+ _sync_visibility(item->widget, NULL, item);
+ }
+
+ // if it's the first widget of a module, we need to create the module box structure
+ if(item_pos != NORMAL)
+ {
+ // we create the module header box
+ GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *evb = gtk_event_box_new();
+ gtk_container_add(GTK_CONTAINER(evb), header_box);
+ gtk_widget_show_all(evb);
+ g_object_set_data(G_OBJECT(evb), "module", item->module->so);
+ g_signal_connect(evb, "button-press-event", G_CALLBACK(_manage_direct_module_popup), self);
+ gtk_widget_set_name(header_box, "basics-header-box");
+ dt_gui_add_class(header_box, "dt_big_btn_canvas");
+ gtk_box_pack_start(GTK_BOX(d->vbox_basic), evb, FALSE, FALSE, 0);
+
+ // we create the module box structure
+ GtkWidget *hbox_basic = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hbox_basic, "basics-module-hbox");
+ gtk_box_pack_start(GTK_BOX(d->vbox_basic), hbox_basic, TRUE, TRUE, 0);
+ d->mod_vbox_basic = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(hbox_basic), d->mod_vbox_basic, TRUE, TRUE, 0);
+ gtk_widget_show_all(hbox_basic);
+
+ // we create the link to the full iop
+ GtkWidget *wbt = dtgtk_button_new(dtgtk_cairo_paint_link, 0, NULL);
+ gtk_widget_show(wbt);
+ gchar *tt = g_strdup_printf(_("go to the full version of the %s module"), item->module->name());
+ gtk_widget_set_tooltip_text(wbt, tt);
+ gtk_widget_set_name(wbt, "basics-link");
+ gtk_widget_set_valign(wbt, GTK_ALIGN_CENTER);
+ g_free(tt);
+ g_signal_connect(G_OBJECT(wbt), "button-press-event", G_CALLBACK(_basics_goto_module), item->module);
+ gtk_box_pack_end(GTK_BOX(compact_ui ? hbox_basic : header_box), wbt, FALSE, FALSE, 0);
+
+ // we create a button to open the presets menu
+ GtkWidget *pbt = dt_iop_gui_header_button(item->module,
+ dtgtk_cairo_paint_presets,
+ DT_ACTION_ELEMENT_PRESETS,
+ compact_ui ? hbox_basic : header_box);
+ gtk_widget_set_name(pbt, "quick-presets");
+ gtk_widget_set_valign(pbt, GTK_ALIGN_CENTER);
+
+ // we create a button to reset the module
+ GtkWidget *rbt = dt_iop_gui_header_button(item->module,
+ dtgtk_cairo_paint_reset,
+ DT_ACTION_ELEMENT_RESET,
+ compact_ui ? hbox_basic : header_box);
+ gtk_widget_set_name(rbt, "quick-reset");
+ gtk_widget_set_valign(rbt, GTK_ALIGN_CENTER);
+
+ if(!compact_ui)
+ {
+ // we add the on-off button
+ GtkWidget *btn = dt_iop_gui_header_button(item->module,
+ dtgtk_cairo_paint_switch,
+ DT_ACTION_ELEMENT_ENABLE,
+ header_box);
+
+ gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
+ dt_gui_add_class(btn, "dt_transparent_background");
+ // we add to the module header the section label and the link to the full iop
+ GtkWidget *sect = dt_ui_section_label_new(item->module->name());
+ gtk_label_set_xalign(GTK_LABEL(sect), 0.5); // we center the module name
+ gtk_widget_show(sect);
+ gtk_box_pack_start(GTK_BOX(header_box), sect, TRUE, TRUE, 0);
+ }
+ else if(item_pos == FIRST_MODULE)
+ // if there is no label, we handle separately in css the first module header
+ gtk_widget_set_name(header_box, "basics-header-box-first");
+ }
+
+ if(item->box) gtk_box_pack_start(GTK_BOX(d->mod_vbox_basic), item->box, FALSE, FALSE, 0);
+}
+
+static gchar *_action_id(dt_action_t *action)
+{
+ if(action->type != DT_ACTION_TYPE_IOP && action->owner)
+ {
+ gchar *owner_id = _action_id(action->owner);
+ gchar *combined_id = g_strdup_printf("%s/%s", owner_id, action->id);
+ g_free(owner_id);
+ return combined_id;
+ }
+ else
+ return g_strdup(action->id);
+}
+
+static dt_lib_modulegroups_basic_item_position_t
+_basics_add_items_from_module_widget(dt_lib_module_t *self, dt_iop_module_t *module, GtkWidget *w,
+ dt_lib_modulegroups_basic_item_position_t item_pos)
+{
+ if(!w) return item_pos;
+ dt_lib_modulegroups_t *d = self->data;
+
+ // search for a corresponding basic item
+ dt_action_t *ac = module->so->actions.target;
+ while(ac)
+ {
+ if(ac->type >= DT_ACTION_TYPE_WIDGET && ac->target == w)
+ {
+ gchar *action_id = _action_id(ac);
+
+ for(const GList *l = d->basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ if(!item->module && g_strcmp0(item->module_op, module->op) == 0
+ && item->widget_type != WIDGET_TYPE_ACTIVATE_BTN)
+ {
+ if(!strcmp(item->id, action_id))
+ {
+ item->module = module;
+ _basics_add_widget(self, item, ac->target, item_pos);
+ // we have found and added the widget. no need to go further
+ g_free(action_id);
+ return NORMAL;
+ }
+ }
+ }
+ g_free(action_id);
+ }
+
+ if(ac->type == DT_ACTION_TYPE_SECTION)
+ ac = ac->target;
+ else if(!ac->next && ac->owner->type == DT_ACTION_TYPE_SECTION)
+ ac = ac->owner->next;
+ else
+ ac = ac->next;
+ }
+
+ // if w is a container, test all subwidgets
+ if(GTK_IS_CONTAINER(w))
+ {
+ GList *ll = gtk_container_get_children(GTK_CONTAINER(w));
+ for(const GList *l = ll; l; l = g_list_next(l))
+ {
+ item_pos = _basics_add_items_from_module_widget(self, module, l->data, item_pos);
+ }
+ g_list_free(ll);
+ }
+
+ return item_pos;
+}
+
+static void _basics_show(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ if(d->vbox_basic && gtk_widget_get_visible(d->vbox_basic)) return;
+
+ if(!d->vbox_basic)
+ {
+ d->vbox_basic = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ dt_ui_container_add_widget(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER, d->vbox_basic);
+ }
+ if(dt_conf_get_bool("plugins/darkroom/modulegroups_basics_sections_labels"))
+ gtk_widget_set_name(d->vbox_basic, "basics-box-labels");
+ else
+ gtk_widget_set_name(d->vbox_basic, "basics-box");
+ dt_gui_add_class(d->vbox_basic,"dt_plugin_ui");
+
+ dt_lib_modulegroups_basic_item_position_t item_pos = FIRST_MODULE;
+ for(GList *modules = g_list_last(darktable.develop->iop); modules; modules = g_list_previous(modules))
+ {
+ dt_iop_module_t *module = modules->data;
+
+ // we record if it's a new module or not to set css class and box structure
+ if(item_pos != FIRST_MODULE) item_pos = NEW_MODULE;
+
+ if(!dt_iop_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED) && module->iop_order != INT_MAX)
+ {
+ // first, we add on-off buttons if any
+ for(const GList *l = d->basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ if(!item->module && g_strcmp0(item->module_op, module->op) == 0)
+ {
+ if(item->widget_type == WIDGET_TYPE_ACTIVATE_BTN)
+ {
+ item->module = module;
+ _basics_add_widget(self, item, NULL, item_pos);
+ item_pos = NORMAL;
+ }
+ }
+ }
+
+ // for the other items, we want them in same order as the module gui
+ _basics_add_items_from_module_widget(self, module, module->widget, item_pos);
+ }
+ }
+
+ gtk_widget_show(d->vbox_basic);
+}
+
+static uint32_t _lib_modulegroups_get_activated(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we get the current group and verify that it is effectively activated
+ // this can not be the case if we are in search mode
+ GtkWidget *bt = _buttons_get_from_pos(self, d->current);
+ if(bt && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(bt))) return d->current;
+ return DT_MODULEGROUP_NONE;
+}
+
+static gboolean _is_module_in_history(dt_iop_module_t *module)
+{
+ for(const GList *hists = darktable.develop->history; hists; hists = g_list_next(hists))
+ {
+ dt_dev_history_item_t *hist = hists->data;
+ if(hist->module == module) return TRUE;
+ }
+ return FALSE;
+}
+
+static void _lib_modulegroups_update_iop_visibility(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we hide eventual basic panel
+ if(d->current == DT_MODULEGROUP_BASICS && !d->basics_show) d->current = DT_MODULEGROUP_ACTIVE_PIPE;
+ _basics_hide(self);
+
+ // if we have a module to force, set d-current to active pipe
+ if(d->current == DT_MODULEGROUP_INVALID) d->current = DT_MODULEGROUP_ACTIVE_PIPE;
+
+ const gchar *text_entered = (gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
+ ? gtk_entry_get_text(GTK_ENTRY(d->text_entry))
+ : NULL;
+
+ dt_print(DT_DEBUG_IOPORDER, "[lib_modulegroups_update_iop_visibility] modulegroups");
+
+ // update basic button selection too
+ ++darktable.gui->reset;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basic_btn), d->current == DT_MODULEGROUP_BASICS);
+
+ /* only show module group as selected if not currently searching */
+ if((d->show_search || d->force_show_module) && d->current != DT_MODULEGROUP_NONE)
+ {
+ GtkWidget *bt = _buttons_get_from_pos(self, d->current);
+ if(bt)
+ {
+ /* toggle button visibility without executing callback */
+ if((text_entered && text_entered[0] != '\0') || d->force_show_module)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), FALSE);
+ else
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), TRUE);
+ }
+ }
+ --darktable.gui->reset;
+
+ // hide deprecated message. it will be shown after if needed
+ gtk_widget_set_visible(d->deprecated, FALSE);
+
+ for(const GList *modules = darktable.develop->iop; modules; modules = g_list_next(modules))
+ {
+ /*
+ * iterate over iop modules and do various test to
+ * detect if the modules should be shown or not.
+ */
+ dt_iop_module_t *module = modules->data;
+ GtkWidget *w = module->expander;
+
+ if(module->enabled)
+ dt_print(DT_DEBUG_IOPORDER, "%20s %d%s",
+ module->op, module->iop_order, (dt_iop_is_hidden(module)) ? ", hidden" : "");
+
+ /* skip modules without an gui */
+ if(dt_iop_is_hidden(module)) continue;
+
+ // do not show non-active modules
+ // we don't want the user to mess with those
+ if(module->iop_order == INT_MAX)
+ {
+ if(darktable.develop->gui_module == module) dt_iop_request_focus(NULL);
+ if(w) gtk_widget_hide(w);
+ continue;
+ }
+
+ // if we have a module that should be forced shown
+ if(d->force_show_module)
+ {
+ if(d->force_show_module == module && w)
+ gtk_widget_show(w);
+ else
+ gtk_widget_hide(w);
+
+ continue;
+ }
+
+ // if there's some search text show matching modules only
+ if(text_entered && text_entered[0] != '\0')
+ {
+ /* don't show deprecated ones unless they are enabled */
+ if(module->flags() & IOP_FLAGS_DEPRECATED && !(module->enabled))
+ {
+ if(darktable.develop->gui_module == module) dt_iop_request_focus(NULL);
+ if(w) gtk_widget_hide(w);
+ }
+ else
+ {
+ const int is_match = (g_strstr_len(g_utf8_casefold(dt_iop_get_localized_name(module->op), -1), -1,
+ g_utf8_casefold(text_entered, -1))
+ != NULL) ||
+ (g_strstr_len(g_utf8_casefold(dt_iop_get_localized_aliases(module->op), -1), -1,
+ g_utf8_casefold(text_entered, -1))
+ != NULL) ||
+ (g_strstr_len(g_utf8_casefold(module->multi_name, -1), -1,
+ g_utf8_casefold(text_entered, -1))
+ != NULL);
+
+
+ if(is_match)
+ gtk_widget_show(w);
+ else
+ gtk_widget_hide(w);
+ }
+ continue;
+ }
+
+ /* lets show/hide modules dependent on current group*/
+ const gboolean show_deprecated =
+ dt_conf_is_equal("plugins/darkroom/modulegroups_preset", _(DEPRECATED_PRESET_NAME));
+ gboolean show_module = TRUE;
+ switch(d->current)
+ {
+ case DT_MODULEGROUP_BASICS:
+ {
+ show_module = FALSE;
+ }
+ break;
+
+ case DT_MODULEGROUP_ACTIVE_PIPE:
+ {
+ if(d->full_active)
+ show_module = _is_module_in_history(module);
+ else
+ show_module = module->enabled;
+ }
+ break;
+
+ case DT_MODULEGROUP_NONE:
+ {
+ /* show all except hidden ones */
+ show_module = (((!(module->flags() & IOP_FLAGS_DEPRECATED) || show_deprecated)
+ && _lib_modulegroups_test_visible(self, module->op))
+ || module->enabled);
+ }
+ break;
+
+ default:
+ {
+ // show deprecated module in specific group deprecated
+ gtk_widget_set_visible(d->deprecated,
+ show_deprecated || d->force_deprecated_message);
+
+ show_module = (_lib_modulegroups_test_internal(self, d->current, module)
+ && (!(module->flags() & IOP_FLAGS_DEPRECATED)
+ || module->enabled
+ || show_deprecated));
+ }
+ }
+
+ if(show_module)
+ {
+ if(darktable.develop->gui_module == module && !module->expanded) dt_iop_request_focus(NULL);
+ if(w) gtk_widget_show(w);
+ }
+ else
+ {
+ if(darktable.develop->gui_module == module) dt_iop_request_focus(NULL);
+ if(w) gtk_widget_hide(w);
+ }
+
+ }
+
+ // we show eventual basic panel but only if no text in the search box
+ if(d->current == DT_MODULEGROUP_BASICS && !(text_entered && text_entered[0] != '\0')) _basics_show(self);
+}
+
+static void _lib_modulegroups_toggle(GtkWidget *button, dt_lib_module_t *self)
+{
+ if(darktable.gui->reset) return;
+ dt_lib_modulegroups_t *d = self->data;
+ const gchar *text_entered = (gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
+ ? gtk_entry_get_text(GTK_ENTRY(d->text_entry))
+ : NULL;
+
+ ++darktable.gui->reset;
+
+ /* deactivate all buttons */
+ int gid = 0;
+ const int ngroups = g_list_length(d->groups);
+ for(int k = 0; k <= ngroups; k++)
+ {
+ const GtkWidget *bt = _buttons_get_from_pos(self, k);
+ /* store toggled modulegroup */
+ if(bt == button)
+ gid = k;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), FALSE);
+ }
+ if(button == d->basic_btn) gid = DT_MODULEGROUP_BASICS;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basic_btn), FALSE);
+
+ if(d->current == DT_MODULEGROUP_BASICS) dt_iop_request_focus(NULL);
+
+ /* only deselect button if not currently searching else re-enable module */
+ if(d->current == gid && gid != DT_MODULEGROUP_BASICS && !(text_entered && text_entered[0] != '\0'))
+ d->current = DT_MODULEGROUP_NONE;
+ else
+ {
+ d->current = gid;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_buttons_get_from_pos(self, gid)), TRUE);
+ }
+
+ /* clear search text */
+ if(gtk_widget_is_visible(GTK_WIDGET(d->hbox_search_box)))
+ gtk_entry_set_text(GTK_ENTRY(d->text_entry), "");
+
+ --darktable.gui->reset;
+
+ /* update visibility */
+ d->force_show_module = NULL;
+ _lib_modulegroups_update_iop_visibility(self);
+}
+
+typedef struct _set_gui_thread_t
+{
+ dt_lib_module_t *self;
+ uint32_t group;
+} _set_gui_thread_t;
+
+static gboolean _lib_modulegroups_set_gui_thread(gpointer user_data)
+{
+ _set_gui_thread_t *params = (_set_gui_thread_t *)user_data;
+
+ /* set current group and update visibility */
+ GtkWidget *bt = _buttons_get_from_pos(params->self, params->group);
+ if(bt) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bt), TRUE);
+ _lib_modulegroups_update_iop_visibility(params->self);
+
+ free(params);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean _lib_modulegroups_upd_gui_thread(gpointer user_data)
+{
+ _set_gui_thread_t *params = (_set_gui_thread_t *)user_data;
+
+ _lib_modulegroups_update_iop_visibility(params->self);
+
+ free(params);
+ return G_SOURCE_REMOVE;
+}
+
+/* this is a proxy function so it might be called from another thread */
+static void _lib_modulegroups_set(dt_lib_module_t *self, uint32_t group)
+{
+ _set_gui_thread_t *params = malloc(sizeof(_set_gui_thread_t));
+ if(!params) return;
+ params->self = self;
+ params->group = group;
+ g_main_context_invoke(NULL, _lib_modulegroups_set_gui_thread, params);
+}
+
+/* this is a proxy function so it might be called from another thread */
+static void _lib_modulegroups_update_visibility_proxy(dt_lib_module_t *self)
+{
+ _set_gui_thread_t *params = malloc(sizeof(_set_gui_thread_t));
+ if(!params) return;
+ params->self = self;
+ g_main_context_invoke(NULL, _lib_modulegroups_upd_gui_thread, params);
+}
+
+static void _lib_modulegroups_switch_group(dt_lib_module_t *self, dt_iop_module_t *module)
+{
+ /* lets find the group which is not active pipe */
+ dt_lib_modulegroups_t *d = self->data;
+ const int ngroups = g_list_length(d->groups);
+ for(int k = 1; k <= ngroups; k++)
+ {
+ if(_lib_modulegroups_test(self, k, module))
+ {
+ d->force_show_module = NULL;
+ _lib_modulegroups_set(self, k);
+ return;
+ }
+ }
+ // if we arrive here, that means the module is not part of any group
+ // so we force it to be shown outside any group
+ d->force_show_module = module;
+ d->current = DT_MODULEGROUP_INVALID;
+ _lib_modulegroups_set(self, DT_MODULEGROUP_INVALID);
+}
+
+static uint32_t _lib_modulegroups_get(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ return d->current;
+}
+
+static dt_lib_modulegroup_iop_visibility_type_t _preset_retrieve_old_search_pref(gchar **ret)
+{
+ // show the search box ?
+ const char *show_text_entry = dt_conf_get_string_const("plugins/darkroom/search_iop_by_text");
+ dt_lib_modulegroup_iop_visibility_type_t val = DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE;
+
+ if(strcmp(show_text_entry, "show search text") == 0)
+ {
+ // we only show the search box. no groups
+ dt_util_str_cat(&*ret, "1ꬹ1");
+ val = DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE;
+ }
+ else if(strcmp(show_text_entry, "show groups") == 0)
+ {
+ // we don't show the search box
+ dt_util_str_cat(&*ret, "0");
+ val = DT_MODULEGROUP_SEARCH_IOP_GROUPS_VISIBLE;
+ }
+ else
+ {
+ // we show both
+ dt_util_str_cat(&*ret, "1");
+ val = DT_MODULEGROUP_SEARCH_IOP_TEXT_GROUPS_VISIBLE;
+ }
+ return val;
+}
+
+/* presets syntax :
+ Layout presets are saved as string consisting of blocs separated by ꬹ
+ OPTIONSꬹBLOC_0ꬹBLOC_1ꬹBLOC_2....
+ OPTION : show_search(0-1) | active == history (0-1)
+ BLOC_0 : reserved for future use. Always 1
+ BLOC_1.... : blocs describing each group, contains :
+ name|icon_name||iop_name_0|iop_name_1....
+*/
+
+static gchar *_preset_retrieve_old_layout_updated()
+{
+ gchar *ret = NULL;
+
+ // show the search box ?
+ if(_preset_retrieve_old_search_pref(&ret) == DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE) return ret;
+
+ // layout with "new" 3 groups
+ for(int i = 0; i < 4; i++)
+ {
+ // group name and icon
+ if(i == 0)
+ {
+ dt_util_str_cat(&ret,
+ "1|0ꬹ1|||%s",
+ "exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast"
+ "|colorbalancergb/global vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation"
+ "|ashift/rotation|denoiseprofile|lens|bilat");
+ dt_util_str_cat(&ret, "ꬹfavorites|favorites|");
+ }
+ else if(i == 1)
+ dt_util_str_cat(&ret, "ꬹtechnical|technical|");
+ else if(i == 2)
+ dt_util_str_cat(&ret, "ꬹgrading|grading|");
+ else if(i == 3)
+ dt_util_str_cat(&ret, "ꬹeffects|effect|");
+
+ // list of modules
+ for(const GList *modules = darktable.iop; modules; modules = g_list_next(modules))
+ {
+ dt_iop_module_so_t *module = modules->data;
+
+ if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
+ {
+ // get previous visibility values
+ const int group = module->default_group();
+ gchar *key = g_strdup_printf("plugins/darkroom/%s/visible", module->op);
+ const gboolean visi = dt_conf_get_bool(key);
+ g_free(key);
+ key = g_strdup_printf("plugins/darkroom/%s/favorite", module->op);
+ const gboolean fav = dt_conf_get_bool(key);
+ g_free(key);
+
+ if((i == 0 && fav && visi) || (i == 1 && (group & IOP_GROUP_TECHNICAL) && visi)
+ || (i == 2 && (group & IOP_GROUP_GRADING) && visi) || (i == 3 && (group & IOP_GROUP_EFFECTS) && visi))
+ {
+ dt_util_str_cat(&ret, "|%s", module->op);
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+static gchar *_preset_retrieve_old_layout(const char *list, const char *list_fav)
+{
+ gchar *ret = NULL;
+
+ // show the search box ?
+ if(_preset_retrieve_old_search_pref(&ret) == DT_MODULEGROUP_SEARCH_IOP_TEXT_VISIBLE) return ret;
+
+ // layout with "old" 5 groups
+ for(int i = 0; i < 6; i++)
+ {
+ // group name and icon
+ if(i == 0)
+ {
+ // we don't have to care about "modern" workflow for temperature as it's more recent than this layout
+ dt_util_str_cat(&ret,
+ "1|0ꬹ1|||%s",
+ "exposure/exposure|temperature/temperature|temperature/tint|colorbalancergb/contrast"
+ "|colorbalancergb/global vibrance|colorbalancergb/global chroma|colorbalancergb/global saturation"
+ "|ashift/rotation|denoiseprofile|lens|bilat");
+ dt_util_str_cat(&ret, "ꬹfavorites|favorites|");
+ }
+ else if(i == 1)
+ dt_util_str_cat(&ret, "ꬹbase|basic|");
+ else if(i == 2)
+ dt_util_str_cat(&ret, "ꬹtone|tone|");
+ else if(i == 3)
+ dt_util_str_cat(&ret, "ꬹcolor|color|");
+ else if(i == 4)
+ dt_util_str_cat(&ret, "ꬹcorrect|correct|");
+ else if(i == 5)
+ dt_util_str_cat(&ret, "ꬹeffect|effect|");
+
+ // list of modules
+ for(const GList *modules = darktable.iop; modules; modules = g_list_next(modules))
+ {
+ dt_iop_module_so_t *module = modules->data;
+
+ if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
+ {
+ gchar *search = g_strdup_printf("|%s|", module->op);
+ gchar *key;
+
+ // get previous visibility values
+ int group = -1;
+ if(i > 0 && list)
+ {
+ // we retrieve the group from hardcoded one
+ const int gr = module->default_group();
+ if(gr & IOP_GROUP_BASIC)
+ group = 1;
+ else if(gr & IOP_GROUP_TONE)
+ group = 2;
+ else if(gr & IOP_GROUP_COLOR)
+ group = 3;
+ else if(gr & IOP_GROUP_CORRECT)
+ group = 4;
+ else if(gr & IOP_GROUP_EFFECT)
+ group = 5;
+ }
+ else if(i > 0)
+ {
+ key = g_strdup_printf("plugins/darkroom/%s/modulegroup", module->op);
+ group = dt_conf_get_int(key);
+ g_free(key);
+ }
+
+ gboolean visi = FALSE;
+ if(list)
+ visi = (strstr(list, search) != NULL);
+ else
+ {
+ key = g_strdup_printf("plugins/darkroom/%s/visible", module->op);
+ visi = dt_conf_get_bool(key);
+ g_free(key);
+ }
+
+ gboolean fav = FALSE;
+ if(i == 0 && list_fav)
+ fav = (strstr(list_fav, search) != NULL);
+ else if(i == 0)
+ {
+ key = g_strdup_printf("plugins/darkroom/%s/favorite", module->op);
+ fav = dt_conf_get_bool(key);
+ g_free(key);
+ }
+
+ if((i == 0 && fav && visi) || (i == group && visi))
+ {
+ dt_util_str_cat(&ret, "|%s", module->op);
+ }
+
+ g_free(search);
+ }
+ }
+ }
+ return ret;
+}
+
+static void _preset_retrieve_old_presets(dt_lib_module_t *self)
+{
+ // we retrieve old modulelist presets
+ sqlite3_stmt *stmt;
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT name, op_params"
+ " FROM data.presets"
+ " WHERE operation = 'modulelist' AND op_version = 1 AND writeprotect = 0",
+ -1, &stmt, NULL);
+ // clang-format on
+
+ while(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ const char *pname = (char *)sqlite3_column_text(stmt, 0);
+ const char *p = (char *)sqlite3_column_blob(stmt, 1);
+ const int size = sqlite3_column_bytes(stmt, 1);
+
+ gchar *list = NULL;
+ gchar *fav = NULL;
+ int pos = 0;
+ while(pos < size)
+ {
+ const char *op = p + pos;
+ const int op_len = strlen(op);
+ dt_iop_module_state_t state = p[pos + op_len + 1];
+
+ if(state == IOP_STATE_ACTIVE)
+ dt_util_str_cat(&list, "|%s", op);
+ else if(state == IOP_STATE_FAVORITE)
+ {
+ dt_util_str_cat(&fav, "|%s", op);
+ dt_util_str_cat(&list, "|%s", op);
+ }
+ pos += op_len + 2;
+ }
+ dt_util_str_cat(&list, "|");
+ dt_util_str_cat(&fav, "|");
+
+ gchar *tx = _preset_retrieve_old_layout(list, fav);
+ dt_lib_presets_add(pname, self->plugin_name, self->version(), tx, strlen(tx), FALSE, 0);
+ g_free(tx);
+ g_free(list);
+ g_free(fav);
+ }
+ sqlite3_finalize(stmt);
+
+ // and we remove all existing modulelist presets
+ DT_DEBUG_SQLITE3_EXEC(dt_database_get(darktable.db),
+ "DELETE FROM data.presets"
+ " WHERE operation = 'modulelist' AND op_version = 1",
+ NULL, NULL, NULL);
+}
+
+static gchar *_preset_to_string(dt_lib_module_t *self, gboolean edition)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ gchar *res = NULL;
+ const gboolean show_search = edition ? d->edit_show_search : d->show_search;
+ const gboolean full_active = edition ? d->edit_full_active : d->full_active;
+ dt_util_str_cat(&res, "%d|%d", show_search ? 1 : 0, full_active ? 1 : 0);
+
+ const gboolean basics_show = edition ? d->edit_basics_show : d->basics_show;
+ GList *basics = edition ? d->edit_basics : d->basics;
+ GList *groups = edition ? d->edit_groups : d->groups;
+
+ // basics widgets
+ dt_util_str_cat(&res, "ꬹ%d||", basics_show ? 1 : 0);
+ for(const GList *l = basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ dt_util_str_cat(&res, "|%s", item->id);
+ }
+
+ for(const GList *l = groups; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_group_t *g = l->data;
+ dt_util_str_cat(&res, "ꬹ%s|%s|", g->name, g->icon);
+ for(const GList *ll = g->modules; ll; ll = g_list_next(ll))
+ {
+ gchar *m = (gchar *)ll->data;
+ dt_util_str_cat(&res, "|%s", m);
+ }
+ }
+
+ return res;
+}
+
+static void _preset_from_string(dt_lib_module_t *self, gchar *txt, gboolean edition)
+{
+ if(!txt) return;
+ dt_lib_modulegroups_t *d = self->data;
+
+ gboolean show_search = TRUE;
+ gboolean full_active = FALSE;
+
+ gchar **gr = g_strsplit(txt, "ꬹ", -1);
+
+ // read the general options
+ if(g_strv_length(gr) > 0)
+ {
+ gchar **gr2 = g_strsplit(gr[0], "|", -1);
+ // do we show the search bar
+ if(!g_strcmp0(gr2[0], "0")) show_search = FALSE;
+ // do we show all history module in active group
+ if(g_strv_length(gr2) > 1 && (g_strcmp0(gr2[1], "1") == 0)) full_active = TRUE;
+
+ g_strfreev(gr2);
+ }
+
+ // read the basics widgets
+ if(g_strv_length(gr) > 1)
+ {
+ if(gr[1])
+ {
+ gchar **gr2 = g_strsplit(gr[1], "|", -1);
+ gboolean basics_show = FALSE;
+ if(g_strv_length(gr2) > 3 && (g_strcmp0(gr2[0], "1") == 0)) basics_show = TRUE;
+ if(edition)
+ d->edit_basics_show = basics_show;
+ else
+ d->basics_show = basics_show;
+
+ for(int j = 3; j < g_strv_length(gr2); j++)
+ {
+ dt_lib_modulegroups_basic_item_t *item = g_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
+ if(!item)
+ continue;
+ item->id = g_strdup(gr2[j]);
+ _basics_init_item(item);
+
+ if(edition)
+ d->edit_basics = g_list_append(d->edit_basics, item);
+ else
+ d->basics = g_list_append(d->basics, item);
+ }
+ g_strfreev(gr2);
+ }
+ }
+
+ GList *res = NULL;
+ // read the groups
+ for(int i = 2; i < g_strv_length(gr); i++)
+ {
+ gchar *tx = gr[i];
+ if(tx)
+ {
+ gchar **gr2 = g_strsplit(tx, "|", -1);
+ const int nb = g_strv_length(gr2);
+ if(nb > 2)
+ {
+ dt_lib_modulegroups_group_t *group = g_malloc0(sizeof(dt_lib_modulegroups_group_t));
+ if(group)
+ {
+ group->name = g_strdup(gr2[0]);
+ group->icon = g_strdup(gr2[1]);
+ // gr2[2] is reserved for eventual future use
+ for(int j = 3; j < nb; j++)
+ {
+ group->modules = g_list_append(group->modules, g_strdup(gr2[j]));
+ }
+ res = g_list_prepend(res, group);
+ }
+ }
+ g_strfreev(gr2);
+ }
+ }
+ g_strfreev(gr);
+ res = g_list_reverse(res); // list was built in reverse order, so un-reverse it
+
+ // and we set the values
+ if(edition)
+ {
+ d->edit_show_search = show_search;
+ d->edit_full_active = full_active;
+ d->edit_groups = res;
+ }
+ else
+ {
+ d->show_search = show_search;
+ d->full_active = full_active;
+ d->groups = res;
+ }
+}
+
+// start no quick access
+#define SNQA() \
+ { \
+ g_free(tx); \
+ tx = g_strdup("1|0ꬹ0||"); \
+ }
+
+// start quick access
+#define SQA(is_scene_referred) \
+ { \
+ g_free(tx); \
+ tx = g_strdup_printf("1|0ꬹ1||"); \
+ if(is_scene_referred) \
+ { \
+ if(wf_filmic) \
+ { \
+ AM("filmicrgb/white relative exposure"); \
+ AM("filmicrgb/black relative exposure"); \
+ AM("filmicrgb/contrast"); \
+ } \
+ else if(wf_sigmoid) \
+ { \
+ AM("sigmoid/contrast"); \
+ AM("sigmoid/skew"); \
+ } \
+ else if(wf_agx) \
+ { \
+ /*AM("agx/white relative exposure");*/ \
+ /*AM("agx/black relative exposure");*/ \
+ AM("agx/auto tune levels"); \
+ AM("agx/curve/contrast"); \
+ AM("agx/curve/shoulder power"); \
+ AM("agx/curve/toe power"); \
+ AM("agx/look/saturation"); \
+ AM("agx/look/preserve hue"); \
+ } \
+ else if(wf_basecurve) \
+ { \
+ AM("basecurve/highlight gain"); \
+ AM("basecurve/shadow lift"); \
+ AM("basecurve/color look"); \
+ } \
+ AM("channelmixerrgb/temperature"); \
+ AM("channelmixerrgb/chroma"); \
+ AM("channelmixerrgb/hue"); \
+ AM("channelmixerrgb/illuminant"); \
+ AM("channelmixerrgb/F source"); \
+ AM("channelmixerrgb/LED source"); \
+ } \
+ else \
+ { \
+ AM("temperature/temperature"); \
+ AM("temperature/tint"); \
+ } \
+ AM("colorequal/page"); \
+ AM("colorequal/graph"); \
+ AM("colorequal/node placement"); \
+ AM("exposure/exposure"); \
+ if(!is_scene_referred) AM("colorbalancergb/contrast"); /* contrast is already in filmic/sigmoid */ \
+ AM("colorbalancergb/global chroma"); \
+ AM("colorbalancergb/global vibrance"); \
+ AM("colorbalancergb/global saturation"); \
+ AM("colorbalancergb/global brilliance"); \
+ AM("ashift/rotation"); \
+ AM("denoiseprofile/strength"); \
+ AM("toneequal/graph"); \
+ AM("toneequal/mask exposure compensation"); \
+ AM("toneequal/mask contrast compensation"); \
+ AM("lens"); \
+ AM("bilat/detail"); \
+ }
+
+// start module group
+#define SMG(g,n) dt_util_str_cat(&tx, "ꬹ%s|%s|", g, n)
+
+// add module
+#define AM(n) dt_util_str_cat(&tx, "|%s", n)
+
+void init_presets(dt_lib_module_t *self)
+{
+ self->pref_based_presets = TRUE;
+
+ /*
+ For the record, one can create the preset list by using the following code:
+
+ $ cat <( git grep "return.*IOP_GROUP_TONE" -- src/iop/ | cut -d':' -f1 ) \
+ <( git grep IOP_FLAGS_DEPRECATED -- src/iop/ | cut -d':' -f1 ) | \
+ grep -E -v "useless|mask_manager|gamma" | sort | uniq --unique | \
+ while read file; do BN=$(basename $(basename $file .cc) .c); \
+ echo "AM(\"${BN:0:16}\");" ; done
+ */
+
+ const gboolean is_scene_referred = dt_is_scene_referred();
+ const gboolean wf_filmic =
+ dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (filmic)");
+ const gboolean wf_sigmoid =
+ dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (sigmoid)");
+ const gboolean wf_agx =
+ dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (AgX)");
+ const gboolean wf_basecurve =
+ dt_conf_is_equal("plugins/darkroom/workflow", "scene-referred (basecurve)");
+ const gboolean wf_none =
+ dt_conf_is_equal("plugins/darkroom/workflow", "none");
+
+ // all modules
+ gchar *tx = NULL;
+
+ SQA(is_scene_referred);
+
+ SMG(C_("modulegroup", "base"), "basic");
+ AM("basecurve");
+ AM("crop");
+ AM("ashift");
+ AM("colisa");
+ AM("colorreconstruct");
+ AM("demosaic");
+ AM("exposure");
+ AM("finalscale");
+ AM("flip");
+ AM("highlights");
+ AM("negadoctor");
+ AM("overexposed");
+ AM("rawoverexposed");
+ AM("rawprepare");
+ AM("shadhi");
+ AM("temperature");
+ AM("toneequal");
+
+ SMG(C_("modulegroup", "tone"), "tone");
+ AM("agx");
+ AM("bilat");
+ AM("filmicrgb");
+ AM("levels");
+ AM("rgbcurve");
+ AM("rgblevels");
+ AM("sigmoid");
+ AM("tonecurve");
+
+ SMG(C_("modulegroup", "color"), "color");
+ AM("channelmixerrgb");
+ AM("colorbalancergb");
+ AM("colorchecker");
+ AM("colorcontrast");
+ AM("colorcorrection");
+ AM("colorin");
+ AM("colorout");
+ AM("colorzones");
+ AM("colorequal");
+ AM("lut3d");
+ AM("monochrome");
+ AM("profile");
+ AM("primaries");
+ AM("gamma");
+ AM("velvia");
+
+ SMG(C_("modulegroup", "correct"), "correct");
+ AM("atrous");
+ AM("bilateral");
+ AM("cacorrect");
+ AM("cacorrectrgb");
+ AM("denoiseprofile");
+ AM("dither");
+ AM("hazeremoval");
+ AM("hotpixels");
+ AM("lens");
+ AM("liquify");
+ AM("nlmeans");
+ AM("rasterfile");
+ AM("rawdenoise");
+ AM("retouch");
+ AM("rotatepixels");
+ AM("scalepixels");
+ AM("sharpen");
+
+ SMG(C_("modulegroup", "effect"), "effect");
+ AM("bloom");
+ AM("borders");
+ AM("colorize");
+ AM("colormapping");
+ AM("enlargecanvas");
+ AM("graduatednd");
+ AM("grain");
+ AM("highpass");
+ AM("lowlight");
+ AM("lowpass");
+ AM("overlay");
+ AM("soften");
+ AM("splittoning");
+ AM("vignette");
+ AM("watermark");
+ AM("censorize");
+ AM("blurs");
+ AM("diffuse");
+
+ dt_lib_presets_add(_("modules: all"),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+
+ // minimal / 3 tabs
+
+ SQA(is_scene_referred);
+
+ SMG(C_("modulegroup", "base"), "basic");
+ AM("ashift");
+
+ if(is_scene_referred)
+ AM("sigmoid");
+ else
+ AM("basecurve");
+
+ AM("crop");
+ AM("denoiseprofile");
+ AM("exposure");
+ AM("flip");
+ AM("lens");
+ AM("temperature");
+
+ SMG(C_("modulegroup", "grading"), "grading");
+ AM("channelmixerrgb");
+ AM("colorequal");
+ AM("graduatednd");
+ AM("rgbcurve");
+ AM("rgblevels");
+ AM("splittoning");
+
+ SMG(C_("modulegroup", "effects"), "effect");
+ AM("borders");
+ AM("monochrome");
+ AM("retouch");
+ AM("sharpen");
+ AM("vignette");
+ AM("watermark");
+
+ dt_lib_presets_add(_("workflow: beginner"),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+
+ // display referred
+ SQA(FALSE);
+
+ SMG(C_("modulegroup", "base"), "basic");
+ AM("basecurve");
+ AM("toneequal");
+ AM("crop");
+ AM("ashift");
+ AM("flip");
+ AM("exposure");
+ AM("temperature");
+ AM("rgbcurve");
+ AM("rgblevels");
+ AM("bilat");
+ AM("shadhi");
+ AM("highlights");
+
+ SMG(C_("modulegroup", "color"), "color");
+ AM("channelmixerrgb");
+ AM("colorbalancergb");
+ AM("colorcorrection");
+ AM("colorzones");
+ AM("monochrome");
+ AM("velvia");
+
+ SMG(C_("modulegroup", "correct"), "correct");
+ AM("cacorrect");
+ AM("cacorrectrgb");
+ AM("denoiseprofile");
+ AM("hazeremoval");
+ AM("hotpixels");
+ AM("lens");
+ AM("retouch");
+ AM("liquify");
+ AM("rasterfile");
+ AM("sharpen");
+ AM("nlmeans");
+
+ SMG(C_("modulegroup", "effect"), "effect");
+ AM("borders");
+ AM("enlargecanvas");
+ AM("colorize");
+ AM("graduatednd");
+ AM("grain");
+ AM("overlay");
+ AM("splittoning");
+ AM("vignette");
+ AM("watermark");
+ AM("censorize");
+
+ dt_lib_presets_add(_("workflow: display-referred"),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+
+ // scene referred
+
+ SQA(TRUE);
+
+ SMG(C_("modulegroup", "base"), "basic");
+ if(wf_filmic || wf_none)
+ AM("filmicrgb");
+ if(wf_sigmoid || wf_none)
+ AM("sigmoid");
+ if(wf_agx || wf_none)
+ AM("agx");
+ if(wf_basecurve || wf_none)
+ AM("basecurve");
+ AM("toneequal");
+ AM("crop");
+ AM("ashift");
+ AM("flip");
+ AM("exposure");
+ AM("temperature");
+ AM("bilat");
+ AM("highlights");
+
+ SMG(C_("modulegroup", "color"), "color");
+ AM("channelmixerrgb");
+ AM("colorbalancergb");
+ AM("colorequal");
+ AM("primaries");
+
+ SMG(C_("modulegroup", "correct"), "correct");
+ AM("cacorrect");
+ AM("cacorrectrgb");
+ AM("denoiseprofile");
+ AM("hazeremoval");
+ AM("hotpixels");
+ AM("lens");
+ AM("retouch");
+ AM("liquify");
+ AM("rasterfile");
+ AM("sharpen");
+ AM("nlmeans");
+
+ SMG(C_("modulegroup", "effect"), "effect");
+ AM("atrous");
+ AM("borders");
+ AM("enlargecanvas");
+ AM("graduatednd");
+ AM("grain");
+ AM("overlay");
+ AM("vignette");
+ AM("watermark");
+ AM("censorize");
+ AM("blurs");
+ AM("diffuse");
+
+ dt_lib_presets_add(_("workflow: scene-referred"),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+
+ // search only (only active modules visible)
+ SNQA();
+ dt_lib_presets_add(_("search only"),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+
+ // There is no need for the deprecated modules group now, as there have been
+ // no new module deprecations for a long time. The group is not for access
+ // to all once deprecated modules, it should only contain deprecated modules
+ // temporarily (planned for 1 year) to prepare users of these modules for
+ // the need to learn the replacement modules.
+ // We are not removing the following code, just commenting it out for possible
+ // updating if we decide to deprecate any modules again in the future.
+#if 0
+ // This is a special preset for all newly deprecated modules, so users still
+ // have a chance to access them until next release (with warning messages)
+ SNQA();
+ SMG(C_("modulegroup", "deprecated"), "basic");
+ // these modules are deprecated in 4.4 and should be removed in 4.8 (1 year later)
+ AM("levels");
+ AM("colisa");
+
+ dt_lib_presets_add(_(DEPRECATED_PRESET_NAME),
+ self->plugin_name, self->version(), tx, strlen(tx), TRUE, 0);
+#endif
+
+ g_free(tx);
+
+ // if needed, we add a new preset, based on last user config
+ if(!dt_conf_key_exists("plugins/darkroom/modulegroups_preset"))
+ {
+ tx = _preset_retrieve_old_layout(NULL, NULL);
+ dt_lib_presets_add(_("previous config"),
+ self->plugin_name, self->version(), tx, strlen(tx), FALSE, 0);
+ dt_conf_set_string("plugins/darkroom/modulegroups_preset", _("previous layout"));
+ g_free(tx);
+
+ tx = _preset_retrieve_old_layout_updated();
+ dt_lib_presets_add(_("previous config with new layout"),
+ self->plugin_name, self->version(), tx,
+ strlen(tx), FALSE, 0);
+ g_free(tx);
+ }
+ // if they exists, we retrieve old user presets from old modulelist lib
+ _preset_retrieve_old_presets(self);
+}
+
+static gchar *_presets_get_minimal(dt_lib_module_t *self)
+{
+ const gboolean is_scene_referred = dt_is_scene_referred();
+ const gboolean wf_filmic = dt_conf_is_equal("plugins/darkroom/workflow",
+ "scene-referred (filmic)");
+ const gboolean wf_sigmoid = dt_conf_is_equal("plugins/darkroom/workflow",
+ "scene-referred (sigmoid)");
+ const gboolean wf_agx = dt_conf_is_equal("plugins/darkroom/workflow",
+ "scene-referred (AgX)");
+ const gboolean wf_basecurve = dt_conf_is_equal("plugins/darkroom/workflow",
+ "scene-referred (basecurve)");
+
+ // all modules
+ gchar *tx = NULL;
+
+ SQA(is_scene_referred);
+ AM("exposure/exposure");
+ AM("colorbalancergb/contrast");
+
+ SMG(C_("modulegroup", "base"), "basic");
+ if(is_scene_referred)
+ {
+ if(wf_filmic)
+ AM("filmicrgb");
+ else if(wf_sigmoid)
+ AM("sigmoid");
+ else if(wf_agx)
+ AM("agx");
+ else if(wf_basecurve)
+ AM("basecurve");
+ }
+ else
+ AM("basecurve");
+ AM("exposure");
+ AM("colorbalancergb");
+
+ return tx;
+}
+
+#undef SNQA
+#undef SQA
+#undef SMG
+#undef AM
+
+void *legacy_params(dt_lib_module_t *self,
+ const void *const old_params,
+ const size_t old_params_size,
+ const int old_version,
+ int *new_version,
+ size_t *new_size)
+{
+ return NULL;
+}
+
+void *get_params(dt_lib_module_t *self, int *size)
+{
+ gchar *tx = _preset_to_string(self, FALSE);
+ *size = strlen(tx);
+ return tx;
+}
+
+static void _manage_editor_groups_cleanup(dt_lib_module_t *self,
+ const gboolean edition)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ GList *l = edition ? d->edit_groups : d->groups;
+
+ for(; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_group_t *gr = l->data;
+ g_free(gr->name);
+ g_free(gr->icon);
+ g_list_free_full(gr->modules, g_free);
+ }
+
+ if(edition)
+ {
+ g_list_free_full(d->edit_groups, g_free);
+ d->edit_groups = NULL;
+ }
+ else
+ {
+ g_list_free_full(d->groups, g_free);
+ d->groups = NULL;
+ _basics_hide(self);
+ }
+
+ l = edition ? d->edit_basics : d->basics;
+ for(; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ _basics_free_item(item);
+ }
+ if(edition)
+ {
+ g_list_free_full(d->edit_basics, g_free);
+ d->edit_basics = NULL;
+ }
+ else
+ {
+ g_list_free_full(d->basics, g_free);
+ d->basics = NULL;
+ }
+}
+
+static void _manage_editor_basics_remove(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ const char *id = (char *)g_object_get_data(G_OBJECT(widget), "widget_id");
+ for(GList *l = d->edit_basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+ if(g_strcmp0(item->id, id) == 0)
+ {
+ _basics_free_item(item);
+ d->edit_basics = g_list_delete_link(d->edit_basics, l);
+ gtk_widget_destroy(gtk_widget_get_parent(widget));
+ break;
+ }
+ }
+}
+
+static int _manage_editor_module_find_multi(gconstpointer a, gconstpointer b)
+{
+ // we search for a other instance of module with lower priority
+ dt_iop_module_t *ma = (dt_iop_module_t *)a;
+ dt_iop_module_t *mb = (dt_iop_module_t *)b;
+ if(g_strcmp0(ma->op, mb->op) != 0) return 1;
+ if(ma->multi_priority >= mb->multi_priority) return 0;
+ return 1;
+}
+
+static void _manage_editor_basics_update_list(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // first, we remove all existing modules
+ dt_gui_container_destroy_children(GTK_CONTAINER(d->edit_basics_box));
+
+ // and we add the ones from the list
+ for(const GList *modules = g_list_last(darktable.develop->iop);
+ modules;
+ modules = g_list_previous(modules))
+ {
+ dt_iop_module_t *module = modules->data;
+ for(const GList *l = d->edit_basics; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_basic_item_t *item = l->data;
+
+ if(g_strcmp0(module->op, item->module_op) == 0 && !dt_iop_is_hidden(module))
+ {
+ // we want to avoid showing multiple instances of the same module
+ if(module->multi_priority <= 0
+ || g_list_find_custom(darktable.develop->iop, module,
+ _manage_editor_module_find_multi) == NULL)
+ {
+ GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb, "modulegroups-iop-header");
+ gchar *lbn = g_strdup_printf("%s\n %s", module->name(), item->widget_name);
+ GtkWidget *lb = gtk_label_new(lbn);
+ gtk_label_set_ellipsize(GTK_LABEL(lb), PANGO_ELLIPSIZE_END);
+ gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
+ g_free(lbn);
+ gtk_widget_set_name(lb, "iop-panel-label");
+ gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0);
+ if(!d->edit_ro)
+ {
+ GtkWidget *btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL);
+
+ gtk_widget_set_tooltip_text(btn, _("remove this widget"));
+ g_object_set_data(G_OBJECT(btn), "widget_id", item->id);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_basics_remove), self);
+ gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0);
+ }
+ gtk_box_pack_start(GTK_BOX(d->edit_basics_box), hb, FALSE, TRUE, 0);
+ }
+ }
+ }
+ }
+
+ gtk_widget_show_all(d->edit_basics_box);
+}
+
+int set_params(dt_lib_module_t *self, const void *params, int size)
+{
+ if(!params) return 1;
+
+ // cleanup existing groups
+ _manage_editor_groups_cleanup(self, FALSE);
+
+ _preset_from_string(self, (char *)params, FALSE);
+
+ gchar *tx = g_strdup_printf("plugins/darkroom/%s/last_preset", self->plugin_name);
+
+ gchar *value = dt_conf_get_string(tx);
+ dt_conf_set_string("plugins/darkroom/modulegroups_preset", value);
+ g_free(value);
+ g_free(tx);
+
+ _buttons_update(self);
+ return 0;
+}
+
+static void _manage_editor_save(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(!d->edit_preset) return;
+
+ // get all the values
+ d->edit_show_search =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->edit_search_cb));
+ d->edit_full_active =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->edit_full_active_cb));
+ gchar *params = _preset_to_string(self, TRUE);
+
+ // update the preset in the database
+ dt_lib_presets_update(d->edit_preset,
+ self->plugin_name, self->version(), d->edit_preset, "", params,
+ strlen(params));
+ g_free(params);
+
+ // update groups
+ const char *preset = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
+ if(g_strcmp0(preset, d->edit_preset) == 0)
+ {
+ const int cur = d->current;
+ // we update the gui
+ if(!dt_lib_presets_apply(d->edit_preset, self->plugin_name, self->version()))
+ dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
+ self->plugin_name, self->version());
+
+ // and we ensure the right group is selected
+ d->current = cur;
+ _lib_modulegroups_update_iop_visibility(self);
+ }
+}
+
+static void _manage_editor_module_remove(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ const char *module = (char *)g_object_get_data(G_OBJECT(widget), "module_name");
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+
+ for(GList *l = gr->modules; l; l = g_list_next(l))
+ {
+ const char *tx = (char *)l->data;
+ if(g_strcmp0(tx, module) == 0)
+ {
+ g_free(l->data);
+ gr->modules = g_list_delete_link(gr->modules, l);
+ gtk_widget_destroy(gtk_widget_get_parent(widget));
+ break;
+ }
+ }
+}
+
+static void _manage_editor_module_update_list(dt_lib_module_t *self,
+ dt_lib_modulegroups_group_t *gr)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // first, we remove all existing modules
+ dt_gui_container_destroy_children(GTK_CONTAINER(gr->iop_box));
+
+ // and we add the ones from the list
+ for(GList *modules2 = g_list_last(darktable.develop->iop);
+ modules2;
+ modules2 = g_list_previous(modules2))
+ {
+ dt_iop_module_t *module = modules2->data;
+ if((!(module->flags() & IOP_FLAGS_DEPRECATED)
+ || !g_strcmp0(gr->name, C_("modulegroup", "deprecated")))
+ && !dt_iop_is_hidden(module)
+ && g_list_find_custom(gr->modules, module->op, _iop_compare))
+ {
+ // we want to avoid showing multiple instances of the same module
+ if(module->multi_priority <= 0
+ || g_list_find_custom(darktable.develop->iop, module,
+ _manage_editor_module_find_multi) == NULL)
+ {
+ GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb, "modulegroups-iop-header");
+ GtkWidget *lb = gtk_label_new(module->name());
+ gtk_label_set_ellipsize(GTK_LABEL(lb), PANGO_ELLIPSIZE_END);
+ gtk_label_set_xalign(GTK_LABEL(lb), 0.0);
+ gtk_widget_set_name(lb, "iop-panel-label");
+ gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0);
+ if(!d->edit_ro)
+ {
+ GtkWidget *btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL);
+ gtk_widget_set_tooltip_text(btn, _("remove this module"));
+ g_object_set_data(G_OBJECT(btn), "module_name", module->op);
+ g_object_set_data(G_OBJECT(btn), "group", gr);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_module_remove), self);
+ gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0);
+ }
+ gtk_box_pack_start(GTK_BOX(gr->iop_box), hb, FALSE, TRUE, 0);
+ }
+ }
+ }
+
+ gtk_widget_show_all(gr->iop_box);
+}
+
+static void _manage_editor_group_update_arrows(GtkWidget *box)
+{
+ // we go throw all group columns
+ GList *lw = gtk_container_get_children(GTK_CONTAINER(box));
+ int pos = 0;
+ const int max = g_list_length(lw) - 1;
+ for(const GList *lw_iter = lw; lw_iter; lw_iter = g_list_next(lw_iter))
+ {
+ GtkWidget *w = (GtkWidget *)lw_iter->data;
+ GtkWidget *hb = dt_gui_container_nth_child(GTK_CONTAINER(w), 1);
+ if(pos > 0 && hb) // we skip the first item as it's quick access panel
+ {
+ GList *lw2 = gtk_container_get_children(GTK_CONTAINER(hb));
+ if(!g_list_shorter_than(lw2, 3)) //do we have at least three?
+ {
+ GtkWidget *left = (GtkWidget *)lw2->data;
+ GtkWidget *right = (GtkWidget *)g_list_nth_data(lw2, 2);
+ gtk_widget_set_sensitive(left, pos > 1);
+ gtk_widget_set_sensitive(right, pos < max);
+ }
+ g_list_free(lw2);
+ }
+ pos++;
+ }
+ g_list_free(lw);
+}
+
+static void _manage_direct_save(dt_lib_module_t *self)
+{
+ // get all the values
+ gchar *params = _preset_to_string(self, FALSE);
+ // update the preset in the database
+ dt_lib_presets_add(_(CURRENT_PRESET_NAME),
+ self->plugin_name, self->version(), params, strlen(params), FALSE, 0);
+ g_free(params);
+
+ // update the preset name
+ dt_conf_set_string("plugins/darkroom/modulegroups_preset", _(CURRENT_PRESET_NAME));
+ // and we update the gui
+ if(!dt_lib_presets_apply(_(CURRENT_PRESET_NAME), self->plugin_name, self->version()))
+ dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
+ self->plugin_name, self->version());
+}
+
+static void _manage_direct_module_toggle(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ const gchar *module = (gchar *)g_object_get_data(G_OBJECT(widget), "module_op");
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ if(g_strcmp0(module, "") == 0) return;
+
+ GList *found_item = g_list_find_custom(gr->modules, module, _iop_compare);
+ if(!found_item)
+ {
+ gr->modules = g_list_append(gr->modules, g_strdup(module));
+ }
+ else
+ {
+ gr->modules = g_list_delete_link(gr->modules, found_item);
+ }
+
+ _manage_direct_save(self);
+}
+
+static gint _basics_item_find(gconstpointer a, gconstpointer b)
+{
+ dt_lib_modulegroups_basic_item_t *ia = (dt_lib_modulegroups_basic_item_t *)a;
+ return g_strcmp0(ia->id, (char *)b);
+}
+
+static int _lib_modulegroups_basics_module_toggle_action(dt_lib_module_t *self,
+ dt_action_t *action,
+ const gboolean doit)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ gchar *action_id = _action_id(action);
+ GList *found_item = g_list_find_custom(d->basics, action_id, _basics_item_find);
+
+ if(!doit)
+ g_free(action_id);
+ else
+ {
+ _basics_hide(self); // to be sure we put back all widget in their right modules
+
+ if(!found_item)
+ {
+ dt_lib_modulegroups_basic_item_t *item = g_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
+ if(item)
+ {
+ item->id = action_id;
+ _basics_init_item(item);
+ d->basics = g_list_append(d->basics, item);
+ }
+ }
+ else
+ {
+ _basics_free_item(found_item->data);
+ d->basics = g_list_delete_link(d->basics, found_item);
+
+ g_free(action_id);
+ }
+
+ _manage_direct_save(self);
+ }
+
+ return found_item ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP;
+}
+
+static int _lib_modulegroups_basics_module_toggle(dt_lib_module_t *self,
+ GtkWidget *widget,
+ const gboolean doit)
+{
+ if(GTK_IS_BUTTON(widget)) return 0;
+
+ dt_action_t *action = dt_action_widget(widget);
+
+ dt_action_t *owner = action;
+ while(owner && owner->type >= DT_ACTION_TYPE_SECTION)
+ owner = owner->owner;
+ if(!owner || owner->type != DT_ACTION_TYPE_IOP)
+ return 0;
+
+ return _lib_modulegroups_basics_module_toggle_action(self, action, doit);
+}
+
+static void _manage_direct_basics_module_toggle(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_action_t *action = g_object_get_data(G_OBJECT(widget), "widget_id");
+ if(!action) return;
+
+ _lib_modulegroups_basics_module_toggle_action(self, action, TRUE);
+}
+
+
+static void _manage_editor_basics_add(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ dt_action_t *action = g_object_get_data(G_OBJECT(widget), "widget_id");
+ gchar *action_id = _action_id(action);
+
+ if(g_list_find_custom(d->edit_basics, action_id, _basics_item_find))
+ g_free(action_id);
+ else
+ {
+ dt_lib_modulegroups_basic_item_t *item = g_malloc0(sizeof(dt_lib_modulegroups_basic_item_t));
+ if(item)
+ {
+ item->id = action_id;
+ _basics_init_item(item);
+ d->edit_basics = g_list_append(d->edit_basics, item);
+ }
+ _manage_editor_basics_update_list(self);
+ }
+}
+
+static void _manage_editor_module_add(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ const gchar *module = (gchar *)g_object_get_data(G_OBJECT(widget), "module_op");
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ if(g_strcmp0(module, "") == 0) return;
+
+ if(!g_list_find_custom(gr->modules, module, _iop_compare))
+ {
+ gr->modules = g_list_append(gr->modules, g_strdup(module));
+ _manage_editor_module_update_list(self, gr);
+ }
+}
+
+static int _manage_editor_module_so_add_sort(gconstpointer a, gconstpointer b)
+{
+ dt_iop_module_so_t *ma = (dt_iop_module_so_t *)a;
+ dt_iop_module_so_t *mb = (dt_iop_module_so_t *)b;
+ gchar *s1 = g_utf8_normalize(ma->name(), -1, G_NORMALIZE_ALL);
+ gchar *sa = g_utf8_casefold(s1, -1);
+ g_free(s1);
+ s1 = g_utf8_normalize(mb->name(), -1, G_NORMALIZE_ALL);
+ gchar *sb = g_utf8_casefold(s1, -1);
+ g_free(s1);
+ const int res = g_strcmp0(sa, sb);
+ g_free(sa);
+ g_free(sb);
+ return -res;
+}
+
+static void _manage_module_add_popup(GtkWidget *widget,
+ dt_lib_modulegroups_group_t *gr,
+ GCallback callback,
+ gpointer data,
+ const gboolean toggle)
+{
+ GtkWidget *pop = gtk_menu_new();
+ gtk_widget_set_name(pop, "modulegroups-popup");
+
+ int nba = 0; // nb of already present items
+
+ GtkMenu *sm_all = (GtkMenu *)gtk_menu_new();
+
+ GList *m2 = g_list_sort(g_list_copy(darktable.iop), _manage_editor_module_so_add_sort);
+ for(const GList *modules = m2; modules; modules = g_list_next(modules))
+ {
+ dt_iop_module_so_t *module = modules->data;
+
+ if(!dt_iop_so_is_hidden(module) && !(module->flags() & IOP_FLAGS_DEPRECATED))
+ {
+ if(!g_list_find_custom(gr->modules, module->op, _iop_compare))
+ {
+ // does it belong to recommended modules ?
+ if(((module->default_group() & IOP_GROUP_BASIC)
+ && g_strcmp0(gr->name, _("base")) == 0)
+ || ((module->default_group() & IOP_GROUP_COLOR)
+ && g_strcmp0(gr->name, _("color")) == 0)
+ || ((module->default_group() & IOP_GROUP_CORRECT)
+ && g_strcmp0(gr->name, _("correct")) == 0)
+ || ((module->default_group() & IOP_GROUP_TONE)
+ && g_strcmp0(gr->name, _("tone")) == 0)
+ || ((module->default_group() & IOP_GROUP_EFFECT)
+ && g_strcmp0(gr->name, C_("modulegroup", "effect")) == 0)
+ || ((module->default_group() & IOP_GROUP_TECHNICAL)
+ && g_strcmp0(gr->name, _("technical")) == 0)
+ || ((module->default_group() & IOP_GROUP_GRADING)
+ && g_strcmp0(gr->name, _("grading")) == 0)
+ || ((module->default_group() & IOP_GROUP_EFFECTS)
+ && g_strcmp0(gr->name, C_("modulegroup", "effects")) == 0))
+ {
+ GtkMenuItem *smir = (GtkMenuItem *)gtk_menu_item_new_with_label(module->name());
+ gtk_widget_set_name(GTK_WIDGET(smir), "modulegroups-popup-item");
+ gtk_widget_set_tooltip_text(GTK_WIDGET(smir), _("add this module"));
+ g_object_set_data(G_OBJECT(smir), "module_op", module->op);
+ g_object_set_data(G_OBJECT(smir), "group", gr);
+ g_signal_connect_data(G_OBJECT(smir), "activate", callback, data, NULL, 0);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(pop), GTK_WIDGET(smir), nba);
+ }
+ GtkMenuItem *smi = (GtkMenuItem *)gtk_menu_item_new_with_label(module->name());
+ gtk_widget_set_name(GTK_WIDGET(smi), "modulegroups-popup-item2");
+ gtk_widget_set_tooltip_text(GTK_WIDGET(smi), _("add this module"));
+ g_object_set_data(G_OBJECT(smi), "module_op", module->op);
+ g_object_set_data(G_OBJECT(smi), "group", gr);
+ g_signal_connect_data(G_OBJECT(smi), "activate", callback, data, NULL, 0);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(sm_all), GTK_WIDGET(smi));
+ }
+ else if(toggle)
+ {
+ GtkMenuItem *smi = (GtkMenuItem *)gtk_menu_item_new_with_label(module->name());
+ gtk_widget_set_name(GTK_WIDGET(smi), "modulegroups-popup-item");
+ gtk_widget_set_tooltip_text(GTK_WIDGET(smi), _("remove this module"));
+ g_object_set_data(G_OBJECT(smi), "module_op", module->op);
+ g_object_set_data(G_OBJECT(smi), "group", gr);
+ g_signal_connect_data(G_OBJECT(smi), "activate", callback, data, NULL, 0);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(pop), GTK_WIDGET(smi), 0);
+ nba++;
+ }
+ }
+ }
+ g_list_free(m2);
+
+ // show the submenu with all the modules
+ GtkWidget *smt = gtk_menu_item_new_with_label(_("all available modules"));
+ gtk_widget_set_name(smt, "modulegroups-popup-item-all");
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(smt), GTK_WIDGET(sm_all));
+ gtk_menu_shell_append(GTK_MENU_SHELL(pop), smt);
+
+ // show the add/remove sections titles if needed
+ if(toggle && nba > 0)
+ {
+ smt = gtk_menu_item_new_with_label(_("add module"));
+ gtk_widget_set_name(smt, "modulegroups-popup-title");
+ gtk_widget_set_sensitive(smt, FALSE);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(pop), smt, nba);
+
+ smt = gtk_menu_item_new_with_label(_("remove module"));
+ gtk_widget_set_name(smt, "modulegroups-popup-title");
+ gtk_widget_set_sensitive(smt, FALSE);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(pop), smt);
+ }
+
+ dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH);
+}
+
+static gchar *_action_label(dt_action_t *action)
+{
+ if(action->type != DT_ACTION_TYPE_IOP && action->owner)
+ {
+ gchar *owner_id = _action_label(action->owner);
+ gchar *combined_id = g_strdup_printf("%s - %s", owner_id, action->label);
+ g_free(owner_id);
+ return combined_id;
+ }
+ else
+ return g_strdup(action->label);
+}
+
+static GtkWidget *_build_menu_from_actions(dt_action_t *actions,
+ dt_lib_module_t *self,
+ GtkWidget *on_off,
+ GtkWidget *base_menu,
+ const gboolean full_menu,
+ int *num_selected)
+{
+ GCallback callback = G_CALLBACK(full_menu
+ ? _manage_direct_basics_module_toggle
+ : _manage_editor_basics_add);
+
+ GtkWidget *new_base = NULL;
+ while(actions)
+ {
+ if(actions == &darktable.control->actions_focus ||
+ actions == &darktable.control->actions_blend)
+ {
+ actions = actions->next;
+ continue;
+ }
+
+ if(actions->type == DT_ACTION_TYPE_IOP)
+ {
+ dt_iop_module_so_t *so = (dt_iop_module_so_t *)actions;
+ if(dt_iop_so_is_hidden(so) || so->flags() & IOP_FLAGS_DEPRECATED)
+ {
+ actions = actions->next;
+ continue;
+ }
+ }
+
+ gchar *action_label = NULL;
+ GtkWidget *item = NULL, *new_sub = NULL;
+ dt_action_t *action = NULL;
+
+ if(actions->type >= DT_ACTION_TYPE_SECTION && !on_off) // not an iop module or blending (CATEGORY)
+ {
+ // FIXME don't check here if on/off is enabled, because it
+ // depends on image (reload_defaults) respond later to image
+ // changed signal
+ on_off = item = gtk_check_menu_item_new_with_label(_("on-off"));
+ action = actions->owner;
+ action_label = g_strdup_printf("%s - %s", actions->owner->label, _("on-off"));
+
+ // in next loop deal with first actual widget or section
+ }
+ else
+ {
+ if(actions->type <= DT_ACTION_TYPE_SECTION)
+ new_sub = _build_menu_from_actions(actions->target,
+ self, on_off,
+ base_menu, full_menu,
+ num_selected);
+
+ if(new_sub
+ || (actions->type >= DT_ACTION_TYPE_WIDGET
+ && actions->target
+ && !GTK_IS_BUTTON(actions->target)))
+ {
+ item = new_sub ? gtk_menu_item_new_with_label(actions->label)
+ : gtk_check_menu_item_new_with_label(actions->label);
+ action = actions;
+ action_label = _action_label(actions);
+ }
+
+ actions = actions->next;
+ }
+
+ if(item)
+ {
+ gtk_widget_set_name(item, "modulegroups-popup-item2");
+
+ if(!new_base)
+ new_base = gtk_menu_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(new_base), item);
+
+ if(new_sub)
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), new_sub);
+ else
+ {
+ dt_lib_modulegroups_t *d = self->data;
+
+ GtkWidget *item_top = NULL;
+
+ gchar *action_id = _action_id(action);
+ if(g_list_find_custom(full_menu
+ ? d->basics
+ : d->edit_basics,
+ action_id, _basics_item_find))
+ {
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
+ if(!full_menu)
+ gtk_widget_set_sensitive(item, FALSE);
+ else
+ gtk_widget_set_tooltip_text(item, _("remove this widget"));
+
+ const gboolean compact_ui =
+ !dt_conf_get_bool("plugins/darkroom/modulegroups_basics_sections_labels");
+ if(!compact_ui && item != on_off)
+ {
+ gtk_check_menu_item_set_inconsistent(GTK_CHECK_MENU_ITEM(on_off), TRUE);
+ gtk_widget_set_tooltip_text(on_off, _("header needed for other widgets"));
+ }
+
+ if(full_menu)
+ {
+ item_top = gtk_menu_item_new_with_label(action_label);;
+ gtk_widget_set_tooltip_text(item_top, _("remove this widget"));
+ gtk_widget_set_name(item_top, "modulegroups-popup-item");
+ g_object_set_data(G_OBJECT(item_top), "widget_id", action);
+ g_signal_connect_data(G_OBJECT(item_top), "activate", callback, self, NULL, 0);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(base_menu), item_top, *num_selected);
+ ++*num_selected;
+ }
+ }
+ else
+ {
+ gtk_widget_set_tooltip_text(item, _("add this widget"));
+
+ gchar *delimited_id = g_strdup_printf("|%s|", action_id);
+
+ if(strstr(RECOMMENDED_BASICS, delimited_id))
+ {
+ item_top = gtk_menu_item_new_with_label(action_label);;
+ gtk_widget_set_tooltip_text(item_top, _("add this widget"));
+ gtk_widget_set_name(item_top, "modulegroups-popup-item");
+ g_object_set_data(G_OBJECT(item_top), "widget_id", action);
+ g_signal_connect_data(G_OBJECT(item_top), "activate", callback, self, NULL, 0);
+ gtk_menu_shell_append(GTK_MENU_SHELL(base_menu), item_top);
+ }
+ g_free(delimited_id);
+ }
+
+ if(item != on_off && dt_action_widget_invisible(action->target))
+ {
+ gtk_check_menu_item_set_inconsistent(GTK_CHECK_MENU_ITEM(item), TRUE);
+ gchar *toolmark = gtk_widget_get_tooltip_text(item);
+ dt_util_str_cat(&toolmark, " (%s)", _("currently invisible"));
+ gtk_widget_set_tooltip_markup(item, toolmark);
+ if(item_top)
+ gtk_widget_set_tooltip_markup(item_top, toolmark);
+ g_free(toolmark);
+ }
+
+ g_object_set_data(G_OBJECT(item), "widget_id", action);
+ g_signal_connect_data(G_OBJECT(item), "activate", callback, self, NULL, 0);
+ g_free(action_id);
+ }
+ g_free(action_label);
+ }
+ }
+
+ return new_base;
+}
+
+static void _manage_basics_add_popup(GtkWidget *widget,
+ dt_lib_module_t *self,
+ const gboolean full_menu)
+{
+ int nba = 0; // nb of already present items
+ GtkWidget *pop = gtk_menu_new();
+ gtk_widget_set_name(pop, "modulegroups-popup");
+
+ GtkWidget *all_modules =
+ _build_menu_from_actions(darktable.control->actions_iops.target, self, NULL,
+ pop, full_menu, &nba);
+
+ // show the add/remove sections titles if needed
+ if(full_menu && nba > 0)
+ {
+ GtkWidget *smt = gtk_menu_item_new_with_label(_("add widget"));
+ gtk_widget_set_name(smt, "modulegroups-popup-title");
+ gtk_widget_set_sensitive(smt, FALSE);
+ gtk_menu_shell_insert(GTK_MENU_SHELL(pop), smt, nba);
+
+ smt = gtk_menu_item_new_with_label(_("remove widget"));
+ gtk_widget_set_name(smt, "modulegroups-popup-title");
+ gtk_widget_set_sensitive(smt, FALSE);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(pop), smt);
+ }
+
+ GList *children = gtk_container_get_children(GTK_CONTAINER(pop));
+ if(children)
+ {
+ g_list_free(children);
+
+ GtkWidget *smt = gtk_menu_item_new_with_label(_("all available modules"));
+ gtk_widget_set_name(smt, "modulegroups-popup-item-all");
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(smt), GTK_WIDGET(all_modules));
+ gtk_menu_shell_append(GTK_MENU_SHELL(pop), smt);
+ }
+ else
+ {
+ gtk_widget_destroy(pop);
+ pop = all_modules;
+ }
+
+ dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH);
+}
+
+static void _manage_editor_basics_add_popup(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ _manage_basics_add_popup(widget, self, FALSE);
+}
+
+static void _manage_editor_module_add_popup(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ _manage_module_add_popup(widget, gr,
+ G_CALLBACK(_manage_editor_module_add), self, FALSE);
+}
+
+static gboolean _presets_pressed(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ if(dt_modifier_is(event->state, GDK_CONTROL_MASK))
+ {
+ manage_presets(self);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean _manage_direct_popup(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ if(event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY)
+ {
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ if(!g_strcmp0(gr->name, C_("modulegroup", "deprecated"))) return FALSE;
+ _manage_module_add_popup(widget, gr,
+ G_CALLBACK(_manage_direct_module_toggle), self, TRUE);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean _manage_direct_basic_popup(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ if(event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY)
+ {
+ _manage_basics_add_popup(widget, self, TRUE);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean _manage_direct_module_popup(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ dt_action_t *module = g_object_get_data(G_OBJECT(widget), "module");
+
+ if(event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY)
+ {
+ int nba = 0; // nb of already present items
+ GtkWidget *pop = gtk_menu_new();
+ gtk_widget_set_name(pop, "modulegroups-popup");
+
+ GtkWidget *this_module = _build_menu_from_actions(module->target, self,
+ NULL, pop, TRUE, &nba);
+
+ dt_gui_menu_popup(GTK_MENU(this_module), NULL, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH);
+
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void _manage_direct_full_active_toggled(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ d->full_active = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
+ const int cur = d->current;
+ _manage_direct_save(self);
+ d->current = cur;
+ _lib_modulegroups_update_iop_visibility(self);
+}
+
+static gboolean _manage_direct_active_popup(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ if(event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY)
+ {
+ dt_lib_modulegroups_t *d = self->data;
+ GtkWidget *pop = gtk_menu_new();
+ gtk_widget_set_name(pop, "modulegroups-popup");
+
+ GtkWidget *smt = gtk_check_menu_item_new_with_label(_("show all history modules"));
+ gtk_widget_set_tooltip_text(
+ smt,
+ _("show modules that are present in the history stack,"
+ " regardless of whether or not they are currently enabled"));
+ gtk_widget_set_name(smt, "modulegroups-popup-item");
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(smt), d->full_active);
+ g_signal_connect(G_OBJECT(smt), "toggled",
+ G_CALLBACK(_manage_direct_full_active_toggled), self);
+ gtk_menu_shell_append(GTK_MENU_SHELL(pop), smt);
+
+ dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void _dt_dev_image_changed_callback(gpointer instance,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ dt_develop_t *dev = darktable.develop;
+ if(!dev || !dt_is_valid_imgid(dev->image_storage.id)) return;
+
+ const dt_image_t *image = dt_image_cache_get(dev->image_storage.id, 'r');
+
+ if(!image) return;
+
+ char *format_filter = dt_presets_get_filter(image);
+
+ char query[1024];
+ // clang-format off
+ snprintf(query, sizeof(query),
+ "SELECT name"
+ " FROM data.presets"
+ " WHERE operation='modulegroups'"
+ " AND op_version=?1"
+ " AND autoapply=1"
+ " AND ((?2 LIKE model AND ?3 LIKE maker) OR (?4 LIKE model AND ?5 LIKE maker))"
+ " AND ?6 LIKE lens AND ?7 BETWEEN iso_min AND iso_max"
+ " AND ?8 BETWEEN exposure_min AND exposure_max"
+ " AND ?9 BETWEEN aperture_min AND aperture_max"
+ " AND ?10 BETWEEN focal_length_min AND focal_length_max"
+ " AND (%s)"
+ " ORDER BY writeprotect DESC, name DESC"
+ " LIMIT 1",
+ format_filter);
+ // clang-format on
+
+ g_free(format_filter);
+
+ sqlite3_stmt *stmt;
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, self->version());
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, image->exif_model, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, image->exif_maker, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 4, image->camera_alias, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 5, image->camera_maker, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 6, image->exif_lens, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 7, fmaxf(0.0f, fminf(FLT_MAX, image->exif_iso)));
+ DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 8, fmaxf(0.0f, fminf(1000000, image->exif_exposure)));
+ DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 9, fmaxf(0.0f, fminf(1000000, image->exif_aperture)));
+ DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 10, fmaxf(0.0f, fminf(1000000, image->exif_focal_length)));
+
+ dt_image_cache_read_release(image);
+
+ if(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ const char *preset = (char *)sqlite3_column_text(stmt, 0);
+ dt_lib_presets_apply(preset, self->plugin_name, self->version());
+ }
+ sqlite3_finalize(stmt);
+
+ // check for missing camera samples
+ if(image->camera_missing_sample)
+ {
+ gchar *label = dt_image_camera_missing_sample_message(image, FALSE);
+ d->force_deprecated_message = TRUE;
+ gtk_label_set_markup(GTK_LABEL(d->deprecated), label);
+ g_free(label);
+ gtk_widget_set_visible(d->deprecated, TRUE);
+ }
+ else
+ {
+ d->force_deprecated_message = FALSE;
+ gtk_label_set_markup
+ (GTK_LABEL(d->deprecated),
+ _("the following modules are deprecated because they have internal design mistakes"
+ " that can't be corrected and alternative modules that correct them.\n"
+ "they will be removed for new edits in the next release."));
+ }
+
+}
+
+static gboolean _scroll_group_buttons(GtkWidget *widget,
+ GdkEventScroll *event,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ int delta;
+ if(dt_gui_get_scroll_unit_delta(event, &delta))
+ {
+ GtkWidget *adjacent = d->current == DT_MODULEGROUP_BASICS && delta < 0
+ ? d->active_btn
+ : d->current <= DT_MODULEGROUP_ACTIVE_PIPE && delta > 0
+ ? d->basic_btn
+ : _buttons_get_from_pos(self, d->current - delta);
+ if(adjacent) gtk_button_clicked(GTK_BUTTON(adjacent));
+ }
+
+ return TRUE;
+}
+
+void gui_init(dt_lib_module_t *self)
+{
+ /* initialize ui widgets */
+ dt_lib_modulegroups_t *d = g_malloc0(sizeof(dt_lib_modulegroups_t));
+ self->data = (void *)d;
+
+ self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(self->widget, "modules-tabs");
+ dt_gui_add_class(self->widget, "dt_big_btn_canvas");
+
+ d->hbox_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ d->hbox_search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ // groups
+ d->hbox_groups = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *scrollbox = gtk_event_box_new();
+ gtk_container_add(GTK_CONTAINER(scrollbox), d->hbox_groups);
+ g_signal_connect(scrollbox, "scroll-event",
+ G_CALLBACK(_scroll_group_buttons), self);
+ gtk_widget_add_events(scrollbox, darktable.gui->scroll_mask);
+ gtk_box_pack_start(GTK_BOX(d->hbox_buttons), scrollbox, TRUE, TRUE, 0);
+
+ // basic group button
+ d->basic_btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_modulegroup_basics, 0, NULL);
+ g_signal_connect(d->basic_btn, "button-press-event",
+ G_CALLBACK(_manage_direct_basic_popup), self);
+ g_signal_connect(d->basic_btn, "toggled", G_CALLBACK(_lib_modulegroups_toggle), self);
+ gtk_widget_set_tooltip_text(d->basic_btn, _("quick access panel\n"
+ "right-click tab icon to add/remove widgets"));
+ dt_action_define(DT_ACTION(self), NULL,
+ N_("quick access panel"), d->basic_btn, &dt_action_def_toggle);
+ gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->basic_btn, TRUE, TRUE, 0);
+
+ d->vbox_basic = NULL;
+ d->basics = NULL;
+
+ // active group button
+ d->active_btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_modulegroup_active, 0, NULL);
+ g_signal_connect(d->active_btn, "button-press-event",
+ G_CALLBACK(_manage_direct_active_popup), self);
+ g_signal_connect(d->active_btn, "toggled",
+ G_CALLBACK(_lib_modulegroups_toggle), self);
+ gtk_widget_set_tooltip_text(d->active_btn, _("show only active modules"));
+ dt_action_define(DT_ACTION(self), NULL, N_("active modules"),
+ d->active_btn, &dt_action_def_toggle);
+ gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->active_btn, TRUE, TRUE, 0);
+
+ // we load now the presets btn
+ self->presets_button = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL);
+ gtk_widget_set_tooltip_text(self->presets_button, _("presets\nctrl+click to manage"));
+ gtk_box_pack_start(GTK_BOX(d->hbox_buttons), self->presets_button, FALSE, FALSE, 0);
+ g_signal_connect(self->presets_button, "button-press-event",
+ G_CALLBACK(_presets_pressed), self);
+
+ /* search box */
+ d->text_entry = gtk_search_entry_new();
+ dt_action_define(&darktable.view_manager->proxy.darkroom.view->actions, NULL, N_("search modules"), d->text_entry, &dt_action_def_entry);
+ gtk_entry_set_placeholder_text(GTK_ENTRY(d->text_entry),
+ _("search modules by name or tag"));
+ g_signal_connect(G_OBJECT(d->text_entry), "search-changed",
+ G_CALLBACK(_text_entry_changed_callback), self);
+ g_signal_connect(G_OBJECT(d->text_entry), "stop-search",
+ G_CALLBACK(dt_gui_search_stop), dt_ui_center(darktable.gui->ui));
+ g_signal_connect_data(G_OBJECT(d->text_entry), "focus-in-event",
+ G_CALLBACK(gtk_widget_show),
+ d->hbox_search_box, NULL, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+
+ GtkWidget *visibility_wrapper = gtk_event_box_new(); // extra layer prevents disabling shortcuts when hidden
+ gtk_container_add(GTK_CONTAINER(visibility_wrapper), d->text_entry);
+ gtk_box_pack_start(GTK_BOX(d->hbox_search_box), visibility_wrapper, TRUE, TRUE, 0);
+ gtk_entry_set_width_chars(GTK_ENTRY(d->text_entry), 0);
+ gtk_entry_set_max_width_chars(GTK_ENTRY(d->text_entry), 35);
+ gtk_entry_set_icon_tooltip_text(GTK_ENTRY(d->text_entry),
+ GTK_ENTRY_ICON_SECONDARY, _("clear text"));
+
+ gtk_box_pack_start(GTK_BOX(self->widget), d->hbox_buttons, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(self->widget), d->hbox_search_box, TRUE, TRUE, 0);
+
+ // deprecated message
+ d->deprecated
+ = gtk_label_new(_("the following modules are deprecated because they have internal design mistakes"
+ " that can't be corrected and alternative modules that correct them.\n"
+ "they will be removed for new edits in the next release."));
+ dt_gui_add_class(d->deprecated, "dt_warning");
+ gtk_label_set_line_wrap(GTK_LABEL(d->deprecated), TRUE);
+ gtk_box_pack_start(GTK_BOX(self->widget), d->deprecated, TRUE, TRUE, 0);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->active_btn), TRUE);
+ d->current = dt_conf_get_int("plugins/darkroom/groups");
+ if(d->current == DT_MODULEGROUP_NONE) _lib_modulegroups_update_iop_visibility(self);
+ gtk_widget_show_all(self->widget);
+ gtk_widget_set_no_show_all(d->deprecated, TRUE);
+ gtk_widget_set_no_show_all(d->hbox_buttons, TRUE);
+ gtk_widget_set_no_show_all(d->hbox_search_box, TRUE);
+
+ /*
+ * set the proxy functions
+ */
+ darktable.develop->proxy.modulegroups.module = self;
+ darktable.develop->proxy.modulegroups.set = _lib_modulegroups_set;
+ darktable.develop->proxy.modulegroups.update_visibility = _lib_modulegroups_update_visibility_proxy;
+ darktable.develop->proxy.modulegroups.get = _lib_modulegroups_get;
+ darktable.develop->proxy.modulegroups.get_activated = _lib_modulegroups_get_activated;
+ darktable.develop->proxy.modulegroups.test = _lib_modulegroups_test;
+ darktable.develop->proxy.modulegroups.switch_group = _lib_modulegroups_switch_group;
+ darktable.develop->proxy.modulegroups.test_visible = _lib_modulegroups_test_visible;
+ darktable.develop->proxy.modulegroups.basics_module_toggle = _lib_modulegroups_basics_module_toggle;
+
+ // check for autoapplypresets on image change
+ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_IMAGE_CHANGED, _dt_dev_image_changed_callback);
+ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_INITIALIZE, _dt_dev_image_changed_callback);
+}
+
+void gui_cleanup(dt_lib_module_t *self)
+{
+ darktable.develop->proxy.modulegroups.module = NULL;
+ darktable.develop->proxy.modulegroups.set = NULL;
+ darktable.develop->proxy.modulegroups.get = NULL;
+ darktable.develop->proxy.modulegroups.get_activated = NULL;
+ darktable.develop->proxy.modulegroups.test = NULL;
+ darktable.develop->proxy.modulegroups.switch_group = NULL;
+
+ g_free(self->data);
+ self->data = NULL;
+}
+
+static void _buttons_update(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // ensure we have no remaining force shown modules
+ d->force_show_module = NULL;
+
+ // first, we destroy all existing buttons except active one an preset one
+ GList *children = gtk_container_get_children(GTK_CONTAINER(d->hbox_groups));
+ GList *l = children;
+ if(!g_list_is_empty(l))
+ l = g_list_next(l); // skip basics group
+ if(!g_list_is_empty(l))
+ l = g_list_next(l); // skip active group
+
+ for(; l; l = g_list_next(l))
+ {
+ GtkWidget *bt = (GtkWidget *)l->data;
+ gtk_widget_destroy(bt);
+ }
+ g_list_free(children);
+ gtk_widget_set_visible(d->basic_btn, d->basics_show);
+
+ // if there's no groups, we ensure that the preset button is on the
+ // search line and we hide the active button
+ gtk_widget_set_visible(d->hbox_search_box, d->show_search);
+ if(!d->groups && d->show_search)
+ {
+ if(gtk_widget_get_parent(self->presets_button) != d->hbox_search_box)
+ {
+ g_object_ref(self->presets_button);
+ gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(self->presets_button)),
+ self->presets_button);
+ gtk_box_pack_start(GTK_BOX(d->hbox_search_box),
+ self->presets_button, FALSE, FALSE, 0);
+ g_object_unref(self->presets_button);
+ }
+ gtk_widget_hide(d->hbox_buttons);
+ d->current = DT_MODULEGROUP_ACTIVE_PIPE;
+ _lib_modulegroups_update_iop_visibility(self);
+ return;
+ }
+ else
+ {
+ if(gtk_widget_get_parent(self->presets_button) != d->hbox_buttons)
+ {
+ g_object_ref(self->presets_button);
+ gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(self->presets_button)),
+ self->presets_button);
+ gtk_box_pack_start(GTK_BOX(d->hbox_buttons), self->presets_button, FALSE, FALSE, 0);
+ g_object_unref(self->presets_button);
+ }
+ gtk_widget_show(d->hbox_buttons);
+ gtk_widget_show(d->hbox_groups);
+ }
+
+ // then we repopulate the box with new buttons
+ for(l = d->groups; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_group_t *gr = l->data;
+ GtkWidget *bt = dtgtk_togglebutton_new(_buttons_get_icon_fct(gr->icon), 0, NULL);
+ g_object_set_data(G_OBJECT(bt), "group", gr);
+ g_signal_connect(bt, "button-press-event", G_CALLBACK(_manage_direct_popup), self);
+ g_signal_connect(bt, "toggled", G_CALLBACK(_lib_modulegroups_toggle), self);
+ char *tooltip = g_strdup_printf(_("%s\nright-click tab icon to add/remove modules"), gr->name);
+ gtk_widget_set_tooltip_text(bt, tooltip);
+ g_free(tooltip);
+ gr->button = bt;
+ gtk_box_pack_start(GTK_BOX(d->hbox_groups), bt, TRUE, TRUE, 0);
+ gtk_widget_show(bt);
+ }
+
+ // last, if d->current still valid, we select it otherwise the first one
+ if(d->current == DT_MODULEGROUP_BASICS
+ ? !d->basics_show
+ : d->current > g_list_length(d->groups))
+ {
+ d->current = DT_MODULEGROUP_ACTIVE_PIPE;
+ }
+
+ if(d->current == DT_MODULEGROUP_ACTIVE_PIPE)
+ {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->active_btn), TRUE);
+ }
+ else if(d->current == DT_MODULEGROUP_BASICS)
+ {
+ if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->basic_btn)))
+ {
+ // we need to manually refresh the list
+ _lib_modulegroups_update_iop_visibility(self);
+ }
+ else
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basic_btn), TRUE);
+ }
+ else
+ {
+ dt_lib_modulegroups_group_t *gr = g_list_nth_data(d->groups, d->current - 1);
+ d->current = DT_MODULEGROUP_NONE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gr->button), TRUE);
+ }
+}
+
+static void _manage_editor_group_move_right(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ GtkWidget *vb = gtk_widget_get_parent(gtk_widget_get_parent(widget));
+
+ // we move the group inside the list
+ const int pos = g_list_index(d->edit_groups, gr);
+ if(pos < 0 || pos >= g_list_length(d->edit_groups) - 1) return;
+ d->edit_groups = g_list_remove(d->edit_groups, gr);
+ d->edit_groups = g_list_insert(d->edit_groups, gr, pos + 1);
+
+ // we move the group in the ui (the position need +1 due to the quick access panel)
+ gtk_box_reorder_child(GTK_BOX(gtk_widget_get_parent(vb)), vb, pos + 2);
+ // and we update arrows
+ _manage_editor_group_update_arrows(gtk_widget_get_parent(vb));
+}
+
+static void _manage_editor_group_move_left(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ GtkWidget *vb = gtk_widget_get_parent(gtk_widget_get_parent(widget));
+
+ // we move the group inside the list
+ const int pos = g_list_index(d->edit_groups, gr);
+ if(pos <= 0) return;
+ d->edit_groups = g_list_remove(d->edit_groups, gr);
+ d->edit_groups = g_list_insert(d->edit_groups, gr, pos - 1);
+
+ // we move the group in the ui (the position need +1 due to the quick access panel)
+ gtk_box_reorder_child(GTK_BOX(gtk_widget_get_parent(vb)), vb, pos);
+ // and we update arrows
+ _manage_editor_group_update_arrows(gtk_widget_get_parent(vb));
+}
+
+static void _manage_editor_group_remove(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ // we don't allow to remove the last group if no quick access or searchbox
+ if(g_list_is_singleton(d->edit_groups)
+ && !d->edit_basics_show
+ && !d->edit_show_search)
+ {
+ return;
+ }
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group");
+ GtkWidget *vb =
+ gtk_widget_get_parent(gtk_widget_get_parent(gtk_widget_get_parent(widget)));
+ GtkWidget *groups_box = gtk_widget_get_parent(vb);
+
+ // we remove the group from the list and destroy it
+ d->edit_groups = g_list_remove(d->edit_groups, gr);
+ g_free(gr->name);
+ g_free(gr->icon);
+ g_list_free_full(gr->modules, g_free);
+ g_free(gr);
+
+ // we remove the group from the ui
+ gtk_widget_destroy(vb);
+
+ // and we update arrows
+ _manage_editor_group_update_arrows(groups_box);
+}
+
+static void _manage_editor_group_name_changed(GtkWidget *tb,
+ GdkEventButton *event,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_group_t *gr = (dt_lib_modulegroups_group_t *)g_object_get_data(G_OBJECT(tb), "group");
+ const gchar *txt = gtk_entry_get_text(GTK_ENTRY(tb));
+ g_free(gr->name);
+ gr->name = g_strdup(txt);
+}
+
+static gboolean _manage_editor_group_icon_changed(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_lib_modulegroups_group_t *gr)
+{
+ const char *ic = (char *)g_object_get_data(G_OBJECT(widget), "ic_name");
+ g_free(gr->icon);
+ gr->icon = g_strdup(ic);
+ GtkWidget *pop = gtk_widget_get_parent(gtk_widget_get_parent(widget));
+ GtkWidget *btn = gtk_popover_get_relative_to(GTK_POPOVER(pop));
+ dtgtk_button_set_paint(DTGTK_BUTTON(btn), _buttons_get_icon_fct(ic), 0, NULL);
+ gtk_popover_popdown(GTK_POPOVER(pop));
+ return TRUE;
+}
+
+static void _manage_editor_group_icon_popup(GtkWidget *btn,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(btn), "group");
+
+ GtkWidget *pop = gtk_popover_new(btn);
+ GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(pop, "modulegroups-icons-popup");
+
+ GtkWidget *eb, *hb, *ic;
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_basic, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("basic icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "basic");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_active, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("active icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "active");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_color, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("color icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "color");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_correct, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("correct icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "correct");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_effect, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("effect icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "effect");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_favorites, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("favorites icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "favorites");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_tone, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("tone icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "tone");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_grading, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("grading icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "grading");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ eb = gtk_event_box_new();
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ ic = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_technical, 0, NULL);
+ gtk_box_pack_start(GTK_BOX(hb), ic, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("technical icon")), TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(eb), "ic_name", "technical");
+ g_signal_connect(G_OBJECT(eb), "button-press-event",
+ G_CALLBACK(_manage_editor_group_icon_changed), gr);
+ gtk_container_add(GTK_CONTAINER(eb), hb);
+ gtk_box_pack_start(GTK_BOX(vb), eb, FALSE, TRUE, 0);
+
+ gtk_container_add(GTK_CONTAINER(pop), vb);
+ gtk_widget_show_all(pop);
+}
+
+static GtkWidget *_manage_editor_group_init_basics_box(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ GtkWidget *vb2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(vb2, "modulegroups-groupbox");
+ // line to edit the group
+ GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb2, "modulegroups-header");
+
+ GtkWidget *btn = NULL;
+
+ GtkWidget *hb3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb3, "modulegroups-header-center");
+ gtk_widget_set_hexpand(hb3, TRUE);
+
+ btn = dtgtk_button_new(dtgtk_cairo_paint_modulegroup_basics, 0, NULL);
+ gtk_widget_set_name(btn, "modulegroups-group-icon");
+ gtk_widget_set_sensitive(btn, FALSE);
+ gtk_box_pack_start(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
+
+ GtkWidget *tb = gtk_entry_new();
+ gtk_entry_set_width_chars(GTK_ENTRY(tb), 5);
+ gtk_widget_set_tooltip_text(tb, _("quick access panel widgets"));
+ gtk_widget_set_sensitive(tb, FALSE);
+ gtk_entry_set_text(GTK_ENTRY(tb), _("quick access"));
+ gtk_box_pack_start(GTK_BOX(hb3), tb, TRUE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(hb2), hb3, FALSE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(vb2), hb2, FALSE, TRUE, 0);
+
+ // chosen widgets
+ GtkWidget *vb3 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *sw = dt_gui_scroll_wrap(vb3);
+ d->edit_basics_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ _manage_editor_basics_update_list(self);
+ gtk_box_pack_start(GTK_BOX(vb3), d->edit_basics_box, FALSE, TRUE, 0);
+
+ // '+' button to add new widgets
+ if(!d->edit_ro)
+ {
+ GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *bt = dtgtk_button_new(dtgtk_cairo_paint_square_plus,
+ CPF_DIRECTION_LEFT, NULL);
+ gtk_widget_set_tooltip_text(bt, _("add widget to the quick access panel"));
+ gtk_widget_set_name(bt, "modulegroups-btn");
+ g_signal_connect(G_OBJECT(bt), "clicked",
+ G_CALLBACK(_manage_editor_basics_add_popup), self);
+ gtk_widget_set_halign(hb4, GTK_ALIGN_CENTER);
+ gtk_box_pack_start(GTK_BOX(hb4), bt, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0);
+ }
+
+ gtk_box_pack_start(GTK_BOX(vb2), sw, TRUE, TRUE, 0);
+
+ return vb2;
+}
+
+static GtkWidget *_manage_editor_group_init_modules_box(dt_lib_module_t *self,
+ dt_lib_modulegroups_group_t *gr)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ GtkWidget *vb2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(vb2, "modulegroups-groupbox");
+ // line to edit the group
+ GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb2, "modulegroups-header");
+
+ GtkWidget *btn = NULL;
+ GtkWidget *hb3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb3, "modulegroups-header-center");
+ gtk_widget_set_hexpand(hb3, TRUE);
+
+ // icon
+ btn = dtgtk_button_new(_buttons_get_icon_fct(gr->icon), 0, NULL);
+ gtk_widget_set_name(btn, "modulegroups-group-icon");
+ gtk_widget_set_tooltip_text(btn, _("group icon"));
+ gtk_widget_set_sensitive(btn, !d->edit_ro);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_group_icon_popup), self);
+ g_object_set_data(G_OBJECT(btn), "group", gr);
+ gtk_box_pack_start(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
+
+ // entry for group name
+ GtkWidget *tb = gtk_entry_new();
+ gtk_entry_set_width_chars(GTK_ENTRY(tb), 5);
+ gtk_widget_set_tooltip_text(tb, _("group name"));
+ g_object_set_data(G_OBJECT(tb), "group", gr);
+ gtk_widget_set_sensitive(tb, !d->edit_ro);
+ g_signal_connect(G_OBJECT(tb), "changed",
+ G_CALLBACK(_manage_editor_group_name_changed), self);
+ gtk_entry_set_text(GTK_ENTRY(tb), gr->name);
+ gtk_box_pack_start(GTK_BOX(hb3), tb, TRUE, TRUE, 0);
+
+ // remove button
+ if(!d->edit_ro)
+ {
+ btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL);
+ gtk_widget_set_tooltip_text(btn, _("remove group"));
+ g_object_set_data(G_OBJECT(btn), "group", gr);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_group_remove), self);
+ gtk_box_pack_end(GTK_BOX(hb3), btn, FALSE, TRUE, 0);
+ }
+
+ gtk_box_pack_start(GTK_BOX(hb2), hb3, FALSE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(vb2), hb2, FALSE, TRUE, 0);
+
+ // chosen modules
+ GtkWidget *vb3 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *sw = dt_gui_scroll_wrap(vb3);
+ gr->iop_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ _manage_editor_module_update_list(self, gr);
+ gtk_box_pack_start(GTK_BOX(vb3), gr->iop_box, FALSE, TRUE, 0);
+
+ // '+' button to add new module
+ if(!d->edit_ro)
+ {
+ GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ // left arrow
+ btn = dtgtk_button_new(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_RIGHT, NULL);
+ gtk_widget_set_name(btn, "modulegroups-btn");
+ gtk_widget_set_tooltip_text(btn, _("move group to the left"));
+ g_object_set_data(G_OBJECT(btn), "group", gr);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_group_move_left), self);
+ gtk_box_pack_start(GTK_BOX(hb4), btn, FALSE, FALSE, 2);
+
+ // plus button
+ GtkWidget *plusbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *bt = dtgtk_button_new(dtgtk_cairo_paint_square_plus,
+ CPF_DIRECTION_LEFT, NULL);
+ gtk_widget_set_tooltip_text(bt, _("add module to the group"));
+ gtk_widget_set_name(bt, "modulegroups-btn");
+ g_object_set_data(G_OBJECT(bt), "group", gr);
+ g_signal_connect(G_OBJECT(bt), "clicked",
+ G_CALLBACK(_manage_editor_module_add_popup), self);
+ gtk_widget_set_halign(plusbox, GTK_ALIGN_CENTER);
+ gtk_box_pack_start(GTK_BOX(plusbox), bt, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hb4), plusbox, TRUE, TRUE, 0);
+
+ //right arrow
+ btn = dtgtk_button_new(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_LEFT, NULL);
+ gtk_widget_set_name(btn, "modulegroups-btn");
+ gtk_widget_set_tooltip_text(btn, _("move group to the right"));
+ g_object_set_data(G_OBJECT(btn), "group", gr);
+ g_signal_connect(G_OBJECT(btn), "clicked",
+ G_CALLBACK(_manage_editor_group_move_right), self);
+ gtk_box_pack_end(GTK_BOX(hb4), btn, FALSE, FALSE, 2);
+
+ gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0);
+ }
+
+ gtk_box_pack_start(GTK_BOX(vb2), sw, TRUE, TRUE, 0);
+
+ return vb2;
+}
+
+static void _manage_editor_reset(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ gchar *txt = g_strdup(d->edit_preset);
+ _manage_editor_load(txt, self);
+ g_free(txt);
+}
+
+static void _manage_editor_group_add(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ dt_lib_modulegroups_group_t *gr = g_malloc0(sizeof(dt_lib_modulegroups_group_t));
+ if(gr)
+ {
+ gr->name = g_strdup(_("new"));
+ gr->icon = g_strdup("basic");
+ d->edit_groups = g_list_append(d->edit_groups, gr);
+
+ // we update the group list
+ GtkWidget *vb2 = _manage_editor_group_init_modules_box(self, gr);
+ gtk_box_pack_start(GTK_BOX(d->preset_groups_box), vb2, FALSE, TRUE, 0);
+ gtk_widget_show_all(vb2);
+ }
+ // and we update arrows
+ _manage_editor_group_update_arrows(d->preset_groups_box);
+}
+
+static void _manage_editor_basics_toggle(GtkWidget *button,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(d->editor_reset) return;
+ const gboolean state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+ // we don't allow that to be false if there's no group or search
+ if(!state
+ && g_list_is_empty(d->edit_groups)
+ && !d->edit_show_search)
+ {
+ d->editor_reset = TRUE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
+ d->editor_reset = FALSE;
+ }
+ d->edit_basics_show = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+ gtk_widget_set_visible(d->edit_basics_groupbox, d->edit_basics_show);
+}
+
+static void _manage_editor_search_toggle(GtkWidget *button,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(d->editor_reset) return;
+ const gboolean state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+ // we don't allow that to be false if there's no group or quick access
+ if(!state
+ && g_list_is_empty(d->edit_groups)
+ && !d->edit_basics_show)
+ {
+ d->editor_reset = TRUE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
+ d->editor_reset = FALSE;
+ }
+ d->edit_show_search = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+}
+
+static void _manage_editor_full_active_toggle(GtkWidget *button,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(d->editor_reset) return;
+
+ d->edit_full_active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+}
+
+static void _preset_autoapply_changed(dt_gui_presets_edit_dialog_t *g)
+{
+ dt_lib_module_t *self = g->data;
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we reread the presets autoapply values from the database
+ sqlite3_stmt *stmt;
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT autoapply"
+ " FROM data.presets"
+ " WHERE operation = ?1 AND op_version = ?2 AND name = ?3",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, self->version());
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, d->edit_preset, -1, SQLITE_TRANSIENT);
+
+ int autoapply = 0;
+ if(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ autoapply = sqlite3_column_int(stmt, 0);
+ sqlite3_finalize(stmt);
+ }
+ else
+ {
+ sqlite3_finalize(stmt);
+ return;
+ }
+
+ // we refresh the checkbox
+ d->editor_reset = TRUE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_autoapply_chkbox), autoapply);
+ d->editor_reset = FALSE;
+}
+
+static void _manage_editor_preset_name_verify(GtkWidget *tb,
+ gpointer params[])
+{
+ GtkDialog *dialog = params[0];
+ GList *names = params[1];
+ GtkWidget *warning_label = params[2];
+
+ const gchar *txt = gtk_entry_get_text(GTK_ENTRY(tb));
+ gboolean good = *txt;
+
+ // we don't want empty name
+ if(good)
+ {
+ for(const GList *l = names; l; l = g_list_next(l))
+ {
+ if(!g_strcmp0(l->data, txt))
+ {
+ good = FALSE;
+ break;
+ }
+ }
+ }
+ gtk_widget_set_visible(warning_label, !good);
+ gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_OK, good);
+}
+
+static void _manage_editor_preset_action(GtkWidget *btn,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we get the default name
+ gchar *new_name = NULL;
+ if(btn == d->presets_btn_rename)
+ new_name = g_strdup(d->edit_preset);
+ else if(btn == d->presets_btn_new)
+ new_name = g_strdup(_("new"));
+ else if(btn == d->presets_btn_dup)
+ new_name = g_strdup_printf("%s_1", d->edit_preset);
+ else
+ return;
+
+ // we first get the list of all the existing preset names
+ GList *names = NULL;
+ sqlite3_stmt *stmt;
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT name"
+ " FROM data.presets"
+ " WHERE operation = ?1 AND op_version = ?2",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, self->version());
+ while(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ const char *name = (char *)sqlite3_column_text(stmt, 0);
+ names = g_list_prepend(names, g_strdup(name));
+ }
+ sqlite3_finalize(stmt);
+
+ gint res = GTK_RESPONSE_OK;
+ GtkWidget *dialog = gtk_dialog_new_with_buttons(_("rename preset"), GTK_WINDOW(d->dialog),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ _("_cancel"), GTK_RESPONSE_CANCEL,
+ _("_rename"), GTK_RESPONSE_OK, NULL);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
+#ifdef GDK_WINDOWING_QUARTZ
+ dt_osx_disallow_fullscreen(dialog);
+#endif
+ GtkWidget *lb = gtk_label_new(_("a preset with this name already exists!"));
+ GtkWidget *tb = gtk_entry_new();
+ gtk_entry_set_activates_default(GTK_ENTRY(tb), TRUE);
+ gtk_entry_set_width_chars(GTK_ENTRY(tb),
+ 10 + g_utf8_strlen(gtk_window_get_title(GTK_WINDOW(dialog)),
+ -1));
+ gpointer verify_params[] = {dialog, names, lb};
+ g_signal_connect(G_OBJECT(tb), "changed",
+ G_CALLBACK(_manage_editor_preset_name_verify), verify_params);
+ dt_gui_dialog_add(GTK_DIALOG(dialog), gtk_label_new(_("new preset name:")), tb, lb);
+ gtk_widget_show_all(dialog);
+ gtk_entry_set_text(GTK_ENTRY(tb), new_name);
+ res = gtk_dialog_run(GTK_DIALOG(dialog));
+
+ g_free(new_name);
+
+ if(res == GTK_RESPONSE_OK)
+ {
+ if(btn == d->presets_btn_rename)
+ {
+ // we update the database
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "UPDATE data.presets"
+ " SET name=?1"
+ " WHERE name=?2 AND operation = ?3 AND op_version = ?4",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1,
+ gtk_entry_get_text(GTK_ENTRY(tb)), -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, d->edit_preset, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 4, self->version());
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+
+ // we update the presets list
+ g_free(d->edit_preset);
+ d->edit_preset = g_strdup(gtk_entry_get_text(GTK_ENTRY(tb)));
+ d->editor_reset = TRUE;
+ _manage_preset_update_list(self);
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(d->presets_combo), d->edit_preset);
+ d->editor_reset = FALSE;
+ }
+ else if(btn == d->presets_btn_new)
+ {
+ // create a new minimal preset
+ char *tx = _presets_get_minimal(self);
+ dt_lib_presets_add(gtk_entry_get_text(GTK_ENTRY(tb)),
+ self->plugin_name, self->version(), tx, strlen(tx),
+ FALSE, 0);
+ g_free(tx);
+ // update the presets list
+ d->editor_reset = TRUE;
+ _manage_preset_update_list(self);
+ d->editor_reset = FALSE;
+ // select the new preset
+ _manage_editor_load(gtk_entry_get_text(GTK_ENTRY(tb)), self);
+ }
+ else if(btn == d->presets_btn_dup)
+ {
+ char *tx = _preset_to_string(self, TRUE);
+ dt_lib_presets_add(gtk_entry_get_text(GTK_ENTRY(tb)),
+ self->plugin_name, self->version(), tx, strlen(tx),
+ FALSE, 0);
+ g_free(tx);
+ // update the presets list
+ d->editor_reset = TRUE;
+ _manage_preset_update_list(self);
+ d->editor_reset = FALSE;
+ // select the new preset
+ _manage_editor_load(gtk_entry_get_text(GTK_ENTRY(tb)), self);
+ }
+ }
+
+ gtk_widget_destroy(dialog);
+ g_list_free_full(names, g_free);
+}
+
+static void _preset_autoapply_edit(GtkButton *button,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(d->editor_reset) return;
+ sqlite3_stmt *stmt;
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT rowid"
+ " FROM data.presets"
+ " WHERE operation = ?1 AND op_version = ?2 AND name = ?3",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, self->version());
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, d->edit_preset, -1, SQLITE_TRANSIENT);
+
+ if(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ const int rowid = sqlite3_column_int(stmt, 0);
+ sqlite3_finalize(stmt);
+ dt_gui_presets_show_edit_dialog(d->edit_preset,
+ rowid, G_CALLBACK(_preset_autoapply_changed),
+ self, FALSE, FALSE, FALSE, GTK_WINDOW(d->dialog));
+ }
+ else
+ sqlite3_finalize(stmt);
+}
+
+static void _manage_editor_load(const char *preset,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // if we have a currently edited preset, we save it
+ if(d->edit_preset && g_strcmp0(preset, d->edit_preset) != 0)
+ {
+ _manage_editor_save(self);
+ }
+
+ // we avoid widgets events to be executed
+ d->editor_reset = TRUE;
+
+ // we remove all widgets from the groups-box
+ dt_gui_container_destroy_children(GTK_CONTAINER(d->preset_groups_box));
+ gtk_box_set_homogeneous(GTK_BOX(d->preset_groups_box), TRUE);
+
+ // we select the right preset in the combobox (or we select the first one)
+ gboolean sel_ok = FALSE;
+ if(preset) sel_ok = gtk_combo_box_set_active_id(GTK_COMBO_BOX(d->presets_combo), preset);
+ if(!sel_ok) gtk_combo_box_set_active(GTK_COMBO_BOX(d->presets_combo), 0);
+ const gchar *sel_preset = gtk_combo_box_get_active_id(GTK_COMBO_BOX(d->presets_combo));
+
+ // get all presets groups
+ if(d->edit_groups) _manage_editor_groups_cleanup(self, TRUE);
+ if(d->edit_preset) g_free(d->edit_preset);
+ d->edit_groups = NULL;
+ d->edit_preset = NULL;
+ sqlite3_stmt *stmt;
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT writeprotect, op_params, autoapply"
+ " FROM data.presets"
+ " WHERE operation = ?1 AND op_version = ?2 AND name = ?3",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, self->version());
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, sel_preset, -1, SQLITE_TRANSIENT);
+
+ int autoapply = 0;
+ if(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ d->edit_ro = sqlite3_column_int(stmt, 0);
+ const void *blob = sqlite3_column_blob(stmt, 1);
+ _preset_from_string(self, (char *)blob, TRUE);
+ d->edit_basics_box = NULL;
+ d->edit_basics_groupbox = NULL;
+ d->edit_preset = g_strdup(sel_preset);
+
+ autoapply = sqlite3_column_int(stmt, 2);
+ sqlite3_finalize(stmt);
+ }
+ else
+ {
+ d->editor_reset = FALSE;
+ sqlite3_finalize(stmt);
+ return;
+ }
+
+ // presets buttons
+ gtk_widget_set_sensitive(d->presets_btn_rename, !d->edit_ro);
+ gtk_widget_set_sensitive(d->presets_btn_remove, !d->edit_ro);
+ gtk_widget_set_sensitive(d->presets_btn_dup, g_strcmp0(sel_preset,
+ _(DEPRECATED_PRESET_NAME)));
+
+ // search checkbox
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_search_cb), d->edit_show_search);
+ gtk_widget_set_sensitive(d->edit_search_cb, !d->edit_ro);
+
+ // full_active checkbox
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_full_active_cb),
+ d->edit_full_active);
+ gtk_widget_set_sensitive(d->edit_full_active_cb, !d->edit_ro);
+
+ // basics checkbox
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->basics_chkbox), d->edit_basics_show);
+ gtk_widget_set_sensitive(d->basics_chkbox, !d->edit_ro);
+
+ // autoapply
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->edit_autoapply_chkbox), autoapply);
+ gtk_widget_set_sensitive(d->edit_autoapply_btn, !d->edit_ro);
+
+ // new group button
+ gtk_widget_set_visible(d->preset_btn_add_group, !d->edit_ro);
+
+ // set up basics widgets
+ d->edit_basics_groupbox = _manage_editor_group_init_basics_box(self);
+ gtk_box_pack_start(GTK_BOX(d->preset_groups_box),
+ d->edit_basics_groupbox, FALSE, TRUE, 0);
+ gtk_widget_show_all(d->edit_basics_groupbox);
+ gtk_widget_set_no_show_all(d->edit_basics_groupbox, TRUE);
+ gtk_widget_set_visible(d->edit_basics_groupbox, d->edit_basics_show);
+
+ // other groups
+ for(const GList *l = d->edit_groups; l; l = g_list_next(l))
+ {
+ dt_lib_modulegroups_group_t *gr = l->data;
+ GtkWidget *vb2 = _manage_editor_group_init_modules_box(self, gr);
+ gtk_widget_show_all(vb2);
+ gtk_box_pack_start(GTK_BOX(d->preset_groups_box), vb2, FALSE, TRUE, 0);
+ }
+
+ // read-only message
+ gtk_widget_set_visible(d->preset_read_only_label, d->edit_ro);
+
+ // reset button
+ gtk_widget_set_visible(d->preset_reset_btn, !d->edit_ro);
+
+ // and we update arrows
+ if(!d->edit_ro) _manage_editor_group_update_arrows(d->preset_groups_box);
+
+ d->editor_reset = FALSE;
+ // set keyboard focus on the scrollable window (not on a widget)
+ // gtk_widget_grab_focus(sw);
+}
+
+static void _manage_preset_change(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+ if(d->editor_reset) return;
+ const char *preset = gtk_combo_box_get_active_id(GTK_COMBO_BOX(d->presets_combo));
+ _manage_editor_load(preset, self);
+}
+
+static void _manage_preset_delete(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ if(!dt_conf_get_bool("plugins/lighttable/preset/ask_before_delete_preset")
+ || dt_gui_show_yes_no_dialog(_("delete preset?"), "",
+ _("do you really want to delete the preset `%s'?"),
+ d->edit_preset))
+ {
+ dt_lib_presets_remove(d->edit_preset, self->plugin_name, self->version());
+
+ // if the deleted preset was the one currently in use, load default preset
+ if(dt_conf_key_exists("plugins/darkroom/modulegroups_preset"))
+ {
+ const char *cur = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
+ if(g_strcmp0(cur, d->edit_preset) == 0)
+ {
+ dt_conf_set_string("plugins/darkroom/modulegroups_preset",
+ C_("modulegroup", FALLBACK_PRESET_NAME));
+ dt_lib_presets_apply((gchar *)C_("modulegroup", FALLBACK_PRESET_NAME),
+ self->plugin_name, self->version());
+ }
+ }
+
+ // reload presets list
+ _manage_preset_update_list(self);
+ // we fallback to the first preset
+ _manage_editor_load(NULL, self);
+ }
+}
+
+static void _manage_preset_update_list(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we first remove all existing entries from the combobox
+ gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(d->presets_combo));
+
+ // and we repopulate it
+ sqlite3_stmt *stmt;
+ // order: get shipped defaults first
+ // clang-format off
+ DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
+ "SELECT name"
+ " FROM data.presets"
+ " WHERE operation=?1 AND op_version=?2"
+ " ORDER BY writeprotect DESC, name, rowid",
+ -1, &stmt, NULL);
+ // clang-format on
+ DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, self->plugin_name, -1, SQLITE_TRANSIENT);
+ DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, self->version());
+
+ while(sqlite3_step(stmt) == SQLITE_ROW)
+ {
+ const char *name = (char *)sqlite3_column_text(stmt, 0);
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->presets_combo), name, name);
+ }
+ sqlite3_finalize(stmt);
+}
+
+static void _manage_editor_destroy(GtkWidget *widget,
+ dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ // we save the last edited preset
+ _manage_editor_save(self);
+
+ // and we free editing data
+ if(d->edit_groups) _manage_editor_groups_cleanup(self, TRUE);
+ if(d->edit_preset) g_free(d->edit_preset);
+ d->edit_groups = NULL;
+ d->edit_preset = NULL;
+}
+
+static void _manage_show_window(dt_lib_module_t *self)
+{
+ dt_lib_modulegroups_t *d = self->data;
+
+ GtkWindow *win = GTK_WINDOW(dt_ui_main_window(darktable.gui->ui));
+ d->dialog = gtk_dialog_new_with_buttons
+ (_("manage module layouts"), win,
+ GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, NULL, NULL);
+
+#ifdef GDK_WINDOWING_QUARTZ
+ dt_osx_disallow_fullscreen(d->dialog);
+#endif
+ dt_gui_dialog_restore_size(GTK_DIALOG(d->dialog), "modulegroups");
+ gtk_widget_set_name(d->dialog, "modulegroups-manager");
+ gtk_window_set_title(GTK_WINDOW(d->dialog), _("manage module layouts"));
+ // remove the small border
+ GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(d->dialog));
+ gtk_container_set_border_width(GTK_CONTAINER(content), 0);
+
+ GtkWidget *vb_main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // main box
+ // preset combobox
+ GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb, "modulegroups-topbox");
+ GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(vb, "modulegroups-top-boxes");
+ GtkWidget *hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_pack_start(GTK_BOX(hb2), gtk_label_new(_("preset: ")), FALSE, TRUE, 2);
+ d->presets_combo = gtk_combo_box_text_new();
+ g_signal_connect(G_OBJECT(d->presets_combo), "changed",
+ G_CALLBACK(_manage_preset_change), self);
+ gtk_box_pack_start(GTK_BOX(hb2), d->presets_combo, TRUE, TRUE, 2);
+ gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 2);
+ // presets buttons
+ hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ d->presets_btn_remove = dt_action_button_new(NULL, N_("remove"),
+ _manage_preset_delete,
+ self,_("remove the preset"), 0, 0);
+ gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_remove, TRUE, TRUE, 2);
+ d->presets_btn_dup = dt_action_button_new(NULL, N_("duplicate"),
+ _manage_editor_preset_action,
+ self,_("duplicate the preset"), 0, 0);
+ gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_dup, TRUE, TRUE, 2);
+ d->presets_btn_rename = dt_action_button_new(NULL, N_("rename"),
+ _manage_editor_preset_action,
+ self,_("rename the preset"), 0, 0);
+ gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_rename, TRUE, TRUE, 2);
+ d->presets_btn_new = dt_action_button_new(NULL, N_("new"),
+ _manage_editor_preset_action,
+ self,_("create a new empty preset"), 0, 0);
+ gtk_box_pack_start(GTK_BOX(hb2), d->presets_btn_new, TRUE, TRUE, 2);
+ gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 2);
+ gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 2);
+
+ // presets settings (search + quick access + full active)
+ vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(vb, "modulegroups-top-boxes");
+ d->edit_search_cb = gtk_check_button_new_with_label(_("show search line"));
+ g_signal_connect(G_OBJECT(d->edit_search_cb), "toggled",
+ G_CALLBACK(_manage_editor_search_toggle), self);
+ gtk_box_pack_start(GTK_BOX(vb), d->edit_search_cb, FALSE, TRUE, 0);
+ d->basics_chkbox = gtk_check_button_new_with_label(_("show quick access panel"));
+ g_signal_connect(G_OBJECT(d->basics_chkbox), "toggled",
+ G_CALLBACK(_manage_editor_basics_toggle), self);
+ gtk_box_pack_start(GTK_BOX(vb), d->basics_chkbox, FALSE, TRUE, 0);
+ d->edit_full_active_cb =
+ gtk_check_button_new_with_label(_("show all history modules in active group"));
+ gtk_widget_set_tooltip_text(
+ d->edit_full_active_cb,
+ _("show modules that are present in the history stack,"
+ " regardless of whether or not they are currently enabled"));
+ g_signal_connect(G_OBJECT(d->edit_full_active_cb), "toggled",
+ G_CALLBACK(_manage_editor_full_active_toggle),
+ self);
+ gtk_box_pack_start(GTK_BOX(vb), d->edit_full_active_cb, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 0);
+
+ // presets settings (autoapply)
+ vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ d->edit_autoapply_chkbox = gtk_check_button_new_with_label(_("auto-apply this preset"));
+ gtk_widget_set_sensitive(d->edit_autoapply_chkbox, FALSE); // always readonly. change are done with the button...
+ gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_chkbox, FALSE, TRUE, 0);
+ d->edit_autoapply_btn = dtgtk_button_new(dtgtk_cairo_paint_preferences, 0, NULL);
+ g_signal_connect(G_OBJECT(d->edit_autoapply_btn), "clicked",
+ G_CALLBACK(_preset_autoapply_edit), self);
+ gtk_widget_set_name(d->edit_autoapply_btn, "modulegroups-autoapply-btn");
+ gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_btn, FALSE, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(vb_main), hb, FALSE, TRUE, 0);
+
+ // groups title line
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(hb, "modulegroups-groups-title");
+ gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("module groups")), FALSE, TRUE, 0);
+ d->preset_btn_add_group = dtgtk_button_new(dtgtk_cairo_paint_square_plus,
+ CPF_DIRECTION_LEFT, NULL);
+ g_signal_connect(G_OBJECT(d->preset_btn_add_group), "clicked",
+ G_CALLBACK(_manage_editor_group_add),
+ self);
+ gtk_box_pack_start(GTK_BOX(hb), d->preset_btn_add_group, FALSE, FALSE, 0);
+ gtk_widget_set_halign(hb, GTK_ALIGN_CENTER);
+ gtk_box_pack_start(GTK_BOX(vb_main), hb, FALSE, TRUE, 0);
+
+ // groups line
+ d->preset_groups_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_name(d->preset_groups_box, "modulegroups-groups-box");
+ gtk_widget_set_vexpand(d->preset_groups_box, TRUE);
+ gtk_widget_set_halign(d->preset_groups_box, GTK_ALIGN_FILL);
+ gtk_box_pack_start(GTK_BOX(vb_main), d->preset_groups_box, TRUE, TRUE, 0);
+
+ // read only message
+ d->preset_read_only_label
+ = gtk_label_new(_("this is a built-in read-only preset."
+ " duplicate it if you want to make changes"));
+ gtk_widget_set_name(d->preset_read_only_label, "modulegroups-ro");
+ gtk_box_pack_start(GTK_BOX(vb_main), d->preset_read_only_label, FALSE, TRUE, 0);
+
+ // reset button
+ hb2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ d->preset_reset_btn = gtk_button_new_with_label(_("reset"));
+ g_signal_connect(G_OBJECT(d->preset_reset_btn), "clicked",
+ G_CALLBACK(_manage_editor_reset), self);
+ gtk_box_pack_end(GTK_BOX(hb2), d->preset_reset_btn, FALSE, TRUE, 0);
+
+ GtkWidget *help = gtk_button_new_with_label(_("?"));
+ dt_gui_add_help_link(help, "modulegroups");
+ g_signal_connect(help, "clicked", G_CALLBACK(dt_gui_show_help), NULL);
+ gtk_box_pack_end(GTK_BOX(hb2), help, FALSE, FALSE, 0);
+
+ gtk_box_pack_start(GTK_BOX(vb_main), hb2, FALSE, TRUE, 0);
+
+ // we load the presets list
+ _manage_preset_update_list(self);
+
+ gtk_widget_show_all(vb_main);
+
+ // and we select the current one
+ const char *preset = dt_conf_get_string_const("plugins/darkroom/modulegroups_preset");
+ _manage_editor_load(preset, self);
+
+ dt_gui_dialog_add(GTK_DIALOG(d->dialog), vb_main);
+
+ g_signal_connect(d->dialog, "destroy", G_CALLBACK(_manage_editor_destroy), self);
+ gtk_window_set_resizable(GTK_WINDOW(d->dialog), TRUE);
+
+ gtk_window_set_position(GTK_WINDOW(d->dialog), GTK_WIN_POS_CENTER_ON_PARENT);
+ gtk_widget_show(d->dialog);
+}
+
+
+void manage_presets(dt_lib_module_t *self)
+{
+ _manage_show_window(self);
+}
+
+void view_leave(dt_lib_module_t *self,
+ dt_view_t *old_view,
+ dt_view_t *new_view)
+{
+ if(!strcmp(old_view->module_name, "darkroom"))
+ {
+ _basics_hide(self);
+ }
+}
+
+void view_enter(dt_lib_module_t *self,
+ dt_view_t *old_view,
+ dt_view_t *new_view)
+{
+ if(!strcmp(new_view->module_name, "darkroom"))
+ {
+ dt_lib_modulegroups_t *d = self->data;
+
+ // and we initialize the buttons too
+ char *preset = dt_conf_get_string("plugins/darkroom/modulegroups_preset");
+ if(!dt_lib_presets_apply(preset, self->plugin_name, self->version()))
+ dt_lib_presets_apply(_(FALLBACK_PRESET_NAME), self->plugin_name, self->version());
+ g_free(preset);
+
+ // and set the current group
+ d->current = dt_conf_get_int("plugins/darkroom/groups");
+ }
+}
+
+gboolean preset_autoapply(dt_lib_module_t *self)
+{
+ return TRUE;
+}
+
+#undef PADDING
+// clang-format off
+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
+// vim: shiftwidth=2 expandtab tabstop=2 cindent
+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
+// clang-format on
\ No newline at end of file
diff --git a/src/gui/presets.c b/src/gui/presets.c
index 77b49b19c81d..efe42545710a 100644
--- a/src/gui/presets.c
+++ b/src/gui/presets.c
@@ -1205,17 +1205,21 @@ gboolean dt_gui_presets_autoapply_for_module(dt_iop_module_t *module, GtkWidget
" OR (name = ?13)) AND op_version = ?14"
" ORDER BY writeprotect ASC, rowid DESC",
format_filter,
- is_display_referred?"":"basecurve");
+ ""); // The basecurve is no longer excluded in non-display mode.
// clang-format on
g_free(format_filter);
sqlite3_stmt *stmt;
+ // display-referred : legacy curve
+ // scene-referred : preset "display-referred default"
const char *workflow_preset = has_matrix && is_display_referred
? BUILTIN_PRESET("display-referred default")
: (has_matrix && is_scene_referred
? BUILTIN_PRESET("scene-referred default")
- : "\t\n");
+ : (has_matrix
+ ? BUILTIN_PRESET("display-referred default")
+ : "\t\n"));
DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, module->op, -1, SQLITE_TRANSIENT);
@@ -2044,4 +2048,4 @@ void dt_gui_presets_update_filter(const char *name,
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
-// clang-format on
+// clang-format on
\ No newline at end of file
diff --git a/src/iop/basecurve.c b/src/iop/basecurve.c
old mode 100644
new mode 100755
index 260e53913870..476c694eb840
--- a/src/iop/basecurve.c
+++ b/src/iop/basecurve.c
@@ -1,7 +1,6 @@
/*
This file is part of darktable,
Copyright (C) 2010-2026 darktable developers.
-
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
@@ -48,7 +47,7 @@
#define MAXNODES 20
-DT_MODULE_INTROSPECTION(6, dt_iop_basecurve_params_t)
+DT_MODULE_INTROSPECTION(7, dt_iop_basecurve_params_t)
typedef struct dt_iop_basecurve_node_t
{
@@ -63,15 +62,34 @@ typedef struct dt_iop_basecurve_params_t
dt_iop_basecurve_node_t basecurve[3][MAXNODES];
int basecurve_nodes[3]; // $MIN: 0 $MAX: MAXNODES $DEFAULT: 0
int basecurve_type[3]; // $MIN: 0 $MAX: MONOTONE_HERMITE $DEFAULT: MONOTONE_HERMITE
- int exposure_fusion; /* number of exposure fusion steps
- $DEFAULT: 0 $DESCRIPTION: "fusion" */
- float exposure_stops; /* number of stops between fusion images
- $MIN: 0.01 $MAX: 4.0 $DEFAULT: 1.0 $DESCRIPTION: "exposure shift" */
- float exposure_bias; /* whether to do exposure-fusion with over or under-exposure
- $MIN: -1.0 $MAX: 1.0 $DEFAULT: 1.0 $DESCRIPTION: "exposure bias" */
+ int exposure_fusion; // number of exposure fusion steps $DEFAULT: 0 $DESCRIPTION: "fusion"
+ float exposure_stops; // number of stops between fusion images $MIN: 0.01 $MAX: 4.0 $DEFAULT: 1.0 $DESCRIPTION: "exposure shift"
+ float exposure_bias; // whether to do exposure-fusion with over or under-exposure $MIN: -1.0 $MAX: 1.0 $DEFAULT: 1.0 $DESCRIPTION: "exposure bias"
dt_iop_rgb_norms_t preserve_colors; /* $DEFAULT: DT_RGB_NORM_LUMINANCE $DESCRIPTION: "preserve colors" */
+ int workflow_mode; // $DEFAULT: 1
+ float shadow_lift; // $MIN: 0.25 $MAX: 1.75 $DEFAULT: 1.0 $DESCRIPTION: "shadow correction"
+ float highlight_gain; // $MIN: 0.25 $MAX: 1.75 $DEFAULT: 1.0 $DESCRIPTION: "highlight gain"
+ float ucs_saturation_balance; // $MIN: -0.75 $MAX: 0.75 $DEFAULT: 0.2 $DESCRIPTION: "balance saturation ucs"
+ float gamut_strength; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "gamut compression"
+ float highlight_corr; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlight hue/sat"
+ int target_gamut; // $DEFAULT: 2 $DESCRIPTION: "target gamut"
+ int color_look; // $DEFAULT: 0 $DESCRIPTION: "color look style"
+ float look_opacity; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 1.0 $DESCRIPTION: "look opacity"
} dt_iop_basecurve_params_t;
+static const float color_looks[10][10] = {
+ {1.000f, 0.000f, 0.000f, 0.000f, 1.000f, 0.000f, 0.000f, 0.000f, 1.000f}, // 1. neutral
+ {0.932f, 0.051f, 0.017f, 0.021f, 0.945f, 0.034f, 0.011f, 0.025f, 0.964f}, // 2. natural look
+ {1.029f, -0.008f, -0.074f, -0.023f, 1.008f, 0.046f, -0.002f, 0.007f, 1.010f}, // 3. portrait
+ {1.084f, -0.006f, -0.093f, -0.074f, 1.008f, 0.060f, -0.011f, 0.005f, 1.024f}, // 4. nature
+ {1.074f, 0.006f, -0.103f, -0.054f, 1.009f, 0.060f, -0.071f, -0.059f, 1.086f}, // 5. vibrant
+ {1.218f, 0.007f, -0.192f, -0.119f, 1.076f, 0.048f, -0.099f, -0.069f, 1.154f}, // 6. blue sky
+ {1.082f, -0.020f, 0.103f, -0.051f, 1.052f, 0.042f, -0.047f, -0.045f, 1.073f}, // 7. soft warm
+ {1.050f, 0.020f, -0.010f, -0.020f, 1.020f, 0.000f, -0.010f, -0.020f, 1.030f}, // 8. soft
+ {0.980f, -0.010f, -0.010f, 0.000f, 1.050f, -0.020f, 0.020f, 0.010f, 1.100f}, // 9. deep cool
+ {1.020f, -0.010f, -0.010f, -0.030f, 1.040f, -0.010f, 0.000f, -0.030f, 1.030f} // 10. authentic cinema
+};
+
int legacy_params(dt_iop_module_t *self,
const void *const old_params,
const int old_version,
@@ -227,6 +245,26 @@ int legacy_params(dt_iop_module_t *self,
*new_version = 6;
return 0;
}
+ if(old_version == 6)
+ {
+ const dt_iop_basecurve_params_v6_t *o = (dt_iop_basecurve_params_v6_t *)old_params;
+ dt_iop_basecurve_params_t *n = calloc(1, sizeof(dt_iop_basecurve_params_t));
+ memcpy(n, o, sizeof(dt_iop_basecurve_params_v6_t));
+ n->workflow_mode = 0;
+ n->shadow_lift = 1.0f;
+ n->highlight_gain = 1.0f;
+ n->ucs_saturation_balance = 0.2f;
+ n->gamut_strength = 0.0f;
+ n->highlight_corr = 0.0f;
+ n->target_gamut = 2;
+ n->color_look = 0;
+ n->look_opacity = 1.0f;
+
+ *new_params = n;
+ *new_params_size = sizeof(dt_iop_basecurve_params_t);
+ *new_version = 7;
+ return 0;
+ }
return 1;
}
@@ -234,18 +272,23 @@ typedef struct dt_iop_basecurve_gui_data_t
{
dt_draw_curve_t *minmax_curve; // curve for gui to draw
int minmax_curve_type, minmax_curve_nodes;
- GtkBox *hbox;
GtkDrawingArea *area;
- GtkWidget *fusion, *exposure_step, *exposure_bias;
+ GtkWidget *fusion, *exposure_step, *exposure_bias, *shadow_lift, *highlight_gain;
GtkWidget *cmb_preserve_colors;
+ GtkWidget *workflow_mode;
double mouse_x, mouse_y;
int selected;
- double selected_offset, selected_y, selected_min, selected_max;
- float draw_xs[DT_IOP_TONECURVE_RES], draw_ys[DT_IOP_TONECURVE_RES];
- float draw_min_xs[DT_IOP_TONECURVE_RES], draw_min_ys[DT_IOP_TONECURVE_RES];
- float draw_max_xs[DT_IOP_TONECURVE_RES], draw_max_ys[DT_IOP_TONECURVE_RES];
+ float draw_ys[DT_IOP_TONECURVE_RES];
float loglogscale;
GtkWidget *logbase;
+ GtkWidget *ucs_saturation_balance;
+ GtkWidget *gamut_strength;
+ GtkWidget *highlight_corr;
+ GtkWidget *target_gamut;
+ GtkWidget *color_look;
+ GtkWidget *look_opacity;
+ int last_workflow_mode;
+ gboolean look_selected_first_time;
} dt_iop_basecurve_gui_data_t;
typedef struct basecurve_preset_t
@@ -335,6 +378,15 @@ typedef struct dt_iop_basecurve_data_t
float exposure_stops;
float exposure_bias;
int preserve_colors;
+ int workflow_mode;
+ float shadow_lift;
+ float highlight_gain;
+ float ucs_saturation_balance;
+ float gamut_strength;
+ float highlight_corr;
+ int target_gamut;
+ int color_look;
+ float look_opacity;
} dt_iop_basecurve_data_t;
typedef struct dt_iop_basecurve_global_data_t
@@ -370,8 +422,8 @@ const char **description(dt_iop_module_t *self)
_("apply a view transform based on personal or camera maker look,\n"
"for corrective purposes, to prepare images for display"),
_("corrective"),
- _("linear, RGB, display-referred"),
- _("non-linear, RGB"),
+ _("linear, RGB, scene-referred"),
+ _("linear, non-linear, RGB"),
_("non-linear, RGB, display-referred"));
}
@@ -490,6 +542,8 @@ void reload_defaults(dt_iop_module_t *self)
{
dt_iop_basecurve_params_t *const d = self->default_params;
+ *d = basecurve_presets[0].params;
+
if(self->multi_priority == 0)
{
const dt_image_t *const image = &(self->dev->image_storage);
@@ -529,6 +583,24 @@ void reload_defaults(dt_iop_module_t *self)
d->exposure_stops = 1.0f;
d->exposure_bias = 1.0f;
}
+
+ d->target_gamut = 2;
+
+ if(!dt_is_display_referred())
+ {
+ // Force kinematic defaults for scene-referred workflow:
+ d->workflow_mode = 1;
+ d->shadow_lift = 1.0f;
+ d->highlight_gain = 1.0f;
+ d->ucs_saturation_balance = 0.2f;
+ d->color_look = 0; // Neutral look
+ d->look_opacity = 1.0f;
+
+ d->basecurve_nodes[0] = 2;
+ d->basecurve_type[0] = CUBIC_SPLINE;
+ d->basecurve[0][0].x = 0.0f; d->basecurve[0][0].y = 0.0f;
+ d->basecurve[0][1].x = 1.0f; d->basecurve[0][1].y = 1.0f;
+ }
}
void init_presets(dt_iop_module_so_t *self)
@@ -547,6 +619,20 @@ void init_presets(dt_iop_module_so_t *self)
const gboolean is_display_referred = dt_is_display_referred();
+ if(!is_display_referred)
+ {
+ dt_gui_presets_add_generic
+ (_("scene-referred default"), self->op, self->version(),
+ NULL, 0,
+ TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY);
+
+ dt_gui_presets_update_format(BUILTIN_PRESET("scene-referred default"), self->op,
+ self->version(), FOR_RAW);
+
+ dt_gui_presets_update_autoapply(BUILTIN_PRESET("scene-referred default"),
+ self->op, self->version(), TRUE);
+ }
+
if(is_display_referred)
{
dt_gui_presets_add_generic
@@ -583,12 +669,12 @@ int gauss_blur_cl(dt_iop_module_t *self,
cl_int err = DT_OPENCL_DEFAULT_ERROR;
const int devid = piece->pipe->devid;
- /* horizontal blur */
+ //horizontal blur
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_blur_h, width, height,
CLARG(dev_in), CLARG(dev_tmp), CLARG(width), CLARG(height));
if(err != CL_SUCCESS) return FALSE;
- /* vertical blur */
+ // vertical blur
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_blur_v, width, height,
CLARG(dev_tmp), CLARG(dev_out), CLARG(width), CLARG(height));
if(err != CL_SUCCESS) return FALSE;
@@ -687,12 +773,18 @@ int process_cl_fusion(dt_iop_module_t *self,
cl_mem *dev_col = calloc(num_levels_max, sizeof(cl_mem));
cl_mem *dev_comb = calloc(num_levels_max, sizeof(cl_mem));
+ if(!dev_col || !dev_comb) goto error;
cl_mem dev_tmp1 = NULL;
cl_mem dev_tmp2 = NULL;
cl_mem dev_m = NULL;
cl_mem dev_coeffs = NULL;
+ // Prepare Color Look matrix (9 floats packed into float16 for OpenCL)
+ float look_mat_buf[16] = {0.0f};
+ for(int i=0; i<9; i++) look_mat_buf[i] = color_looks[d->color_look][i];
+ const float alpha = 0.6f;
+
const int use_work_profile = (work_profile == NULL) ? 0 : 1;
const int preserve_colors = d->preserve_colors;
@@ -743,24 +835,27 @@ int process_cl_fusion(dt_iop_module_t *self,
for(int e = 0; e < d->exposure_fusion + 1; e++)
{
- // for every exposure fusion image: push by some ev, apply base curve and compute features
{
const float mul = exposure_increment(d->exposure_stops, e, d->exposure_fusion, d->exposure_bias);
if(d->preserve_colors == DT_RGB_NORM_NONE)
+ {
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_legacy_lut, width, height,
- CLARG(dev_in), CLARG(dev_tmp1),
- CLARG(width), CLARG(height), CLARG(mul), CLARG(dev_m), CLARG(dev_coeffs));
+ CLARG(dev_in), CLARG(dev_tmp1), CLARG(width), CLARG(height),
+ CLARGFLOAT(mul), CLARG(dev_m), CLARG(dev_coeffs));
+ if(err != CL_SUCCESS) goto error;
+ }
else
- err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_lut, width, height,
- CLARG(dev_in), CLARG(dev_tmp1), CLARG(width),
- CLARG(height), CLARG(mul), CLARG(dev_m), CLARG(dev_coeffs), CLARG(preserve_colors), CLARG(dev_profile_info),
- CLARG(dev_profile_lut), CLARG(use_work_profile));
- if(err != CL_SUCCESS) goto error;
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_lut, width, height, CLARG(dev_in),
+ CLARG(dev_tmp1), CLARG(width), CLARG(height), CLARGFLOAT(mul),
+ CLARG(dev_m), CLARG(dev_coeffs), CLARG(preserve_colors),
+ CLARG(dev_profile_info), CLARG(dev_profile_lut), CLARG(use_work_profile));
+ if(err != CL_SUCCESS) goto error;
+ }
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_compute_features, width, height,
- CLARG(dev_tmp1), CLARG(dev_col[0]),
- CLARG(width), CLARG(height));
+ CLARG(dev_tmp1), CLARG(dev_col[0]), CLARG(width), CLARG(height));
if(err != CL_SUCCESS) goto error;
}
@@ -881,8 +976,12 @@ int process_cl_fusion(dt_iop_module_t *self,
}
// copy output buffer
+ // Apply shadow_lift and tone mapping here if needed
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_finalize, width, height,
- CLARG(dev_in), CLARG(dev_comb[0]), CLARG(dev_out), CLARG(width), CLARG(height));
+ CLARG(dev_in), CLARG(dev_comb[0]), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(d->workflow_mode),
+ CLARGFLOAT(d->shadow_lift), CLARGFLOAT(d->highlight_gain), CLARGFLOAT(d->ucs_saturation_balance),
+ CLARGFLOAT(d->gamut_strength), CLARGFLOAT(d->highlight_corr), CLARG(d->target_gamut), CLARGFLOAT(d->look_opacity),
+ CLARG(look_mat_buf), CLARGFLOAT(alpha));
error:
for(int k = 0; k < num_levels_max; k++)
@@ -915,7 +1014,8 @@ int process_cl_lut(dt_iop_module_t *self,
cl_mem dev_m = NULL;
cl_mem dev_coeffs = NULL;
- cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ cl_int err = DT_OPENCL_DEFAULT_ERROR;
+ cl_mem dev_tmp = NULL;
cl_mem dev_profile_info = NULL;
cl_mem dev_profile_lut = NULL;
@@ -928,28 +1028,61 @@ int process_cl_lut(dt_iop_module_t *self,
const int height = roi_in->height;
const int preserve_colors = d->preserve_colors;
+ const float mul = 1.0f;
+
dev_m = dt_opencl_copy_host_to_device(devid, d->table, 256, 256, sizeof(float));
- dev_coeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3, d->unbounded_coeffs);
- if(!dev_m || !dev_coeffs) goto error;
+ if(dev_m == NULL) goto error;
err = dt_ioppr_build_iccprofile_params_cl(work_profile, devid, &profile_info_cl, &profile_lut_cl,
&dev_profile_info, &dev_profile_lut);
if(err != CL_SUCCESS) goto error;
+ dev_coeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3, d->unbounded_coeffs);
+
+ if(dev_coeffs == NULL) goto error;
+
+ cl_mem dev_dest = dev_out;
+
+ float look_mat_buf[16] = {0.0f};
+ for(int i=0; i<9; i++) look_mat_buf[i] = color_looks[d->color_look][i];
+ const float alpha = 0.6f;
+
+ if(d->workflow_mode > 0)
+ {
+ dev_tmp = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
+ if(dev_tmp == NULL) goto error;
+ dev_dest = dev_tmp;
+ }
+
// read data/kernels/basecurve.cl for a description of "legacy" vs current
// Conditional is moved outside of the OpenCL operations for performance.
if(d->preserve_colors == DT_RGB_NORM_NONE)
- err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_legacy_lut, width, height,
- CLARG(dev_in), CLARG(dev_out),
- CLARG(width), CLARG(height), CLARGFLOAT(1.0f), CLARG(dev_m), CLARG(dev_coeffs));
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_legacy_lut, width, height, CLARG(dev_in),
+ CLARG(dev_dest), CLARG(width), CLARG(height), CLARGFLOAT(mul), CLARG(dev_m),
+ CLARG(dev_coeffs));
+ }
else
- err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_lut, width, height,
- CLARG(dev_in), CLARG(dev_out),
- CLARG(width), CLARG(height),
- CLARGFLOAT(1.0f), CLARG(dev_m), CLARG(dev_coeffs), CLARG(preserve_colors), CLARG(dev_profile_info),
- CLARG(dev_profile_lut), CLARG(use_work_profile));
+ {
+ //FIXME: There are still conditionals on d->preserve_colors within this flow that could impact performance
+ err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_lut, width, height, CLARG(dev_in),
+ CLARG(dev_dest), CLARG(width), CLARG(height), CLARGFLOAT(mul), CLARG(dev_m),
+ CLARG(dev_coeffs), CLARG(preserve_colors), CLARG(dev_profile_info),
+ CLARG(dev_profile_lut), CLARG(use_work_profile));
+ }
+
+ if(d->workflow_mode > 0)
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_basecurve_finalize, width, height,
+ CLARG(dev_in), CLARG(dev_tmp), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(d->workflow_mode),
+ CLARGFLOAT(d->shadow_lift), CLARGFLOAT(d->highlight_gain), CLARGFLOAT(d->ucs_saturation_balance),
+ CLARGFLOAT(d->gamut_strength), CLARGFLOAT(d->highlight_corr), CLARG(d->target_gamut),
+ CLARGFLOAT(d->look_opacity), CLARG(look_mat_buf), CLARGFLOAT(alpha));
+ if(err != CL_SUCCESS) goto error;
+ }
error:
+ dt_opencl_release_mem_object(dev_tmp);
dt_opencl_release_mem_object(dev_m);
dt_opencl_release_mem_object(dev_coeffs);
dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
@@ -980,22 +1113,58 @@ void tiling_callback(dt_iop_module_t *self,
{
dt_iop_basecurve_data_t *const d = piece->data;
- tiling->maxbuf = 1.0f;
- tiling->overhead = 0;
- tiling->align = 1;
-
if(d->exposure_fusion)
{
const int rad = MIN(roi_in->width, (int)ceilf(256 * roi_in->scale / piece->iscale));
+
tiling->factor = 6.666f; // in + out + col[] + comb[] + 2*tmp
+ tiling->maxbuf = 1.0f;
+ tiling->overhead = 0;
+ tiling->align = 1;
tiling->overlap = rad;
}
else
{
tiling->factor = 2.0f; // in + out
+ tiling->maxbuf = 1.0f;
+ tiling->overhead = 0;
+ tiling->align = 1;
tiling->overlap = 0;
}
}
+/*
+ Narkowicz (2016) rational approximation of the ACES RRT+ODT curve for sRGB output.
+ Widely used in real-time rendering for its simplicity and visual quality.
+ Does NOT implement the full ACES pipeline (no color space transform, no D60 whitepoint).
+ Reference: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+*/
+static inline float _aces_tone_map(const float x)
+{
+ const float a = 2.51f;
+ const float b = 0.03f;
+ const float c = 2.43f;
+ const float d = 0.59f;
+ const float e = 0.14f;
+
+ return CLAMP((x * (a * x + b)) / (x * (c * x + d) + e), 0.0f, 1.0f);
+}
+/*
+ Narkowicz & Filiberto (2021) rational approximation of the ACES 2.0 RRT curve.
+ More precise than the basic Narkowicz 2016 fit, with a softer shoulder.
+ The pre-scale factor (x * 1.680) in the caller adjusts the exposure point (0.75EV).
+ Does NOT implement the full ACES pipeline (no color space transform, no D60 whitepoint).
+ Reference: https://github.com/h3r2tic/tony-mc-mapface (Narkowicz/Filiberto fit)
+*/
+static inline float _aces_20_tonemap(const float x)
+{
+ const float a = 0.0245786f;
+ const float b = 0.000090537f;
+ const float c = 0.983729f;
+ const float d = 0.4329510f;
+ const float e = 0.238081f;
+
+ return CLAMP((x * (x + a) - b) / (x * (c * x + d) + e), 0.0f, 1.0f);
+}
// See comments of opencl version in data/kernels/basecurve.cl for description of the meaning of "legacy"
static inline void apply_legacy_curve(
@@ -1013,12 +1182,16 @@ static inline void apply_legacy_curve(
{
for(int i = 0; i < 3; i++)
{
- const float f = in[k+i] * mul;
+ float f = in[k+i] * mul;
+
+ float val;
// use base curve for values < 1, else use extrapolation.
if(f < 1.0f)
- out[k+i] = fmaxf(table[CLAMP((int)(f * 0x10000ul), 0, 0xffff)], 0.f);
+ val = fmaxf(table[CLAMP((int)(f * 0x10000ul), 0, 0xffff)], 0.f);
else
- out[k+i] = fmaxf(dt_iop_eval_exp(unbounded_coeffs, f), 0.f);
+ val = fmaxf(dt_iop_eval_exp(unbounded_coeffs, f), 0.f);
+
+ out[k+i] = val;
}
out[k+3] = in[k+3];
}
@@ -1049,6 +1222,7 @@ static inline void apply_curve(
const float curve_lum = (lum < 1.0f)
? table[CLAMP((int)(lum * 0x10000ul), 0, 0xffff)]
: dt_iop_eval_exp(unbounded_coeffs, lum);
+
ratio = mul * curve_lum / lum;
}
for(size_t c = 0; c < 3; c++)
@@ -1194,7 +1368,302 @@ static inline void gauss_reduce(
}
}
-void process_fusion(dt_iop_module_t *self,
+static void process_lut(dt_iop_module_t *self,
+ dt_dev_pixelpipe_iop_t *piece,
+ const void *const ivoid,
+ void *const ovoid,
+ const dt_iop_roi_t *const roi_in,
+ const dt_iop_roi_t *const roi_out)
+{
+ const float *const in = (const float *)ivoid;
+ float *const out = (float *)ovoid;
+ dt_iop_basecurve_data_t *const d = piece->data;
+ const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_iop_work_profile_info(piece->module, piece->module->dev->iop);
+
+ const int wd = roi_in->width, ht = roi_in->height;
+
+ if(d->preserve_colors == DT_RGB_NORM_NONE)
+ apply_legacy_curve(in, out, wd, ht, 1.0, d->table, d->unbounded_coeffs);
+ else
+ apply_curve(in, out, wd, ht, d->preserve_colors, 1.0, d->table, d->unbounded_coeffs, work_profile);
+
+ if(d->workflow_mode > 0)
+ {
+ const float *mat = color_looks[d->color_look];
+
+ const size_t npixels = (size_t)wd * ht;
+ DT_OMP_FOR()
+ for(size_t k = 0; k < 4 * npixels; k += 4)
+ {
+ float r = out[k];
+ float g = out[k+1];
+ float b = out[k+2];
+
+ // Sanitize to avoid Inf/NaN issues
+ r = fmaxf(-1e6f, fminf(r, 1e6f));
+ g = fmaxf(-1e6f, fminf(g, 1e6f));
+ b = fmaxf(-1e6f, fminf(b, 1e6f));
+
+ // Apply Color Look
+ const float tr = r * mat[0] + g * mat[1] + b * mat[2];
+ const float tg = r * mat[3] + g * mat[4] + b * mat[5];
+ const float tb = r * mat[6] + g * mat[7] + b * mat[8];
+
+ // Mix with opacity
+ out[k] = r * (1.0f - d->look_opacity) + tr * d->look_opacity;
+ out[k+1] = g * (1.0f - d->look_opacity) + tg * d->look_opacity;
+ out[k+2] = b * (1.0f - d->look_opacity) + tb * d->look_opacity;
+
+ out[k] = fmaxf(out[k], 0.0f);
+ out[k+1] = fmaxf(out[k+1], 0.0f);
+ out[k+2] = fmaxf(out[k+2], 0.0f);
+
+ // Reload for next steps
+ r = out[k];
+ g = out[k+1];
+ b = out[k+2];
+
+ if(d->highlight_gain != 1.0f)
+ {
+ r *= d->highlight_gain;
+ g *= d->highlight_gain;
+ b *= d->highlight_gain;
+ }
+ if(d->shadow_lift != 1.0f)
+ {
+ r = powf(r, d->shadow_lift);
+ g = powf(g, d->shadow_lift);
+ b = powf(b, d->shadow_lift);
+ }
+
+ const float r_coeff = 0.2627f, g_coeff = 0.6780f, b_coeff = 0.0593f;
+ float y_in = r * r_coeff + g * g_coeff + b * b_coeff;
+ float y_out = y_in;
+
+ /* Scene-referred: apply luminance-adaptive shoulder extension for
+ ACES-like tonemapping. Compute perceptual luminance Jz from RGB
+ and derive scale k = 1 + alpha * L^2 where L = clamp(Jz,0,1).
+ Then tone-map x_scaled = y_in / k and rescale result by k to
+ extend the shoulder progressively. Keep alpha constant and
+ avoid changing UI or legacy/display-referred behavior. */
+ if(d->workflow_mode == 1 || d->workflow_mode == 2)
+ {
+ // compute Jz from current RGB (Rec2020 -> XYZ -> JzAzBz)
+ float xyz[3];
+ xyz[0] = 0.636958f * r + 0.144617f * g + 0.168881f * b;
+ xyz[1] = 0.262700f * r + 0.677998f * g + 0.059302f * b;
+ xyz[2] = 0.000000f * r + 0.028073f * g + 1.060985f * b;
+ for(int i=0;i<3;i++) xyz[i] = fmaxf(xyz[i], 0.0f);
+
+ float xyz_scaled[4];
+ xyz_scaled[0] = xyz[0] * 400.0f;
+ xyz_scaled[1] = xyz[1] * 400.0f;
+ xyz_scaled[2] = xyz[2] * 400.0f;
+ xyz_scaled[3] = 0.0f;
+
+ float jab[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+ dt_XYZ_2_JzAzBz(xyz_scaled, jab);
+
+ const float L = fminf(fmaxf(jab[0], 0.0f), 1.0f);
+ const float alpha = 0.6f;
+ const float k_scale = 1.0f + alpha * L * L;
+
+ // scale luminance, apply selected tonemap, then undo scaling
+ const float x_scaled = y_in / k_scale;
+ if(d->workflow_mode == 1)
+ y_out = _aces_tone_map(x_scaled) * k_scale;
+ else /* workflow_mode == 2 */
+ y_out = _aces_20_tonemap(x_scaled * 1.680f) * k_scale; //CB 20260307 1.680 (0.75ev) to better match ACES 1.0 tonemap at mid-tones
+ }
+
+ float gain = y_out / fmaxf(y_in, 1e-6f);
+
+ out[k] = r * gain;
+ out[k+1] = g * gain;
+ out[k+2] = b * gain;
+
+ const float threshold = 0.80f;
+ if(y_out > threshold)
+ {
+ float factor = (y_out - threshold) / (1.0f - threshold);
+ factor = CLAMP(factor, 0.0f, 1.0f);
+ out[k] = out[k] * (1.0f - factor) + y_out * factor;
+ out[k+1] = out[k+1] * (1.0f - factor) + y_out * factor;
+ out[k+2] = out[k+2] * (1.0f - factor) + y_out * factor;
+ }
+
+ if(d->ucs_saturation_balance != 0.0f || d->gamut_strength > 0.0f || d->highlight_corr != 0.0f)
+ {
+ // RGB Rec2020 to XYZ D65
+ float xyz[4];
+ xyz[0] = 0.636958f * out[k] + 0.144617f * out[k+1] + 0.168881f * out[k+2];
+ xyz[1] = 0.262700f * out[k] + 0.677998f * out[k+1] + 0.059302f * out[k+2];
+ xyz[2] = 0.000000f * out[k] + 0.028073f * out[k+1] + 1.060985f * out[k+2];
+
+ for(int i=0; i<3; i++) xyz[i] = fmaxf(xyz[i], 0.0f);
+
+ // XYZ to JzAzBz
+ float jab[4];
+ float xyz_scaled[4];
+ for(int i=0; i<3; i++) xyz_scaled[i] = xyz[i] * 400.0f; // Scale to 400 nits for JzAzBz
+ dt_XYZ_2_JzAzBz(xyz_scaled, jab);
+
+ int modified = 0;
+
+ if(d->ucs_saturation_balance != 0.0f)
+ {
+ // Chroma-based modulation for saturation balance
+ const float r_sat = out[k];
+ const float g_sat = out[k+1];
+ const float b_sat = out[k+2];
+ const float chroma = fmaxf(fmaxf(r_sat, g_sat), b_sat) - fminf(fminf(r_sat, g_sat), b_sat);
+ const float effective_saturation = d->ucs_saturation_balance * fminf(chroma * 2.0f, 1.0f);
+
+ // Apply saturation balance
+ // Use Rec2020 Luminance Y for mask
+ const float Y = xyz[1];
+ const float L = powf(fmaxf(Y, 0.0f), 0.5f);
+ const float fulcrum = 0.5f;
+ const float n = (L - fulcrum) / fulcrum;
+ const float mask_shadow = 1.0f / (1.0f + expf(n * 4.0f));
+ float sat_adjust = effective_saturation * (2.0f * mask_shadow - 1.0f);
+ sat_adjust *= fminf(L * 4.0f, 1.0f);
+ const float sat_factor = 1.0f + sat_adjust;
+ jab[1] *= sat_factor;
+ jab[2] *= sat_factor;
+ modified = 1;
+ }
+
+ if(d->gamut_strength > 0.0f)
+ {
+ const float Y = xyz[1];
+ const float L = powf(fmaxf(Y, 0.0f), 0.5f);
+ const float chroma_factor = 1.0f - d->gamut_strength * (0.2f + 0.2f * L);
+ jab[1] *= chroma_factor;
+ jab[2] *= chroma_factor;
+ modified = 1;
+ }
+
+ if(d->highlight_corr != 0.0f)
+ {
+ // HIGHLIGHT HUE AND SATURATION CORRECTION (sync with OpenCL)
+ // Mask starts at Jz = 0.20 and is full at Jz = 0.90. Linear transition.
+ float hl_mask = CLAMP((jab[0] - 0.20f) / 0.70f, 0.0f, 1.0f);
+
+ if(hl_mask > 0.0f)
+ {
+ // 1. Soft symmetric desaturation (0.75 factor)
+ float desat = 1.0f - (fabsf(d->highlight_corr) * hl_mask * 0.75f);
+ jab[1] *= desat;
+ jab[2] *= desat;
+
+ // 2. Controlled Hue Rotation (2.0 factor)
+ const float angle = d->highlight_corr * hl_mask * 2.0f;
+ const float ca = cosf(angle);
+ const float sa = sinf(angle);
+ const float az = jab[1];
+ const float bz = jab[2];
+ jab[1] = az * ca - bz * sa;
+ jab[2] = az * sa + bz * ca;
+ modified = 1;
+ }
+ }
+
+ if(jab[0] > 0.95f)
+ {
+ const float desat = CLAMP((1.0f - jab[0]) * 20.0f, 0.0f, 1.0f);
+ jab[1] *= desat;
+ jab[2] *= desat;
+ modified = 1;
+ }
+
+ if(modified)
+ {
+ // JzAzBz to XYZ
+ dt_JzAzBz_2_XYZ(jab, xyz_scaled);
+ for(int i=0; i<3; i++) xyz[i] = xyz_scaled[i] / 400.0f;
+
+ // XYZ D65 to RGB Rec2020
+ out[k] = 1.716651f * xyz[0] - 0.355671f * xyz[1] - 0.253366f * xyz[2];
+ out[k+1] = -0.666684f * xyz[0] + 1.616481f * xyz[1] + 0.015768f * xyz[2];
+ out[k+2] = 0.017640f * xyz[0] - 0.042770f * xyz[1] + 0.942103f * xyz[2];
+
+ float min_val = fminf(out[k], fminf(out[k+1], out[k+2]));
+ if(min_val < 0.0f)
+ {
+ float lum = 0.2627f * out[k] + 0.6780f * out[k+1] + 0.0593f * out[k+2];
+ if(lum > 0.0f)
+ {
+ float factor = lum / (lum - min_val);
+ out[k] = lum + factor * (out[k] - lum);
+ out[k+1] = lum + factor * (out[k+1] - lum);
+ out[k+2] = lum + factor * (out[k+2] - lum);
+ }
+ }
+ }
+
+ if(d->gamut_strength > 0.0f)
+ {
+ const float orig_r = out[k];
+ const float orig_g = out[k+1];
+ const float orig_b = out[k+2];
+
+ const float Y = 0.2126f * orig_r + 0.7152f * orig_g + 0.0722f * orig_b;
+ float lum_weight = CLAMP((Y - 0.3f) / (0.8f - 0.3f), 0.0f, 1.0f);
+ lum_weight = lum_weight * lum_weight * (3.0f - 2.0f * lum_weight);
+ const float effective_strength = d->gamut_strength * lum_weight;
+
+ float limit = 0.90f;
+ if(d->target_gamut == 1) limit = 0.95f;
+ else if(d->target_gamut == 2) limit = 1.00f;
+
+ float gamut_threshold = limit * (1.0f - (effective_strength * 0.25f));
+ float max_val = fmaxf(out[k], fmaxf(out[k+1], out[k+2]));
+
+ if(max_val > gamut_threshold)
+ {
+ const float range = limit - gamut_threshold;
+ const float delta = max_val - gamut_threshold;
+ const float compressed = gamut_threshold + range * delta / (delta + range);
+ const float factor = compressed / max_val;
+
+ const float range_blue = 1.1f * range;
+ const float compressed_blue = gamut_threshold + range * delta / (delta + range_blue);
+ const float factor_blue = compressed_blue / max_val;
+
+ out[k] *= factor;
+ out[k+1] *= factor;
+ out[k+2] *= factor_blue;
+ }
+
+ out[k] = orig_r * (1.0f - effective_strength) + out[k] * effective_strength;
+ out[k+1] = orig_g * (1.0f - effective_strength) + out[k+1] * effective_strength;
+ out[k+2] = orig_b * (1.0f - effective_strength) + out[k+2] * effective_strength;
+ }
+ }
+
+ // Final gamut check to preserve hue (exact color)
+ if(out[k] < 0.0f || out[k] > 1.0f || out[k+1] < 0.0f || out[k+1] > 1.0f || out[k+2] < 0.0f || out[k+2] > 1.0f)
+ {
+ const float luma = 0.2627f * out[k] + 0.6780f * out[k+1] + 0.0593f * out[k+2];
+ const float target_luma = CLAMP(luma, 0.0f, 1.0f);
+ float t = 1.0f;
+ if(out[k] < 0.0f) t = fminf(t, target_luma / (target_luma - out[k]));
+ if(out[k+1] < 0.0f) t = fminf(t, target_luma / (target_luma - out[k+1]));
+ if(out[k+2] < 0.0f) t = fminf(t, target_luma / (target_luma - out[k+2]));
+ if(out[k] > 1.0f) t = fminf(t, (1.0f - target_luma) / (out[k] - target_luma));
+ if(out[k+1] > 1.0f) t = fminf(t, (1.0f - target_luma) / (out[k+1] - target_luma));
+ if(out[k+2] > 1.0f) t = fminf(t, (1.0f - target_luma) / (out[k+2] - target_luma));
+ t = fmaxf(0.0f, t);
+ out[k] = target_luma + t * (out[k] - target_luma);
+ out[k+1] = target_luma + t * (out[k+1] - target_luma);
+ out[k+2] = target_luma + t * (out[k+2] - target_luma);
+ }
+ }
+ }
+}
+
+static void process_fusion(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid,
void *const ovoid,
@@ -1297,14 +1766,10 @@ void process_fusion(dt_iop_module_t *self,
{
// blend images into output pyramid
if(k == num_levels - 1) // blend gaussian base
-#ifdef DEBUG_VIS2
- ;
-#else
{
for(int c = 0; c < 3; c++)
comb[k][x + c] += col[k][x + 3] * col[k][x + c];
}
-#endif
else // laplacian
{
for(int c = 0; c < 3; c++)
@@ -1315,7 +1780,6 @@ void process_fusion(dt_iop_module_t *self,
}
}
-#ifndef DEBUG_VIS // DEBUG: switch off when visualising weight buf
// normalise and reconstruct output pyramid buffer coarse to fine
for(int k = num_levels - 1; k >= 0; k--)
{
@@ -1344,14 +1808,269 @@ void process_fusion(dt_iop_module_t *self,
}
}
}
-#endif
+
// copy output buffer
+ const float *mat = color_looks[d->color_look];
DT_OMP_FOR()
for(size_t k = 0; k < (size_t)4 * wd * ht; k += 4)
{
- out[k + 0] = fmaxf(comb[0][k + 0], 0.f);
- out[k + 1] = fmaxf(comb[0][k + 1], 0.f);
- out[k + 2] = fmaxf(comb[0][k + 2], 0.f);
+ float val[3];
+ val[0] = fmaxf(comb[0][k + 0], 0.f);
+ val[1] = fmaxf(comb[0][k + 1], 0.f);
+ val[2] = fmaxf(comb[0][k + 2], 0.f);
+
+ // Sanitize to avoid Inf/NaN issues
+ val[0] = fminf(val[0], 1e6f);
+ val[1] = fminf(val[1], 1e6f);
+ val[2] = fminf(val[2], 1e6f);
+
+ // If using scene-referred workflow
+ if(d->workflow_mode > 0)
+ {
+ // Apply Color Look
+ float r = val[0], g = val[1], b = val[2];
+ float tr = r * mat[0] + g * mat[1] + b * mat[2];
+ float tg = r * mat[3] + g * mat[4] + b * mat[5];
+ float tb = r * mat[6] + g * mat[7] + b * mat[8];
+
+ // Mix with opacity
+ val[0] = r * (1.0f - d->look_opacity) + tr * d->look_opacity;
+ val[1] = g * (1.0f - d->look_opacity) + tg * d->look_opacity;
+ val[2] = b * (1.0f - d->look_opacity) + tb * d->look_opacity;
+
+ val[0] = fmaxf(val[0], 0.0f);
+ val[1] = fmaxf(val[1], 0.0f);
+ val[2] = fmaxf(val[2], 0.0f);
+
+ if(d->highlight_gain != 1.0f) {
+ val[0] *= d->highlight_gain;
+ val[1] *= d->highlight_gain;
+ val[2] *= d->highlight_gain;
+ }
+ if(d->shadow_lift != 1.0f) {
+ val[0] = powf(val[0], d->shadow_lift);
+ val[1] = powf(val[1], d->shadow_lift);
+ val[2] = powf(val[2], d->shadow_lift);
+ }
+
+ const float r_coeff = 0.2627f, g_coeff = 0.6780f, b_coeff = 0.0593f;
+ float y_in = val[0] * r_coeff + val[1] * g_coeff + val[2] * b_coeff;
+ float y_out = y_in;
+
+ if(d->workflow_mode == 1 || d->workflow_mode == 2)
+ {
+ float xyz_local[3];
+ xyz_local[0] = 0.636958f * val[0] + 0.144617f * val[1] + 0.168881f * val[2];
+ xyz_local[1] = 0.262700f * val[0] + 0.677998f * val[1] + 0.059302f * val[2];
+ xyz_local[2] = 0.000000f * val[0] + 0.028073f * val[1] + 1.060985f * val[2];
+ for(int i=0;i<3;i++) xyz_local[i] = fmaxf(xyz_local[i], 0.0f);
+
+ float xyz_scaled_local[4];
+ xyz_scaled_local[0] = xyz_local[0] * 400.0f;
+ xyz_scaled_local[1] = xyz_local[1] * 400.0f;
+ xyz_scaled_local[2] = xyz_local[2] * 400.0f;
+ xyz_scaled_local[3] = 0.0f;
+
+ float jab_local[4] = {0.0f,0.0f,0.0f,0.0f};
+ dt_XYZ_2_JzAzBz(xyz_scaled_local, jab_local);
+
+ const float L = fminf(fmaxf(jab_local[0], 0.0f), 1.0f);
+ const float alpha = 0.6f;
+ const float k_scale = 1.0f + alpha * L * L;
+
+ const float x_scaled = y_in / k_scale;
+ if(d->workflow_mode == 1)
+ y_out = _aces_tone_map(x_scaled) * k_scale;
+ else
+ y_out = _aces_20_tonemap(x_scaled * 1.680f) * k_scale; //CB 20260307 1.680 (0.75ev) to better match ACES 1.0 tonemap at mid-tones
+ }
+
+ float gain = y_out / fmaxf(y_in, 1e-6f);
+
+ val[0] *= gain;
+ val[1] *= gain;
+ val[2] *= gain;
+
+ const float threshold = 0.80f;
+ if(y_out > threshold)
+ {
+ float factor = (y_out - threshold) / (1.0f - threshold);
+ factor = CLAMP(factor, 0.0f, 1.0f);
+ val[0] = val[0] * (1.0f - factor) + y_out * factor;
+ val[1] = val[1] * (1.0f - factor) + y_out * factor;
+ val[2] = val[2] * (1.0f - factor) + y_out * factor;
+ }
+
+ if(d->ucs_saturation_balance != 0.0f || d->gamut_strength > 0.0f || d->highlight_corr != 0.0f)
+ {
+ // RGB Rec2020 to XYZ D65
+ float xyz[4];
+ xyz[0] = 0.636958f * val[0] + 0.144617f * val[1] + 0.168881f * val[2];
+ xyz[1] = 0.262700f * val[0] + 0.677998f * val[1] + 0.059302f * val[2];
+ xyz[2] = 0.000000f * val[0] + 0.028073f * val[1] + 1.060985f * val[2];
+
+ for(int i=0; i<3; i++) xyz[i] = fmaxf(xyz[i], 0.0f);
+
+ // XYZ to JzAzBz
+ float jab[4];
+ float xyz_scaled[4];
+ for(int i=0; i<3; i++) xyz_scaled[i] = xyz[i] * 400.0f; // Scale to 400 nits for JzAzBz
+ dt_XYZ_2_JzAzBz(xyz_scaled, jab);
+
+ int modified = 0;
+
+ if(d->ucs_saturation_balance != 0.0f)
+ {
+ // Chroma-based modulation for saturation balance
+ const float r_sat = val[0];
+ const float g_sat = val[1];
+ const float b_sat = val[2];
+ const float chroma = fmaxf(fmaxf(r_sat, g_sat), b_sat) - fminf(fminf(r_sat, g_sat), b_sat);
+ const float effective_saturation = d->ucs_saturation_balance * fminf(chroma * 2.0f, 1.0f);
+
+ // Apply saturation balance
+ // Use Rec2020 Luminance Y for mask
+ const float Y = xyz[1];
+ const float L = powf(fmaxf(Y, 0.0f), 0.5f);
+ const float fulcrum = 0.5f;
+ const float n = (L - fulcrum) / fulcrum;
+ const float mask_shadow = 1.0f / (1.0f + expf(n * 4.0f));
+ float sat_adjust = effective_saturation * (2.0f * mask_shadow - 1.0f);
+ sat_adjust *= fminf(L * 4.0f, 1.0f);
+ const float sat_factor = 1.0f + sat_adjust;
+ jab[1] *= sat_factor;
+ jab[2] *= sat_factor;
+ modified = 1;
+ }
+
+ if(d->gamut_strength > 0.0f)
+ {
+ const float Y = xyz[1];
+ const float L = powf(fmaxf(Y, 0.0f), 0.5f);
+ const float chroma_factor = 1.0f - d->gamut_strength * (0.2f + 0.2f * L);
+ jab[1] *= chroma_factor;
+ jab[2] *= chroma_factor;
+ modified = 1;
+ }
+
+ if(d->highlight_corr != 0.0f)
+ {
+ // HIGHLIGHT HUE AND SATURATION CORRECTION (sync with OpenCL)
+ // Mask starts at Jz = 0.20 and is full at Jz = 0.90. Linear transition.
+ float hl_mask = CLAMP((jab[0] - 0.20f) / 0.70f, 0.0f, 1.0f);
+
+ if(hl_mask > 0.0f)
+ {
+ // 1. Soft symmetric desaturation (0.75 factor)
+ const float desat = 1.0f - (fabsf(d->highlight_corr) * hl_mask * 0.75f);
+ jab[1] *= desat;
+ jab[2] *= desat;
+
+ // 2. Controlled Hue Rotation (2.0 factor)
+ const float angle = d->highlight_corr * hl_mask * 2.0f;
+ const float ca = cosf(angle);
+ const float sa = sinf(angle);
+ const float az = jab[1];
+ const float bz = jab[2];
+ jab[1] = az * ca - bz * sa;
+ jab[2] = az * sa + bz * ca;
+ modified = 1;
+ }
+ }
+
+ if(jab[0] > 0.95f)
+ {
+ const float desat = CLAMP((1.0f - jab[0]) * 20.0f, 0.0f, 1.0f);
+ jab[1] *= desat;
+ jab[2] *= desat;
+ modified = 1;
+ }
+
+ if(modified)
+ {
+ // JzAzBz to XYZ
+ dt_JzAzBz_2_XYZ(jab, xyz_scaled);
+ for(int i=0; i<3; i++) xyz[i] = xyz_scaled[i] / 400.0f;
+
+ // XYZ D65 to RGB Rec2020
+ val[0] = 1.716651f * xyz[0] - 0.355671f * xyz[1] - 0.253366f * xyz[2];
+ val[1] = -0.666684f * xyz[0] + 1.616481f * xyz[1] + 0.015768f * xyz[2];
+ val[2] = 0.017640f * xyz[0] - 0.042770f * xyz[1] + 0.942103f * xyz[2];
+
+ float min_val = fminf(val[0], fminf(val[1], val[2]));
+ if(min_val < 0.0f)
+ {
+ float lum = 0.2627f * val[0] + 0.6780f * val[1] + 0.0593f * val[2];
+ if(lum > 0.0f)
+ {
+ float factor = lum / (lum - min_val);
+ val[0] = lum + factor * (val[0] - lum);
+ val[1] = lum + factor * (val[1] - lum);
+ val[2] = lum + factor * (val[2] - lum);
+ }
+ }
+ }
+
+ if(d->gamut_strength > 0.0f)
+ {
+ const float orig_r = val[0];
+ const float orig_g = val[1];
+ const float orig_b = val[2];
+
+ const float Y = 0.2126f * orig_r + 0.7152f * orig_g + 0.0722f * orig_b;
+ float lum_weight = CLAMP((Y - 0.3f) / (0.8f - 0.3f), 0.0f, 1.0f);
+ lum_weight = lum_weight * lum_weight * (3.0f - 2.0f * lum_weight);
+ const float effective_strength = d->gamut_strength * lum_weight;
+
+ float limit = 0.90f;
+ if(d->target_gamut == 1) limit = 0.95f;
+ else if(d->target_gamut == 2) limit = 1.00f;
+
+ float gamut_threshold = limit * (1.0f - (effective_strength * 0.25f));
+ float max_val = fmaxf(val[0], fmaxf(val[1], val[2]));
+
+ if(max_val > gamut_threshold)
+ {
+ float range = limit - gamut_threshold;
+ float delta = max_val - gamut_threshold;
+ const float compressed = gamut_threshold + range * delta / (delta + range);
+ const float factor = compressed / max_val;
+
+ float range_blue = 1.1f * range;
+ const float compressed_blue = gamut_threshold + range * delta / (delta + range_blue);
+ const float factor_blue = compressed_blue / max_val;
+
+ val[0] *= factor;
+ val[1] *= factor;
+ val[2] *= factor_blue;
+ }
+
+ val[0] = orig_r * (1.0f - effective_strength) + val[0] * effective_strength;
+ val[1] = orig_g * (1.0f - effective_strength) + val[1] * effective_strength;
+ val[2] = orig_b * (1.0f - effective_strength) + val[2] * effective_strength;
+ }
+ }
+
+ // Final gamut check to preserve hue (exact color)
+ if(val[0] < 0.0f || val[0] > 1.0f || val[1] < 0.0f || val[1] > 1.0f || val[2] < 0.0f || val[2] > 1.0f)
+ {
+ const float luma = 0.2627f * val[0] + 0.6780f * val[1] + 0.0593f * val[2];
+ const float target_luma = CLAMP(luma, 0.0f, 1.0f);
+ float t = 1.0f;
+ if(val[0] < 0.0f) t = fminf(t, target_luma / (target_luma - val[0]));
+ if(val[1] < 0.0f) t = fminf(t, target_luma / (target_luma - val[1]));
+ if(val[2] < 0.0f) t = fminf(t, target_luma / (target_luma - val[2]));
+ if(val[0] > 1.0f) t = fminf(t, (1.0f - target_luma) / (val[0] - target_luma));
+ if(val[1] > 1.0f) t = fminf(t, (1.0f - target_luma) / (val[1] - target_luma));
+ if(val[2] > 1.0f) t = fminf(t, (1.0f - target_luma) / (val[2] - target_luma));
+ t = fmaxf(0.0f, t);
+ val[0] = target_luma + t * (val[0] - target_luma);
+ val[1] = target_luma + t * (val[1] - target_luma);
+ val[2] = target_luma + t * (val[2] - target_luma);
+ }
+ }
+
+ for(int i = 0; i < 3; i++) out[k + i] = val[i];
out[k + 3] = in[k + 3]; // pass on 4th channel
}
@@ -1366,32 +2085,6 @@ void process_fusion(dt_iop_module_t *self,
free(comb);
}
-void process_lut(dt_iop_module_t *self,
- dt_dev_pixelpipe_iop_t *piece,
- const void *const ivoid,
- void *const ovoid,
- const dt_iop_roi_t *const roi_in,
- const dt_iop_roi_t *const roi_out)
-{
- const float *const in = (const float *)ivoid;
- float *const out = (float *)ovoid;
- //const int ch = piece->colors; <-- it appears someone was trying to make this handle monochrome data,
- //however the for loops only handled RGBA - FIXME, determine what possible data formats and channel
- //configurations we might encounter here and handle those too
- dt_iop_basecurve_data_t *const d = piece->data;
- const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_iop_work_profile_info(piece->module, piece->module->dev->iop);
-
- const int wd = roi_in->width, ht = roi_in->height;
-
- // Compared to previous implementation, we've at least moved this conditional outside of the image processing loops
- // so that it is evaluated only once. See FIXME comments in apply_curve for more potential performance improvements
- if(d->preserve_colors == DT_RGB_NORM_NONE)
- apply_legacy_curve(in, out, wd, ht, 1.0, d->table, d->unbounded_coeffs);
- else
- apply_curve(in, out, wd, ht, d->preserve_colors, 1.0, d->table, d->unbounded_coeffs, work_profile);
-}
-
-
void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid,
@@ -1420,6 +2113,16 @@ void commit_params(dt_iop_module_t *self,
d->exposure_stops = p->exposure_stops;
d->exposure_bias = p->exposure_bias;
d->preserve_colors = p->preserve_colors;
+ d->workflow_mode = p->workflow_mode;
+ // Intentional inversion: slider to the right (>1.0) -> exponent < 1.0 -> lightens shadows
+ d->shadow_lift = 2.0f - p->shadow_lift;
+ d->highlight_gain = p->highlight_gain;
+ d->ucs_saturation_balance = p->ucs_saturation_balance;
+ d->gamut_strength = p->gamut_strength;
+ d->highlight_corr = p->highlight_corr;
+ d->target_gamut = p->target_gamut;
+ d->color_look = p->color_look;
+ d->look_opacity = p->look_opacity;
const int ch = 0;
// take care of possible change of curve type or number of nodes (not yet implemented in UI)
@@ -1474,18 +2177,6 @@ void cleanup_pipe(dt_iop_module_t *self,
piece->data = NULL;
}
-void gui_update(dt_iop_module_t *self)
-{
- dt_iop_basecurve_params_t *p = self->params;
- dt_iop_basecurve_gui_data_t *g = self->gui_data;
-
- gtk_widget_set_visible(g->exposure_step, p->exposure_fusion != 0);
- gtk_widget_set_visible(g->exposure_bias, p->exposure_fusion != 0);
-
- // gui curve is read directly from params during expose event.
- gtk_widget_queue_draw(GTK_WIDGET(g->area));
-}
-
static float eval_grey(float x)
{
// "log base" is a combined scaling and offset change so that x->[0,1], with
@@ -1499,6 +2190,8 @@ void init(dt_iop_module_t *self)
dt_iop_basecurve_params_t *d = self->default_params;
d->basecurve[0][1].x = d->basecurve[0][1].y = 1.0;
d->basecurve_nodes[0] = 2;
+ d->shadow_lift = 1.0f;
+ d->highlight_gain = 1.0f;
}
void init_global(dt_iop_module_so_t *self)
@@ -1594,7 +2287,7 @@ static gboolean dt_iop_basecurve_draw(GtkWidget *widget, cairo_t *crf, dt_iop_mo
dt_draw_curve_set_point(g->minmax_curve, k, p->basecurve[0][k].x, p->basecurve[0][k].y);
}
dt_draw_curve_t *minmax_curve = g->minmax_curve;
- dt_draw_curve_calc_values(minmax_curve, 0.0, 1.0, DT_IOP_TONECURVE_RES, g->draw_xs, g->draw_ys);
+ dt_draw_curve_calc_values(minmax_curve, 0.0, 1.0, DT_IOP_TONECURVE_RES, NULL, g->draw_ys);
float unbounded_coeffs[3];
const float xm = basecurve[nodes - 1].x;
@@ -1818,7 +2511,7 @@ static gboolean dt_iop_basecurve_motion_notify(GtkWidget *widget,
// got a vertex selected:
if(g->selected >= 0)
{
- // this is used to translate mause position in loglogscale to make this behavior unified with linear scale.
+ // this is used to translate mouse position in loglogscale to make this behavior unified with linear scale.
const float translate_mouse_x = old_m_x / width - to_log(basecurve[g->selected].x, g->loglogscale);
const float translate_mouse_y = 1 - old_m_y / height - to_log(basecurve[g->selected].y, g->loglogscale);
// dx & dy are in linear coordinates
@@ -1936,12 +2629,22 @@ static gboolean dt_iop_basecurve_button_press(GtkWidget *widget,
else if(event->type == GDK_2BUTTON_PRESS)
{
// reset current curve
- p->basecurve_nodes[ch] = d->basecurve_nodes[ch];
- p->basecurve_type[ch] = d->basecurve_type[ch];
- for(int k = 0; k < d->basecurve_nodes[ch]; k++)
+ if(p->workflow_mode > 0)
{
- p->basecurve[ch][k].x = d->basecurve[ch][k].x;
- p->basecurve[ch][k].y = d->basecurve[ch][k].y;
+ p->basecurve_nodes[ch] = 2;
+ p->basecurve_type[ch] = CUBIC_SPLINE;
+ p->basecurve[ch][0].x = 0.0f; p->basecurve[ch][0].y = 0.0f;
+ p->basecurve[ch][1].x = 1.0f; p->basecurve[ch][1].y = 1.0f;
+ }
+ else
+ {
+ p->basecurve_nodes[ch] = d->basecurve_nodes[ch];
+ p->basecurve_type[ch] = d->basecurve_type[ch];
+ for(int k = 0; k < d->basecurve_nodes[ch]; k++)
+ {
+ p->basecurve[ch][k].x = d->basecurve[ch][k].x;
+ p->basecurve[ch][k].y = d->basecurve[ch][k].y;
+ }
}
g->selected = -2; // avoid motion notify re-inserting immediately.
dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget);
@@ -2077,9 +2780,132 @@ void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
gtk_widget_set_visible(g->exposure_step, FALSE);
gtk_widget_set_visible(g->exposure_bias, FALSE);
}
+ }
+
+ if(!w || w == g->workflow_mode || w == g->color_look)
+ {
+ if(p->workflow_mode == 1 || p->workflow_mode == 2)
+ {
+ gtk_widget_set_visible(g->cmb_preserve_colors, FALSE);
+ if(p->preserve_colors != DT_RGB_NORM_NONE)
+ dt_bauhaus_combobox_set(g->cmb_preserve_colors, DT_RGB_NORM_NONE);
+ gtk_widget_set_visible(g->shadow_lift, TRUE);
+ gtk_widget_set_visible(g->highlight_gain, TRUE);
+ gtk_widget_set_visible(g->ucs_saturation_balance, TRUE);
+ gtk_widget_set_visible(g->gamut_strength, TRUE);
+ gtk_widget_set_visible(g->highlight_corr, TRUE);
+ gtk_widget_set_visible(g->target_gamut, TRUE);
+ gtk_widget_set_visible(g->color_look, TRUE);
+ gtk_widget_set_visible(g->look_opacity, p->color_look > 0);
+ gtk_widget_set_sensitive(g->shadow_lift, TRUE);
+ gtk_widget_set_sensitive(g->highlight_gain, TRUE);
+ gtk_widget_set_sensitive(g->ucs_saturation_balance, TRUE);
+ gtk_widget_set_sensitive(g->gamut_strength, TRUE);
+ gtk_widget_set_sensitive(g->highlight_corr, TRUE);
+ gtk_widget_set_sensitive(g->target_gamut, TRUE);
+ gtk_widget_set_sensitive(g->color_look, TRUE);
+ gtk_widget_set_sensitive(g->look_opacity, p->color_look > 0);
+ if(w == g->color_look)
+ {
+ // Only reset opacity to 100% if a look is selected for the first time.
+ if(p->color_look > 0 && !g->look_selected_first_time)
+ {
+ p->look_opacity = 1.0f;
+ dt_bauhaus_slider_set(g->look_opacity, 1.0f);
+ g->look_selected_first_time = TRUE;
+ }
+ }
+
+ gtk_widget_set_tooltip_text(g->fusion, _("exposure fusion operates in linear scene-referred space as a luminance normalization step,\n"
+ "providing a stable radiometric reference prior to the final tone-mapping curve.\n"
+ "it does not perform HDR blending nor exposure compensation."));
+ if(w == g->workflow_mode)
+ {
+ if(!((p->workflow_mode == 1 && g->last_workflow_mode == 2) || (p->workflow_mode == 2 && g->last_workflow_mode == 1)))
+ {
+ p->shadow_lift = 1.0f;
+ dt_bauhaus_slider_set(g->shadow_lift, 1.0f);
+ p->highlight_gain = 1.0f;
+ dt_bauhaus_slider_set(g->highlight_gain, 1.0f);
+ p->ucs_saturation_balance = 0.2f;
+ dt_bauhaus_slider_set(g->ucs_saturation_balance, 0.2f);
+ // Set default color look when switching to this workflow
+ p->color_look = 0; // Neutral look
+ dt_bauhaus_combobox_set(g->color_look, 0);
+ p->look_opacity = 1.0f;
+ dt_bauhaus_slider_set(g->look_opacity, 1.0f);
+ p->basecurve_type[0] = CUBIC_SPLINE;
+ p->basecurve_nodes[0] = 2;
+ p->basecurve[0][0].x = 0.0f; p->basecurve[0][0].y = 0.0f;
+ p->basecurve[0][1].x = 1.0f; p->basecurve[0][1].y = 1.0f;
+
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+ }
+ g->last_workflow_mode = p->workflow_mode;
+ }
+ }
+ else
+ {
+ gtk_widget_set_visible(g->cmb_preserve_colors, TRUE);
+ gtk_widget_set_visible(g->shadow_lift, FALSE);
+ gtk_widget_set_visible(g->highlight_gain, FALSE);
+ gtk_widget_set_visible(g->ucs_saturation_balance, FALSE);
+ gtk_widget_set_visible(g->gamut_strength, FALSE);
+ gtk_widget_set_visible(g->highlight_corr, FALSE);
+ gtk_widget_set_visible(g->target_gamut, FALSE);
+ gtk_widget_set_visible(g->color_look, FALSE);
+ gtk_widget_set_visible(g->look_opacity, FALSE);
+ gtk_widget_set_sensitive(g->shadow_lift, FALSE);
+ gtk_widget_set_sensitive(g->highlight_gain, FALSE);
+ gtk_widget_set_sensitive(g->ucs_saturation_balance, FALSE);
+ gtk_widget_set_sensitive(g->gamut_strength, FALSE);
+ gtk_widget_set_sensitive(g->highlight_corr, FALSE);
+ gtk_widget_set_sensitive(g->target_gamut, FALSE);
+ gtk_widget_set_sensitive(g->color_look, FALSE);
+ gtk_widget_set_sensitive(g->look_opacity, FALSE);
+ gtk_widget_set_tooltip_text(g->fusion, _("fuse this image stopped up/down a couple of times with itself, to "
+ "compress high dynamic range. expose for the highlights before use."));
+ }
+ }
+
+ if(!w || w == g->workflow_mode)
+ {
+ if(p->workflow_mode != 0)
+ {
+ gtk_widget_hide(g->logbase);
+ }
+ else
+ {
+ gtk_widget_show(g->logbase);
+ }
}
}
+void gui_update(dt_iop_module_t *self)
+{
+ dt_iop_basecurve_params_t *p = self->params;
+ dt_iop_basecurve_gui_data_t *g = self->gui_data;
+
+ gtk_widget_set_visible(g->exposure_step, p->exposure_fusion != 0);
+ gtk_widget_set_visible(g->exposure_bias, p->exposure_fusion != 0);
+
+ dt_bauhaus_slider_set(g->gamut_strength, p->gamut_strength);
+ dt_bauhaus_slider_set(g->highlight_corr, p->highlight_corr);
+ dt_bauhaus_combobox_set(g->target_gamut, p->target_gamut);
+ dt_bauhaus_combobox_set(g->workflow_mode, p->workflow_mode);
+ dt_bauhaus_slider_set(g->shadow_lift, p->shadow_lift);
+ dt_bauhaus_slider_set(g->highlight_gain, p->highlight_gain);
+ dt_bauhaus_slider_set(g->ucs_saturation_balance, p->ucs_saturation_balance);
+ dt_bauhaus_combobox_set(g->color_look, p->color_look);
+ dt_bauhaus_slider_set(g->look_opacity, p->look_opacity);
+ g->last_workflow_mode = p->workflow_mode;
+ g->look_selected_first_time = (p->color_look != 0);
+ gui_changed(self, NULL, NULL);
+
+ // gui curve is read directly from params during expose event.
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+}
+
static void logbase_callback(GtkWidget *slider, dt_iop_module_t *self)
{
dt_iop_basecurve_gui_data_t *g = self->gui_data;
@@ -2100,23 +2926,72 @@ void gui_init(dt_iop_module_t *self)
g->mouse_x = g->mouse_y = -1.0;
g->selected = -1;
g->loglogscale = 0;
+ g->look_selected_first_time = FALSE;
- g->area = GTK_DRAWING_AREA(dtgtk_drawing_area_new_with_height(0));
+ g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, DT_PIXEL_APPLY_DPI(100), "plugins/darkroom/basecurve/graph_height"));
gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("abscissa: input, ordinate: output. works on RGB channels"));
g_object_set_data(G_OBJECT(g->area), "iop-instance", self);
dt_action_define_iop(self, NULL, N_("curve"), GTK_WIDGET(g->area), NULL);
- self->widget = dt_gui_vbox(g->area);
+ self->widget = dt_gui_vbox(GTK_WIDGET(g->area));
g->cmb_preserve_colors = dt_bauhaus_combobox_from_params(self, "preserve_colors");
gtk_widget_set_tooltip_text(g->cmb_preserve_colors, _("method to preserve colors when applying contrast"));
+ g->workflow_mode = dt_bauhaus_combobox_from_params(self, "workflow_mode");
+ dt_bauhaus_combobox_add(g->workflow_mode, _("display"));
+ dt_bauhaus_combobox_add(g->workflow_mode, _("kinematic"));
+ dt_bauhaus_combobox_add(g->workflow_mode, _("dynamic"));
+ gtk_widget_set_tooltip_text(g->workflow_mode, _("tone mapping method applied after the curve"));
+
+ g->color_look = dt_bauhaus_combobox_from_params(self, "color_look");
+ dt_bauhaus_widget_set_label(g->color_look, NULL, _("color look"));
+ dt_bauhaus_combobox_add(g->color_look, "neutral");
+ dt_bauhaus_combobox_add(g->color_look, "natural look");
+ dt_bauhaus_combobox_add(g->color_look, "portrait");
+ dt_bauhaus_combobox_add(g->color_look, "vibrant");
+ dt_bauhaus_combobox_add(g->color_look, "nature");
+ dt_bauhaus_combobox_add(g->color_look, "blue sky");
+ dt_bauhaus_combobox_add(g->color_look, "soft warm");
+ dt_bauhaus_combobox_add(g->color_look, "soft");
+ dt_bauhaus_combobox_add(g->color_look, "deep cool");
+ dt_bauhaus_combobox_add(g->color_look, "authentic cinema");
+ gtk_widget_set_tooltip_text(g->color_look, _("apply a color style: neutral (none), portrait (skin tones), nature (landscapes), blue sky (depth), soft (organic), or warm/cool artistic tints."));
+
+ g->look_opacity = dt_bauhaus_slider_from_params(self, "look_opacity");
+ dt_bauhaus_widget_set_label(g->look_opacity, NULL, _("look opacity"));
+ dt_bauhaus_slider_set_format(g->look_opacity, "%");
+ dt_bauhaus_slider_set_factor(g->look_opacity, 100.0);
+ gtk_widget_set_tooltip_text(g->look_opacity, _("adjust the strength of the selected color style (10% to 100%)."));
+
+ g->highlight_gain = dt_bauhaus_slider_from_params(self, "highlight_gain");
+ dt_bauhaus_widget_set_label(g->highlight_gain, NULL, _("highlight gain"));
+ gtk_widget_set_tooltip_text(g->highlight_gain, _("adjusts the gain before tone mapping.\n"
+ "higher values push more data into highlights compression."));
+ dt_bauhaus_slider_set_soft_range(g->highlight_gain, 0.25, 1.75);
+ dt_bauhaus_slider_set_format(g->highlight_gain, "%");
+ dt_bauhaus_slider_set_factor(g->highlight_gain, 100.0);
+ dt_bauhaus_slider_set_offset(g->highlight_gain, -100.0);
+ dt_bauhaus_slider_set_default(g->highlight_gain, 1.0);
+
+ g->shadow_lift = dt_bauhaus_slider_from_params(self, "shadow_lift");
+ dt_bauhaus_widget_set_label(g->shadow_lift, NULL, _("shadow lift"));
+ gtk_widget_set_tooltip_text(g->shadow_lift, _("adjusts the shadows brightness.\n"
+ "positive values lift shadows,\n"
+ "while negative values darken them."));
+ dt_bauhaus_slider_set_soft_range(g->shadow_lift, 0.25, 1.75);
+ dt_bauhaus_slider_set_format(g->shadow_lift, "%");
+ dt_bauhaus_slider_set_factor(g->shadow_lift, 100.0);
+ dt_bauhaus_slider_set_offset(g->shadow_lift, -100.0);
+ dt_bauhaus_slider_set_default(g->shadow_lift, 1.0);
+
g->fusion = dt_bauhaus_combobox_from_params(self, "exposure_fusion");
dt_bauhaus_combobox_add(g->fusion, _("none"));
dt_bauhaus_combobox_add(g->fusion, _("two exposures"));
dt_bauhaus_combobox_add(g->fusion, _("three exposures"));
gtk_widget_set_tooltip_text(g->fusion, _("fuse this image stopped up/down a couple of times with itself, to "
"compress high dynamic range. expose for the highlights before use."));
+ gtk_widget_set_margin_bottom(g->fusion, DT_PIXEL_APPLY_DPI(10));
g->exposure_step = dt_bauhaus_slider_from_params(self, "exposure_stops");
dt_bauhaus_slider_set_digits(g->exposure_step, 3);
@@ -2134,11 +3009,54 @@ void gui_init(dt_iop_module_t *self)
gtk_widget_set_no_show_all(g->exposure_bias, TRUE);
gtk_widget_set_visible(g->exposure_bias, p->exposure_fusion != 0 ? TRUE : FALSE);
+ g->ucs_saturation_balance = dt_bauhaus_slider_from_params(self, "ucs_saturation_balance");
+ dt_bauhaus_widget_set_label(g->ucs_saturation_balance, NULL, _("balance saturation ucs"));
+ gtk_widget_set_tooltip_text(g->ucs_saturation_balance,
+ _("balances saturation between shadows and highlights (JzAzBz space).\n"
+ " move right to boost shadow saturation while taming highlights.\n"
+ " move left to boost highlight saturation while taming shadows.\n"
+ " ideal for making dark colors pop without clipping speculars."));
+ dt_bauhaus_slider_set_format(g->ucs_saturation_balance, "%");
+ dt_bauhaus_slider_set_factor(g->ucs_saturation_balance, 100.0);
+ dt_bauhaus_slider_set_soft_range(g->ucs_saturation_balance, -0.75, 0.75);
+ dt_bauhaus_slider_set_default(g->ucs_saturation_balance, 0.2);
+
+ g->highlight_corr = dt_bauhaus_slider_from_params(self, "highlight_corr");
+ dt_bauhaus_widget_set_label(g->highlight_corr, NULL, _("highlight hue/sat"));
+ dt_bauhaus_slider_set_format(g->highlight_corr, "%");
+ dt_bauhaus_slider_set_factor(g->highlight_corr, 100.0);
+ dt_bauhaus_slider_set_digits(g->highlight_corr, 1);
+ dt_bauhaus_slider_set_soft_range(g->highlight_corr, -1.0, 1.0);
+ dt_bauhaus_slider_set_default(g->highlight_corr, 0.0);
+ dt_bauhaus_slider_set_step(g->highlight_corr, 0.001);
+ gtk_widget_set_tooltip_text(g->highlight_corr, _("corrects hue and saturation in highlights to mitigate color shifts\n"
+ "(e.g. salmon sunsets or magenta blues)"));
+
+ g->target_gamut = dt_bauhaus_combobox_from_params(self, "target_gamut");
+ dt_bauhaus_combobox_add(g->target_gamut, "sRGB (Rec.709)");
+ dt_bauhaus_combobox_add(g->target_gamut, "AdobeRGB");
+ dt_bauhaus_combobox_add(g->target_gamut, "Rec.2020");
+ gtk_widget_set_tooltip_text(g->target_gamut, _("select the destination color space (sRGB, AdobeRGB,\n"
+ "or Rec.2020). this sets the legal boundary for color saturation."));
+
+ g->gamut_strength = dt_bauhaus_slider_from_params(self, "gamut_strength");
+ dt_bauhaus_widget_set_label(g->gamut_strength, NULL, _("compression smoothness"));
+ gtk_widget_set_tooltip_text(g->gamut_strength,
+ _("defines how high in the highlights the compression starts.\n"
+ " lower values keep more saturation but may clip;\n"
+ " higher values create a professional roll-off\n"
+ " in the brightest colors without affecting midtones."));
+ dt_bauhaus_slider_set_format(g->gamut_strength, "%");
+ dt_bauhaus_slider_set_factor(g->gamut_strength, 100.0);
+ dt_bauhaus_slider_set_digits(g->gamut_strength, 1);
+ dt_bauhaus_slider_set_step(g->gamut_strength, 0.001);
+ dt_bauhaus_slider_set_soft_range(g->gamut_strength, 0.0, 1.0);
+
g->logbase = dt_bauhaus_slider_new_with_range(self, 0.0f, 40.0f, 0, 0.0f, 2);
dt_bauhaus_widget_set_label(g->logbase, NULL, N_("scale for graph"));
g_signal_connect(G_OBJECT(g->logbase), "value-changed", G_CALLBACK(logbase_callback), self);
- dt_gui_box_add(self->widget, g->logbase);
-
+ gtk_box_pack_start(GTK_BOX(self->widget), g->logbase, TRUE, TRUE, 0);
+
gtk_widget_add_events(GTK_WIDGET(g->area), GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask
| GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
| GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
@@ -2161,4 +3079,4 @@ void gui_cleanup(dt_iop_module_t *self)
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
-// clang-format on
+// clang-format on
\ No newline at end of file