/* * e-html-editor.c * * Copyright (C) 2012 Dan Vrátil * * 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 #include #include #include #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 ( "%s", 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 ( "%s", 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; }