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 @@ +commit bed486a3bfd2384faba1c831ca9859a2bdb65efa (HEAD -> basecurve_PR) +Author: Christian Bouhon +Date: Fri Mar 6 16:54:47 2026 +0100 + + 20260306 use of _args(), and use of CLARGFLOAT + +commit 2055e5f8e2fcbd5ee868774628c28fd59027a370 +Author: Christian Bouhon +Date: Mon Mar 2 23:26:38 2026 +0100 + + 20260301 no capital letters on default ui & Rec2020 + +commit 42521946bad4e2e458a8e058b806aa123d5b6c69 +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 + +commit 7bb608c14cee36d42c6564523b98bc41faf52311 +Author: Christian Bouhon +Date: Sat Feb 28 01:42:27 2026 +0100 + + 20260228 Requested changes, const and final style adjustments + +commit 41423773369a6580d9feb243aca8529f97c1dd26 +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