Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions data/themes/darktable.css
Original file line number Diff line number Diff line change
Expand Up @@ -517,14 +517,21 @@ separator
.dt_accels_stick,
#filter-colors-box .dt_module_btn,
#header-toolbar .dt_module_btn,
#footer-toolbar .dt_module_btn
#footer-toolbar .dt_module_btn,
#second_window .dt_accels_btn
{
min-height: 1.7em;
min-width: 1.7em;
border: 0.08em solid transparent;
}

#footer-toolbar button:checked
#second_window button
{
background-color: @darkroom_bg_color;
}

#footer-toolbar button:checked,
#second_window button:checked
{
border: 0.08em solid shade(@button_border, 1.6);
}
Expand Down
286 changes: 280 additions & 6 deletions src/develop/develop.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@

#define DT_DEV_AVERAGE_DELAY_COUNT 5

// Forward declaration
static inline void _dt_dev_load_raw(dt_develop_t *dev, const dt_imgid_t imgid);

void dt_dev_init(dt_develop_t *dev,
const gboolean gui_attached)
{
Expand Down Expand Up @@ -152,6 +155,41 @@ void dt_dev_init(dt_develop_t *dev,
dev->full.closeup = dev->preview2.closeup = 0;
dev->full.zoom_x = dev->full.zoom_y = dev->preview2.zoom_x = dev->preview2.zoom_y = 0.0f;
dev->full.zoom_scale = dev->preview2.zoom_scale = 1.0f;

// Set back-pointers from viewports to their owning develop
dev->full.dev = dev;
dev->preview2.dev = dev;

// Initialize pinned image state
dev->preview2_pinned = FALSE;
dev->preview2_pinned_dev = NULL;
}

// Shutdown and cleanup a pinned dev, waiting for any in-progress jobs
static void _cleanup_pinned_dev(dt_develop_t *pinned_dev)
{
if(!pinned_dev) return;

pinned_dev->gui_leaving = TRUE;
pinned_dev->preview2.widget = NULL;

if(pinned_dev->preview2.pipe)
dt_atomic_set_int(&pinned_dev->preview2.pipe->shutdown, DT_DEV_PIXELPIPE_STOP_NODES);
if(pinned_dev->preview_pipe)
dt_atomic_set_int(&pinned_dev->preview_pipe->shutdown, DT_DEV_PIXELPIPE_STOP_NODES);
if(pinned_dev->full.pipe)
dt_atomic_set_int(&pinned_dev->full.pipe->shutdown, DT_DEV_PIXELPIPE_STOP_NODES);

if(pinned_dev->preview2.pipe)
{
dt_pthread_mutex_lock(&pinned_dev->preview2.pipe->mutex);
dt_pthread_mutex_unlock(&pinned_dev->preview2.pipe->mutex);
dt_pthread_mutex_lock(&pinned_dev->preview2.pipe->busy_mutex);
dt_pthread_mutex_unlock(&pinned_dev->preview2.pipe->busy_mutex);
}

dt_dev_cleanup(pinned_dev);
free(pinned_dev);
}

void dt_dev_cleanup(dt_develop_t *dev)
Expand Down Expand Up @@ -201,8 +239,18 @@ void dt_dev_cleanup(dt_develop_t *dev)
dev->allprofile_info = g_list_delete_link(dev->allprofile_info, dev->allprofile_info);
}
dt_pthread_mutex_destroy(&dev->history_mutex);
free(dev->histogram_pre_tonecurve);
free(dev->histogram_pre_levels);
if(dev->histogram_pre_tonecurve) free(dev->histogram_pre_tonecurve);
if(dev->histogram_pre_levels) free(dev->histogram_pre_levels);
dev->histogram_pre_tonecurve = dev->histogram_pre_levels = NULL;

// Clean up pinned develop
// Clean up pinned develop
if(dev->preview2_pinned_dev)
{
_cleanup_pinned_dev(dev->preview2_pinned_dev);
dev->preview2_pinned_dev = NULL;
}
dev->preview2_pinned = FALSE;

g_list_free_full(dev->forms, (void (*)(void *))dt_masks_free_form);
g_list_free_full(dev->allforms, (void (*)(void *))dt_masks_free_form);
Expand Down Expand Up @@ -263,6 +311,213 @@ void dt_dev_invalidate_all(dt_develop_t *dev)
dev->timestamp++;
}

// Helper to find the cloned module in pinned_dev that corresponds to a module in src_dev
static dt_iop_module_t *_find_cloned_module(dt_develop_t *dev, dt_iop_module_t *src_mod)
{
if(!src_mod) return NULL;
for(GList *iter = dev->iop; iter; iter = g_list_next(iter))
{
dt_iop_module_t *mod = (dt_iop_module_t *)iter->data;
// During cloning we preserve the instance ID and other unique fields
if(mod->instance == src_mod->instance && g_strcmp0(mod->op, src_mod->op) == 0 &&
mod->multi_priority == src_mod->multi_priority)
return mod;
}
return NULL;
}

// Helper to clone a module for pinned dev
static dt_iop_module_t *_clone_module(dt_develop_t *dev, dt_iop_module_t *src_mod)
{
dt_iop_module_t *new_mod = calloc(1, sizeof(dt_iop_module_t));
if(dt_iop_load_module_by_so(new_mod, src_mod->so, dev))
{
free(new_mod);
return NULL;
}

new_mod->instance = src_mod->instance;
new_mod->enabled = src_mod->enabled;
new_mod->iop_order = src_mod->iop_order;
new_mod->multi_priority = src_mod->multi_priority;
g_strlcpy(new_mod->multi_name, src_mod->multi_name, sizeof(new_mod->multi_name));
new_mod->multi_name_hand_edited = src_mod->multi_name_hand_edited;
new_mod->hide_enable_button = src_mod->hide_enable_button;

if(src_mod->params)
memcpy(new_mod->params, src_mod->params, src_mod->params_size);

if(new_mod->blend_params && src_mod->blend_params)
memcpy(new_mod->blend_params, src_mod->blend_params, sizeof(dt_develop_blend_params_t));

return new_mod;
}

static GList *_duplicate_iop_list(dt_develop_t *pinned_dev, dt_develop_t *main_dev)
{
GList *new_list = NULL;
for(GList *iter = main_dev->iop; iter; iter = g_list_next(iter))
{
dt_iop_module_t *src_mod = (dt_iop_module_t *)iter->data;
dt_iop_module_t *new_mod = _clone_module(pinned_dev, src_mod);
if(new_mod)
new_list = g_list_append(new_list, new_mod);
}
return new_list;
}

static GList *_duplicate_history_list(dt_develop_t *pinned_dev, GList *src_history)
{
GList *new_history = dt_history_duplicate(src_history);
for(GList *iter = new_history; iter; iter = g_list_next(iter))
{
dt_dev_history_item_t *item = (dt_dev_history_item_t *)iter->data;
item->module = _find_cloned_module(pinned_dev, item->module);
}
return new_history;
}

// Helper function to initialize a pinned develop structure
static void _init_pinned_dev(dt_develop_t *pinned_dev, dt_develop_t *main_dev,
const dt_imgid_t imgid)
{
// Initialize without GUI to avoid GUI callbacks during module loading
dt_dev_init(pinned_dev, FALSE);

// Copy viewport settings from main dev's preview2
pinned_dev->preview2.width = main_dev->preview2.width;
pinned_dev->preview2.height = main_dev->preview2.height;
pinned_dev->preview2.orig_width = main_dev->preview2.orig_width;
pinned_dev->preview2.orig_height = main_dev->preview2.orig_height;
pinned_dev->preview2.border_size = main_dev->preview2.border_size;
pinned_dev->preview2.dpi = main_dev->preview2.dpi;
pinned_dev->preview2.dpi_factor = main_dev->preview2.dpi_factor;
pinned_dev->preview2.ppd = main_dev->preview2.ppd;
pinned_dev->preview2.color_assessment = main_dev->preview2.color_assessment;
pinned_dev->preview2.zoom = main_dev->preview2.zoom;
pinned_dev->preview2.closeup = main_dev->preview2.closeup;
pinned_dev->preview2.zoom_x = main_dev->preview2.zoom_x;
pinned_dev->preview2.zoom_y = main_dev->preview2.zoom_y;
pinned_dev->preview2.zoom_scale = main_dev->preview2.zoom_scale;

// Share the widget reference for redraw triggers
pinned_dev->preview2.widget = main_dev->preview2.widget;
pinned_dev->preview2.pin_button = NULL;

// Ensure the dev pointer is set to the pinned_dev
pinned_dev->preview2.dev = pinned_dev;

// Manually create the pipes (since gui_attached=FALSE doesn't create them)
pinned_dev->full.pipe = malloc(sizeof(dt_dev_pixelpipe_t));
pinned_dev->preview_pipe = malloc(sizeof(dt_dev_pixelpipe_t));
pinned_dev->preview2.pipe = malloc(sizeof(dt_dev_pixelpipe_t));
dt_dev_pixelpipe_init(pinned_dev->full.pipe);
dt_dev_pixelpipe_init_preview(pinned_dev->preview_pipe);
dt_dev_pixelpipe_init_preview2(pinned_dev->preview2.pipe);

// Load raw image data
dt_lock_image(imgid);
_dt_dev_load_raw(pinned_dev, imgid);
pinned_dev->full.pipe->loading = FALSE; // Mark full pipe as not loading to avoid blocking preview2
pinned_dev->preview_pipe->loading = FALSE;
pinned_dev->preview2.pipe->loading = TRUE;
pinned_dev->preview2.pipe->status = DT_DEV_PIXELPIPE_DIRTY;

// Load modules (gui_attached is FALSE so no GUI widgets created)
dt_pthread_mutex_lock(&darktable.dev_threadsafe);
// Clone modules and forms from the main develop instance directly
// This ensures we get exactly what the user sees, including unsaved changes
// and specific history state, instead of reloading from database.
pinned_dev->iop = _duplicate_iop_list(pinned_dev, main_dev);
pinned_dev->forms = dt_masks_dup_forms_deep(main_dev->forms, NULL);
pinned_dev->history_end = main_dev->history_end;
pinned_dev->iop_instance = main_dev->iop_instance;
pinned_dev->history = _duplicate_history_list(pinned_dev, main_dev->history);
pinned_dev->history_last_module = _find_cloned_module(pinned_dev, main_dev->history_last_module);

// Copy iop order information
pinned_dev->iop_order_version = main_dev->iop_order_version;
pinned_dev->iop_order_list = dt_ioppr_iop_order_copy_deep(main_dev->iop_order_list);

// Copy chroma state and handle module pointers
memcpy(&pinned_dev->chroma, &main_dev->chroma, sizeof(dt_dev_chroma_t));
pinned_dev->chroma.temperature = _find_cloned_module(pinned_dev, main_dev->chroma.temperature);
pinned_dev->chroma.adaptation = _find_cloned_module(pinned_dev, main_dev->chroma.adaptation);

dt_pthread_mutex_unlock(&darktable.dev_threadsafe);

dt_unlock_image(imgid);
}

void _pin_image(dt_develop_t *dev)
{
const dt_imgid_t pinned_imgid = dev->image_storage.id;
if(dev->preview2_pinned_dev)
{
_cleanup_pinned_dev(dev->preview2_pinned_dev);
dev->preview2_pinned_dev = NULL;
}

dev->preview2_pinned_dev = malloc(sizeof(dt_develop_t));
if(!dev->preview2_pinned_dev)
{
dev->preview2_pinned = FALSE;
dt_toast_log(_("failed to create pinned develop"));
return;
}

_init_pinned_dev(dev->preview2_pinned_dev, dev, pinned_imgid);
dev->preview2_pinned_dev->preview2.pipe->status = DT_DEV_PIXELPIPE_DIRTY;
dev->preview2_pinned_dev->preview2.pipe->changed |= DT_DEV_PIPE_SYNCH;
dt_dev_process_preview2(dev->preview2_pinned_dev);
dt_toast_log(_("image pinned"));
}

void _unpin_image(dt_develop_t *dev)
{
if(dev->preview2_pinned_dev)
{
_cleanup_pinned_dev(dev->preview2_pinned_dev);
dev->preview2_pinned_dev = NULL;
}
// Force main dev's preview2 pipe to update
dev->preview2.pipe->status = DT_DEV_PIXELPIPE_DIRTY;
dev->preview2.pipe->changed |= DT_DEV_PIPE_SYNCH;
dt_toast_log(_("image unpinned"));
}

void dt_dev_toggle_preview2_pinned(dt_develop_t *dev)
{
if(!dev) return;

// If we're trying to pin, validate the image first
if(!dev->preview2_pinned)
{
if(!dt_is_valid_imgid(dev->image_storage.id))
{
dt_toast_log(_("no valid image to pin"));
return;
}

if(dev->full.pipe && dev->full.pipe->loading)
{
dt_toast_log(_("please wait for image to load"));
return;
}
}

dev->preview2_pinned = !dev->preview2_pinned;

if(dev->preview2_pinned)
_pin_image(dev);
else
_unpin_image(dev);

// Force a redraw of the second window
if(dev->preview2.widget)
gtk_widget_queue_draw(dev->preview2.widget);
}

void dt_dev_invalidate_preview(dt_develop_t *dev)
{
dev->preview_pipe->status = DT_DEV_PIXELPIPE_DIRTY;
Expand Down Expand Up @@ -302,7 +557,7 @@ void dt_dev_process_image_job(dt_develop_t *dev,

dt_pthread_mutex_lock(&pipe->mutex);

if(dev->gui_leaving)
if(dev->gui_leaving || dt_pipe_shutdown(pipe))
{
dt_pthread_mutex_unlock(&pipe->mutex);
return;
Expand Down Expand Up @@ -365,6 +620,17 @@ void dt_dev_process_image_job(dt_develop_t *dev,
dev->gui_synch = TRUE; // notify gui thread we want to synch
// (call gui_update on the modules)
}
else
{
// Explicitly set loading to FALSE to avoid a race condition
// where the pipe is still marked as loading when the caller
// checks it. Without this, the caller might wait for the
// pipe to finish loading, but it never will. This was causing
// failure to render composites in overlay.c and watermark.c.
pipe->loading = FALSE;
if(dev->preview_pipe) dev->preview_pipe->loading = FALSE;
if(dev->preview2.pipe) dev->preview2.pipe->loading = FALSE;
}
pipe->changed |= DT_DEV_PIPE_SYNCH;
}
else
Expand Down Expand Up @@ -510,6 +776,9 @@ void dt_dev_process_image_job(dt_develop_t *dev,
{
if(signalling && signal != DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED)
DT_CONTROL_SIGNAL_RAISE(signal);
else if(port->widget && !dev->gui_attached)
// pinned dev has gui_attached=FALSE, so manually queue redraw
dt_control_queue_redraw_widget(port->widget);
return;
}

Expand Down Expand Up @@ -2687,13 +2956,14 @@ void dt_dev_zoom_move(dt_dev_viewport_t *port,
const float y,
const gboolean constrain)
{
dt_develop_t *dev = darktable.develop;
// Use the viewport's own develop, or fall back to global
dt_develop_t *dev = port->dev ? port->dev : darktable.develop;

dt_pthread_mutex_lock(&darktable.control->global_mutex);
dt_pthread_mutex_lock(&dev->history_mutex);

float pts[2] = { port->zoom_x, port->zoom_y };
_dev_distort_transform_locked(darktable.develop, port->pipe, FALSE, 0.0f, DT_DEV_TRANSFORM_DIR_ALL_GEOMETRY, pts, 1);
_dev_distort_transform_locked(dev, port->pipe, FALSE, 0.0f, DT_DEV_TRANSFORM_DIR_ALL_GEOMETRY, pts, 1);

const float old_pts0 = pts[0];
const float old_pts1 = pts[1];
Expand Down Expand Up @@ -2853,6 +3123,9 @@ void dt_dev_zoom_move(dt_dev_viewport_t *port,
&& old_closeup == port->closeup)
return;

// Mark pipe as needing zoom update
port->pipe->changed |= DT_DEV_PIPE_ZOOMED;

if(port->widget)
dt_control_queue_redraw_widget(port->widget);
if(port == &dev->full)
Expand Down Expand Up @@ -2923,7 +3196,7 @@ void dt_dev_get_viewport_params(dt_dev_viewport_t *port,
if(x && y && port->pipe)
{
float pts[2] = { port->zoom_x, port->zoom_y };
dt_dev_distort_transform_plus(darktable.develop, port->pipe,
dt_dev_distort_transform_plus(port->dev ? port->dev : darktable.develop, port->pipe,
0.0f, DT_DEV_TRANSFORM_DIR_ALL_GEOMETRY, pts, 1);
*x = pts[0] / (float)port->pipe->processed_width - 0.5f;
*y = pts[1] / (float)port->pipe->processed_height - 0.5f;
Expand Down Expand Up @@ -3529,6 +3802,7 @@ void dt_dev_image(const dt_imgid_t imgid,
dt_dev_pop_history_items_ext(&dev, history_end);

dev.full = darktable.develop->full;
dev.full.dev = &dev;
dev.full.pipe = pipe;

if(!zoom_pos)
Expand Down
Loading
Loading