Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
dev_history_gui.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#include "develop/imageop_gui.h"
22
23#include "develop/blend_gui.h"
24#include "develop/dev_history.h"
26#include "develop/develop.h"
27#include "develop/imageop.h"
29#include "system/macros.h"
30
31#include <gtk/gtk.h>
32
33/* Undoing an edit also undoes what the user was LOOKING at: the recorded mask-edit view
34 * for the focused module. The data half (dt_masks_set_edit_mode, request_mask_display) is
35 * restored by the engine; this pokes the blending panel's widgets to match. */
36static void _undo_restore_gui(dt_develop_t *dev, const int mask_edit_mode,
37 const int request_mask_display)
38{
39 (void)mask_edit_mode; // consumed by the engine's dt_masks_set_edit_mode() call
40
44 : NULL;
45 if(bd)
47 request_mask_display == DT_DEV_PIXELPIPE_DISPLAY_MASK);
48}
49
51{
52 if(!dev->gui_attached) return;
53
54 // Match the live module instances to the reloaded history before touching GTK.
55 // This loop may remove obsolete instances or expose instances newly created
56 // while reading a style/history from the database.
61
63
64 // Destroy the widgets of the instances the reconciliation removed -- AFTER the lock,
65 // which is the point of the removed-list protocol (see dev_history.h); the modules are
66 // parked in dev->alliop and already unlinked, so nothing else touches them.
67 for(GList *l = removed; l; l = g_list_next(l))
68 {
71 if(!dt_iop_is_hidden(mod) && mod->gui && mod->gui->expander)
72 {
73 // hide first to avoid a burst of gtk critical warnings from the live container
74 gtk_widget_hide(mod->gui->expander);
75 // frees mod->gui and destroys the whole expander/header/widget tree itself
77 }
78 }
80
81 for(GList *module = g_list_first(dev->iop); module; module = g_list_next(module))
82 {
83 dt_iop_module_t *mod = (dt_iop_module_t *)(module->data);
84
85 // History reload is backend-only and creates new multi-instances without
86 // GTK state. Attach every missing GUI here, after releasing history_mutex,
87 // so styles and global history actions expose their complete module set.
88 if(!dt_iop_is_hidden(mod) && (IS_NULL_PTR(mod->gui) || IS_NULL_PTR(mod->gui->expander)))
89 {
90 // a backend-created instance has no gui struct at all -- that IS "never initialised"
91 if(IS_NULL_PTR(mod->gui) || IS_NULL_PTR(mod->gui->widget)) dt_iop_gui_init(mod);
93 }
94
95 // Parameters, enabled state, headers and blending controls may all have
96 // changed, therefore refresh every module rather than only history entries.
98 }
99
102
104}
105/* ---------------------------------------------------------------------------------------
106 * Throttling the history commit, at the one place every widget already goes through.
107 *
108 * A history commit is not cheap: it records an undo step, takes history_mutex as writer,
109 * rehashes the whole history, writes the image cache, rebuilds the masks list, schedules a
110 * DB+XMP write, and finally tells the pipes their history changed -- which also raises their
111 * `shutdown` atomic, so the worker abandons the frame it is rendering. Running that per step
112 * of a slider drag or a combobox scroll blocks the GUI thread often enough that the widget
113 * cannot even repaint its own value, and aborts each render before it can finish.
114 *
115 * Widgets used to dodge that by deferring their own `value-changed` emission through
116 * gui_throttle (bauhaus, and a copy of the same dance in nine curve modules). That put the
117 * policy in every widget that wanted it and left every other widget without it. The commit
118 * is deferred here instead, where all of them already arrive.
119 *
120 * TWO RULES, and the difference between them matters:
121 *
122 * 1. NOTHING IS MERGED. A tempting optimisation is to collapse pending requests into one
123 * carrying the union of their flags; do not. Drawn masks, raster masks, module
124 * enable/disable, the mask manager and plain parameter edits each commit differently, and
125 * any merging rule is somewhere to silently drop one of them. The queue is FIFO and every
126 * distinct request is kept, in order.
127 *
128 * 2. A REPEAT OF THE PENDING TAIL IS NOT A NEW REQUEST. If the last thing queued is already
129 * "commit `module`, enable=`enable`" and that is exactly what is being asked for again,
130 * there is nothing to add: the queued request reads module->params when it drains, so it
131 * necessarily commits the newest value. This is consecutive-duplicate suppression against
132 * the TAIL only -- never a search of the queue, never a combination of two different
133 * requests -- so ordering is exactly preserved. It is what keeps a 300-tick scroll from
134 * queueing 300 commits, which is the whole point: a transient intermediate value of an
135 * ongoing gesture is not a history event.
136 *
137 * This is a batching window, not a debounce: the timer is armed by the first request and is
138 * NOT re-armed by the ones that follow, so a sustained drag keeps committing once per window
139 * instead of freezing until the user stops moving.
140 *
141 * dt_dev_add_history_item_ext() is NOT throttled and never was. Anything that must land
142 * before the next statement (bulk history loads, style application, image duplication)
143 * already goes through it.
144 *
145 * GUI thread only, like the commit path it serves ("always called from GUI controls" below),
146 * hence no lock -- same contract as gui_throttle's own queue.
147 */
148
155
157static guint _pending_commit_source = 0;
158
159
160static gboolean _drain_pending_commits(gpointer user_data)
161{
162 (void)user_data;
163
165
166 // Detach the queue before draining: a commit can trigger another one (a module reacting in
167 // post_history_commit), which would push onto the queue we are iterating.
168 GQueue ready = G_QUEUE_INIT;
169 ready.head = _pending_commits.head;
170 ready.tail = _pending_commits.tail;
171 ready.length = _pending_commits.length;
173
174 while(!g_queue_is_empty(&ready))
175 {
177 dt_dev_history_commit_item_now(request->dev, request->module, request->enable);
178 dt_free(request);
179 }
180
181 return G_SOURCE_REMOVE;
182}
183
185{
186 // A zero timeout means the user turned throttling off in the preferences (and is also what
187 // a headless run gets, since nothing ever sets one there). Same call, same place, just now.
189 if(timeout_ms == 0)
190 {
192 return;
193 }
194
195 // Rule 2 above: a repeat of what is already queued at the tail adds nothing.
197 ? (const dt_dev_pending_commit_t *)_pending_commits.tail->data
198 : NULL;
199 if(!IS_NULL_PTR(tail) && tail->dev == dev && tail->module == module && tail->enable == enable) return;
200
201 dt_dev_pending_commit_t *queued = (dt_dev_pending_commit_t *)calloc(1, sizeof(*queued));
202 if(IS_NULL_PTR(queued))
203 {
204 // Out of memory for a 24-byte record: commit now rather than lose the edit.
206 return;
207 }
208
209 queued->dev = dev;
210 queued->module = module;
211 queued->enable = enable;
213
216}
217
219{
220 // Run them, do not drop them: a pending request IS the user's last edit, and dropping it
221 // would lose the value they left a slider on. Callers invoke this while `dev` is still
222 // whole -- before any pipe node, iop or history teardown -- so committing here is safe,
223 // and it is the only moment at which it still is. A request draining after teardown is the
224 // race CLAUDE.md documents, which corrupts the heap and crashes somewhere unrelated.
226 {
229 }
230
232 while(iter)
233 {
234 GList *next = g_list_next(iter);
236 if(IS_NULL_PTR(dev) || request->dev == dev)
237 {
239 dt_dev_history_commit_item_now(request->dev, request->module, request->enable);
240 dt_free(request);
241 }
242 iter = next;
243 }
244
245 // Something else's requests may remain (another dev): give them their timer back.
247 {
251 }
252}
253
255{
256 // Last-resort counterpart to the flush, for a `dev` that is already too far gone to commit
257 // to. Nothing should normally be left here by the time this runs.
259 while(iter)
260 {
261 GList *next = g_list_next(iter);
263 if(IS_NULL_PTR(dev) || request->dev == dev)
264 {
266 dt_free(request);
267 }
268 iter = next;
269 }
270
272 {
275 }
276}
277
278
279// The next 2 functions are always called from GUI controls setting parameters
280// This is why they directly start a pipeline recompute.
281// Otherwise, please keep GUI and pipeline fully separated.
282
284{
285 (void)redraw;
287}
288
289/* After each immediate commit: keep the viewport geometry and the enable toggle honest. */
291{
292 // If module params change the geometry of the ROI, update immediately so we avoid
293 // drawing glitches.
294 if(module->modify_roi_in || module->modify_roi_out)
296
297 // Changing a parameter of a disabled module enables it, so update the GUI toggle state
298 // to reflect it -- frozen, so setting the state does not run its callbacks.
302}
303
309
310// clang-format off
311// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
312// vim: shiftwidth=2 expandtab tabstop=2 cindent
313// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
314// clang-format on
void dt_iop_gui_update_blendif(dt_iop_module_t *module)
Definition blend_gui.c:3452
The blending panel's GUI state and API, cut out of develop/blend.h.
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
void dt_dev_history_set_commit_gui_handler(dt_dev_history_commit_gui_handler_t handler)
void dt_dev_history_set_undo_restore_gui_handler(dt_dev_history_undo_restore_gui_handler_t handler)
int dt_dev_history_refresh_nodes_ext(dt_develop_t *dev, GList **iop, GList *history, GList **removed_gui_modules)
Refresh GUI module nodes to match history state.
void dt_dev_history_commit_item_now(dt_develop_t *dev, dt_iop_module_t *module, gboolean enable)
Commit one history item immediately – record undo, take history_mutex as writer, write through,...
void dt_dev_add_history_item_real(dt_develop_t *dev, dt_iop_module_t *module, gboolean enable, gboolean redraw)
Thread-safe wrapper around dt_dev_add_history_item_ext().
void dt_dev_history_drop_pending_commits(dt_develop_t *dev)
Discard every history commit still queued for dev, without running it.
static gboolean _drain_pending_commits(gpointer user_data)
void dt_dev_history_flush_pending_commits(dt_develop_t *dev)
Run every history commit still queued for dev, now.
static GQueue _pending_commits
void dt_dev_history_gui_init(void)
Install the history engine's GUI handlers. Called once from dt_init() beside the other handler instal...
static guint _pending_commit_source
static void _commit_gui(dt_develop_t *dev, dt_iop_module_t *module)
static void _queue_pending_commit(dt_develop_t *dev, dt_iop_module_t *module, const gboolean enable)
void dt_dev_history_gui_update(dt_develop_t *dev)
Apply history-loaded params to module GUIs.
static void _undo_restore_gui(dt_develop_t *dev, const int mask_edit_mode, const int request_mask_display)
The GTK half of the history engine, following the blend.c/blend_gui.c pattern.
void dt_dev_signal_modules_moved(dt_develop_t *dev)
Definition develop.c:1736
void dt_dev_masks_list_change(dt_develop_t *dev)
Definition develop.c:1302
int dt_dev_get_thumbnail_size(dt_develop_t *dev)
Definition develop.c:347
@ DT_DEV_PIXELPIPE_DISPLAY_MASK
Definition develop.h:123
static int dt_pthread_rwlock_unlock(dt_pthread_rwlock_t *rwlock)
Definition dtpthread.h:453
static int dt_pthread_rwlock_wrlock(dt_pthread_rwlock_t *rwlock)
Definition dtpthread.h:535
guint dt_gui_throttle_get_timeout_ms(void)
static gboolean enable(const dt_image_t *image)
Definition highlights.c:729
gboolean dt_iop_is_hidden(dt_iop_module_t *module)
Definition imageop.c:684
void dt_iop_gui_cleanup_module(dt_iop_module_t *module)
void dt_iop_gui_set_expander(dt_iop_module_t *module)
void dt_iop_gui_init(dt_iop_module_t *module)
void dt_iop_gui_update(dt_iop_module_t *module)
void dt_iop_gui_set_enable_button(dt_iop_module_t *module)
void dt_iop_request_focus(dt_iop_module_t *module)
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:65
#define dt_free(ptr)
Definition mem_alloc.h:97
dt_iop_module_t *gboolean enable
int32_t gui_attached
Definition develop.h:167
GList * iop
Definition develop.h:252
struct dt_iop_module_t * gui_module
Definition develop.h:170
dt_pthread_rwlock_t history_mutex
Definition develop.h:230
GList * history
Definition develop.h:242
struct dt_iop_module_gui_t * gui
Definition imageop.h:326
#define dt_gui_freeze_begin()
#define dt_gui_freeze_end()