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