From 4c25b2ebdeaf772357ab9464b24d3a1ede2a6162 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Tue, 25 Jan 2022 18:31:27 +0100 Subject: [PATCH] Add a simple EMarkdownEditor widget Related to https://gitlab.gnome.org/GNOME/evolution/-/issues/449 --- data/icons/CMakeLists.txt | 18 + ...or_actions_scalable_markdown-bold-dark.svg | 19 + ...hicolor_actions_scalable_markdown-bold.svg | 18 + ...actions_scalable_markdown-bullets-dark.svg | 31 + ...olor_actions_scalable_markdown-bullets.svg | 31 + ...or_actions_scalable_markdown-code-dark.svg | 28 + ...hicolor_actions_scalable_markdown-code.svg | 26 + ..._actions_scalable_markdown-header-dark.svg | 23 + ...color_actions_scalable_markdown-header.svg | 22 + ...or_actions_scalable_markdown-help-dark.svg | 34 + ...hicolor_actions_scalable_markdown-help.svg | 33 + ..._actions_scalable_markdown-italic-dark.svg | 16 + ...color_actions_scalable_markdown-italic.svg | 16 + ...or_actions_scalable_markdown-link-dark.svg | 30 + ...hicolor_actions_scalable_markdown-link.svg | 27 + ...actions_scalable_markdown-numbers-dark.svg | 40 ++ ...olor_actions_scalable_markdown-numbers.svg | 40 ++ ...r_actions_scalable_markdown-quote-dark.svg | 25 + ...icolor_actions_scalable_markdown-quote.svg | 25 + .../evolution-mail-composer-docs.sgml.in | 4 + .../evolution-util-docs.sgml.in | 5 + po/POTFILES.in | 1 + src/e-util/CMakeLists.txt | 5 + src/e-util/e-markdown-editor.c | 585 ++++++++++++++++++ src/e-util/e-markdown-editor.h | 57 ++ src/e-util/e-util.h | 1 + src/e-util/test-markdown-editor.c | 81 +++ 27 files changed, 1241 insertions(+) create mode 100644 data/icons/hicolor_actions_scalable_markdown-bold-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-bold.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-bullets-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-bullets.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-code-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-code.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-header-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-header.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-help-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-help.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-italic-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-italic.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-link-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-link.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-numbers-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-numbers.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-quote-dark.svg create mode 100644 data/icons/hicolor_actions_scalable_markdown-quote.svg create mode 100644 src/e-util/e-markdown-editor.c create mode 100644 src/e-util/e-markdown-editor.h create mode 100644 src/e-util/test-markdown-editor.c diff --git a/data/icons/CMakeLists.txt b/data/icons/CMakeLists.txt index 2d445c9695..d6cbfb2b3f 100644 --- a/data/icons/CMakeLists.txt +++ b/data/icons/CMakeLists.txt @@ -92,6 +92,24 @@ set(private_icons hicolor_actions_32x32_view-calendar-month.png hicolor_actions_32x32_view-calendar-week.png hicolor_actions_32x32_view-calendar-workweek.png + hicolor_actions_scalable_markdown-bold.svg + hicolor_actions_scalable_markdown-bold-dark.svg + hicolor_actions_scalable_markdown-bullets.svg + hicolor_actions_scalable_markdown-bullets-dark.svg + hicolor_actions_scalable_markdown-code.svg + hicolor_actions_scalable_markdown-code-dark.svg + hicolor_actions_scalable_markdown-header.svg + hicolor_actions_scalable_markdown-header-dark.svg + hicolor_actions_scalable_markdown-help.svg + hicolor_actions_scalable_markdown-help-dark.svg + hicolor_actions_scalable_markdown-italic.svg + hicolor_actions_scalable_markdown-italic-dark.svg + hicolor_actions_scalable_markdown-link.svg + hicolor_actions_scalable_markdown-link-dark.svg + hicolor_actions_scalable_markdown-numbers.svg + hicolor_actions_scalable_markdown-numbers-dark.svg + hicolor_actions_scalable_markdown-quote.svg + hicolor_actions_scalable_markdown-quote-dark.svg hicolor_actions_scalable_view-calendar-day.svg hicolor_actions_scalable_view-calendar-list.svg hicolor_actions_scalable_view-calendar-month.svg diff --git a/data/icons/hicolor_actions_scalable_markdown-bold-dark.svg b/data/icons/hicolor_actions_scalable_markdown-bold-dark.svg new file mode 100644 index 0000000000..e56fb34935 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-bold-dark.svg @@ -0,0 +1,19 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-bold.svg b/data/icons/hicolor_actions_scalable_markdown-bold.svg new file mode 100644 index 0000000000..39a46d8331 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-bold.svg @@ -0,0 +1,18 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-bullets-dark.svg b/data/icons/hicolor_actions_scalable_markdown-bullets-dark.svg new file mode 100644 index 0000000000..fa2bf472e6 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-bullets-dark.svg @@ -0,0 +1,31 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-bullets.svg b/data/icons/hicolor_actions_scalable_markdown-bullets.svg new file mode 100644 index 0000000000..fe13db36e8 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-bullets.svg @@ -0,0 +1,31 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-code-dark.svg b/data/icons/hicolor_actions_scalable_markdown-code-dark.svg new file mode 100644 index 0000000000..23b6dcf3ff --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-code-dark.svg @@ -0,0 +1,28 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-code.svg b/data/icons/hicolor_actions_scalable_markdown-code.svg new file mode 100644 index 0000000000..ad3aa41fd4 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-code.svg @@ -0,0 +1,26 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-header-dark.svg b/data/icons/hicolor_actions_scalable_markdown-header-dark.svg new file mode 100644 index 0000000000..d5dcf32db1 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-header-dark.svg @@ -0,0 +1,23 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-header.svg b/data/icons/hicolor_actions_scalable_markdown-header.svg new file mode 100644 index 0000000000..e509013d69 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-header.svg @@ -0,0 +1,22 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-help-dark.svg b/data/icons/hicolor_actions_scalable_markdown-help-dark.svg new file mode 100644 index 0000000000..fcd2864596 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-help-dark.svg @@ -0,0 +1,34 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-help.svg b/data/icons/hicolor_actions_scalable_markdown-help.svg new file mode 100644 index 0000000000..6be86d3402 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-help.svg @@ -0,0 +1,33 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-italic-dark.svg b/data/icons/hicolor_actions_scalable_markdown-italic-dark.svg new file mode 100644 index 0000000000..87e0445fae --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-italic-dark.svg @@ -0,0 +1,16 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-italic.svg b/data/icons/hicolor_actions_scalable_markdown-italic.svg new file mode 100644 index 0000000000..3b64797ad3 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-italic.svg @@ -0,0 +1,16 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-link-dark.svg b/data/icons/hicolor_actions_scalable_markdown-link-dark.svg new file mode 100644 index 0000000000..5cafddcdf8 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-link-dark.svg @@ -0,0 +1,30 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-link.svg b/data/icons/hicolor_actions_scalable_markdown-link.svg new file mode 100644 index 0000000000..e7bfbbbb9f --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-link.svg @@ -0,0 +1,27 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-numbers-dark.svg b/data/icons/hicolor_actions_scalable_markdown-numbers-dark.svg new file mode 100644 index 0000000000..f79f5f551d --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-numbers-dark.svg @@ -0,0 +1,40 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-numbers.svg b/data/icons/hicolor_actions_scalable_markdown-numbers.svg new file mode 100644 index 0000000000..7ca25fb951 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-numbers.svg @@ -0,0 +1,40 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-quote-dark.svg b/data/icons/hicolor_actions_scalable_markdown-quote-dark.svg new file mode 100644 index 0000000000..2b57fc1e60 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-quote-dark.svg @@ -0,0 +1,25 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_markdown-quote.svg b/data/icons/hicolor_actions_scalable_markdown-quote.svg new file mode 100644 index 0000000000..3b301881d3 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_markdown-quote.svg @@ -0,0 +1,25 @@ + + + + diff --git a/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in b/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in index ea4fca7cd8..82059231af 100644 --- a/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in +++ b/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in @@ -29,6 +29,10 @@ API Index + + Index of new symbols in 3.44 + + Index of new symbols in 3.42 diff --git a/docs/reference/evolution-util/evolution-util-docs.sgml.in b/docs/reference/evolution-util/evolution-util-docs.sgml.in index 779f3ae6c4..7f7b3bb5ab 100644 --- a/docs/reference/evolution-util/evolution-util-docs.sgml.in +++ b/docs/reference/evolution-util/evolution-util-docs.sgml.in @@ -280,6 +280,7 @@ + @@ -326,6 +327,10 @@ Index + + Index of new symbols in 3.44 + + Index of new symbols in 3.42 diff --git a/po/POTFILES.in b/po/POTFILES.in index 6faa645e63..b003bb2dad 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -237,6 +237,7 @@ src/e-util/e-mail-signature-editor.c src/e-util/e-mail-signature-manager.c src/e-util/e-mail-signature-script-dialog.c src/e-util/e-map.c +src/e-util/e-markdown-editor.c src/e-util/e-misc-utils.c src/e-util/e-name-selector-dialog.c src/e-util/e-name-selector-entry.c diff --git a/src/e-util/CMakeLists.txt b/src/e-util/CMakeLists.txt index 7048adf146..e81570ad3c 100644 --- a/src/e-util/CMakeLists.txt +++ b/src/e-util/CMakeLists.txt @@ -168,6 +168,7 @@ set(SOURCES e-mail-signature-script-dialog.c e-mail-signature-tree-view.c e-map.c + e-markdown-editor.c e-marshal.c e-menu-tool-action.c e-menu-tool-button.c @@ -443,6 +444,7 @@ set(HEADERS e-mail-signature-script-dialog.h e-mail-signature-tree-view.h e-map.h + e-markdown-editor.h e-menu-tool-action.h e-menu-tool-button.h e-misc-utils.h @@ -651,6 +653,7 @@ target_include_directories(evolution-util PUBLIC ${GEO_INCLUDE_DIRS} ${GNOME_PLATFORM_INCLUDE_DIRS} ${GSPELL_INCLUDE_DIRS} + ${MARKDOWN_INCLUDE_DIRS} ) target_link_libraries(evolution-util @@ -663,6 +666,7 @@ target_link_libraries(evolution-util ${GSPELL_LDFLAGS} ${ICONV_LIBS} ${MATH_LDFLAGS} + ${MARKDOWN_LDFLAGS} ) if(HAVE_LDAP) @@ -816,6 +820,7 @@ add_private_programs_simple( test-dateedit test-html-editor test-mail-signatures + test-markdown-editor test-name-selector test-preferences-window test-proxy-preferences diff --git a/src/e-util/e-markdown-editor.c b/src/e-util/e-markdown-editor.c new file mode 100644 index 0000000000..3325ea6395 --- /dev/null +++ b/src/e-util/e-markdown-editor.c @@ -0,0 +1,585 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#ifdef HAVE_MARKDOWN +#include +#endif + +#include +#include + +#include + +#include "e-misc-utils.h" +#include "e-spell-text-view.h" +#include "e-web-view.h" + +#include "e-markdown-editor.h" + +struct _EMarkdownEditorPrivate { + GtkTextView *text_view; + EWebView *web_view; + GtkToolbar *action_toolbar; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (EMarkdownEditor, e_markdown_editor, GTK_TYPE_BOX) + +static void +e_markdown_editor_get_selection (EMarkdownEditor *self, + GtkTextIter *out_start, + GtkTextIter *out_end, + gchar **out_selected_text) +{ + GtkTextBuffer *buffer; + GtkTextIter start, end; + + buffer = gtk_text_view_get_buffer (self->priv->text_view); + + if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end) && out_selected_text) { + *out_selected_text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); + } else if (out_selected_text) { + *out_selected_text = NULL; + } + + if (out_start) + *out_start = start; + + if (out_end) + *out_end = end; +} + +static void +e_markdown_editor_surround_selection (EMarkdownEditor *self, + gboolean whole_lines, + const gchar *prefix, + const gchar *suffix) +{ + GtkTextIter start, end; + GtkTextBuffer *buffer; + + e_markdown_editor_get_selection (self, &start, &end, NULL); + + buffer = gtk_text_view_get_buffer (self->priv->text_view); + + gtk_text_buffer_begin_user_action (buffer); + + if (whole_lines) { + gint to_line, ii; + + to_line = gtk_text_iter_get_line (&end); + + for (ii = gtk_text_iter_get_line (&start); ii <= to_line; ii++) { + GtkTextIter iter; + + gtk_text_buffer_get_iter_at_line (buffer, &iter, ii); + + if (prefix && *prefix) + gtk_text_buffer_insert (buffer, &iter, prefix, -1); + + if (suffix && *suffix) { + gtk_text_iter_forward_to_line_end (&iter); + gtk_text_buffer_insert (buffer, &iter, suffix, -1); + } + } + } else { + gint end_offset = gtk_text_iter_get_offset (&end); + + if (prefix && *prefix) { + gtk_text_buffer_insert (buffer, &start, prefix, -1); + /* Keep the cursor where it is, move it only when the suffix is used */ + end_offset += strlen (prefix); + gtk_text_buffer_get_iter_at_offset (buffer, &end, end_offset); + } + + if (suffix && *suffix) { + gtk_text_buffer_insert (buffer, &end, suffix, -1); + /* Place the cursor before the suffix */ + gtk_text_buffer_get_iter_at_offset (buffer, &end, end_offset); + gtk_text_buffer_select_range (buffer, &end, &end); + } + } + + gtk_text_buffer_end_user_action (buffer); +} + +static void +e_markdown_editor_add_bold_text_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, FALSE, "**", "**"); +} + +static void +e_markdown_editor_add_italic_text_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, FALSE, "*", "*"); +} + +static void +e_markdown_editor_insert_quote_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, TRUE, "> ", NULL); +} + +static void +e_markdown_editor_insert_code_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + GtkTextIter start, end; + gchar *selection = NULL; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_get_selection (self, &start, &end, &selection); + + if (selection && strchr (selection, '\n')) { + GtkTextBuffer *buffer; + GtkTextIter iter; + gint start_line, end_line; + + buffer = gtk_text_view_get_buffer (self->priv->text_view); + + gtk_text_buffer_begin_user_action (buffer); + + start_line = gtk_text_iter_get_line (&start); + end_line = gtk_text_iter_get_line (&end); + + gtk_text_buffer_get_iter_at_line (buffer, &iter, start_line); + gtk_text_buffer_insert (buffer, &iter, "```\n", -1); + + /* One line added above + 1 for the end line itself */ + end_line = end_line + 2; + gtk_text_buffer_get_iter_at_line (buffer, &iter, end_line); + if (gtk_text_iter_is_end (&iter) && gtk_text_iter_get_line_offset (&iter) > 0) { + gtk_text_buffer_insert (buffer, &iter, "\n```\n", -1); + } else { + if (gtk_text_iter_is_end (&iter)) + end_line--; + gtk_text_buffer_insert (buffer, &iter, "```\n", -1); + } + + /* Place the cursor before the suffix */ + gtk_text_buffer_get_iter_at_line (buffer, &iter, end_line); + gtk_text_buffer_select_range (buffer, &iter, &iter); + + gtk_text_buffer_end_user_action (buffer); + } else { + e_markdown_editor_surround_selection (self, FALSE, "`", "`"); + } + + g_free (selection); +} + +static void +e_markdown_editor_add_link_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + GtkTextBuffer *buffer; + GtkTextIter start, end; + gchar *selection = NULL; + gint offset; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_get_selection (self, &start, &end, &selection); + + buffer = gtk_text_view_get_buffer (self->priv->text_view); + offset = gtk_text_iter_get_offset (&start); + + gtk_text_buffer_begin_user_action (buffer); + + if (selection && *selection) { + gint end_offset = gtk_text_iter_get_offset (&end); + + if (g_ascii_strncasecmp (selection, "http:", 5) == 0 || + g_ascii_strncasecmp (selection, "https:", 6) == 0) { + gtk_text_buffer_insert (buffer, &start, "[](", -1); + gtk_text_buffer_get_iter_at_offset (buffer, &end, end_offset + 3); + gtk_text_buffer_insert (buffer, &end, ")", -1); + gtk_text_buffer_get_iter_at_offset (buffer, &start, offset + 1); + end = start; + } else { + gtk_text_buffer_insert (buffer, &start, "[", -1); + gtk_text_buffer_get_iter_at_offset (buffer, &end, end_offset + 1); + gtk_text_buffer_insert (buffer, &end, "](https://)", -1); + gtk_text_buffer_get_iter_at_offset (buffer, &start, end_offset + 1 + 2); + gtk_text_buffer_get_iter_at_offset (buffer, &end, end_offset + 1 + 10); + } + + gtk_text_buffer_select_range (buffer, &start, &end); + } else { + gtk_text_buffer_insert (buffer, &start, "[](https://)", -1); + + /* skip "[](" */ + offset += 3; + + gtk_text_buffer_get_iter_at_offset (buffer, &start, offset); + + /* after the "https://" text */ + gtk_text_buffer_get_iter_at_offset (buffer, &end, offset + 8); + + gtk_text_buffer_select_range (buffer, &start, &end); + } + + gtk_text_buffer_end_user_action (buffer); +} + +static void +e_markdown_editor_add_bullet_list_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, TRUE, "- ", NULL); +} + +static void +e_markdown_editor_add_numbered_list_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, TRUE, "1. ", NULL); +} + +static void +e_markdown_editor_add_header_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + e_markdown_editor_surround_selection (self, TRUE, "# ", NULL); +} + +static void +e_markdown_editor_markdown_syntax_cb (GtkToolButton *button, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + GtkWidget *toplevel; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + + e_show_uri (GTK_IS_WINDOW (toplevel) ? GTK_WINDOW (toplevel) : NULL, "https://commonmark.org/help/"); +} + +#ifdef HAVE_MARKDOWN +static void +e_markdown_editor_switch_page_cb (GtkNotebook *notebook, + GtkWidget *page, + guint page_num, + gpointer user_data) +{ + EMarkdownEditor *self = user_data; + gchar *converted; + gchar *html; + + g_return_if_fail (E_IS_MARKDOWN_EDITOR (self)); + + gtk_widget_set_visible (GTK_WIDGET (self->priv->action_toolbar), page_num != 1); + + /* Not the Preview page */ + if (page_num != 1) + return; + + converted = e_markdown_editor_dup_html (self); + + html = g_strconcat ("
", + converted ? converted : "", + "
", + NULL); + + e_web_view_load_string (self->priv->web_view, html); + + g_free (converted); + g_free (html); +} +#endif /* HAVE_MARKDOWN */ + +static gboolean +e_markdown_editor_is_dark_theme (EMarkdownEditor *self) +{ + GdkRGBA rgba; + gdouble brightness; + + e_utils_get_theme_color (GTK_WIDGET (self), "theme_text_color,theme_fg_color", E_UTILS_DEFAULT_THEME_TEXT_COLOR, &rgba); + + brightness = + (0.2109 * 255.0 * rgba.red) + + (0.5870 * 255.0 * rgba.green) + + (0.1021 * 255.0 * rgba.blue); + + return brightness > 140; +} + +static void +e_markdown_editor_constructed (GObject *object) +{ + struct _items { + const gchar *label; + const gchar *icon_name; + const gchar *icon_name_dark; + GCallback callback; + } items[] = { + #define ITEM(lbl, icn, cbk) { lbl, icn, icn "-dark", G_CALLBACK (cbk) } + ITEM (N_("Add bold text"), "markdown-bold", e_markdown_editor_add_bold_text_cb), + ITEM (N_("Add italic text"), "markdown-italic", e_markdown_editor_add_italic_text_cb), + ITEM (N_("Insert a quote"), "markdown-quote", e_markdown_editor_insert_quote_cb), + ITEM (N_("Insert code"), "markdown-code", e_markdown_editor_insert_code_cb), + ITEM (N_("Add a link"), "markdown-link", e_markdown_editor_add_link_cb), + ITEM (N_("Add a bullet list"), "markdown-bullets", e_markdown_editor_add_bullet_list_cb), + ITEM (N_("Add a numbered list"), "markdown-numbers", e_markdown_editor_add_numbered_list_cb), + ITEM (N_("Add a header"), "markdown-header", e_markdown_editor_add_header_cb), + ITEM (NULL, "", NULL), + ITEM (N_("Open online common mark documentation"), "markdown-help", G_CALLBACK (e_markdown_editor_markdown_syntax_cb)) + #undef ITEM + }; + EMarkdownEditor *self = E_MARKDOWN_EDITOR (object); + GtkWidget *widget; + GtkNotebook *notebook; + GtkScrolledWindow *scrolled_window; + gboolean is_dark_theme; + guint ii; + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (e_markdown_editor_parent_class)->constructed (object); + + widget = gtk_notebook_new (); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + "show-border", FALSE, + "show-tabs", TRUE, + NULL); + gtk_box_pack_start (GTK_BOX (self), widget, TRUE, TRUE, 0); + + notebook = GTK_NOTEBOOK (widget); + + widget = gtk_scrolled_window_new (NULL, NULL); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + "hscrollbar-policy", GTK_POLICY_AUTOMATIC, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, + NULL); + + gtk_notebook_append_page (notebook, widget, gtk_label_new_with_mnemonic (_("_Write"))); + + scrolled_window = GTK_SCROLLED_WINDOW (widget); + + widget = gtk_text_view_new (); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + "margin", 4, + "monospace", TRUE, + "wrap-mode", GTK_WRAP_WORD_CHAR, + NULL); + + gtk_container_add (GTK_CONTAINER (scrolled_window), widget); + + self->priv->text_view = GTK_TEXT_VIEW (widget); + + e_buffer_tagger_connect (self->priv->text_view); + e_spell_text_view_attach (self->priv->text_view); + + #ifdef HAVE_MARKDOWN + widget = gtk_scrolled_window_new (NULL, NULL); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + "hscrollbar-policy", GTK_POLICY_AUTOMATIC, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, + NULL); + + gtk_notebook_append_page (notebook, widget, gtk_label_new_with_mnemonic (_("_Preview"))); + + scrolled_window = GTK_SCROLLED_WINDOW (widget); + + widget = e_web_view_new (); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + "margin", 4, + NULL); + + gtk_container_add (GTK_CONTAINER (scrolled_window), widget); + + self->priv->web_view = E_WEB_VIEW (widget); + #endif /* HAVE_MARKDOWN */ + + widget = gtk_toolbar_new (); + gtk_widget_show (widget); + gtk_notebook_set_action_widget (notebook, widget, GTK_PACK_END); + + self->priv->action_toolbar = GTK_TOOLBAR (widget); + + is_dark_theme = e_markdown_editor_is_dark_theme (self); + + for (ii = 0; ii < G_N_ELEMENTS (items); ii++) { + GtkToolItem *item; + + if (items[ii].callback) { + GtkWidget *icon; + const gchar *icon_name; + + icon_name = is_dark_theme ? items[ii].icon_name_dark : items[ii].icon_name; + icon = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_widget_show (GTK_WIDGET (icon)); + item = gtk_tool_button_new (icon, _(items[ii].label)); + gtk_tool_item_set_tooltip_text (item, _(items[ii].label)); + g_signal_connect_object (item, "clicked", items[ii].callback, self, 0); + } else { + item = gtk_separator_tool_item_new (); + } + + gtk_widget_show (GTK_WIDGET (item)); + gtk_toolbar_insert (self->priv->action_toolbar, item, -1); + } + + #ifdef HAVE_MARKDOWN + g_signal_connect_object (notebook, "switch-page", G_CALLBACK (e_markdown_editor_switch_page_cb), self, 0); + #endif +} + +static void +e_markdown_editor_class_init (EMarkdownEditorClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->constructed = e_markdown_editor_constructed; +} + +static void +e_markdown_editor_init (EMarkdownEditor *self) +{ + self->priv = e_markdown_editor_get_instance_private (self); +} + +/** + * e_markdown_editor_new: + * + * Creates a new #EMarkdownEditor + * + * Returns: (transfer full): a new #EMarkdownEditor + * + * Since: 3.44 + */ +GtkWidget * +e_markdown_editor_new (void) +{ + return g_object_new (E_TYPE_MARKDOWN_EDITOR, NULL); +} + +/** + * e_markdown_editor_dup_text: + * @self: an #EMarkdownEditor + * + * Get the markdown text entered in the @self. To get + * the HTML version of it use e_markdown_editor_dup_html(). + * Free the returned string with g_free(), when no longer needed. + * + * Returns: (transfer full): the markdown text + * + * Since: 3.44 + **/ +gchar * +e_markdown_editor_dup_text (EMarkdownEditor *self) +{ + GtkTextBuffer *buffer; + GtkTextIter start, end; + + g_return_val_if_fail (E_IS_MARKDOWN_EDITOR (self), NULL); + + buffer = gtk_text_view_get_buffer (self->priv->text_view); + gtk_text_buffer_get_bounds (buffer, &start, &end); + + return gtk_text_buffer_get_text (buffer, &start, &end, FALSE); +} + +/** + * e_markdown_editor_dup_html: + * @self: an #EMarkdownEditor + * + * Get the HTML version of the markdown text entered in the @self. + * To get the markdown text use e_markdown_editor_dup_text(). + * Free the returned string with g_free(), when no longer needed. + * + * Note: The function can return %NULL when was not built + * with the markdown support. + * + * Returns: (transfer full) (nullable): the markdown text converted + * into HTML, or %NULL, when was not built with the markdown support + * + * Since: 3.44 + **/ +gchar * +e_markdown_editor_dup_html (EMarkdownEditor *self) +{ + #ifdef HAVE_MARKDOWN + GString *html; + gchar *text, *converted; + #endif + + g_return_val_if_fail (E_IS_MARKDOWN_EDITOR (self), NULL); + + #ifdef HAVE_MARKDOWN + text = e_markdown_editor_dup_text (self); + converted = cmark_markdown_to_html (text ? text : "", text ? strlen (text) : 0, + CMARK_OPT_VALIDATE_UTF8 | CMARK_OPT_UNSAFE); + + html = e_str_replace_string (converted, "
", "
"); + + g_free (converted); + g_free (text); + + return g_string_free (html, FALSE); + #else + return NULL; + #endif +} diff --git a/src/e-util/e-markdown-editor.h b/src/e-util/e-markdown-editor.h new file mode 100644 index 0000000000..bdc429c8b9 --- /dev/null +++ b/src/e-util/e-markdown-editor.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION) +#error "Only should be included directly." +#endif + +#ifndef E_MARKDOWN_EDITOR_H +#define E_MARKDOWN_EDITOR_H + +#include + +/* Standard GObject macros */ +#define E_TYPE_MARKDOWN_EDITOR \ + (e_markdown_editor_get_type ()) +#define E_MARKDOWN_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_MARKDOWN_EDITOR, EMarkdownEditor)) +#define E_MARKDOWN_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_MARKDOWN_EDITOR, EMarkdownEditorClass)) +#define E_IS_MARKDOWN_EDITOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_MARKDOWN_EDITOR)) +#define E_IS_MARKDOWN_EDITOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_MARKDOWN_EDITOR)) +#define E_MARKDOWN_EDITOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_MARKDOWN_EDITOR, EMarkdownEditorClass)) + +G_BEGIN_DECLS + +typedef struct _EMarkdownEditor EMarkdownEditor; +typedef struct _EMarkdownEditorClass EMarkdownEditorClass; +typedef struct _EMarkdownEditorPrivate EMarkdownEditorPrivate; + +struct _EMarkdownEditor { + GtkBox parent; + EMarkdownEditorPrivate *priv; +}; + +struct _EMarkdownEditorClass { + GtkBoxClass parent_class; +}; + +GType e_markdown_editor_get_type (void) G_GNUC_CONST; +GtkWidget * e_markdown_editor_new (void); +gchar * e_markdown_editor_dup_text (EMarkdownEditor *self); +gchar * e_markdown_editor_dup_html (EMarkdownEditor *self); + +G_END_DECLS + +#endif /* E_MARKDOWN_EDITOR_H */ diff --git a/src/e-util/e-util.h b/src/e-util/e-util.h index c430fed8bd..666363e9e8 100644 --- a/src/e-util/e-util.h +++ b/src/e-util/e-util.h @@ -152,6 +152,7 @@ #include #include #include +#include #include #include #include diff --git a/src/e-util/test-markdown-editor.c b/src/e-util/test-markdown-editor.c new file mode 100644 index 0000000000..88d439dd7e --- /dev/null +++ b/src/e-util/test-markdown-editor.c @@ -0,0 +1,81 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +static gboolean +window_delete_event_cb (GtkWidget *widget, + GdkEvent *event, + gpointer user_data) +{ + gtk_main_quit (); + + return FALSE; +} + +static gint +on_idle_create_widget (gpointer user_data) +{ + GtkWidget *window, *editor; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size (GTK_WINDOW (window), 640, 480); + + g_signal_connect ( + window, "delete-event", + G_CALLBACK (window_delete_event_cb), NULL); + + editor = e_markdown_editor_new (); + + g_object_set (G_OBJECT (editor), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "visible", TRUE, + NULL); + + gtk_container_add (GTK_CONTAINER (window), editor); + + gtk_widget_show (window); + + return FALSE; +} + +gint +main (gint argc, + gchar **argv) +{ + GList *modules; + + bindtextdomain (GETTEXT_PACKAGE, EVOLUTION_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + gtk_init (&argc, &argv); + + e_util_init_main_thread (NULL); + e_passwords_init (); + + g_setenv ("EVOLUTION_SOURCE_WEBKITDATADIR", EVOLUTION_SOURCE_WEBKITDATADIR, FALSE); + + gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (), EVOLUTION_ICONDIR); + gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (), E_DATA_SERVER_ICONDIR); + + modules = e_module_load_all_in_directory (EVOLUTION_MODULEDIR); + g_list_free_full (modules, (GDestroyNotify) g_type_module_unuse); + + g_idle_add ((GSourceFunc) on_idle_create_widget, NULL); + + gtk_main (); + + e_misc_util_free_global_memory (); + + return 0; +}