Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
textnotes.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
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 "system/macros.h"
20#include "common/act_on.h"
21#include "colorprofiles/colorspaces.h" // dt_colorprofiles_srgb_to_display_strided()
22#include "system/mem_alloc.h"
24#include "common/paths.h"
25#include "widgets/gdkkeys.h"
26#include "common/datetime.h"
28#include "common/image.h"
29#include "caches/image_cache.h"
30#include "common/variables.h"
31#include "control/control.h"
32#include "control/jobs.h"
33#include "control/signal.h"
34#include "gui/application.h"
35#include "widgets/gtkentry.h"
36#include "libs/lib.h"
37#include "views/view.h"
38
39#include <glib.h>
40#include <glib/gstdio.h>
41#include <limits.h>
42#include <stdlib.h>
43#include <string.h>
44#include "widgets/label.h"
45#include "widgets/popup.h"
46#include "widgets/scroll_wrap.h"
48
49#ifdef HAVE_HTTP_SERVER
50#include <libsoup/soup.h>
51#endif
52
53#ifdef HAVE_CMARK
54#include <cmark.h>
55#endif
56DT_MODULE(1)
57
58typedef struct dt_lib_textnotes_t
59{
63 GtkTextView *edit_view;
64 GtkTextView *preview_view;
70 GtkListStore *completion_model;
71 GtkTextMark *completion_mark;
72 gchar *path;
73 gchar *image_path;
74 gchar *image_dir;
77 int32_t imgid;
79 int preview_render_width; // panel-driven width used at the last preview render (loop/debounce guard)
80 gboolean loading;
81 gboolean dirty;
82 gboolean rendering;
84#ifdef HAVE_HTTP_SERVER
85 GHashTable *download_inflight;
86#endif
88
89const char *name(dt_lib_module_t *self)
90{
91 return _("Notes");
92}
93
94const char **views(dt_lib_module_t *self)
95{
96 static const char *v[] = { "darkroom", "lighttable", "studio_capture", NULL };
97 return v;
98}
99
101{
103}
104
106{
107 return 200;
108}
109
110static void _save_now(dt_lib_module_t *self);
111static void _render_preview(dt_lib_textnotes_t *d, const char *text);
113static gboolean _image_has_txt_flag(const int32_t imgid);
114static gboolean _set_image_paths(dt_lib_textnotes_t *d, const int32_t imgid);
116static gboolean _textnotes_load_finish_idle(gpointer user_data);
117static int32_t _textnotes_load_job_run(dt_job_t *job);
119static void _textnotes_load_job_cleanup(void *data);
120
129
137
138static gchar *_get_buffer_text(GtkTextBuffer *buffer)
139{
140 GtkTextIter start, end;
141 gtk_text_buffer_get_bounds(buffer, &start, &end);
142 return gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
143}
144
146{
147 if(IS_NULL_PTR(d) || !d->edit_view) return g_strdup("");
148 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
149 return _get_buffer_text(buffer);
150}
151
152static void _set_edit_text(dt_lib_textnotes_t *d, const char *text)
153{
154 if(IS_NULL_PTR(d) || !d->edit_view) return;
155 d->loading = TRUE;
156 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
157 gtk_text_buffer_set_text(buffer, text ? text : "", -1);
158 d->loading = FALSE;
159}
160
162{
163 if(size) *size = 0;
165 if(IS_NULL_PTR(d) || !d->edit_view) return NULL;
166
167 gchar *text = _get_edit_text(d);
168 if(IS_NULL_PTR(text)) return NULL;
169
170 if(size) *size = strlen(text) + 1;
171 return text;
172}
173
174int set_params(dt_lib_module_t *self, const void *params, int size)
175{
176 if(IS_NULL_PTR(params) || size <= 0) return 1;
177
179 if(IS_NULL_PTR(d) || !d->edit_view) return 1;
180
181 gchar *text = g_strndup((const gchar *)params, size);
182 _set_edit_text(d, text);
183 d->dirty = TRUE;
184 _save_now(self);
185
186 dt_free(text);
187 return 0;
188}
189
191{
192 static const char default_text[] =
193 "## Todo\n"
194 "\n"
195 "- [ ] Normalize illuminant & colors\n"
196 "- [ ] Normalize contrast & dynamic range\n"
197 "- [ ] Fix lens distortion and noise\n"
198 "- [ ] Enhance colors\n"
199 "\n"
200 "## Resources\n"
201 "\n"
202 "- [Documentation](https://ansel.photos/en/doc)\n"
203 "\n"
204 "## Lifecycle\n"
205 "\n"
206 "- Shot: $(EXIF.YEAR)-$(EXIF.MONTH)-$(EXIF.DAY) $(EXIF.HOUR):$(EXIF.MINUTE)\n"
207 "- Imported: $(IMPORT.DATE)\n"
208 "- Last edited: $(CHANGE.DATE)\n"
209 "- Exported: $(EXPORT.DATE)\n"
210 "\n"
211 "![](https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba)";
212
213 dt_lib_presets_add(_("Default"), self->plugin_name, self->version(),
214 default_text, sizeof(default_text), TRUE);
215}
216
218{
219 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->preview_view)) return 0;
220
221 GtkTextView *tv = d->preview_view;
222 GdkWindow *tw = gtk_text_view_get_window(tv, GTK_TEXT_WINDOW_TEXT);
223 if(IS_NULL_PTR(tw)) return 0;
224
225 return gdk_window_get_width(tw);
226}
227
229{
230 if(IS_NULL_PTR(d)) return;
231 gchar *text = _get_edit_text(d);
232 _render_preview(d, text);
233 dt_free(text);
234}
235
245static void _preview_width_changed(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data)
246{
247 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
249 if(IS_NULL_PTR(d) || d->rendering || IS_NULL_PTR(d->stack)) return;
250 if(g_strcmp0(gtk_stack_get_visible_child_name(GTK_STACK(d->stack)), "preview") != 0) return;
251
252 if(ABS(allocation->width - d->preview_render_width) < DT_PIXEL_APPLY_DPI(8)) return;
253 d->preview_render_width = allocation->width;
255}
256
258{
259 if(IS_NULL_PTR(d)) return;
260 if(d->completion_popover)
261 gtk_widget_hide(d->completion_popover);
262 if(d->completion_mark && d->edit_view)
263 {
264 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
265 gtk_text_buffer_delete_mark(buffer, d->completion_mark);
266 d->completion_mark = NULL;
267 }
268}
269
270static gboolean _completion_match(const char *item, const char *prefix)
271{
272 if(IS_NULL_PTR(prefix) || !*prefix) return TRUE;
273 if(IS_NULL_PTR(item)) return FALSE;
274
275 gchar *norm_item = g_utf8_normalize(item, -1, G_NORMALIZE_ALL);
276 gchar *norm_prefix = g_utf8_normalize(prefix, -1, G_NORMALIZE_ALL);
277 if(!norm_item || !norm_prefix)
278 {
279 dt_free(norm_item);
280 dt_free(norm_prefix);
281 return FALSE;
282 }
283
284 gchar *case_item = g_utf8_casefold(norm_item, -1);
285 gchar *case_prefix = g_utf8_casefold(norm_prefix, -1);
286 const gboolean match = case_item && case_prefix && g_str_has_prefix(case_item, case_prefix);
287 dt_free(case_item);
288 dt_free(case_prefix);
289 dt_free(norm_item);
290 dt_free(norm_prefix);
291 return match;
292}
293
294static void _completion_fill(dt_lib_textnotes_t *d, const char *prefix)
295{
296 if(IS_NULL_PTR(d) || !d->completion_model) return;
297 gtk_list_store_clear(d->completion_model);
298
300 GtkTreeIter iter;
301 for(const dt_gtkentry_completion_spec *l = list; l && l->varname; l++)
302 {
303 if(!_completion_match(l->varname, prefix)) continue;
304 gtk_list_store_append(d->completion_model, &iter);
305 gtk_list_store_set(d->completion_model, &iter, COMPL_VARNAME, l->varname,
306 COMPL_DESCRIPTION, _(l->description), -1);
307 }
308}
309
310static gboolean _completion_find_prefix(dt_lib_textnotes_t *d, GtkTextIter *cursor,
311 GtkTextIter *start_iter, gchar **prefix_out)
312{
313 if(IS_NULL_PTR(d) || !d->edit_view || IS_NULL_PTR(cursor) || IS_NULL_PTR(start_iter) || IS_NULL_PTR(prefix_out)) return FALSE;
314
315 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
316 GtkTextIter line_start = *cursor;
317 gtk_text_iter_set_line_offset(&line_start, 0);
318
319 gchar *line = gtk_text_buffer_get_text(buffer, &line_start, cursor, FALSE);
320 if(IS_NULL_PTR(line)) return FALSE;
321
322 gchar *match = g_strrstr(line, "$(");
323 if(!match)
324 {
325 dt_free(line);
326 return FALSE;
327 }
328
329 if(strchr(match, ')'))
330 {
331 dt_free(line);
332 return FALSE;
333 }
334
335 gchar *prefix = match + 2;
336 for(const gchar *p = prefix; *p; p++)
337 {
338 if(g_ascii_isspace(*p))
339 {
340 dt_free(line);
341 return FALSE;
342 }
343 }
344
345 const int byte_offset = (int)(match - line);
346 const int char_offset = g_utf8_strlen(line, byte_offset);
347 *start_iter = line_start;
348 gtk_text_iter_set_line_offset(start_iter, char_offset + 2);
349
350 *prefix_out = g_strdup(prefix);
351 dt_free(line);
352 return TRUE;
353}
354
356{
358 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->completion_tree) || !d->edit_view || IS_NULL_PTR(d->completion_mark)) return FALSE;
359
360 GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->completion_tree));
361 GtkTreeModel *model = NULL;
362 GtkTreeIter iter;
363 if(!gtk_tree_selection_get_selected(sel, &model, &iter)) return FALSE;
364
365 gchar *varname = NULL;
366 gtk_tree_model_get(model, &iter, COMPL_VARNAME, &varname, -1);
367 if(IS_NULL_PTR(varname)) return FALSE;
368
369 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
370 GtkTextIter start, end;
371 gtk_text_buffer_get_iter_at_mark(buffer, &start, d->completion_mark);
372 gtk_text_buffer_get_iter_at_mark(buffer, &end, gtk_text_buffer_get_insert(buffer));
373 gtk_text_buffer_delete(buffer, &start, &end);
374
375 gchar *insert = g_strdup_printf("%s)", varname);
376 gtk_text_buffer_insert(buffer, &start, insert, -1);
377 dt_free(insert);
378 dt_free(varname);
379
381 return TRUE;
382}
383
385{
387 if(IS_NULL_PTR(d) || !d->edit_view || IS_NULL_PTR(d->completion_popover)) return;
388 if(!gtk_widget_get_visible(GTK_WIDGET(d->edit_view)))
389 {
391 return;
392 }
393
394 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
395 GtkTextIter cursor;
396 gtk_text_buffer_get_iter_at_mark(buffer, &cursor, gtk_text_buffer_get_insert(buffer));
397
398 GtkTextIter start_iter;
399 gchar *prefix = NULL;
400 if(!_completion_find_prefix(d, &cursor, &start_iter, &prefix))
401 {
403 return;
404 }
405
406 _completion_fill(d, prefix);
407 dt_free(prefix);
408
409 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->completion_tree));
410 if(!model || gtk_tree_model_iter_n_children(model, NULL) <= 0)
411 {
413 return;
414 }
415
416 GtkTreeIter first;
417 if(gtk_tree_model_get_iter_first(model, &first))
418 {
419 GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->completion_tree));
420 gtk_tree_selection_select_iter(sel, &first);
421 }
422
423 if(d->completion_mark)
424 gtk_text_buffer_move_mark(buffer, d->completion_mark, &start_iter);
425 else
426 d->completion_mark = gtk_text_buffer_create_mark(buffer, NULL, &start_iter, TRUE);
427
428 GdkRectangle rect = { 0 };
429 gtk_text_view_get_iter_location(d->edit_view, &cursor, &rect);
430 gtk_text_view_buffer_to_window_coords(d->edit_view, GTK_TEXT_WINDOW_WIDGET,
431 rect.x, rect.y + rect.height, &rect.x, &rect.y);
432 GtkWidget *anchor = dt_gui_get_popup_relative_widget(d->root ? d->root : GTK_WIDGET(d->edit_view), NULL);
433 gtk_popover_set_relative_to(GTK_POPOVER(d->completion_popover), anchor ? anchor : GTK_WIDGET(d->edit_view));
434 if(anchor && anchor != GTK_WIDGET(d->edit_view))
435 gtk_widget_translate_coordinates(GTK_WIDGET(d->edit_view), anchor, rect.x, rect.y, &rect.x, &rect.y);
436 if(rect.width <= 0) rect.width = 1;
437 rect.height = 1;
438 gtk_popover_set_pointing_to(GTK_POPOVER(d->completion_popover), &rect);
439 gtk_widget_show_all(d->completion_popover);
440#if GTK_CHECK_VERSION(3, 22, 0)
441 gtk_popover_popup(GTK_POPOVER(d->completion_popover));
442#endif
443}
444
445static gboolean _completion_focus_out_idle(gpointer user_data)
446{
447 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
448 dt_lib_textnotes_t *d = self ? (dt_lib_textnotes_t *)self->data : NULL;
449 if(IS_NULL_PTR(d)) return G_SOURCE_REMOVE;
450
451 if(d->completion_popover && gtk_widget_get_visible(d->completion_popover))
452 {
453 GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(d->edit_view));
454 if(GTK_IS_WINDOW(toplevel))
455 {
456 GtkWidget *focus = gtk_window_get_focus(GTK_WINDOW(toplevel));
457 if(focus && gtk_widget_is_ancestor(focus, d->completion_popover))
458 return G_SOURCE_REMOVE;
459 }
460 }
461
463 _save_now(self);
464 return G_SOURCE_REMOVE;
465}
466
467static gboolean _edit_key_press(GtkWidget *widget, GdkEventKey *event, dt_lib_module_t *self)
468{
470 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->completion_popover)) return FALSE;
471 if(!gtk_widget_get_visible(d->completion_popover)) return FALSE;
472 guint key = dt_keys_mainpad_alternatives(event->keyval);
473
474 if(key == GDK_KEY_Escape)
475 {
477 return TRUE;
478 }
479
480 if(key == GDK_KEY_Return || key == GDK_KEY_Tab)
481 {
483 return TRUE;
484 }
485
486 (void)widget;
487 return FALSE;
488}
489
490static gboolean _edit_key_release(GtkWidget *widget, GdkEventKey *event, dt_lib_module_t *self)
491{
492 _completion_update(self);
493 (void)widget;
494 (void)event;
495 return FALSE;
496}
497
498static gboolean _edit_button_release(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
499{
500 _completion_update(self);
501 (void)widget;
502 (void)event;
503 return FALSE;
504}
505
506static void _completion_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column,
507 dt_lib_module_t *self)
508{
510 (void)tree;
511 (void)path;
512 (void)column;
513}
514
515static void _setup_completion(dt_lib_module_t *self, GtkWidget *textview)
516{
518 if(IS_NULL_PTR(d) || IS_NULL_PTR(textview)) return;
519
520 d->completion_model = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
521 GtkWidget *completion_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(d->completion_model));
522 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(completion_tree), FALSE);
523 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
524 GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes(_("variable"), renderer,
525 "text", COMPL_DESCRIPTION, NULL);
526 gtk_tree_view_append_column(GTK_TREE_VIEW(completion_tree), col);
527 GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(completion_tree));
528 gtk_tree_selection_set_mode(sel, GTK_SELECTION_SINGLE);
529 g_signal_connect(completion_tree, "row-activated", G_CALLBACK(_completion_row_activated), self);
530
531 GtkWidget *completion_sw = gtk_scrolled_window_new(NULL, NULL);
532 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(completion_sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
533 gtk_container_add(GTK_CONTAINER(completion_sw), completion_tree);
534 gtk_widget_set_size_request(completion_sw, 360, 200);
535
536 d->completion_popover = gtk_popover_new(NULL);
537 gtk_popover_set_position(GTK_POPOVER(d->completion_popover), GTK_POS_BOTTOM);
538 GtkWidget *relative = dt_gui_get_popup_relative_widget(textview, NULL);
539 gtk_popover_set_relative_to(GTK_POPOVER(d->completion_popover), relative ? relative : textview);
540 gtk_container_add(GTK_CONTAINER(d->completion_popover), completion_sw);
541 d->completion_tree = completion_tree;
542}
543
544static void _colorcorrect_pixbuf(GdkPixbuf *pixbuf)
545{
546 if(IS_NULL_PTR(pixbuf)) return;
547
548 /* The pixbuf is sRGB; bring it into display space. The transform, the lock and the
549 * row scratch all live inside colorprofiles -- this side only reads the geometry
550 * GdkPixbuf reports. */
551 dt_colorprofiles_srgb_to_display_strided(gdk_pixbuf_get_pixels(pixbuf),
552 gdk_pixbuf_get_width(pixbuf),
553 gdk_pixbuf_get_height(pixbuf),
554 gdk_pixbuf_get_rowstride(pixbuf),
555 gdk_pixbuf_get_n_channels(pixbuf),
556 gdk_pixbuf_get_has_alpha(pixbuf));
557}
558
559static void _toggle_mode(GtkToggleButton *button, dt_lib_module_t *self);
560static void _load_for_image(dt_lib_module_t *self, const int32_t imgid);
561static gboolean _refresh_preview_idle(gpointer user_data);
562
563static void _open_uri(const char *uri)
564{
565 if(IS_NULL_PTR(uri) || !*uri) return;
566
567 GtkWindow *win = NULL;
569 win = GTK_WINDOW(dt_gui_main_window());
570
571 GError *error = NULL;
572 const gboolean ok = gtk_show_uri_on_window(win, uri, GDK_CURRENT_TIME, &error);
573 if(!ok && error)
574 {
575 dt_control_log(_("could not open link: %s"), error->message);
576 g_clear_error(&error);
577 }
578}
579
580static gchar *_expand_text_for_preview(dt_lib_textnotes_t *d, const char *source_text)
581{
582 if(IS_NULL_PTR(d) || d->imgid <= 0) return NULL;
583 if(IS_NULL_PTR(source_text) || !*source_text) return NULL;
584 if(!strstr(source_text, "$(")) return NULL;
585
586 if(!_set_image_paths(d, d->imgid))
587 return NULL;
588 if(IS_NULL_PTR(d->vars_params))
589 dt_variables_params_init(&d->vars_params);
590
591 dt_variables_params_t *vp = d->vars_params;
592 vp->filename = d->image_path;
593 vp->jobcode = "textnotes";
594 vp->imgid = d->imgid;
595 vp->sequence = 0;
596 vp->escape_markup = FALSE;
597
598 gchar *tmp = g_strdup(source_text ? source_text : "");
599 gchar *expanded = dt_variables_expand(vp, tmp, TRUE);
600 dt_free(tmp);
601 return expanded;
602}
603
604#ifdef HAVE_CMARK
605typedef struct dt_textnotes_list_state_t
606{
607 gboolean ordered;
608 int index;
609} dt_textnotes_list_state_t;
610
611typedef struct dt_textnotes_image_state_t
612{
613 gboolean suppress_text;
614 gboolean tag_added;
615} dt_textnotes_image_state_t;
616
617static void _buffer_append_newline(GtkTextBuffer *buffer)
618{
619 GtkTextIter end;
620 gtk_text_buffer_get_end_iter(buffer, &end);
621 if(gtk_text_iter_is_start(&end)) return;
622 GtkTextIter it = end;
623 if(gtk_text_iter_backward_char(&it) && gtk_text_iter_get_char(&it) != '\n')
624 gtk_text_buffer_insert(buffer, &end, "\n", 1);
625}
626
627static void _buffer_append_blankline(GtkTextBuffer *buffer)
628{
629 GtkTextIter end;
630 gtk_text_buffer_get_end_iter(buffer, &end);
631 if(gtk_text_iter_is_start(&end)) return;
632
633 GtkTextIter it = end;
634 if(gtk_text_iter_backward_char(&it))
635 {
636 if(gtk_text_iter_get_char(&it) == '\n')
637 {
638 GtkTextIter it2 = it;
639 if(gtk_text_iter_backward_char(&it2) && gtk_text_iter_get_char(&it2) == '\n') return;
640 gtk_text_buffer_insert(buffer, &end, "\n", 1);
641 return;
642 }
643 }
644
645 gtk_text_buffer_insert(buffer, &end, "\n\n", 2);
646}
647
648static void _insert_with_tags(GtkTextBuffer *buffer, const char *text, GPtrArray *tags)
649{
650 if(IS_NULL_PTR(text) || !*text) return;
651 GtkTextIter start, end;
652 gtk_text_buffer_get_end_iter(buffer, &start);
653 GtkTextMark *mark = gtk_text_buffer_create_mark(buffer, NULL, &start, TRUE);
654 end = start;
655 gtk_text_buffer_insert(buffer, &end, text, -1);
656 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
657 for(guint i = 0; i < tags->len; i++)
658 gtk_text_buffer_apply_tag(buffer, g_ptr_array_index(tags, i), &start, &end);
659 gtk_text_buffer_delete_mark(buffer, mark);
660}
661
662static void _emit_list_prefix(GtkTextBuffer *buffer, GArray *list_stack, const gboolean checkbox,
663 const gboolean checked, const int checklist_line)
664{
665 GtkTextIter end;
666 gtk_text_buffer_get_end_iter(buffer, &end);
667
668 const int depth = list_stack->len;
669 for(int i = 1; i < depth; i++) gtk_text_buffer_insert(buffer, &end, " ", 2);
670
671 dt_textnotes_list_state_t *st = NULL;
672 if(depth > 0)
673 st = &g_array_index(list_stack, dt_textnotes_list_state_t, depth - 1);
674
675 if(checkbox)
676 {
677 GtkTextTag *checkbox_tag = gtk_text_buffer_create_tag(buffer, NULL, "scale", 1.1, NULL);
678 if(checklist_line > 0)
679 g_object_set_data(G_OBJECT(checkbox_tag), "checklist_line", GINT_TO_POINTER(checklist_line));
680 GtkTextIter start = end;
681 GtkTextMark *mark = gtk_text_buffer_create_mark(buffer, NULL, &start, TRUE);
682 gtk_text_buffer_insert(buffer, &end, checked ? "\u2611" : "\u2610", -1);
683 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
684 gtk_text_buffer_apply_tag(buffer, checkbox_tag, &start, &end);
685 gtk_text_buffer_delete_mark(buffer, mark);
686 gtk_text_buffer_insert(buffer, &end, " ", 1);
687 if(st && st->ordered) st->index++;
688 return;
689 }
690
691 if(st && st->ordered)
692 {
693 gchar *num = g_strdup_printf("%d. ", st->index);
694 gtk_text_buffer_insert(buffer, &end, num, -1);
695 dt_free(num);
696 st->index++;
697 }
698 else
699 {
700 gtk_text_buffer_insert(buffer, &end, "- ", 2);
701 }
702}
703
704static void _collect_text_tag(GtkTextTag *tag, gpointer user_data)
705{
706 GPtrArray *tags = (GPtrArray *)user_data;
707 g_ptr_array_add(tags, tag);
708}
709
710static void _clear_tag_table(GtkTextBuffer *buffer)
711{
712 GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
713 GPtrArray *tags = g_ptr_array_new();
714 gtk_text_tag_table_foreach(table, _collect_text_tag, tags);
715 for(guint i = 0; i < tags->len; i++)
716 gtk_text_tag_table_remove(table, g_ptr_array_index(tags, i));
717 g_ptr_array_free(tags, TRUE);
718}
719
720typedef struct dt_textnotes_tags_t
721{
722 GtkTextTag *bold;
723 GtkTextTag *italic;
724 GtkTextTag *mono;
725 GtkTextTag *h1;
726 GtkTextTag *h2;
727 GtkTextTag *h3;
728} dt_textnotes_tags_t;
729
730static dt_textnotes_tags_t _create_preview_tags(GtkTextBuffer *buffer)
731{
732 dt_textnotes_tags_t tags = { 0 };
733 tags.bold = gtk_text_buffer_create_tag(buffer, "tn_bold", "weight", PANGO_WEIGHT_BOLD, NULL);
734 tags.italic = gtk_text_buffer_create_tag(buffer, "tn_italic", "style", PANGO_STYLE_ITALIC, NULL);
735 tags.mono = gtk_text_buffer_create_tag(buffer, "tn_mono", "family", "monospace", NULL);
736 tags.h1 = gtk_text_buffer_create_tag(buffer, "tn_h1", "weight", PANGO_WEIGHT_BOLD, "scale", 1.4, NULL);
737 tags.h2 = gtk_text_buffer_create_tag(buffer, "tn_h2", "weight", PANGO_WEIGHT_BOLD, "scale", 1.25, NULL);
738 tags.h3 = gtk_text_buffer_create_tag(buffer, "tn_h3", "weight", PANGO_WEIGHT_BOLD, "scale", 1.15, NULL);
739 return tags;
740}
741
742static void _pop_active_tag(GPtrArray *active_tags)
743{
744 if(active_tags->len > 0)
745 g_ptr_array_remove_index(active_tags, active_tags->len - 1);
746}
747
748static void _push_link_tag(GtkTextBuffer *buffer, GPtrArray *active_tags, const char *url)
749{
750 GtkTextTag *tag = gtk_text_buffer_create_tag(buffer, NULL,
751 "underline", PANGO_UNDERLINE_SINGLE,
752 NULL);
753 if(url && *url)
754 g_object_set_data_full(G_OBJECT(tag), "href", g_strdup(url), g_free);
755 g_ptr_array_add(active_tags, tag);
756}
757
758static void _insert_mono_text(GtkTextBuffer *buffer, GtkTextTag *tag_mono, const char *lit)
759{
760 if(IS_NULL_PTR(lit) || !*lit) return;
761 GtkTextIter start, end;
762 gtk_text_buffer_get_end_iter(buffer, &start);
763 GtkTextMark *mark = gtk_text_buffer_create_mark(buffer, NULL, &start, TRUE);
764 end = start;
765 gtk_text_buffer_insert(buffer, &end, lit, -1);
766 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
767 gtk_text_buffer_apply_tag(buffer, tag_mono, &start, &end);
768 gtk_text_buffer_delete_mark(buffer, mark);
769}
770
771static void _emit_pending_list_prefix(GtkTextBuffer *buffer, GArray *list_stack,
772 gboolean *item_pending_prefix)
773{
774 if(*item_pending_prefix)
775 {
776 _emit_list_prefix(buffer, list_stack, FALSE, FALSE, 0);
777 *item_pending_prefix = FALSE;
778 }
779}
780
781static const char *_handle_list_text_prefix(GtkTextBuffer *buffer, GArray *list_stack,
782 gboolean *item_pending_prefix,
783 const char *lit, const int line_no)
784{
785 if(IS_NULL_PTR(lit) || !*lit) return lit;
786
787 if(*item_pending_prefix && lit[0] == '[' && lit[2] == ']'
788 && (lit[1] == ' ' || lit[1] == 'x' || lit[1] == 'X'))
789 {
790 const gboolean checked = (lit[1] == 'x' || lit[1] == 'X');
791 _emit_list_prefix(buffer, list_stack, TRUE, checked, line_no);
792 *item_pending_prefix = FALSE;
793 int offset = 3;
794 if(lit[3] == ' ') offset = 4;
795 return lit + offset;
796 }
797
798 if(*item_pending_prefix)
799 {
800 _emit_list_prefix(buffer, list_stack, FALSE, FALSE, 0);
801 *item_pending_prefix = FALSE;
802 }
803
804 return lit;
805}
806
807static void _list_push(GArray *list_stack, cmark_node *node)
808{
809 dt_textnotes_list_state_t st = {
810 .ordered = (cmark_node_get_list_type(node) == CMARK_ORDERED_LIST),
811 .index = cmark_node_get_list_start(node)
812 };
813 g_array_append_val(list_stack, st);
814}
815
816static void _list_pop(GtkTextBuffer *buffer, GArray *list_stack)
817{
818 if(list_stack->len > 0) g_array_remove_index(list_stack, list_stack->len - 1);
819 _buffer_append_blankline(buffer);
820}
821
822static void _list_item_enter(GtkTextBuffer *buffer, gboolean *in_list_item, gboolean *item_pending_prefix)
823{
824 _buffer_append_newline(buffer);
825 *in_list_item = TRUE;
826 *item_pending_prefix = TRUE;
827}
828
829static void _list_item_leave(GtkTextBuffer *buffer, gboolean *in_list_item, gboolean *item_pending_prefix)
830{
831 _buffer_append_newline(buffer);
832 *in_list_item = FALSE;
833 *item_pending_prefix = FALSE;
834}
835
836static gboolean _is_remote_url(const char *url)
837{
838 if(IS_NULL_PTR(url) || !*url) return FALSE;
839 return g_str_has_prefix(url, "http://") || g_str_has_prefix(url, "https://");
840}
841
842static gchar *_remote_cache_path(const char *url)
843{
844 if(IS_NULL_PTR(url) || !*url) return NULL;
845
846 gchar *hash = g_compute_checksum_for_string(G_CHECKSUM_SHA1, url, -1);
847 if(IS_NULL_PTR(hash)) return NULL;
848
849 const char *end = strchr(url, '?');
850 if(IS_NULL_PTR(end)) end = url + strlen(url);
851 const char *slash = end;
852 while(slash > url && *slash != '/') slash--;
853 if(*slash == '/') slash++;
854 const char *dot = NULL;
855 for(const char *p = end - 1; p > slash; p--)
856 {
857 if(*p == '.')
858 {
859 dot = p;
860 break;
861 }
862 }
863
864 gchar *filename = NULL;
865 if(dot && (end - dot) <= 8)
866 filename = g_strconcat(hash, dot, NULL);
867 else
868 filename = g_strdup(hash);
869
870 dt_free(hash);
871
872 /* dt_loc_cachedir(), not g_get_user_cache_dir(): this built the path a download is WRITTEN to
873 * while _textnotes_download_start() created the directory under dt_loc_cachedir(), so under
874 * --cachedir the two named different places and the write went to a directory nobody made. */
875 gchar *cache_dir = g_build_filename(dt_loc_cachedir(), "downloads", NULL);
876 gchar *path = g_build_filename(cache_dir, filename, NULL);
877 dt_free(cache_dir);
878 dt_free(filename);
879 return path;
880}
881
882#ifdef HAVE_HTTP_SERVER
883typedef struct dt_textnotes_fetch_t
884{
885 dt_lib_module_t *self;
887 gchar *url;
888 gchar *path;
889} dt_textnotes_fetch_t;
890
891static SoupSession *_textnotes_soup_session(void)
892{
893 static SoupSession *session = NULL;
894 if(session) return session;
895 session = soup_session_new();
896 if(session)
897 g_object_set(session, "timeout", 10, "user-agent", "Ansel", NULL);
898 return session;
899}
900
901static void _finish_remote_download(dt_textnotes_fetch_t *fetch, gboolean ok)
902{
903 if(fetch->d && fetch->d->download_inflight && fetch->url)
904 g_hash_table_remove(fetch->d->download_inflight, fetch->url);
905
906 if(ok && fetch->self)
907 g_idle_add(_refresh_preview_idle, fetch->self);
908
909 dt_free(fetch->url);
910 dt_free(fetch->path);
911 dt_free(fetch);
912}
913
914#if LIBSOUP_VERSION_MAJOR >= 3
915static void _remote_download_cb(GObject *source, GAsyncResult *res, gpointer user_data)
916{
917 dt_textnotes_fetch_t *fetch = (dt_textnotes_fetch_t *)user_data;
918 SoupSession *session = SOUP_SESSION(source);
919 GError *error = NULL;
920 GBytes *bytes = soup_session_send_and_read_finish(session, res, &error);
921
922 if(IS_NULL_PTR(bytes) || error)
923 {
924 if(error) g_clear_error(&error);
925 if(bytes) g_bytes_unref(bytes);
926 _finish_remote_download(fetch, FALSE);
927 return;
928 }
929
930 const gsize len = g_bytes_get_size(bytes);
931 const void *data = g_bytes_get_data(bytes, NULL);
932 gboolean ok = FALSE;
933 if(fetch->path && data && len > 0)
934 ok = g_file_set_contents(fetch->path, data, (gssize)len, NULL);
935
936 g_bytes_unref(bytes);
937 _finish_remote_download(fetch, ok);
938}
939#else
940static void _remote_download_cb(SoupSession *session, SoupMessage *msg, gpointer user_data)
941{
942 dt_textnotes_fetch_t *fetch = (dt_textnotes_fetch_t *)user_data;
943 gboolean ok = FALSE;
944
945 if(msg->status_code == SOUP_STATUS_OK && msg->response_body && msg->response_body->data)
946 {
947 ok = g_file_set_contents(fetch->path,
948 msg->response_body->data,
949 (gssize)msg->response_body->length,
950 NULL);
951 }
952
953 _finish_remote_download(fetch, ok);
954}
955#endif
956
957static void _queue_remote_download(dt_lib_module_t *self, dt_lib_textnotes_t *d,
958 const char *url, const char *path)
959{
960 if(IS_NULL_PTR(self) || IS_NULL_PTR(d) || IS_NULL_PTR(url) || IS_NULL_PTR(path)) return;
961 if(IS_NULL_PTR(d->download_inflight))
962 d->download_inflight = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
963 if(g_hash_table_contains(d->download_inflight, url)) return;
964
965 gchar *cache_dir = g_build_filename(dt_loc_cachedir(), "downloads", NULL);
966 g_mkdir_with_parents(cache_dir, 0700);
967 dt_free(cache_dir);
968
969 SoupSession *session = _textnotes_soup_session();
970 if(IS_NULL_PTR(session)) return;
971
972 SoupMessage *msg = soup_message_new("GET", url);
973 if(IS_NULL_PTR(msg)) return;
974
975 dt_textnotes_fetch_t *fetch = g_new0(dt_textnotes_fetch_t, 1);
976 fetch->self = self;
977 fetch->d = d;
978 fetch->url = g_strdup(url);
979 fetch->path = g_strdup(path);
980
981 g_hash_table_add(d->download_inflight, g_strdup(url));
982
983#if LIBSOUP_VERSION_MAJOR >= 3
984 soup_session_send_and_read_async(session, msg, G_PRIORITY_DEFAULT, NULL, _remote_download_cb, fetch);
985#else
986 soup_session_queue_message(session, msg, _remote_download_cb, fetch);
987#endif
988}
989#endif
990
991static gchar *_resolve_image_path(const char *url, const char *base_dir)
992{
993 if(IS_NULL_PTR(url) || !*url) return NULL;
994 if(_is_remote_url(url) || g_str_has_prefix(url, "ftp://"))
995 return NULL;
996
997 if(g_str_has_prefix(url, "file://"))
998 return g_filename_from_uri(url, NULL, NULL);
999
1000 if(g_path_is_absolute(url))
1001 {
1002 gchar *unescaped = g_uri_unescape_string(url, NULL);
1003 return unescaped ? unescaped : g_strdup(url);
1004 }
1005
1006 if(IS_NULL_PTR(base_dir) || !*base_dir)
1007 return NULL;
1008
1009 gchar *unescaped = g_uri_unescape_string(url, NULL);
1010 gchar *result = g_build_filename(base_dir, unescaped ? unescaped : url, NULL);
1011 dt_free(unescaped);
1012 return result;
1013}
1014
1015static gchar *_get_image_base_dir(dt_lib_textnotes_t *d)
1016{
1017 if(IS_NULL_PTR(d) || d->imgid <= 0) return NULL;
1018
1019 if(!_set_image_paths(d, d->imgid))
1020 return NULL;
1021 if(d->image_dir) return g_strdup(d->image_dir);
1022 return g_path_get_dirname(d->image_path);
1023}
1024
1025static int _get_preview_scale(dt_lib_textnotes_t *d)
1026{
1027 int scale = 1;
1028 if(d && d->preview_view)
1029 scale = gtk_widget_get_scale_factor(GTK_WIDGET(d->preview_view));
1030 if(scale <= 0) scale = 1;
1031 return scale;
1032}
1033
1034static int _compute_max_image_width(dt_lib_textnotes_t *d, const int scale, gboolean *have_device)
1035{
1036 int device_w = _preview_text_window_width_px(d);
1037 if(have_device) *have_device = (device_w > 0);
1038
1039 int max_w = 0;
1040 if(device_w > 0)
1041 {
1042 const int dpad = (scale > 1) ? 3 : 2; // slightly tighter on HiDPI
1043 if(device_w > dpad) device_w -= dpad;
1044 max_w = device_w / scale;
1045 if(max_w < 1) max_w = 1;
1046 }
1047
1048 if(max_w <= 0 && d->preview_view)
1049 {
1050 GdkRectangle rect = { 0 };
1051 gtk_text_view_get_visible_rect(d->preview_view, &rect);
1052 if(rect.width > 0) max_w = rect.width;
1053 }
1054 if(max_w <= 0 && d->preview_view)
1055 max_w = gtk_widget_get_allocated_width(GTK_WIDGET(d->preview_view));
1056 if(max_w <= 0 && d->preview_sw)
1057 max_w = gtk_widget_get_allocated_width(GTK_WIDGET(d->preview_sw));
1058 if(max_w <= 0 && d->root)
1059 max_w = gtk_widget_get_allocated_width(d->root);
1060
1061 if(d->preview_view)
1062 {
1063 const int margin = gtk_text_view_get_left_margin(d->preview_view)
1064 + gtk_text_view_get_right_margin(d->preview_view);
1065 if(margin > 0 && max_w > margin) max_w -= margin;
1066 }
1067
1068 if((!have_device || !*have_device) && d->preview_view)
1069 {
1070 GtkStyleContext *ctx = gtk_widget_get_style_context(GTK_WIDGET(d->preview_view));
1071 GtkStateFlags state = gtk_widget_get_state_flags(GTK_WIDGET(d->preview_view));
1072 GtkBorder padding = { 0 }, border = { 0 };
1073 gtk_style_context_get_padding(ctx, state, &padding);
1074 gtk_style_context_get_border(ctx, state, &border);
1075 const int chrome = padding.left + padding.right + border.left + border.right;
1076 if(chrome > 0 && max_w > chrome) max_w -= chrome;
1077 }
1078
1079 // Hard cap: never wider than the width the panel grants the scrolled window, so a wide image can
1080 // never push the textview / module / sidebar wider. This enforces top-down sizing as a safety net
1081 // on top of the size-allocate driven re-render.
1082 if(d->preview_sw)
1083 {
1084 const int sw_w = gtk_widget_get_allocated_width(d->preview_sw);
1085 if(sw_w > 0)
1086 {
1087 // leave room for the vertical scrollbar gutter + recessed frame padding
1088 const int chrome = DT_PIXEL_APPLY_DPI(16);
1089 const int cap = (sw_w > chrome) ? sw_w - chrome : sw_w;
1090 if(max_w <= 0 || max_w > cap) max_w = cap;
1091 }
1092 }
1093
1094 if(max_w > 2) max_w -= 2;
1095 return max_w;
1096}
1097
1098static GdkPixbuf *_load_scaled_pixbuf(const char *path, const int target_w, GError **error)
1099{
1100 if(target_w > 0)
1101 return gdk_pixbuf_new_from_file_at_scale(path, target_w, -1, TRUE, error);
1102 return gdk_pixbuf_new_from_file(path, error);
1103}
1104
1105static void _insert_pixbuf_widget(dt_lib_textnotes_t *d, GtkTextBuffer *buffer,
1106 GdkPixbuf *pixbuf, const int max_w)
1107{
1108 GtkWidget *image = gtk_image_new_from_pixbuf(pixbuf);
1109 if(max_w > 0)
1110 gtk_widget_set_size_request(image, max_w, -1);
1111 gtk_widget_set_halign(image, GTK_ALIGN_START);
1112 gtk_widget_set_margin_top(image, 2);
1113 gtk_widget_set_margin_bottom(image, 6);
1114
1115 GtkTextIter iter;
1116 gtk_text_buffer_get_end_iter(buffer, &iter);
1117 GtkTextChildAnchor *anchor = gtk_text_buffer_create_child_anchor(buffer, &iter);
1118 gtk_text_view_add_child_at_anchor(d->preview_view, image, anchor);
1119 gtk_widget_show(image);
1120}
1121
1122static gboolean _insert_markdown_image(dt_lib_textnotes_t *d, GtkTextBuffer *buffer,
1123 const char *url, const char *fallback_url,
1124 const char *base_dir)
1125{
1126 const char *remote_url = NULL;
1127 if(_is_remote_url(url)) remote_url = url;
1128 else if(_is_remote_url(fallback_url)) remote_url = fallback_url;
1129
1130 gchar *path = NULL;
1131 if(remote_url)
1132 {
1133 path = _remote_cache_path(remote_url);
1134#ifdef HAVE_HTTP_SERVER
1135 if(path && !g_file_test(path, G_FILE_TEST_EXISTS))
1136 _queue_remote_download(d->self, d, remote_url, path);
1137#endif
1138 }
1139 else
1140 {
1141 path = _resolve_image_path(url, base_dir);
1142 if(IS_NULL_PTR(path) && fallback_url && (IS_NULL_PTR(url) || g_strcmp0(url, fallback_url) != 0))
1143 path = _resolve_image_path(fallback_url, base_dir);
1144 }
1145 if(IS_NULL_PTR(path)) return FALSE;
1146
1147 if(!g_file_test(path, G_FILE_TEST_EXISTS))
1148 {
1149 dt_free(path);
1150 return FALSE;
1151 }
1152
1153 const int scale = _get_preview_scale(d);
1154 gboolean have_device = FALSE;
1155 int max_w = _compute_max_image_width(d, scale, &have_device);
1156 if(max_w <= 0)
1157 {
1158 dt_free(path);
1159 return FALSE;
1160 }
1161
1162 const int target_w = max_w * scale;
1163 GError *error = NULL;
1164 GdkPixbuf *pixbuf = _load_scaled_pixbuf(path, target_w, &error);
1165 if(IS_NULL_PTR(pixbuf))
1166 {
1167 if(error) g_clear_error(&error);
1168 dt_free(path);
1169 return FALSE;
1170 }
1171
1172 _colorcorrect_pixbuf(pixbuf);
1173 _insert_pixbuf_widget(d, buffer, pixbuf, max_w);
1174 g_object_unref(pixbuf);
1175
1176 dt_free(path);
1177 return TRUE;
1178}
1179
1180static GArray *_build_line_offsets(const char *text)
1181{
1182 GArray *offsets = g_array_new(FALSE, FALSE, sizeof(gsize));
1183 gsize off = 0;
1184 g_array_append_val(offsets, off);
1185 if(IS_NULL_PTR(text)) return offsets;
1186 for(const char *p = text; *p; p++, off++)
1187 {
1188 if(*p == '\n')
1189 {
1190 gsize next = off + 1;
1191 g_array_append_val(offsets, next);
1192 }
1193 }
1194 return offsets;
1195}
1196
1197static gchar *_normalize_markdown_images(const char *text)
1198{
1199 if(IS_NULL_PTR(text)) return g_strdup("");
1200
1201 GString *out = g_string_sized_new(strlen(text) + 16);
1202 const char *p = text;
1203 while(*p)
1204 {
1205 if(p[0] == '!' && p[1] == '[')
1206 {
1207 const char *alt_end = strchr(p + 2, ']');
1208 if(alt_end && alt_end[1] == '(')
1209 {
1210 const char *dest_start = alt_end + 2;
1211 const char *line_end = strchr(dest_start, '\n');
1212 if(IS_NULL_PTR(line_end)) line_end = dest_start + strlen(dest_start);
1213 const char *close_paren = memchr(dest_start, ')', (size_t)(line_end - dest_start));
1214 if(close_paren && close_paren > dest_start)
1215 {
1216 const char *s = dest_start;
1217 const char *e = close_paren;
1218 while(s < e && g_ascii_isspace(*s)) s++;
1219 while(e > s && g_ascii_isspace(*(e - 1))) e--;
1220
1221 gboolean has_space = FALSE;
1222 gboolean has_quote = FALSE;
1223 for(const char *q = s; q < e; q++)
1224 {
1225 if(g_ascii_isspace(*q)) has_space = TRUE;
1226 if(*q == '"' || *q == '\'') has_quote = TRUE;
1227 }
1228
1229 if(has_space && !has_quote && s < e && *s != '<')
1230 {
1231 g_string_append_len(out, p, (gsize)(dest_start - p));
1232 g_string_append_c(out, '<');
1233 g_string_append_len(out, s, (gsize)(e - s));
1234 g_string_append(out, ">)");
1235 p = close_paren + 1;
1236 continue;
1237 }
1238 }
1239 }
1240 }
1241
1242 g_string_append_c(out, *p);
1243 p++;
1244 }
1245
1246 return g_string_free(out, FALSE);
1247}
1248
1249static gchar *_extract_image_dest_from_source(const char *text, const GArray *offsets, cmark_node *node)
1250{
1251 if(IS_NULL_PTR(text) || !offsets || offsets->len == 0) return NULL;
1252 const int sl = cmark_node_get_start_line(node);
1253 const int sc = cmark_node_get_start_column(node);
1254 if(sl <= 0 || sc <= 0 || sl > (int)offsets->len) return NULL;
1255
1256 const gsize line_start = g_array_index(offsets, gsize, sl - 1);
1257 const gsize line_end = (sl < (int)offsets->len)
1258 ? g_array_index(offsets, gsize, sl) - 1
1259 : strlen(text);
1260 if(line_start >= line_end) return NULL;
1261
1262 gsize start = line_start + (gsize)(sc - 1);
1263 if(start >= line_end) start = line_start;
1264
1265 const char *line = text + line_start;
1266 const gsize line_len = line_end - line_start;
1267 const char *p = line + (start - line_start);
1268 const char *line_endp = line + line_len;
1269
1270 const char *open_paren = NULL;
1271 for(const char *q = p; q < line_endp; q++)
1272 {
1273 if(*q == '(')
1274 {
1275 open_paren = q;
1276 break;
1277 }
1278 }
1279 if(IS_NULL_PTR(open_paren) || open_paren + 1 >= line_endp) return NULL;
1280
1281 const char *dest_start = open_paren + 1;
1282 const char *dest_end = NULL;
1283
1284 if(*dest_start == '<')
1285 {
1286 const char *close = strchr(dest_start + 1, '>');
1287 if(close && close < line_endp) dest_end = close;
1288 }
1289 else
1290 {
1291 for(const char *q = line_endp - 1; q > dest_start; q--)
1292 {
1293 if(*q == ')')
1294 {
1295 dest_end = q;
1296 break;
1297 }
1298 }
1299 }
1300
1301 if(IS_NULL_PTR(dest_end) || dest_end <= dest_start) return NULL;
1302
1303 gchar *raw = g_strndup(dest_start, dest_end - dest_start);
1304 if(IS_NULL_PTR(raw)) return NULL;
1305
1306 gchar *trimmed = g_strstrip(raw);
1307 if(trimmed[0] == '<' && trimmed[strlen(trimmed) - 1] == '>')
1308 {
1309 trimmed[strlen(trimmed) - 1] = '\0';
1310 trimmed++;
1311 }
1312
1313 GString *out = g_string_new(NULL);
1314 for(const char *q = trimmed; *q; q++)
1315 {
1316 if(*q == '\\' && q[1] != '\0')
1317 {
1318 q++;
1319 g_string_append_c(out, *q);
1320 }
1321 else
1322 {
1323 g_string_append_c(out, *q);
1324 }
1325 }
1326
1327 gchar *result = g_string_free(out, FALSE);
1328 dt_free(raw);
1329 return result;
1330}
1331#endif
1332
1333static void _render_preview(dt_lib_textnotes_t *d, const char *text)
1334{
1335 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->preview_view)) return;
1336 if(!dt_lib_gui_get_expanded(d->self)) return;
1337 d->rendering = TRUE;
1338 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->preview_view);
1339 gtk_text_buffer_set_text(buffer, "", -1);
1340
1341#ifdef HAVE_CMARK
1342 _clear_tag_table(buffer);
1343 dt_textnotes_tags_t tags = _create_preview_tags(buffer);
1344 GPtrArray *active_tags = g_ptr_array_new();
1345
1346 const char *source_text = text ? text : "";
1347 gchar *expanded = _expand_text_for_preview(d, source_text);
1348 const char *render_text = expanded ? expanded : source_text;
1349
1350 gchar *normalized = _normalize_markdown_images(render_text);
1351 cmark_node *doc = cmark_parse_document(normalized,
1352 strlen(normalized),
1353 CMARK_OPT_DEFAULT | CMARK_OPT_SOURCEPOS);
1354 if(IS_NULL_PTR(doc))
1355 {
1356 g_ptr_array_free(active_tags, TRUE);
1357 dt_free(normalized);
1358 dt_free(expanded);
1359 d->rendering = FALSE;
1360 return;
1361 }
1362
1363 cmark_iter *it = cmark_iter_new(doc);
1364 GArray *list_stack = g_array_new(FALSE, FALSE, sizeof(dt_textnotes_list_state_t));
1365 GArray *image_stack = g_array_new(FALSE, FALSE, sizeof(dt_textnotes_image_state_t));
1366 gboolean in_list_item = FALSE;
1367 gboolean item_pending_prefix = FALSE;
1368
1369 GArray *line_offsets = _build_line_offsets(render_text);
1370 gchar *base_dir = _get_image_base_dir(d);
1371
1372 for(cmark_event_type ev = cmark_iter_next(it); ev != CMARK_EVENT_DONE; ev = cmark_iter_next(it))
1373 {
1374 cmark_node *node = cmark_iter_get_node(it);
1375 const cmark_node_type t = cmark_node_get_type(node);
1376 const gboolean entering = (ev == CMARK_EVENT_ENTER);
1377
1378 switch(t)
1379 {
1380 case CMARK_NODE_PARAGRAPH:
1381 if(!entering)
1382 {
1383 if(in_list_item) _buffer_append_newline(buffer);
1384 else _buffer_append_blankline(buffer);
1385 }
1386 break;
1387 case CMARK_NODE_TEXT:
1388 if(entering)
1389 {
1390 const char *lit = cmark_node_get_literal(node);
1391 if(IS_NULL_PTR(lit)) break;
1392 if(image_stack->len > 0)
1393 {
1394 dt_textnotes_image_state_t *st =
1395 &g_array_index(image_stack, dt_textnotes_image_state_t, image_stack->len - 1);
1396 if(st->suppress_text) break;
1397 }
1398 lit = _handle_list_text_prefix(buffer, list_stack, &item_pending_prefix,
1399 lit, cmark_node_get_start_line(node));
1400 _insert_with_tags(buffer, lit, active_tags);
1401 }
1402 break;
1403 case CMARK_NODE_SOFTBREAK:
1404 case CMARK_NODE_LINEBREAK:
1405 if(entering)
1406 {
1407 GtkTextIter it_end;
1408 gtk_text_buffer_get_end_iter(buffer, &it_end);
1409 gtk_text_buffer_insert(buffer, &it_end, "\n", 1);
1410 }
1411 break;
1412 case CMARK_NODE_EMPH:
1413 if(entering)
1414 g_ptr_array_add(active_tags, tags.italic);
1415 else
1416 _pop_active_tag(active_tags);
1417 break;
1418 case CMARK_NODE_STRONG:
1419 if(entering)
1420 g_ptr_array_add(active_tags, tags.bold);
1421 else
1422 _pop_active_tag(active_tags);
1423 break;
1424 case CMARK_NODE_CODE:
1425 if(entering)
1426 {
1427 _emit_pending_list_prefix(buffer, list_stack, &item_pending_prefix);
1428 _insert_mono_text(buffer, tags.mono, cmark_node_get_literal(node));
1429 }
1430 break;
1431 case CMARK_NODE_CODE_BLOCK:
1432 if(entering)
1433 {
1434 _buffer_append_blankline(buffer);
1435 _emit_pending_list_prefix(buffer, list_stack, &item_pending_prefix);
1436 _insert_mono_text(buffer, tags.mono, cmark_node_get_literal(node));
1437 _buffer_append_blankline(buffer);
1438 }
1439 break;
1440 case CMARK_NODE_HEADING:
1441 if(entering)
1442 {
1443 GtkTextTag *tag = tags.h3;
1444 const int level = cmark_node_get_heading_level(node);
1445 if(level <= 1) tag = tags.h1;
1446 else if(level == 2) tag = tags.h2;
1447 g_ptr_array_add(active_tags, tag);
1448 }
1449 else
1450 {
1451 _pop_active_tag(active_tags);
1452 _buffer_append_blankline(buffer);
1453 }
1454 break;
1455 case CMARK_NODE_LINK:
1456 if(entering)
1457 _push_link_tag(buffer, active_tags, cmark_node_get_url(node));
1458 else
1459 _pop_active_tag(active_tags);
1460 break;
1461 case CMARK_NODE_IMAGE:
1462 if(entering)
1463 {
1464 _emit_pending_list_prefix(buffer, list_stack, &item_pending_prefix);
1465 const char *url = cmark_node_get_url(node);
1466 gchar *fallback = _extract_image_dest_from_source(render_text, line_offsets, node);
1467 const gboolean inlined = _insert_markdown_image(d, buffer, url, fallback, base_dir);
1468 dt_free(fallback);
1469 dt_textnotes_image_state_t st = { .suppress_text = inlined, .tag_added = FALSE };
1470 if(!inlined)
1471 {
1472 _push_link_tag(buffer, active_tags, url);
1473 st.tag_added = TRUE;
1474 }
1475 g_array_append_val(image_stack, st);
1476 }
1477 else if(image_stack->len > 0)
1478 {
1479 dt_textnotes_image_state_t st =
1480 g_array_index(image_stack, dt_textnotes_image_state_t, image_stack->len - 1);
1481 if(st.tag_added) _pop_active_tag(active_tags);
1482 g_array_remove_index(image_stack, image_stack->len - 1);
1483 }
1484 break;
1485 case CMARK_NODE_LIST:
1486 if(entering)
1487 _list_push(list_stack, node);
1488 else
1489 _list_pop(buffer, list_stack);
1490 break;
1491 case CMARK_NODE_ITEM:
1492 if(entering)
1493 _list_item_enter(buffer, &in_list_item, &item_pending_prefix);
1494 else
1495 _list_item_leave(buffer, &in_list_item, &item_pending_prefix);
1496 break;
1497 default:
1498 break;
1499 }
1500 }
1501
1502 g_array_free(list_stack, TRUE);
1503 g_array_free(image_stack, TRUE);
1504 g_ptr_array_free(active_tags, TRUE);
1505 cmark_iter_free(it);
1506 cmark_node_free(doc);
1507 g_array_free(line_offsets, TRUE);
1508 dt_free(base_dir);
1509 dt_free(normalized);
1510 dt_free(expanded);
1511#else
1512 const char *source_text = text ? text : "";
1513 gchar *expanded = _expand_text_for_preview(d, source_text);
1514 gtk_text_buffer_set_text(buffer, expanded ? expanded : source_text, -1);
1515 dt_free(expanded);
1516#endif
1517 d->rendering = FALSE;
1518}
1519
1521{
1522 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->mtime_label)) return;
1523 gtk_label_set_text(GTK_LABEL(d->mtime_label), "");
1524 gtk_widget_set_visible(d->mtime_label, FALSE);
1525}
1526
1528{
1530 if(IS_NULL_PTR(d->mtime_label)) return;
1531 if(!dt_lib_gui_get_expanded(self)) return;
1532 if(IS_NULL_PTR(d->path) || !_image_has_txt_flag(d->imgid))
1533 {
1535 return;
1536 }
1537
1538 GStatBuf statbuf;
1539 if(g_stat(d->path, &statbuf) != 0)
1540 {
1542 return;
1543 }
1544
1545 GDateTime *gdt = g_date_time_new_from_unix_local((gint64)statbuf.st_mtime);
1546 char local[128] = { 0 };
1547 if(gdt && dt_datetime_gdatetime_to_local(local, sizeof(local), gdt, FALSE, FALSE))
1548 {
1549 gchar *text = g_strdup_printf(_("Last modified: %s"), local);
1550 gchar *markup = g_markup_printf_escaped("<i>%s</i>", text);
1551 gtk_label_set_markup(GTK_LABEL(d->mtime_label), markup);
1552 gtk_widget_set_visible(d->mtime_label, TRUE);
1553 dt_free(markup);
1554 dt_free(text);
1555 }
1556 else
1557 {
1559 }
1560
1561 if(gdt) g_date_time_unref(gdt);
1562}
1563
1564static void _toggle_checklist_at_line(dt_lib_module_t *self, const int line_no)
1565{
1566 if(line_no < 1) return;
1567
1569 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->edit_view);
1570 GtkTextIter line_start, line_end;
1571 gtk_text_buffer_get_iter_at_line(buffer, &line_start, line_no - 1);
1572 line_end = line_start;
1573 gtk_text_iter_forward_to_line_end(&line_end);
1574
1575 GtkTextIter s_space, e_space, s_x, e_x, s_X, e_X;
1576 gboolean f_space = gtk_text_iter_forward_search(&line_start, "[ ]", 0, &s_space, &e_space, &line_end);
1577 gboolean f_x = gtk_text_iter_forward_search(&line_start, "[x]", 0, &s_x, &e_x, &line_end);
1578 gboolean f_X = gtk_text_iter_forward_search(&line_start, "[X]", 0, &s_X, &e_X, &line_end);
1579
1580 if(!f_space && !f_x && !f_X) return;
1581
1582 GtkTextIter *s = NULL;
1583 GtkTextIter *e = NULL;
1584 gboolean checked = FALSE;
1585
1586 if(f_space)
1587 {
1588 s = &s_space; e = &e_space; checked = FALSE;
1589 }
1590 if(f_x && (!s || gtk_text_iter_get_offset(&s_x) < gtk_text_iter_get_offset(s)))
1591 {
1592 s = &s_x; e = &e_x; checked = TRUE;
1593 }
1594 if(f_X && (!s || gtk_text_iter_get_offset(&s_X) < gtk_text_iter_get_offset(s)))
1595 {
1596 s = &s_X; e = &e_X; checked = TRUE;
1597 }
1598
1599 if(!s || !e) return;
1600
1601 gtk_text_buffer_begin_user_action(buffer);
1602 gtk_text_buffer_delete(buffer, s, e);
1603 gtk_text_buffer_insert(buffer, s, checked ? "[ ]" : "[x]", -1);
1604 gtk_text_buffer_end_user_action(buffer);
1605
1606 GtkTextBuffer *edit_buffer = gtk_text_view_get_buffer(d->edit_view);
1607 gchar *text = _get_buffer_text(edit_buffer);
1608 if(d->mode_toggle && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->mode_toggle)))
1609 {
1610 _toggle_mode(GTK_TOGGLE_BUTTON(d->mode_toggle), self);
1611 }
1612 else
1613 {
1614 _render_preview(d, text);
1615 }
1616 dt_free(text);
1617}
1618
1619static gboolean _preview_button_press(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
1620{
1621 if(event->type != GDK_BUTTON_PRESS || event->button != 1) return FALSE;
1622
1623 GtkTextView *view = GTK_TEXT_VIEW(widget);
1624 gint bx = 0, by = 0;
1625 gtk_text_view_window_to_buffer_coords(view, GTK_TEXT_WINDOW_TEXT,
1626 (gint)event->x, (gint)event->y, &bx, &by);
1627 GtkTextIter iter;
1628 gtk_text_view_get_iter_at_location(view, &iter, bx, by);
1629
1630 GSList *tags = gtk_text_iter_get_tags(&iter);
1631 for(GSList *t = tags; t; t = g_slist_next(t))
1632 {
1633 GtkTextTag *tag = t->data;
1634 gpointer linep = g_object_get_data(G_OBJECT(tag), "checklist_line");
1635 if(linep)
1636 {
1637 _toggle_checklist_at_line(self, GPOINTER_TO_INT(linep));
1638 g_slist_free(tags);
1639 tags = NULL;
1640 return TRUE;
1641 }
1642 }
1643
1644 for(GSList *t = tags; t; t = g_slist_next(t))
1645 {
1646 GtkTextTag *tag = t->data;
1647 const char *href = g_object_get_data(G_OBJECT(tag), "href");
1648 if(href && *href)
1649 {
1650 _open_uri(href);
1651 g_slist_free(tags);
1652 tags = NULL;
1653 return TRUE;
1654 }
1655 }
1656
1657 g_slist_free(tags);
1658 tags = NULL;
1659
1660 GtkTextIter line_start = iter;
1661 gtk_text_iter_set_line_offset(&line_start, 0);
1662 GtkTextIter line_end = line_start;
1663 gtk_text_iter_forward_to_line_end(&line_end);
1664
1665 GtkTextIter scan = line_start;
1666 while(TRUE)
1667 {
1668 GSList *ltags = gtk_text_iter_get_tags(&scan);
1669 for(GSList *t = ltags; t; t = g_slist_next(t))
1670 {
1671 GtkTextTag *tag = t->data;
1672 gpointer linep = g_object_get_data(G_OBJECT(tag), "checklist_line");
1673 if(linep)
1674 {
1675 _toggle_checklist_at_line(self, GPOINTER_TO_INT(linep));
1676 g_slist_free(ltags);
1677 ltags = NULL;
1678 return TRUE;
1679 }
1680 }
1681 g_slist_free(ltags);
1682 ltags = NULL;
1683 if(gtk_text_iter_compare(&scan, &line_end) >= 0) break;
1684 if(!gtk_text_iter_forward_char(&scan)) break;
1685 }
1686
1687 return FALSE;
1688}
1689
1690static gboolean _refresh_preview_idle(gpointer user_data)
1691{
1692 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1693 if(IS_NULL_PTR(self)) return G_SOURCE_REMOVE;
1695 if(IS_NULL_PTR(d) || !d->edit_view) return G_SOURCE_REMOVE;
1696 if(d->mode_toggle && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->mode_toggle)))
1697 return G_SOURCE_REMOVE;
1698
1700 return G_SOURCE_REMOVE;
1701}
1702
1703static void _preview_map(GtkWidget *widget, dt_lib_module_t *self)
1704{
1706 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->preview_view) || !d->edit_view) return;
1707 if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->mode_toggle))) return;
1708
1710 (void)widget;
1711}
1712
1713static void _edit_map(GtkWidget *widget, dt_lib_module_t *self)
1714{
1716}
1717
1718static gboolean _initial_load_idle(gpointer user_data)
1719{
1720 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1722 if(IS_NULL_PTR(d)) return G_SOURCE_REMOVE;
1723 if(d->imgid > 0) return G_SOURCE_REMOVE;
1725 return G_SOURCE_REMOVE;
1726}
1727
1728static void _ensure_has_txt_flag(const int32_t imgid)
1729{
1730 if(imgid <= 0) return;
1731
1732 dt_image_t *img = dt_image_cache_get(imgid, 'w');
1733 if(IS_NULL_PTR(img)) return;
1734
1735 if(!(img->flags & DT_IMAGE_HAS_TXT))
1736 img->flags |= DT_IMAGE_HAS_TXT;
1737
1739}
1740
1741static gboolean _image_has_txt_flag(const int32_t imgid)
1742{
1743 if(imgid <= 0) return FALSE;
1744
1745 dt_image_t *img = dt_image_cache_get(imgid, 'r');
1746 if(IS_NULL_PTR(img)) return FALSE;
1747 const gboolean has_txt = (img->flags & DT_IMAGE_HAS_TXT);
1749 return has_txt;
1750}
1751
1753{
1754 if(IS_NULL_PTR(d) || IS_NULL_PTR(d->vars_params)) return;
1755 dt_variables_params_destroy(d->vars_params);
1756 d->vars_params = NULL;
1757}
1758
1759static gboolean _set_image_paths(dt_lib_textnotes_t *d, const int32_t imgid)
1760{
1761 if(IS_NULL_PTR(d)) return FALSE;
1762
1763 if(imgid <= 0) return FALSE;
1764 if(d->image_path && d->image_dir) return TRUE;
1765
1766 dt_free(d->image_path);
1767 dt_free(d->image_dir);
1768 d->image_path = NULL;
1769 d->image_dir = NULL;
1770
1771 gboolean from_cache = FALSE;
1772 char image_path[DT_PATH_MAX] = { 0 };
1773 dt_image_full_path(imgid, image_path, sizeof(image_path), &from_cache, __FUNCTION__);
1774 if(image_path[0] == '\0' || !g_file_test(image_path, G_FILE_TEST_EXISTS))
1775 {
1776 from_cache = TRUE;
1777 dt_image_full_path(imgid, image_path, sizeof(image_path), &from_cache, __FUNCTION__);
1778 }
1779
1780 if(image_path[0] == '\0') return FALSE;
1781
1782 d->image_path = g_strdup(image_path);
1783 d->image_dir = g_path_get_dirname(image_path);
1784 return (d->image_path && d->image_dir);
1785}
1786
1787static char *_text_sidecar_save_path(dt_lib_textnotes_t *d, const int32_t imgid)
1788{
1789 if(IS_NULL_PTR(d) || imgid <= 0) return NULL;
1790 if(!_set_image_paths(d, imgid))
1791 return NULL;
1792 return dt_image_build_text_path_from_path(d->image_path);
1793}
1794
1796{
1798 if(IS_NULL_PTR(params)) return 1;
1799
1800 if(params->path && g_file_get_contents(params->path, &params->text, NULL, NULL))
1801 params->loaded = TRUE;
1802
1803 if(IS_NULL_PTR(params->text)) params->text = g_strdup("");
1804 return 0;
1805}
1806
1807static void _textnotes_load_job_cleanup(void *data)
1808{
1810 if(IS_NULL_PTR(params)) return;
1811 dt_free(params->path);
1812 dt_free(params->text);
1813 dt_free(params);
1814}
1815
1817{
1818 if(state != DT_JOB_STATE_FINISHED) return;
1820 if(IS_NULL_PTR(params)) return;
1821
1823 result->self = params->self;
1824 result->token = params->token;
1825 result->text = params->text ? params->text : g_strdup("");
1826 result->loaded = params->loaded;
1827 params->text = NULL;
1828
1829 g_idle_add(_textnotes_load_finish_idle, result);
1830}
1831
1833{
1835 gchar *text = _get_edit_text(d);
1836
1837 _render_preview(d, text);
1838
1839 if(!d->dirty || IS_NULL_PTR(d->path) || d->imgid <= 0)
1840 goto done;
1841
1842 GError *error = NULL;
1843 if(!g_file_set_contents(d->path, text, -1, &error))
1844 {
1845 gchar *item = g_strdup_printf("%s: %s", d->path, error->message);
1846 dt_control_alert(_("Could not save"), _("failed to save text notes"), item);
1847 dt_free(item);
1848 g_clear_error(&error);
1849 goto done;
1850 }
1851
1852 _ensure_has_txt_flag(d->imgid);
1853 d->dirty = FALSE;
1854
1855done:
1856 _update_mtime_label(self);
1857 dt_free(text);
1858}
1859
1860static gboolean _save_timeout_cb(gpointer user_data)
1861{
1862 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1864 d->save_timeout_id = 0;
1865 _save_and_render(self);
1866 return G_SOURCE_REMOVE;
1867}
1868
1869static void _save_now(dt_lib_module_t *self)
1870{
1872 if(d->save_timeout_id)
1873 {
1874 g_source_remove(d->save_timeout_id);
1875 d->save_timeout_id = 0;
1876 }
1877 _save_and_render(self);
1878}
1879
1880static void _textbuffer_changed(GtkTextBuffer *buffer, dt_lib_module_t *self)
1881{
1883 if(d->loading) return;
1884 d->dirty = TRUE;
1885
1886 if(d->save_timeout_id)
1887 {
1888 g_source_remove(d->save_timeout_id);
1889 d->save_timeout_id = 0;
1890 }
1891
1892 d->save_timeout_id = g_timeout_add(750, _save_timeout_cb, self);
1893
1894 _completion_update(self);
1895}
1896
1897static gboolean _textview_focus_out(GtkWidget *widget, GdkEventFocus *event, dt_lib_module_t *self)
1898{
1900 (void)d;
1901 g_idle_add(_completion_focus_out_idle, self);
1902 return FALSE;
1903}
1904
1905static void _toggle_mode(GtkToggleButton *button, dt_lib_module_t *self)
1906{
1908 const gboolean preview = gtk_toggle_button_get_active(button);
1909 gtk_stack_set_visible_child_name(GTK_STACK(d->stack), preview ? "preview" : "edit");
1910 gtk_button_set_label(GTK_BUTTON(d->mode_toggle), preview ? _("Edit") : _("Preview"));
1911
1912 if(preview)
1913 {
1915 gchar *text = _get_edit_text(d);
1916 _render_preview(d, text);
1917 dt_free(text);
1918 }
1919}
1920
1921static gboolean _textnotes_load_finish_idle(gpointer user_data)
1922{
1924 if(IS_NULL_PTR(result)) return G_SOURCE_REMOVE;
1925
1926 dt_lib_module_t *self = result->self;
1927 if(IS_NULL_PTR(self) || IS_NULL_PTR(self->data)) goto cleanup;
1929
1930 if(result->token != d->load_token) goto cleanup;
1931
1932 _set_edit_text(d, result->text);
1933 d->dirty = FALSE;
1934
1935 if(result->loaded) _ensure_has_txt_flag(d->imgid);
1936
1937 if(d->mode_toggle && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->mode_toggle)))
1938 _toggle_mode(GTK_TOGGLE_BUTTON(d->mode_toggle), self);
1939 else
1940 _render_preview(d, result->text);
1941
1942 gtk_widget_set_sensitive(GTK_WIDGET(d->edit_view), TRUE);
1943 gtk_widget_set_sensitive(d->mode_toggle, TRUE);
1944 if(result->loaded)
1945 _update_mtime_label(self);
1946 else
1948
1949cleanup:
1950 dt_free(result->text);
1951 dt_free(result);
1952 return G_SOURCE_REMOVE;
1953}
1954
1955static void _load_for_image(dt_lib_module_t *self, const int32_t imgid)
1956{
1958
1959 if(d->save_timeout_id)
1960 {
1961 g_source_remove(d->save_timeout_id);
1962 d->save_timeout_id = 0;
1963 }
1964
1965 const int32_t old_imgid = d->imgid;
1966 const gboolean changed = (old_imgid != imgid);
1967 d->imgid = imgid;
1968 dt_free(d->path);
1969 if(changed) _clear_variables_cache(d);
1970 if(changed)
1971 g_clear_pointer(&d->image_path, g_free);
1972 if(changed)
1973 g_clear_pointer(&d->image_dir, g_free);
1974
1975 const gboolean has_img = (imgid > 0);
1976 gtk_widget_set_sensitive(GTK_WIDGET(d->edit_view), has_img);
1977 gtk_widget_set_sensitive(d->mode_toggle, has_img);
1978 if(has_img) d->path = _text_sidecar_save_path(d, imgid);
1979
1980 _set_edit_text(d, "");
1981 d->dirty = FALSE;
1982
1983 if(d->preview_view)
1984 {
1985 GtkTextBuffer *buffer = gtk_text_view_get_buffer(d->preview_view);
1986 gtk_text_buffer_set_text(buffer, "", -1);
1987 }
1989
1990 if(!has_img) return;
1991
1992 const gboolean has_text = _image_has_txt_flag(imgid);
1993 if(!has_text || IS_NULL_PTR(d->path)) return;
1994
1995 gtk_widget_set_sensitive(GTK_WIDGET(d->edit_view), FALSE);
1996 gtk_widget_set_sensitive(d->mode_toggle, FALSE);
1997
1998 d->load_token++;
1999 dt_job_t *job = dt_control_job_create(&_textnotes_load_job_run, "textnotes load %d", imgid);
2000 if(IS_NULL_PTR(job))
2001 {
2002 gtk_widget_set_sensitive(GTK_WIDGET(d->edit_view), TRUE);
2003 gtk_widget_set_sensitive(d->mode_toggle, TRUE);
2004 return;
2005 }
2006
2008 params->self = self;
2009 params->token = d->load_token;
2010 params->path = g_strdup(d->path);
2014}
2015
2016static void _image_changed_callback(gpointer instance, gpointer user_data)
2017{
2018 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
2020}
2021
2022static void _mouse_over_image_callback(gpointer instance, gpointer user_data)
2023{
2024 dt_lib_module_t *self = (dt_lib_module_t *)user_data;
2026}
2027
2029{
2031 if(IS_NULL_PTR(d)) return;
2032
2033 int32_t img_id = dt_control_get_mouse_over_id();
2034 if(img_id > -1)
2035 ;
2036 else if(dt_act_on_get_first_image() > -1)
2037 img_id = dt_act_on_get_first_image();
2038
2039 if(img_id == d->imgid) return; // nothing to update, spare the SQL queries
2040 if(!d->loading) _save_now(self);
2041 if(!dt_lib_gui_get_expanded(self)) return;
2042
2043 _load_for_image(self, img_id);
2044}
2045
2047{
2049 self->data = (void *)d;
2050 d->self = self;
2051
2052 d->imgid = -1;
2053 d->height_setting = g_strdup("plugins/darkroom/textnotes/text_height");
2054
2055 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2056 self->widget = vbox;
2057 d->root = vbox;
2058
2059 GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2060 gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
2061
2062 d->mode_toggle = gtk_toggle_button_new_with_label(_("preview"));
2063 gtk_widget_set_tooltip_text(d->mode_toggle, _("toggle Markdown preview"));
2064 gtk_box_pack_end(GTK_BOX(toolbar), d->mode_toggle, FALSE, FALSE, 0);
2065 g_signal_connect(G_OBJECT(d->mode_toggle), "toggled", G_CALLBACK(_toggle_mode), self);
2066
2067 d->mtime_label = gtk_label_new("");
2068 gtk_label_set_xalign(GTK_LABEL(d->mtime_label), 0.0f);
2069 gtk_widget_set_halign(d->mtime_label, GTK_ALIGN_START);
2070 gtk_widget_set_visible(d->mtime_label, FALSE);
2071 gtk_box_pack_start(GTK_BOX(toolbar), d->mtime_label, TRUE, TRUE, 0);
2072
2073 d->stack = gtk_stack_new();
2074 gtk_stack_set_transition_type(GTK_STACK(d->stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE);
2075 gtk_box_pack_start(GTK_BOX(vbox), d->stack, TRUE, TRUE, 0);
2076
2077 GtkWidget *textview = gtk_text_view_new();
2079 dt_gui_textview_set_padding(GTK_TEXT_VIEW(textview));
2080 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR);
2081 gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(textview), FALSE);
2082 gtk_widget_set_hexpand(textview, TRUE);
2083
2084 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
2085 g_signal_connect(buffer, "changed", G_CALLBACK(_textbuffer_changed), self);
2086 g_signal_connect(textview, "focus-out-event", G_CALLBACK(_textview_focus_out), self);
2087 g_signal_connect(textview, "key-press-event", G_CALLBACK(_edit_key_press), self);
2088 g_signal_connect(textview, "key-release-event", G_CALLBACK(_edit_key_release), self);
2089 g_signal_connect(textview, "button-release-event", G_CALLBACK(_edit_button_release), self);
2090 g_signal_connect(textview, "map", G_CALLBACK(_edit_map), self);
2091
2092 d->edit_view = GTK_TEXT_VIEW(textview);
2093 _setup_completion(self, textview);
2094
2095 GtkWidget *edit_sw = dt_ui_scroll_wrap(textview, 140, d->height_setting, DT_UI_RESIZE_STATIC);
2096 GtkWidget *edit_inner = dt_ui_scroll_wrap_get_scrolled_window(edit_sw);
2097 gtk_widget_set_hexpand(edit_sw, TRUE);
2098 gtk_widget_set_vexpand(edit_sw, TRUE);
2099 gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(edit_inner), FALSE);
2100 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(edit_inner), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2101 gtk_stack_add_named(GTK_STACK(d->stack), edit_sw, "edit");
2102
2103 GtkWidget *preview_view = gtk_text_view_new();
2104 dt_gui_textview_set_padding(GTK_TEXT_VIEW(preview_view));
2105 gtk_text_view_set_editable(GTK_TEXT_VIEW(preview_view), FALSE);
2106 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(preview_view), FALSE);
2107 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(preview_view), GTK_WRAP_WORD_CHAR);
2108 gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(preview_view), FALSE);
2109 gtk_widget_set_hexpand(preview_view, TRUE);
2110 gtk_widget_add_events(preview_view, GDK_BUTTON_PRESS_MASK);
2111 g_signal_connect(G_OBJECT(preview_view), "button-press-event",
2112 G_CALLBACK(_preview_button_press), self);
2113 g_signal_connect(G_OBJECT(preview_view), "map", G_CALLBACK(_preview_map), self);
2114 gtk_widget_set_hexpand(preview_view, TRUE);
2115 gtk_widget_set_vexpand(preview_view, TRUE);
2116 d->preview_view = GTK_TEXT_VIEW(preview_view);
2117
2118 GtkWidget *preview_sw = dt_ui_scroll_wrap(preview_view, 140, d->height_setting, DT_UI_RESIZE_STATIC);
2119 GtkWidget *preview_inner = dt_ui_scroll_wrap_get_scrolled_window(preview_sw);
2120 d->preview_sw = preview_sw;
2121 gtk_widget_set_hexpand(preview_sw, TRUE);
2122 gtk_widget_set_vexpand(preview_sw, TRUE);
2123 gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(preview_inner), FALSE);
2124 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(preview_inner), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2125 // Rescale embedded images whenever the panel hands the preview a new width.
2126 g_signal_connect(G_OBJECT(preview_inner), "size-allocate", G_CALLBACK(_preview_width_changed), self);
2127 gtk_stack_add_named(GTK_STACK(d->stack), preview_sw, "preview");
2128 gtk_stack_set_visible_child_name(GTK_STACK(d->stack), "preview");
2129 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->mode_toggle), TRUE);
2130
2132 G_CALLBACK(_image_changed_callback), self);
2134 G_CALLBACK(_image_changed_callback), self);
2136 G_CALLBACK(_mouse_over_image_callback), self);
2137
2138 gtk_widget_show_all(self->widget);
2139
2141 g_idle_add(_initial_load_idle, self);
2142}
2143
2145{
2146 if(IS_NULL_PTR(self->data)) return;
2148
2151
2152 if(d->save_timeout_id)
2153 {
2154 g_source_remove(d->save_timeout_id);
2155 d->save_timeout_id = 0;
2156 }
2157
2158#ifdef HAVE_HTTP_SERVER
2159 if(d->download_inflight)
2160 {
2161 g_hash_table_destroy(d->download_inflight);
2162 d->download_inflight = NULL;
2163 }
2164#endif
2165
2166 if(d->completion_popover)
2167 {
2168 gtk_widget_destroy(d->completion_popover);
2169 d->completion_popover = NULL;
2170 }
2171 if(d->completion_model)
2172 {
2173 g_object_unref(d->completion_model);
2174 d->completion_model = NULL;
2175 }
2176
2177 dt_free(d->path);
2178 dt_free(d->image_path);
2179 dt_free(d->image_dir);
2181 dt_free(d->height_setting);
2182 dt_free(d);
2183 self->data = NULL;
2184}
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnect accels while a text or search entry has the focus, and reconnect them when it loses it....
int32_t dt_act_on_get_first_image()
Definition act_on.c:67
GtkWidget * dt_gui_main_window(void)
struct dt_ui_t * dt_gui_get_ui(void)
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:170
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
const int t
const float v
gboolean dt_colorprofiles_srgb_to_display_strided(uint8_t *const pixels, const int width, const int height, const int rowstride, const int n_channels, const gboolean has_alpha)
Convert a strided, packed-RGB(A) 8-bit buffer (GdkPixbuf shape) from sRGB to the display profile,...
The colour-profile module's API: which profiles exist, and how to apply one.
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const dt_colormatrix_t dt_aligned_pixel_t out
char * dt_image_build_text_path_from_path(const char *image_path)
void dt_image_full_path(const int32_t imgid, char *pathname, size_t pathname_len, gboolean *from_cache, const char *calling_func)
Get the full path of an image out of the database.
int32_t dt_control_get_mouse_over_id()
Definition control.c:1032
void dt_control_alert(const char *title, const char *message, const char *item)
Tell the user something that must not go unseen: what it says stays true after a toast would have fad...
Definition control.c:883
void dt_control_log(const char *msg,...)
Definition control.c:839
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:654
struct dt_gui_gtk_t * dt_gui_get_global(void)
Definition darktable.c:526
gboolean dt_datetime_gdatetime_to_local(char *local, const size_t local_size, GDateTime *gdt, const gboolean msec, const gboolean tz)
Definition datetime.c:146
GtkWidget * preview
what the selected row actually captures
GtkWidget * view
the tree view
const int res
Definition dtpthread.h:351
const char * dt_loc_cachedir(void)
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:118
const dt_gtkentry_completion_spec * dt_gtkentry_get_default_path_compl_list()
Definition gtkentry.c:200
G_BEGIN_DECLS struct completion_spec dt_gtkentry_completion_spec
@ COMPL_DESCRIPTION
Definition gtkentry.h:46
@ COMPL_VARNAME
Definition gtkentry.h:45
@ DT_IMAGE_HAS_TXT
Definition image.h:145
void dt_image_cache_write_release(dt_image_t *img, dt_image_cache_write_mode_t mode)
dt_image_t * dt_image_cache_get(const int32_t imgid, char mode)
void dt_image_cache_read_release(const dt_image_t *img)
@ DT_IMAGE_CACHE_SAFE
Definition image_cache.h:48
const char * model
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:138
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:419
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:132
void dt_control_job_set_state_callback(_dt_job_t *job, dt_job_state_change_callback cb)
Definition jobs.c:168
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:115
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:57
dt_job_state_t
Definition jobs.h:43
@ DT_JOB_STATE_FINISHED
Definition jobs.h:47
void dt_gui_textview_set_padding(GtkTextView *textview)
Apply the standard recessed-input text padding to a GtkTextView.
Definition label.c:149
hash
Definition layers.h:90
static const dt_iop_drawlayer_params_t * params
Definition layers.h:80
gboolean dt_lib_gui_get_expanded(dt_lib_module_t *module)
Definition lib.c:912
void dt_lib_presets_add(const char *name, const char *plugin_name, const int32_t version, const void *params, const int32_t params_size, gboolean readonly)
Definition lib.c:1163
#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:96
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
size_t size
Definition mipmap_cache.c:3
#define DT_MODULE(MODVER)
Instantiate the version handshake every module must export. Use once, at file scope,...
fetch(component, metrics)
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
const char * name
Definition pdf.h:90
#define ABS(n)
GtkWidget * dt_gui_get_popup_relative_widget(GtkWidget *widget, GdkRectangle *rect)
Resolve the widget to use as parent for a nested popup on Wayland.
Definition popup.c:25
GtkWidget * dt_ui_scroll_wrap_get_scrolled_window(GtkWidget *wrapper)
Return the inner scrolled window of a dt_ui_scroll_wrap() wrapper, or NULL.
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, const char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
@ DT_UI_RESIZE_STATIC
Definition scroll_wrap.h:47
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:411
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ DT_SIGNAL_DEVELOP_INITIALIZE
This signal is raised when darktable.develop is initialized.
Definition signal.h:173
@ DT_SIGNAL_DEVELOP_IMAGE_CHANGED
This signal is raised when image is changed in darkroom.
Definition signal.h:225
@ DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE
This signal is raised when mouse hovers over image thumbs both on lighttable and in the filmstrip....
Definition signal.h:63
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
char * dt_variables_expand(dt_variables_params_t *params, gchar *source, gboolean iterate)
void dt_variables_params_destroy(dt_variables_params_t *params)
void dt_variables_params_init(dt_variables_params_t **params)
const float uint32_t state[4]
unsigned __int64 uint64_t
Definition strptime.c:75
int32_t flags
Definition image.h:410
char plugin_name[128]
Definition lib.h:81
GModule *void * data
Definition lib.h:79
GtkWidget * widget
Definition lib.h:83
GtkWidget * root
Definition textnotes.c:61
dt_lib_module_t * self
Definition textnotes.c:60
GtkTextMark * completion_mark
Definition textnotes.c:71
gchar * height_setting
Definition textnotes.c:75
GtkWidget * completion_popover
Definition textnotes.c:68
GtkWidget * stack
Definition textnotes.c:62
GtkTextView * edit_view
Definition textnotes.c:63
GtkWidget * mtime_label
Definition textnotes.c:67
gboolean rendering
Definition textnotes.c:82
uint64_t load_token
Definition textnotes.c:78
GtkWidget * mode_toggle
Definition textnotes.c:66
GtkWidget * completion_tree
Definition textnotes.c:69
GtkListStore * completion_model
Definition textnotes.c:70
GtkTextView * preview_view
Definition textnotes.c:64
GtkWidget * preview_sw
Definition textnotes.c:65
dt_variables_params_t * vars_params
Definition textnotes.c:76
dt_lib_module_t * self
Definition textnotes.c:123
dt_lib_module_t * self
Definition textnotes.c:132
const gchar * filename
Definition variables.h:48
const gchar * jobcode
Definition variables.h:51
double x
double width
double y
static void _setup_completion(dt_lib_module_t *self, GtkWidget *textview)
Definition textnotes.c:515
static gboolean _textnotes_load_finish_idle(gpointer user_data)
Definition textnotes.c:1921
int set_params(dt_lib_module_t *self, const void *params, int size)
Definition textnotes.c:174
static void _preview_map(GtkWidget *widget, dt_lib_module_t *self)
Definition textnotes.c:1703
static gboolean _completion_apply_selected(dt_lib_module_t *self)
Definition textnotes.c:355
void * get_params(dt_lib_module_t *self, int *size)
Definition textnotes.c:161
static char * _text_sidecar_save_path(dt_lib_textnotes_t *d, const int32_t imgid)
Definition textnotes.c:1787
static void _completion_fill(dt_lib_textnotes_t *d, const char *prefix)
Definition textnotes.c:294
static void _save_and_render(dt_lib_module_t *self)
Definition textnotes.c:1832
static gboolean _edit_key_release(GtkWidget *widget, GdkEventKey *event, dt_lib_module_t *self)
Definition textnotes.c:490
static void _colorcorrect_pixbuf(GdkPixbuf *pixbuf)
Definition textnotes.c:544
static gboolean _completion_focus_out_idle(gpointer user_data)
Definition textnotes.c:445
void gui_cleanup(dt_lib_module_t *self)
Definition textnotes.c:2144
static void _render_preview(dt_lib_textnotes_t *d, const char *text)
Definition textnotes.c:1333
static void _image_changed_callback(gpointer instance, gpointer user_data)
Definition textnotes.c:2016
static void _update_mtime_label(dt_lib_module_t *self)
Definition textnotes.c:1527
static void _render_preview_from_edit(dt_lib_textnotes_t *d)
Definition textnotes.c:228
static void _clear_variables_cache(dt_lib_textnotes_t *d)
Definition textnotes.c:1752
static void _edit_map(GtkWidget *widget, dt_lib_module_t *self)
Definition textnotes.c:1713
static gboolean _set_image_paths(dt_lib_textnotes_t *d, const int32_t imgid)
Definition textnotes.c:1759
static int32_t _textnotes_load_job_run(dt_job_t *job)
Definition textnotes.c:1795
static gchar * _get_buffer_text(GtkTextBuffer *buffer)
Definition textnotes.c:138
static gboolean _textview_focus_out(GtkWidget *widget, GdkEventFocus *event, dt_lib_module_t *self)
Definition textnotes.c:1897
static void _toggle_checklist_at_line(dt_lib_module_t *self, const int line_no)
Definition textnotes.c:1564
static gchar * _expand_text_for_preview(dt_lib_textnotes_t *d, const char *source_text)
Definition textnotes.c:580
static void _textbuffer_changed(GtkTextBuffer *buffer, dt_lib_module_t *self)
Definition textnotes.c:1880
static void _completion_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, dt_lib_module_t *self)
Definition textnotes.c:506
static int _preview_text_window_width_px(dt_lib_textnotes_t *d)
Definition textnotes.c:217
static gboolean _completion_find_prefix(dt_lib_textnotes_t *d, GtkTextIter *cursor, GtkTextIter *start_iter, gchar **prefix_out)
Definition textnotes.c:310
void init_presets(dt_lib_module_t *self)
Definition textnotes.c:190
static void _completion_hide(dt_lib_textnotes_t *d)
Definition textnotes.c:257
static void _mouse_over_image_callback(gpointer instance, gpointer user_data)
Definition textnotes.c:2022
static void _save_now(dt_lib_module_t *self)
Definition textnotes.c:1869
uint32_t container(dt_lib_module_t *self)
Definition textnotes.c:100
static void _set_edit_text(dt_lib_textnotes_t *d, const char *text)
Definition textnotes.c:152
static void _completion_update(dt_lib_module_t *self)
Definition textnotes.c:384
static gchar * _get_edit_text(dt_lib_textnotes_t *d)
Definition textnotes.c:145
static void _ensure_has_txt_flag(const int32_t imgid)
Definition textnotes.c:1728
static void _load_for_image(dt_lib_module_t *self, const int32_t imgid)
Definition textnotes.c:1955
static void _textnotes_load_job_cleanup(void *data)
Definition textnotes.c:1807
static void _clear_mtime_label(dt_lib_textnotes_t *d)
Definition textnotes.c:1520
void gui_init(dt_lib_module_t *self)
Definition textnotes.c:2046
static gboolean _save_timeout_cb(gpointer user_data)
Definition textnotes.c:1860
int position()
Definition textnotes.c:105
static void _textnotes_load_job_state(dt_job_t *job, dt_job_state_t state)
Definition textnotes.c:1816
const char ** views(dt_lib_module_t *self)
Definition textnotes.c:94
static void _update_for_current_image(dt_lib_module_t *self)
Definition textnotes.c:2028
static gboolean _preview_button_press(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
Definition textnotes.c:1619
static gboolean _image_has_txt_flag(const int32_t imgid)
Definition textnotes.c:1741
static void _open_uri(const char *uri)
Definition textnotes.c:563
static gboolean _refresh_preview_idle(gpointer user_data)
Definition textnotes.c:1690
static gboolean _edit_key_press(GtkWidget *widget, GdkEventKey *event, dt_lib_module_t *self)
Definition textnotes.c:467
static void _toggle_mode(GtkToggleButton *button, dt_lib_module_t *self)
Definition textnotes.c:1905
static gboolean _completion_match(const char *item, const char *prefix)
Definition textnotes.c:270
static void _preview_width_changed(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data)
Re-render the preview when the panel-given width changes, so embedded images rescale to fit the avail...
Definition textnotes.c:245
static gboolean _edit_button_release(GtkWidget *widget, GdkEventButton *event, dt_lib_module_t *self)
Definition textnotes.c:498
static gboolean _initial_load_idle(gpointer user_data)
Definition textnotes.c:1718
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
@ DT_UI_CONTAINER_PANEL_LEFT_CENTER