aboutsummaryrefslogblamecommitdiffstats
path: root/e-util/e-html-editor.c
blob: aef38bc6f869dc7881820cfedd39eaec32c63a1c (plain) (tree)



















































































































































































































                                                                                      
                                                               





























































































                                                                       
                                                  





























































































































































































                                                                                         

                                              












































                                                                          
                                                       














                                                                                


                                              














                                                                       


                                            

















                                                                       



                               


                                                                          






























































































































































                                                                                    
                                        
















































































































































































                                                                                    
                                                   


































                                                                              
                                                        

























                                                                
                                                           














































                                                                                
                                                  



























































                                                                          
                                           






















                                                                                             


                                          




































                                                                    
/*
 * e-html-editor.c
 *
 * Copyright (C) 2012 Dan Vrátil <dvratil@redhat.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>
#include <glib/gi18n-lib.h>

#include <camel/camel.h>
#include <enchant/enchant.h>

#include "e-html-editor.h"

#include "e-activity-bar.h"
#include "e-alert-bar.h"
#include "e-alert-dialog.h"
#include "e-alert-sink.h"
#include "e-html-editor-private.h"
#include "e-html-editor-utils.h"
#include "e-html-editor-selection.h"

#define E_HTML_EDITOR_GET_PRIVATE(obj) \
    (G_TYPE_INSTANCE_GET_PRIVATE \
    ((obj), E_TYPE_HTML_EDITOR, EHTMLEditorPrivate))

/**
 * EHTMLEditor:
 *
 * #EHTMLEditor provides GUI for manipulating with properties of #EHTMLEditorView and
 * its #EHTMLEditorSelection - i.e. toolbars and actions.
 */

/* This controls how spelling suggestions are divided between the primary
 * context menu and a secondary menu.  The idea is to prevent the primary
 * menu from growing too long.
 *
 * The constants below are used as follows:
 *
 * if TOTAL_SUGGESTIONS <= MAX_LEVEL1_SUGGETIONS:
 *     LEVEL1_SUGGESTIONS = TOTAL_SUGGESTIONS
 * elif TOTAL_SUGGESTIONS - MAX_LEVEL1_SUGGESTIONS < MIN_LEVEL2_SUGGESTIONS:
 *     LEVEL1_SUGGESTIONS = TOTAL_SUGGESTIONS
 * else
 *     LEVEL1_SUGGESTIONS = MAX_LEVEL1_SUGGETIONS
 *
 * LEVEL2_SUGGESTIONS = TOTAL_SUGGESTIONS - LEVEL1_SUGGESTIONS
 *
 * Note that MAX_LEVEL1_SUGGETIONS is not a hard maximum.
 */
#define MAX_LEVEL1_SUGGESTIONS  4
#define MIN_LEVEL2_SUGGESTIONS  3

enum {
    PROP_0,
    PROP_FILENAME
};

enum {
    UPDATE_ACTIONS,
    SPELL_LANGUAGES_CHANGED,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

/* Forward Declarations */
static void e_html_editor_alert_sink_init
                    (EAlertSinkInterface *interface);

G_DEFINE_TYPE_WITH_CODE (
    EHTMLEditor,
    e_html_editor,
    GTK_TYPE_GRID,
    G_IMPLEMENT_INTERFACE (
        E_TYPE_ALERT_SINK,
        e_html_editor_alert_sink_init))

/* Action callback for context menu spelling suggestions.
 * XXX This should really be in e-html-editor-actions.c */
static void
action_context_spell_suggest_cb (GtkAction *action,
                                 EHTMLEditor *editor)
{
    EHTMLEditorView *view;
    EHTMLEditorSelection *selection;
    const gchar *word;

    word = g_object_get_data (G_OBJECT (action), "word");
    g_return_if_fail (word != NULL);

    view = e_html_editor_get_view (editor);
    selection = e_html_editor_view_get_selection (view);

    e_html_editor_selection_replace_caret_word (selection, word);
}

static void
html_editor_inline_spelling_suggestions (EHTMLEditor *editor)
{
    EHTMLEditorView *view;
    EHTMLEditorSelection *selection;
    WebKitSpellChecker *checker;
    GtkActionGroup *action_group;
    GtkUIManager *manager;
    gchar **suggestions;
    const gchar *path;
    gchar *word;
    guint count = 0;
    guint length;
    guint merge_id;
    guint threshold;
    gint ii;

    view = e_html_editor_get_view (editor);
    selection = e_html_editor_view_get_selection (view);
    checker = WEBKIT_SPELL_CHECKER (webkit_get_text_checker ());

    word = e_html_editor_selection_get_caret_word (selection);
    if (word == NULL || *word == '\0')
        return;

    suggestions = webkit_spell_checker_get_guesses_for_word (checker, word, NULL);

    path = "/context-menu/context-spell-suggest/";
    manager = e_html_editor_get_ui_manager (editor);
    action_group = editor->priv->suggestion_actions;
    merge_id = editor->priv->spell_suggestions_merge_id;

    length = (suggestions != NULL) ? g_strv_length (suggestions) : 0;

    /* Calculate how many suggestions to put directly in the
     * context menu.  The rest will go in a secondary menu. */
    if (length <= MAX_LEVEL1_SUGGESTIONS) {
        threshold = length;
    } else if (length - MAX_LEVEL1_SUGGESTIONS < MIN_LEVEL2_SUGGESTIONS) {
        threshold = length;
    } else {
        threshold = MAX_LEVEL1_SUGGESTIONS;
    }

    ii = 0;
    for (ii = 0; suggestions && suggestions[ii]; ii++) {
        gchar *suggestion = suggestions[ii];
        gchar *action_name;
        gchar *action_label;
        GtkAction *action;
        GtkWidget *child;
        GSList *proxies;

        /* Once we reach the threshold, put all subsequent
         * spelling suggestions in a secondary menu. */
        if (count == threshold)
            path = "/context-menu/context-more-suggestions-menu/";

        /* Action name just needs to be unique. */
        action_name = g_strdup_printf ("suggest-%d", count++);

        action_label = g_markup_printf_escaped (
            "<b>%s</b>", suggestion);

        action = gtk_action_new (
            action_name, action_label, NULL, NULL);

        g_object_set_data_full (
            G_OBJECT (action), "word",
            g_strdup (suggestion), g_free);

        g_signal_connect (
            action, "activate", G_CALLBACK (
            action_context_spell_suggest_cb), editor);

        gtk_action_group_add_action (action_group, action);

        gtk_ui_manager_add_ui (
            manager, merge_id, path,
            action_name, action_name,
            GTK_UI_MANAGER_AUTO, FALSE);

        /* XXX GtkAction offers no support for Pango markup,
         *     so we have to manually set "use-markup" on the
         *     child of the proxy widget. */
        gtk_ui_manager_ensure_update (manager);
        proxies = gtk_action_get_proxies (action);
        child = gtk_bin_get_child (proxies->data);
        g_object_set (child, "use-markup", TRUE, NULL);

        g_free (action_name);
        g_free (action_label);
    }

    g_free (word);
    g_strfreev (suggestions);
}

/* Helper for html_editor_update_actions() */
static void
html_editor_spell_checkers_foreach (EHTMLEditor *editor,
                                    const gchar *language_code)
{
    EHTMLEditorView *view;
    EHTMLEditorSelection *selection;
    ESpellChecker *spell_checker;
    ESpellDictionary *dictionary;
    GtkActionGroup *action_group;
    GtkUIManager *manager;
    GList *list, *link;
    gchar *path;
    gchar *word;
    gint ii = 0;
    guint merge_id;

    view = e_html_editor_get_view (editor);
    selection = e_html_editor_view_get_selection (view);
    spell_checker = e_html_editor_view_get_spell_checker (view);

    word = e_html_editor_selection_get_caret_word (selection);
    if (word == NULL || *word == '\0')
        return;

    dictionary = e_spell_checker_ref_dictionary (
        spell_checker, language_code);
    if (dictionary != NULL) {
        list = e_spell_dictionary_get_suggestions (
            dictionary, word, -1);
        g_object_unref (dictionary);
    } else {
        list = NULL;
    }

    manager = e_html_editor_get_ui_manager (editor);
    action_group = editor->priv->suggestion_actions;
    merge_id = editor->priv->spell_suggestions_merge_id;

    path = g_strdup_printf (
        "/context-menu/context-spell-suggest/"
        "context-spell-suggest-%s-menu", language_code);

    for (link = list; link != NULL; link = g_list_next (link)) {
        gchar *suggestion = link->data;
        gchar *action_name;
        gchar *action_label;
        GtkAction *action;
        GtkWidget *child;
        GSList *proxies;

        /* Action name just needs to be unique. */
        action_name = g_strdup_printf (
            "suggest-%s-%d", language_code, ii);

        action_label = g_markup_printf_escaped (
            "<b>%s</b>", suggestion);

        action = gtk_action_new (
            action_name, action_label, NULL, NULL);

        g_object_set_data_full (
            G_OBJECT (action), "word",
            g_strdup (suggestion), g_free);

        g_signal_connect (
            action, "activate", G_CALLBACK (
            action_context_spell_suggest_cb), editor);

        gtk_action_group_add_action (action_group, action);

        gtk_ui_manager_add_ui (
            manager, merge_id, path,
            action_name, action_name,
            GTK_UI_MANAGER_AUTO, FALSE);

        /* XXX GtkAction offers no supports for Pango markup,
         *     so we have to manually set "use-markup" on the
         *     child of the proxy widget. */
        gtk_ui_manager_ensure_update (manager);
        proxies = gtk_action_get_proxies (action);
        if (proxies && proxies->data) {
            child = gtk_bin_get_child (proxies->data);
            g_object_set (child, "use-markup", TRUE, NULL);
        }

        g_free (action_name);
        g_free (action_label);
    }

    g_list_free_full (list, (GDestroyNotify) g_free);

    g_free (path);
    g_free (word);
}

static void
html_editor_update_actions (EHTMLEditor *editor,
                            GdkEventButton *event)
{
    WebKitWebView *web_view;
    WebKitSpellChecker *checker;
    WebKitHitTestResult *hit_test;
    WebKitHitTestResultContext context;
    WebKitDOMNode *node;
    EHTMLEditorSelection *selection;
    EHTMLEditorView *view;
    ESpellChecker *spell_checker;
    GtkUIManager *manager;
    GtkActionGroup *action_group;
    GList *list;
    gchar **languages;
    guint ii, n_languages;
    gboolean visible;
    guint merge_id;
    gint loc, len;

    view = e_html_editor_get_view (editor);
    spell_checker = e_html_editor_view_get_spell_checker (view);

    web_view = WEBKIT_WEB_VIEW (view);
    manager = e_html_editor_get_ui_manager (editor);

    editor->priv->image = NULL;
    editor->priv->table_cell = NULL;

    /* Update context menu item visibility. */
    hit_test = webkit_web_view_get_hit_test_result (web_view, event);
    g_object_get (
        G_OBJECT (hit_test),
        "context", &context,
        "inner-node", &node, NULL);
    g_object_unref (hit_test);

    visible = (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE);
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_IMAGE), visible);
    if (visible)
        editor->priv->image = node;

    visible = (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK);
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_LINK), visible);

    visible = (WEBKIT_DOM_IS_HTMLHR_ELEMENT (node));
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_RULE), visible);

    visible = (webkit_dom_node_get_node_type (node) == 3);
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_TEXT), visible);

    visible =
        gtk_action_get_visible (ACTION (CONTEXT_PROPERTIES_IMAGE)) ||
        gtk_action_get_visible (ACTION (CONTEXT_PROPERTIES_LINK)) ||
        gtk_action_get_visible (ACTION (CONTEXT_PROPERTIES_TEXT));
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_PARAGRAPH), visible);

    /* Set to visible if any of these are true:
     *   - Selection is active and contains a link.
     *   - Cursor is on a link.
     *   - Cursor is on an image that has a URL or target.
     */
    visible = (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (node) ||
        (e_html_editor_dom_node_find_parent_element (node, "A") != NULL));
    gtk_action_set_visible (ACTION (CONTEXT_REMOVE_LINK), visible);

    visible = (WEBKIT_DOM_IS_HTML_TABLE_CELL_ELEMENT (node) ||
        (e_html_editor_dom_node_find_parent_element (node, "TD") != NULL) ||
        (e_html_editor_dom_node_find_parent_element (node, "TH") != NULL));
    gtk_action_set_visible (ACTION (CONTEXT_DELETE_CELL), visible);
    gtk_action_set_visible (ACTION (CONTEXT_DELETE_COLUMN), visible);
    gtk_action_set_visible (ACTION (CONTEXT_DELETE_ROW), visible);
    gtk_action_set_visible (ACTION (CONTEXT_DELETE_TABLE), visible);
    gtk_action_set_visible (ACTION (CONTEXT_INSERT_COLUMN_AFTER), visible);
    gtk_action_set_visible (ACTION (CONTEXT_INSERT_COLUMN_BEFORE), visible);
    gtk_action_set_visible (ACTION (CONTEXT_INSERT_ROW_ABOVE), visible);
    gtk_action_set_visible (ACTION (CONTEXT_INSERT_ROW_BELOW), visible);
    gtk_action_set_visible (ACTION (CONTEXT_INSERT_TABLE), visible);
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_CELL), visible);
    if (visible)
        editor->priv->table_cell = node;

    /* Note the |= (cursor must be in a table cell). */
    visible |= (WEBKIT_DOM_IS_HTML_TABLE_ELEMENT (node) ||
        (e_html_editor_dom_node_find_parent_element (node, "TABLE") != NULL));
    gtk_action_set_visible (ACTION (CONTEXT_PROPERTIES_TABLE), visible);

    /********************** Spell Check Suggestions **********************/

    action_group = editor->priv->suggestion_actions;

    /* Remove the old content from the context menu. */
    merge_id = editor->priv->spell_suggestions_merge_id;
    if (merge_id > 0) {
        gtk_ui_manager_remove_ui (manager, merge_id);
        editor->priv->spell_suggestions_merge_id = 0;
    }

    /* Clear the action group for spelling suggestions. */
    list = gtk_action_group_list_actions (action_group);
    while (list != NULL) {
        GtkAction *action = list->data;

        gtk_action_group_remove_action (action_group, action);
        list = g_list_delete_link (list, list);
    }

    languages = e_spell_checker_list_active_languages (
        spell_checker, &n_languages);

    /* Decide if we should show spell checking items. */
    checker = WEBKIT_SPELL_CHECKER (webkit_get_text_checker ());
    selection = e_html_editor_view_get_selection (view);
    visible = FALSE;
    if ((n_languages > 0) && e_html_editor_selection_has_text (selection)) {
        gchar *word = e_html_editor_selection_get_caret_word (selection);
        if (word && *word) {
            webkit_spell_checker_check_spelling_of_string (
                checker, word, &loc, &len);
            visible = (loc > -1);
        } else {
            visible = FALSE;
        }
        g_free (word);
    }

    action_group = editor->priv->spell_check_actions;
    gtk_action_group_set_visible (action_group, visible);

    /* Exit early if spell checking items are invisible. */
    if (!visible) {
        g_strfreev (languages);
        return;
    }

    merge_id = gtk_ui_manager_new_merge_id (manager);
    editor->priv->spell_suggestions_merge_id = merge_id;

    /* Handle a single active language as a special case. */
    if (n_languages == 1) {
        html_editor_inline_spelling_suggestions (editor);
        g_strfreev (languages);
        return;
    }

    /* Add actions and context menu content for active languages. */
    for (ii = 0; ii < n_languages; ii++)
        html_editor_spell_checkers_foreach (editor, languages[ii]);

    g_strfreev (languages);
}

static void
html_editor_spell_languages_changed (EHTMLEditor *editor)
{
    EHTMLEditorView *view;
    ESpellChecker *spell_checker;
    WebKitWebSettings *settings;
    gchar *comma_separated;
    gchar **languages;

    view = e_html_editor_get_view (editor);
    spell_checker = e_html_editor_view_get_spell_checker (view);

    languages = e_spell_checker_list_active_languages (spell_checker, NULL);
    comma_separated = g_strjoinv (",", languages);
    g_strfreev (languages);

    /* Set the languages for webview to highlight misspelled words */
    settings = webkit_web_view_get_settings (
        WEBKIT_WEB_VIEW (editor->priv->html_editor_view));

    g_object_set (
        G_OBJECT (settings),
        "spell-checking-languages", comma_separated,
        NULL);

    if (editor->priv->spell_check_dialog != NULL)
        e_html_editor_spell_check_dialog_update_dictionaries (
            E_HTML_EDITOR_SPELL_CHECK_DIALOG (
            editor->priv->spell_check_dialog));

    if (*comma_separated)
        e_html_editor_view_force_spell_check (editor->priv->html_editor_view);
    else
        e_html_editor_view_turn_spell_check_off (editor->priv->html_editor_view);

    g_free (comma_separated);
}

static gboolean
html_editor_show_popup (EHTMLEditor *editor,
                        GdkEventButton *event,
                        gpointer user_data)
{
    GtkWidget *menu;

    menu = e_html_editor_get_managed_widget (editor, "/context-menu");

    g_signal_emit (editor, signals[UPDATE_ACTIONS], 0, event);

    if (event != NULL)
        gtk_menu_popup (
            GTK_MENU (menu), NULL, NULL, NULL,
            user_data, event->button, event->time);
    else
        gtk_menu_popup (
            GTK_MENU (menu), NULL, NULL, NULL,
            user_data, 0, gtk_get_current_event_time ());

    return TRUE;
}

static gchar *
html_editor_find_ui_file (const gchar *basename)
{
    gchar *filename;

    g_return_val_if_fail (basename != NULL, NULL);

    /* Support running directly from the source tree. */
    filename = g_build_filename (".", basename, NULL);
    if (g_file_test (filename, G_FILE_TEST_EXISTS))
        return filename;
    g_free (filename);

    /* XXX This is kinda broken. */
    filename = g_build_filename (EVOLUTION_UIDIR, basename, NULL);
    if (g_file_test (filename, G_FILE_TEST_EXISTS))
        return filename;
    g_free (filename);

    g_critical ("Could not locate '%s'", basename);

    return NULL;
}

static void
html_editor_parent_changed (GtkWidget *widget,
                            GtkWidget *previous_parent)
{
    GtkWidget *top_level;
    EHTMLEditor *editor = E_HTML_EDITOR (widget);

    /* If he now have a window, then install our accelators to it */
    top_level = gtk_widget_get_toplevel (widget);
    if (GTK_IS_WINDOW (top_level)) {
        gtk_window_add_accel_group (
            GTK_WINDOW (top_level),
            gtk_ui_manager_get_accel_group (editor->priv->manager));
    }
}

static void
html_editor_set_property (GObject *object,
                          guint property_id,
                          const GValue *value,
                          GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_FILENAME:
            e_html_editor_set_filename (
                E_HTML_EDITOR (object),
                g_value_get_string (value));
            return;

    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
html_editor_get_property (GObject *object,
                          guint property_id,
                          GValue *value,
                          GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_FILENAME:
            g_value_set_string (
                value, e_html_editor_get_filename (
                E_HTML_EDITOR (object)));
            return;
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
html_editor_constructed (GObject *object)
{
    EHTMLEditor *editor = E_HTML_EDITOR (object);
    EHTMLEditorPrivate *priv = editor->priv;
    GtkIMMulticontext *im_context;
    GtkWidget *widget;
    GtkToolbar *toolbar;
    GtkToolItem *tool_item;

    /* Chain up to parent's method. */
    G_OBJECT_CLASS (e_html_editor_parent_class)->constructed (object);

    /* Construct the editing toolbars. */

    widget = e_html_editor_get_managed_widget (editor, "/edit-toolbar");
    gtk_widget_set_hexpand (widget, TRUE);
    gtk_toolbar_set_style (GTK_TOOLBAR (widget), GTK_TOOLBAR_BOTH_HORIZ);
    gtk_grid_attach (GTK_GRID (editor), widget, 0, 0, 1, 1);
    priv->edit_toolbar = g_object_ref (widget);
    gtk_widget_show (widget);

    widget = e_html_editor_get_managed_widget (editor, "/html-toolbar");
    gtk_widget_set_hexpand (widget, TRUE);
    gtk_toolbar_set_style (GTK_TOOLBAR (widget), GTK_TOOLBAR_BOTH_HORIZ);
    gtk_grid_attach (GTK_GRID (editor), widget, 0, 1, 1, 1);
    priv->html_toolbar = g_object_ref (widget);
    gtk_widget_show (widget);

    /* Construct the activity bar. */

    widget = e_activity_bar_new ();
    gtk_widget_set_hexpand (widget, TRUE);
    gtk_grid_attach (GTK_GRID (editor), widget, 0, 2, 1, 1);
    priv->activity_bar = g_object_ref (widget);

    /* Construct the alert bar for errors. */

    widget = e_alert_bar_new ();
    gtk_widget_set_hexpand (widget, TRUE);
    gtk_grid_attach (GTK_GRID (editor), widget, 0, 3, 1, 1);
    priv->alert_bar = g_object_ref (widget);
    /* EAlertBar controls its own visibility. */

    /* Construct the main editing area. */

    widget = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (
        GTK_SCROLLED_WINDOW (widget),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_shadow_type (
        GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN);
    gtk_widget_set_hexpand (widget, TRUE);
    gtk_widget_set_vexpand (widget, TRUE);
    gtk_grid_attach (GTK_GRID (editor), widget, 0, 4, 1, 1);
    priv->scrolled_window = g_object_ref (widget);
    gtk_widget_show (widget);

    widget = GTK_WIDGET (e_html_editor_get_view (editor));
    gtk_container_add (GTK_CONTAINER (priv->scrolled_window), widget);
    gtk_widget_show (widget);
    g_signal_connect_swapped (
        widget, "popup-event",
        G_CALLBACK (html_editor_show_popup), editor);

    /* Add some combo boxes to the "edit" toolbar. */

    toolbar = GTK_TOOLBAR (priv->edit_toolbar);

    tool_item = gtk_tool_item_new ();
    widget = e_action_combo_box_new_with_action (
        GTK_RADIO_ACTION (ACTION (STYLE_NORMAL)));
    gtk_combo_box_set_focus_on_click (GTK_COMBO_BOX (widget), FALSE);
    gtk_container_add (GTK_CONTAINER (tool_item), widget);
    gtk_widget_set_tooltip_text (widget, _("Paragraph Style"));
    gtk_toolbar_insert (toolbar, tool_item, 0);
    priv->style_combo_box = g_object_ref (widget);
    gtk_widget_show_all (GTK_WIDGET (tool_item));

    tool_item = gtk_separator_tool_item_new ();
    gtk_toolbar_insert (toolbar, tool_item, 0);
    gtk_widget_show_all (GTK_WIDGET (tool_item));

    tool_item = gtk_tool_item_new ();
    widget = e_action_combo_box_new_with_action (
        GTK_RADIO_ACTION (ACTION (MODE_HTML)));
    gtk_combo_box_set_focus_on_click (GTK_COMBO_BOX (widget), FALSE);
    gtk_container_add (GTK_CONTAINER (tool_item), widget);
    gtk_widget_set_tooltip_text (widget, _("Editing Mode"));
    gtk_toolbar_insert (toolbar, tool_item, 0);
    priv->mode_combo_box = g_object_ref (widget);
    gtk_widget_show_all (GTK_WIDGET (tool_item));

    /* Add some combo boxes to the "html" toolbar. */

    toolbar = GTK_TOOLBAR (priv->html_toolbar);

    tool_item = gtk_tool_item_new ();
    widget = e_color_combo_new ();
    gtk_container_add (GTK_CONTAINER (tool_item), widget);
    gtk_widget_set_tooltip_text (widget, _("Font Color"));
    gtk_toolbar_insert (toolbar, tool_item, 0);
    priv->color_combo_box = g_object_ref (widget);
    gtk_widget_show_all (GTK_WIDGET (tool_item));
    g_object_bind_property (
        priv->color_combo_box, "current-color",
        priv->selection, "font-color",
        G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
    g_object_bind_property (
        priv->html_editor_view, "editable",
        priv->color_combo_box, "sensitive",
        G_BINDING_SYNC_CREATE);

    tool_item = gtk_tool_item_new ();
    widget = e_action_combo_box_new_with_action (
        GTK_RADIO_ACTION (ACTION (SIZE_PLUS_ZERO)));
    gtk_combo_box_set_focus_on_click (GTK_COMBO_BOX (widget), FALSE);
    gtk_container_add (GTK_CONTAINER (tool_item), widget);
    gtk_widget_set_tooltip_text (widget, _("Font Size"));
    gtk_toolbar_insert (toolbar, tool_item, 0);
    priv->size_combo_box = g_object_ref (widget);
    gtk_widget_show_all (GTK_WIDGET (tool_item));

    /* Add input methods to the context menu. */
    widget = e_html_editor_get_managed_widget (
        editor, "/context-menu/context-input-methods-menu");
    widget = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget));
    g_object_get (
        G_OBJECT (priv->html_editor_view), "im-context", &im_context, NULL);
    gtk_im_multicontext_append_menuitems (
        GTK_IM_MULTICONTEXT (im_context),
        GTK_MENU_SHELL (widget));
}

static void
html_editor_dispose (GObject *object)
{
    EHTMLEditorPrivate *priv;

    priv = E_HTML_EDITOR_GET_PRIVATE (object);

    g_clear_object (&priv->manager);
    g_clear_object (&priv->core_actions);
    g_clear_object (&priv->html_actions);
    g_clear_object (&priv->context_actions);
    g_clear_object (&priv->html_context_actions);
    g_clear_object (&priv->language_actions);
    g_clear_object (&priv->spell_check_actions);
    g_clear_object (&priv->suggestion_actions);

    g_clear_object (&priv->main_menu);
    g_clear_object (&priv->main_toolbar);
    g_clear_object (&priv->edit_toolbar);
    g_clear_object (&priv->html_toolbar);
    g_clear_object (&priv->activity_bar);
    g_clear_object (&priv->alert_bar);
    g_clear_object (&priv->edit_area);

    g_clear_object (&priv->color_combo_box);
    g_clear_object (&priv->mode_combo_box);
    g_clear_object (&priv->size_combo_box);
    g_clear_object (&priv->style_combo_box);
    g_clear_object (&priv->scrolled_window);

    g_clear_object (&priv->html_editor_view);

    /* Chain up to parent's dispose() method. */
    G_OBJECT_CLASS (e_html_editor_parent_class)->dispose (object);
}

static void
html_editor_submit_alert (EAlertSink *alert_sink,
                          EAlert *alert)
{
    EHTMLEditorPrivate *priv;
    EAlertBar *alert_bar;
    GtkWidget *toplevel;
    GtkWidget *widget;
    GtkWindow *parent;

    priv = E_HTML_EDITOR_GET_PRIVATE (alert_sink);

    switch (e_alert_get_message_type (alert)) {
        case GTK_MESSAGE_INFO:
        case GTK_MESSAGE_WARNING:
        case GTK_MESSAGE_ERROR:
            alert_bar = E_ALERT_BAR (priv->alert_bar);
            e_alert_bar_add_alert (alert_bar, alert);
            break;

        default:
            widget = GTK_WIDGET (alert_sink);
            toplevel = gtk_widget_get_toplevel (widget);
            if (GTK_IS_WINDOW (toplevel))
                parent = GTK_WINDOW (toplevel);
            else
                parent = NULL;
            widget = e_alert_dialog_new (parent, alert);
            gtk_dialog_run (GTK_DIALOG (widget));
            gtk_widget_destroy (widget);
    }
}

static void
e_html_editor_class_init (EHTMLEditorClass *class)
{
    GObjectClass *object_class;
    GtkWidgetClass *widget_class;

    g_type_class_add_private (class, sizeof (EHTMLEditorPrivate));

    object_class = G_OBJECT_CLASS (class);
    object_class->set_property = html_editor_set_property;
    object_class->get_property = html_editor_get_property;
    object_class->constructed = html_editor_constructed;
    object_class->dispose = html_editor_dispose;

    widget_class = GTK_WIDGET_CLASS (class);
    widget_class->parent_set = html_editor_parent_changed;

    class->update_actions = html_editor_update_actions;
    class->spell_languages_changed = html_editor_spell_languages_changed;

    g_object_class_install_property (
        object_class,
        PROP_FILENAME,
        g_param_spec_string (
            "filename",
            NULL,
            NULL,
            NULL,
            G_PARAM_READWRITE |
            G_PARAM_STATIC_STRINGS));

    signals[UPDATE_ACTIONS] = g_signal_new (
        "update-actions",
        G_TYPE_FROM_CLASS (class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (EHTMLEditorClass, update_actions),
        NULL, NULL,
        g_cclosure_marshal_VOID__BOXED,
        G_TYPE_NONE, 1,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    signals[SPELL_LANGUAGES_CHANGED] = g_signal_new (
        "spell-languages-changed",
        G_OBJECT_CLASS_TYPE (class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (EHTMLEditorClass, spell_languages_changed),
        NULL, NULL,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);
}

static void
e_html_editor_alert_sink_init (EAlertSinkInterface *interface)
{
    interface->submit_alert = html_editor_submit_alert;
}

static void
e_html_editor_init (EHTMLEditor *editor)
{
    EHTMLEditorPrivate *priv;
    GtkWidget *widget;
    gchar *filename;
    GError *error = NULL;

    editor->priv = E_HTML_EDITOR_GET_PRIVATE (editor);

    priv = editor->priv;

    priv->manager = gtk_ui_manager_new ();
    priv->core_actions = gtk_action_group_new ("core");
    priv->html_actions = gtk_action_group_new ("html");
    priv->context_actions = gtk_action_group_new ("core-context");
    priv->html_context_actions = gtk_action_group_new ("html-context");
    priv->language_actions = gtk_action_group_new ("language");
    priv->spell_check_actions = gtk_action_group_new ("spell-check");
    priv->suggestion_actions = gtk_action_group_new ("suggestion");
    priv->html_editor_view = g_object_ref_sink (e_html_editor_view_new ());
    priv->selection = e_html_editor_view_get_selection (priv->html_editor_view);

    filename = html_editor_find_ui_file ("e-html-editor-manager.ui");
    if (!gtk_ui_manager_add_ui_from_file (priv->manager, filename, &error)) {
        g_critical ("Couldn't load builder file: %s\n", error->message);
        g_clear_error (&error);
    }
    g_free (filename);

    editor_actions_init (editor);
    priv->editor_layout_row = 2;

    /* Tweak the main-toolbar style. */
    widget = e_html_editor_get_managed_widget (editor, "/main-toolbar");
    gtk_style_context_add_class (
        gtk_widget_get_style_context (widget),
        GTK_STYLE_CLASS_PRIMARY_TOOLBAR);
}

/**
 * e_html_editor_new:
 *
 * Constructs a new #EHTMLEditor.
 *
 * Returns: A newly created widget. [transfer-full]
 */
GtkWidget *
e_html_editor_new (void)
{
    return g_object_new (E_TYPE_HTML_EDITOR, NULL);
}

/**
 * e_html_editor_get_view:
 * @editor: an #EHTMLEditor
 *
 * Returns instance of #EHTMLEditorView used in the @editor.
 */
EHTMLEditorView *
e_html_editor_get_view (EHTMLEditor *editor)
{
    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    return editor->priv->html_editor_view;
}

/**
 * e_html_editor_get_ui_manager:
 * @editor: an #EHTMLEditor
 *
 * Returns #GtkUIManager that manages all the actions in the @editor.
 */
GtkUIManager *
e_html_editor_get_ui_manager (EHTMLEditor *editor)
{
    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    return editor->priv->manager;
}

/**
 * e_html_editor_get_action:
 * @editor: an #EHTMLEditor
 * @action_name: name of action to lookup and return
 *
 * Returns: A #GtkAction matching @action_name or @NULL if no such action exists.
 */
GtkAction *
e_html_editor_get_action (EHTMLEditor *editor,
                          const gchar *action_name)
{
    GtkUIManager *manager;
    GtkAction *action = NULL;
    GList *list;

    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);
    g_return_val_if_fail (action_name != NULL, NULL);

    manager = e_html_editor_get_ui_manager (editor);
    list = gtk_ui_manager_get_action_groups (manager);

    while (list != NULL && action == NULL) {
        GtkActionGroup *action_group = list->data;

        action = gtk_action_group_get_action (
            action_group, action_name);

        list = g_list_next (list);
    }

    g_return_val_if_fail (action != NULL, NULL);

    return action;
}

/**
 * e_html_editor_get_action_group:
 * @editor: an #EHTMLEditor
 * @group_name: name of action group to lookup and return
 *
 * Returns: A #GtkActionGroup matching @group_name or @NULL if not such action
 * group exists.
 */
GtkActionGroup *
e_html_editor_get_action_group (EHTMLEditor *editor,
                                const gchar *group_name)
{
    GtkUIManager *manager;
    GList *list;

    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);
    g_return_val_if_fail (group_name != NULL, NULL);

    manager = e_html_editor_get_ui_manager (editor);
    list = gtk_ui_manager_get_action_groups (manager);

    while (list != NULL) {
        GtkActionGroup *action_group = list->data;
        const gchar *name;

        name = gtk_action_group_get_name (action_group);
        if (strcmp (name, group_name) == 0)
            return action_group;

        list = g_list_next (list);
    }

    return NULL;
}

GtkWidget *
e_html_editor_get_managed_widget (EHTMLEditor *editor,
                                  const gchar *widget_path)
{
    GtkUIManager *manager;
    GtkWidget *widget;

    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);
    g_return_val_if_fail (widget_path != NULL, NULL);

    manager = e_html_editor_get_ui_manager (editor);
    widget = gtk_ui_manager_get_widget (manager, widget_path);

    g_return_val_if_fail (widget != NULL, NULL);

    return widget;
}

GtkWidget *
e_html_editor_get_style_combo_box (EHTMLEditor *editor)
{
    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    return editor->priv->style_combo_box;
}

/**
 * e_html_editor_get_filename:
 * @editor: an #EHTMLEditor
 *
 * Returns path and name of file to which content of the editor should be saved.
 */
const gchar *
e_html_editor_get_filename (EHTMLEditor *editor)
{
    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    return editor->priv->filename;
}

/**
 * e_html_editor_set_filename:
 * @editor: an #EHTMLEditor
 * @filename: Target file
 *
 * Sets file to which content of the editor should be saved (see
 * e_html_editor_save()).
 */
void
e_html_editor_set_filename (EHTMLEditor *editor,
                            const gchar *filename)
{
    g_return_if_fail (E_IS_HTML_EDITOR (editor));

    if (g_strcmp0 (editor->priv->filename, filename) == 0)
        return;

    g_free (editor->priv->filename);
    editor->priv->filename = g_strdup (filename);

    g_object_notify (G_OBJECT (editor), "filename");
}

EActivityBar *
e_html_editor_get_activity_bar (EHTMLEditor *editor)
{
    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    return E_ACTIVITY_BAR (editor->priv->activity_bar);
}

/**
 * e_html_editor_new_activity:
 * @editor: an #EHTMLEditor
 *
 * Creates and configures a new #EActivity so its progress is shown in
 * the @editor.  The #EActivity comes pre-loaded with a #CamelOperation.
 *
 * Returns: a new #EActivity for use with @editor
 **/
EActivity *
e_html_editor_new_activity (EHTMLEditor *editor)
{
    EActivity *activity;
    EActivityBar *activity_bar;
    GCancellable *cancellable;

    g_return_val_if_fail (E_IS_HTML_EDITOR (editor), NULL);

    activity = e_activity_new ();
    e_activity_set_alert_sink (activity, E_ALERT_SINK (editor));

    cancellable = camel_operation_new ();
    e_activity_set_cancellable (activity, cancellable);
    g_object_unref (cancellable);

    activity_bar = E_ACTIVITY_BAR (editor->priv->activity_bar);
    e_activity_bar_set_activity (activity_bar, activity);

    return activity;
}

/**
 * e_html_editor_pack_above:
 * @editor: an #EHTMLEditor
 * @child: a #GtkWidget
 *
 * Inserts @child right between the toolbars and the editor widget itself.
 */
void
e_html_editor_pack_above (EHTMLEditor *editor,
                          GtkWidget *child)
{
    g_return_if_fail (E_IS_HTML_EDITOR (editor));
    g_return_if_fail (GTK_IS_WIDGET (child));

    gtk_grid_insert_row (GTK_GRID (editor), editor->priv->editor_layout_row);
    gtk_grid_attach (GTK_GRID (editor), child, 0, editor->priv->editor_layout_row, 1, 1);
    editor->priv->editor_layout_row++;
}

/**
 * e_html_editor_save:
 * @editor: an #EHTMLEditor
 * @filename: file into which to save the content
 * @as_html: whether the content should be saved as HTML or plain text
 * @error:[out] a #GError
 *
 * Saves current content of the #EHTMLEditorView into given file. When @as_html
 * is @FALSE, the content is first converted into plain text.
 *
 * Returns: @TRUE when content is succesfully saved, @FALSE otherwise.
 */
gboolean
e_html_editor_save (EHTMLEditor *editor,
                    const gchar *filename,
                    gboolean as_html,
                    GError **error)
{
    GFile *file;
    GFileOutputStream *stream;
    gchar *content;
    gsize written;

    file = g_file_new_for_path (filename);
    stream = g_file_replace (
        file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error);
    if ((error && *error) || !stream)
        return FALSE;

    if (as_html)
        content = e_html_editor_view_get_text_html (
            E_HTML_EDITOR_VIEW (editor));
    else
        content = e_html_editor_view_get_text_plain (
            E_HTML_EDITOR_VIEW (editor));

    if (!content || !*content) {
        g_set_error (
            error, G_IO_ERROR, G_IO_ERROR_FAILED,
            "Failed to obtain content of editor");
        return FALSE;
    }

    g_output_stream_write_all (
        G_OUTPUT_STREAM (stream), content, strlen (content),
        &written, NULL, error);

    g_free (content);
    g_object_unref (stream);
    g_object_unref (file);

    return TRUE;
}