diff options
Diffstat (limited to 'e-util/e-html-editor.c')
-rw-r--r-- | e-util/e-html-editor.c | 1178 |
1 files changed, 1178 insertions, 0 deletions
diff --git a/e-util/e-html-editor.c b/e-util/e-html-editor.c new file mode 100644 index 0000000000..aeae5373b0 --- /dev/null +++ b/e-util/e-html-editor.c @@ -0,0 +1,1178 @@ +/* + * 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; + + /* 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; +} + |